From eb689363d00a748dc7b5de83a3224cbee80086e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Tue, 28 Jul 2026 17:37:48 +0800 Subject: [PATCH] 1 --- short_drama.db | Bin 462848 -> 462848 bytes shortdrama/service/episode_service.go | 284 ++++++++++++++++++++++---- 2 files changed, 240 insertions(+), 44 deletions(-) diff --git a/short_drama.db b/short_drama.db index 44a7da93c8d22d88d3e7a757f3356bff4ff0cf9d..6fc35f8ddd9d7b17b82ab1fb9ce1c1b41affc838 100644 GIT binary patch delta 29400 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 Bxa|M{ delta 11538 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 olw{ 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)