diff --git a/config.yml b/config.yml index 51c4a2a..bac188f 100644 --- a/config.yml +++ b/config.yml @@ -10,11 +10,11 @@ server: name: video workerId: 1 clientMaxBodySize: 209715200 # 200MB 上传文件限制 - requestTimeout: 500 # HTTP请求超时(秒),默认60秒,改为3分钟以支持AI生成脚本 + requestTimeout: 3000 # HTTP请求超时(秒),默认60秒,改为3分钟以支持AI生成脚本 # AI模型调用配置 chat: - timeout: 500 # 对话模型API请求超时时间(秒) + timeout: 3000 # 对话模型API请求超时时间(秒) max_retries: 3 # 请求失败最大重试次数 # 各内容类型的画风/镜间质量/防崩坏约束及镜头时长规则(生成脚本时注入 prompt) diff --git a/prompt.md b/prompt.md index 1864593..22a4aa7 100644 --- a/prompt.md +++ b/prompt.md @@ -75,19 +75,21 @@ - **丰富而非删减**:在保留全部原有细节的基础上,可以增加合理的微观动作、反应表情、环境互动来让每个镜头更丰满。不要删除任何已有内容来"精简"。 - **禁止概括**:如果你发现自己把一个具体的动作写成了概括性的描述,说明你在删减。请回到剧情描述中把具体细节还原出来。 -### 三大字段的严格区分 +### 四大字段的严格区分 -每个镜头的 JSON 中有三个文本字段:`event`(画面描述)、`narration`(旁白配音)、`dialogue`(角色台词)。**它们的用途完全不同,严禁混用:** +每个镜头的 JSON 中有四个文本字段:`event`(画面描述)、`dialogue`(角色台词)、`narration`(旁白配音)、`ambientSound`(环境音效)。**它们的用途完全不同,严禁混用:** | 字段 | 用途 | 是否会配音 | 示例 | |------|------|-----------|------| | `event` | **画面描述**——观众在屏幕上直接看到的一切:场景环境、角色动作、表情变化、物体位置等。这是视频模型生成画面的唯一依据,必须详细准确。 | 否 | "他落在夜市街上,荧光冲锋衣在人群里格外扎眼" | -| `narration` | **旁白配音**——需要配音演员念出来的解说文字,如角色内心独白、故事背景交代、时间跳跃说明等。**必须是不能通过画面直接传达的信息。** | 是 | "虾仁万万没想到,一个外卖竟然改变了他的一生" | | `dialogue` | **角色台词**——角色在画面中亲口说出的对白。多人对话用「角色名:台词」格式。 | 是 | "拿下" | +| `narration` | **旁白配音**——需要配音演员念出来的解说文字,如角色内心独白、故事背景交代、时间跳跃说明等。**必须是不能通过画面直接传达的信息。** | 是 | "虾仁万万没想到,一个外卖竟然改变了他的一生" | +| `ambientSound` | **环境音效**——背景中的声音元素,如风声、雨声、脚步声、警笛声、人群嘈杂、门铃声、音乐等。**无需配音演员,后期音效合成。** | 否(音效) | "夜市人声嘈杂,远处传来救护车的鸣笛声" | **核心判断方法:** - 如果这段文字描述的是观众能直接看到的画面内容(谁做了什么、在哪里、环境什么样)→ 放入 `event`,**严禁放入 `narration`** - 只有在"画面上看不到这段内容,但需要配音解说才能让观众理解"时 → 才放入 `narration` +- 如果描述的是背景中的声音元素(观众听到但画面中无人提及)→ 放入 `ambientSound` **常见错误**(禁止): ``` @@ -99,10 +101,15 @@ narration: "他落在夜市街上" ← 画面描述不应是旁白 event: "他落在夜市街上" ← 同一内容写两遍 +错误3:把环境音混在事件描述中 + event: "夜市人声嘈杂,他落在夜市街上" ← 画面内容归event,环境音单独分离 + ambientSound: "夜市人声嘈杂" ← 背景音效应放入ambientSound + 正确示范: event: "他落在夜市街上,荧光冲锋衣在人群里格外扎眼" ← 画面描述归event - narration: "" ← 纯视觉内容不需要配音旁白 dialogue: "" ← 本镜头无人说话 + narration: "" ← 纯视觉内容不需要配音旁白 + ambientSound: "夜市人声嘈杂" ← 背景音效归ambientSound ``` **短剧需要旁白解说和角色对白来推动剧情,但旁白必须是有配音价值的内容(角色内心想法、背景说明等),不能把画面描述放进旁白凑字数。** diff --git a/short_drama.db b/short_drama.db index 38788e3..214801a 100644 Binary files a/short_drama.db and b/short_drama.db differ diff --git a/shortdrama/dao/account_transaction_dao.go b/shortdrama/dao/account_transaction_dao.go index b87fa3b..b96ff50 100644 --- a/shortdrama/dao/account_transaction_dao.go +++ b/shortdrama/dao/account_transaction_dao.go @@ -2,11 +2,9 @@ package dao import ( "context" - "video-factory/common" "video-factory/shortdrama/consts" "video-factory/shortdrama/model/entity" - "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/util/gconv" @@ -56,13 +54,7 @@ func (d *accountTransactionDao) Insert(ctx context.Context, data *entity.Account } func (d *accountTransactionDao) ListByUser(ctx context.Context, userId int64, typeFilter string, page, pageSize int) ([]*entity.AccountTransaction, int, error) { - cacheKey := "accountTransaction_ListByUser_" + gconv.String(userId) - if typeFilter != "" { - cacheKey += "_" + typeFilter - } - m := g.DB().Model(consts.TableNameAccountTransaction).Ctx(ctx). - Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}). - Where("user_id", userId) + m := g.DB().Model(consts.TableNameAccountTransaction).Ctx(ctx).Where("user_id", userId) if typeFilter != "" { m = m.Where("type", typeFilter) } diff --git a/shortdrama/dao/agent_profile_dao.go b/shortdrama/dao/agent_profile_dao.go index a2dd7da..4ec93d4 100644 --- a/shortdrama/dao/agent_profile_dao.go +++ b/shortdrama/dao/agent_profile_dao.go @@ -34,8 +34,8 @@ type agentListRow struct { func (d *agentProfileDao) ListAgentWithProfile(ctx context.Context, keyword, phone, province, region, expiredAtFrom, expiredAtTo string, page, pageSize int) ([]*agentListRow, int, error) { m := g.DB().Model(consts.TableNameUser+" u"). LeftJoin(consts.TableNameAgentProfile+" ap", "ap.user_id = u.id"). - Where("u.role", "agent"). - Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: "agent_ListAgentWithProfile"}) + Where("u.role", "agent") + // 分页+筛选查询不缓存(参数不同导致缓存键碰撞) if keyword != "" { m = m.Where("u.name LIKE ? OR u.username LIKE ? OR u.phone LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") } diff --git a/shortdrama/dao/customer_profile_dao.go b/shortdrama/dao/customer_profile_dao.go index 582f5ec..0ca1fc4 100644 --- a/shortdrama/dao/customer_profile_dao.go +++ b/shortdrama/dao/customer_profile_dao.go @@ -36,8 +36,8 @@ func (d *customerProfileDao) ListCustomerWithAgent(ctx context.Context, agentId m := g.DB().Model(consts.TableNameUser+" u"). InnerJoin(consts.TableNameCustomerProfile+" cp", "cp.user_id = u.id"). LeftJoin(consts.TableNameUser+" au", "au.id = cp.agent_id"). - Where("u.role", "customer"). - Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: "customer_ListCustomerWithAgent"}) + Where("u.role", "customer") + // 分页+筛选查询不缓存(参数不同导致缓存键碰撞) if agentId > 0 { m = m.Where("cp.agent_id", agentId) diff --git a/shortdrama/dao/episode_dao.go b/shortdrama/dao/episode_dao.go index 6c2beac..c8cfa89 100644 --- a/shortdrama/dao/episode_dao.go +++ b/shortdrama/dao/episode_dao.go @@ -6,7 +6,6 @@ import ( "video-factory/shortdrama/consts" "video-factory/shortdrama/model/entity" - "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/util/gconv" @@ -52,16 +51,14 @@ func init() { g.Log().Debugf(ctx, "添加 description 列失败(可能已存在): %v", err) } } -func clearEpisodeCache(ctx context.Context, id int64, dramaId int64) { +func clearEpisodeCache(ctx context.Context, id int64) { _, _ = gcache.Remove(ctx, consts.TableNameEpisode+"_GetOneByPk_"+gconv.String(id)) - _, _ = gcache.Remove(ctx, "episode_ListPageByDrama_"+gconv.String(dramaId)) - _, _ = gcache.Remove(ctx, "episode_GetMaxIndex_"+gconv.String(dramaId)) } func (d *episodeDao) Insert(ctx context.Context, data *entity.Episode) (int64, error) { id, err := common.InsertAndReturnId(ctx, consts.TableNameEpisode, data) if err == nil { - clearEpisodeCache(ctx, id, data.DramaId) + clearEpisodeCache(ctx, id) } return id, err } @@ -82,8 +79,6 @@ func (d *episodeDao) Delete(ctx context.Context, id int64) error { func (d *episodeDao) DeleteByDrama(ctx context.Context, dramaId int64) error { _, err := g.DB().Model(consts.TableNameEpisode).Ctx(ctx).Unscoped().Where("drama_id", dramaId).Delete() - _, _ = gcache.Remove(ctx, "episode_ListPageByDrama_"+gconv.String(dramaId)) - _, _ = gcache.Remove(ctx, "episode_GetMaxIndex_"+gconv.String(dramaId)) return err } @@ -103,11 +98,7 @@ func (d *episodeDao) ListPageByDrama(ctx context.Context, dramaId int64, page, p m = m.Where("title LIKE ?", "%"+keyword[0]+"%") } if pageSize == -1 { - cacheKey := "episode_ListPageByDrama_" + gconv.String(dramaId) - if len(keyword) > 0 && keyword[0] != "" { - cacheKey += "_" + keyword[0] - } - r, err := m.OrderAsc("idx").Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).All() + r, err := m.OrderAsc("idx").All() if err != nil { return nil, 0, err } @@ -121,11 +112,7 @@ func (d *episodeDao) ListPageByDrama(ctx context.Context, dramaId int64, page, p if pageSize < 1 { pageSize = 10 } - cacheKey := "episode_ListPageByDrama_" + gconv.String(dramaId) - if len(keyword) > 0 && keyword[0] != "" { - cacheKey += "_" + keyword[0] - } - r, total, err := m.OrderAsc("idx").Limit(pageSize).Offset((page - 1) * pageSize).Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).AllAndCount(false) + r, total, err := m.OrderAsc("idx").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false) if err != nil { return } @@ -135,9 +122,7 @@ func (d *episodeDao) ListPageByDrama(ctx context.Context, dramaId int64, page, p } func (d *episodeDao) GetMaxIndex(ctx context.Context, dramaId int64) (int, error) { - r, err := g.DB().Model(consts.TableNameEpisode).Ctx(ctx). - Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: "episode_GetMaxIndex_" + gconv.String(dramaId)}). - Where("drama_id", dramaId).OrderDesc("idx").Value("idx") + r, err := g.DB().Model(consts.TableNameEpisode).Ctx(ctx).Where("drama_id", dramaId).OrderDesc("idx").Value("idx") if err != nil { return 0, err } diff --git a/shortdrama/dao/model_config_dao.go b/shortdrama/dao/model_config_dao.go index 5494e97..ba61775 100644 --- a/shortdrama/dao/model_config_dao.go +++ b/shortdrama/dao/model_config_dao.go @@ -224,7 +224,7 @@ func (d *modelConfigDao) ListPage(ctx context.Context, page, pageSize int, model if keyword != "" { cacheKey += "_" + keyword } - r, err := m.OrderAsc("id").Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).All() + r, err := m.OrderAsc("id").All() if err != nil { return nil, 0, err } @@ -245,7 +245,7 @@ func (d *modelConfigDao) ListPage(ctx context.Context, page, pageSize int, model if keyword != "" { cacheKey += "_" + keyword } - r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).AllAndCount(false) + r, total, err := m.OrderAsc("id").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false) if err != nil { return } @@ -284,6 +284,22 @@ func (d *modelConfigDao) GetActiveModelByType(ctx context.Context, modelType str return } +func (d *modelConfigDao) GetByIds(ctx context.Context, ids []int64) (res []*entity.ModelConfig, err error) { + if len(ids) == 0 { + return make([]*entity.ModelConfig, 0), nil + } + r, err := g.DB().Model(consts.TableNameModelConfig).Ctx(ctx). + WhereIn("id", ids).OrderAsc("id").All() + if err != nil { + return nil, err + } + res = make([]*entity.ModelConfig, 0) + if r != nil { + err = r.Structs(&res) + } + return +} + func (d *modelConfigDao) GetAll(ctx context.Context) (res []*entity.ModelConfig, err error) { r, err := g.DB().Model(consts.TableNameModelConfig).Ctx(ctx). Cache(gdb.CacheOption{Duration: time.Minute, Name: "model_config_GetAll"}). diff --git a/shortdrama/dao/region_pricing_dao.go b/shortdrama/dao/region_pricing_dao.go index a6e1abf..87f9110 100644 --- a/shortdrama/dao/region_pricing_dao.go +++ b/shortdrama/dao/region_pricing_dao.go @@ -87,7 +87,7 @@ func (d *regionPricingDao) ListPage(ctx context.Context, page, pageSize int, key if protected == 0 || protected == 1 { cacheKey += "_protected_" + gconv.String(protected) } - r, err := m.OrderAsc("region, protected").Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).All() + r, err := m.OrderAsc("region, protected").All() if err != nil { return nil, 0, err } @@ -108,7 +108,7 @@ func (d *regionPricingDao) ListPage(ctx context.Context, page, pageSize int, key if protected == 0 || protected == 1 { cacheKey += "_protected_" + gconv.String(protected) } - r, total, err := m.OrderAsc("region, protected").Limit(pageSize).Offset((page - 1) * pageSize).Cache(gdb.CacheOption{Duration: common.CacheTTL(), Name: cacheKey}).AllAndCount(false) + r, total, err := m.OrderAsc("region, protected").Limit(pageSize).Offset((page - 1) * pageSize).AllAndCount(false) if err != nil { return } diff --git a/shortdrama/dao/user_model_config_dao.go b/shortdrama/dao/user_model_config_dao.go index ae29d97..fe4bc71 100644 --- a/shortdrama/dao/user_model_config_dao.go +++ b/shortdrama/dao/user_model_config_dao.go @@ -223,7 +223,7 @@ func (d *userModelConfigDao) Save(ctx context.Context, data *entity.UserModelCon // SaveBatch 批量保存用户模型配置(事务内执行) func (d *userModelConfigDao) SaveBatch(ctx context.Context, items []*entity.UserModelConfig) error { - return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, data := range items { m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}}) delete(m, "id") @@ -242,6 +242,12 @@ func (d *userModelConfigDao) SaveBatch(ctx context.Context, items []*entity.User } return nil }) + if err == nil { + for _, item := range items { + clearUserModelConfigCache(ctx, item.UserId, item.ModelType, item.ModelConfigId) + } + } + return err } // clearUserModelConfigCache 清除用户模型配置的缓存 diff --git a/shortdrama/model/domain/shot.go b/shortdrama/model/domain/shot.go index ebde1b3..6c7c051 100644 --- a/shortdrama/model/domain/shot.go +++ b/shortdrama/model/domain/shot.go @@ -9,16 +9,17 @@ import ( // Shot 单集剧本中的单个镜头 type Shot struct { Index int `json:"index"` - StartTime string `json:"startTime"` // "MM:SS" - EndTime string `json:"endTime"` // "MM:SS" - Event string `json:"event"` // 事件描述/动作描写 - Narration string `json:"narration,omitempty"` // 旁白(画外音解说,不是角色说的话) - Dialogue string `json:"dialogue,omitempty"` // 主台词(角色亲口说的对白) - CameraMovement string `json:"cameraMovement"` // 运镜描述 - ShotSize string `json:"shotSize"` // 景别(远景/全景/中景/近景/特写) - Characters []string `json:"characters"` // 出演人物(演员名列表) - Scene string `json:"scene"` // 场景(场景名) - Props []string `json:"props"` // 道具(道具名列表) + StartTime string `json:"startTime"` // "MM:SS" + EndTime string `json:"endTime"` // "MM:SS" + Event string `json:"event"` // 事件描述/动作描写 + Narration string `json:"narration,omitempty"` // 旁白/画外音(需要配音的解说文字) + Dialogue string `json:"dialogue,omitempty"` // 主台词(角色亲口说的对白) + AmbientSound string `json:"ambientSound,omitempty"` // 环境音(背景音效,如风声、脚步声、警笛声等) + CameraMovement string `json:"cameraMovement"` // 运镜描述 + ShotSize string `json:"shotSize"` // 景别(远景/全景/中景/近景/特写) + Characters []string `json:"characters"` // 出演人物(演员名列表) + Scene string `json:"scene"` // 场景(场景名) + Props []string `json:"props"` // 道具(道具名列表) } // ShotsToText 将镜头数组转回纯文本格式,供视频生成流程(ReAct Agent)使用 @@ -29,28 +30,24 @@ func ShotsToText(shots []Shot) string { } for _, s := range shots { fmt.Fprintf(&b, "【镜头%d】(%s-%s)\n", s.Index, s.StartTime, s.EndTime) + if s.Event != "" { + fmt.Fprintf(&b, "事件:%s\n", s.Event) + } if s.Dialogue != "" { fmt.Fprintf(&b, "台词:%s\n", s.Dialogue) } - // event 是视频模型的画面依据,把 narration 内容合并到 event 中, - // 防止 AI 误把画面描述写进 narration 导致视频模型缺少画面依据 - eventText := s.Event if s.Narration != "" { - if eventText != "" { - eventText = s.Narration + "。" + eventText - } else { - eventText = s.Narration - } + fmt.Fprintf(&b, "画外音:%s\n", s.Narration) } - if eventText != "" { - fmt.Fprintf(&b, "事件:%s\n", eventText) - } - if s.CameraMovement != "" { - fmt.Fprintf(&b, "运镜:%s\n", s.CameraMovement) + if s.AmbientSound != "" { + fmt.Fprintf(&b, "环境音:%s\n", s.AmbientSound) } if s.ShotSize != "" { fmt.Fprintf(&b, "景别:%s\n", s.ShotSize) } + if s.CameraMovement != "" { + fmt.Fprintf(&b, "运镜:%s\n", s.CameraMovement) + } b.WriteString("\n") } return strings.TrimRight(b.String(), "\n") diff --git a/shortdrama/model/dto/account_transaction_dto.go b/shortdrama/model/dto/account_transaction_dto.go index 32a3d54..c549137 100644 --- a/shortdrama/model/dto/account_transaction_dto.go +++ b/shortdrama/model/dto/account_transaction_dto.go @@ -17,7 +17,7 @@ type ListTransactionReq struct { UserId int64 `v:"required" json:"user_id"` Type string `json:"type" v:"in:recharge,deduct" dc:"筛选类型:recharge/deduct"` Page int `json:"page" d:"1" v:"min:1" dc:"页码"` - PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"` + PageSize int `json:"page_size" d:"20" v:"min:-1" dc:"每页条数"` } type ListTransactionRes struct { diff --git a/shortdrama/model/dto/agent_profile_dto.go b/shortdrama/model/dto/agent_profile_dto.go index f9c708e..b2334ee 100644 --- a/shortdrama/model/dto/agent_profile_dto.go +++ b/shortdrama/model/dto/agent_profile_dto.go @@ -20,7 +20,7 @@ type CreateAgentReq struct { type ListAgentReq struct { g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"代理商列表"` Page int `json:"page" d:"1" v:"min:1" dc:"页码"` - PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"` + PageSize int `json:"page_size" d:"20" v:"min:-1" dc:"每页条数"` Keyword string `json:"keyword" dc:"搜索关键词"` Phone string `json:"phone" dc:"手机号"` Province string `json:"province" dc:"省份"` diff --git a/shortdrama/model/dto/customer_profile_dto.go b/shortdrama/model/dto/customer_profile_dto.go index a21662e..e2b6c25 100644 --- a/shortdrama/model/dto/customer_profile_dto.go +++ b/shortdrama/model/dto/customer_profile_dto.go @@ -18,7 +18,7 @@ type ListCustomerReq struct { g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"客户列表"` AgentId int64 `json:"agent_id" dc:"代理商ID"` Page int `json:"page" d:"1" v:"min:1" dc:"页码"` - PageSize int `json:"page_size" d:"20" v:"min:1|max:100" dc:"每页条数"` + PageSize int `json:"page_size" d:"20" v:"min:-1" dc:"每页条数"` Keyword string `json:"keyword" dc:"搜索关键词"` Phone string `json:"phone" dc:"手机号"` Province string `json:"province" dc:"省份"` diff --git a/shortdrama/model/dto/drama_dto.go b/shortdrama/model/dto/drama_dto.go index 5e04234..bc3cc0d 100644 --- a/shortdrama/model/dto/drama_dto.go +++ b/shortdrama/model/dto/drama_dto.go @@ -12,7 +12,7 @@ import ( type ListDramaReq struct { g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"短剧列表"` Page int `json:"page" d:"1" v:"min:1" dc:"页码,从1开始"` - PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数"` + PageSize int `json:"pageSize" d:"20" v:"min:-1" dc:"每页条数"` Keyword string `json:"keyword" dc:"搜索关键词"` } @@ -29,7 +29,7 @@ type ListEpisodeReq struct { g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"剧集列表"` DramaId int64 `v:"required" json:"dramaId" dc:"短剧ID"` Page int `json:"page" d:"1" v:"min:1" dc:"页码,从1开始"` - PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数"` + PageSize int `json:"pageSize" d:"20" v:"min:-1" dc:"每页条数"` Keyword string `json:"keyword" dc:"搜索关键词"` } diff --git a/shortdrama/model/dto/model_config_dto.go b/shortdrama/model/dto/model_config_dto.go index ec3a1f4..cfd098b 100644 --- a/shortdrama/model/dto/model_config_dto.go +++ b/shortdrama/model/dto/model_config_dto.go @@ -11,7 +11,7 @@ import ( type GetModelConfigListReq struct { g.Meta `path:"/get-model-list" method:"get" tags:"模型配置" summary:"获取模型配置列表"` Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"` - PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"` + PageSize int `json:"pageSize" d:"20" v:"min:-1" dc:"每页条数,默认20"` ModelType string `json:"modelType" v:"in:chat,video" dc:"模型类型筛选: chat/video"` Keyword string `json:"keyword" dc:"模型名称关键词"` } diff --git a/shortdrama/model/dto/region_pricing_dto.go b/shortdrama/model/dto/region_pricing_dto.go index 27edcf8..6c8848a 100644 --- a/shortdrama/model/dto/region_pricing_dto.go +++ b/shortdrama/model/dto/region_pricing_dto.go @@ -9,7 +9,7 @@ import ( type ListRegionPricingReq struct { g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"地区定价列表"` Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"` - PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"` + PageSize int `json:"pageSize" d:"20" v:"min:-1" dc:"每页条数,默认20"` Keyword string `json:"keyword" dc:"地区名称关键词"` Protected int `json:"protected" d:"-1" v:"in:-1,0,1" dc:"类型筛选: -1=全部 0=普通 1=受保护"` } diff --git a/shortdrama/model/dto/user_model_config_dto.go b/shortdrama/model/dto/user_model_config_dto.go index 7681dbc..d988d1f 100644 --- a/shortdrama/model/dto/user_model_config_dto.go +++ b/shortdrama/model/dto/user_model_config_dto.go @@ -36,7 +36,7 @@ type SaveUserModelConfigReq struct { type GetUserModelListReq struct { g.Meta `path:"/get-user-model-list" method:"get" tags:"模型配置" summary:"获取系统模型列表及用户配置状态"` Page int `json:"page" d:"1" v:"min:1" dc:"页码,默认1"` - PageSize int `json:"pageSize" d:"20" v:"min:1|max:100" dc:"每页条数,默认20"` + PageSize int `json:"pageSize" d:"20" v:"min:-1" dc:"每页条数,默认20"` ModelType string `json:"modelType" v:"in:chat,video" dc:"模型类型筛选: chat/video"` Keyword string `json:"keyword" dc:"模型名称关键词"` } diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index 128590c..63d56e2 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -350,9 +350,9 @@ func (s *episodeService) buildScriptGenUserInput(ctx context.Context, d *entity. b.WriteString("\n") } - b.WriteString("【输出格式】\nJSON数组,每个元素是一个镜头对象(shot),包含以下字段:\n- index: 镜头序号(从1开始)\n- startTime: 开始时间(格式MM:SS)\n- endTime: 结束时间(格式MM:SS)\n- event: 画面描述——观众在屏幕上直接看到的一切。场景环境、角色动作、表情变化、物体位置等。**这是视频模型生成画面的唯一依据,所有画面内容必须写在这里**,严禁把画面描述放入 narration 字段。不包含旁白和台词。\n- dialogue: 主台词——角色在画面中亲口说出的对白。多人对话用「角色名:台词」格式。旁白和内心独白不属于这里。如果本镜头无人说话,留空即可。\n- narration: 旁白配音——需要配音演员念出来的解说文字。必须是**通过画面无法直接传达**的信息(如角色内心独白、故事背景交代、时间跳跃说明等)。**如果一段文字描述的是观众能直接看到的画面内容,它就不属于旁白,必须放进 event 字段。** 本镜头无旁白时留空即可。\n- cameraMovement: 运镜方式,从以下标准类型中选择一种:固定镜头、推、拉、摇、移、跟、升、降、旋转、晃动、航拍\n- shotSize: 景别,从以下标准类型中选择一种:远景、全景、中景、近景、特写\n- characters: 出演人物数组,填写演员名称,如[\"张三\", \"李四\"],从「可用演员」中选择\n- scene: 场景名称,从「可用场景」中选择\n- props: 道具名称数组,如[\"剑\", \"酒杯\"],从「可用道具」中选择\n\n直接输出JSON数组,不要markdown代码块标记,不要其他任何内容。\n\n") + b.WriteString("【输出格式】\nJSON数组,每个元素是一个镜头对象(shot),包含以下字段:\n- index: 镜头序号(从1开始)\n- startTime: 开始时间(格式MM:SS)\n- endTime: 结束时间(格式MM:SS)\n- event: 画面描述——观众在屏幕上直接看到的一切。场景环境、角色动作、表情变化、物体位置等。**这是视频模型生成画面的唯一依据,所有画面内容必须写在这里**,严禁把画面描述放入 narration 字段。不包含旁白和台词。\n- dialogue: 主台词——角色在画面中亲口说出的对白。多人对话用「角色名:台词」格式。旁白和内心独白不属于这里。如果本镜头无人说话,留空即可。\n- narration: 旁白配音——需要配音演员念出来的解说文字。必须是**通过画面无法直接传达**的信息(如角色内心独白、故事背景交代、时间跳跃说明等)。**如果一段文字描述的是观众能直接看到的画面内容,它就不属于旁白,必须放进 event 字段。** 本镜头无旁白时留空即可。\n- ambientSound: 环境音效——背景中的声音元素,如风声、雨声、脚步声、警笛声、门铃声、人群嘈杂等。后期音效合成,不需要配音。本镜头无特殊环境音时留空即可。\n- shotSize: 景别,从以下标准类型中选择一种:远景、全景、中景、近景、特写\n- cameraMovement: 运镜方式,从以下标准类型中选择一种:固定镜头、推、拉、摇、移、跟、升、降、旋转、晃动、航拍\n- characters: 出演人物数组,填写演员名称,如[\"张三\", \"李四\"],从「可用演员」中选择\n- scene: 场景名称,从「可用场景」中选择\n- props: 道具名称数组,如[\"剑\", \"酒杯\"],从「可用道具」中选择\n\n直接输出JSON数组,不要markdown代码块标记,不要其他任何内容。\n\n") - b.WriteString("【创作规范】\n\n1. 剧情完整性【核心规则】—— 剧情描述是唯一创作依据,严格按每一句话、每一个细节逐拍还原。\n - 剧情描述中出现的每一句角色说的话,必须一字不差地写入对应镜头的dialogue字段。严禁把台词\"翻译\"成事件描述。\n - 剧情描述中的每一个具体细节(人物的动作、表情反应、对话、环境互动等)都必须忠实地保留在对应镜头的event、narration或dialogue字段中,不能省略、概括或改写为泛化描述。\n - 所有镜头时长之和应等于 %d 秒(50秒至少20个镜头,20-25个镜头为最佳密度)。\n\n2. 场景一致性 —— 每个镜头的 scene 字段值必须与 event 中画面描述的地点一致。\n - 如果 event 描述角色在\"实验室内\",scene 必须是该实验室的名称,不能写成其他地点。\n - 如果角色从一个场景移动到另一个场景,必须先切换 scene(如从「街道」切到「室内」),再描述新地点中的动作。\n\n3. 景别与动作匹配规则 —— 按镜头内容选择正确的 shotSize:\n - shotSize=特写:面部表情、微表情、眼神变化、细节反应(手部小动作、物品细节)。不能用于动作展示。\n - shotSize=近景:对话、上半身互动、情绪交流。能看清面部表情和手势。\n - shotSize=中景:日常动作、手持物体、多人交互。膝盖以上,既有动作又有空间感。\n - shotSize=全景:连贯动作、奔跑、打斗、空间关系。展示全身动作和环境关系。\n - shotSize=远景:环境交代、空镜过渡、场景建立。没有人或人很小,用于转场。\n\n4. 运镜搭配规则 —— 按场景类型选择 cameraMovement:\n - 对话/情绪交流 → 固定镜头或缓推\n - 动作/打斗 → 跟、摇、移\n - 人物进入/离开画面 → 跟、移\n - 情绪揭示 → 推\n - 环境展示 → 摇、升降、航拍\n - 紧张/混乱 → 晃动\n - 固定镜头是默认选项,无特殊需要不使用复杂运镜。\n\n5. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n - 不说话只做反应的角色可用其他景别。\n") + b.WriteString("【创作规范】\n\n1. 剧情完整性【核心规则】—— 剧情描述是唯一创作依据,严格按每一句话、每一个细节逐拍还原。\n - 剧情描述中出现的每一句角色说的话,必须一字不差地写入对应镜头的dialogue字段。严禁把台词\"翻译\"成事件描述。\n - 剧情描述中的每一个具体细节(人物的动作、表情反应、对话、环境互动等)都必须忠实地保留在对应镜头的event、dialogue或narration字段中,不能省略、概括或改写为泛化描述。\n - 所有镜头时长之和应等于 %d 秒(50秒至少20个镜头,20-25个镜头为最佳密度)。\n\n2. 场景一致性 —— 每个镜头的 scene 字段值必须与 event 中画面描述的地点一致。\n - 如果 event 描述角色在\"实验室内\",scene 必须是该实验室的名称,不能写成其他地点。\n - 如果角色从一个场景移动到另一个场景,必须先切换 scene(如从「街道」切到「室内」),再描述新地点中的动作。\n\n3. 景别与动作匹配规则 —— 按镜头内容选择正确的 shotSize:\n - shotSize=特写:面部表情、微表情、眼神变化、细节反应(手部小动作、物品细节)。不能用于动作展示。\n - shotSize=近景:对话、上半身互动、情绪交流。能看清面部表情和手势。\n - shotSize=中景:日常动作、手持物体、多人交互。膝盖以上,既有动作又有空间感。\n - shotSize=全景:连贯动作、奔跑、打斗、空间关系。展示全身动作和环境关系。\n - shotSize=远景:环境交代、空镜过渡、场景建立。没有人或人很小,用于转场。\n\n4. 运镜搭配规则 —— 按场景类型选择 cameraMovement:\n - 对话/情绪交流 → 固定镜头或缓推\n - 动作/打斗 → 跟、摇、移\n - 人物进入/离开画面 → 跟、移\n - 情绪揭示 → 推\n - 环境展示 → 摇、升降、航拍\n - 紧张/混乱 → 晃动\n - 固定镜头是默认选项,无特殊需要不使用复杂运镜。\n\n5. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n - 不说话只做反应的角色可用其他景别。\n") b.WriteString(agent.GetShotDurationPrompt(ctx, d.Type)) b.WriteString(agent.GetStylePrompt(ctx, d.Type)) b.WriteString("\n\n6. JSON格式要求:输出必须是合法的 JSON 数组,字符串值中若需使用引号请用「」代替 ASCII 双引号,确保 json.Unmarshal 能正确解析。\n7. 风控规避:视频模型对某些画面有安全审查机制,生成镜头描述时必须规避。政治敏感内容(真实政治人物/标志/当代军警标识)零容忍。暴力血腥画面(刀刺入身体、血溅、断肢)改为反应镜头或切镜回避,不展示伤害过程。当代背景中的军警抓捕动作将「警察/武警」改为「保安/黑衣人」,动作从「按倒铐住」改为「围住请离」。武器描写聚焦持武器的人物(「他握紧长刀,目光凌厉」),不写武器接触身体的画面。暴力结果用间接描写:写周围人反应而非伤口血迹。\n") diff --git a/shortdrama/service/user_model_config_service.go b/shortdrama/service/user_model_config_service.go index 49a99ae..84de4e2 100644 --- a/shortdrama/service/user_model_config_service.go +++ b/shortdrama/service/user_model_config_service.go @@ -190,15 +190,23 @@ func (s *userModelConfigService) GetMergedConfig(ctx context.Context, userId int } func (s *userModelConfigService) GetUserModelList(ctx context.Context, userId int64) *dto.GetUserModelListRes { - models := ModelConfigService.getModelList(ctx) - list := make([]*dto.UserModelItem, 0, len(models)) - userCfgs, _ := dao.UserModelConfig.GetByUserId(ctx, userId) - cfgMap := make(map[int64]*entity.UserModelConfig) - for _, uc := range userCfgs { - cfgMap[uc.ModelConfigId] = uc + if len(userCfgs) == 0 { + return &dto.GetUserModelListRes{List: make([]*dto.UserModelItem, 0)} } - + ids := make([]int64, 0, len(userCfgs)) + cfgByModelId := make(map[int64]*entity.UserModelConfig, len(userCfgs)) + for _, uc := range userCfgs { + if uc.ModelConfigId > 0 { + ids = append(ids, uc.ModelConfigId) + cfgByModelId[uc.ModelConfigId] = uc + } + } + models, err := dao.ModelConfig.GetByIds(ctx, ids) + if err != nil || len(models) == 0 { + return &dto.GetUserModelListRes{List: make([]*dto.UserModelItem, 0)} + } + list := make([]*dto.UserModelItem, 0, len(models)) for _, m := range models { sysMax := schemaMaxTokens(m.Schema) item := &dto.UserModelItem{ @@ -208,7 +216,7 @@ func (s *userModelConfigService) GetUserModelList(ctx context.Context, userId in ConcurrencyCount: m.ConcurrencyCount, SystemMaxTokens: sysMax, } - if uc, ok := cfgMap[m.Id]; ok { + if uc, ok := cfgByModelId[m.Id]; ok { item.Configured = true item.UserApiKey = uc.ApiKey item.EndpointUrl = uc.EndpointUrl @@ -223,18 +231,25 @@ func (s *userModelConfigService) GetUserModelList(ctx context.Context, userId in } func (s *userModelConfigService) GetUserModelListPage(ctx context.Context, userId int64, page, pageSize int, modelType, keyword string) *dto.GetUserModelListRes { - models, total, err := dao.ModelConfig.ListPage(ctx, page, pageSize, modelType, keyword) - if err != nil { + // 先查用户配置,获取 model_config_id 列表 + userCfgs, _ := dao.UserModelConfig.GetByUserId(ctx, userId) + if len(userCfgs) == 0 { + return &dto.GetUserModelListRes{List: make([]*dto.UserModelItem, 0)} + } + ids := make([]int64, 0, len(userCfgs)) + cfgByModelId := make(map[int64]*entity.UserModelConfig, len(userCfgs)) + for _, uc := range userCfgs { + if uc.ModelConfigId > 0 { + ids = append(ids, uc.ModelConfigId) + cfgByModelId[uc.ModelConfigId] = uc + } + } + // 根据 model_config_id 查询系统模型 + models, err := dao.ModelConfig.GetByIds(ctx, ids) + if err != nil || len(models) == 0 { return &dto.GetUserModelListRes{List: make([]*dto.UserModelItem, 0)} } list := make([]*dto.UserModelItem, 0, len(models)) - - userCfgs, _ := dao.UserModelConfig.GetByUserId(ctx, userId) - cfgMap := make(map[int64]*entity.UserModelConfig) - for _, uc := range userCfgs { - cfgMap[uc.ModelConfigId] = uc - } - for _, m := range models { sysMax := schemaMaxTokens(m.Schema) item := &dto.UserModelItem{ @@ -244,7 +259,7 @@ func (s *userModelConfigService) GetUserModelListPage(ctx context.Context, userI ConcurrencyCount: m.ConcurrencyCount, SystemMaxTokens: sysMax, } - if uc, ok := cfgMap[m.Id]; ok { + if uc, ok := cfgByModelId[m.Id]; ok { item.Configured = true item.UserApiKey = uc.ApiKey item.EndpointUrl = uc.EndpointUrl @@ -255,5 +270,5 @@ func (s *userModelConfigService) GetUserModelListPage(ctx context.Context, userI } list = append(list, item) } - return &dto.GetUserModelListRes{List: list, Total: total} + return &dto.GetUserModelListRes{List: list, Total: len(list)} }