[Pipeline OpenSpec]:完成开源语音助手式重构规划,包含事件总线、TurnController和主说话人端点

This commit is contained in:
mkbk
2026-06-17 21:23:57 +08:00
parent 95e4996434
commit 1ee9f8ba95
4 changed files with 130 additions and 9 deletions
@@ -25,11 +25,15 @@
```text
run-live
-> AppConfig(.env)
-> VoiceAssistantPipeline
-> PipelineEventBus
-> TurnController
-> SoundDeviceAudioTransport
-> SherpaOnnxKeywordWakeWordProvider(local models/wake)
-> VadRecorder(user utterance, hybrid local+energy VAD by default)
-> AcknowledgeStage(local "我在")
-> CaptureStage(primary speaker endpoint by default)
-> CloudAsrSttProvider or SherpaOnnxSttProvider
-> TerminalRuntimeReporter.transcript()
-> DialogStage(session memory)
-> ConversationContext
-> OpenAICompatibleLlmProvider
-> CloudTtsProvider or MacSayTtsProvider
@@ -46,11 +50,12 @@ run-live
6. Wait for KWS wake event by feeding frames directly into wake provider.
7. On wake hit, reset wake stream and VAD recorder.
8. Record user utterance with VAD. The default provider is `hybrid`: project-local `sherpa-onnx` VAD remains the primary detector, and an energy threshold fallback prevents low microphone gain from being treated as no speech.
9. Transcribe user utterance.
10. Emit transcript to terminal.
11. Append user text and call LLM.
12. Synthesize/play reply.
13. Append assistant reply and return to standby.
9. When `OWNER_ENDPOINT_MODE=primary_speaker`, build a temporary per-turn speaker profile from the first valid user speech frames and end the capture when the primary speaker is absent for `OWNER_SPEAKER_ABSENT_MS`.
10. Transcribe user utterance.
11. Emit transcript event to terminal.
12. Append user text and call LLM.
13. Synthesize/play reply.
14. Append assistant reply and return to standby.
## Interfaces
@@ -73,6 +78,29 @@ class RuntimeReporter(Protocol):
def error(self, stage: str, code: str, message: str, *, turn_id: int | None = None) -> None: ...
```
### Pipeline events
```python
class PipelineEventBus:
def subscribe(self, listener): ...
def emit(self, event_type, *, turn_id=None, state=None, message="", payload=None): ...
```
Required event types are `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`, and `stage_error`.
### `TurnController`
```python
class TurnController:
def run_turn(self, turn_id: int) -> TurnResult: ...
```
The controller owns one turn state machine and delegates work to provider-backed stages. It does not write terminal text directly; it emits events only.
### Primary speaker endpoint
The first implementation is a per-turn heuristic endpoint, not persistent voiceprint recognition. It extracts local PCM features from speech frames and compares future frames against the profile. If profile creation fails because the user speech is too short or too quiet, capture falls back to existing VAD silence endpoint.
## Model Files
```text
@@ -99,6 +127,8 @@ models/
4. Empty user STT: existing `STT_EMPTY_TRANSCRIPT`.
5. Invalid wake provider config: `CONFIG_MISSING_VALUE`.
6. Real microphone VAD miss: default `OWNER_VAD_PROVIDER=hybrid` SHALL accept speech when either the local model or the energy fallback detects speech.
7. Primary speaker endpoint profile failure: fallback to VAD silence endpoint.
8. Pipeline stage failure: emit `stage_error`, recover to standby, and keep the process alive unless startup dependencies are missing.
## Testing Strategy
@@ -108,6 +138,8 @@ models/
4. Unit test LLM user content excludes wake keyword.
5. Unit test KWS provider missing model raises structured error.
6. Model-check test validates wake required files.
7. Pipeline event order test validates successful two-turn event sequence.
8. Primary speaker endpoint test validates that background noise or a later repeated utterance does not extend the current turn after the main speaker disappears.
## Migration
@@ -4,6 +4,8 @@
当前 `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,也不希望唤醒词本身污染正式对话历史。
### 目标用户场景
@@ -22,6 +24,8 @@
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` 仍可完成一轮真实语音交互。
7. 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`
8. 主说话人端点有效:当用户只说一次“你是谁”并在后续出现背景噪声或第二次重复提问时,第一轮音色消失连续约 300 ms 后必须结束采集,不得把第二次重复提问拼进同一个 `AudioSegment`
### 预期影响
@@ -31,6 +35,8 @@
4. 终端 reporter 将新增转写输出能力,用于显示正式问题文本。
5. 测试将更新旧的两轮 runtime 断言:STT 调用次数从“wake+question 每轮两次”改为“question 每轮一次”。
6. README 将说明首次准备需要下载 wake/VAD/STT 本地模型,唤醒词检测为本地模型路径。
7. Runtime 将新增 `VoiceAssistantPipeline``TurnController` 和 pipeline event bus`run-live` 通过统一 pipeline 执行,旧 `LiveVoiceRuntime` 仅作为兼容入口或测试辅助,不再承载新的 live 主流程。
8. 采集阶段将新增本轮临时主说话人端点,默认不保存声纹、不跨进程记忆、不新增长期主人注册流程。
### 对现有问题的系统性总结
@@ -40,6 +46,8 @@
4. 架构问题:wake、VAD、STT 的职责边界不清晰,`LiveVoiceRuntime._wait_for_wake_and_user_text()` 同时承担唤醒识别、唤醒词剥离、问题录音和问题转写。
5. 测试问题:现有 live runtime 测试把 wake transcript 放进 `QueueSttProvider`,等于把“唤醒必须经过 STT”固化成测试事实。
6. 模型管理问题:已有 `models/` manifest 只覆盖 VAD/STT,不覆盖专用 KWS 模型和关键词表。
7. Pipeline 边界问题:live runtime 缺少 stage 级事件和 controller,终端输出、错误恢复、TTS 播放、STT 调用顺序散落在同一个类里,后续 GUI 桌宠无法复用稳定事件。
8. 端点问题:`HybridVadProvider` 用“本地 VAD 或能量阈值任一为语音”同时控制开始和结束,底噪偏高时会持续重置静音计数,导致录音结束慢。
## 详细需求
@@ -55,6 +63,10 @@
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”。
11. `run-live` SHALL 使用统一 `VoiceAssistantPipeline` 执行 turn-based 语音助手流程,stage 之间通过明确输入输出传递,不得让 wake 音频、正式问题音频、LLM 上下文或 TTS 播放状态互相污染。
12. 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`
13. 正式问题采集 SHALL 默认使用本轮临时主说话人端点:唤醒后以正式问题开头短音频建立本轮音色画像,主说话人音色连续消失达到配置时间后结束采集。
14. 主说话人端点 SHALL 不保存长期声纹、不写音频文件、不跨进程复用音色画像。
### 非功能需求
@@ -63,6 +75,8 @@
3. 安全:wake/KWS 在本机执行,不上传连续麦克风流;`.env` key 和 `models/` 仍不得提交。
4. 可扩展性:wake Provider 必须是独立接口,后续可以替换为 Porcupine、CoreML、Apple Speech 或其他本地 KWS,不影响 STT/LLM/TTS。
5. 测试性:自动化测试必须能注入 fake wake provider,不依赖真实麦克风或真实 KWS 模型。
6. 架构可观测性:所有用户可见状态必须来自 pipeline event bus,终端 reporter 和后续 GUI 只消费事件,不直接嵌入 stage 逻辑。
7. 端点性能:默认配置下,主说话人音色消失后 300 ms 左右应结束采集,并进入 STT;最大录音时长仍作为兜底。
### 边缘案例
@@ -73,6 +87,8 @@
5. STT 返回空文本:输出空转写/错误并恢复待机,不调用 LLM。
6. 用户正式问题中包含“小杰小杰”:只有 wake 命中后的正式录音内容可进入上下文;如果用户确实在正式问题中重复该词,允许保留。
7. 播放回声误触发:播放期间仍遵循既有音频反馈抑制要求,不能把 TTS 当作新 wake。
8. 背景噪声拖尾:用户停止说话后若仍有非主说话人或噪声,主说话人端点必须允许结束录音。
9. 音色画像不足:如果开头音频太短或能量不足,采集阶段必须回退到普通 VAD 静音端点,不能卡死。
### 输入输出规格
@@ -83,6 +99,13 @@
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 关键词分数。
6. `OWNER_PIPELINE_MODE=live_turn_based`:第一版固定 turn-based pipeline。
7. `OWNER_ENDPOINT_MODE=primary_speaker`:默认主说话人端点;可设为 `vad` 回退普通 VAD。
8. `OWNER_SPEAKER_PROFILE_MS=600`:建立本轮临时音色画像的目标音频长度。
9. `OWNER_SPEAKER_ABSENT_MS=300`:主说话人音色连续消失多少毫秒后结束录音。
10. `OWNER_SPEAKER_SIMILARITY_THRESHOLD=0.70`:音色相似度阈值。
11. `OWNER_SPEAKER_MIN_RMS=0.012`:进入音色画像/匹配的最低能量。
12. `OWNER_CONTEXT_MODE=session_memory`:本次进程内临时上下文。
终端输出:
@@ -106,11 +129,13 @@
```text
SoundDeviceAudioTransport
-> PipelineEventBus
-> LocalWakeWordProvider(sherpa-onnx KeywordSpotter, models/wake, keywords.txt)
-> wake_hit
-> VadRecorder(user utterance only)
-> AcknowledgeStage("我在")
-> CaptureStage(primary speaker endpoint, user utterance only)
-> SttProvider(cloud or local, configured by OWNER_SPEECH_PROVIDER)
-> RuntimeReporter.transcript("转写结果:...")
-> PipelineEvent(transcript_final)
-> ConversationContext(temporary process history)
-> Cloud LLM
-> TTS
@@ -128,6 +153,7 @@ SoundDeviceAudioTransport
6. STT 成功后调用 `reporter.transcript(user_text, final=True)`,终端立即显示转写结果。
7. Runtime 将 `user_text` 追加到临时上下文并调用 LLM。
8. TTS 播放后追加 assistant 历史并恢复待机。
9. 所有 stage 同步发出 pipeline events;终端 reporter 只把事件映射为中文文案。
### 接口定义
@@ -141,6 +167,17 @@ WakeWordProvider.reset() -> None
RuntimeReporter.transcript(text: str, final: bool, turn_id: int | None = None) -> None
```
```text
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
```
```text
TurnController.run_turn(turn_id: int) -> TurnResult
VoiceAssistantPipeline.run(once: bool = False, max_turns: int | None = None) -> RuntimeSummary
```
```text
SherpaOnnxKeywordWakeWordProvider(
models_dir: Path,
@@ -157,6 +194,7 @@ SherpaOnnxKeywordWakeWordProvider(
standby
-> local_wake_listening
-> wake_hit
-> acknowledging
-> recording_user_utterance
-> transcribing_user_utterance
-> transcript_visible
@@ -171,6 +209,7 @@ standby
2. 关键词文件生成:下载模型后写入默认关键词表,默认内容为 `x iǎo j ié x iǎo j ié @小杰小杰`,并允许用户修改。
3. 唤醒隔离:wake 命中后清理 VAD 状态,正式问题只从后续 frames 构建 `AudioSegment`
4. 转写显示:STT 成功后先 reporter 输出,再追加上下文,再 LLM。
5. 主说话人端点:从正式问题开头的有效语音帧提取 RMS、过零率、谱质心、谱带宽、谱滚降、谱平坦度和频带能量比例,构建本轮临时画像;后续帧相似度低于阈值且连续达到 `OWNER_SPEAKER_ABSENT_MS` 后结束采集。
### 数据库/状态管理变更
@@ -198,6 +237,9 @@ standby
| KWS 模型误唤醒或漏唤醒 | 中 | 中 | 暴露 threshold/score 配置;保留状态输出;后续可替换 KWS Provider |
| 唤醒应答期间用户抢说被缓冲清理吞掉 | 中 | 高 | 终端提示顺序改为“唤醒命中 -> 应答中 -> 请说出问题 -> 录音中”,用户只在应答完成后收到提问提示;默认播放后排水从 250 ms 降为 50 ms |
| 本地 VAD 对真实麦克风音量过保守 | 中 | 高 | 默认 VAD provider 改为 `hybrid`,本地模型判断和能量阈值兜底任一命中即认为有语音;保留 `local``energy` 可配置回退 |
| 能量兜底让录音无法及时结束 | 高 | 高 | 将能量兜底限制为“开始录音辅助”,结束录音优先使用主说话人音色消失和本地 VAD 静音 |
| 手写音色特征不等于严格声纹识别 | 中 | 中 | 明确第一版为本轮临时主说话人端点;不承诺长期主人识别;保留后续接入 speaker embedding 模型的接口空间 |
| Pipeline 重构影响现有 CLI/测试 | 中 | 高 | 保留 `run-live` 命令和兼容类名;新增事件顺序测试、两轮回归测试和错误恢复测试 |
| 本地 KWS 增加启动加载时间 | 低 | 中 | 模型约 15 MB,启动加载一次;不在每轮重复加载 |
| 正式问题和唤醒词连在同一句导致问题前半段丢失 | 中 | 中 | 第一版交互明确为先唤醒再提问;后续可做 pre-roll buffer,但不得把 wake 音频直接进 LLM |
| 终端转写不是逐字流式 | 中 | 低 | 第一版至少在 LLM 前即时显示最终 STT 文本;后续可接入本地 streaming STT partial |
@@ -248,6 +290,15 @@ standby
- [ ] 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 分钟。
### 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 分钟。
## Spec Deltas
### 新增能力
@@ -262,6 +313,8 @@ standby
4. `Testability`:新增 wake/STT 分离测试和唤醒污染回归测试。
5. `Wake acknowledgement before recording`:明确“请说出问题”必须在应答播放完成后输出,避免用户抢说被清缓冲。
6. `Fast user utterance endpointing`:默认 VAD provider 改为 `hybrid`,用能量阈值兜底真实麦克风音量差异。
7. `Live assistant pipeline events`:新增 stage 化事件要求,终端和后续 GUI 必须消费事件。
8. `Primary speaker endpointing`:新增本轮临时主说话人音色消失结束录音要求。
### 删除项
@@ -278,6 +331,7 @@ standby
3. M3Runtime 独立 wake provider 和转写输出完成并提交。
4. M4README、真实验收、archive 和最终提交完成。
5. M5:真人验收反馈修正唤醒提示顺序、KWS 阈值和 hybrid VAD,并在门禁通过后提交。
6. M6Stage 化 pipeline、事件总线、TurnController、主说话人端点和文档验收分模块提交。
估时:
@@ -1,5 +1,31 @@
## ADDED Requirements
### Requirement: Live assistant pipeline events
The live runtime SHALL execute as a stage-based voice assistant pipeline with stable events for every externally observable stage.
#### Scenario: Successful live turn emits ordered events
- **WHEN** a live turn completes successfully
- **THEN** the event stream SHALL include wake listening, wake detected, acknowledgement, capture started, speech started, speech ended, STT started, final transcript, LLM started, TTS started, playback finished, and standby resumed in that order
#### Scenario: Terminal reporting consumes events
- **WHEN** the terminal reporter prints user-facing live status
- **THEN** it SHALL derive the status from pipeline events rather than duplicating stage logic
### Requirement: Primary speaker endpointing
The live runtime SHALL support a per-turn primary speaker endpoint mode that ends the user utterance when the speaker who started the utterance is no longer present.
#### Scenario: Primary speaker disappears
- **WHEN** the user starts speaking after wake and their temporary per-turn voice profile is established
- **THEN** capture SHALL end after the configured primary speaker absence duration even if background noise or non-primary speech remains
#### Scenario: Temporary profile is insufficient
- **WHEN** the live runtime cannot establish a reliable per-turn voice profile because speech is too short or too quiet
- **THEN** capture SHALL fall back to configured VAD silence endpointing and SHALL NOT hang indefinitely
#### Scenario: No persistent voiceprint
- **WHEN** the process exits
- **THEN** no primary speaker profile SHALL be stored or reused across runs
### Requirement: Realtime transcript terminal output
The live runtime SHALL display the recognized user utterance text in the terminal after STT succeeds and before the LLM request is sent.
@@ -45,3 +45,12 @@
- [x] 6.2 降低默认 KWS 阈值;前置条件:真人反馈“小杰小杰”难触发;验收标准:默认 `OWNER_WAKE_KWS_THRESHOLD=0.15`README 给出 `0.10``0.20` 调参建议;测试要点:配置默认值测试;优先级:P0;预计:20 分钟。
- [x] 6.3 增加 `hybrid` VAD 并设为默认;前置条件:真人验收出现 `VAD_TIMEOUT_NO_SPEECH`;验收标准:本地 VAD 和能量阈值任一判断为语音即可开始/继续录音,默认 `OWNER_VAD_PROVIDER=hybrid`;测试要点:能量兜底单测、配置校验测试;优先级:P0;预计:45 分钟。
- [x] 6.4 验证并提交“唤醒灵敏度与端点恢复”模块;前置条件:6.1 至 6.3 完成;验收标准:compileall、unittest、security-check、model-check、device-check、OpenSpec strict 通过后 commit;优先级:P0;预计:30 分钟。
## 7. 开源语音助手式 Pipeline 重构
- [x] 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 分钟。