242 lines
8.6 KiB
Markdown
242 lines
8.6 KiB
Markdown
# Python 桌宠语音 Pipeline 技术设计
|
||
|
||
## Context
|
||
|
||
当前仓库从空 Git 仓库开始,没有既有 OpenSpec 能力、源码、测试、依赖、资产或构建脚本。本设计作为实现约束,后续必须创建 Python 源码、测试、运行入口、验收命令和项目内桌宠资产;API key 不得写入仓库。
|
||
|
||
目标系统是本地 Python 桌宠应用。用户说“小杰小杰”后,系统通过本机麦克风接收语音,经过 VAD、STT、对话上下文、云端 LLM、本地 TTS,再通过系统扬声器播放回复。桌宠窗口展示 pipeline 状态,并使用后续由 `imagegen` 生成的可爱 3D 透明资产。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
1. 明确后续实现的模块边界、接口、状态机和错误码。
|
||
2. 固定第一版范围:Python 桌面宠物、本机麦克风、本机扬声器、“小杰小杰”唤醒、本地 STT/TTS、云端 LLM。
|
||
3. 建立可测试的 pipeline 能力规范。
|
||
4. 把性能、UI、功能、安全和可扩展性风险提前纳入任务拆解。
|
||
|
||
**Non-Goals:**
|
||
|
||
1. 第一版不支持 WebSocket Transport、硬件设备协议、移动端、云端 STT/TTS 或长期记忆。
|
||
2. 第一版不要求播放中语音打断,默认播放期间自抑制。
|
||
3. 自动化测试不依赖真实麦克风、真实扬声器或真实模型文件;真实设备能力通过可选 Provider 和人工验收入口保留。
|
||
|
||
## Decisions
|
||
|
||
### Decision 1: 使用 Python 本地桌面应用作为第一版运行形态
|
||
|
||
选择:Python 主进程管理桌宠 UI、音频采集播放和 pipeline 状态。
|
||
|
||
理由:
|
||
|
||
1. 音频 I/O、本地模型和桌面窗口都可以在同一运行时内协调。
|
||
2. 初期链路短,便于调试 wake/VAD/STT/TTS 延迟。
|
||
3. 不需要额外维护前端进程和后端服务通信协议。
|
||
|
||
替代方案:
|
||
|
||
1. 前端桌宠加 Python 服务:UI 灵活,但第一版多一个进程通信边界。
|
||
2. 仅 pipeline 服务:实现更快,但不满足桌宠状态反馈目标。
|
||
|
||
### Decision 2: Transport 第一版只支持本机麦克风和扬声器
|
||
|
||
选择:定义可扩展 Transport 接口,但第一版只规划本机输入输出。
|
||
|
||
理由:
|
||
|
||
1. 用户当前目标是桌面宠物,不是远程设备网关。
|
||
2. 本机 Transport 能覆盖完整语音闭环。
|
||
3. 避免第一版被 WebSocket、硬件协议和文件流复杂度拖慢。
|
||
|
||
替代方案:
|
||
|
||
1. 多 Transport 同时开发:测试能力更强,但超出第一期范围。
|
||
2. 文件回放优先:适合测试,但不能满足真实麦克风唤醒体验。
|
||
|
||
### Decision 3: 唤醒词固定为“小杰小杰”
|
||
|
||
选择:规范固定第一版唤醒词为“小杰小杰”,检测在本地完成。
|
||
|
||
理由:
|
||
|
||
1. 唤醒词是用户明确指定的产品入口。
|
||
2. 本地检测减少持续上传音频的隐私风险。
|
||
3. 固定关键词便于建立误唤醒和漏唤醒验收。
|
||
|
||
替代方案:
|
||
|
||
1. 按钮触发:降低模型风险,但不满足 hands-free 目标。
|
||
2. 英文预置热词:容易接入,但不符合中文桌宠产品体验。
|
||
|
||
### Decision 4: STT/TTS 优先本地,LLM 走云端
|
||
|
||
选择:STT/TTS 默认候选 `sherpa-onnx`,LLM 使用 OpenAI Responses API streaming。
|
||
|
||
理由:
|
||
|
||
1. STT/TTS 本地化可以减少原始音频和回复音频的云端依赖。
|
||
2. LLM 云端可以获得更稳定的推理质量和更低工程风险。
|
||
3. Provider 接口可替换,后续可按效果调整模型栈。
|
||
|
||
替代方案:
|
||
|
||
1. 全云端:开发快,但隐私和成本风险更高。
|
||
2. 全本地:隐私好,但模型效果、体积和性能风险更高。
|
||
|
||
### Decision 5: Pipeline 用异步状态机驱动
|
||
|
||
选择:所有阶段通过状态机和事件串联,Provider 失败统一进入错误恢复。
|
||
|
||
理由:
|
||
|
||
1. 语音链路存在大量异步事件:音频帧、唤醒、VAD、LLM streaming、TTS 队列、播放完成。
|
||
2. 状态机便于 UI 同步展示和测试断言。
|
||
3. 错误恢复路径清晰,避免某个阶段异常导致应用退出。
|
||
|
||
替代方案:
|
||
|
||
1. 顺序阻塞脚本:实现简单,但 UI 卡顿、延迟高、难以打断和恢复。
|
||
2. 多线程无状态共享:容易产生竞态和不可复现错误。
|
||
|
||
### Decision 6: 桌宠资产后续由 `imagegen` 生成,项目内保存
|
||
|
||
选择:用 `imagegen` 生成可爱 3D 透明 PNG,并在项目资产目录保存运行时引用的资产;若生成流程不可用,必须提供明确失败原因并保留代码级资产校验入口。
|
||
|
||
理由:
|
||
|
||
1. 资产必须和 UI 状态绑定,不能只做临时预览图。
|
||
2. 项目引用的资产不能留在 `$CODEX_HOME` 生成目录。
|
||
3. 透明背景资产需要本地验证边缘质量。
|
||
|
||
替代方案:
|
||
|
||
1. 用占位 SVG:可快速开发,但不满足“桌宠照片自己用生图技能来做”。
|
||
2. 使用外部图片素材:版权和风格一致性不可控。
|
||
|
||
### Decision 7: 每个大模块完成后立即验证并提交
|
||
|
||
选择:以后续实施中的一级任务组和阶段里程碑作为强制提交边界。每完成一个大模块,必须先运行当前阶段适用的构建、测试、lint/type check 和 OpenSpec 校验,再使用中文格式提交,格式为“`[模块名]:完成[具体功能描述],包含[关键变更]`”。
|
||
|
||
理由:
|
||
|
||
1. 该项目从空仓库开始,模块边界清晰提交可以避免大量中间状态堆积。
|
||
2. 构建先于提交可以确保每个里程碑都是可回退的稳定点。
|
||
3. 中文固定格式能让后续审计和阶段回溯更直接。
|
||
|
||
替代方案:
|
||
|
||
1. 最后统一提交:减少提交数量,但会隐藏中间失败和跨模块混杂变更。
|
||
2. 随手小提交:回退粒度细,但不能保证每个提交对应可验收模块。
|
||
|
||
## Architecture
|
||
|
||
```text
|
||
AudioTransport(input)
|
||
-> WakeWordProvider
|
||
-> VadProvider
|
||
-> SttProvider
|
||
-> ConversationContext
|
||
-> LlmProvider
|
||
-> SentenceBuffer
|
||
-> TtsProvider
|
||
-> AudioTransport(output)
|
||
|
||
Pipeline State Machine
|
||
-> PetUI State Adapter
|
||
-> PetWindow / PetSpriteView / SettingsPanel / ErrorToast
|
||
|
||
Config Loader
|
||
-> Provider config
|
||
-> Audio device config
|
||
-> LLM config
|
||
-> Asset paths
|
||
|
||
Structured Logger
|
||
<- state transitions
|
||
<- provider latency
|
||
<- error codes
|
||
```
|
||
|
||
## Data Flow
|
||
|
||
1. 应用启动后进入 `idle`,完成配置校验和 Provider 预加载。
|
||
2. 输入 Transport 持续提供音频帧。
|
||
3. WakeWordProvider 检测到“小杰小杰”后触发 `WakeEvent`。
|
||
4. VAD 开始端点检测,确认用户说话后聚合语音片段。
|
||
5. STT 转写音频片段,生成用户文本。
|
||
6. ConversationContext 添加用户消息并构造 LLM messages。
|
||
7. LLM Provider 返回流式 delta。
|
||
8. SentenceBuffer 按分句边界输出 TTS 文本块。
|
||
9. TTS Provider 合成音频块。
|
||
10. 输出 Transport 播放音频块。
|
||
11. UI 订阅状态变化,不直接调用 Provider。
|
||
|
||
## Interfaces
|
||
|
||
### AudioTransport
|
||
|
||
```text
|
||
start_input(device_id, sample_rate, channels) -> None
|
||
read_frames(timeout_ms) -> list[AudioFrame]
|
||
play_pcm(segment, interrupt=false) -> PlaybackResult
|
||
stop() -> None
|
||
health() -> TransportHealth
|
||
```
|
||
|
||
### WakeWordProvider
|
||
|
||
```text
|
||
load(model_path, keyword) -> None
|
||
detect(frame) -> WakeEvent | None
|
||
reset() -> None
|
||
```
|
||
|
||
### VadProvider
|
||
|
||
```text
|
||
load(model_path) -> None
|
||
analyze(frame) -> VadResult
|
||
reset() -> None
|
||
```
|
||
|
||
### SttProvider
|
||
|
||
```text
|
||
load(model_path, language) -> None
|
||
transcribe(segment) -> Transcript
|
||
```
|
||
|
||
### LlmProvider
|
||
|
||
```text
|
||
stream_reply(messages, model, timeout_s) -> AsyncIterator[ReplyDelta]
|
||
```
|
||
|
||
### TtsProvider
|
||
|
||
```text
|
||
load(model_path, voice) -> None
|
||
synthesize(text) -> AudioSegment
|
||
```
|
||
|
||
## Risks / Trade-offs
|
||
|
||
1. 中文唤醒词效果风险 -> Provider 可替换,先定义误唤醒和漏唤醒验收。
|
||
2. 本地模型性能风险 -> 模型预加载,阶段耗时日志,必要时替换 Provider。
|
||
3. LLM 网络依赖风险 -> 统一错误码,超时配置,错误恢复状态。
|
||
4. TTS 自回声风险 -> 播放期间默认抑制 Wake/VAD。
|
||
5. UI 与音频并发风险 -> UI 只订阅事件,不阻塞 pipeline。
|
||
6. 空仓库范围膨胀风险 -> 本阶段只写 OpenSpec,第一版只支持本机 Transport。
|
||
7. 未提交中间状态风险 -> 大模块完成后强制先验证再提交,失败时不得提交。
|
||
|
||
## Migration Plan
|
||
|
||
当前无既有数据和源码,不需要迁移。后续实现按 tasks.md 分阶段落地。若引入持久化配置或历史记录,必须保证旧配置有默认值或明确启动错误,并另起 OpenSpec 变更处理长期记忆。
|
||
|
||
## Open Questions
|
||
|
||
1. `sherpa-onnx` 是否作为唤醒词、STT、TTS 三者统一默认实现,还是只作为 STT/TTS 默认候选。
|
||
2. OpenAI Responses API 默认模型名和预算限制。
|
||
3. 桌宠角色的名字、颜色、性格和禁止元素。
|
||
4. 是否需要在播放期间支持用户语音打断。
|