47 KiB
功能目标
完整业务价值
当前 run-live 已能完成真实重复语音对话,但唤醒路径仍把麦克风片段送入 STT/ASR 判断“是否包含小杰小杰”。这种做法会把一次唤醒变成一次完整语音识别请求,造成明显延迟;同时当用户连续说“小杰小杰”或环境里有回声时,唤醒片段会被混入正式用户问题,最终出现终端中“思考中:杰小杰。小杰小杰...”这类错误输入。本变更的业务价值是把“唤醒词检测”从“用户问题转写”中彻底拆开,形成独立、本地、低延迟的 wake pipeline,并让用户在终端中看到被识别出来的正式问题文本。
真人验收继续暴露出第二类问题:即使唤醒和正式 STT 已拆开,run-live 仍然是一段串行脚本式流程,录音端点、状态输出、错误恢复、TTS 播放和上下文推进耦合在同一个 runtime 方法里;一旦 VAD 能量兜底被背景噪声拖住,用户第一次说完后不会马上进入 STT,最终把第二次重复提问也拼进同一段音频。参考 Home Assistant Assist、Rhasspy、Wyoming、OpenVoiceOS/Mycroft 的公开架构后,本变更继续把 live runtime 重构为 stage 化语音助手 pipeline:音频输入、唤醒、应答、正式问题采集、STT、对话上下文、LLM、TTS、播放和恢复待机各自独立,通过统一事件总线向终端和后续 GUI 汇报状态。
目标用户是正在本机运行语音桌宠的使用者。用户希望桌宠像真实语音助手一样先快速识别“小杰小杰”,进入听取问题状态,再把后续问题转写显示出来并交给 LLM;用户不希望每次唤醒都等待云端 ASR,也不希望唤醒词本身污染正式对话历史。
目标用户场景
- 低延迟唤醒:用户说“小杰小杰”,本地 KWS 模型在麦克风流中命中后立即输出“唤醒命中”,不调用云端 ASR 判断唤醒词。
- 正式问题转写:用户唤醒后说“帮我记住苹果这个词”,终端必须显示“转写结果:帮我记住苹果这个词”,随后才进入“思考中”。
- 重复对话:第一轮提问和回复完成后恢复待机,第二轮再次通过本地 KWS 唤醒,并携带本次运行进程内历史。
- 模型诊断:
download_speech_models.py --dir models必须下载/准备 wake KWS、VAD、STT 所需模型;model-check必须检查 wake 模型文件。 - 故障恢复:本地 wake 模型缺失、加载失败或检测失败时,启动阶段必须清楚报错,不得静默回退到云端 ASR 唤醒。
量化成功指标 KPI
- 唤醒路径零云端 ASR:自动化测试必须证明 wake 阶段不调用
SttProvider.transcribe();STT 只用于唤醒后的正式问题。 - 唤醒污染清零:LLM 请求中的当前 user message 不得包含“小杰小杰”唤醒词,除非用户在正式问题里明确重复说出该词。
- 终端可见转写:每轮正式 STT 完成后,终端必须输出一条包含用户问题文本的转写消息,且该消息出现在 LLM 请求之前。
- 本地 wake 模型验收:
model-check --models-dir models必须检查 KWS tokens、encoder、decoder、joiner、keywords 文件。 - 重复对话不回退:两轮 fake runtime 测试必须验证两次 wake、两次正式 STT、两次 LLM、两次 TTS、两次播放,并最终恢复待机。
- 真实运行可用:在模型、设备和
.env齐全时,run-live --once仍可完成一轮真实语音交互。 - Pipeline 事件顺序稳定:自动化测试必须验证成功 turn 的事件顺序至少包含
wake_listening -> wake_detected -> ack_started -> capture_started -> speech_started -> speech_ended -> stt_started -> transcript_final -> llm_started -> tts_started -> playback_finished -> standby_resumed。 - 主说话人端点有效:当用户只说一次“你是谁”并在后续出现背景噪声或第二次重复提问时,第一轮音色消失连续约 300 ms 后必须结束采集,不得把第二次重复提问拼进同一个
AudioSegment。
预期影响
- OpenSpec 主规范将明确 wake word detection 必须优先使用本地 KWS 模型,不再把完整 ASR 作为默认唤醒实现。
- 模型目录将新增 wake KWS 模型和关键词文件,仍位于 ignored
models/,不提交 Git。 - Runtime 将新增
wakewordProvider 依赖,唤醒检测直接消费麦克风帧;正式问题录音和 STT 在唤醒后才开始。 - 终端 reporter 将新增转写输出能力,用于显示正式问题文本。
- 测试将更新旧的两轮 runtime 断言:STT 调用次数从“wake+question 每轮两次”改为“question 每轮一次”。
- README 将说明首次准备需要下载 wake/VAD/STT 本地模型,唤醒词检测为本地模型路径。
- Runtime 将新增
VoiceAssistantPipeline、TurnController和 pipeline event bus;run-live通过统一 pipeline 执行,旧LiveVoiceRuntime仅作为兼容入口或测试辅助,不再承载新的 live 主流程。 - 采集阶段将新增本轮临时主说话人端点,默认不保存声纹、不跨进程记忆、不新增长期主人注册流程。
对现有问题的系统性总结
- 性能问题:旧实现每次检测唤醒都要先用 VAD 截出一段音频,再调用 STT/ASR 转文字,wake latency 被网络、模型转写和 VAD 端点共同放大。
- 功能问题:旧实现从 wake transcript 中提取 wake word 后,可能把残留文本或重复唤醒词当成用户正文,导致 LLM 输入污染。
- UI/UX 问题:终端只显示“思考中:xxx”,用户无法明确分辨“这是转写结果”还是“LLM 正在处理”,也看不到 STT 阶段的实际识别文本。
- 架构问题:wake、VAD、STT 的职责边界不清晰,
LiveVoiceRuntime._wait_for_wake_and_user_text()同时承担唤醒识别、唤醒词剥离、问题录音和问题转写。 - 测试问题:现有 live runtime 测试把 wake transcript 放进
QueueSttProvider,等于把“唤醒必须经过 STT”固化成测试事实。 - 模型管理问题:已有
models/manifest 只覆盖 VAD/STT,不覆盖专用 KWS 模型和关键词表。 - Pipeline 边界问题:live runtime 缺少 stage 级事件和 controller,终端输出、错误恢复、TTS 播放、STT 调用顺序散落在同一个类里,后续 GUI 桌宠无法复用稳定事件。
- 端点问题:
HybridVadProvider用“本地 VAD 或能量阈值任一为语音”同时控制开始和结束,底噪偏高时会持续重置静音计数,导致录音结束慢。 - 首句保留问题:真人验收显示唤醒应答后仍会感觉“第一句话没有获取到”,当前 ACK 后会执行
flush_input -> read/drop -> flush_input,默认额外丢弃 50 ms 麦克风输入,用户若紧跟提示开口会损失正式问题开头。 - 实时消费问题:
SoundDeviceAudioTransport.read_frames()每次只返回一个队列帧,在音频回调批量积压时会增加 pipeline 对真实麦克风流的追帧成本。 - 画像门槛问题:
PrimarySpeakerVadRecorder._profile_ready()把主说话人画像就绪阈值绑定到OWNER_VAD_MIN_DURATION_MS,默认至少等待 250 ms 后主说话人端点才参与结束判断,短句用户会被迫等普通 VAD 静音或重复说话。 - 实时转写问题:当前终端只在整段录音结束并完成 final STT 后显示“转写结果”,用户说话期间看不到任何文字反馈,无法判断系统是否已经听到并识别当前句子。
- 实时字幕质量问题:真人日志中
实时转写:家、实时转写:家确来自旧本地 14M streaming STT partial,最终云端 ASR 虽然较准,但用户看到的实时反馈会被单字噪声和短暂跳变污染。 - 语音链路一致性问题:当前
OWNER_SPEECH_PROVIDER=cloud时 final STT/TTS 走云端,而 partial 走本地模型;同一轮对话里 partial 和 final 来自不同模型,容易出现“实时字幕和最终转写明显冲突”的体验。 - 降噪缺失问题:正式问题录音直接把原始麦克风帧送入 VAD、partial STT 和 final STT,背景噪声会同时影响端点、实时字幕和最终识别。
- 本地模型落后问题:默认 STT 仍是 2023 年 14M 小模型,适合最小验收但不适合作为默认实时字幕质量基线;应升级为 sherpa-onnx 官方 2025 中文 CTC int8 模型。
- 自测闭环问题:现有
acceptance只覆盖旧单轮 pipeline,不能证明当前VoiceAssistantPipeline在“模拟麦克风 -> wake -> capture -> partial -> final -> LLM -> TTS -> standby -> 第二轮”路径上完整正常;真人测试前缺少可重复的自动调试入口。
详细需求
功能需求
run-liveSHALL 使用本地 wake word Provider 检测“小杰小杰”,默认实现为sherpa-onnxKeywordSpotter或等价本地 KWS 模型。- wake 检测 SHALL 直接消费麦克风流
AudioFrame,不得调用云端 ASR,也不得调用正式问题SttProvider.transcribe()。 - wake 命中后 SHALL 重置 VAD/录音缓冲,并进入“请说出问题/录音中”状态。
- 正式问题的音频 SHALL 从 wake 命中后开始采集;wake 音频不得传入 LLM 上下文。
- 正式问题 STT 完成后 SHALL 立即输出终端转写文本,例如“转写结果:帮我记住苹果这个词”。
- LLM 请求 SHALL 只携带正式问题文本和本次运行临时历史,不携带 wake transcript 或 KWS 结果字符串。
scripts/download_speech_models.py --dir modelsSHALL 下载或准备 KWS 模型,生成models/wake/keywords.txt。model-checkSHALL 检查 wake KWS 模型关键文件和关键词文件,并尝试加载 wake provider。.env.exampleSHALL 增加 wake provider 配置:OWNER_WAKE_PROVIDER=local_kws、wake 模型/阈值/关键词配置。- README SHALL 明确“唤醒使用本地模型,ASR/TTS 可按
OWNER_SPEECH_PROVIDER走 cloud 或 local”。 run-liveSHALL 使用统一VoiceAssistantPipeline执行 turn-based 语音助手流程,stage 之间通过明确输入输出传递,不得让 wake 音频、正式问题音频、LLM 上下文或 TTS 播放状态互相污染。- Pipeline SHALL 发出稳定事件:
pipeline_started、wake_listening、wake_detected、ack_started、capture_started、speech_started、speech_ended、stt_started、transcript_final、llm_started、tts_started、playback_finished、standby_resumed、stage_error。 - 正式问题采集 SHALL 默认使用本轮临时主说话人端点:唤醒后以正式问题开头短音频建立本轮音色画像,主说话人音色连续消失达到配置时间后结束采集。
- 主说话人端点 SHALL 不保存长期声纹、不写音频文件、不跨进程复用音色画像。
- ACK 后输入清理 SHALL 只清除播放期间已积压的麦克风缓冲,不得在播放结束后额外读取并丢弃新的正式问题音频;默认
OWNER_POST_PLAYBACK_DRAIN_MSSHALL 为0。 - SoundDevice 音频输入 SHALL 支持一次读取当前队列中可用的多个帧,避免 pipeline 在真实麦克风输入积压时逐帧追赶。
- 主说话人画像就绪 SHALL 使用独立配置
OWNER_SPEAKER_PROFILE_MIN_MS,默认120ms;该阈值不得被OWNER_VAD_MIN_DURATION_MS放大。 - 主说话人端点在画像就绪后 SHALL 以
OWNER_SPEAKER_ABSENT_MS作为主要结束条件;主说话人连续缺席达到配置值后 SHALL 结束采集,不得额外等待普通 VAD 的最小时长门槛。 - 录音期间 SHALL 支持 partial transcript 事件;当本地 streaming STT 产生新的中间文本时,终端 SHALL 立即显示
实时转写:<文本>。 - partial transcript SHALL 只作为用户可见反馈,不得直接写入对话上下文;LLM 输入仍以最终
transcript_final文本为准。 - 当
OWNER_SPEECH_PROVIDER=cloud时,partial transcript SHALL 使用本地 streaming STT,避免对云端 ASR 进行高频请求。 - 下一版默认语音链路 SHALL 改为“除 LLM 外全本地”:wake、VAD、STT、partial transcript、TTS、噪音过滤均在本机执行;LLM 仍走配置中的云端 OpenAI-compatible endpoint。
.env.example和代码默认 SHALL 将OWNER_SPEECH_PROVIDER设为local,从而默认 final STT 使用本地 sherpa-onnx 模型,TTS 使用 macOS 本地say/afplayprovider。- 模型 manifest SHALL 默认使用 sherpa-onnx 官方 2025 中文 Zipformer2 CTC int8 模型,关键文件为
tokens.txt和model.int8.onnx,不再以旧 14M transducer 作为默认 STT 模型。 - 模型 manifest SHALL 新增 GTCRN/sherpa-onnx speech denoiser 模型
denoise/gtcrn_simple.onnx,download_speech_models.py和model-check必须把它列为 required file。 - Pipeline SHALL 新增
AudioPreprocessStage。默认OWNER_NOISE_FILTER_ENABLED=1时,唤醒后的正式问题采集必须先对音频帧降噪,再把同一份降噪后帧送入 VAD、实时 partial STT 和 final STT segment。 - wake 阶段默认 SHALL 继续使用原始音频帧,避免 denoiser 改变 KWS 特征;仅当用户显式设置
OWNER_WAKE_DENOISE_ENABLED=1时才允许对 wake 帧预处理。 - 降噪 provider 失败 SHALL 作为结构化可恢复错误进入
stage_error -> recovering -> standby,不得把未经标记的半处理音频写入对话上下文。 - partial transcript SHALL 增加稳定过滤:不得显示单个中文/英文有效字符,不得重复显示同一文本,不得把极短的瞬态跳变作为终端实时字幕输出。
- final transcript SHALL 是唯一进入 LLM 的用户文本;降噪帧、partial 文本、denoiser metadata 和 ASR raw metadata 均不得进入
ConversationContext。 - 系统 SHALL 提供
owner_voice_pet simulate-live命令,用模拟麦克风帧驱动当前VoiceAssistantPipeline,默认完成两轮 wake-to-playback turn。 simulate-liveSHALL 输出 JSON 检查项,至少包含完成轮数、失败轮数、wake/speech/STT/LLM/TTS/standby 事件计数、final transcripts、partial 噪声过滤、第二轮临时上下文和播放段数。- 模拟麦克风输入 SHALL 包含 wake 帧、正式问题主说话人帧、短噪声 partial、背景噪声/非主说话人帧和第二轮重复唤醒帧。
- 模拟 transport SHALL 有边界保护:如果帧提前耗尽,命令必须结构化失败并退出,不能无限等待。
simulate-liveSHALL 支持写入和回放 JSONL fixture,便于后续持续复现同一组模拟麦克风输入。
非功能需求
- 性能:本地 wake 检测目标是在本地模型可用时 800 ms 内给出可见“唤醒命中”状态;wake 不受 LLM/ASR 网络延迟影响。
- UI/UX:终端输出必须区分“待机监听唤醒”“唤醒命中”“录音中”“转写中”“转写结果”“思考中”“播放中”“恢复待机”。
- 安全:wake/KWS 在本机执行,不上传连续麦克风流;
.envkey 和models/仍不得提交。 - 可扩展性:wake Provider 必须是独立接口,后续可以替换为 Porcupine、CoreML、Apple Speech 或其他本地 KWS,不影响 STT/LLM/TTS。
- 测试性:自动化测试必须能注入 fake wake provider,不依赖真实麦克风或真实 KWS 模型。
- 架构可观测性:所有用户可见状态必须来自 pipeline event bus,终端 reporter 和后续 GUI 只消费事件,不直接嵌入 stage 逻辑。
- 端点性能:默认配置下,主说话人音色消失后 300 ms 左右应结束采集,并进入 STT;最大录音时长仍作为兜底。
- 实时字幕质量:本地 partial 默认不显示 1 个有效字符以内的文本,避免
家、嗯、啊这类背景噪声触发可见字幕;最终字幕仍由 final STT 决定。 - 语音隐私:除 LLM 请求文本外,正式问题原始音频、降噪音频、VAD 特征和临时音色画像均不得上传云端、不得落盘。
- 降噪性能:GTCRN online denoiser 只能在 capture 阶段运行,目标是不明显拖慢端点;如果 denoiser 不可用,启动/模型检查必须明确失败,而不是静默回退为无降噪。
边缘案例
- wake 模型缺失:
run-live启动前失败,错误指向model-check或下载脚本,不回退到云端 ASR。 - wake 模型加载失败:输出结构化
WAKE_MODEL_LOAD_FAILED,不进入假待机。 - 未命中唤醒词:保持待机,不调用 STT、LLM、TTS。
- wake 命中后用户不说话:VAD no-speech timeout 后恢复待机,不调用 LLM。
- STT 返回空文本:输出空转写/错误并恢复待机,不调用 LLM。
- 用户正式问题中包含“小杰小杰”:只有 wake 命中后的正式录音内容可进入上下文;如果用户确实在正式问题中重复该词,允许保留。
- 播放回声误触发:播放期间仍遵循既有音频反馈抑制要求,不能把 TTS 当作新 wake。
- 背景噪声拖尾:用户停止说话后若仍有非主说话人或噪声,主说话人端点必须允许结束录音。
- 音色画像不足:如果开头音频太短或能量不足,采集阶段必须回退到普通 VAD 静音端点,不能卡死。
- 降噪模型缺失:
model-check必须报告denoise/gtcrn_simple.onnx缺失;run-live启动时不得进入“看似可用但未降噪”的状态。 - 降噪运行时异常:如果 GTCRN provider 在某帧处理失败,当前 turn 必须结构化报错并恢复待机;不得把可能损坏的片段送入 STT 或 LLM。
- partial 单字误识别:本地 streaming STT 输出
家、确、a等单字时,终端不得显示为实时转写。 - partial 瞬态跳变:本地 streaming STT 从“你是谁”短暂跳到“加”再回到“你是谁”时,不得显示中间极短跳变。
- CTC 模型缺旧 transducer 文件:当 manifest 类型为
sherpa-onnx-streaming-zipformer2-ctc时,不应再要求 encoder/decoder/joiner 文件存在。
输入输出规格
新增 .env 输入:
OWNER_WAKE_PROVIDER=local_kws:第一版默认本地 KWS 唤醒。OWNER_WAKE_KEYWORD=小杰小杰:唤醒词。OWNER_WAKE_KEYWORDS_FILE=models/wake/keywords.txt:KWS 关键词表路径。OWNER_WAKE_KWS_THRESHOLD=0.25:KWS 命中阈值。OWNER_WAKE_KWS_SCORE=1.0:KWS 关键词分数。OWNER_PIPELINE_MODE=live_turn_based:第一版固定 turn-based pipeline。OWNER_ENDPOINT_MODE=primary_speaker:默认主说话人端点;可设为vad回退普通 VAD。OWNER_SPEAKER_PROFILE_MS=600:建立本轮临时音色画像的目标音频长度。OWNER_SPEAKER_ABSENT_MS=300:主说话人音色连续消失多少毫秒后结束录音。OWNER_SPEAKER_SIMILARITY_THRESHOLD=0.70:音色相似度阈值。OWNER_SPEAKER_MIN_RMS=0.012:进入音色画像/匹配的最低能量。OWNER_CONTEXT_MODE=session_memory:本次进程内临时上下文。OWNER_POST_PLAYBACK_DRAIN_MS=0:ACK 或 TTS 播放完成后只 flush 已积压输入,不额外读取并丢弃新音频。OWNER_SPEAKER_PROFILE_MIN_MS=120:主说话人画像参与端点判断的最低有效语音长度。OWNER_REALTIME_TRANSCRIPT_ENABLED=1:启用录音期间本地 streaming STT 中间结果显示。OWNER_SPEECH_PROVIDER=local:默认本地 STT/TTS,云端只保留 LLM。OWNER_NOISE_FILTER_ENABLED=1:正式问题阶段默认启用本地降噪。OWNER_WAKE_DENOISE_ENABLED=0:wake 阶段默认不启用降噪。OWNER_NOISE_FILTER_PROVIDER=sherpa_onnx_gtcrn:第一版本地降噪 provider。
终端输出:
- 待机:
[第N轮] 待机:等待唤醒词“小杰小杰” - 唤醒:
[第N轮] 唤醒命中:请说出问题 - 转写中:
[第N轮] 转写中:正在识别问题 - 转写结果:
[第N轮] 转写结果:<正式问题文本> - 思考:
[第N轮] 思考中:正在生成回复
数据验证规则
- wake provider 只允许
local_kws和测试注入 provider;无效配置启动失败。 - wake keywords 文件必须存在且非空。
- KWS 模型 tokens、encoder、decoder、joiner 文件必须存在。
- 终端转写输出不得包含 API key 或 Authorization header。
- LLM messages 的最后一条 user content 必须等于正式 STT 文本。
设计方案
文字版全新架构图
SoundDeviceAudioTransport
-> PipelineEventBus
-> LocalWakeWordProvider(sherpa-onnx KeywordSpotter, models/wake, keywords.txt)
-> wake_hit
-> AcknowledgeStage("我在")
-> CaptureStage(raw user utterance only)
-> AudioPreprocessStage(sherpa-onnx GTCRN denoise, capture only)
-> PrimarySpeakerEndpoint + VAD(denoised frames)
-> RealtimeSttProvider(local CTC, denoised frames, stable partial filter)
-> SttProvider(local CTC by default, denoised final segment)
-> PipelineEvent(transcript_final)
-> ConversationContext(temporary process history)
-> Cloud LLM
-> Local TTS(MacSayTtsProvider)
-> Speaker
-> standby
数据流
- 启动时加载
.env、wake KWS 模型、VAD、正式 STT、LLM、TTS。 - 待机阶段持续读取麦克风帧,调用
wakeword.detect(frame)。 - KWS 返回 wake event 后输出“唤醒命中”,重置 VAD 和输入缓冲。
- 进入正式问题录音,VAD 判断用户问题起止。
- 正式问题结束后调用 STT。
- STT 成功后调用
reporter.transcript(user_text, final=True),终端立即显示转写结果。 - Runtime 将
user_text追加到临时上下文并调用 LLM。 - TTS 播放后追加 assistant 历史并恢复待机。
- 所有 stage 同步发出 pipeline events;终端 reporter 只把事件映射为中文文案。
- 正式问题 capture 收到原始麦克风帧后,先调用
AudioPreprocessor.process_frame(frame)得到降噪帧。 - VAD、主说话人端点、partial streaming STT 和 final STT segment builder 必须使用同一份降噪帧,保证用户看到的 realtime partial 和最终转写来自一致音频来源。
- wake listening 默认使用原始帧;后续仅在
OWNER_WAKE_DENOISE_ENABLED=1时把同一预处理接口接到 wake 阶段。
接口定义
WakeWordProvider.load() -> None
WakeWordProvider.detect(frame: AudioFrame) -> WakeEvent | None
WakeWordProvider.reset() -> None
RuntimeReporter.transcript(text: str, final: bool, turn_id: int | None = None) -> None
PipelineEvent(type: str, turn_id: int | None, state: PipelineState | None, message: str, payload: dict)
PipelineEventBus.emit(event_type: str, *, turn_id: int | None, state: PipelineState | None, message: str, payload: dict | None) -> PipelineEvent
PipelineEventBus.subscribe(listener: Callable[[PipelineEvent], None]) -> None
TurnController.run_turn(turn_id: int) -> TurnResult
VoiceAssistantPipeline.run(once: bool = False, max_turns: int | None = None) -> RuntimeSummary
AudioPreprocessor.load() -> None
AudioPreprocessor.reset() -> None
AudioPreprocessor.process_frame(frame: AudioFrame) -> AudioFrame
AudioPreprocessor.flush() -> list[AudioFrame]
SherpaOnnxDenoiserPreprocessor(
models_dir: Path,
provider: "sherpa_onnx_gtcrn",
enabled: bool,
sherpa_module: object | None = None,
)
SherpaOnnxSttProvider.load()
manifest type "sherpa-onnx-streaming-transducer" -> OnlineRecognizer.from_transducer(...)
manifest type "sherpa-onnx-streaming-zipformer2-ctc" -> OnlineRecognizer.from_zipformer2_ctc(tokens, model, ...)
SherpaOnnxKeywordWakeWordProvider(
models_dir: Path,
keyword: str,
keywords_file: Path,
threshold: float,
score: float,
)
状态机变更
standby
-> local_wake_listening
-> wake_hit
-> acknowledging
-> recording_user_utterance
-> transcribing_user_utterance
-> transcript_visible
-> thinking
-> speaking
-> standby
关键算法
- KWS 检测:把每个 16 kHz int16 frame 转为 float32,送入
sherpa_onnx.KeywordSpotterstream;当get_result()返回非空关键词时立即 reset stream 并返回WakeEvent。 - 关键词文件生成:下载模型后写入默认关键词表,默认内容为
x iǎo j ié x iǎo j ié @小杰小杰,并允许用户修改。 - 唤醒隔离:wake 命中后清理 VAD 状态,正式问题只从后续 frames 构建
AudioSegment。 - 转写显示:STT 成功后先 reporter 输出,再追加上下文,再 LLM。
- 主说话人端点:从正式问题开头的有效语音帧提取 RMS、过零率、谱质心、谱带宽、谱滚降、谱平坦度和频带能量比例,构建本轮临时画像;后续帧相似度低于阈值且连续达到
OWNER_SPEAKER_ABSENT_MS后结束采集。 - ACK 后首句保留:播放“我在”期间允许输入队列积压,播放完成后只执行一次队列 flush 清掉播放回声,不再额外读取
post_playback_drain_ms毫秒并丢弃,默认值改为 0。 - 批量读帧:真实 SoundDevice 输入在拿到首帧后立即 drain 当前队列中所有可用帧并返回给 pipeline,使 wake、capture 和 VAD 能在同一个循环内处理积压帧。
- 快速主说话人结束:画像就绪最低语音长度由
OWNER_SPEAKER_PROFILE_MIN_MS控制,默认 120 ms;一旦画像就绪,主说话人缺席计时达到OWNER_SPEAKER_ABSENT_MS即结束,不再叠加OWNER_VAD_MIN_DURATION_MS。 - 实时转写显示:CaptureStage 在
speech_started后把已录入的帧同时送入本地 streaming STT session;每当 partial 文本变化时发出transcript_partial,终端显示实时转写:<文本>;最终段落仍交给 configured STT provider 生成transcript_final。 - 捕获阶段降噪:
AudioPreprocessStage将 int16 PCM 转为 float32,调用sherpa_onnx.OnlineSpeechDenoiser.run(samples, sample_rate),把返回的DenoisedAudio.samples转回 int16 PCM,并在 metadata 中加入denoised=True、noise_filter_provider=sherpa_onnx_gtcrn。 - CTC STT 加载:manifest
stt.type=sherpa-onnx-streaming-zipformer2-ctc时,只校验tokens和model,调用OnlineRecognizer.from_zipformer2_ctc;旧 manifest 仍兼容 transducer 路径。 - partial 稳定过滤:实时字幕会先去除空白和标点,计算有效字符数;有效字符数小于 2 的文本直接忽略;若新文本比上一次已显示文本短且不构成稳定前缀推进,也忽略,避免把瞬时噪声显示给用户。
数据库/状态管理变更
无数据库变更。新增 wake provider 内部 stream 状态,Runtime 创建时加载,reset() 在命中或恢复待机时重置。临时对话历史仍只保存在当前进程内。
UI 组件重构方案
第一版仍是终端 UI。重构点是将“思考中:<用户文本>”调整为两条语义明确的输出:
转写结果:<用户文本>表示 STT 输出。思考中:正在生成回复表示 LLM 阶段。
依赖影响分析
- 继续使用已有
sherpa-onnxPython 包。 - 新增 KWS 模型约 15 MB,下载到 ignored
models/wake/。 - 不新增 Python 运行依赖。
model-check变严格:缺 wake 模型会失败。- 默认 STT 模型升级为 2025 中文 CTC int8,模型体积和加载时间高于旧 14M 小模型,但换来更稳定的本地 partial/final 一致性。
- 新增 GTCRN denoiser 单文件模型,下载到 ignored
models/denoise/gtcrn_simple.onnx。 - 继续使用已安装的
sherpa-onnx包;不新增 Python 依赖,不接入云 ASR/TTS 作为默认路径。
风险与权衡
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| KWS 关键词拼音格式不匹配模型 | 中 | 高 | 默认写入 sherpa 示例格式;保留 keywords 文件可编辑;测试下载后用真实模型加载;README 标明关键词文件位置 |
| KWS 模型误唤醒或漏唤醒 | 中 | 中 | 暴露 threshold/score 配置;保留状态输出;后续可替换 KWS Provider |
| 唤醒应答期间用户抢说被缓冲清理吞掉 | 中 | 高 | 终端提示顺序改为“唤醒命中 -> 应答中 -> 请说出问题 -> 录音中”,用户只在应答完成后收到提问提示;默认 OWNER_POST_PLAYBACK_DRAIN_MS=0,不再额外读取并丢弃播放后的新音频 |
| 本地 VAD 对真实麦克风音量过保守 | 中 | 高 | 默认 VAD provider 改为 hybrid,本地模型判断和能量阈值兜底任一命中即认为有语音;保留 local 和 energy 可配置回退 |
| 能量兜底让录音无法及时结束 | 高 | 高 | 将能量兜底限制为“开始录音辅助”,结束录音优先使用主说话人音色消失和本地 VAD 静音 |
| ACK 后额外丢弃音频截断首句 | 高 | 高 | 默认 OWNER_POST_PLAYBACK_DRAIN_MS=0;播放结束后只 flush 已积压输入;新增首句保留回归测试 |
| 主说话人画像等待过久 | 高 | 中 | 新增 OWNER_SPEAKER_PROFILE_MIN_MS=120;画像就绪后主说话人缺席结束不再等待普通 VAD 最小时长 |
| 麦克风帧队列积压导致状态滞后 | 中 | 中 | SoundDeviceAudioTransport.read_frames() 批量返回已积压帧;新增批量读帧测试 |
| partial STT 误识别影响 LLM | 中 | 中 | partial 只显示给用户,不进入 ConversationContext;最终 LLM 输入仍以 final STT 为准 |
| 本地 streaming STT 增加 CPU 占用 | 中 | 中 | 只在 capture started 且用户语音已开始后 feed;重复 partial 文本不重复输出;提供 OWNER_REALTIME_TRANSCRIPT_ENABLED=0 回退 |
| 手写音色特征不等于严格声纹识别 | 中 | 中 | 明确第一版为本轮临时主说话人端点;不承诺长期主人识别;保留后续接入 speaker embedding 模型的接口空间 |
| Pipeline 重构影响现有 CLI/测试 | 中 | 高 | 保留 run-live 命令和兼容类名;新增事件顺序测试、两轮回归测试和错误恢复测试 |
| 本地 KWS 增加启动加载时间 | 低 | 中 | 模型约 15 MB,启动加载一次;不在每轮重复加载 |
| 正式问题和唤醒词连在同一句导致问题前半段丢失 | 中 | 中 | 第一版交互明确为先唤醒再提问;后续可做 pre-roll buffer,但不得把 wake 音频直接进 LLM |
| 终端转写不是逐字流式 | 中 | 低 | 第一版至少在 LLM 前即时显示最终 STT 文本;后续可接入本地 streaming STT partial |
| 模型下载网络失败 | 中 | 中 | 下载脚本保留重试;model-check 给明确缺失文件 |
| 误提交模型或 key | 低 | 高 | .gitignore、security-check、提交前 git status --short |
| 降噪模型 API 与当前 sherpa-onnx 版本不一致 | 中 | 高 | 用本机 sherpa_onnx 1.13.3 introspection 确认 OnlineSpeechDenoiser 构造;新增 fake module 单测;model-check 尝试加载 provider |
| GTCRN 对 wake 特征造成误伤 | 中 | 中 | wake 默认不走降噪,仅正式问题阶段降噪;保留 OWNER_WAKE_DENOISE_ENABLED=1 作为显式实验开关 |
| CTC 模型比旧 14M 模型更大导致下载慢 | 中 | 中 | 仍放 models/ 并跳过已存在文件;README 明确首次下载较久;下载脚本保留重试 |
| partial 过滤过严导致实时字幕少显示 | 中 | 低 | 过滤只影响用户可见 partial,不影响 final STT 和 LLM;后续可配置更细阈值 |
| 降噪失败后用户无法继续本轮 | 低 | 中 | 当前 turn 结构化失败并恢复待机,避免错误音频进入 LLM;下一轮可继续唤醒 |
| 模拟验收与真实麦克风仍有差异 | 中 | 中 | 明确模拟验收用于自动调试 live pipeline 状态机和事件顺序;仍保留 4.3 真人 run-live 验收,不在用户确认前归档 |
| 模拟帧耗尽导致测试卡死 | 中 | 高 | 使用 bounded simulated transport,空读超过阈值直接抛结构化错误并让命令返回非 0 |
| 模拟 provider 掩盖真实模型加载问题 | 中 | 中 | 模拟命令只验证 pipeline;模型加载仍由 model-check 真实加载 KWS/VAD/STT/denoiser 覆盖 |
任务分解
1. OpenSpec 修正与提交
- 1.1 编写 proposal/design/spec/tasks;前置条件:当前工作树干净;验收标准:文档明确本地 KWS 唤醒和终端转写;测试要点:OpenSpec strict 通过;优先级:P0;预计:50 分钟。
- 1.2 校验并提交 OpenSpec;前置条件:1.1 完成;验收标准:
openspec validate separate-wake-and-realtime-transcript --strict和openspec validate --all --strict通过后提交;测试要点:中文 commit;优先级:P0;预计:15 分钟。
2. 本地 KWS 模型与配置
- 2.1 扩展 speech model manifest,加入 KWS URL、目录、关键文件和 keywords 文件;前置条件:OpenSpec 提交;验收标准:manifest 包含 wake provider;测试要点:manifest unit test;优先级:P0;预计:45 分钟。
- 2.2 扩展下载脚本下载 KWS 模型并生成关键词文件;前置条件:2.1 完成;验收标准:
models/wake/...和models/wake/keywords.txt存在;测试要点:脚本幂等;优先级:P0;预计:60 分钟。 - 2.3 扩展
.env.example和 AppConfig wake 配置;前置条件:配置字段确定;验收标准:默认OWNER_WAKE_PROVIDER=local_kws;测试要点:配置加载测试;优先级:P0;预计:30 分钟。 - 2.4 扩展
model-check检查 KWS 并尝试加载;前置条件:2.1 至 2.3;验收标准:缺文件失败、完整模型成功;测试要点:mock/临时目录测试;优先级:P0;预计:45 分钟。 - 2.5 验证并提交“本地唤醒模型”模块;前置条件:2.1 至 2.4;验收标准:compileall、相关 unittest、model-check、security-check、OpenSpec 通过;优先级:P0;预计:20 分钟。
3. Runtime 独立唤醒与实时转写
- 3.1 实现
SherpaOnnxKeywordWakeWordProvider;前置条件:KWS 路径 helper 完成;验收标准:缺模型结构化失败,fake sherpa 可检测;测试要点:unit test;优先级:P0;预计:60 分钟。 - 3.2 修改
LiveVoiceRuntime使用 wake provider,不再用 STT 判断唤醒;前置条件:3.1;验收标准:wake 阶段 STT 调用次数为 0;测试要点:两轮 runtime 测试;优先级:P0;预计:60 分钟。 - 3.3 新增
RuntimeReporter.transcript并在 LLM 前输出转写结果;前置条件:3.2;验收标准:终端显示转写文本;测试要点:状态顺序测试;优先级:P0;预计:40 分钟。 - 3.4 保证 wake 词不进入 LLM 当前 user message;前置条件:3.2;验收标准:LLM call 最后一条 user 不含 wake;测试要点:污染回归测试;优先级:P0;预计:30 分钟。
- 3.5 验证并提交“独立唤醒与转写显示”模块;前置条件:3.1 至 3.4;验收标准:compileall、unittest、security-check、OpenSpec 通过;优先级:P0;预计:20 分钟。
4. 文档、真实验收与归档
- 4.1 更新 README 运行说明;前置条件:实现完成;验收标准:说明本地 wake 模型、下载、model-check、run-live 输出;测试要点:命令可复制;优先级:P0;预计:30 分钟。
- 4.2 下载 KWS 模型并运行
model-check;前置条件:脚本完成;验收标准:本机模型检查通过;测试要点:输出不泄露 key;优先级:P0;预计:60 分钟。 - 4.3 执行 run-live 单轮/两轮验收;前置条件:模型、设备、.env 齐全;验收标准:唤醒命中不等待云 ASR,终端显示转写结果;优先级:P0;预计:60 分钟。
- 4.4 最终门禁、归档、提交;前置条件:全部任务完成;验收标准:compileall、unittest、security-check、model-check、device-check、OpenSpec strict、工作树干净;优先级:P0;预计:45 分钟。
5. 唤醒应答与快速端点修正
- 5.1 增加唤醒后本地语音应答;前置条件:本地 KWS 已可唤醒;验收标准:wake 命中后播放“我在”再进入录音;测试要点:fake runtime 播放顺序;优先级:P0;预计:45 分钟。
- 5.2 清理应答播放期间的麦克风缓冲;前置条件:5.1 完成;验收标准:应答音频不进入正式问题 VAD/STT;测试要点:transport flush 测试;优先级:P0;预计:45 分钟。
- 5.3 live 默认改用本地
sherpa-onnxVAD 并缩短静音端点;前置条件:模型已下载;验收标准:.env可配置 VAD provider、静音结束时间和最大录音时长;测试要点:配置和 runtime 构造测试;优先级:P0;预计:45 分钟。 - 5.4 验证并提交“唤醒应答与快速端点”模块;前置条件:5.1 至 5.3 完成;验收标准:compileall、unittest、security-check、model-check、OpenSpec strict 通过后 commit;优先级:P0;预计:20 分钟。
6. 灵敏度与录音端点恢复
- 6.1 调整唤醒提示顺序;前置条件:真人验收暴露用户会在“我在”播放前抢说;验收标准:终端顺序为“唤醒命中 -> 应答中:我在 -> 请说出问题 -> 录音中:正在听取问题”;测试要点:runtime reporter 顺序断言;优先级:P0;预计:30 分钟。
- 6.2 降低默认 KWS 阈值;前置条件:真人反馈唤醒难触发;验收标准:默认
OWNER_WAKE_KWS_THRESHOLD=0.15,README 说明 0.10 至 0.20 调参范围;测试要点:配置默认值测试;优先级:P0;预计:20 分钟。 - 6.3 增加
hybridVAD;前置条件:真人验收出现VAD_TIMEOUT_NO_SPEECH;验收标准:本地 VAD 和能量阈值任一判断为语音即可开始录音,默认OWNER_VAD_PROVIDER=hybrid;测试要点:能量兜底单测;优先级:P0;预计:45 分钟。 - 6.4 验证并提交“唤醒灵敏度与端点恢复”模块;前置条件:6.1 至 6.3 完成;验收标准:compileall、unittest、security-check、model-check、device-check、OpenSpec strict 通过后 commit;优先级:P0;预计:30 分钟。
7. 开源语音助手式 Pipeline 重构
- 7.1 更新 OpenSpec 以描述 stage 化 pipeline、事件总线、TurnController 和主说话人端点;前置条件:公开参考已确认;验收标准:proposal/design/spec/tasks 覆盖新架构和任务;测试要点:OpenSpec strict;优先级:P0;预计:45 分钟。
- 7.2 实现 pipeline event bus 和终端事件映射;前置条件:7.1 完成;验收标准:所有 live 用户可见状态由事件产生;测试要点:事件顺序和终端文案测试;优先级:P0;预计:60 分钟。
- 7.3 实现
TurnController和VoiceAssistantPipeline;前置条件:7.2 完成;验收标准:run-live使用统一 pipeline,成功/失败 turn 均恢复待机;测试要点:两轮 fake runtime、错误恢复、上下文回归;优先级:P0;预计:60 分钟。 - 7.4 实现本轮主说话人端点;前置条件:7.3 完成;验收标准:主说话人音色消失约 300 ms 后结束采集;测试要点:一次提问后背景噪声不拖尾、短暂停顿不断句、画像不足回退;优先级:P0;预计:60 分钟。
- 7.5 更新 README、
.env.example、本地.env非密钥配置;前置条件:7.2 至 7.4 完成;验收标准:运行说明匹配新 pipeline;测试要点:--show-config不泄露 key;优先级:P0;预计:30 分钟。 - 7.6 验证并提交“Pipeline 文档验收”模块;前置条件:7.1 至 7.5 完成;验收标准:compileall、unittest、security-check、model-check、device-check、OpenSpec strict 全通过;优先级:P0;预计:30 分钟。
10. 本地语音链路、高质量实时转写与噪音过滤
- 10.1 更新 OpenSpec 描述本地语音链路和降噪阶段;前置条件:真人日志确认旧本地 14M partial 会产生
家/家确等噪声误识别;验收标准:proposal/design/spec/tasks 明确 wake/VAD/STT/partial/TTS/denoise 全本地、LLM 云端、正式问题阶段默认降噪;测试要点:OpenSpec strict;优先级:P0;预计:45 分钟。 - 10.2 升级 speech model manifest 和下载脚本;前置条件:确认 sherpa-onnx 官方 2025 中文 CTC int8 模型和 GTCRN denoiser URL;验收标准:
models/manifest.json默认 STT 指向 CTCmodel.int8.onnx,新增denoise/gtcrn_simple.onnxrequired file;测试要点:manifest、required files、下载脚本单测/真实下载;优先级:P0;预计:60 分钟。 - 10.3 实现
AudioPreprocessStage;前置条件:10.2 完成;验收标准:新增可注入的 no-op 和 sherpa-onnx GTCRN online denoiser provider,正式问题帧在进入 VAD、partial STT、final STT 前共用同一份降噪后音频;测试要点:fake denoiser 验证 capture/STT 收到降噪后帧;优先级:P0;预计:60 分钟。 - 10.4 扩展本地 STT Provider 支持 CTC 模型和 partial 稳定过滤;前置条件:10.2 完成;验收标准:
SherpaOnnxSttProvider可按 manifest 加载 transducer 或 zipformer2 CTC,partial 不显示单字噪声和短暂跳变;测试要点:fake sherpa CTC、single-char partial filter、transient partial filter;优先级:P0;预计:60 分钟。 - 10.5 修改 runtime 默认本地语音链路;前置条件:10.3 至 10.4 完成;验收标准:默认
OWNER_SPEECH_PROVIDER=local,TTS 使用MacSayTtsProvider,云端只用于 LLM,--show-config显示噪音过滤配置且不泄露 key;测试要点:build runtime provider 类型、cloud ASR/TTS 不被调用;优先级:P0;预计:45 分钟。 - 10.6 更新 README、
.env.example和本地.env非密钥配置;前置条件:10.5 完成;验收标准:中文运行说明包含本地语音默认值、降噪开关、模型下载、model-check、run-live 验收;测试要点:命令可复制,.env不进入提交;优先级:P0;预计:35 分钟。 - 10.7 下载/校验新本地模型并执行门禁;前置条件:10.2 至 10.6 完成;验收标准:下载脚本、compileall、unittest、security-check、model-check、device-check、OpenSpec strict、git diff check 通过或记录真实设备失败原因;测试要点:输出不泄露 key,模型文件不进 Git;优先级:P0;预计:60 分钟。
- 10.8 提交“本地语音降噪”模块;前置条件:10.7 门禁通过;验收标准:中文 commit 信息为
[本地语音降噪]:完成本地语音链路和噪音过滤,包含降噪模型、本地ASR和实时字幕稳定策略,提交后除本地.env非提交修改外无未提交源码/文档中间状态;优先级:P0;预计:10 分钟。
11. 模拟麦克风自动验收与自调试
- 11.1 更新 OpenSpec 描述模拟麦克风验收;前置条件:用户要求“自己一直测试到完整正常,自己弄模拟音频给麦克风”;验收标准:proposal/design/spec/tasks 明确新增可重复
simulate-live,覆盖两轮 live pipeline、实时字幕、降噪、上下文和恢复待机;测试要点:OpenSpec strict;优先级:P0;预计:35 分钟。 - 11.2 实现模拟麦克风帧生成和 bounded transport;前置条件:live pipeline 已 stage 化;验收标准:模拟输入包含 wake、正式问题、短噪声 partial、背景噪声、两轮 turn,空队列不会无限等待;测试要点:失败时命令返回非 0;优先级:P0;预计:50 分钟。
- 11.3 新增
owner_voice_pet simulate-liveCLI;前置条件:11.2 完成;验收标准:默认两轮模拟,输出 JSON checks,可写入/回放 JSONL fixture;测试要点:CLI 单测和真实命令;优先级:P0;预计:40 分钟。 - 11.4 补充 README 和自动化测试;前置条件:11.3 完成;验收标准:README 包含模拟验收命令,测试验证两轮闭环、partial 噪声过滤、第二轮临时上下文、fixture 写入/回放;测试要点:unittest;优先级:P0;预计:35 分钟。
- 11.5 执行全量门禁并提交“模拟麦克风验收”模块;前置条件:11.1 至 11.4 完成;验收标准:
simulate-live --turns 2、compileall、unittest、security-check、model-check、device-check、OpenSpec strict、git diff check 通过后中文 commit;优先级:P0;预计:45 分钟。
Spec Deltas
新增能力
无。继续修改既有 voice-pet-pipeline 能力。
修改能力
Wake word detection:从“监听本地唤醒词”强化为“默认必须使用本地 KWS 模型,不得通过云 ASR 或正式 STT 判定唤醒”。Live terminal state reporting:新增终端转写结果输出要求。Local speech model management:新增 wake KWS 模型和关键词文件管理。Testability:新增 wake/STT 分离测试和唤醒污染回归测试。Wake acknowledgement before recording:明确“请说出问题”必须在应答播放完成后输出,避免用户抢说被清缓冲。Fast user utterance endpointing:默认 VAD provider 改为hybrid,用能量阈值兜底真实麦克风音量差异。Live assistant pipeline events:新增 stage 化事件要求,终端和后续 GUI 必须消费事件。Primary speaker endpointing:新增本轮临时主说话人音色消失结束录音要求。Low latency capture and first utterance preservation:新增 ACK 后不额外丢弃正式问题、批量读帧、独立画像就绪阈值和快速主说话人端点要求。Realtime partial transcript output:新增录音期间 partial transcript 事件、终端显示和上下文隔离要求。Local voice chain and noise filtering:新增本地 STT/TTS 默认、GTCRN 降噪、CTC 模型、partial 稳定过滤和音频不上传要求。Simulated microphone live acceptance:新增不依赖真人麦克风的 live pipeline 模拟验收,覆盖两轮重复对话、短噪声过滤、背景噪声端点、fixture 写入/回放和空帧防卡死。
删除项
不删除能力,但废弃“通过完整 STT/ASR transcript 搜索唤醒词”的运行路径作为默认实现。
推翻重做理由
旧路径把唤醒和正式语音识别耦合,已经在真实运行中表现为响应慢和文本污染。该问题不是调阈值可以解决的局部问题,必须拆分架构。
实施计划
- M1:OpenSpec 修正完成并提交。
- M2:KWS 模型下载、manifest、config、model-check 完成并提交。
- M3:Runtime 独立 wake provider 和转写输出完成并提交。
- M4:README、真实验收、archive 和最终提交完成。
- M5:真人验收反馈修正唤醒提示顺序、KWS 阈值和 hybrid VAD,并在门禁通过后提交。
- M6:Stage 化 pipeline、事件总线、TurnController、主说话人端点和文档验收分模块提交。
- M7:低延迟端点与首句保留修正完成后提交,保留真人
run-live验收任务,不在用户确认前归档。 - M8:录音期间实时转写显示完成后提交,保留真人
run-live验收任务,不在用户确认前归档。 - M9:本地语音链路、高质量实时字幕和正式问题降噪完成后提交,继续保留真人
run-live验收任务,不在用户确认前归档。 - M10:模拟麦克风自动验收完成后提交,作为真人验收前的可重复自测入口;仍不归档变更,直到真实
run-live行为由用户确认。
估时:
- 乐观:4 小时,KWS 模型格式一次通过。
- 最可能:6 小时,需要调关键词格式和测试。
- 悲观:9 小时,KWS 模型对“小杰小杰”识别效果差,需要改关键词文件或阈值。
迁移策略:
- 现有
.env继续兼容,新增 wake 配置使用默认值。 - 现有
models/保留,下载脚本增量补 wake 模型。 - 旧测试按新职责更新,不保留 STT 唤醒路径。
Git 提交规范
- 每完成一个大模块必须立即 commit。
- 提交前必须运行该模块适用验证;源码模块至少运行 compileall、unittest、security-check、OpenSpec strict。
- 模型模块必须运行下载脚本或
model-check。 - 提交信息必须为中文格式:“[模块名]:完成[具体功能描述],包含[关键变更]”。
.env、.venv/、models/不得进入提交。