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