发布时间5月5日标签#语音交互 #VAD #TTS #ASR #人机交互字数约2000字一、为什么语音交互这么难做做语音助手的都知道一个痛点AI还没说完你就知道它错了。但你必须忍着听完。手机上用Siri或小爱同学时这种体验特别常见。AI进入幻觉模式开始编造答案而你能做的只有拿起手机点停止。好的语音交互打断体验和回答质量同等重要。二、语音活动检测VAD让程序知道你在说话为什么不用简单的音量阈值一开始我尝试用音量的RMS值判断是否有人说话import pyaudio import numpy as np def simple_vad(audio_chunk, threshold500): rms np.sqrt(np.mean(np.square(audio_chunk))) return rms threshold问题环境噪声变化时阈值失效。风扇声、键盘声、翻书声都会误触发。而且无法区分人声和非人声。最终方案TEN VADTEN VAD是一个轻量级深度学习模型专门训练用于区分人声和噪声class VADDetector: def __init__(self, model_path, sample_rate16000): self.model TenVAD(model_path) self.sample_rate sample_rate self.speech_started False self.silence_duration 0 self.silence_threshold 0.8 # 0.8秒静音判定说话结束 def is_speaking(self, audio_chunk): 返回是否检测到人声 result self.model.detect(audio_chunk, self.sample_rate) return result[speech_probability] 0.7 def should_end_utterance(self, audio_chunk, dt): 判断是否应该结束录音 if self.is_speaking(audio_chunk): self.silence_duration 0 return False else: self.silence_duration dt return self.silence_duration self.silence_threshold三、打断机制的双模设计语音打断用户开始说话 → VAD检测到人声 → 触发打断 → 进入2秒冷却期关键设计冷却期不设冷却期的话TTS播报的声音会被VAD检测为用户在说话导致刚打断恢复就立即再次触发打断形成振荡。按键打断import keyboard class KeyInterrupt: def __init__(self, callback): self.callback callback def start(self): keyboard.on_press_key(space, lambda _: self.callback()) def stop(self): keyboard.unhook_all()空格键作为万能打断键不依赖VAD状态不受冷却期限制。四、语音识别ASRfaster-whisper选型对比了几个方案方案模型大小中文准确率推理速度Vosk~50MB一般快SpeechRecognition在线好依赖网络whisper.cpp~200MB好中faster-whisper244MB好较快选择faster-whisper的原因CTranslate2加速比原版whisper快4-6倍INT8量化内存占用减半16GB笔记本上只占不到1GB内存from faster_whisper import WhisperModel class ASREngine: def __init__(self, model_sizesmall): # 第一次运行会自动下载模型 self.model WhisperModel( model_size, devicecpu, compute_typeint8 # INT8量化节省内存 ) def transcribe(self, audio_data): segments, info self.model.transcribe( audio_data, languagezh, beam_size5 ) return .join([seg.text for seg in segments])五、语音合成TTS流式播报为什么不用Edge TTS或讯飞离线优先。pyttsx3完全本地运行不需要网络。流式播报实现import pyttsx3 import re class StreamSpeaker: def __init__(self): self.engine pyttsx3.init() self.engine.setProperty(rate, 180) # 语速 def speak_stream(self, text_generator, interrupt_handler): 流式播报支持打断 buffer for token in text_generator: if interrupt_handler.is_interrupted(): self.engine.stop() break buffer token # 遇到标点就播报当前句子 if re.search(r[。\n], buffer): self.engine.say(buffer) self.engine.runAndWait() buffer # 播报剩余内容 if buffer and not interrupt_handler.is_interrupted(): self.engine.say(buffer) self.engine.runAndWait()效果模型每生成完一句话立刻开始朗读用户不需要等全部生成完。停顿自然的句号和问号位置刚好成为语音播报的断点。六、前置指令让常用操作秒回PRESET_COMMANDS { 你好: 你好我是你的本地AI助手。你可以问我知识库中的任何问题。, 在吗: 我在。请随时提问。, 再见: 再见, 退出: 退出系统。, 谢谢: 不客气 } def handle_query(query): # 先检查是否前置指令 if query.strip() in PRESET_COMMANDS: return PRESET_COMMANDS[query.strip()] # 前置指令再见退出需要退出程序 if query.strip() in [再见, 退出]: sys.exit(0) # 否则走正常RAG流程 return rag_pipeline(query)这类问候和告别只做字符串匹配跳过整个RAG流水线响应时间从10秒降到0.1秒。七、整体交互体验总结用户使用这台语音助手的典型流程说你好 → 0.1秒收到回应问ROS中的TF2是什么 → 听到检索中轻微等待约5秒上下文处理然后开始流式播报听到一半发现问题 → 说停 → 播报立即中断换个方式追问 → 继续对话说再见 → 程序退出整个过程不碰键盘不联网所有数据留在本地。