fix: surface resilient subject asset generation
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user