From 472d1a652169a5353d1230fb43226ff79088bf84 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 12:57:57 +0800 Subject: [PATCH] 1 --- prompt.md | 12 +++++ short_drama.db | Bin 462848 -> 462848 bytes shortdrama/service/episode_service.go | 6 +-- shortdrama/service/generation_service.go | 57 ++++++----------------- 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/prompt.md b/prompt.md index cd20d9d..0773c32 100644 --- a/prompt.md +++ b/prompt.md @@ -103,6 +103,18 @@ - 只有在"画面上看不到这段内容,但需要配音解说才能让观众理解"时 → 才放入 `narration` - 如果描述的是背景中的声音元素(观众听到但画面中无人提及)→ 放入 `ambientSound` +### ⚠️ 最高优先级规则:画外音与台词严格分离 + +剧情描述中如果出现以下标记,必须原文原样放入对应的字段,**绝对不能混淆**: + +| 剧情描述中的标记 | 必须放入的字段 | 绝对禁止放入的字段 | +|---|---|---| +| 标记为「画外音」「旁白」「内心独白」「OS(Overlapping Sound)」的内容 | `narration` | `dialogue` | +| 标记为「台词」「对白」「说」「喊道」「低语」「质问」等角色亲口说出的话 | `dialogue` | `narration` | +| 引号「」括起来的角色说话内容 | `dialogue` | `narration` | + +**这条规则的优先级别高于其他所有规则。** 即使你觉得某段画外音放到 dialogue 中"更合适",也必须严格按照原文标记放。原文标记为画外音的,只能出现在 `narration` 字段,出现在 `dialogue` 字段就是错误。 + **常见错误**(禁止): ``` 错误1:把画面描述放入旁白 diff --git a/short_drama.db b/short_drama.db index 5c1204b29a705ae3ebfa38b4098a42736a680cae..e9e79d82c90f14e3835b3a334632fd36a601e522 100644 GIT binary patch delta 768 zcmZ{hOH30%7{_;ZX0~10#g+pGicsSt6790DvfUmK6Vm#iN)%Er@~E_p2LZwGh>_IT zK!`DjJ&Z;Ziw8Xz4p_rnBtg6w!l6emCUAj+CTfBg0>NpONJ#vWf96Xj^Zowce^S}1 zRJQ7o$GyaH+%2X{Obr|-mQ0<}!P0CessrInyzT`Imqs%-Z}LZ#F8^!s}@Iht!Aa?T%L z4?tgf=>Fyo-P{2VFNX6RoM)qRu=>gA^k5kWfW1PBM5KFn!Hfw^fGYdrgE!(sWIBxH zGTk3kELqe2mZqOn0zpL~Eosz4mdEfp;ysIN=~f6;Lt1fy|3>`61Sd?0ZsuTL*?+ki zNIWb>bV&j)Yl@s?yeHn@bvx6tp<>3-46+)=W8^^ywUIYXsDftKumS1#1^g3KKORUA z70imeX8#V^Yt{$^HB(k~y2jBo{4nx}T2QruhSeG>UekBOD4N7F+x#MNt)O-H71p*g zRWW%AC$+Dl?RFbnKJ5_oo}7!JQDom#e-p$B4tgl@%5 zpK6*hpP?pHUsrd+@~Qq$Sve*W#Mc&k$R%0V*ER6M!IZ4Y598yWUQX|5Rzz%pY zf(DaAeYFA?h!;V{I=b(&K@kKNUY3ODl^5M;ovX#LbF64xJn-`Jyf4r5Q%XLi zp9iTPHr`0=_E#cT+6fdyDT$@M`EH(dzhCa)uz zbU5&IO6&Tuhy9F$gH_*?U927lNxN^1@WCazOm^N2sX5D!|M;f>N7%?0x|^+cLV%sC z5(!7NyfJL~V*)#$f|rwvq_QZ`lF(v*%EVC`)C2yEo%AMHDMwrQT$c7-hd#66CDyz` z*IZrZ*k;OF1ub#=Dy^qQ#E>|F7%P7y{j6Ijhkf~snr%$;cnn7{^^nn_;cINQ6FRIw zZcQ^XzvJ+$r(un_3psVHv2@A1Rr+s(#)C)i$OdM2p3ovQft}QIvW@1I6t-zUI|*8AqvsxQEtqUew#(?R%6D0#7xLR9Q_|w8DHkhU<+*Ht!ITL Lm{ed^u@!y+P!PWo diff --git a/shortdrama/service/episode_service.go b/shortdrama/service/episode_service.go index f59d63e..0a899db 100644 --- a/shortdrama/service/episode_service.go +++ b/shortdrama/service/episode_service.go @@ -592,13 +592,13 @@ 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- ambientSound: 环境音效——背景中的声音元素,如风声、雨声、脚步声、警笛声、门铃声、人群嘈杂等。后期音效合成,不需要配音。本镜头无特殊环境音时留空即可。\n- shotSize: 景别,从以下标准类型中选择一种:远景、全景、中景、近景、特写\n- cameraMovement: 运镜方式,从以下标准类型中选择一种:固定镜头、推、拉、摇、移、跟、升、降、旋转、晃动、航拍\n- characters: 出演人物数组,填写演员名称,如[\"张三\", \"李四\"],从「可用演员」中选择。**推断规则:从本镜头的event(画面出现了谁)、dialogue(看\"角色名:台词\"格式中的角色名)、narration(旁白提到了谁)三个字段中提取出演人物,三者取并集。画面中没出现、没说话、旁白也没提到的人物绝不能出现在characters中**\n- scene: 场景名称,从「可用场景」中选择\n- props: 道具名称数组,如[\"剑\", \"酒杯\"],从「可用道具」中选择\n\n**朗读时长约束(硬性规则)**:每个镜头的时长(endTime−startTime)必须足以容纳其台词(dialogue)和旁白(narration)的配音。参考中文语速" + speedPrompt + ",台词+旁白的预估朗读时间(总字数÷对应语速值)不得大于镜头时长的80%。如果当前时长不足,必须延长endTime,并按需调整后续镜头的startTime。\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: 主台词——角色在画面中亲口说出的对白。多人对话用「角色名:台词」格式。**旁白、内心独白、画外音不属于这里。如果剧情描述将其标记为「画外音/旁白」,即使它看起来像一段话,也绝对禁止放入 dialogue。** 如果本镜头无人说话,留空即可。\n- narration: 旁白配音——需要配音演员念出来的解说文字。必须是**通过画面无法直接传达**的信息(如角色内心独白、故事背景交代、时间跳跃说明等)。**如果一段文字描述的是观众能直接看到的画面内容,它就不属于旁白,必须放进 event 字段。** 本镜头无旁白时留空即可。\n\n**最高优先级规则:剧情描述中明确标记为「画外音/旁白」的内容,必须原文原样放入 narration 字段,绝对不能放入 dialogue 字段。dialogue 字段只收录角色亲口说出的对白。这条规则的优先级高于其他所有规则。**\n- ambientSound: 环境音效——背景中的声音元素,如风声、雨声、脚步声、警笛声、门铃声、人群嘈杂等。后期音效合成,不需要配音。本镜头无特殊环境音时留空即可。\n- shotSize: 景别,从以下标准类型中选择一种:远景、全景、中景、近景、特写\n- cameraMovement: 运镜方式,从以下标准类型中选择一种:固定镜头、推、拉、摇、移、跟、升、降、旋转、晃动、航拍\n- characters: 出演人物数组,填写演员名称,如[\"张三\", \"李四\"],从「可用演员」中选择。**推断规则:从本镜头的event(画面出现了谁)、dialogue(看\"角色名:台词\"格式中的角色名)、narration(旁白提到了谁)三个字段中提取出演人物,三者取并集。画面中没出现、没说话、旁白也没提到的人物绝不能出现在characters中**\n- scene: 场景名称,从「可用场景」中选择\n- props: 道具名称数组,如[\"剑\", \"酒杯\"],从「可用道具」中选择\n\n**朗读时长约束(硬性规则)**:每个镜头的时长(endTime−startTime)必须足以容纳其台词(dialogue)和旁白(narration)的配音。参考中文语速" + speedPrompt + ",台词+旁白的预估朗读时间(总字数÷对应语速值)不得大于镜头时长的80%。如果当前时长不足,必须延长endTime,并按需调整后续镜头的startTime。\n\n直接输出JSON数组,不要markdown代码块标记,不要其他任何内容。\n\n") // 根据剧情描述是否已有结构化信息,动态调整创作规范 if isStructuredDescription(description) { - fmt.Fprintf(&b, "【创作规范】\n\n【总时长约束】本集固定时长 %d 秒,所有镜头时长之和必须等于此值(含朗读时间)。\n\n【重要】剧情描述已包含结构化镜头信息,执行「只补缺不创作」模式:\n\n1. 严格保留已有内容 —— 剧情描述中每个镜头已包含的字段(场景、天气、运镜、开始时间、结束时间、台词、画外音、环境音、景别、人物、道具)直接原样保留到输出 JSON 的对应字段,不做任何改写或润色。\n\n2. 只补充缺失字段 —— 检查每个镜头缺少哪些字段,仅对确实缺失的字段进行补充:\n - 缺少景别(shotSize)→ 按镜头内容从「远景/全景/中景/近景/特写」中选择合适的\n - 缺少运镜(cameraMovement)→ 按场景类型从「固定镜头/推/拉/摇/移/跟/升/降/旋转/晃动/航拍」中选择\n - 缺少台词/画外音/环境音 → 若剧情描述中已写明则原样保留,未写就留空\n - 缺少时间 → 按总时长和镜头顺序合理分配\n - scene/characters/props 缺失 → 从「可用场景/演员/道具」中选择匹配的\n - characters 推断依据:当前镜头的 event(画面描述)中出现的角色、dialogue(台词\"角色名:\"前缀)中的发言角色、narration 中涉及的角色,三者必选其一。角色既没在画面中出现、也没台词、旁白也没涉及,就不能写入 characters\n\n3. 禁止新增镜头或情节 —— 严格按照剧情描述中已有的镜头分段输出,不得自行拆分、合并、增加剧情描述中没有的镜头或动作。剧情描述中有几个镜头就输出几个镜头。\n\n - 如果剧情内容超过 %d 秒容量,优先保证所有台词完整保留,非核心画面描述可适当精简。\n\n4. 朗读时长约束 —— 有台词(dialogue)或旁白(narration)的镜头必须遵守:\n - 镜头时长(endTime−startTime)必须足以容纳配音内容。参考中文语速"+speedPrompt+"。\n - 要求:endTime−startTime ≥ (len(dialogue)+len(narration))÷对应语速值,即至少满足朗读所需时间。\n - 如果当前endTime不够,必须向后延长,并相应调整后续镜头的startTime。\n\n5. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n", d.EpisodeDuration, d.EpisodeDuration) + fmt.Fprintf(&b, "【创作规范】\n\n【总时长约束】本集固定时长 %d 秒,所有镜头时长之和必须等于此值(含朗读时间)。\n\n【重要】剧情描述已包含结构化镜头信息,执行「只补缺不创作」模式:\n\n1. 严格保留已有内容 —— 剧情描述中每个镜头已包含的字段(场景、天气、运镜、开始时间、结束时间、台词、画外音、环境音、景别、人物、道具)直接原样保留到输出 JSON 的对应字段,不做任何改写或润色。\n - **特别重要的硬性规则:剧情描述中标记为「画外音/旁白」的内容,必须原样放入对应镜头的 narration 字段,绝对不能放入 dialogue 字段。** 即使你觉得某段画外音写成 dialogue 很合理,也必须严格遵循原文标记。这条规则优先级高于所有其他规则。\n\n2. 只补充缺失字段 —— 检查每个镜头缺少哪些字段,仅对确实缺失的字段进行补充:\n - 缺少景别(shotSize)→ 按镜头内容从「远景/全景/中景/近景/特写」中选择合适的\n - 缺少运镜(cameraMovement)→ 按场景类型从「固定镜头/推/拉/摇/移/跟/升/降/旋转/晃动/航拍」中选择\n - 缺少台词/画外音/环境音 → 若剧情描述中已写明则原样保留,未写就留空\n - 缺少时间 → 按总时长和镜头顺序合理分配\n - scene/characters/props 缺失 → 从「可用场景/演员/道具」中选择匹配的\n - characters 推断依据:当前镜头的 event(画面描述)中出现的角色、dialogue(台词\"角色名:\"前缀)中的发言角色、narration 中涉及的角色,三者必选其一。角色既没在画面中出现、也没台词、旁白也没涉及,就不能写入 characters\n\n3. 禁止新增镜头或情节 —— 严格按照剧情描述中已有的镜头分段输出,不得自行拆分、合并、增加剧情描述中没有的镜头或动作。剧情描述中有几个镜头就输出几个镜头。\n\n - 如果剧情内容超过 %d 秒容量,优先保证所有台词完整保留,非核心画面描述可适当精简。\n\n4. 朗读时长约束 —— 有台词(dialogue)或旁白(narration)的镜头必须遵守:\n - 镜头时长(endTime−startTime)必须足以容纳配音内容。参考中文语速"+speedPrompt+"。\n - 要求:endTime−startTime ≥ (len(dialogue)+len(narration))÷对应语速值,即至少满足朗读所需时间。\n - 如果当前endTime不够,必须向后延长,并相应调整后续镜头的startTime。\n\n5. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n", d.EpisodeDuration, d.EpisodeDuration) } else { - b.WriteString(fmt.Sprintf("【创作规范】\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. 人物引用规则(硬性规则)—— characters 字段必须从本镜头的画面描述、台词、旁白中推断:\n - event 画面描述中提到了谁 → 谁就必须在 characters 中\n - dialogue 中的发言角色(\"角色名:台词\"格式中的角色名)→ 该角色必须在 characters 中\n - narration 中涉及的角色 → 该角色必须在 characters 中\n - 以上三条取并集。不在画面中、不说话、旁白也不涉及的角色即使在整部剧里很重要,本镜头也不能写入 characters。\n\n4. 景别与动作匹配规则 —— 按镜头内容选择正确的 shotSize:\n - shotSize=特写:面部表情、微表情、眼神变化、细节反应(手部小动作、物品细节)。不能用于动作展示。\n - shotSize=近景:对话、上半身互动、情绪交流。能看清面部表情和手势。\n - shotSize=中景:日常动作、手持物体、多人交互。膝盖以上,既有动作又有空间感。\n - shotSize=全景:连贯动作、奔跑、打斗、空间关系。展示全身动作和环境关系。\n - shotSize=远景:环境交代、空镜过渡、场景建立。没有人或人很小,用于转场。\n\n5. 朗读时长约束(硬性规则)—— 有台词(dialogue)或旁白(narration)的镜头必须遵守:\n - 镜头时长(endTime−startTime)必须足以容纳配音内容。参考中文语速"+speedPrompt+"。\n - 要求:endTime−startTime ≥ (len(dialogue)+len(narration))÷对应语速值,即至少满足朗读所需时间。\n - 如果当前endTime不够,必须向后延长,并相应调整后续镜头的startTime。\n\n6. 运镜搭配规则 —— 按场景类型选择 cameraMovement:\n - 对话/情绪交流 → 固定镜头或缓推\n - 动作/打斗 → 跟、摇、移\n - 人物进入/离开画面 → 跟、移\n - 情绪揭示 → 推\n - 环境展示 → 摇、升降、航拍\n - 紧张/混乱 → 晃动\n - 固定镜头是默认选项,无特殊需要不使用复杂运镜。\n\n7. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n - 不说话只做反应的角色可用其他景别。\n", d.EpisodeDuration)) + b.WriteString(fmt.Sprintf("【创作规范】\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. 人物引用规则(硬性规则)—— characters 字段必须从本镜头的画面描述、台词、旁白中推断:\n - event 画面描述中提到了谁 → 谁就必须在 characters 中\n - dialogue 中的发言角色(\"角色名:台词\"格式中的角色名)→ 该角色必须在 characters 中\n - narration 中涉及的角色 → 该角色必须在 characters 中\n - 以上三条取并集。不在画面中、不说话、旁白也不涉及的角色即使在整部剧里很重要,本镜头也不能写入 characters。\n\n4. 景别与动作匹配规则 —— 按镜头内容选择正确的 shotSize:\n - shotSize=特写:面部表情、微表情、眼神变化、细节反应(手部小动作、物品细节)。不能用于动作展示。\n - shotSize=近景:对话、上半身互动、情绪交流。能看清面部表情和手势。\n - shotSize=中景:日常动作、手持物体、多人交互。膝盖以上,既有动作又有空间感。\n - shotSize=全景:连贯动作、奔跑、打斗、空间关系。展示全身动作和环境关系。\n - shotSize=远景:环境交代、空镜过渡、场景建立。没有人或人很小,用于转场。\n\n5. 朗读时长约束(硬性规则)—— 有台词(dialogue)或旁白(narration)的镜头必须遵守:\n - 镜头时长(endTime−startTime)必须足以容纳配音内容。参考中文语速"+speedPrompt+"。\n - 要求:endTime−startTime ≥ (len(dialogue)+len(narration))÷对应语速值,即至少满足朗读所需时间。\n - 如果当前endTime不够,必须向后延长,并相应调整后续镜头的startTime。\n\n6. 运镜搭配规则 —— 按场景类型选择 cameraMovement:\n - 对话/情绪交流 → 固定镜头或缓推\n - 动作/打斗 → 跟、摇、移\n - 人物进入/离开画面 → 跟、移\n - 情绪揭示 → 推\n - 环境展示 → 摇、升降、航拍\n - 紧张/混乱 → 晃动\n - 固定镜头是默认选项,无特殊需要不使用复杂运镜。\n\n7. **画外音与台词严格分离(硬性规则)—— 如果某段文字是角色内心独白、故事背景介绍、时间跳跃说明等内容(画外音/旁白),必须放入 narration 字段,绝对不能放入 dialogue 字段。dialogue 只放角色亲口说出的对白。这条规则优先级最高。**\n\n8. 口型适配规则 —— 有台词的镜头(dialogue 不为空)必须遵守:\n - shotSize 必须为中景或近景(不能是远景、全景或特写)。\n - 说话角色必须面向或侧向镜头,不能背对镜头说话。\n - 不说话只做反应的角色可用其他景别。\n", d.EpisodeDuration)) } b.WriteString(agent.GetShotDurationPrompt(ctx, d.Type)) b.WriteString(agent.GetStylePrompt(ctx, d.Type)) diff --git a/shortdrama/service/generation_service.go b/shortdrama/service/generation_service.go index 1e3f11a..5eccb63 100644 --- a/shortdrama/service/generation_service.go +++ b/shortdrama/service/generation_service.go @@ -1620,6 +1620,8 @@ func (s *generationService) StartVideoPoller(ctx context.Context) { g.Log().Infof(ctx, "视频轮询池已初始化,并发数=%d", poolSize) go func() { + // 启动后立即执行一次,不等第一个 tick + s.pollPendingVideos(ctx) ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() g.Log().Infof(ctx, "后台视频轮询器已启动,间隔 15s") @@ -1924,53 +1926,22 @@ func (s *generationService) pollVideoTaskOnce(ctx context.Context, modelCfg *Mer return "", fmt.Errorf("task error: %s - %s", result.Code, result.Message) } func (s *generationService) waitForSegmentVideo(ctx context.Context, d *entity.Drama, ep *entity.Episode, taskId int64) error { - modelCfg := UserModelConfigService.GetMergedConfig(ctx, d.UserId, "video") - if modelCfg.ApiKey == "" || modelCfg.BaseUrl == "" { - return fmt.Errorf("video model not configured or query URL is empty") - } - - task, err := dao.GenerationTask.GetOne(ctx, taskId) - if err != nil || task == nil { - return fmt.Errorf("get task failed: %w", err) - } - if task.VideoTaskId == "" { - return fmt.Errorf("task %d has no video_task_id yet", taskId) - } - - videoTaskID := task.VideoTaskId - maxAttempts := 50 // 约 5 分钟(每次轮询间隔 6s) - for attempt := 0; attempt < maxAttempts; attempt++ { - videoURL, err := s.pollVideoTaskOnce(ctx, modelCfg, videoTaskID) - if err == nil && videoURL != "" { - // 视频就绪,下载到本地 - dramaTitle := sanitizeDirName(d.Title) - safeEp := sanitizeDirName(ep.Title) - localPath, dlErr := s.downloadToLocal(ctx, videoURL, dramaTitle, "产出视频", - fmt.Sprintf("%s_seg_%d.mp4", safeEp, task.SegmentIdx)) - if dlErr != nil { - g.Log().Warningf(ctx, "serial wait: task %d segment %d video download failed, using remote url: %v", task.Id, task.SegmentIdx+1, dlErr) - _ = dao.GenerationTask.UpdateFields(ctx, task.Id, g.Map{"video_url": videoURL, "video_task_id": ""}) - } else { - g.Log().Infof(ctx, "串行等待: 任务 %d 第%d段视频已下载: %s", task.Id, task.SegmentIdx+1, localPath) - _ = dao.GenerationTask.UpdateFields(ctx, task.Id, g.Map{"video_url": localPath, "video_task_id": ""}) - } + // 不直接调 API 轮询,由全局 StartVideoPoller(每 15 秒)负责, + // 这里只查数据库等待 video_url 被 poller 写入 + for { + task, err := dao.GenerationTask.GetOne(ctx, taskId) + if err != nil || task == nil { + return fmt.Errorf("get task failed: %w", err) + } + if task.VideoUrl != "" { + g.Log().Infof(ctx, "串行等待: task %d segment %d video ready: %s", task.Id, task.SegmentIdx+1, task.VideoUrl) return nil } - if err != nil { - errMsg := strings.ToLower(err.Error()) - // RUNNING/PENDING 是正常处理中,继续轮询 - // "duration" 类错误也是可重试的 - if strings.Contains(errMsg, "running") || strings.Contains(errMsg, "pending") { - g.Log().Infof(ctx, "serial wait: segment %d video task still %s, retrying...", task.SegmentIdx+1, err.Error()) - } else if strings.Contains(errMsg, "duration") { - g.Log().Warningf(ctx, "serial wait: segment %d video task duration error, retrying: %v", task.SegmentIdx+1, err) - } else { - return fmt.Errorf("poll video task failed: %w", err) - } + if task.Status == consts.TaskStatusFailed { + return fmt.Errorf("segment %d video generation failed", task.SegmentIdx+1) } - time.Sleep(6 * time.Second) + time.Sleep(15 * time.Second) } - return fmt.Errorf("serial wait timeout: task %d segment %d not ready after %d polls", task.Id, task.SegmentIdx+1, maxAttempts) } // ==================== Video Generation ====================