## 功能目标 ### 完整业务价值 当前 `run-live` 已能完成真实重复语音对话,但唤醒路径仍把麦克风片段送入 STT/ASR 判断“是否包含小杰小杰”。这种做法会把一次唤醒变成一次完整语音识别请求,造成明显延迟;同时当用户连续说“小杰小杰”或环境里有回声时,唤醒片段会被混入正式用户问题,最终出现终端中“思考中:杰小杰。小杰小杰...”这类错误输入。本变更的业务价值是把“唤醒词检测”从“用户问题转写”中彻底拆开,形成独立、本地、低延迟的 wake pipeline,并让用户在终端中看到被识别出来的正式问题文本。 目标用户是正在本机运行语音桌宠的使用者。用户希望桌宠像真实语音助手一样先快速识别“小杰小杰”,进入听取问题状态,再把后续问题转写显示出来并交给 LLM;用户不希望每次唤醒都等待云端 ASR,也不希望唤醒词本身污染正式对话历史。 ### 目标用户场景 1. 低延迟唤醒:用户说“小杰小杰”,本地 KWS 模型在麦克风流中命中后立即输出“唤醒命中”,不调用云端 ASR 判断唤醒词。 2. 正式问题转写:用户唤醒后说“帮我记住苹果这个词”,终端必须显示“转写结果:帮我记住苹果这个词”,随后才进入“思考中”。 3. 重复对话:第一轮提问和回复完成后恢复待机,第二轮再次通过本地 KWS 唤醒,并携带本次运行进程内历史。 4. 模型诊断:`download_speech_models.py --dir models` 必须下载/准备 wake KWS、VAD、STT 所需模型;`model-check` 必须检查 wake 模型文件。 5. 故障恢复:本地 wake 模型缺失、加载失败或检测失败时,启动阶段必须清楚报错,不得静默回退到云端 ASR 唤醒。 ### 量化成功指标 KPI 1. 唤醒路径零云端 ASR:自动化测试必须证明 wake 阶段不调用 `SttProvider.transcribe()`;STT 只用于唤醒后的正式问题。 2. 唤醒污染清零:LLM 请求中的当前 user message 不得包含“小杰小杰”唤醒词,除非用户在正式问题里明确重复说出该词。 3. 终端可见转写:每轮正式 STT 完成后,终端必须输出一条包含用户问题文本的转写消息,且该消息出现在 LLM 请求之前。 4. 本地 wake 模型验收:`model-check --models-dir models` 必须检查 KWS tokens、encoder、decoder、joiner、keywords 文件。 5. 重复对话不回退:两轮 fake runtime 测试必须验证两次 wake、两次正式 STT、两次 LLM、两次 TTS、两次播放,并最终恢复待机。 6. 真实运行可用:在模型、设备和 `.env` 齐全时,`run-live --once` 仍可完成一轮真实语音交互。 ### 预期影响 1. OpenSpec 主规范将明确 wake word detection 必须优先使用本地 KWS 模型,不再把完整 ASR 作为默认唤醒实现。 2. 模型目录将新增 wake KWS 模型和关键词文件,仍位于 ignored `models/`,不提交 Git。 3. Runtime 将新增 `wakeword` Provider 依赖,唤醒检测直接消费麦克风帧;正式问题录音和 STT 在唤醒后才开始。 4. 终端 reporter 将新增转写输出能力,用于显示正式问题文本。 5. 测试将更新旧的两轮 runtime 断言:STT 调用次数从“wake+question 每轮两次”改为“question 每轮一次”。 6. README 将说明首次准备需要下载 wake/VAD/STT 本地模型,唤醒词检测为本地模型路径。 ### 对现有问题的系统性总结 1. 性能问题:旧实现每次检测唤醒都要先用 VAD 截出一段音频,再调用 STT/ASR 转文字,wake latency 被网络、模型转写和 VAD 端点共同放大。 2. 功能问题:旧实现从 wake transcript 中提取 wake word 后,可能把残留文本或重复唤醒词当成用户正文,导致 LLM 输入污染。 3. UI/UX 问题:终端只显示“思考中:xxx”,用户无法明确分辨“这是转写结果”还是“LLM 正在处理”,也看不到 STT 阶段的实际识别文本。 4. 架构问题:wake、VAD、STT 的职责边界不清晰,`LiveVoiceRuntime._wait_for_wake_and_user_text()` 同时承担唤醒识别、唤醒词剥离、问题录音和问题转写。 5. 测试问题:现有 live runtime 测试把 wake transcript 放进 `QueueSttProvider`,等于把“唤醒必须经过 STT”固化成测试事实。 6. 模型管理问题:已有 `models/` manifest 只覆盖 VAD/STT,不覆盖专用 KWS 模型和关键词表。 ## 详细需求 ### 功能需求 1. `run-live` SHALL 使用本地 wake word Provider 检测“小杰小杰”,默认实现为 `sherpa-onnx` `KeywordSpotter` 或等价本地 KWS 模型。 2. wake 检测 SHALL 直接消费麦克风流 `AudioFrame`,不得调用云端 ASR,也不得调用正式问题 `SttProvider.transcribe()`。 3. wake 命中后 SHALL 重置 VAD/录音缓冲,并进入“请说出问题/录音中”状态。 4. 正式问题的音频 SHALL 从 wake 命中后开始采集;wake 音频不得传入 LLM 上下文。 5. 正式问题 STT 完成后 SHALL 立即输出终端转写文本,例如“转写结果:帮我记住苹果这个词”。 6. LLM 请求 SHALL 只携带正式问题文本和本次运行临时历史,不携带 wake transcript 或 KWS 结果字符串。 7. `scripts/download_speech_models.py --dir models` SHALL 下载或准备 KWS 模型,生成 `models/wake/keywords.txt`。 8. `model-check` SHALL 检查 wake KWS 模型关键文件和关键词文件,并尝试加载 wake provider。 9. `.env.example` SHALL 增加 wake provider 配置:`OWNER_WAKE_PROVIDER=local_kws`、wake 模型/阈值/关键词配置。 10. README SHALL 明确“唤醒使用本地模型,ASR/TTS 可按 `OWNER_SPEECH_PROVIDER` 走 cloud 或 local”。 ### 非功能需求 1. 性能:本地 wake 检测目标是在本地模型可用时 800 ms 内给出可见“唤醒命中”状态;wake 不受 LLM/ASR 网络延迟影响。 2. UI/UX:终端输出必须区分“待机监听唤醒”“唤醒命中”“录音中”“转写中”“转写结果”“思考中”“播放中”“恢复待机”。 3. 安全:wake/KWS 在本机执行,不上传连续麦克风流;`.env` key 和 `models/` 仍不得提交。 4. 可扩展性:wake Provider 必须是独立接口,后续可以替换为 Porcupine、CoreML、Apple Speech 或其他本地 KWS,不影响 STT/LLM/TTS。 5. 测试性:自动化测试必须能注入 fake wake provider,不依赖真实麦克风或真实 KWS 模型。 ### 边缘案例 1. wake 模型缺失:`run-live` 启动前失败,错误指向 `model-check` 或下载脚本,不回退到云端 ASR。 2. wake 模型加载失败:输出结构化 `WAKE_MODEL_LOAD_FAILED`,不进入假待机。 3. 未命中唤醒词:保持待机,不调用 STT、LLM、TTS。 4. wake 命中后用户不说话:VAD no-speech timeout 后恢复待机,不调用 LLM。 5. STT 返回空文本:输出空转写/错误并恢复待机,不调用 LLM。 6. 用户正式问题中包含“小杰小杰”:只有 wake 命中后的正式录音内容可进入上下文;如果用户确实在正式问题中重复该词,允许保留。 7. 播放回声误触发:播放期间仍遵循既有音频反馈抑制要求,不能把 TTS 当作新 wake。 ### 输入输出规格 新增 `.env` 输入: 1. `OWNER_WAKE_PROVIDER=local_kws`:第一版默认本地 KWS 唤醒。 2. `OWNER_WAKE_KEYWORD=小杰小杰`:唤醒词。 3. `OWNER_WAKE_KEYWORDS_FILE=models/wake/keywords.txt`:KWS 关键词表路径。 4. `OWNER_WAKE_KWS_THRESHOLD=0.25`:KWS 命中阈值。 5. `OWNER_WAKE_KWS_SCORE=1.0`:KWS 关键词分数。 终端输出: 1. 待机:`[第N轮] 待机:等待唤醒词“小杰小杰”` 2. 唤醒:`[第N轮] 唤醒命中:请说出问题` 3. 转写中:`[第N轮] 转写中:正在识别问题` 4. 转写结果:`[第N轮] 转写结果:<正式问题文本>` 5. 思考:`[第N轮] 思考中:正在生成回复` ### 数据验证规则 1. wake provider 只允许 `local_kws` 和测试注入 provider;无效配置启动失败。 2. wake keywords 文件必须存在且非空。 3. KWS 模型 tokens、encoder、decoder、joiner 文件必须存在。 4. 终端转写输出不得包含 API key 或 Authorization header。 5. LLM messages 的最后一条 user content 必须等于正式 STT 文本。 ## 设计方案 ### 文字版全新架构图 ```text SoundDeviceAudioTransport -> LocalWakeWordProvider(sherpa-onnx KeywordSpotter, models/wake, keywords.txt) -> wake_hit -> VadRecorder(user utterance only) -> SttProvider(cloud or local, configured by OWNER_SPEECH_PROVIDER) -> RuntimeReporter.transcript("转写结果:...") -> ConversationContext(temporary process history) -> Cloud LLM -> TTS -> Speaker -> standby ``` ### 数据流 1. 启动时加载 `.env`、wake KWS 模型、VAD、正式 STT、LLM、TTS。 2. 待机阶段持续读取麦克风帧,调用 `wakeword.detect(frame)`。 3. KWS 返回 wake event 后输出“唤醒命中”,重置 VAD 和输入缓冲。 4. 进入正式问题录音,VAD 判断用户问题起止。 5. 正式问题结束后调用 STT。 6. STT 成功后调用 `reporter.transcript(user_text, final=True)`,终端立即显示转写结果。 7. Runtime 将 `user_text` 追加到临时上下文并调用 LLM。 8. TTS 播放后追加 assistant 历史并恢复待机。 ### 接口定义 ```text WakeWordProvider.load() -> None WakeWordProvider.detect(frame: AudioFrame) -> WakeEvent | None WakeWordProvider.reset() -> None ``` ```text RuntimeReporter.transcript(text: str, final: bool, turn_id: int | None = None) -> None ``` ```text SherpaOnnxKeywordWakeWordProvider( models_dir: Path, keyword: str, keywords_file: Path, threshold: float, score: float, ) ``` ### 状态机变更 ```text standby -> local_wake_listening -> wake_hit -> recording_user_utterance -> transcribing_user_utterance -> transcript_visible -> thinking -> speaking -> standby ``` ### 关键算法 1. KWS 检测:把每个 16 kHz int16 frame 转为 float32,送入 `sherpa_onnx.KeywordSpotter` stream;当 `get_result()` 返回非空关键词时立即 reset stream 并返回 `WakeEvent`。 2. 关键词文件生成:下载模型后写入默认关键词表,默认内容为 `x iǎo j ié x iǎo j ié @小杰小杰`,并允许用户修改。 3. 唤醒隔离:wake 命中后清理 VAD 状态,正式问题只从后续 frames 构建 `AudioSegment`。 4. 转写显示:STT 成功后先 reporter 输出,再追加上下文,再 LLM。 ### 数据库/状态管理变更 无数据库变更。新增 wake provider 内部 stream 状态,Runtime 创建时加载,`reset()` 在命中或恢复待机时重置。临时对话历史仍只保存在当前进程内。 ### UI 组件重构方案 第一版仍是终端 UI。重构点是将“思考中:<用户文本>”调整为两条语义明确的输出: 1. `转写结果:<用户文本>` 表示 STT 输出。 2. `思考中:正在生成回复` 表示 LLM 阶段。 ### 依赖影响分析 1. 继续使用已有 `sherpa-onnx` Python 包。 2. 新增 KWS 模型约 15 MB,下载到 ignored `models/wake/`。 3. 不新增 Python 运行依赖。 4. `model-check` 变严格:缺 wake 模型会失败。 ## 风险与权衡 | 风险 | 概率 | 影响 | 缓解措施 | | --- | --- | --- | --- | | KWS 关键词拼音格式不匹配模型 | 中 | 高 | 默认写入 sherpa 示例格式;保留 keywords 文件可编辑;测试下载后用真实模型加载;README 标明关键词文件位置 | | KWS 模型误唤醒或漏唤醒 | 中 | 中 | 暴露 threshold/score 配置;保留状态输出;后续可替换 KWS Provider | | 唤醒应答期间用户抢说被缓冲清理吞掉 | 中 | 高 | 终端提示顺序改为“唤醒命中 -> 应答中 -> 请说出问题 -> 录音中”,用户只在应答完成后收到提问提示;默认播放后排水从 250 ms 降为 50 ms | | 本地 VAD 对真实麦克风音量过保守 | 中 | 高 | 默认 VAD provider 改为 `hybrid`,本地模型判断和能量阈值兜底任一命中即认为有语音;保留 `local` 和 `energy` 可配置回退 | | 本地 KWS 增加启动加载时间 | 低 | 中 | 模型约 15 MB,启动加载一次;不在每轮重复加载 | | 正式问题和唤醒词连在同一句导致问题前半段丢失 | 中 | 中 | 第一版交互明确为先唤醒再提问;后续可做 pre-roll buffer,但不得把 wake 音频直接进 LLM | | 终端转写不是逐字流式 | 中 | 低 | 第一版至少在 LLM 前即时显示最终 STT 文本;后续可接入本地 streaming STT partial | | 模型下载网络失败 | 中 | 中 | 下载脚本保留重试;`model-check` 给明确缺失文件 | | 误提交模型或 key | 低 | 高 | `.gitignore`、security-check、提交前 `git status --short` | ## 任务分解 ### 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-onnx` VAD 并缩短静音端点;前置条件:模型已下载;验收标准:`.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 增加 `hybrid` VAD;前置条件:真人验收出现 `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 分钟。 ## Spec Deltas ### 新增能力 无。继续修改既有 `voice-pet-pipeline` 能力。 ### 修改能力 1. `Wake word detection`:从“监听本地唤醒词”强化为“默认必须使用本地 KWS 模型,不得通过云 ASR 或正式 STT 判定唤醒”。 2. `Live terminal state reporting`:新增终端转写结果输出要求。 3. `Local speech model management`:新增 wake KWS 模型和关键词文件管理。 4. `Testability`:新增 wake/STT 分离测试和唤醒污染回归测试。 5. `Wake acknowledgement before recording`:明确“请说出问题”必须在应答播放完成后输出,避免用户抢说被清缓冲。 6. `Fast user utterance endpointing`:默认 VAD provider 改为 `hybrid`,用能量阈值兜底真实麦克风音量差异。 ### 删除项 不删除能力,但废弃“通过完整 STT/ASR transcript 搜索唤醒词”的运行路径作为默认实现。 ### 推翻重做理由 旧路径把唤醒和正式语音识别耦合,已经在真实运行中表现为响应慢和文本污染。该问题不是调阈值可以解决的局部问题,必须拆分架构。 ## 实施计划 1. M1:OpenSpec 修正完成并提交。 2. M2:KWS 模型下载、manifest、config、model-check 完成并提交。 3. M3:Runtime 独立 wake provider 和转写输出完成并提交。 4. M4:README、真实验收、archive 和最终提交完成。 5. M5:真人验收反馈修正唤醒提示顺序、KWS 阈值和 hybrid VAD,并在门禁通过后提交。 估时: 1. 乐观:4 小时,KWS 模型格式一次通过。 2. 最可能:6 小时,需要调关键词格式和测试。 3. 悲观:9 小时,KWS 模型对“小杰小杰”识别效果差,需要改关键词文件或阈值。 迁移策略: 1. 现有 `.env` 继续兼容,新增 wake 配置使用默认值。 2. 现有 `models/` 保留,下载脚本增量补 wake 模型。 3. 旧测试按新职责更新,不保留 STT 唤醒路径。 ## Git 提交规范 1. 每完成一个大模块必须立即 commit。 2. 提交前必须运行该模块适用验证;源码模块至少运行 compileall、unittest、security-check、OpenSpec strict。 3. 模型模块必须运行下载脚本或 `model-check`。 4. 提交信息必须为中文格式:“[模块名]:完成[具体功能描述],包含[关键变更]”。 5. `.env`、`.venv/`、`models/` 不得进入提交。