fix: surface resilient subject asset generation
This commit is contained in:
@@ -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**(不全屏)—— `<FrameLightbox embedded ... />`,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=<sec> 手动按时间戳加帧
|
||||
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` 尝试语音路径。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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` 依次尝试语音路径)
|
||||
|
||||
21
api/main.py
21
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
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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<string | null>(null)
|
||||
const [subjectMode, setSubjectMode] = useState<SubjectMode>("source_similar")
|
||||
const [subjectStyle, setSubjectStyle] = useState<SubjectStyleMode>("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({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={`transition ${subjectMode === "source_similar" ? "pointer-events-none opacity-38 grayscale" : ""}`}>
|
||||
{subjectMode === "template" ? (
|
||||
<div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-2">
|
||||
{subjectTemplateLibrary.map((template) => {
|
||||
const preview = characterPreviewImage(template)
|
||||
@@ -2494,6 +2503,11 @@ function SourceReferenceBuildPanel({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-cyan-200/18 bg-cyan-300/[0.06] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/62">
|
||||
当前跳过模板库:本次只用源视频关键帧的文字化主体特征生成创新主体。模板卡已收起,避免占用生成结果区域。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subjectMode === "template" && (selectedSubjectTemplate?.images?.length || selectedCharacter?.images?.length) ? (
|
||||
<div className="mt-2 flex gap-1.5 overflow-x-auto pb-0.5">
|
||||
@@ -2545,6 +2559,45 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
<ModelTrace trace={similarSubjectModelTrace(runtimeModels, subjectStyle)} compact />
|
||||
</div>
|
||||
{subjectBusyFor ? (
|
||||
<div className="mb-2 rounded-md border border-cyan-200/20 bg-cyan-300/[0.07] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/70">
|
||||
正在为素材 {subjectBusyFor.jobLabel} 生成 {subjectBusyFor.viewCount} 张主体视图;本次请求已锁定素材、参考帧、模式和视图数量,切换其他模块不会改变生成目标,完成后会回写到该素材。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleActorAssets.length ? (
|
||||
<div className="mb-2 grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2">
|
||||
{visibleActorAssets.map((asset) => {
|
||||
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||
return (
|
||||
<MediaAssetTile
|
||||
key={asset.id}
|
||||
src={subjectAssetUrl(job, asset)}
|
||||
href={subjectAssetUrl(job, asset)}
|
||||
alt={asset.label || asset.view}
|
||||
label={asset.label || asset.view || "主体视图预览"}
|
||||
meta={asset.width && asset.height ? `${asset.width}x${asset.height}` : undefined}
|
||||
className="aspect-[9/16] w-20 bg-white 2xl:w-24"
|
||||
objectFit="contain"
|
||||
title={asset.label || asset.view}
|
||||
actions={[{
|
||||
key: "regen",
|
||||
label: "重新生成这一张",
|
||||
icon: <RefreshCw className="h-3 w-3" />,
|
||||
tone: "cyan",
|
||||
busy: busyMode === "regen",
|
||||
disabled: !!subjectAssetBusy,
|
||||
onClick: () => void regenerateSubjectAsset(asset),
|
||||
}]}
|
||||
onDelete={() => void deleteActorAsset(asset)}
|
||||
deleting={busyMode === "delete"}
|
||||
deleteDisabled={!!subjectAssetBusy}
|
||||
deleteLabel="删除这一张"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[auto_auto_minmax(220px,1fr)_auto] xl:items-start">
|
||||
<div className="flex rounded-md border border-white/10 bg-black/28 p-0.5">
|
||||
@@ -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 ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
|
||||
{generationCtaLabel}
|
||||
{subjectBusyFor ? `生成中 · ${subjectBusyFor.jobLabel}` : generationCtaLabel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2623,45 +2676,13 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleActorAssets.length ? (
|
||||
<div className="mt-2 grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2">
|
||||
{visibleActorAssets.map((asset) => {
|
||||
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||
return (
|
||||
<MediaAssetTile
|
||||
key={asset.id}
|
||||
src={subjectAssetUrl(job, asset)}
|
||||
href={subjectAssetUrl(job, asset)}
|
||||
alt={asset.label || asset.view}
|
||||
label={asset.label || asset.view || "主体视图预览"}
|
||||
meta={asset.width && asset.height ? `${asset.width}x${asset.height}` : undefined}
|
||||
className="aspect-[9/16] w-20 bg-white 2xl:w-24"
|
||||
objectFit="contain"
|
||||
title={asset.label || asset.view}
|
||||
actions={[{
|
||||
key: "regen",
|
||||
label: "重新生成这一张",
|
||||
icon: <RefreshCw className="h-3 w-3" />,
|
||||
tone: "cyan",
|
||||
busy: busyMode === "regen",
|
||||
disabled: !!subjectAssetBusy,
|
||||
onClick: () => void regenerateSubjectAsset(asset),
|
||||
}]}
|
||||
onDelete={() => void deleteActorAsset(asset)}
|
||||
deleting={busyMode === "delete"}
|
||||
deleteDisabled={!!subjectAssetBusy}
|
||||
deleteLabel="删除这一张"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
{!visibleActorAssets.length ? (
|
||||
<div className="mt-2 rounded border border-dashed border-white/12 px-2 py-2 text-[10.5px] leading-snug text-white/32">
|
||||
{subjectMode === "template"
|
||||
? "先选主体模板,再生成新主体视图;模板只作为文字化创意方向,不再作为强参考图复制。"
|
||||
: "直接使用关键帧的文字化主体特征生成创新主体;后端不会上传源图给生图端点。"}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -673,7 +673,7 @@ export const Dashboard = forwardRef<DashboardHandle, Props>(function Dashboard({
|
||||
{key === "videogen" && (
|
||||
<>
|
||||
<KanbanCard tone="violet" tags={["SKG 网关"]} title="Seedance / Kling / Veo 3">
|
||||
<div className="text-[11px] text-[var(--text-soft)]">通过 /v1/videos 网关提交,模型 ID 走环境变量映射</div>
|
||||
<div className="text-[11px] text-[var(--text-soft)]">按后端 VIDEO_CREATE_PATHS 提交,模型 ID 走环境变量映射</div>
|
||||
</KanbanCard>
|
||||
<KanbanCard tone="violet" tags={["外部"]} title="Seedance">
|
||||
<div className="text-[11px] text-[var(--text-soft)]">字节跳动 · 需独立 API key</div>
|
||||
|
||||
Reference in New Issue
Block a user