diff --git a/api/main.py b/api/main.py index d907a9f..d8392a9 100644 --- a/api/main.py +++ b/api/main.py @@ -3785,6 +3785,16 @@ class RewriteStoryboardScriptReq(BaseModel): segments: list[ScriptRewriteSegmentReq] = Field(default_factory=list) +class UpdateAudioScriptReq(BaseModel): + source_text: str | None = None + source_zh: str | None = None + rewritten_text: str | None = None + rewritten_text_zh: str | None = None + speaker_profile: str | None = None + rhythm_profile: str | None = None + background_audio_profile: str | None = None + + _TRANSLATION_CACHE: dict[str, str] = {} @@ -4272,6 +4282,33 @@ async def trigger_transcribe(job_id: str, bg: BackgroundTasks) -> Job: return job_with_artifacts(job) +@app.patch("/jobs/{job_id}/audio-script", response_model=Job) +def update_audio_script(job_id: str, req: UpdateAudioScriptReq) -> Job: + job = JOBS.get(job_id) + if not job: + raise HTTPException(404, "job not found") + audio_script = job.audio_script or AudioScript() + patch: dict[str, str] = {} + for field in ( + "source_text", + "source_zh", + "rewritten_text", + "rewritten_text_zh", + "speaker_profile", + "rhythm_profile", + "background_audio_profile", + ): + value = getattr(req, field) + if value is not None: + patch[field] = str(value).strip() + if not patch: + return job_with_artifacts(job) + patch["status"] = "completed" + patch["created_at"] = audio_script.created_at or time.time() + update(job, audio_script=audio_script.model_copy(update=patch)) + return job_with_artifacts(job) + + @app.get("/jobs/{job_id}/video.mp4") def get_video(job_id: str): p = job_dir(job_id) / "source.mp4" diff --git a/docs/source-analysis.html b/docs/source-analysis.html index fe3d46c..ad57787 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -569,12 +569,12 @@

业务管线

-

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。顶部固定显示 01-09 流程顺序和每一步的判定依据,编号不再是装饰文本,而是按素材任务、源视频、音频文案、抽帧、主体资产、产品资产、分镜文案、三字段规划和视频候选这些状态解锁。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;视频视觉路同步抽取参考帧,参考帧只用于人工选择主体并生成相似主体白底视图。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、相似主体资产和产品资产汇合;每条分镜默认是左侧“文案 / 场景一句话 / 人物+产品+动作”三字段、右侧横向视频候选轨。客户可直接改中文镜像,前端会调用改写/翻译链路自动优化对应英文主值;单条和整片都可选择生成数量,整片按行排队提交。首尾帧、视觉规划、产品出现方式等细节保留在高级抽屉和后端自动展开逻辑里,不再作为客户默认闸门。

+

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。顶部固定显示 01-09 流程顺序和每一步的判定依据,编号不再是装饰文本,而是按素材任务、源视频、音频文案、抽帧、主体资产、产品资产、分镜文案、三字段规划和视频候选这些状态解锁。用户粘贴 TK 链接按回车或点击“导入并提文案”,或上传视频后,系统自动下载/保存源视频;下载完成后默认只启动音频文案路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;可编辑的中文确认稿和英文生成稿直接显示在左侧素材输入区下方。抽帧参考不再作为素材输入后的默认动作,只保留在源视频工作区里给用户手动抓参考帧。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、相似主体资产和产品资产汇合;每条分镜默认是左侧“文案 / 场景一句话 / 人物+产品+动作”三字段、右侧横向视频候选轨。客户可直接改中文镜像,前端会调用改写/翻译链路自动优化对应英文主值;单条和整片都可选择生成数量,整片按行排队提交。首尾帧、视觉规划、产品出现方式等细节保留在高级抽屉和后端自动展开逻辑里,不再作为客户默认闸门。

-
01

素材输入

有当前素材任务即通过;输入框只负责创建或切换任务。

+
01

素材输入

有当前素材任务即通过;导入后自动进入下载和音频文案抽取。

02

源视频下载

job.video_url 存在即通过;created/downloading 视为运行中。公开视频默认不带 cookies 下载;只有 TikTok 明确要求登录态时才配置 YTDLP_COOKIES_FILE,生产容器禁止使用 YTDLP_COOKIES_FROM_BROWSER=chrome

03

音频文案

audio_script.source_texttranscript 逐句时间轴有内容即通过。

-
04

抽帧参考

job.frames.length > 0 即通过;参考帧只做主体重构证据。

+
04

抽帧参考

job.frames.length > 0 即通过;当前只做手动参考工具,不在导入后自动执行。

05

相似主体

关键帧里存在 subject_assets 即通过;生成类似创新主体,不复刻原人。

06

产品素材池

product_refs 有记录即通过;不限量上传,后续按分镜最多挑 6 张。

07

分镜文案

逐句时间轴生成后进入分镜;新口播可单段或整片改写。

@@ -592,8 +592,8 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、信息流工作台同源品牌 token、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。工作台在 skg-board-theme 内定义 --skg-gold-1--skg-gold-2--skg-cream--skg-bg-*--skg-text-*--skg-radius-* 和按钮阴影等变量,并新增 skg-board-brandskg-stat-cardskg-primary-actionskg-secondary-actionskg-empty-state 等样式。暗色工作台复用登录页金色聚焦、米白主按钮和弱暖光氛围;明亮模式通过 skg-board-theme--light 复用同一套结构,改成暖白底、白色 panel、黑底主 CTA 和深色文本,不另起一套界面。 - web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 12 张参考帧,形成“音频文案路 + 视频视觉路”同步推进;底部吸附音频条和旧全局浮动主题按钮不再从主界面渲染,避免和工作台内的明暗模式切换重复。 - web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharacters。顶部由 buildWorkflowSteps 统一生成 01-09 流程顺序、状态和判定依据,WorkflowOrderBar 展示完整顺序,WorkflowStepBadge / PipelineLane / 分镜列标题共用同一套编号。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧顶部用“音频文案、抽帧参考、相似主体、产品素材池”四个状态条显示后台并行进度。源视频工作区展示视频下载状态和默认折叠的文案依据。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧放大为按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方左侧是参考帧池,右侧是逐句时间轴;下一行只保留“相似主体 / 主体模板”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。逐句时间轴左侧参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。“生成 10 张高清图”放在下方相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,可选择桌面导入的 5 套内置形象作为创意方向,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端调用 generateSubjectAssets 时按主体类型传 subject_style=transparent_humansource_actor,按需传 character_id,并使用 reconstruction_mode=similar;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。主体生成完成后会形成 subject_consensus_brief,主体模板保存区可预览/编辑这段 brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,主体只传 subject_brief,不再传主体图;产品按端点选择最多 1-2 张硬参考图,默认正面,侧面/后颈/厚度/特写等关键词会额外补一张对应视角。关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按端点视角策略自动挑选最多 1-2 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型,并走纯文字首尾帧。只有该行勾选“人物”时,才会把主体 brief 注入 prompt;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 + web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“导入并提文案”会把 job 放入第一步队列,下载完成后触发 triggerTranscribe 解析音频,不再默认触发 analyzeJob 抽帧;底部吸附音频条和旧全局浮动主题按钮不再从主界面渲染,避免和工作台内的明暗模式切换重复。 + web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部先展示与登录页连续的 SKG brand strip,包含 SKG 字标、“未来健康 · 营销内容工作台”和“营销内容工作台 · TK 二创”;右侧素材/任务/视频/文案统计改为米白 stat 卡片,主动作按钮统一走 skg-primary-action,次动作走 skg-secondary-action,空状态复用 AnimatedLoginCharacters。顶部由 buildWorkflowSteps 统一生成 01-09 流程顺序、状态和判定依据,WorkflowOrderBar 展示完整顺序,WorkflowStepBadge / PipelineLane / 分镜列标题共用同一套编号。左侧素材输入负责链接/上传、任务切换和第一步文案确认;导入后自动下载/保存素材并触发音频文案抽取,生成的中文确认稿和英文生成稿直接在输入区下方可编辑,中文失焦会自动翻译/优化英文并通过 PATCH /jobs/{id}/audio-script 保存。右侧顶部保留“音频文案、抽帧参考、相似主体、产品素材池”状态条,但抽帧参考只作为手动工具,不再随导入自动执行。源视频工作区展示视频下载状态和默认折叠的文案依据。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧放大为按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方左侧是参考帧池,右侧是逐句时间轴;下一行只保留“相似主体 / 主体模板”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。逐句时间轴左侧参考帧池的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,点选状态直接叠在参考帧池缩略图上,鼠标停留会通过固定浮层放大展示完整帧。“生成 10 张高清图”放在下方相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,可选择桌面导入的 5 套内置形象作为创意方向,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端调用 generateSubjectAssets 时按主体类型传 subject_style=transparent_humansource_actor,按需传 character_id,并使用 reconstruction_mode=similar;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。主体生成完成后会形成 subject_consensus_brief,主体模板保存区可预览/编辑这段 brief。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,主体只传 subject_brief,不再传主体图;产品按端点选择最多 1-2 张硬参考图,默认正面,侧面/后颈/厚度/特写等关键词会额外补一张对应视角。关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按端点视角策略自动挑选最多 1-2 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型,并走纯文字首尾帧。只有该行勾选“人物”时,才会把主体 brief 注入 prompt;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 AudioStoryboardPlanPanel 三字段候选生成当前分镜主路径:每行是左右双栏,左侧默认显示 skg_copy_*scene_one_line_*action_one_line_* 三组中英字段,右侧直接显示视频候选横向轨。用户改中文镜像后,字段失焦会通过 refineStoryboard 优化对应英文主值,失败时退回 translateText;英文仍是后续 prompt 主值。quickPlanStoryboard 把三字段和主体 brief 展开为完整 StoryboardScenegenerateStoryboardVideocount 可由单行数字控件选择,候选新生成后持续向右追加,不再用 4-grid 撑高每行。整片生成同样可选择每行数量,并以 concurrency=1 按行排队提交。产品素材池、批量控制、每行主体区和高级区都可折叠,高级抽屉仍展示旧 6 字段、首尾帧 prompt 和首尾帧资产槽,但客户默认不用先处理首尾帧。 web/components/resource-library/library-drawer.tsx全局资源中心浮窗:由工作台顶部“资源库”按钮打开,叠加在工作台上方但不阻塞主界面;尺寸、位置和当前 Tab 写入 localStorage["skg-resource-library-drawer"]。提示词 Tab 固定 5 列(场景描述、视频描述、主体描述、SKG 文案、产品角度),每列先显示 use_count 排名前 5 的“常用”,再按月份倒序分组;提示词节点常驻复制按钮,hover 可选英文/中文/双语复制,并调用 use 接口。素材 Tab 固定 4 列(主体、产品、场景、视频),节点不可拖动,按月份倒序硬编码排列;“应用到当前 job”只调用后端复制接口,得到普通 ImageRef(kind="asset") 后再写入产品素材池或复制 ID。浮窗顶部最近 24 小时横条混合显示提示词和素材;新建提示词、上传素材、删除前查引用、详情侧栏都在该组件内完成。 AdRecreationBoard 主题切换顶部指标区左侧有“明亮/暗色”按钮,使用 Sun / Moon 图标切换 skg-board-theme--light 类名,并把选择写入 localStorage["skg-board-theme"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 @@ -637,9 +637,9 @@
前端主链路:
 web/app/page.tsx
   -> 信息流广告复刻工作表:web/components/ad-recreation-board.tsx
-  -> 开始分析:创建/激活 job → 下载完成后并行触发视频视觉路 analyzeJob 与音频文案路 triggerTranscribe
+  -> 导入并提文案:创建/激活 job → 下载完成后默认只触发音频文案路 triggerTranscribe;analyzeJob 抽帧保留为手动参考工具
   -> WorkflowOrderBar:01 素材输入 → 02 源视频下载 → 03 音频文案 → 04 抽帧参考 → 05 相似主体 → 06 产品素材池 → 07 分镜文案 → 08 三字段规划 → 09 视频候选;每步从 buildWorkflowSteps 取判定依据和状态
-  -> 左侧素材输入列 + 右侧 03-06 状态条 + 默认折叠的文案依据 + 源视频工作区(音频解析结果默认折叠,竖版 9:16 原视频播放器放大并内置当前点抽帧,右侧上方连续响度波形显示当前/总时长/指针停点,右侧下方左侧是参考帧池,右侧是逐句时间轴联动滚动;参考帧池缩略图自身显示是否已选,不再单独重复显示已选关键帧;下方只保留相似主体 / 主体模板和相似主体高清视图包;不勾选帧则默认用全部帧,勾选后只用已选帧,可叠加 5 套内置形象;主体模板区分为模板库与本次生成 / 入库草稿,数据库接口未完成前只允许命名和备注,不提交保存)
+  -> 左侧素材输入列内直接显示文案确认稿(中文可编辑,失焦自动同步英文并保存)+ 右侧 03-06 状态条 + 默认折叠的文案依据 + 源视频工作区(音频解析结果默认折叠,竖版 9:16 原视频播放器放大并内置当前点抽帧,右侧上方连续响度波形显示当前/总时长/指针停点,右侧下方左侧是参考帧池,右侧是逐句时间轴联动滚动;参考帧池缩略图自身显示是否已选,不再单独重复显示已选关键帧;下方只保留相似主体 / 主体模板和相似主体高清视图包;不勾选帧则默认用全部帧,勾选后只用已选帧,可叠加 5 套内置形象;主体模板区分为模板库与本次生成 / 入库草稿,数据库接口未完成前只允许命名和备注,不提交保存)
   -> 信息流复刻分镜工作台:06 同一产品素材池不限量上传 → 自动识别视角 / 背景 / 用途 / 风险 → 人工检查备注 → 07 逐句时间轴 / 原内容 / 新口播文案 → 08 紧凑三字段(文案、场景一句话、人物+产品+动作;可折叠)→ quick-plan 自动展开高级字段 → 单条生成 4 个视频候选 / 收起态迷你缩略条 / 展开态 4-grid / 追加生成 / 选中候选 → 09 整片一键后台批量提交
   -> 底部音频条:不再渲染,音频结果集中到右侧工作表
   -> 旧节点/深度素材面板:web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx(底层保留,当前不作为主入口)
@@ -648,7 +648,7 @@ web/app/page.tsx
 后端主链路:
 api/main.py
   -> Job / KeyFrame / KeyElement / StoryboardScene / AudioScript
-  -> 下载 / 上传 / 音频提取 / ASR / 翻译 / 声音背景音分析 / 抽帧 / Vision brief / GPT 图像生成 / 产品视角识别 / 分镜保存 / 首尾帧生成 / 后续 Azure OpenAI 配音预留
+  -> 下载 / 上传 / 音频提取 / ASR / 翻译 / 声音背景音分析 / 文案保存 / 手动抽帧 / Vision brief / GPT 图像生成 / 产品视角识别 / 分镜保存 / 首尾帧生成 / 后续 Azure OpenAI 配音预留
   -> jobs/<jobId>/state.json + 图片文件落盘
@@ -658,7 +658,7 @@ api/main.py
你看到的区域信息流广告复刻工作表
主要源码AdRecreationBoard in web/components/ad-recreation-board.tsx;品牌 token、暖光背景、主/次按钮、stat 卡片和空状态角色样式在 web/app/globals.css;状态、轮询和接口回写仍在 web/app/page.tsx
-
适合怎么描述“登录页进工作台后的品牌条、米白主按钮、金色选中态、明亮/暗色模式、素材输入列、开始分析后的自动下载、音频文案路和视频视觉路怎样并行、四路状态条和声音背景音结果怎么展示”。
+
适合怎么描述“登录页进工作台后的品牌条、米白主按钮、金色选中态、明亮/暗色模式、素材输入列、导入后怎样自动下载并提文案、文案确认稿怎么编辑、四路状态条和声音背景音结果怎么展示”。
你看到的区域音频解析结果表
@@ -757,7 +757,7 @@ api/main.py

AudioScript

-

第一步音频解析的结构化产物。pipeline_transcribe 提取 audio.wav 后先保存原始英文转写、中文翻译、讲话人画像、口播节奏和背景音乐/环境声/音效分析。rewritten_text 是英文新口播,rewritten_text_zh 只作为团队审稿镜像;voice_url 等字段仍保留给后续新配音阶段。

+

第一步音频解析的结构化产物。pipeline_transcribe 提取 audio.wav 后先保存原始英文转写、中文翻译、讲话人画像、口播节奏和背景音乐/环境声/音效分析。rewritten_text 是英文生成稿,rewritten_text_zh 是左侧素材输入区可编辑的中文确认稿;用户保存时通过 PATCH /jobs/{id}/audio-script 回写。voice_url 等字段仍保留给后续新配音阶段。

AudioScript {
   status: idle | rewriting | completed | failed,
   source_text,
@@ -969,8 +969,9 @@ ProductRefStateItem {
             重试下载POST /jobs/{id}/download/retryretryJobDownload用于 TK 链接下载失败且没有 video_url 的素材;清空错误、重新进入下载状态,并在后台再次执行 pipeline_download。上传视频不能重下载,需要重新上传文件。
             上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态;当前上传后也加入第一步队列,下载完成后自动解析音频。
             删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。
-            解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob抽参考帧能力。当前开始流程会在视频下载完成后自动调用一次,默认 frames=12target=motionquality=accuratemode=replace,形成全局动作/节奏参考帧池;原版视频旁的“抽参考 12 帧”也会用同一参数显式重跑。target 仍支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。
+            解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob抽参考帧能力。当前导入第一步不再自动调用;只有用户在源视频工作区点击“自动抽帧 12 张”或手动补参考帧时才进入该接口。target 仍支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。
             音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;远端启用时把 audio.wav 上传到 ASR_BASE_URL 的 OpenAI Audio Transcriptions 兼容接口,用 ASR_MODEL 提取原始文案,并传 ASR_LANGUAGE=en 降低英文素材延迟。微软官方路径包括 /openai/deployments/{deployment}/audio/transcriptions?api-version=.../openai/v1/audio/transcriptions?api-version=preview;当前 SKG 网关探测这些路径均未返回可用 ASR,gpt-4o-transcribe 返回 DeploymentNotFound。当前生产因此复制本地成功策略:ASR_REMOTE_ENABLED=falseASR_LOCAL_FALLBACK_ENABLED=true,直接走容器内 CPU 版 faster-whisper 生成真实逐句时间轴;ASR_AUDIO_FALLBACK_ENABLED=false,避免 Gemini 多模态假字幕。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profilerhythm_profilebackground_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。
+            保存文案确认稿PATCH /jobs/{id}/audio-scriptupdateAudioScript左侧素材输入区的“文案确认稿”保存入口。可更新 audio_script.rewritten_textrewritten_text_zh,也支持必要时修正原文、讲话人、节奏和背景音字段;中文确认稿失焦后前端先调用 /translate 优化英文,再通过该接口持久化到当前 job。
             分镜脚本改写POST /jobs/{id}/script/rewriterewriteStoryboardScript根据原英文参考文案、当前英文新口播、英文 role enum、时间段和作者想法改写英文口播;作者想法若含中文,后端会先经 _ensure_english 兜底翻译。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。后端按 AUDIO_REWRITE_MODELASR_FALLBACK_MODELTRANSLATE_MODEL 依次尝试,全部失败时用英文本地模板保留可编辑文案。接口返回 items[index,text,text_zh],其中 text 是写入模型链路的英文主值,text_zh 只供团队审稿镜像显示;点击保存规划后写入 StoryboardScene.action。
             原始音频文件GET /jobs/{id}/audio.wavsourceAudioUrl返回拆轨得到的 wav;当前主界面不再渲染底部吸附音频条,右侧复刻工作表会读取该文件生成参考图式横向响度波形,并和原视频、逐句时间轴联动;波形标题栏显示当前播放秒数、总时长和鼠标指针停点秒数。
             改写配音文件GET /jobs/{id}/audio-script.mp3apiAssetUrl(job.audio_script.voice_url)后续新配音阶段保留的 TTS 产物;服务端固定走 VOICE_PROVIDER=azure_openai,通过 AZURE_OPENAI_BASE_URL 的 OpenAI 协议生成 mp3,并按 AZURE_TTS_PATHS 依次尝试 /audio/speech/v1/audio/speech 等路径。当前第一步不默认生成该文件。
@@ -1108,6 +1109,19 @@ ProductRefStateItem {
         

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-19 · 第一步改为导入后直接提文案

+ Workflow + API + UI +
+
+

问题:用户确认“抽帧先不管”,第一步应该是素材输入后自动下载/上传、提取音频文案,并马上在输入区改文案,而不是继续把默认流程带向参考帧。

+

改动:web/app/page.tsx 的开始队列只触发 triggerTranscribe,不再默认调用 analyzeJobMaterialColumn 新增“文案确认稿”,中文改完失焦自动翻译/优化英文并保存。后端新增 PATCH /jobs/{id}/audio-script,前端新增 updateAudioScript

+

影响:用户导入素材后先在左侧完成中文确认稿和英文生成稿;抽帧保留在源视频工作区作为手动参考功能,不再作为第一步默认动作。

+
+

2026-05-19 · 视频候选按音频分镜行隔离

diff --git a/web/app/page.tsx b/web/app/page.tsx index 8b32bb6..d4bf463 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -229,7 +229,7 @@ export default function Home() { const created = await uploadJob(file) addJob(created) setProductionJobIds((prev) => new Set(prev).add(created.id)) - toast.success(`已上传 ${created.id.slice(0, 8)},视频就绪后自动跑音频和抽帧`) + toast.success(`已上传 ${created.id.slice(0, 8)},视频就绪后自动提取音频文案`) } catch (e) { toast.error("上传失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -473,31 +473,13 @@ export default function Home() { try { const updated = await triggerTranscribe(target.id) updateJobInList(updated) - toast.info("音频路已启动:字幕、讲话人、节奏和背景音同步解析") + toast.info("文案路已启动:字幕、讲话人、节奏和背景音同步解析") } catch (e) { autoTriggeredRef.current.delete(audioKey) toast.error("音频解析启动失败:" + (e instanceof Error ? e.message : String(e))) } } - - const visualKey = `${target.id}:visual` - const hasVisualResult = target.frames.length > 0 - const visualRunning = target.status === "splitting" - if (!hasVisualResult && !visualRunning && !autoTriggeredRef.current.has(visualKey)) { - autoTriggeredRef.current.add(visualKey) - const frameTarget = frameTargets[target.id] ?? "motion" - const frameCount = frameCounts[target.id] ?? 12 - const frameQuality = frameQualities[target.id] ?? "accurate" - try { - const updated = await analyzeJob(target.id, frameCount, frameTarget, "replace", frameQuality) - updateJobInList(updated) - toast.info(`视觉路已启动:${FRAME_QUALITY_LABELS[frameQuality]} · ${FRAME_TARGET_LABELS[frameTarget]} · ${frameCount} 张参考帧`) - } catch (e) { - autoTriggeredRef.current.delete(visualKey) - toast.error("视觉抽帧启动失败:" + (e instanceof Error ? e.message : String(e))) - } - } - }, [frameCounts, frameQualities, frameTargets, updateJobInList]) + }, [updateJobInList]) const ensureDefaultProductRefs = useCallback(async (jobId: string) => { const cached = defaultProductRefsByJob[jobId] @@ -577,21 +559,20 @@ export default function Home() { } if (!created && target.status === "failed") { autoTriggeredRef.current.delete(`${target.id}:audio`) - autoTriggeredRef.current.delete(`${target.id}:visual`) } if (!created && target.status === "failed" && !target.video_url) { try { target = await retryJobDownload(target.id) updateJobInList(target) - toast.info("已重新提交下载;下载完成后会自动跑音频文案路和视觉抽帧路") + toast.info("已重新提交下载;下载完成后会自动跑音频文案路") } catch (e) { toast.error("重新下载失败:" + (e instanceof Error ? e.message : String(e))) return } } setProductionJobIds((prev) => new Set(prev).add(target.id)) - if (target.video_url) toast.success("已进入并行素材分析:音频文案路和视觉抽帧路会同步推进") - else toast.success("已进入并行素材分析:下载完成后自动跑音频文案路和视觉抽帧路") + if (target.video_url) toast.success("已进入第一步:自动提取音频文案") + else toast.success("已进入第一步:下载完成后自动提取音频文案") void startProductionLanesForJob(target) }, [handleSubmit, job, startProductionLanesForJob, updateJobInList]) diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index a366991..e776530 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -61,6 +61,7 @@ import { sourceAudioUrl, subjectTemplateImageUrl, updateElement, + updateAudioScript, updateStoryboard, uploadStoryboardAsset, translateText, @@ -1836,13 +1837,6 @@ export function AdRecreationBoard({ } }, []) - const submitUrl = () => { - const trimmed = url.trim() - if (!trimmed) return - data.onSubmitUrl(trimmed) - setUrl("") - } - const startProduction = () => { const trimmed = url.trim() data.onStartProduction?.(trimmed || undefined) @@ -2079,8 +2073,8 @@ export function AdRecreationBoard({ url={url} setUrl={setUrl} fileRef={fileRef} - onSubmitUrl={submitUrl} onStartProduction={startProduction} + runtimeModels={runtimeModels} />
@@ -2090,10 +2084,10 @@ export function AdRecreationBoard({
-

源视频解析与参考帧

+

源视频文案提取

- {statusMessage || "下载源视频后解析音频,再抽参考帧并生成相似主体。"} + {statusMessage || "导入素材后自动下载并提取音频文案;抽帧参考保留为手动工具。"}
@@ -2111,7 +2105,6 @@ export function AdRecreationBoard({ -
@@ -2179,8 +2172,8 @@ function MaterialColumn({ url, setUrl, fileRef, - onSubmitUrl, onStartProduction, + runtimeModels, }: { data: NodeData step: WorkflowStep @@ -2190,12 +2183,12 @@ function MaterialColumn({ url: string setUrl: (value: string) => void fileRef: RefObject - onSubmitUrl: () => void onStartProduction: () => void + runtimeModels?: RuntimeModels }) { const actionLabel = !url.trim() && job?.status === "failed" - ? job.video_url ? "重新解析" : "重新下载" - : "开始分析" + ? job.video_url ? "重新提文案" : "重新下载" + : "导入并提文案" return (
@@ -2204,14 +2197,14 @@ function MaterialColumn({

素材输入

-

一个素材就是一次文件任务

+

导入后自动下载、提取音频并生成可改文案

setUrl(e.target.value)} - onKeyDown={(e) => { if (e.key === "Enter") onSubmitUrl() }} + onKeyDown={(e) => { if (e.key === "Enter") onStartProduction() }} placeholder="粘贴 TK / 信息流视频链接" className="h-10 min-w-0 flex-1 rounded-md border border-white/10 bg-black/45 px-3 text-[13px] text-white outline-none placeholder:text-white/28 focus:border-[#d6b36a]/60" /> @@ -2245,6 +2238,13 @@ function MaterialColumn({ />
+ data.onTranscribeAudio?.(job?.id)} + runtimeModels={runtimeModels} + /> +
{jobs.length ? jobs.map((item, index) => ( data.onDeleteJob?.(item.id) : undefined} /> )) : ( - + )}
) } +function MaterialScriptEditor({ + job, + onJobUpdate, + onTranscribe, + runtimeModels, +}: { + job: Job | null + onJobUpdate: (job: Job) => void + onTranscribe?: () => Promise | void + runtimeModels?: RuntimeModels +}) { + const script = job?.audio_script + const [draftZh, setDraftZh] = useState("") + const [draftEn, setDraftEn] = useState("") + const [saving, setSaving] = useState(false) + const [optimizing, setOptimizing] = useState(false) + const lastOptimizedZh = useRef("") + + useEffect(() => { + const nextZh = script?.rewritten_text_zh?.trim() || script?.source_zh?.trim() || "" + const nextEn = script?.rewritten_text?.trim() || script?.source_text?.trim() || "" + setDraftZh(nextZh) + setDraftEn(nextEn) + lastOptimizedZh.current = nextZh + }, [job?.id, script?.rewritten_text, script?.rewritten_text_zh, script?.source_text, script?.source_zh]) + + const processing = !!job && ( + job.status === "created" || + job.status === "downloading" || + job.status === "transcribing" || + script?.status === "rewriting" + ) + const hasScript = !!(script?.source_text?.trim() || script?.source_zh?.trim() || draftEn.trim() || draftZh.trim()) + const canSave = !!job && !!(draftEn.trim() || draftZh.trim()) + + const saveDraft = async (nextEn = draftEn, nextZh = draftZh, quiet = false) => { + if (!job) return false + setSaving(true) + try { + const updated = await updateAudioScript(job.id, { + rewritten_text: nextEn.trim(), + rewritten_text_zh: nextZh.trim(), + }) + onJobUpdate(updated) + if (!quiet) toast.success("文案已保存到当前素材") + return true + } catch (e) { + toast.error("保存文案失败:" + (e instanceof Error ? e.message : String(e))) + return false + } finally { + setSaving(false) + } + } + + const optimizeChinese = async () => { + if (!job) return + const zh = draftZh.trim() + if (!zh || !containsCjk(zh) || zh === lastOptimizedZh.current) return + setOptimizing(true) + try { + const english = await translateText(zh, "en") + const nextEn = english.trim() || draftEn + setDraftEn(nextEn) + lastOptimizedZh.current = zh + const saved = await saveDraft(nextEn, zh, true) + if (saved) toast.success("中文已同步优化成英文") + } catch (e) { + toast.error("中文优化英文失败:" + (e instanceof Error ? e.message : String(e))) + } finally { + setOptimizing(false) + } + } + + return ( +
+
+ } title="文案确认稿" /> +
+ {runtimeModels ? : null} + +
+
+ + {!job ? ( +
+ 粘贴链接或上传视频后,这里会自动显示原音频文案和可编辑的新广告文案。 +
+ ) : !hasScript ? ( +
+ {processing ? "正在下载 / 提取音频 / 生成文案,完成后会自动出现在这里。" : job.video_url ? "视频已就绪,等待音频文案。可点“重提”手动启动。" : "等待视频下载完成后自动提取音频文案。"} +
+ ) : ( +
+