This commit is contained in:
2026-07-27 09:34:02 +08:00
parent a55b4f847f
commit 735eb69e35
20 changed files with 114 additions and 96 deletions
+2 -2
View File
@@ -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)
+11 -4
View File
@@ -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
```
**短剧需要旁白解说和角色对白来推动剧情,但旁白必须是有配音价值的内容(角色内心想法、背景说明等),不能把画面描述放进旁白凑字数。**
BIN
View File
Binary file not shown.
+1 -9
View File
@@ -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)
}
+2 -2
View File
@@ -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+"%")
}
+2 -2
View File
@@ -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)
+5 -20
View File
@@ -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
}
+18 -2
View File
@@ -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"}).
+2 -2
View File
@@ -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
}
+7 -1
View File
@@ -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 清除用户模型配置的缓存
+20 -23
View File
@@ -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")
@@ -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 {
+1 -1
View File
@@ -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:"省份"`
+1 -1
View File
@@ -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:"省份"`
+2 -2
View File
@@ -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:"搜索关键词"`
}
+1 -1
View File
@@ -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:"模型名称关键词"`
}
+1 -1
View File
@@ -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=受保护"`
}
@@ -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:"模型名称关键词"`
}
+2 -2
View File
@@ -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")
+34 -19
View File
@@ -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)}
}