最近一周下班后,用 ClaudeCode CLI + DeepSeek-v4-flash 全程 vibe coding 搓了一个实时语音对话系统:麦克风进、AI 语音回复出,ASR + LLM + TTS 全流程跑在本地,零云服务。代码拆成三个独立子项目,各带可插拔后端:
语音对话系统入口在 voice1 项目的 voice_dialogue.py。三个项目的模型服务、按两种方式调用:
/v1/chat/completions + SSE 流式),LLM 跑在独立进程(本地 llmx 服务或云端 DeepSeek),voice1 通过 HTTP 请求与LLM进行进程间通信。可先接在线 DeepSeek API 测试,再切本地 llmx,只需改 dialogue/config.local.json 三行配置即可,详见使用文档python examples/voice_dialogue.py),由后端抽象层(Backend ABC + 模型注册表)统一调度(调用是函数级的,无进程间通信),与编排代码同进程协作、共享显存对话系统支持自定义唤醒词唤醒,可通过指定关键词休眠,或 idletime 超时无对话活动自行休眠,支持指定用户系统提示词,并且设计为实时可打断:
三个项目同级别目录下git clone放置,可各自独立安装跑通,若使用本地 llm 服务,需要在 llmx 项目目录下先执行 run_server.bat --model qwen3-4b,之后在 voice1 项目目录下即可命令行启动聊天对话系统:
1 | python examples/voice_dialogue.py --asr-device cuda --tts-device cuda --vad-tail 300 --vad-threshold-db -42 --system-prompt dialogue/user_prompt.txt --tts-normalize rms --llm-config dialogue/config.local.json |
项目架构通览(mermaid):
sequenceDiagram
autonumber
participant MIC as 主线程
PortAudio回调
每~20ms一块
participant ASR as ASR worker
voice1
participant CTL as DialogueController
participant LLM as LLM线程
dialogue-llm
participant TTS as TTS worker
voice0 queue
Note over MIC,TTS: 全链路非阻塞:主线程只采麦克风,各段在各自线程干活
rect rgb(238,244,255)
Note over MIC,ASR: ① 采集→断句→识别(voice1)
loop 持续(说话/静音都喂)
MIC->>ASR: asr.ingest(mono)
非阻塞入队(满则背压阻塞)
ASR->>ASR: 能量VAD:静音尾≥vad-tail
→判定"这句说完了"
ASR->>ASR: paraformer识别
(流式cache + 句末flush定稿)
end
ASR-->>MIC: on_partial → "…出字"(未定稿,不进LLM)
ASR-->>CTL: on_sentence(定稿句)
(ASR worker线程回调)
end
rect rgb(255,248,230)
Note over CTL,LLM: ② 提交LLM(controller快操作,不阻塞识别)
CTL->>CTL: feed_asr_sentence():累加本轮
gen+=1 · 在途/post-commit检查
CTL->>LLM: 启动 _llm_loop 线程(非阻塞)
CTL-->>MIC: on_user → 控制台时间戳定稿行
end
rect rgb(235,250,235)
Note over LLM,TTS: ③ LLM流式输出→按句切分→TTS串行合成播放
LLM->>LLM: stream_chat() 阻塞读SSE
(专用线程,不卡主线程)
loop 每个token增量
LLM-->>CTL: on_ai_delta → 控制台"AI: …"流式原地刷新
CTL->>CTL: _emit_sentences():按 。!? 切句
(逗号不切;40字硬切兜底)
CTL->>TTS: tts.submit(句)(非阻塞入队)
TTS->>TTS: melo合成(~0.4s) + 播放(queue串行)
end
LLM->>CTL: 流结束:flush残句 + 记录usage
commit(user→assistant)进历史
end
rect rgb(252,240,246)
Note over MIC,CTL: ④ 打断与回声门控(半双工)
Note over CTL: 新定稿句 → 三种情况:
Note over CTL: ·LLM在途 → gen+1弃流 + tts.interrupt() → 累计重发
Note over CTL: ·已答完、音频未开播(post-commit窗口内)→ 撤答复合并重发
Note over CTL: ·过窗口(音频已开播)→ 新轮,不打断语音
Note over MIC: 回声门控:mic回调读 ctrl.tts_busy
播放期只喂 ingest_kws_only(听"停下")
滚动grace:开播后 echo-guard 内
有语音能量 → 仍喂正常识别(抓续句尾巴)
endPS:以上项目总计消耗 deepseek 4.2亿 token,总花费 61 元
后续更新:
[26.9.2] TTS 播放支持联动 live2d(可选项,进程外服务化调用,启动语音对话系统需要前先启动 live2d server:启动命令),随语音播放自动发送心态表情 + 头顶说话框(说话框文本逐句链式跟播)至 live2d server,嘴型由 live2d 服务端监听播放音频自行对口型
[26.9.5] 之前我们的对话系统是直接接入 LLM 大模型(ASR->LLM->TTS),不具备工具调用能力,连询问今天是几月几号都不知道,因此最新实现支持接入本地 claudecode agent(ASR->AGENT->TTS),可随时自定义 skill、mcp 扩展,譬如添加天气获取的能力等,缺点是回复延迟稍大一丢丢(因为涉及多轮工具调用和 LLM 推理)。最新一键启动语音对话系统命令:start_dialogue.bat [llm|agent]
demo示例(此处演示是接入的 LLM 且调用在线 DeepSeek API):
本文作者:muggledy
版权声明:本博客所有文章除特别声明外,均采用知识共享 署名—非商业性使用—禁止演绎 4.0 国际许可协议 © CC BY-NC-ND 4.0 进行许可。非商用转载请注明出处!严禁商业转载!
本文链接:https://blog.muggledy.top/posts/104e1f98/