This commit is contained in:
2026-07-28 17:37:48 +08:00
parent 44616670ce
commit eb689363d0
2 changed files with 240 additions and 44 deletions
BIN
View File
Binary file not shown.
+240 -44
View File
@@ -846,55 +846,101 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
name string
}
var namedRefs []_namedRef
chars, _, _ := dao.Character.ListPageByDrama(ctx, dramaId, 1, -1)
for _, ch := range chars {
if ch.PortraitPath == "" {
continue
}
namedRefs = append(namedRefs, _namedRef{path: ch.PortraitPath, name: ch.Name})
}
scenes, _ := dao.Scene.ListByDrama(ctx, dramaId)
for _, sc := range scenes {
if sc.ImagePath == "" {
continue
}
namedRefs = append(namedRefs, _namedRef{path: sc.ImagePath, name: sc.Name})
}
props, _ := dao.Prop.ListByDrama(ctx, dramaId)
for _, p := range props {
if p.ImagePath == "" {
continue
}
namedRefs = append(namedRefs, _namedRef{path: p.ImagePath, name: p.Name})
}
refURLs := make([]string, len(namedRefs))
for i, nr := range namedRefs {
refURLs[i] = nr.path
}
// 将绝对路径转为相对路径(以当前工作目录为基准),便于后续 base64 转换
if cwd, err := os.Getwd(); err == nil {
for i, u := range refURLs {
if rel, e := filepath.Rel(cwd, u); e == nil {
refURLs[i] = rel
}
}
}
// 构建名称替换映射(使用模型配置的 reference_template,如 "图%d"
type _nameLabel struct{ name, label string }
var nameLabels []_nameLabel
refLabelTemplate := modelCfg.ReferenceTemplate
if refLabelTemplate != "" {
for i, nr := range namedRefs {
nameLabels = append(nameLabels, _nameLabel{nr.name, fmt.Sprintf(refLabelTemplate, i+1)})
var refURLs []string
// loadEntityRefs 从数据库加载角色/场景/道具记录,构建引用路径。
// 对于尚未上传照片的记录,构造预期的 workspace 路径作为占位,确保引用的完整性。
// 根据 schema 中的 media.max_items 限制总引用数,角色优先于场景优先于道具。
loadEntityRefs := func() []*entity.Character {
// 从 schema_mapping 中的 max_media_items 读取 media 数组长度上限
maxRefItems := 5
if modelCfg.SchemaMapping != "" && strings.HasPrefix(modelCfg.SchemaMapping, "{") {
var sm struct {
MaxMediaItems string `json:"max_media_items"`
}
if err := json.Unmarshal([]byte(modelCfg.SchemaMapping), &sm); err == nil && sm.MaxMediaItems != "" {
// schema_mapping 中的路径相对于 request body(如 input.media.max_items),
// schema 定义在 body 节点下,导航时需要补 body. 前缀
path := sm.MaxMediaItems
if !strings.HasPrefix(path, "body.") {
path = "body." + path
}
if v := nested(vs, strings.Split(path, ".")...); v != nil {
if f, ok := v.(float64); ok {
maxRefItems = int(f)
}
}
}
}
sort.Slice(nameLabels, func(a, b int) bool {
return len(nameLabels[a].name) > len(nameLabels[b].name)
})
chars, _, _ := dao.Character.ListPageByDrama(ctx, dramaId, 1, -1)
namedRefs = nil
for _, ch := range chars {
if len(namedRefs) >= maxRefItems {
break
}
path := ch.PortraitPath
if path == "" {
path = filepath.Join(WorkspaceDir(d.Title), "演员形象", sanitizeDirName(ch.Name)+".jpeg")
}
namedRefs = append(namedRefs, _namedRef{path: path, name: ch.Name})
}
if len(namedRefs) < maxRefItems {
scenes, _ := dao.Scene.ListByDrama(ctx, dramaId)
for _, sc := range scenes {
if len(namedRefs) >= maxRefItems {
break
}
path := sc.ImagePath
if path == "" {
path = filepath.Join(WorkspaceDir(d.Title), "场景", sanitizeDirName(sc.Name)+".jpeg")
}
namedRefs = append(namedRefs, _namedRef{path: path, name: sc.Name})
}
}
if len(namedRefs) < maxRefItems {
props, _ := dao.Prop.ListByDrama(ctx, dramaId)
for _, p := range props {
if len(namedRefs) >= maxRefItems {
break
}
path := p.ImagePath
if path == "" {
path = filepath.Join(WorkspaceDir(d.Title), "道具", sanitizeDirName(p.Name)+".jpeg")
}
namedRefs = append(namedRefs, _namedRef{path: path, name: p.Name})
}
}
refURLs = make([]string, len(namedRefs))
for i, nr := range namedRefs {
refURLs[i] = nr.path
}
if cwd, err := os.Getwd(); err == nil {
for i, u := range refURLs {
if rel, e := filepath.Rel(cwd, u); e == nil {
refURLs[i] = rel
}
}
}
nameLabels = nil
refLabelTemplate := modelCfg.ReferenceTemplate
if refLabelTemplate != "" {
for i, nr := range namedRefs {
nameLabels = append(nameLabels, _nameLabel{nr.name, fmt.Sprintf(refLabelTemplate, i+1)})
}
sort.Slice(nameLabels, func(a, b int) bool {
return len(nameLabels[a].name) > len(nameLabels[b].name)
})
}
return chars
}
chars := loadEntityRefs()
// 读取反向提示词
negativePrompt := ""
if data, err := os.ReadFile(getDataPath("negative_prompt.md")); err == nil {
@@ -940,6 +986,11 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string,
}
}
// ============ 从镜头脚本提取演员/场景/道具,自动写入数据库(避免重复)============
extractAndSaveEntities(ctx, dramaId, allShots)
// 重新加载实体引用,确保新增记录也被纳入后续的 media/prompt 构建
chars = loadEntityRefs()
// ============ 直接为各分段创建生成任务(script 按模型配置的 API 请求体格式构建)============
segDurs := calcSegDurs(d.EpisodeDuration, modelCfg)
// 按镜头数量均分到各分段(避免时间窗口切分导致的空段或信息不全)
@@ -1120,6 +1171,151 @@ func buildCharacterGuide(charNames []string, chars []*entity.Character, labelOf
return guide
}
// extractAndSaveEntities 从镜头数组中提取演员/场景/道具,去重后写入数据库。
// 如果同 drama 下已存在同名记录,跳过插入(避免重复)。
// 描述信息从镜头的 event 画面描述中提取,保证是视觉/外观描述。
func extractAndSaveEntities(ctx context.Context, dramaId int64, shots []domain.Shot) {
if len(shots) == 0 {
return
}
charSet := make(map[string]bool)
sceneSet := make(map[string]bool)
propSet := make(map[string]bool)
// name → []event 片段
charEvents := make(map[string][]string)
sceneEvents := make(map[string][]string)
propEvents := make(map[string][]string)
for _, sh := range shots {
for _, c := range sh.Characters {
if c == "" {
continue
}
if !charSet[c] {
charSet[c] = true
}
if sh.Event != "" {
charEvents[c] = append(charEvents[c], sh.Event)
}
}
if sh.Scene != "" {
sceneSet[sh.Scene] = true
if sh.Event != "" {
sceneEvents[sh.Scene] = append(sceneEvents[sh.Scene], sh.Event)
}
}
for _, p := range sh.Props {
if p == "" {
continue
}
if !propSet[p] {
propSet[p] = true
}
if sh.Event != "" {
propEvents[p] = append(propEvents[p], sh.Event)
}
}
}
// 查询已有记录
existingChars, _, _ := dao.Character.ListPageByDrama(ctx, dramaId, 1, -1)
existingScenes, _ := dao.Scene.ListByDrama(ctx, dramaId)
existingProps, _ := dao.Prop.ListByDrama(ctx, dramaId)
existName := func(list interface{}, name string) bool {
switch l := list.(type) {
case []*entity.Character:
for _, c := range l {
if c.Name == name {
return true
}
}
case []*entity.Scene:
for _, s := range l {
if s.Name == name {
return true
}
}
case []*entity.Prop:
for _, p := range l {
if p.Name == name {
return true
}
}
}
return false
}
buildDesc := func(events []string) string {
seen := make(map[string]bool)
var parts []string
for _, e := range events {
if e == "" || seen[e] {
continue
}
seen[e] = true
parts = append(parts, e)
if len(parts) >= 3 {
break
}
}
if len(parts) == 0 {
return ""
}
result := strings.Join(parts, "")
runes := []rune(result)
if len(runes) > 300 {
result = string(runes[:300]) + "……"
}
return result
}
now := time.Now().Format("2006-01-02 15:04:05")
for name := range charSet {
if existName(existingChars, name) {
continue
}
desc := buildDesc(charEvents[name])
_, _ = g.DB().Model(consts.TableNameCharacter).Ctx(ctx).Data(g.Map{
"drama_id": dramaId,
"name": name,
"description": desc,
"created_at": now,
"updated_at": now,
}).Insert()
}
for name := range sceneSet {
if existName(existingScenes, name) {
continue
}
desc := buildDesc(sceneEvents[name])
_, _ = g.DB().Model(consts.TableNameScene).Ctx(ctx).Data(g.Map{
"drama_id": dramaId,
"name": name,
"description": desc,
"created_at": now,
"updated_at": now,
}).Insert()
}
for name := range propSet {
if existName(existingProps, name) {
continue
}
desc := buildDesc(propEvents[name])
_, _ = g.DB().Model(consts.TableNameProp).Ctx(ctx).Data(g.Map{
"drama_id": dramaId,
"name": name,
"description": desc,
"created_at": now,
"updated_at": now,
}).Insert()
}
}
// inferCharPerSecond 根据台词情绪推断语速(字/秒)
func inferCharPerSecond(text string, normal, fast, slow int) int {
runes := []rune(text)