282 lines
18 KiB
Markdown
282 lines
18 KiB
Markdown
## 功能目标
|
||
|
||
### 完整业务价值
|
||
|
||
当前 `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 |
|
||
| 本地 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 分钟。
|
||
|
||
## 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 分离测试和唤醒污染回归测试。
|
||
|
||
### 删除项
|
||
|
||
不删除能力,但废弃“通过完整 STT/ASR transcript 搜索唤醒词”的运行路径作为默认实现。
|
||
|
||
### 推翻重做理由
|
||
|
||
旧路径把唤醒和正式语音识别耦合,已经在真实运行中表现为响应慢和文本污染。该问题不是调阈值可以解决的局部问题,必须拆分架构。
|
||
|
||
## 实施计划
|
||
|
||
1. M1:OpenSpec 修正完成并提交。
|
||
2. M2:KWS 模型下载、manifest、config、model-check 完成并提交。
|
||
3. M3:Runtime 独立 wake provider 和转写输出完成并提交。
|
||
4. M4:README、真实验收、archive 和最终提交完成。
|
||
|
||
估时:
|
||
|
||
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/` 不得进入提交。
|