From 095c6f1c0033dfc17361c28269a250b4b6faa3e1 Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 18 May 2026 18:15:45 +0800 Subject: [PATCH] fix: surface resilient subject asset generation --- .memory/status.md | 99 +++++++++++-------- .memory/worklog.json | 109 ++++++++++----------- .project.json | 10 +- api/README.md | 20 ++-- api/main.py | 21 +++- docs/source-analysis.html | 38 ++++++-- web/components/ad-recreation-board.tsx | 129 ++++++++++++++----------- web/components/dashboard.tsx | 2 +- 8 files changed, 251 insertions(+), 177 deletions(-) diff --git a/.memory/status.md b/.memory/status.md index 7b3dbcf..2f23764 100644 --- a/.memory/status.md +++ b/.memory/status.md @@ -1,7 +1,7 @@ -# SKG TK 二创验证 — 当前状态(2026-05-13) +# SKG TK 二创验证 — 当前状态(2026-05-18) ## 一句话 -SKG AI 素材生产管线第二条思路:TK 链接/上传 → 拆轨 → 抽关键帧(5 张+手动加)→ Vision 识别 → 改写文案 → 生图 → 生视频 → 合成。**MVP 通到生图,剩余 3 个节点占位**。 +SKG 信息流广告快速复刻工作台:TK 链接/上传 → 下载源视频 → 并行音频解析与 12 张动作/节奏参考帧 → 相似主体 / 产品素材池 → 分镜口播改写 → 首尾帧审核 → 视频候选待开放。当前主流程不直接批量提交视频模型。 ## 路径 / 端口 - 路径:`~/Projects/business/20260512-20260512-skg-tk-二创验证/` @@ -15,52 +15,62 @@ key 写在 `api/.env` 的 `LLM_API_KEY` | 端点 / 字段 | 状态 | 用途 | |---|---|---| -| 远端 ASR | `ASR_MODEL=whisper-1` | 失败后进本机 ASR,再进多模态兜底。 | -| 本机 ASR | `LOCAL_ASR_MODEL=mlx-community/whisper-tiny` | 默认二级兜底,优先产出真实逐句时间轴。 | -| ASR 兜底 / 音频分析 | `ASR_FALLBACK_MODEL=gemini-2.5-flash` | 多模态音频兜底;后端会拒绝假字幕、重复文本和覆盖率过低结果。 | -| 字幕翻译 | `TRANSLATE_MODEL=gemini-2.5-flash` | 保留 Gemini。 | -| 画面理解 | `VISION_MODEL=gpt-4o` | 关键帧 Vision 已切 GPT;旧环境若写 `gemini-*` 会自动归一化到 `GPT_TEXT_MODEL`。 | +| 远端 ASR | `ASR_MODEL=whisper-1` | 第一优先级音频转写;失败后进本机 ASR。 | +| 本机 ASR | `LOCAL_ASR_MODEL=mlx-community/whisper-tiny` | 二级兜底,优先产出真实逐句时间轴。 | +| ASR 兜底 / 音频分析 | `ASR_FALLBACK_MODEL=gemini-2.5-flash` | 远端和本机都失败后才做多模态 ASR;音频画像会读取 `audio.wav` + 转写时间轴,失败则本地估算。后端会拒绝假字幕、重复文本和覆盖率过低结果。 | +| 字幕翻译 | `TRANSLATE_MODEL=gemini-2.5-flash` | 按 ASR 段落补中文;失败时保留英文时间轴,中文可为空。 | +| 画面理解 / brief | `VISION_MODEL=gpt-4o` | 关键帧 Vision 和相似主体非身份化 brief 已切 GPT;旧环境若写 `gemini-*` 会自动归一化到 `GPT_TEXT_MODEL`。 | | 通用改写 / 分镜描述 | `REWRITE_MODEL=gpt-4o` | 已切 GPT;旧 Gemini 覆盖值会自动归一化。 | -| 新口播改写 | `AUDIO_REWRITE_MODEL=gpt-4o` | 默认跟随 `REWRITE_MODEL`;旧 Gemini 覆盖值会自动归一化。 | -| 产品视角识别 | `PRODUCT_VIEW_MODEL=gpt-image-2` | 产品图批量识别视角、左右 / 上下 / 内外侧、用途和风险。 | -| 所有生图 / 修图 | `gpt-image-2` | 服务端硬锁,无图片模型 fallback;覆盖关键帧生图、水印清理、元素提取、主体资产包、产品补角度、首尾帧。 | +| 新口播改写 | `AUDIO_REWRITE_MODEL=gpt-4o` | 默认跟随 `REWRITE_MODEL`;失败后依次尝试 `ASR_FALLBACK_MODEL`、`TRANSLATE_MODEL`,再用本地模板兜底。 | +| 产品视角识别 | `PRODUCT_VIEW_MODEL=gpt-image-2` | 多图批量识别;失败后单图重试,再写本地默认视角和风险备注。 | +| 所有生图 / 修图 | `gpt-image-2` | 服务端硬锁,无其他图片模型 fallback;覆盖关键帧生图、水印清理、元素提取、主体资产包、产品补角度、首尾帧。 | | 配音 | `VOICE_PROVIDER=azure_openai` + `AZURE_TTS_MODEL=gpt-4o-mini-tts` | 语音固定 Azure OpenAI TTS;MiniMax 不再作为 fallback。后端会按 `AZURE_TTS_PATHS` 依次尝试路径,便于区分路径错误和整条语音服务不可用。 | -| 视频 | `VIDEO_MODEL=seedance` | 当前主流程暂停直接提交;生产通道默认 `ai.skg.com/doubao`,Seedance 真实 ID 由 `VIDEO_MODEL_SEEDANCE` 配置。 | +| 视频 | `VIDEO_MODEL=seedance`,别名支持 `kling-omni`、`veo-3.1-fast` | 当前主流程暂停直接提交;真实 ID 由 `VIDEO_MODEL_SEEDANCE` / `VIDEO_MODEL_KLING` / `VIDEO_MODEL_VEO3` 配置,入口按 `VIDEO_CREATE_PATHS`。 | **网关后端 = one-hub 多渠道代理**。当前 key 分组叫「纯OpenAI+AWSClaude+Gemini官方」,缺 audio 渠道(`gpt-4o-audio-preview` 503 "无可用渠道")和 video 渠道。 -## 模型选型(已写入 api/.env) +## 模型选型(运行时默认 / 归一化后) ``` -ASR_MODEL=whisper-1 # ⚠️ 端点 404,ASR 还没真跑通 -TRANSLATE_MODEL=gemini-2.5-flash # ✅ text 已通 -REWRITE_MODEL=gemini-2.5-pro # 占位 -VISION_MODEL=gemini-2.5-flash # ✅ 识别已通 -IMAGE_MODEL=gemini-3-pro-image-preview # ✅ nano-banana-pro,i2i 已通 +ASR_MODEL=whisper-1 +LOCAL_ASR_MODEL=mlx-community/whisper-tiny +ASR_FALLBACK_MODEL=gemini-2.5-flash +TRANSLATE_MODEL=gemini-2.5-flash +GPT_TEXT_MODEL=gpt-4o +VISION_MODEL=gpt-4o +REWRITE_MODEL=gpt-4o +AUDIO_REWRITE_MODEL=gpt-4o +IMAGE_MODEL=gpt-image-2 +PRODUCT_VIEW_MODEL=gpt-image-2 +SUBJECT_ASSET_IMAGE_MODEL=gpt-image-2 +VOICE_PROVIDER=azure_openai +AZURE_TTS_MODEL=gpt-4o-mini-tts +VIDEO_MODEL=seedance +VIDEO_MODEL_KLING=kling-omni +VIDEO_MODEL_VEO3=veo-3.1-fast ``` -## Pipeline 状态(8 节点合并版) -原 10 节点已合并:input + download + split 合一;translate 合到 transcript;videogen 和 compose 占位。 +## Pipeline 状态(9 步工作台版) +当前主入口是信息流复刻工作表,不再是旧 ReactFlow 八节点主画布。 | 步 | 节点 | 状态 | 备注 | |---|---|---|---| -| 1 | **输入·Input**(合并下载+拆分) | ✅ | yt-dlp 真下 + ffmpeg 拆 wav | -| 2 | **关键帧·Keyframes** | ✅ | D 启发式:候选 30 张 → pHash 去重 + Laplacian variance 评分 + 时序分桶 → 5 张;手动加帧 OK | -| 3 | **转录·ASR** | ❌ 阻塞 | SKG 网关 audio 不通;待 IT 开 audio 渠道 / 外部 key | -| 4 | **翻译·Translate** | ❌ 阻塞 | 依赖 ASR | -| 5 | **改写·Rewrite** | ⏳ 占位 | 等用户给产品信息模板 | -| 6 | **生图·Image Gen** | ✅ **刚做完** | nano-banana-pro i2i + 正负 prompt | -| 7 | **生视频·Video Gen** | ⏳ 占位 | sora-2 端点不通 | -| 8 | **合成·Compose** | ⏳ 占位 | 本地 ffmpeg + 字幕 + TTS | +| 1 | **素材输入** | ✅ | TK 链接 / 上传视频;失败素材可重新下载。 | +| 2 | **源视频下载** | ✅ | yt-dlp + cookies 配置;上传视频直接进入 downloaded。 | +| 3 | **音频文案** | ✅ | 拆 `audio.wav`,ASR、翻译、讲话人 / 节奏 / 背景音画像。 | +| 4 | **抽帧参考** | ✅ | 下载完成后自动抽 12 张动作/节奏参考帧;支持当前播放点手动补帧。 | +| 5 | **相似主体** | ✅ | GPT 视觉 brief + `gpt-image-2` 文字生图,生成类似但不复刻的人物/透明骨架主体。 | +| 6 | **产品素材池** | ✅ | 不限量上传;`gpt-image-2` 识别视角 / 用途 / 风险,缺角度可补图。 | +| 7 | **分镜文案** | ✅ | 按逐句时间轴生成行,可单段或整片 GPT 改写;保存后写入 storyboard action。 | +| 8 | **画面首尾帧** | ✅ | 用相似主体视图 + 产品素材池生成首帧/尾帧,审核后保存规划。 | +| 9 | **视频候选** | ⏸️ | 历史候选可看;主流程当前暂停直接提交视频模型。 | ## UI 架构(重要) -- **左侧 sidebar**(108px 极窄):8 个 stage tile 竖排 + DAG 路径分叉表达 -- **主区 ReactFlow**:8 节点 DAG(input → keyframe/asr → ... → compose) -- **点 sidebar tile**:从左滑出 drawer panel(粉/紫/橙 Kanban 风格) -- **关键帧 lightbox**:**embedded 嵌入到 keyframe drawer**(不全屏)—— ``,drawer 宽度有 expandedFrame 时 760,无时 400 -- **Input 节点上方**:多视频缩略图浮条 + 「+」加新视频 -- **关键帧节点上方**:5+ 张缩略图按视频原比例(aspect-ratio: width/height) -- **缩略图 hover**:弹大图静态(关键帧是垫图素材,不放视频) -- **缩略图点击**:打开 keyframe drawer 内的 lightbox(左大图 + 右识别面板) +- 主入口:`web/components/ad-recreation-board.tsx`,左侧素材输入列 + 右侧信息流复刻工作表。 +- 工作流条:01 素材输入 → 02 源视频下载 → 03 音频文案 → 04 抽帧参考 → 05 相似主体 → 06 产品素材池 → 07 分镜文案 → 08 画面首尾帧 → 09 视频候选。 +- 源视频工作区:左侧 9:16 原视频播放器,内置当前点抽帧;右侧音频波形 + 逐句时间轴 + 参考帧池。 +- 相似主体:模板库 / 内置形象 / 源视频相似方向;生成结果统一用媒体素材卡,支持 hover 放大、删除、单张重生。 +- 分镜工作台:产品素材池、逐句口播、画面规划、首尾帧和历史视频候选在同一纵向工作表里处理。 +- 旧 ReactFlow 节点、旧 lightbox、旧 storyboard workbench 底层保留,但当前不作为主入口。 ## 数据模型(关键 typescript / pydantic) ```typescript @@ -81,13 +91,15 @@ Job { frames: KeyFrame[] ... } **前端取帧必须用 `frames.find(x => x.index === activeIndex)` 不能用数组下标**(之前的 bug)。 ## 关键文件 -- `web/app/page.tsx` — 多 job state 管理(jobs[] + activeJobId),8 节点 LAYOUT -- `web/components/dashboard.tsx` — sidebar + drawer + 9 个 Kanban section(input/keyframe/asr/translate/rewrite/imagegen/videogen/compose),含 `ImageGenCard` 子组件 -- `web/components/lightbox.tsx` — `FrameLightbox` 支持 `embedded` prop +- `web/app/page.tsx` — 多 job state 管理(jobs[] + activeJobId),开始后并行触发音频解析和 12 张视觉抽帧 +- `web/components/ad-recreation-board.tsx` — 当前主工作台:素材输入、音频结果、参考帧池、相似主体、产品素材池、分镜规划和首尾帧 +- `web/components/media-asset-tile.tsx` — 图片 / 视频 / 抽帧 / 产品图 / 主体图 / 首尾帧 / 视频候选统一媒体卡 +- `web/components/dashboard.tsx` — 旧 ReactFlow / Kanban 面板,底层保留但当前不作为主入口 +- `web/components/lightbox.tsx` — 旧深度素材面板,底层保留 - `web/components/video-lightbox.tsx` — Input 节点点视频缩略图弹的播放器 - `web/components/nodes/index.tsx` — ReactFlow 8 节点定义 - `web/lib/api.ts` — API client -- `api/main.py` — FastAPI 所有端点,KeyFrame/GeneratedImage 模型 +- `api/main.py` — FastAPI 所有端点,Job/KeyFrame/AudioScript/ProductRef/SubjectAsset/SceneAsset/GeneratedVideo 模型 ## 已通的 API 端点 ``` @@ -95,13 +107,19 @@ POST /jobs 创建 job(链接) POST /jobs/{id}/download/retry TK 链接下载失败后重新下载 POST /jobs/upload 上传视频 GET /jobs/{id} job 状态 -POST /jobs/{id}/analyze?frames=5 拆轨+抽帧+ASR 自动一气呵成 +POST /jobs/{id}/transcribe 音频提取 + ASR + 翻译 + 讲话人/节奏/背景音分析 +POST /jobs/{id}/analyze?frames=12 动作/节奏参考帧抽取 POST /jobs/{id}/frames?t= 手动按时间戳加帧 POST /jobs/{id}/frames/{idx}/describe ✅ Vision 识别(3 次重试 + reasoning_content 兜底) POST /jobs/{id}/frames/{idx}/generate ✅ 生图(i2i / text-only, 含 negative_prompt) GET /jobs/{id}/frames/{idx}/gen/{gen_id}.jpg 生成图二进制 POST /jobs/{id}/frames/{idx}/gen/{gen_id}/select 选用某 gen 给下游 +POST /jobs/{id}/assets/product-views/analyze 产品视角 / 用途 / 风险识别 +POST /jobs/{id}/assets/product-angle 缺产品角度补图 +POST /jobs/{id}/script/rewrite 单段 / 整片新口播改写 +POST /jobs/{id}/frames/{idx}/scene-asset 首帧 / 尾帧 / 场景资产生成 GET /jobs/{id}/video.mp4 原视频 +GET /jobs/{id}/audio.wav 原音频 wav GET /jobs/{id}/frames/{idx}.jpg 关键帧 jpg GET /health ``` @@ -118,6 +136,7 @@ GET /health 9. 关键帧 `index` 是稳定 ID,不等于数组下标;前端取帧用 `frames.find(x => x.index === idx)`。 ## 最近变更 +- 2026-05-18:前端模型链路弹窗、`.project.json`、`api/README.md` 和本状态文档已按真实后端链路重写:音频三级 ASR、翻译失败行为、音频画像兜底、产品识别重试、相似主体 GPT brief + gpt-image-2 文字生图、脚本改写本地模板兜底、视频主入口暂停。 - 2026-05-18:清理个人语音通道残留,`/health`、前端类型、环境模板和文档不再暴露相关字段或配置。 - 2026-05-18:`VISION_MODEL`、`REWRITE_MODEL`、`AUDIO_REWRITE_MODEL` 切到 GPT 默认模型 `gpt-4o`,并加旧 Gemini 环境变量归一化保护。 - 2026-05-18:语音通道固定 Azure OpenAI TTS,并按 `AZURE_TTS_PATHS` 尝试语音路径。 diff --git a/.memory/worklog.json b/.memory/worklog.json index f4c1aa7..b71547f 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,61 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "ae81943", - "message": "auto-save 2026-05-16 03:07 (~1)", - "ts": "2026-05-16T03:07:16+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "40064a6", - "message": "auto-save 2026-05-16 03:12 (~1)", - "ts": "2026-05-16T03:13:05+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "38355aa", - "message": "auto-save 2026-05-16 03:18 (~1)", - "ts": "2026-05-16T03:18:56+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "723a564", - "message": "auto-save 2026-05-16 03:24 (~1)", - "ts": "2026-05-16T03:24:46+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "9d37067", - "message": "auto-save 2026-05-16 03:30 (~1)", - "ts": "2026-05-16T03:30:36+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "9f63878", - "message": "auto-save 2026-05-16 03:36 (~1)", - "ts": "2026-05-16T03:36:25+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "2875779", - "message": "auto-save 2026-05-16 03:42 (~1)", - "ts": "2026-05-16T03:42:14+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "6e6fa16", - "message": "auto-save 2026-05-16 03:47 (~1)", - "ts": "2026-05-16T03:48:04+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "c3c7c89", @@ -3232,6 +3176,59 @@ "message": "auto-save 2026-05-18 17:45 (~3)", "hash": "92f04f1", "files_changed": 3 + }, + { + "ts": "2026-05-18T17:50:39+08:00", + "type": "commit", + "message": "chore: record latest worklog", + "hash": "cc4c021", + "files_changed": 1 + }, + { + "ts": "2026-05-18T09:52:18Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:chore: record latest worklog", + "files_changed": 1 + }, + { + "ts": "2026-05-18T17:56:44+08:00", + "type": "commit", + "message": "auto-save 2026-05-18 17:56 (~3)", + "hash": "d8780e5", + "files_changed": 3 + }, + { + "ts": "2026-05-18T18:02:08+08:00", + "type": "commit", + "message": "auto-save 2026-05-18 18:02 (~3)", + "hash": "59d7ec3", + "files_changed": 3 + }, + { + "ts": "2026-05-18T10:02:18Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-18 18:02 (~3)", + "files_changed": 2 + }, + { + "ts": "2026-05-18T18:07:32+08:00", + "type": "commit", + "message": "auto-save 2026-05-18 18:07 (~7)", + "hash": "c1c4106", + "files_changed": 7 + }, + { + "ts": "2026-05-18T10:12:18Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 4 项未提交变更 · 最近提交:auto-save 2026-05-18 18:07 (~7)", + "files_changed": 4 + }, + { + "ts": "2026-05-18T18:12:58+08:00", + "type": "commit", + "message": "auto-save 2026-05-18 18:12 (~4)", + "hash": "ebfc507", + "files_changed": 4 } ] } diff --git a/.project.json b/.project.json index a7867ba..1609950 100644 --- a/.project.json +++ b/.project.json @@ -8,12 +8,6 @@ "storage": "api/.env / deploy/.env.production", "type": "api_key" }, - { - "description": "MiniMax T2A 配音 API Key,本地开发只放 api/.env 的 MINIMAX_API_KEY,不入库", - "name": "MINIMAX_API_KEY", - "storage": "api/.env", - "type": "api_key" - }, { "description": "OpenAI-compatible GPT 图片模型 Key;未单独配置 IMAGE_API_KEY 时复用 LLM_API_KEY,本地开发只放 api/.env,不入库", "name": "IMAGE_API_KEY", @@ -39,7 +33,7 @@ "type": "web_login" } ], - "description": "SKG 信息流广告快速复刻第一步:粘贴 TK 链接或上传视频后点击开始,系统自动下载源视频;下载完成后优先解析原音频,提取原文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效。抽帧、分镜、元素生成和视频合成暂保留为后续能力,不作为当前开始流程的默认动作。", + "description": "SKG 信息流广告快速复刻工作台:粘贴 TK 链接或上传视频后点击开始,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频路提取原文案/字幕、中文翻译、讲话人、语速节奏、背景音乐/环境声/音效;视觉路自动抽 12 张动作/节奏参考帧,供生成相似主体、产品素材池、分镜口播和首尾帧审核。当前主流程暂停直接提交视频模型,先保存规划和首尾帧。", "kind": "app", "name": "SKG Marketing Studio / SKG 营销内容工作台", "ownership": "company", @@ -64,7 +58,7 @@ "username": "skg" }, "stack": [ - "Next.js + Python(yt-dlp/ffmpeg) + OpenAI-compatible LLM + GPT Image + Azure OpenAI TTS + Seedance" + "Next.js + Python(yt-dlp/ffmpeg) + OpenAI-compatible LLM + GPT Image 2 + Azure OpenAI TTS + Seedance/Kling/Veo video gateway" ], "status": "active", "urls": [ diff --git a/api/README.md b/api/README.md index 6c21794..efbe015 100644 --- a/api/README.md +++ b/api/README.md @@ -1,6 +1,6 @@ # SKG TK 二创 API -FastAPI 后端,跑 yt-dlp + ffmpeg + ASR/翻译/英文 SKG 产品介绍文案 + Azure OpenAI 英文配音管线。 +FastAPI 后端,跑 yt-dlp + ffmpeg + ASR/翻译/音频画像、抽帧、GPT 图像生成/修图、Azure OpenAI TTS 预留和视频候选预留管线。 ## 启动 @@ -18,21 +18,23 @@ uvicorn main:app --host 127.0.0.1 --port 4291 ## 路由 - `GET /health` — 健康检查 + 配置状态 -- `POST /jobs` `{url}` — 创建 job,后台下载源视频,视频就绪后可手动解析或提取音频 +- `POST /jobs` `{url}` — 创建 job,后台下载源视频;前端“开始分析”会在视频就绪后自动启动音频解析和视觉抽帧 +- `POST /jobs/{id}/download/retry` — TK 链接下载失败后重试下载;上传视频任务不能重下载 - `GET /jobs/{id}` — 当前状态 + 产物;若原始音轨已拆出,会返回 `source_audio_url` -- `POST /jobs/{id}/transcribe` — 触发音频提取 + ASR + 翻译 + SKG 英文产品介绍文案;文案长度按原音频时长估算,配置 Azure OpenAI TTS 后从 Azure 音色池生成配音。前端 Audio 节点提供“提取音频 / 重新提取音频”按钮,可与抽帧并行,不自动触发 +- `POST /jobs/{id}/transcribe` — 触发音频提取 + ASR + 中文翻译 + 讲话人 / 节奏 / 背景音分析;当前第一步不默认生成 SKG 新口播或 TTS 配音 - `GET /jobs/{id}/video.mp4` — 原视频 -- `GET /jobs/{id}/audio.wav` — 拆轨后的原始音频,供前端底部音频条生成波形 -- `GET /jobs/{id}/audio-script.mp3` — 英文改写文案的 Azure OpenAI TTS 配音 -- `GET /jobs/{id}/frames/{i}.jpg` — 第 i 张关键帧(0-9) +- `GET /jobs/{id}/audio.wav` — 拆轨后的原始音频,供前端音频波形和多模态音频分析使用 +- `GET /jobs/{id}/audio-script.mp3` — 后续新配音阶段保留的 Azure OpenAI TTS 文件 +- `GET /jobs/{id}/frames/{i}.jpg` — 第 i 张参考帧;当前主流程自动抽 12 张动作 / 节奏参考帧,也支持手动按当前播放点补帧 ## Mock 模式 -未设 `LLM_API_KEY` 时,转录走本地 mock,便于 UI 联调;未设 `AZURE_OPENAI_API_KEY` 且无法复用 `LLM_API_KEY` 时只生成改写文案,不生成配音文件。 +未设 `LLM_API_KEY` 时,转录走本地 mock,便于 UI 联调;未设 `AZURE_OPENAI_API_KEY` 时,后续 TTS 文件不会生成,但不影响当前第一步音频解析。 ## 依赖 - `ffmpeg` 系统二进制(拆轨 / 抽帧) - `yt-dlp` 系统二进制(也可走 Python 包) -- OpenAI 兼容 LLM 网关(ASR / 翻译 / 文案改写);如果 `/audio/transcriptions` 不可用,会用 `ASR_FALLBACK_MODEL` 走 Gemini 多模态音频识别 -- Azure OpenAI TTS(英文产品介绍文案配音,使用 `AZURE_OPENAI_API_KEY` 或回退复用 `LLM_API_KEY`;默认音色池 `alloy,verse,shimmer`) +- OpenAI 兼容 LLM 网关(ASR / 翻译 / 文案改写 / 视觉 brief);远端 `whisper-1` 失败后先走本机 `mlx_whisper`,再用 `ASR_FALLBACK_MODEL` 走 Gemini 多模态音频识别,后端会拒绝疑似假字幕或覆盖率过低的时间轴 +- GPT 图片网关(当前所有生图 / 修图 / 产品视角识别 / 主体资产 / 首尾帧都强制使用 `gpt-image-2`,不做其他图片模型 fallback) +- Azure OpenAI TTS(后续新配音阶段使用 `AZURE_OPENAI_API_KEY`;默认模型 `gpt-4o-mini-tts`,按 `AZURE_TTS_PATHS` 依次尝试语音路径) diff --git a/api/main.py b/api/main.py index 4603aa6..0e0b234 100644 --- a/api/main.py +++ b/api/main.py @@ -4505,6 +4505,8 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat ) models = [GPT_IMAGE_MODEL] generated: list[SubjectAsset] = [] + generation_errors: list[str] = [] + first_generation_error: RuntimeError | None = None try: for view, view_label in _subject_view_labels(req.subject_kind, req.views): closeup_view = view in {"bust", "back_detail", "bust_front", "bust_left_45", "bust_right_45", "back_neck_detail"} or "detail" in view @@ -4572,7 +4574,11 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat raise RuntimeError("subject asset edit reference image missing") img_bytes, _mode = _image_edit_call(model_src, prompt, models=models, fallback_text=False, max_attempts=3, max_side=1280) except RuntimeError as e: - raise HTTPException(_image_error_status(e), f"subject asset {view} failed: {e}") + if first_generation_error is None: + first_generation_error = e + generation_errors.append(f"{view_label}: {e}") + print(f"[subject assets] view failed job={job_id} view={view} error={e}", flush=True) + continue asset_id = f"subject_{idx:03d}_{element_id}_{view}_{uuid.uuid4().hex[:8]}" out_path = job_dir(job_id) / "assets" / f"{asset_id}.jpg" @@ -4596,6 +4602,11 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat try: p.unlink() except OSError: pass + if not generated: + if first_generation_error: + raise HTTPException(_image_error_status(first_generation_error), f"subject assets failed: {'; '.join(generation_errors[:3])}") + raise HTTPException(500, "subject assets failed: no images generated") + src = _source_frame_path(job_id, idx) new_frames = [] for f in job.frames: @@ -4614,7 +4625,13 @@ def generate_subject_assets(job_id: str, idx: int, element_id: str, req: Generat current_assets = [asset for asset in current_assets if asset.view not in replaced_views] e.subject_assets = current_assets + generated new_frames.append(f) - update(job, frames=new_frames, message=f"主体资产包生成完成 · {el.name_zh} · {len(generated)} 张") + if generation_errors: + msg = f"主体资产包部分生成完成 · {el.name_zh} · {len(generated)} 张,失败 {len(generation_errors)} 张" + error_msg = ";".join(generation_errors[:3]) + else: + msg = f"主体资产包生成完成 · {el.name_zh} · {len(generated)} 张" + error_msg = "" + update(job, frames=new_frames, message=msg, error=error_msg) return job diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 37e0bcb..c4a0084 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -595,7 +595,7 @@ web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 12 张参考帧,形成“音频文案路 + 视频视觉路”同步推进;底部吸附音频条和旧全局浮动主题按钮不再从主界面渲染,避免和工作台内的明暗模式切换重复。 web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部由 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;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,先按人物描述、镜头类型、首尾状态和产品佩戴需求,从相似主体 6/10 视图里自动挑选最多 5 张最相关主体视角,再传入 subject_images 和该行自动挑选的产品图 product_images;关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型。只有该行勾选“人物”时,才会传按需筛选后的相似主体参考图;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 AdRecreationBoard 主题切换顶部指标区左侧有“明亮/暗色”按钮,使用 Sun / Moon 图标切换 skg-board-theme--light 类名,并把选择写入 localStorage["skg-board-theme"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 - SourceReferenceBuildPanel“相似主体 / 主体模板”当前承担主体资产生成和主体模板复用的前端入口:顶部用 radio 区分“用模板生成”和“不用模板(从源视频关键帧创新)”,源视频相似 不再作为模板卡混进网格。模板库把 GET /subject-templates 数据库模板和 GET /character-library/skg 内置形象合并成 120px 竖排卡片,选中态统一用 cyan;保存为主体模板的名称、备注和按钮固定在模板区底部一行。下方“生成主体视图”独立显示 gpt-image-2 链路,支持透明骨架/真人、全部 10 / 常用 4 / 自定义视图,并把生成结果缩略图放大为可单张重生、删除和 hover 放大的媒体卡。前端仍传 reconstruction_mode=similar,但后端会先把关键帧/模板转成文字 brief,再走文字生图,不再把参考图作为强 image-edit 锚点。 + SourceReferenceBuildPanel“相似主体 / 主体模板”当前承担主体资产生成和主体模板复用的前端入口:顶部用 radio 区分“用模板生成”和“不用模板(从源视频关键帧创新)”,源视频相似 不再作为模板卡混进网格。模板库把 GET /subject-templates 数据库模板和 GET /character-library/skg 内置形象合并成 120px 竖排卡片,选中态统一用 cyan;当选择“不用模板”时模板网格会收起,避免把生成按钮和结果缩略图挤到折叠区域之外。保存为主体模板的名称、备注和按钮固定在模板区底部一行。下方“生成主体视图”独立显示模型链路,支持透明骨架/真人、全部 10 / 常用 4 / 自定义视图;已有生成结果会优先显示在生成区标题下方,再显示控制项,避免用户生成后还要继续向下找图。主体缩略图放大为可单张重生、删除和 hover 放大的媒体卡;生成中会显示本次请求锁定的素材 ID,切换其他模块不会改变已经提交的生成目标。前端仍传 reconstruction_mode=similar,后端先用 VISION_MODEL 把关键帧/模板图转成非身份化文字 brief;如果 brief 失败,则继续用用户方向、模板文字或内置形象 brief。最终主体图只走 gpt-image-2/images/generations 文字生图,不再把原帧或模板图作为强 image-edit 锚点。 web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。 web/app/login/page.tsx生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 web/app/login/layout.tsx登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 /login 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。 @@ -644,7 +644,7 @@ web/app/page.tsx 后端主链路: api/main.py -> Job / KeyFrame / KeyElement / StoryboardScene / AudioScript - -> 下载 / 上传 / 音频提取 / ASR / 翻译 / 声音背景音分析 / 抽帧 / Vision / 清洗 / 元素提取 / 分镜保存 / 后续音频改写与 Azure OpenAI 英文配音 + -> 下载 / 上传 / 音频提取 / ASR / 翻译 / 声音背景音分析 / 抽帧 / Vision brief / GPT 图像生成 / 产品视角识别 / 分镜保存 / 首尾帧生成 / 后续 Azure OpenAI 配音预留 -> jobs/<jobId>/state.json + 图片文件落盘 @@ -903,9 +903,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 支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前第一步主流程不自动调用该接口;原版视频旁的“抽参考 12 帧”会显式用 target=motionquality=accuratemode=replace 重新生成全局动作/节奏参考帧池。 - 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;随后用 ASR 提取原始文案,翻译成中文,写入 audio_script.source_textsource_zh 和逐句 transcript。远端 ASR_MODEL 失败后先走本机 LOCAL_ASR_BIN/LOCAL_ASR_MODEL(默认 mlx_whisper),再尝试 ASR_FALLBACK_MODEL。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。再用 ASR_FALLBACK_MODEL 多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profilerhythm_profilebackground_audio_profile。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 - 分镜脚本改写POST /jobs/{id}/script/rewriterewriteStoryboardScript根据原参考文案、当前新口播、分镜角色、时间段和作者想法改写中文口播。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。接口只返回 items[index,text],前端暂存在当前页面状态里,保存规划或生成首尾帧时写入 StoryboardScene.action。 + 解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob抽参考帧能力。当前开始流程会在视频下载完成后自动调用一次,默认 frames=12target=motionquality=accuratemode=replace,形成全局动作/节奏参考帧池;原版视频旁的“抽参考 12 帧”也会用同一参数显式重跑。target 仍支持透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。 + 音频文案轨POST /jobs/{id}/transcribetriggerTranscribe若尚未拆轨,先从 source.mp4 提取 audio.wav 并回填 source_audio_url;随后用 ASR 提取原始文案,翻译成中文,写入 audio_script.source_textsource_zh 和逐句 transcript。远端 ASR_MODEL 失败后先走本机 LOCAL_ASR_BIN/LOCAL_ASR_MODEL(默认 mlx_whisper),再尝试 ASR_FALLBACK_MODEL。后端会拒绝重复文本、逐秒假字幕或覆盖率过低的结果,不再把不可听的多模态输出写进时间轴。中文翻译由 TRANSLATE_MODEL 按 ASR 段落补齐,失败时保留原文时间轴且中文可为空。再用 ASR_FALLBACK_MODEL 读取 audio.wav 和已有转写时间轴,多模态音频分析讲话人、语速节奏、停顿、背景音乐/环境声/音效,写入 speaker_profilerhythm_profilebackground_audio_profile;若模型分析失败,则用转写段落、时长和语速做本地估算兜底。当前第一步不默认生成 SKG 新口播和 Azure OpenAI 配音。 + 分镜脚本改写POST /jobs/{id}/script/rewriterewriteStoryboardScript根据原参考文案、当前新口播、分镜角色、时间段和作者想法改写中文口播。mode=segment 只改一段;mode=all 一次改完整片,要求整片前后连贯。后端按 AUDIO_REWRITE_MODELASR_FALLBACK_MODELTRANSLATE_MODEL 依次尝试,全部失败时用本地模板保留可编辑文案。接口只返回 items[index,text],前端暂存在当前页面状态里,点击保存规划后写入 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 等路径。当前第一步不默认生成该文件。 手动加帧POST /jobs/{id}/frames?t=addManualFrame按视频时间戳抽一帧,index 递增但 frames 按 timestamp 排序。当前主界面会把原版视频播放器的播放秒数传给 AudioIntakePanel 标题栏右侧的“当前点抽帧”。 @@ -916,12 +916,12 @@ ProductRefStateItem { 应用清洗POST /cleanup/applyapplyCleanedFrame物理覆盖 frames/{idx}.jpg,并备份原图。 元素增改删POST/PATCH/DELETE /elementsaddElement/updateElement/deleteElement让用户修正 Vision 错误,避免候选结果锁死。 元素提取POST /elements/{element_id}/cutoutcutoutElement调用图像模型生成独立白底素材图,每次累积一张 cutout。 - 主体资产包POST /elements/{element_id}/subject-assets
DELETE /elements/{element_id}/subject-assets/{asset_id}generateSubjectAssets
deleteSubjectAsset根据参考帧、可选内置形象或数据库主体模板重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,也可传 character_id 选择 5 套内置透明骨架形象之一,或传 subject_template_id 使用已保存的主体模板。当前源视频工作区支持 subject_style=transparent_humansource_actor 两种相似主体。reconstruction_mode=similar 是创新路径:后端先用 VISION_MODEL 把关键帧、内置形象或数据库模板反推成非身份化文字 brief,再调用 gpt-image-2/images/generations 文字生图,日志会显示 image_refs=0;不再把 10 张同一人物实拍图上传给 /images/editsreconstruction_mode=same 仍保留旧 image-edit 路径,用于确实需要精确复刻且有授权的场景。生成视图可由前端传 views 控制:全部 10、常用 4 或自定义;每个 view 单独调用一次生图,明确禁止多视图拼图、contact sheet、多主体、多面板、标签或对比排版。replace_views=true 时会替换同一视角旧图;删除接口会移除对应 subject asset 记录并删除本地 jpg 文件。 + 主体资产包POST /elements/{element_id}/subject-assets
DELETE /elements/{element_id}/subject-assets/{asset_id}generateSubjectAssets
deleteSubjectAsset根据参考帧、可选内置形象或数据库主体模板重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,也可传 character_id 选择 5 套内置透明骨架形象之一,或传 subject_template_id 使用已保存的主体模板。当前源视频工作区支持 subject_style=transparent_humansource_actor 两种相似主体。reconstruction_mode=similar 是创新路径:后端先用 VISION_MODEL 把关键帧、内置形象或数据库模板反推成非身份化文字 brief,再调用 gpt-image-2/images/generations 文字生图,日志会显示 image_refs=0;不再把 10 张同一人物实拍图上传给 /images/editsreconstruction_mode=same 仍保留旧 image-edit 路径,用于确实需要精确复刻且有授权的场景。生成视图可由前端传 views 控制:全部 10、常用 4 或自定义;每个 view 单独调用一次生图,明确禁止多视图拼图、contact sheet、多主体、多面板、标签或对比排版。后端不再要求整包全成功才写入:单个视图失败时会保留已成功生成的主体图,返回“部分生成完成”,只有一张都没生成出来才返回错误。replace_views=true 时会替换同一视角旧图;删除接口会移除对应 subject asset 记录并删除本地 jpg 文件。 首尾帧资产POST /frames/{idx}/scene-assetgenerateSceneAsset同一接口兼容旧场景图和新首尾帧;当前信息流复刻流程传 asset_role=first_frame/last_framesubject_imagesproduct_images。后端优先把相似主体白底视图与产品素材拼成 asset contact sheet 给 gpt-image-2 做图像编辑,关键帧只作为行数据承载位置。生成结果保存在 scene_assets,前端再写入 StoryboardScene.first_image/last_image。 产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。 产品图入库到 jobPOST /jobs/{id}/assetsPOST /jobs/{id}/assets/product-libraryuploadStoryboardAssetcopyProductLibraryAsset上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 ImageRef.asset_meta 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 PUT /jobs/{id}/product-refs 持久化。 产品素材池保存PUT /jobs/{id}/product-refssaveProductRefs把当前 job 的产品素材池列表、识别视角、用途标签、方向、结构点、备注、AI 补图和删除结果保存到 Job.product_refs / state.json。前端上传、识别完成、补角度、编辑备注和删除时都会同步保存;刷新页面或热更新后从 job 恢复,不再要求重新上传和重新识别。 - 产品视角识别POST /jobs/{id}/assets/product-views/analyzeanalyzeProductViews读取同一产品素材池,按批次把多张图一次性提交给 PRODUCT_VIEW_MODEL=gpt-image-2 做视角标注,不限制只看前 6 张;识别对象被固定为套在脖子上的 U 形肩颈按摩仪。返回 viewbackgrounduse_tagsorientationlandmarks、中文备注、生成风险和置信度;orientation 明确佩戴者左/右、上/下、内外侧和开口方向对应图中哪边,避免把图片左右误当产品左右。前端不再要求用户手动选择视角,也不做不同产品身份判断。 + 产品视角识别POST /jobs/{id}/assets/product-views/analyzeanalyzeProductViews读取同一产品素材池,按批次把多张图一次性提交给 PRODUCT_VIEW_MODEL=gpt-image-2 做视角标注,不限制只看前 6 张;识别对象被固定为套在脖子上的 U 形肩颈按摩仪。返回 viewbackgrounduse_tagsorientationlandmarks、中文备注、生成风险和置信度;orientation 明确佩戴者左/右、上/下、内外侧和开口方向对应图中哪边,避免把图片左右误当产品左右。批量识别失败会按单图重试,仍失败或文件缺失时写入本地默认视角,并在 risk/note 标明兜底原因。前端不再要求用户手动选择视角,也不做不同产品身份判断。 产品缺角度补图POST /jobs/{id}/assets/product-anglegenerateProductAngleAsset用当前同一产品素材池作为参考,通过 gpt-image-2 自动补全缺失视角,输出新的 ImageRef(kind="asset")。前端不再固定传第一张图,而是按目标视角给已上传/已标注参考图打分,优先选择真实上传图、目标相邻视角、侧厚/触点/底部对应用途标签和低风险高置信图,最多传 6 张;后端通过 /images/edits multipart 的多张 image[] 直接提交给 gpt-image-2,不再把参考图拼成一张板,降低模型误解成拼图/多产品的概率。Prompt 会约束白底产品图、左右非对称、厚度、内侧触点和肩颈真实佩戴比例,并禁止输出拼图/多产品;遇到上游 429 / saturated 会按退避节奏重试,最终仍失败时返回 503 和可读提示;遇到 DNS / ConnectError 也返回 503,并提示配置 AI_HTTP_PROXY / IMAGE_HTTP_PROXY。 角色库GET /character-library/skglistCharacterLibrary读取内置 5 个透明骨架人角色 manifest,每个角色含正面、左右 45 度、侧面、背面、半身近景和背部特写 7 张参考图,以及用于相似主体文字生图的 prompt_brief。 主体模板库GET /subject-templates
GET /subject-templates/images/{filename}
POST /jobs/{id}/subject-templateslistSubjectTemplates
subjectTemplateImageUrl
saveSubjectTemplate数据库化可复用主体库。前端模板库展示这里保存的主体模板;“保存为主体模板”会把当前 job 的相似主体白底视图按名称、备注、主体类型、原 job/frame/element 和 asset 列表复制到 JOBS_DIR/_subject_templates,并由后端用 Vision LLM 从这些图反推 prompt_brief。以后相似生成通过 subject_template_id 读取这个 brief 作为文字创意方向,不再把模板图直接上传给 image-edit。 @@ -1033,6 +1033,30 @@ ProductRefStateItem {

变更记录

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

+
+
+

2026-05-18 · 相似主体生成结果可见性修复

+ API + UI +
+
+

问题:相似主体默认一次生成 10 张,旧后端只在整包全部成功后才写入 subject_assets;只要其中某个视角失败,前面已生成的图也不会显示。前端默认“不用模板”时仍渲染整块模板网格,也会把生成按钮和结果区域压到可视区域下方。

+

改动:generate_subject_assets 改为逐视角容错:单张失败时继续生成下一张,并把已成功的主体图写回 state,返回“部分生成完成”;只有一张都没成功才返回错误。SourceReferenceBuildPanel 在“不用模板”模式收起模板网格,已有主体缩略图优先显示在生成区顶部,并在生成中显示锁定的素材 ID,明确切换其他模块不会改变本次请求。

+

影响:用户不再因为一个视角失败而看到空结果;默认源视频创新路径下,生成按钮和主体缩略图更容易在当前屏幕内看到。

+
+
+
+
+

2026-05-18 · 模型链路描述对齐真实后端

+ Model + Docs +
+
+

问题:前端模型弹窗和状态文档对音频、产品、相似主体、脚本改写和视频入口的描述过于简化,容易误解为所有模型调用都会成功、都会上传参考图或会直接提交视频。

+

改动:ModelTrace 文案改成真实链路:ASR 是远端 whisper、本机 mlx_whisper、Gemini 多模态三级;翻译失败会保留原文时间轴;音频画像使用 audio.wav + 转写时间轴并有本地估算兜底;产品识别批量失败会单图重试再写本地默认视角;相似主体先用 GPT 视觉 brief,再走 gpt-image-2 文字生图;脚本改写全部模型失败后用本地模板;视频入口当前主工作台暂停直接提交。

+

影响:web/components/ad-recreation-board.tsxweb/components/dashboard.tsxapi/README.md.project.json.memory/status.md 和本页同步按实际后端行为描述,后续排查模型问题时优先看弹窗里的“兜底/失败行为”。

+
+

2026-05-18 · 删除个人语音通道残留

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 7709a50..b6d81ee 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -38,6 +38,7 @@ import { generateProductAngleAsset, generateSubjectAssets, generatedImageUrl, + getJob, getRuntimeHealth, hasCutout, listCharacterLibrary, @@ -570,9 +571,9 @@ function audioModelTrace(models?: RuntimeModels): ModelTraceSpec { title: "音频解析", model: modelList([models?.asr, models?.translate, models?.asr_fallback]), chain: [ - `ASR 转写:优先 ${modelValue(models?.asr)},失败后尝试本机 ${modelValue(models?.local_asr)},再回退 ${modelValue(models?.asr_fallback)}`, - `字幕翻译:${modelValue(models?.translate)} 输出中文逐句时间轴`, - `讲话人 / 节奏 / 背景音:${modelValue(models?.asr_fallback)} 读取 audio.wav 做多模态音频分析`, + `ASR 转写:优先 ${modelValue(models?.asr)};失败后尝试本机 ${modelValue(models?.local_asr)};仍失败才回退 ${modelValue(models?.asr_fallback)},并拒绝假字幕/重复时间轴`, + `字幕翻译:${modelValue(models?.translate)} 按 ASR 段落输出中文;失败时保留原文时间轴,中文可为空`, + `讲话人 / 节奏 / 背景音:${modelValue(models?.asr_fallback)} 读取 audio.wav + 转写时间轴做多模态分析;失败时用本地时长/段落估算兜底`, ], note: "点击“解析音频”后触发;开始任务下载完成后也会自动走这条链路。", } @@ -583,9 +584,10 @@ function productModelTrace(models?: RuntimeModels): ModelTraceSpec { title: "产品视角识别 / 补图", model: modelList([models?.product_view, models?.image]), chain: [ - `批量视角识别:${modelValue(models?.product_view)} 一次读取同一产品多张图,标注视角、左右、上下、用途和风险`, - `缺角度补图:${imageModelChain(models)} 读取最相关的多张已上传参考图,按同一肩颈按摩仪结构补齐缺失视角`, - "前端只保存标注和 AI 补图结果;后续生成视频时每条最多挑 6 张相关产品图", + `批量视角识别:${modelValue(models?.product_view)} 多图读取同一产品素材,标注视角、佩戴者左右、上下、内外侧、用途和风险`, + "识别兜底:批量失败会按单图重试;仍失败或文件缺失时写入本地默认视角,并在 risk/note 标明兜底原因", + `缺角度补图:${imageModelChain(models)} 走 /images/edits,最多读取 6 张已上传参考图补齐缺失视角;失败保留重试入口,不自动换模型`, + "前端只保存标注和 AI 补图结果;后续首尾帧/视频规划每条最多挑 6 张相关产品图", ], note: "上传产品图、重新识别、缺视角重试都会使用这组模型链路。", } @@ -594,11 +596,11 @@ function productModelTrace(models?: RuntimeModels): ModelTraceSpec { function similarSubjectModelTrace(models: RuntimeModels | undefined, subjectStyle: SubjectStyleMode): ModelTraceSpec { return { title: subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似普通真人主体", - model: subjectImageModelChain(models), + model: modelList([models?.vision, models?.subject_image]), chain: [ - "参考策略:先用视觉模型把关键帧/模板转成非身份化文字 brief,生图请求不再上传参考图", + `视觉 brief:${modelValue(models?.vision)} 把关键帧/模板图转成非身份化文字 brief;失败时继续用用户方向和模板文字`, `主体类型:${subjectStyle === "transparent_human" ? "透明/半透明皮肤包裹可见白色骨架" : "普通商业广告真人"}`, - `图像生成:${subjectImageModelChain(models)} 走 /images/generations 逐张生成高清图,视图数量由“全部/常用/自定义”决定`, + `图像生成:${subjectImageModelChain(models)} 走 /images/generations 逐张文字生图;当前 similar 模式不上传原帧或模板图作为 image-edit 参考`, "身份锁定:整套图必须是同一个主体,性别表现、年龄段、体型、材质和风格保持一致", ], note: "这是生成类似但创新的主体,不是复制、抠出或复刻源视频人物身份;内置形象也只作为方向参考。", @@ -611,8 +613,8 @@ function scriptRewriteModelTrace(models?: RuntimeModels): ModelTraceSpec { model: modelList([models?.audio_rewrite, models?.asr_fallback, models?.translate]), chain: [ `主改写:${modelValue(models?.audio_rewrite)} 根据原文案、当前分镜、作者想法生成新口播`, - `失败回退:依次尝试 ${modelValue(models?.asr_fallback)} 和 ${modelValue(models?.translate)}`, - "返回结果只写入当前分镜文案编辑框;生成视频时再把当前文案写入分镜 action", + `模型回退:依次尝试 ${modelValue(models?.asr_fallback)} 和 ${modelValue(models?.translate)};全部失败时用本地模板保留分镜可编辑`, + "返回结果只写入当前分镜文案编辑框;点击保存规划后才写入 frame.storyboard.action", ], } } @@ -625,8 +627,9 @@ function videoModelTrace(models: RuntimeModels | undefined, model: string): Mode `前端选择:${model}`, `后端解析:${resolveVideoModelLabel(models, model)}`, `服务商:${modelValue(models?.video_provider)} · ${modelValue(models?.video_base_url)}`, - "输入:已确认的首尾帧、当前分镜文案、产品素材、相似主体资产和画面规划", - "输出:异步候选视频,完成后回填到对应分镜行", + "当前主工作台暂停直接提交视频;旧入口误触也会被页面层保护", + "开放后输入会包含已确认首尾帧、当前分镜文案、产品素材、相似主体资产和画面规划", + "输出为异步候选视频,完成后回填到对应分镜行;Sora 已停用", ], } } @@ -2144,7 +2147,7 @@ function SourceReferenceBuildPanel({ onJobUpdate: (job: Job) => void runtimeModels?: RuntimeModels }) { - const [subjectBusy, setSubjectBusy] = useState(false) + const [subjectBusyFor, setSubjectBusyFor] = useState<{ jobId: string; jobLabel: string; viewCount: number } | null>(null) const [subjectAssetBusy, setSubjectAssetBusy] = useState(null) const [subjectMode, setSubjectMode] = useState("source_similar") const [subjectStyle, setSubjectStyle] = useState("transparent_human") @@ -2220,6 +2223,7 @@ function SourceReferenceBuildPanel({ ? `${selectedCharacter.name} · 模板参考` : "源视频关键帧 · 相似创新" const templateRequired = subjectMode === "template" && !selectedSubjectTemplate && !selectedCharacter + const subjectBusy = !!subjectBusyFor const generationCtaLabel = subjectMode === "template" ? `用模板生成 ${selectedSubjectViews.length} 张主体视图` : `从源视频创新生成 ${selectedSubjectViews.length} 张主体视图` @@ -2265,13 +2269,14 @@ function SourceReferenceBuildPanel({ } const baseFrame = subjectReferenceFrames[0] if (!baseFrame) return - setSubjectBusy(true) + const requestJobId = job.id + setSubjectBusyFor({ jobId: requestJobId, jobLabel: shortId(requestJobId), viewCount: selectedSubjectViews.length }) try { let workingJob = job let workingFrame = workingJob.frames.find((frame) => frame.index === baseFrame.index) ?? baseFrame let element = workingFrame.elements?.find(isSimilarActorElement) if (!element) { - workingJob = await addElement(job.id, baseFrame.index, { + workingJob = await addElement(requestJobId, baseFrame.index, { name_zh: selectedTemplatePrompt ? `相似透明骨架主体 · ${selectedTemplatePrompt.name}` : subjectStyle === "transparent_human" ? "相似透明骨架主体" : "相似广告主角", @@ -2288,7 +2293,7 @@ function SourceReferenceBuildPanel({ } if (!element) throw new Error("similar subject element missing") - const updated = await generateSubjectAssets(job.id, baseFrame.index, element.id, { + const updated = await generateSubjectAssets(requestJobId, baseFrame.index, element.id, { subject_kind: "living", subject_style: subjectStyle, reconstruction_mode: "similar", @@ -2304,9 +2309,12 @@ function SourceReferenceBuildPanel({ onJobUpdate(updated) toast.success(`相似主体 ${selectedSubjectViews.length} 张高清白底图已生成`) } catch (e) { + try { + onJobUpdate(await getJob(requestJobId)) + } catch { /* keep original error visible */ } toast.error("相似主体重构失败:" + (e instanceof Error ? e.message : String(e))) } finally { - setSubjectBusy(false) + setSubjectBusyFor(null) } } @@ -2438,7 +2446,8 @@ function SourceReferenceBuildPanel({ ))}
-
+ {subjectMode === "template" ? ( +
{subjectTemplateLibrary.map((template) => { const preview = characterPreviewImage(template) @@ -2494,6 +2503,11 @@ function SourceReferenceBuildPanel({ })}
+ ) : ( +
+ 当前跳过模板库:本次只用源视频关键帧的文字化主体特征生成创新主体。模板卡已收起,避免占用生成结果区域。 +
+ )} {subjectMode === "template" && (selectedSubjectTemplate?.images?.length || selectedCharacter?.images?.length) ? (
@@ -2545,6 +2559,45 @@ function SourceReferenceBuildPanel({
+ {subjectBusyFor ? ( +
+ 正在为素材 {subjectBusyFor.jobLabel} 生成 {subjectBusyFor.viewCount} 张主体视图;本次请求已锁定素材、参考帧、模式和视图数量,切换其他模块不会改变生成目标,完成后会回写到该素材。 +
+ ) : null} + + {visibleActorAssets.length ? ( +
+ {visibleActorAssets.map((asset) => { + const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : "" + return ( + , + tone: "cyan", + busy: busyMode === "regen", + disabled: !!subjectAssetBusy, + onClick: () => void regenerateSubjectAsset(asset), + }]} + onDelete={() => void deleteActorAsset(asset)} + deleting={busyMode === "delete"} + deleteDisabled={!!subjectAssetBusy} + deleteLabel="删除这一张" + /> + ) + })} +
+ ) : null}
@@ -2595,7 +2648,7 @@ function SourceReferenceBuildPanel({ className="inline-flex h-9 min-w-[170px] items-center justify-center gap-1 rounded-md bg-white px-3 text-[11px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40" > {subjectBusy ? : } - {generationCtaLabel} + {subjectBusyFor ? `生成中 · ${subjectBusyFor.jobLabel}` : generationCtaLabel}
@@ -2623,45 +2676,13 @@ function SourceReferenceBuildPanel({
) : null} - {visibleActorAssets.length ? ( -
- {visibleActorAssets.map((asset) => { - const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : "" - return ( - , - tone: "cyan", - busy: busyMode === "regen", - disabled: !!subjectAssetBusy, - onClick: () => void regenerateSubjectAsset(asset), - }]} - onDelete={() => void deleteActorAsset(asset)} - deleting={busyMode === "delete"} - deleteDisabled={!!subjectAssetBusy} - deleteLabel="删除这一张" - /> - ) - })} -
- ) : ( + {!visibleActorAssets.length ? (
{subjectMode === "template" ? "先选主体模板,再生成新主体视图;模板只作为文字化创意方向,不再作为强参考图复制。" : "直接使用关键帧的文字化主体特征生成创新主体;后端不会上传源图给生图端点。"}
- )} + ) : null} diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index 792f4d1..1efd8c5 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -673,7 +673,7 @@ export const Dashboard = forwardRef(function Dashboard({ {key === "videogen" && ( <> -
通过 /v1/videos 网关提交,模型 ID 走环境变量映射
+
按后端 VIDEO_CREATE_PATHS 提交,模型 ID 走环境变量映射
字节跳动 · 需独立 API key