实时语音表演框架:文本/语音进,带口型轨与时间轴的流式语音出。已内置 Kazebot 的 Agent 框架,如有需要可以更换。
任意客户端(皮套渲染 / 网页 / 设备)
▲ MOKA 协议:WS 文本帧(控制)+ 二进制帧(音频)
▼
Python 服务端(分句 → 流式 TTS → 口型轨 → utterance 推送)
▲ AgentSource 抽象
▼
kazebot Agent 框架(工具调用 / 记忆 / 多节点,LLM 渠道自选)或自定义 Agent
对话式皮套的体感只由一个数决定 —— TTFA(Time To First Audio):用户提交 → 扬声器出声。串行做法要等 LLM 说完、等 TTS 合成完、等整段音频下载完,三段相加轻松 3 秒以上。
MOKA 把这三段重叠起来:
时间 ───────────────────────────────────────────────▶
LLM ████████████████████████████████████ 边吐边送,不等说完
│句1 │句2 │句3
▼ ▼ ▼
TTS ███ ███ ███ 并发 ≤3 池化预合成
│
▼ 首块 PCM(不是整句)
播放 ████████████████████████████████ 首块到就开播
▲
TTFA
具体的:
- 分句器边收边切,首句遇逗号即切(阈值 5 字),不等句号
- TTS 池化预合成:并发 ≤3、启动间隔 250ms,播第 N 句时后面的句子已经在合成,句间不再等首包
- 音频按 40ms 帧推,首帧例外:首帧拿到多少发多少,不等凑满 —— 出块细于一帧的 TTS 后端下,等凑满会把出声推迟几十到几百毫秒
- 口型轨随句下发,服务端按 TTS 字级时间戳预先算好,客户端只做按 PTS 插值 —— 客户端零推理
- 背压由播放位驱动:客户端上报播放进度,发送领先超过 6s 就停手,不靠猜
| 指标 | 实测 |
|---|---|
| 链路自身开销(TTFA 扣除 LLM 首 token) | ≈330 ms |
| 其中管线代码本身(其余为 TTS 首包与 agent 桥) | ≈0.5 ms |
| TTFA P50(LLM TTFT=100ms 模拟) | 430 ms |
| 真 LLM 下「首 token → 出声」 | ≈250 ms |
| 语音全双工「闭嘴 → 开口」(同一模拟条件) | ≈920 ms |
测量环境:同机部署,GSV-TTS-Lite on 4080 Laptop(未开 flash_attn)。LLM 侧用 TTFT 可配的假流式端点隔离外部变量。每条语料跑 5 次取 P50。 管线代码那一行是 in-process 探针单独测的:
input_received→agent_dispatched0.1ms、sentence_ready→tts_request0.0ms、tts_first_chunk→ws_sent0.2ms、出口→客户端 0.2ms。
调优拿掉的时间:agent 轮询 1.0s→0.1s(−271ms)、首句逗号加速(−235ms)、GSV 流式块 25→15(首包 P50 206→170ms)、首帧不等凑满帧长(出块细于一帧的后端 −60~315ms)。
LLM 首 token 是外部变量,不算 MOKA 的账。 本机跨国连 Gemini 时 llm_first_token 要 1.9~3.0s,网络决定,代码不可控;但那之后到出声依然只有 250ms。换低 TTFT 的 provider(本地 Ollama、国内直连)即可逼近 700ms 端到端目标。
埋点是第一天就进代码的,不是事后补的。服务端 7 点 —— input_received → agent_dispatched → llm_first_token → sentence_ready → tts_request_started → tts_first_chunk → ws_first_audio_sent,客户端补 2 点(收到首帧、实际出声)。全部用单调时钟,每轮 reply 输出一行结构化 JSON,debug.metrics 下发到客户端调试面板。
打断是常态路径而非异常分支。用户随时开口或输入,链路立刻清场,并按字幕轨把用户实际听到的那部分截断后写回对话历史 —— 不是把整段回复都当成"说过了"。
两档语义:硬打断(立刻停)与软打断(interrupt:"soft",当前句播完再切换,句边界对齐)。语音场景下用户开口即硬打断(barge-in)。
打断精确到任务:打断通知与下一轮请求是并发在飞的两个调用,按会话粗粒度取消会连坐刚提交的新一轮,让用户空等一个超时。
三条轨道都挂在音频 PTS 上,播放器是唯一时钟:
- 口型轨 —— 字级时间戳 → 拼音 → 五元音权重(
a/i/u/e/o),客户端插值混合;模型只有ParamMouthOpenY也能退化工作 - 逐字字幕 —— 按字符时间戳推进,打断时用它算"听到哪"
- 表演事件 —— 表情/动作标签落到它修饰的那个字上
轨道走 utterance.track 增量下发,只追加不重排,晚到的事件立即补触发。
表情与动作走内联标签([MOOD:happy] / [POSE:nod] / [HOLD:think]),与文本在同一个 token 流里出来,不额外过一次模型 —— 二次 LLM 规划要多花一整个往返,内联标签的固有成本只有约 100ms(标签字符本身的吐字时间)。
情绪不是贴图切换,而是三维连续坐标(愉悦 / 唤醒 / 支配)解算成面部动作单元后写标准参数:
- 带标准参数的模型都能演,不需要美术做表情文件;模型有现成的
.exp3.json或动作文件也能绑上去,与参数驱动叠加而非二选一 - 说话响度实时调制表情强度与头身微摆,重音处带顿挫
- 情绪随时间淡回角色底色,唤醒度掉得快、愉悦度留得久,LLM 不再打标签也不会挂着同一张脸
- 用户开口时自动前倾倾听,空闲时循环模型自带的待机动作
- 动作三档抢占:显式点名 > 情绪绑定 > 待机
内置 12 情绪 / 6 视线 / 6 姿态 / 4 保持,可用名字经 model.info 上报给服务端并写进 LLM 的能力清单 —— 模型演不出来的名字自动剔除,LLM 不会点到空气。预设与自定义见 client/README.md。
sherpa-onnx 一包扛 SenseVoice int8 识别与 Silero VAD,Smart Turn v3.2 做端点判定,识别与端点判定并行跑。用户闭嘴到皮套开口 ≈920ms,瓶颈在 VAD 静音确认窗(250ms)而非推理。
支持多说话人:收音流可声明 speaker.name,识别结果按名字归属,交给 agent 的文本自带前缀。
| 换什么 | 怎么换 |
|---|---|
| Agent | 实现 AgentSource 两个方法(文本增量流 + 打断),register_agent 挂载 |
| TTS | 实现 TTSBackend.synth(),给足字级时间戳则口型轨与逐字字幕自动生效 |
| ASR | create_app(asr_engine=...) 注入 |
| 音频后端 | JS SDK 的 PlaybackSink / CaptureSource 可注入,默认 Web Audio,纯 Node 也能跑 |
服务端不写模块级可变状态,create_app(cfg, agent_factory=..., tts=...) 可直接 add_subapp 进已有 aiohttp 应用。
| 件 | 位置 | 说明 |
|---|---|---|
| 协议规格 | PROTOCOL.md | WS 信封 + 16 字节头二进制音频帧;任何语言实现即可接入 |
| 服务端框架 | server/ | 表演管线:AgentSource / TTSBackend / ASR 可插拔,create_app 可嵌入宿主进程 |
| Agent 后端 | 内置 kazebot 桥 | kazebot 自带工具调用、记忆与多节点委派,LLM 渠道可选 OpenAI / Anthropic / Gemini / DeepSeek。换成别的 agent 只需实现 AgentSource 两个方法,OpenAI 兼容接口的直连实现见 server/examples/ |
| JS 客户端 SDK | sdk/js/ | @mokamoka/client:连接、播放、口型字幕与表演轨、双向打断;音频后端可注入 |
| Python 客户端 SDK | server/ | from mokamoka import MokaClient,与服务端同包共用协议编解码 |
| 桌面参考实现 | client/ | Electron + Cubism 皮套,消费 @mokamoka/client |
音频统一 pcm_s16le / 32000 Hz / 单声道,上行 16000 Hz。
# 服务端
cd server && pip install -e . && mokamoka-server
# 打开 http://127.0.0.1:9480/demo 即可网页对话
# 桌面客户端
cd client && npm install && npm run fetch-cubism && npm run dev自己写客户端:
import { MokaClient, subtitleAt, mixMouth } from "@mokamoka/client";
const client = new MokaClient({ onUtterance: (u) => console.log(u.text) });
await client.connect("ws://127.0.0.1:9480/moka", token);
client.sendText("你好");接自己的 LLM、换 TTS、嵌入已有服务:见 server/README.md 的扩展章节与 server/examples/。
PROTOCOL.md 协议规格(唯一真相源)
server/ Python 服务端框架 + Python 客户端(pip 包 mokamoka-server)
sdk/js/ TypeScript 客户端 SDK(npm 包 @mokamoka/client)
client/ Electron 桌面客户端
MIT(不含第三方专有组件,见 CONTRIBUTING.md)