# AI 数字人前后端接口契约(#1797 / #1822) > 分支:`fix/ai-avatar-v3-1797` > 范围:TTS→对口型链路打通、语速/情绪透传、封面智能选帧、标题字段对齐 > 本文档为前后端联调的唯一字段口径。 --- ## 1. 对口型创建接口 `POST /api/v1/lipsync/jobs` 支持两种输入模式,**二选一**: ### 模式 A(推荐):TTS 直生 —— 传音色 + 文案,后端内部合成音频 前端无需先调 TTS。后端收到请求后:先调 CosyVoice 合成音频 → 转存 OSS → 再提交 MediaKit 对口型。 ```jsonc { "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:直接音频 —— 前端已准备好音频 ```jsonc { "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` 独立接口,**不依赖渲染任务**,前端「智能获取封面」按钮直接调用。 **请求** ```jsonc { "video_url": "https://oss.../avatar_output.mp4", // 必填,数字人视频 "max_frames": 5 // 可选,抽帧数量 1~10,默认 5 } ``` **响应** ```jsonc { "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` ```jsonc { "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`。