Files
xiaoxia-saas/docs/ai-avatar-api-contract-1822.md
xiaoxia 4d09bd630e
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m37s
AI Code Review / AI Code Review (pull_request) Successful in 6m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 163h26m18s
fix(ai-avatar): 契约文档标题字段纠正 + script_id 改为可选
- 文档 §5/§6:标题是单个 title_config dict(非 titles[] 数组),字段以
  build_title_drawtext_filter() 为准:text/content、font_size/size、
  font_color/color、position(top/center/bottom/custom)、pos_x/pos_y、bold、
  stroke、shadow、enabled;删除不存在的 titles[]/fontSize/frame/start/end 描述
- script_id 改为可选:手动输入文案/TTS 直生场景不关联文案库条目,留空不校验,
  避免手动文案用户被 ScriptNotFound 卡住;选了文案库时仍做归属校验
- 迁移 074:ai_avatar_render_jobs.script_id 默认空串
- 新增 schema 单测(script_id 空/空白/title_config 单 dict),94 测试全绿

Refs #1797 #1826
2026-09-09 19:55:01 +08:00

8.1 KiB
Raw Permalink Blame History

AI 数字人前后端接口契约(#1797 / #1822)

分支:fix/ai-avatar-v3-1797 范围:TTS→对口型链路打通、语速/情绪透传、封面智能选帧、标题字段对齐 本文档为前后端联调的唯一字段口径。


1. 对口型创建接口 POST /api/v1/lipsync/jobs

支持两种输入模式,二选一:

模式 A(推荐):TTS 直生 —— 传音色 + 文案,后端内部合成音频

前端无需先调 TTS。后端收到请求后:先调 CosyVoice 合成音频 → 转存 OSS → 再提交 MediaKit 对口型。

{
  "video_url": "https://oss.../person.mp4",   // 必填,人物视频(MP4)
  "voice_id": "cosyvoice-v3-flash-99-xxxx",    // 必填,音色 ID(预置音色 或 克隆 profile UUID)
  "script_text": "省是浙江省,市是永康市……",     // 必填,要合成的文案
  "speed": 1.0,                                 // 可选,语速 0.5~2.0,默认 1.0
  "emotion": "excited",                         // 可选,情绪,见 §3
  "enable_video_loop": false,                   // 可选,音频长于视频时是否循环画面
  "project_id": ""                              // 可选
}

模式 B:直接音频 —— 前端已准备好音频

{
  "video_url": "https://oss.../person.mp4",     // 必填
  "audio_url": "https://oss.../voice.mp3",      // 必填,mp3/aac/wav/m4a/flac
  "enable_video_loop": false
}

校验与错误码

场景 HTTP detail.code
既无 audio_url 又无 voice_id+script_text 422 (schema 校验)
video_url 非 MP4 / audio_url 格式不支持 422 (schema 校验)
克隆音色不属于当前用户 403 VoiceForbidden
克隆音色尚未合成完成 400 VoiceNotReady
TTS 合成失败(如 CosyVoice 欠费) 502 TTSSynthesisFailed
MediaKit 提交失败 502 *(透传 MediaKit code)

轮询

  • GET /api/v1/lipsync/jobs/{id}:非终态任务先返回 DB 缓存,后台异步刷新 MediaKit(不会阻塞轮询)。
  • status 流转:pending → submitted → running/processing(MediaKit 中间态同步)→ completed / failed。
  • completed 时 output_video_url 为已转存自家 OSS 的非临时 URL(不会过期)。
  • 前端每 3s 轮询,命中 completed/failed 即停。

2. TTS 合成接口语速/情绪透传

  • POST /api/v1/tts/synthesize(异步任务)与 POST /api/v1/tts/preview(即时试听)均新增:
    • speed:float,0.5~2.0,默认 1.0 → 透传 CosyVoice payload 的 rate
    • emotion:string,见 §3 映射 → 透传 emotion
  • 透传链路:route → CreateTTSJobUseCase(metadata) → TTSJobWorkflow.start_synthesis / 分段合成 → CosyVoiceService.submit_synthesize_task(rate/emotion)。
  • 分段合成(长文案)与失败重合成路径同样透传 speed/emotion。

3. 情绪枚举(前后端统一)

前端把中文选项映射成英文后传后端;后端同时接受中文/英文,非法值忽略(走默认自然)。

前端选项 传参值 CosyVoice 枚举
自然 natural natural
兴奋 excited excited
沉稳 calm calm
亲切 friendly friendly

后端 normalize_emotion() 也接受中文(自然/兴奋/沉稳/亲切)做兜底映射。


4. 智能封面接口 POST /api/v1/ai-avatar/render/smart-cover

独立接口,不依赖渲染任务,前端「智能获取封面」按钮直接调用。

请求

{
  "video_url": "https://oss.../avatar_output.mp4",  // 必填,数字人视频
  "max_frames": 5                                    // 可选,抽帧数量 1~10,默认 5
}

响应

{
  "cover_url": "https://oss.../ai-avatar/covers/xxx/cover_yy.jpg", // OSS 非临时 URL
  "status": "completed",       // completed / fallback_failed
  "message": ""                // 失败原因
}

实现:复用智能剪辑同款能力 —— MediaKit extract_frames(SpecifiedFrames) 抽 5 帧 → cover_frame_scorer.score_frames(清晰度+亮度+色彩)评分选最佳 → 转存 OSS。 不再使用 FFmpeg 简单首帧。渲染管线最终封面也优先走该智能选帧,MediaKit 不可用时才回退 FFmpeg。


5. 渲染接口 POST /api/v1/ai-avatar/render

{
  "lipsync_job_id": "7c29a3b2-...",     // 必填,已 completed 的对口型任务
  "script_id": "",                       // 可选!见下方说明
  "b_roll_segments": [],                 // 可选,B-roll 片段
  "title_config": { ... },               // 可选,单个标题配置 dict(见 §6)
  "cover_config": { ... },               // 可选,封面配置(建议改用 smart-cover)
  "project_id": ""
}

script_id 是否必填:可选。

  • 从文案库选了文案时传对应文案 ID(后端做归属校验)。
  • 手动输入文案、走 TTS 直生模式时不传(留空)即可——渲染管线不依赖文案内容,script_id 仅用于归属校验。留空不会卡手动文案用户。

6. 标题配置 title_config 字段清单(以 build_title_drawtext_filter 为准)

渲染请求收的是单个 title_config dict(不是 titles[] 数组),字段与 packages/domain/video_filter_builder.py 的 build_title_drawtext_filter() 完全对齐:

字段 别名 类型 必填 默认 说明
text content string ✅ — 标题文字;为空或 enabled=false 时不渲染标题
enabled — bool ❌ true 是否启用标题;false 跳过
font font_preset string ❌ 思源黑体 字体名(后端按名字解析字体文件)
font_size size int ❌ 36 字号(像素)
font_color color string ❌ #ffffff 文字颜色,#RRGGBB;后端自动去掉 #,也可传 RRGGBB 或颜色名
position — string ❌ top 预设位置:top(y=50) / center(垂直居中) / bottom(底部上移50px) / custom
pos_x — int/float ❌ — 自定义 X 坐标(像素),仅 position=custom 生效
pos_y — int/float ❌ — 自定义 Y 坐标(像素),仅 position=custom 生效
bold — bool ❌ true 粗体(Bold 字体变体,回退 borderw 模拟)
stroke — bool/object ❌ — 描边。true=黑描边宽2;object 见下
stroke.enabled — bool ❌ true 是否描边
stroke.width — int ❌ 2 描边宽度
stroke.color — string ❌ #000000 描边颜色
shadow — bool/object ❌ — 阴影。true=黑色阴影偏移2px;object 见下
shadow.enabled — bool ❌ true 是否阴影
shadow.color — string ❌ #000000 阴影颜色
shadow.offset_x — int ❌ 2 阴影 X 偏移
shadow.offset_y — int ❌ 2 阴影 Y 偏移

前端注意事项

  • 标题是整条成片一个标题(单个 dict),不是按时间段的标题数组;没有 start/end/frame/fontSize 这些字段。
  • 位置用 position 四档枚举;自由摆放用 position="custom" + pos_x/pos_y(像素坐标,非比例)。
  • 颜色统一传 #RRGGBB 即可,后端会处理 #;三档预设位置下标题始终水平居中。
  • stroke/shadow 传 true 用默认样式,或传 object 精细控制颜色/宽度/偏移。

7. 前端对接清单

  1. 对口型:改用模式 A(voice_id + script_text + speed + emotion),不要再先调 TTS 拿 audio_url。
  2. 音色 ID:voice_id 可直接传克隆音色的 profile UUID,后端会解析为 CosyVoice voice_id(与 /tts 一致)。
  3. 情绪下拉:自然/兴奋/沉稳/亲切 → natural/excited/calm/friendly。
  4. 封面:点「智能获取封面」→ POST /ai-avatar/render/smart-cover,用返回的 cover_url。
  5. 渲染:手动文案直生场景 script_id 留空;标题传单个 title_config dict(字段见 §6)。
  6. 轮询:识别 running 等中间态,不要只认 submitted。