EchoTwin
一个全双工的 Discord 语音机器人,用克隆的声音回你话——出声约 0.6 秒,本地流式 ASR,还有一套判断"该不该接话"的定向识别管线。
- 类型
- 命令行
- 角色
- 独立开发
- Status
- 进行中
- Tech
- Python 3.11+ discord.py Fish Audio TTS Claude Haiku 4.5 sherpa-onnx (zipformer) SenseVoice Silero VAD Groq (qwen3-32b) asyncio onnxruntime pytest
- Started
- 2026年5月
EchoTwin 会加入 Discord 语音频道,用克隆的声音跟你对话。你说话,它回答——快到像在打电话,而不是在等一次请求返回。
整条管线是 VAD → 本地流式 ASR → 定向判断 → LLM(含工具调用)→ 克隆声音 TTS,每一段都被计时。
一次真实运行,逐段追踪:ASR 转写、定向判断及其分数、对话中途解析的工具调用,以及每次回复的延迟拆解。
延迟,实测
语音 agent 的成败全在”你说完之后的那段空白”。下面是真实生产对话的 p50,跑在 M 系列 Mac、家用网络上——仓库里的 bench 脚本可复现,不是厂商数字。
| p50 | |
|---|---|
| 首次出声(缓存填充语) | 说完后 约 0.6 秒 |
| 完整回复,仅管线耗时 | 约 1.2 秒(ASR 19 ms · LLM 971 ms · Fish TTS 174 ms · 播放 40 ms) |
| 完整回复,嘴到耳 | 约 1.75 秒,含刻意保留的 550 ms 断句等待 |
| 生产日志中最快的一轮 | 从端点到首个音频 361 ms |
真正有用的结论是时间不在哪里:TTS 只占管线的 10%,从来不是瓶颈;两个大头是 LLM 的第一句(55%)和刻意保留的 VAD 断句等待(31%)——后者是对话设计的选择,不是成本。剩下的时间靠 ASR/LLM 的推测式执行、预先建好的 TTS 连接和缓存填充语买回来。
生产日志的聚合值会高估管线成本,延迟报告 解释了原因:llm_first_delta→first_audio 里悄悄包含了 LLM 补完这句话剩余部分的时间,因为分句器只会把完整句子交给 TTS。
判断什么时候该接话
一对一是它最擅长的模式。难题在于一屋子人:在群聊频道里,机器人听到的大部分内容都不是对它说的。三层来决定:
- 反射 —— 查表瞬间解决明显的情况。句首句尾叫到名字,或者频道里只有你和它,一定回。
- 仲裁 —— 模棱两可的话交给一个快模型(Groq qwen3-32b,约 350 ms),它会先读一遍房间里最近的对话记录再判断。
- 启发式规则 —— 一套用 golden set 测过的规则兜住上一层的失误。
被拒绝的闲聊不会被丢掉——它们进入一份滚动的环境记录,所以当机器人真的开口时,它已经知道大家刚才在聊什么。对全场的开放式提问会先等约 1.5 秒,把话语权让给人类。机器人说话期间排队的语句会合并成一个回复,而不是堆着一句句念完。
这个模式默认开启,也确实能用,但它仍是最活跃的开发区——偶尔会看错场合。
其余部分
- 打断 —— 它回话时你开口,它会当场停在半句。反向通道过滤器会丢掉”嗯 / ok / yeah”以及短于 600 ms 的语音,所以随口一个附和不会把回答砍断。
- 热切换人格 —— 一条命令换角色,同时清空历史、刷新唤醒词、定向检测器和缓存的快速应答音频。
- 工具调用 —— 对话中途查时间、日期、天气。
- 成本追踪 —— 每轮花费统计,带日/月预算上限,可在 Discord 里直接查。
- 热重载 ——
kill -HUP或一条斜杠命令即可重读配置和人格文件;声音 id、唤醒词模式、白名单和共同管理者都能跨重启保留。 - 命令本地化 —— Discord 会按客户端语言显示斜杠命令(英文 / 简体中文),加一个新语言只需改一个字符串文件。
刻意锁死的依赖
三个依赖被锁到精确版本,manifest 里都写了原因。discord.py、discord-ext-voice-recv 和 davey 的内部实现被 monkey-patch 过,一次静默升级就可能直接搞坏音频解密。sherpa-onnx 停在 1.13.2,是因为 1.13.4 在 macOS arm64 上会解码出乱码——用原始 API 对干净音频复现验证过,并留了一个 harness 测试,谁要升级先跑它。
听的部分交给本地模型(VAD + 流式 ASR,约 440 MB),唯一的外部请求只有 LLM 和 Fish Audio。