diff --git a/short_drama.db b/short_drama.db index 44a7da9..6fc35f8 100644 Binary files a/short_drama.db and b/short_drama.db differ diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index 87283f7..8e11468 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -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)