diff --git a/short_drama.db b/short_drama.db index b9ff6fa..da91d6d 100644 Binary files a/short_drama.db and b/short_drama.db differ diff --git a/shortdrama/service/drama_service.go b/shortdrama/service/drama_service.go index 94ba184..9ba13bc 100644 --- a/shortdrama/service/drama_service.go +++ b/shortdrama/service/drama_service.go @@ -392,7 +392,7 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama, // JSON 镜头脚本跳过 Agent,直接构建场景描述提交视频模型 var segOutput *model.SegmentOutput if domain.IsShotsJSON(ep.Script) && feedback == "" { - segOutput = buildSegOutputFromShots(ep.Script, segIdx, segStartTime, segDur) + segOutput = buildSegOutputFromShots(ep.Script, segIdx, segStartTime, segDur, genCtx) g.Log().Infof(ctx, "第%d集第%d段 JSON镜头直接提交: %d个镜头", ep.Index, segIdx+1, len(segOutput.Scenes)) } else { result, err := s.generateSegment(ctx, d, ep, segIdx, segDur, segStartTime, totalSegs, modelCfg, feedback, genCtx) @@ -556,7 +556,7 @@ func (s *dramaService) generateOneSegment(ctx context.Context, d *entity.Drama, // buildSegOutputFromShots 从 JSON 镜头数组直接构建 SegmentOutput(跳过 Agent) // 将本段所有镜头合并为一段连贯的场景描述,去掉"旁白/台词/事件"标记,适合视频模型理解 -func buildSegOutputFromShots(script string, segIdx, segStartTime, segDur int) *model.SegmentOutput { +func buildSegOutputFromShots(script string, segIdx, segStartTime, segDur int, genCtx *GenerationContext) *model.SegmentOutput { var allShots []domain.Shot if err := json.Unmarshal([]byte(script), &allShots); err != nil || len(allShots) == 0 { return &model.SegmentOutput{Index: segIdx, TextOutput: script} @@ -584,23 +584,80 @@ func buildSegOutputFromShots(script string, segIdx, segStartTime, segDur int) *m chars = append(chars, model.SegmentCharacter{Name: name}) } - // 将所有镜头合并为一段连贯描述(去掉旁白/台词/事件标记,自然语言拼接) + // 从 genCtx 构建角色外观描述(确保每段角色服装/外貌一致) + var appearanceDescs []string + for name := range charSet { + for _, c := range genCtx.Characters { + if c.Name == name && c.Description != "" { + appearanceDescs = append(appearanceDescs, name+":"+c.Description) + break + } + } + } + // 收集本段涉及场景的描述(确保各段画风一致) + var sceneDescs []string + seenScenes := make(map[string]bool) + for _, sh := range segShots { + if sh.Scene == "" || seenScenes[sh.Scene] { + continue + } + seenScenes[sh.Scene] = true + for _, sc := range genCtx.Scenes { + if sc.Name == sh.Scene && sc.Description != "" { + sceneDescs = append(sceneDescs, sc.Name+":"+sc.Description) + break + } + } + } + + // 将所有镜头合并为一段连贯描述,每个镜头包含场景/运镜/道具信息 descParts := make([]string, 0, len(segShots)*2) for _, sh := range segShots { - // 旁白直接作为叙述 + // 镜头头部信息:场景、运镜、道具 + var shotHeaders []string + if sh.Scene != "" { + shotHeaders = append(shotHeaders, "场景:"+sh.Scene) + } + if sh.CameraMovement != "" { + shotHeaders = append(shotHeaders, "运镜:"+sh.CameraMovement) + } + if len(sh.Props) > 0 { + shotHeaders = append(shotHeaders, "道具:"+strings.Join(sh.Props, "、")) + } + shotParts := make([]string, 0, 4) + if len(shotHeaders) > 0 { + shotParts = append(shotParts, "["+strings.Join(shotHeaders, "][")+"]") + } + // 旁白 if sh.Narration != "" { - descParts = append(descParts, sh.Narration) + shotParts = append(shotParts, sh.Narration) } // 事件描述 if sh.Event != "" { - descParts = append(descParts, sh.Event) + shotParts = append(shotParts, sh.Event) } // 对话以自然方式嵌入 if sh.Dialogue != "" { - descParts = append(descParts, sh.Dialogue) + shotParts = append(shotParts, sh.Dialogue) + } + if len(shotParts) > 0 { + descParts = append(descParts, strings.Join(shotParts, "。")) } } - fullDesc := strings.Join(descParts, ",") + // 前面拼接角色外观和场景描述,确保风格一致 + var prefixParts []string + if len(appearanceDescs) > 0 { + prefixParts = append(prefixParts, "【角色形象】"+strings.Join(appearanceDescs, ";")) + } + if len(sceneDescs) > 0 { + prefixParts = append(prefixParts, "【场景】"+strings.Join(sceneDescs, ";")) + } + var fullDesc string + if len(prefixParts) > 0 { + fullDesc = strings.Join(prefixParts, "。") + "。" + strings.Join(descParts, ",") + } else { + fullDesc = strings.Join(descParts, ",") + } return &model.SegmentOutput{ Index: segIdx, @@ -1460,8 +1517,10 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep } } + // 固定 seed 确保各段画风/人物形象一致(基于短剧ID+剧集序号) + seed := int(d.Id)*1000 + ep.Index*10 + segIdx taskId, requestJSON, err := createVideoTask(ctx, modelCfg.VideoApiKey, modelCfg.VideoBaseUrl, modelCfg.VideoModelName, - prompt, negativePrompt, refURLs, effectiveDur, d.Resolution, d.AspectRatio, modelCfg.VideoSchema) + prompt, negativePrompt, refURLs, effectiveDur, d.Resolution, d.AspectRatio, modelCfg.VideoSchema, seed) if err != nil { return "", "", fmt.Errorf("视频合成请求失败: %w", err) } @@ -1478,7 +1537,7 @@ func (s *dramaService) submitVideoTask(ctx context.Context, d *entity.Drama, ep // // params: 额外参数(如 audio/shot_type/watermark 等) // sizes: 分辨率→尺寸字符串映射表 -func createVideoTask(ctx context.Context, apiKey, baseURL, modelName, prompt, negativePrompt string, refURLs []string, duration int, resolution, aspectRatio, videoSchema string) (string, string, error) { +func createVideoTask(ctx context.Context, apiKey, baseURL, modelName, prompt, negativePrompt string, refURLs []string, duration int, resolution, aspectRatio, videoSchema string, seed int) (string, string, error) { body := map[string]any{ "model": modelName, "input": map[string]any{ @@ -1538,6 +1597,9 @@ func createVideoTask(ctx context.Context, apiKey, baseURL, modelName, prompt, ne if duration > 0 { params["duration"] = duration } + if seed > 0 { + params["seed"] = seed + } body["parameters"] = params payload, _ := json.Marshal(body) diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index bed0e19..77c76bd 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -469,14 +469,11 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string, totalDur int } var cur _shotGroup - var prevScene string for _, sh := range allShots { shDur := parseMMSSToSeconds(sh.EndTime) - parseMMSSToSeconds(sh.StartTime) if shDur <= 0 { shDur = 1 } - // 场景切换 → 必须分段(不同场景不能合并到同一段) - sceneChanged := len(cur.shots) > 0 && sh.Scene != prevScene // 计算加入当前镜头后的候选组总时长和提示词长度 candShots := make([]domain.Shot, len(cur.shots)+1) copy(candShots, cur.shots) @@ -487,7 +484,7 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string, exceedDur := len(cur.shots) > 0 && candDur > effectiveMax exceedChars := promptMaxChars > 0 && len([]rune(candPrompt)) > promptMaxChars - if exceedDur || exceedChars || sceneChanged { + if exceedDur || exceedChars { // 当前组已满,保存并开启新组 taskGroups = append(taskGroups, _taskGroup{ promptText: buildFinalPrompt(domain.ShotsToText(cur.shots)), @@ -498,7 +495,6 @@ func createPendingTasks(ctx context.Context, dramaId, epId int64, script string, cur.shots = candShots cur.totalDur = candDur } - prevScene = sh.Scene } if len(cur.shots) > 0 { taskGroups = append(taskGroups, _taskGroup{