当前位置: 首页 > news >正文

【AI实战日记-手搓聊天机器人】Day 13:彻底解放双手!基于 VAD 算法实现 AI 自动静默检测与连续对话

Day 11-12 我们完成了语音的输入输出,但交互方式依然停留在“按键触发”的原始阶段。今天是 Day 13,我们将引入 VAD (语音活动检测) 算法。通过计算音频流的能量阈值 (RMS),让 Project Echo 能够自动判断用户何时开始说话、何时停止说话。我们将重构录音模块,实现 “唤醒 -> 自动聆听 -> 自动静默检测 -> 自动回复” 的全自动闭环,打造真正的 Hands-free 对话体验。


一、 项目进度:Day 13 启动

根据项目路线图,今天是 Phase 4 的收官之战。
我们要把零散的语音模块整合成一个流畅的系统。

pic1

 

二、 核心原理:VAD (语音活动检测)

1. 痛点:“按键说话”显得很傻

真实的人类交流是流式的。你不会跟朋友聊天时说:“等一下,我按下按钮再跟你说话。”
我们要让 AI 像人一样,通过听觉判断对话节奏。

2. 解决方案:能量检测算法 (Energy Threshold)

最简单的 VAD 实现方式是基于音量(能量)

  • 监听状态:程序实时读取麦克风数据块(Chunk)。

  • 计算能量:计算每个块的 RMS (均方根) 值,代表音量大小。

  • 逻辑判断

    1. 触发 (Trigger):音量 > 阈值(Threshold),判定为“开始说话”。

    2. 维持 (Active):持续录音。

    3. 静默 (Silence):音量 < 阈值,且持续了 X 秒(例如 2秒),判定为“说话结束”。

3. 核心组件:VADRecorder (自动录音机)

在 Day 13 的代码中,VADRecorder 是实现 Hands-free 体验的绝对核心。它不再是一个简单的录音脚本,而是一个具备**“听觉感知”**能力的实时状态机。

它的工作原理基于 能量检测 (Energy-based Detection),具体包含以下三个关键要素:

(1) 听觉量化:RMS (均方根)

计算机听不懂声音的内容,但它能感知声音的震动强度
我们使用 audioop.rms(chunk) 方法来计算每一小段音频块(Chunk)的能量值

  • 环境噪音:RMS 通常在 100~500 之间。

  • 说话声音:RMS 通常在 1000~5000 之间(取决于麦克风距离)。

(2) 两个核心参数 (The Knobs)

我们在代码中定义了两个“旋钮”来调节 AI 的听觉敏感度:

  • threshold (触发阈值):比如设为 1000。声音超过这个线,AI 认为“有人说话了”。

  • silence_limit (静默容忍度):比如设为 2.0 秒。意思是:“如果声音低于阈值持续了 2 秒,我就认为你说完了。”

(3) 录音状态机 (The State Machine)

VADRecorder 的内部逻辑其实是一个在 [监听] 和 [录制] 之间切换的状态机。

请看下面的逻辑流转图:

pic2

(4) 为什么这种设计体验好?
  • 抗干扰:通过 Threshold,过滤掉键盘声、空调声等低频噪音,只有人声才能唤醒录音。

  • 自然断句:通过 Silence Limit,允许用户在说话中间有短暂的停顿(思考),而不会因为喘口气就被强行打断。

总结:VADRecorder 就像一个不知疲倦的守门员,它时刻盯着麦克风的数据流,只有当“真正的声音”出现时,才会打开大门进行录制,从而实现了全自动交互


三、 实战:代码实现

1. 升级录音模块 (src/core/vad_recorder.py)

我们不再使用 Day 12 那个简陋的 AudioRecorder,而是新建一个高级的 VADRecorder。
这个类会自动计算音量,并自动停止录音。

新建文件:src/core/vad_recorder.py

import pyaudio
import wave
import audioop
import math
import time
from collections import deque
from src.utils.logger import loggerclass VADRecorder:def __init__(self, filename="input_voice.wav"):self.filename = filenameself.chunk = 1024self.format = pyaudio.paInt16self.channels = 1self.rate = 44100# --- VAD 核心参数 ---# 能量阈值 (根据你的麦克风灵敏度调整,通常 500-3000)self.threshold = 1000  # 静默超时 (秒):说完话后停顿多久算结束self.silence_limit = 2.0  self.pyaudio_instance = pyaudio.PyAudio()def listen_and_record(self):"""全自动录音:等待声音 -> 开始录音 -> 检测静默 -> 停止录音"""stream = self.pyaudio_instance.open(format=self.format,channels=self.channels,rate=self.rate,input=True,frames_per_buffer=self.chunk)logger.info("👂 正在聆听... (请直接说话)")frames = []started = Falsesilence_start_time = Nonewhile True:data = stream.read(self.chunk)# 计算当前块的音量 (RMS)rms = audioop.rms(data, 2)if not started:# 阶段 1: 等待声音触发if rms > self.threshold:started = Truelogger.info("🎤 检测到声音,开始录音...")frames.append(data)else:# 阶段 2: 正在录音frames.append(data)if rms < self.threshold:# 如果当前是静音if silence_start_time is None:silence_start_time = time.time()else:# 检查静默是否超时if time.time() - silence_start_time > self.silence_limit:logger.info("🛑 检测到静默,录音结束")breakelse:# 如果又有声音了,重置静默计时器silence_start_time = None# 停止流stream.stop_stream()stream.close()# 保存文件self._save_file(frames)return Truedef _save_file(self, frames):wf = wave.open(self.filename, 'wb')wf.setnchannels(self.channels)wf.setsampwidth(self.pyaudio_instance.get_sample_size(self.format))wf.setframerate(self.rate)wf.writeframes(b''.join(frames))wf.close()

2. 终极集成 (main.py)

我们需要修改主循环。不再是 input() 阻塞,而是 vad_recorder.listen_and_record() 阻塞。

修改 main.py

# ==============================================================================
# Project Echo Day 9: Multi-Query RAG Integration
# 集成特性: Redis记忆 + 情绪识别 + Multi-Query知识库检索
# ==============================================================================# LangChain LCEL 核心组件
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from operator import itemgetter  # 【修复】用于从字典中提取字段# 项目核心模块
from src.core.llm import LLMClient
from src.core.prompts import PROMPTS
from src.utils.logger import logger
from src.config.settings import settings
from src.core.emotion import EmotionEngine   # Day 5: 情绪
from src.core.knowledge import KnowledgeBase # Day 7-9: 知识库
from src.core.reranker import RerankEngine   # Day 10: 重排序
from src.core.voice import VoiceEngine # 导入语音引擎
from src.core.vad_recorder import VADRecorder 
from src.core.stt import STTEngine          # --- Redis 历史记录工厂 (Day 6) ---
def get_session_history(session_id: str) -> BaseChatMessageHistory:return RedisChatMessageHistory(session_id=session_id,url=settings.REDIS_URL,ttl=3600 * 24 * 7 # 记忆保留 7 天)# --- 辅助函数:格式化文档 ---
def format_docs(docs):return "\n\n".join([d.page_content for d in docs])def main():logger.info("🚀 --- Project Echo: Day 9 集成版启动 ---")# ==========================================# 1. 初始化组件# ==========================================# 1.1 大模型 (Brain)client = LLMClient()llm = client.get_client()# 1.2 情绪引擎 (Heart)emotion_engine = EmotionEngine()# 1.3 知识库 (Book)kb = KnowledgeBase()# 【Day 9 核心】获取多重查询检索器# 这里我们将 LLM 传进去,让检索器具备"思考"能力base_retriever = kb.get_multiquery_retriever(llm)# 【Day 10 新增】重排序引擎:对粗排结果进行精排reranker = RerankEngine(model_name="BAAI/bge-reranker-base", top_n=3)# 封装成 LCEL Runnable:先粗排,再精排def retriever_with_rerank(query: str):docs = base_retriever.invoke(query)return reranker.rerank(query, docs)retriever = RunnableLambda(retriever_with_rerank)# ==========================================# 2. 构建 Prompt 模板# ==========================================sys_prompt_base = PROMPTS["tsundere"]prompt = ChatPromptTemplate.from_messages([("system", sys_prompt_base),                   # 1. 基础人设 (傲娇)("system", "{emotion_context}"),               # 2. 情绪指令 (动态注入)("system", "【参考资料(必须基于此回答)】:\n{context}"), # 3. 知识库资料 (RAG)MessagesPlaceholder(variable_name="history"),  # 4. 历史记忆 (Redis)("human", "{input}")                           # 5. 用户输入])# ==========================================# 3. 组装 LCEL 流水线# ==========================================rag_chain = ({# 分支 A: 智能检索 (用户输入 -> 裂变3个问题 -> 并行检索 -> 汇总 -> 格式化)"context": itemgetter("input") | retriever | format_docs,  # 【修复】使用 itemgetter 提取 input# 分支 B: 透传参数 (直接传递给 Prompt)"input": itemgetter("input"),              # 【修复】提取 input 字段"emotion_context": itemgetter("emotion_context"),  # 【修复】提取 emotion_context 字段"history": itemgetter("history")           # 【修复】提取 history 字段}| prompt  # 填入模板| llm     # 大模型推理)# ==========================================# 4. 挂载持久化记忆# ==========================================final_chain = RunnableWithMessageHistory(rag_chain,get_session_history,input_messages_key="input",history_messages_key="history",)print("\n✨ 系统就绪!试试问得模糊一点,比如“那个写代码的人爱吃啥?”\n")session_id = "user_day9_demo"voice_engine = VoiceEngine(voice="zh-CN-XiaoyiNeural")# recorder = AudioRecorder()vad_recorder = VADRecorder()stt_engine = STTEngine(model_size="base")# ==========================================# 5. 对话循环# ==========================================while True:# --- Step 1: 录音 (Recording) ---vad_recorder.listen_and_record()user_input = stt_engine.transcribe("input_voice.wav")if not user_input or len(user_input) < 2:print("⚠️ 似乎是杂音,忽略...")continueprint(f"\nYou (语音): {user_input}")# --- Step 2: 识别 (STT) ---user_input = stt_engine.transcribe()if not user_input:print("⚠️ 没听清,请再说一遍。")continueprint(f"You (语音): {user_input}")if user_input.strip():# --- Phase A: 情绪侦探 ---current_emotion = emotion_engine.analyze(user_input)emotion_instruction = "用户情绪平稳。"if "[愤怒]" in current_emotion:emotion_instruction = "⚠️ 警告:用户很生气!请示弱道歉。"elif "[悲伤]" in current_emotion:emotion_instruction = "⚠️ 提示:用户很难过。请温柔安慰。"try:# --- Phase B: 执行 RAG 主链 ---# 这一步会自动触发 Multi-Query 检索logger.info("🔍 正在进行多重检索与思考...")response = final_chain.invoke({"input": user_input, "emotion_context": emotion_instruction},config={"configurable": {"session_id": session_id}})ai_text = response.contentprint(f"Bot ({current_emotion}): {ai_text}\n")voice_engine.speak(ai_text, emotion=current_emotion)except Exception as e:logger.error(f"❌ 调用失败: {e}")if __name__ == "__main__":main()

四、 效果验证:丝滑的对话体验

现在运行 python main.py,你将获得类似 钢铁侠 Jarvis 的体验:

  1. 启动:程序显示 👂 正在聆听...

  2. 你开口:“你好傲娇酱。”(无需按任何键)

  3. 检测:程序显示 🎤 检测到声音...

  4. 你闭嘴:(停顿 2 秒)

  5. 处理:程序自动停止录音 -> 识别文字 -> 思考 -> 播放语音。

  6. 循环:语音播放完毕后,程序立刻又变回 👂 正在聆听...,等待你的下一句话。


五、 总结与预告

今天我们通过引入 VAD 技术,彻底消灭了物理按键。Project Echo 变成了一个**“永远在线 (Always Listening)”** 的智能体。
虽然它现在还运行在 Python 终端里,但它的核心交互逻辑已经和 Siri、小爱同学没有区别了。

Phase 5 预告 (Day 14)
作为一个完美的收官,我们不能总是在黑乎乎的命令行里玩。
明天 Day 14,我们将进入 Phase 5:全栈部署。我们将使用 Streamlit,为 Project Echo 穿上一件漂亮的 Web UI 外衣,把聊天气泡、历史记录、语音波形都可视化展示出来!

 

 

参考资料:

 

http://www.jsqmd.com/news/674016/

相关文章:

  • FanControl终极修复指南:快速解决传感器计数异常问题
  • 同济大学与腾讯联手,如何用“画风配方“造出史上最大风格图库?
  • 谈谈“内卷”与“躺平”:技术人的另一种可能性
  • PHP源码运行是否受硬盘转速影响_7200转vs5400转对比【指南】
  • **点云处理新范式:基于Python的高效三维数据滤波与分割实战**在自动驾
  • 简易在线考试系统(数学版)——结对编程实验报告
  • Codex + 自建中转站,用不完的token+GPT5.4 做成了一个AI机器人
  • 从乘客头衔到船舱号:手把手教你用Python挖掘泰坦尼克号数据里的隐藏特征
  • 如何防止SQL触发器导致事务超时_拆分逻辑为异步队列处理
  • MySQL Explain 查询计划详解
  • 2025-2026年国际东南亚专线物流公司推荐:TOP5口碑服务评测对比顶尖B2B大宗贸易港口拥堵 - 品牌推荐
  • **构建去中心化金融新范式:基于Solidity的DeFi协议开发实战解析**在区块链技术飞速发展的今天,**
  • Cy5-Fe₃O₄ NPs,Cy5标记四氧化三铁纳米颗粒,反应步骤
  • DAMO-YOLO入门指南:理解COCO 80类标准与达摩院扩展类别的映射关系
  • 【大白话说Java面试题】【Java基础篇】第2题:Iterator的fail-fast和fail-safe机制有什么区别?
  • Dify日志审计配置总失败?92%团队忽略的时区陷阱、权限继承断层与审计缓冲区溢出问题全解析,立即修复!
  • 2025-2026年全球25-30万五座SUV车型推荐:五款口碑产品评测对比顶尖城市通勤成本高昂 - 品牌推荐
  • Shopee一面:你使用 RAG 给大模型一个输入,系统是怎样的工作流程?
  • 攻击者可利用的 FortiSandbox 漏洞 PoC 公开,可执行任意命令
  • 从航拍到模型:手把手教你用‘焦距’和‘像元尺寸’反算无人机航高(附Excel计算工具)
  • 88.合并两个有序数组
  • 创建pg_trgm插件报错,提示:“错误,操作符 % 已经存在”
  • 算法训练营第八天|88.合并两个有序数组
  • Dify多模态Pipeline调试失败率下降82%的关键动作:OpenTelemetry埋点+自定义Trace Context注入实战
  • 2026年4月25-30万五座SUV车型推荐:五款口碑产品评测对比顶尖家庭出行空间焦虑 - 品牌推荐
  • Ollama + ModelScope:本地大模型极简部署
  • WuliArt Qwen-Image Turbo部署案例:中小企业AI设计助手低成本GPU部署实践
  • Dify工业知识库性能压测实录:10万页PDF+2000+设备BOM结构,QPS 47.3仍稳如磐石
  • Claude Opus 4.7 API 接入指南:最强模型实测与中转配置教程(2026)
  • 警惕AI全自动攻击!Claude Opus成功构建Chrome漏洞武器化链路