[OpenSpec 与项目骨架]:完成实施型变更与 Python 基础骨架,包含 OpenSpec 工件、核心模型、配置边界和基础测试

This commit is contained in:
mkbk
2026-06-17 18:12:58 +08:00
commit 86ad0c86f2
21 changed files with 2410 additions and 0 deletions
@@ -0,0 +1,241 @@
# 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. 是否需要在播放期间支持用户语音打断。