1. 会话 WebSocket WS /ws
一条连接就是一场对话:上行 16 kHz 单声道 PCM(int16,每帧 80 ms = 1280 样本),下行 JSON 事件 + TTS PCM + 数字人视频帧。连上后服务端先发 hello。
上行:二进制帧
| 内容 | 说明 |
|---|---|
| int16 PCM,16 kHz,mono,1280 样本/帧 | 持续发送麦克风音频。VAD / EOT / ASR / 打断全部在服务端判断,客户端不必做端点检测。 |
上行:控制 JSON
| type | 字段 | 作用 |
|---|---|---|
text | text | 用文字代替一次语音提问,走同一条 LLM → TTS → 数字人链路。 |
set_voice | spk_id | 切换音色(见 /voices),下一句回复生效。 |
set_persona | text, persist(默认 true) | 改人设 system prompt。控制标记规则固定拼在后面;会清空对话历史。回 persona 事件。 |
reset | — | 清空历史、状态机复位。 |
avatar_stop | — | 手动打断数字人(等价于用户插话)。 |
playback_done | — | 客户端播完本回合全部 PCM 后回报,服务端据此回到倾听态。 |
avatar_test | text | 调试:直接 TTS → 数字人,不经过 LLM。 |
下行:JSON 事件
| type | 关键字段 | 含义 |
|---|---|---|
hello | sid, in_sample_rate, frame_samples, tts_sample_rate, components, speaker_lock | 连接就绪与采样率约定。开启第一说话人锁定时 speaker_lock=true。 |
speaker_lock | state (locked / ignore / reset), sim | 已锁定第一说话人 / 忽略其他人声 / 会话重置后重新登记。 |
state | state (listening / armed / speaking), reason | OpenTurn 状态机变化。 |
vad | p, speech_ms, silence_ms | 每帧 VAD 概率与连续语音/静音时长。 |
asr_partial / asr_final | text, preview | 流式识别中间结果 / 本回合最终文本。 |
action | name | 状态机动作流:commit / speculative / interrupt / user_speech_start … |
llm_delta / llm_done | text | LLM 增量与完整回复(已剥掉控制标记)。 |
log | msg | 过程日志,含 ctrl emo=… act=…(LLM 输出的情绪/动作预测)。 |
pcm | sr, data(base64 int16), splice, seq | TTS 音频,24 kHz。收到即按顺序排队播放。 |
bot_speaking / bot_done | audio_sec | 回合开始发声 / 本回合音频已全部下发。 |
interrupt / cut_audio | splice, idle_from_ms, keep_ms, turn_seq, hard | 用户插话:立刻停声。pts < idle_from_ms 的视频帧是在途口型,之后是倾听帧。 |
metrics | commit_to_llm_first_ms, commit_to_first_audio_ms, first_chunk_ms, ttff_ms … | 本回合延迟指标。 |
voice / persona | spk_id / text | 设置已应用。 |
avatar_stream_ready | run_id, transport, persistent | 数字人视频流可用;transport=push 时帧通过本连接二进制下发。 |
avatar_status | state (warm / streaming_audio / idle / waiting …), pts_offset_ms, turn_seq, video_sec | 数字人阶段变化。 |
avatar_chunk | index, frames, video_sec | LiveAct 又解码出一块(16 帧 = 0.8 s)。 |
avatar_audio_start | pts_offset_ms, lead_ms, sr, turn_seq | 口型已生成到开口位置:从此刻起播放本回合 PCM,视频帧按 pts - pts_offset_ms 与音频时钟对齐。 |
avatar_stopped / avatar_done / avatar_error | reason / state / message | 数字人被打断 / 结束 / 出错。 |
下行:二进制视频帧(push 传输)
magic "AVF1" | uint32 index | uint32 pts_ms | uint16 width | uint16 height | uint32 jpeg_len | JPEG… (大端)
20 fps,256×416。pts_ms 是该帧在数字人时间线上的位置;倾听态按 20 fps 顺播即可,说话态用 avatar_audio_start.pts_offset_ms 对齐到音频时钟。每条 run 的前 8 帧(index < 8)是预热图,建议丢弃。
2. 音色 (音色设置)
| 接口 | 说明 |
|---|---|
GET/voices | 音色列表:{speakers:[{id,name,builtin}], default} |
POST/voices | multipart:file(3–6 s 参考音频)、name、prompt_text(可空,自动转写)。one-shot 克隆,返回 {id,name,prompt_text,speech_sec}。 |
POST/voices/preview | JSON {spk_id, text?} → WAV 试听。 |
DELETE/voices/{spk_id} | 删除克隆音色。 |
情绪由 LLM 每句预测(happy / sad / angry / surprise / gentle / excited / calm),服务端转成 CosyVoice instruct 送 TTS,不需要客户端干预。
3. 人设 (对话设置)
| 接口 | 说明 |
|---|---|
GET/api/persona | {persona, default, control_rules, max_chars} |
POST/api/persona | JSON {persona}。写入 persona.json,新会话生效;已连接会话请发 WS set_persona。 |
4. 形象 (形象设置)
| 接口 | 说明 |
|---|---|
GET/api/avatar-image | {avatar_id, mask_revision, preview_url, overlay_url} |
POST/api/avatar-image | multipart image(半身正面照, ≤20 MB)。抠像 → 切换 → 重开数字人流,同步返回;期间画面会空几秒。写入 avatar_settings.json,重启保留。 |
GET/api/avatar-image/{avatar_id}/original.png|overlay.jpg|mask.png | 形象预览资源。 |
5. 其它
| 接口 | 说明 |
|---|---|
GET/healthz | 存活检查。 |
GET/docs | FastAPI 自动生成的 HTTP 文档。 |
6. 最小客户端示意(浏览器)
const ws = new WebSocket(`ws://${location.host}/ws`); ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
if (e.data instanceof ArrayBuffer) { /* AVF1 视频帧 → 解 JPEG 画到 canvas */ return; }
const m = JSON.parse(e.data);
if (m.type === 'pcm') queuePcm(m.data, m.sr); // base64 int16 → WebAudio
if (m.type === 'interrupt' || m.type === 'cut_audio') stopAudioNow();
if (m.type === 'avatar_audio_start') alignVideoTo(m.pts_offset_ms, m.lead_ms);
};
// 麦克风:AudioWorklet 重采样到 16 kHz,每 1280 样本 ws.send(int16Buffer)
ws.send(JSON.stringify({ type: 'text', text: '你好' })); // 或直接文字提问