Files
Owner/openspec/changes/separate-wake-and-realtime-transcript/proposal.md
T

21 KiB
Raw Blame History

功能目标

完整业务价值

当前 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.txtKWS 关键词表路径。
  4. OWNER_WAKE_KWS_THRESHOLD=0.25KWS 命中阈值。
  5. OWNER_WAKE_KWS_SCORE=1.0KWS 关键词分数。

终端输出:

  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 文本。

设计方案

文字版全新架构图

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 历史并恢复待机。

接口定义

WakeWordProvider.load() -> None
WakeWordProvider.detect(frame: AudioFrame) -> WakeEvent | None
WakeWordProvider.reset() -> None
RuntimeReporter.transcript(text: str, final: bool, turn_id: int | None = None) -> None
SherpaOnnxKeywordWakeWordProvider(
  models_dir: Path,
  keyword: str,
  keywords_file: Path,
  threshold: float,
  score: float,
)

状态机变更

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,本地模型判断和能量阈值兜底任一命中即认为有语音;保留 localenergy 可配置回退
本地 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 --strictopenspec 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.15README 说明 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. M1OpenSpec 修正完成并提交。
  2. M2KWS 模型下载、manifest、config、model-check 完成并提交。
  3. M3Runtime 独立 wake provider 和转写输出完成并提交。
  4. M4README、真实验收、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/ 不得进入提交。