diff --git a/docs/README.md b/docs/README.md index 4e72526..d726a3e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,9 @@ - `technical/provider-routing.md` Provider Routing 技术说明。用于解释 Capability / Provider / Adapter / Routing Policy 的职责边界。 +- `technical/voice-co-creation-phase-a-tech-spec.md` + 语音共创 Phase A 技术方案。用于把增量 PRD 收敛成最小可实现的会话模型、API 草图、状态机与实现顺序。 + ## 维护规则 - 新 PRD 放到 `docs/product/` diff --git a/docs/technical/voice-co-creation-phase-a-tech-spec.md b/docs/technical/voice-co-creation-phase-a-tech-spec.md new file mode 100644 index 0000000..c6dd64d --- /dev/null +++ b/docs/technical/voice-co-creation-phase-a-tech-spec.md @@ -0,0 +1,637 @@ +# 技术方案:语音共创 Phase A MVP + +**Version**: 0.1 +**Date**: 2026-04-19 +**Status**: Draft / Companion to `voice-co-creation-mode-incremental-prd.md` + +--- + +## 1. 目标 + +这份技术方案用于把 [语音共创模式增量 PRD](/Users/zt/Code/dreamweaver/docs/product/voice-co-creation-mode-incremental-prd.md) 收敛成一个可实现的 Phase A MVP。 + +Phase A 的核心目标不是做“完全实时的语音陪伴”,而是验证以下最小闭环: + +1. 孩子可以用语音发起一个故事 +2. 孩子可以在中途用语音修正故事走向 +3. 系统能用语音回应并继续讲述 +4. 会话结束后可以保存为正式故事,进入现有故事库 + +这一步必须尽量复用现有统一生成工作流、Profile、Universe、Memory、TTS 与故事持久化主干,而不是重新搭一套平行系统。 + +--- + +## 2. 非目标 + +Phase A 明确不做以下内容: + +- 不做 WebRTC / 全双工实时通话 +- 不做复杂 barge-in(系统说到一半被自然打断) +- 不做多人共创 +- 不做绘本共创主链路 +- 不做每回合即时插图生成 +- 不把 ASR / Realtime 能力立刻并入当前 admin Provider 配置面板 + +换句话说,Phase A 是一个 **回合式 voice session MVP**,不是最终形态。 + +--- + +## 3. 推荐体验形态 + +### 3.1 交互模式 + +- 前端提供独立“语音共创”入口 +- 用户按住说话或点击开始录音 +- 一次提交一段语音 +- 后端完成:转写 -> 意图识别 -> 更新故事状态 -> 生成回复 -> 合成语音 +- 前端轮询或 SSE 获取这一轮结果 +- 播放系统语音并展示文本摘要 +- 进入下一轮 + +### 3.2 为什么先做回合式 + +- 与现有 `generation_job` + 轮询模式更一致 +- 更容易做会话恢复和状态排障 +- 对延迟要求低于全双工实时 +- 能先验证“孩子是否愿意用语音改故事” + +--- + +## 4. 与现有系统的关系 + +### 4.1 可以直接复用的能力 + +- `users` +- `child_profiles` +- `story_universes` +- `build_enhanced_memory_context` +- `stories` +- `generation_jobs` / `generation_job_events` +- `text` 能力对应的现有文本模型 +- `tts` Provider Router +- 现有故事库、故事详情页和后续资产补全链路 + +### 4.2 当前明显缺失的能力 + +- 语音输入识别(ASR / STT) +- 会话级状态模型 +- “剧情修正”语义解析 +- 会话级可观测事件 +- 从 voice session 保存为正式 Story 的收束服务 + +--- + +## 5. 核心设计原则 + +1. **会话先于结果** + 语音共创首先是一个 session,不是一次性 generation request。 + +2. **最终结果仍然落到 Story** + voice session 是过程对象,Story 才是最终可回看、可补资产、可沉淀记忆的正式对象。 + +3. **过程状态与结果状态分离** + `voice_sessions` 管过程,`stories` 管正式结果,避免把会话噪音直接污染正式故事结构。 + +4. **先复用 `text` / `tts` 主干,再决定是否拆新 capability** + 首版把复杂度压到最小,不急着把所有新能力都映射进 admin Provider 面板。 + +5. **首版允许“文本可用但语音失败”降级** + 这与当前 DreamWeaver 主结果优先可读的原则一致。 + +--- + +## 6. 数据模型建议 + +## 6.1 `voice_sessions` + +用于承载一个完整的语音共创会话。 + +**建议字段** + +- `id: str` +- `user_id: str` +- `child_profile_id: str | None` +- `universe_id: str | None` +- `status: str` +- `target_mode: str` + Phase A 固定为 `story` +- `current_turn_index: int` +- `working_title: str | None` +- `story_state: dict` +- `latest_user_transcript: str | None` +- `latest_assistant_text: str | None` +- `final_story_id: int | None` +- `last_error: str | None` +- `created_at` +- `updated_at` + +**`story_state` 建议结构** + +```json +{ + "premise": "小猫去太空", + "characters": ["小猫", "月亮朋友"], + "tone": "温暖冒险", + "beats": [ + "准备出发", + "遇到困难", + "得到帮助" + ], + "narrative_segments": [ + "第一段故事", + "第二段故事" + ], + "latest_direction": "不要让它哭,给它一个朋友", + "safety_flags": [] +} +``` + +### 6.2 `voice_turns` + +用于记录会话中的每一轮输入与响应。 + +**建议字段** + +- `id: str` +- `session_id: str` +- `turn_index: int` +- `status: str` +- `user_audio_path: str | None` +- `user_transcript: str | None` +- `transcript_confidence: float | None` +- `detected_intent: str` +- `intent_confidence: float | None` +- `story_patch: dict` +- `assistant_text: str | None` +- `assistant_audio_path: str | None` +- `assistant_duration_ms: int | None` +- `error_message: str | None` +- `created_at` +- `updated_at` + +**`detected_intent` 首版建议值** + +- `start_story` +- `continue_story` +- `correct_story` +- `end_story` +- `save_story` +- `unknown` + +### 6.3 `voice_session_events` + +建议新增轻量 append-only 事件表,而不是强行复用 `generation_job_events`。 + +原因很简单: + +- `generation_job_events` 的主语是“后台生成任务” +- `voice_session_events` 的主语是“语音会话过程” + +二者关注点不同,不应混在一个表里。 + +**建议字段** + +- `id: int` +- `session_id: str` +- `turn_id: str | None` +- `event_type: str` +- `status: str` +- `message: str | None` +- `event_metadata: dict` +- `created_at` + +### 6.4 为什么首版不单独建 `voice_story_snapshots` + +首版先把故事中间态压缩进 `voice_sessions.story_state` 和 `voice_turns.story_patch` 即可。 + +如果后续需要: + +- 可回滚到任意 turn +- 支持剧情分支 +- 做更复杂的 diff / merge + +再考虑独立 `voice_story_snapshots` 表。 + +--- + +## 7. 状态机建议 + +## 7.1 Session 状态 + +**建议值** + +- `draft` +- `active` +- `processing_turn` +- `waiting_user` +- `finalizing_story` +- `completed` +- `abandoned` +- `failed` + +**状态含义** + +- `draft`: 会话已创建但还没有第一轮输入 +- `active`: 已进入共创流程 +- `processing_turn`: 正在处理某一轮语音 +- `waiting_user`: 系统已经返回,等待下一轮输入 +- `finalizing_story`: 正在把会话收束为正式 Story +- `completed`: 已完成并成功保存 +- `abandoned`: 用户主动结束但未保存 +- `failed`: 当前会话不可继续,需要用户重新开始 + +## 7.2 Turn 状态 + +**建议值** + +- `received` +- `transcribing` +- `intent_resolved` +- `narrative_ready` +- `audio_ready` +- `failed` + +--- + +## 8. API 草图 + +## 8.1 创建会话 + +`POST /api/voice-sessions` + +**Request** + +```json +{ + "child_profile_id": "profile-id", + "universe_id": "universe-id", + "target_mode": "story" +} +``` + +**Response** + +```json +{ + "id": "session-id", + "status": "draft", + "target_mode": "story", + "current_turn_index": 0, + "final_story_id": null +} +``` + +## 8.2 查询会话详情 + +`GET /api/voice-sessions/{session_id}` + +返回: + +- session 基础信息 +- 当前 `story_state` +- 最近若干 turn +- 是否可继续 +- 是否可保存 + +## 8.3 提交一轮语音输入 + +`POST /api/voice-sessions/{session_id}/turns` + +建议首版使用 `multipart/form-data`: + +- `audio_file` +- `duration_ms` +- `client_turn_id`(可选) + +如果后续为了调试和降级,也可以兼容: + +- `transcript_text` + +**Response** + +```json +{ + "turn_id": "turn-id", + "session_id": "session-id", + "status": "received" +} +``` + +后续由前端轮询 turn 或 session detail。 + +## 8.4 查询 turn 结果 + +`GET /api/voice-sessions/{session_id}/turns/{turn_id}` + +返回: + +- `user_transcript` +- `detected_intent` +- `assistant_text` +- `assistant_audio_url` 或音频状态 +- `status` + +## 8.5 结束并保存为正式故事 + +`POST /api/voice-sessions/{session_id}/finalize` + +**Request** + +```json +{ + "save_story": true, + "generate_cover": true, + "generate_final_audio": false +} +``` + +**Response** + +```json +{ + "session_id": "session-id", + "status": "completed", + "story_id": 123, + "generation_job_id": "optional-asset-job-id" +} +``` + +## 8.6 放弃会话 + +`POST /api/voice-sessions/{session_id}/abandon` + +用于显式结束但不保存。 + +--- + +## 9. 后端处理链路 + +## 9.1 一轮 turn 的推荐处理顺序 + +1. 接收语音文件 +2. 创建 `voice_turns` 记录,状态为 `received` +3. 保存原始音频文件到会话目录 +4. 调用 ASR,更新 `user_transcript` +5. 调用意图解析 / 对话编排器,得到: + - `detected_intent` + - `story_patch` + - `assistant_text` +6. 将 `story_patch` 合并进 `voice_sessions.story_state` +7. 调用 TTS 生成本轮系统回应语音 +8. 更新 turn 为 `audio_ready` 或 `narrative_ready` +9. 更新 session 为 `waiting_user` + +## 9.2 为什么首版不直接放进 Celery worker 主链路 + +当前 `generation_jobs` 很适合“提交一个任务,然后后台处理较长流程”,但语音共创 turn 的特点是: + +- 频率更高 +- 单轮更短 +- 更接近交互而不是离线任务 + +因此首版建议: + +- turn 处理由应用层直接编排 +- 必要时用短后台任务或轻量异步任务承接 +- 只有最终“保存为正式故事 + 补资产”再接回现有 generation 主干 + +## 9.3 最终收束为 Story 的方式 + +当用户选择 finalize 时: + +1. 把 `story_state.narrative_segments` 合并为最终正文 +2. 生成标题和摘要 +3. 写入 `stories` +4. 若需要封面或正式整篇朗读音频,再触发已有资产补全链路 + +首版建议不要让 finalize 再走完整“文本生成主任务”,因为正文已经在会话中逐步生成出来了。 + +--- + +## 10. Provider 与模型接入建议 + +## 10.1 Phase A 的最小能力分层 + +### A. ASR + +新增会话内语音转写能力。 + +**建议** + +- Phase A 先接单一稳定供应商 +- 暂不并入当前 admin Provider CRUD +- 先通过配置文件或单独 service 封装 + +理由是: + +- 当前 admin Provider 只有 `text/image/tts/storybook` +- 如果一开始把 `asr` 也并进全套管理能力,改动面会大很多 + +### B. Dialogue Orchestrator + +首版建议直接复用当前 `text` capability,而不是另开一套 provider type。 + +对 orchestrator 来说,我们更需要: + +- 意图识别稳定 +- 遵循结构化输出 +- 能基于已有 `story_state` 生成下一段文本 + +这本质上仍然可以由现有文本模型承担。 + +### C. TTS + +直接复用当前 `tts` Provider Router。 + +### D. Final Story Asset Generation + +继续复用已有 story asset completion 逻辑。 + +--- + +## 11. 文件与存储建议 + +### 11.1 音频文件目录 + +建议新增目录: + +`storage/voice_sessions//` + +目录下可包括: + +- `turn-001-user.webm` +- `turn-001-assistant.mp3` +- `turn-002-user.webm` +- `turn-002-assistant.mp3` + +### 11.2 为什么不直接复用 story audio cache + +因为二者语义不同: + +- `story audio cache` 是正式故事整篇或固定文本的可复用结果 +- `voice session audio` 是会话过程资产 + +不应混存。 + +--- + +## 12. 安全与家长控制 + +Phase A 至少要有以下机制: + +1. **儿童内容安全 prompt** +2. **转写后文本安全检查** +3. **低置信度识别提示** +4. **必要时家长可见的“系统理解为”** + +### 首版推荐策略 + +- 默认不要求每轮家长确认 +- 当 `transcript_confidence` 或 `intent_confidence` 低于阈值时,再提示确认 +- 如果内容越界,则给出柔性改写或安全拒绝 + +--- + +## 13. 可观测性建议 + +### 13.1 需要记录的关键事件 + +- `session_created` +- `turn_received` +- `turn_transcribed` +- `intent_resolved` +- `story_state_updated` +- `assistant_text_ready` +- `assistant_audio_ready` +- `session_finalizing` +- `session_saved_as_story` +- `session_failed` + +### 13.2 成本记录建议 + +Phase A 就应该按 turn 记录: + +- ASR 成本 +- 对话生成成本 +- TTS 成本 + +这部分后续可以汇总到新的语音共创 analytics,而不是一开始就挤进现有故事生成 dashboard。 + +--- + +## 14. 前端建议 + +### 14.1 Phase A 最小 UI + +- 独立入口页或 modal +- 录音按钮 +- 当前会话卡片 +- 本轮识别文本 +- 系统文本回应 +- 系统音频播放按钮 +- “继续说”按钮 +- “保存到故事库”按钮 + +### 14.2 状态反馈必须有 + +至少显示: + +- 正在转写 +- 正在理解故事走向 +- 正在讲述 +- 本轮失败,可重试 + +### 14.3 恢复策略 + +页面重新打开后: + +- 如果存在 `active / waiting_user` session,优先恢复最近会话 +- 展示最近一轮系统回应 +- 允许继续说或结束保存 + +--- + +## 15. 推荐实现顺序 + +### Step 1 + +- 新增 `voice_sessions` +- 新增 `voice_turns` +- 新增 `voice_session_events` +- 新增基础 schema + +### Step 2 + +- 新增会话 API +- 新增 turn API +- 打通音频文件存储 + +### Step 3 + +- 接入 ASR +- 实现 turn 编排 service +- 复用现有 `text` 与 `tts` 能力 + +### Step 4 + +- 实现 finalize -> Story 持久化 +- 接入现有故事库入口 + +### Step 5 + +- 补最小 UI +- 补会话恢复 +- 补日志和成本记录 + +--- + +## 16. 测试建议 + +### 单元测试 + +- `story_patch` 合并逻辑 +- 意图映射逻辑 +- finalize 正文拼接逻辑 + +### API 集成测试 + +- 创建会话 +- 提交 turn +- 查询 turn +- 恢复会话 +- finalize 为 Story + +### 失败场景测试 + +- ASR 失败 +- TTS 失败但文本成功 +- 低置信度输入 +- finalize 重复提交 + +--- + +## 17. 当前最合理的技术判断 + +如果目标是“尽快验证孩子能否通过声音共创故事”,那么最合理的 Phase A 路线是: + +1. 新增 `voice session` 层,而不是修改现有 `generation_jobs` 去硬扛会话语义 +2. 复用现有 `text` 与 `tts` 主干,不急着把所有新能力都产品化成新的 Provider 管理界面 +3. 先做回合式体验,不做复杂实时双工 +4. 最终结果仍然沉淀为 `Story`,继续接入已有故事库、资产补全和记忆系统 + +这条路线的优点是: + +- 架构增量最小 +- 与当前 DreamWeaver 主线一致 +- 既能验证产品价值,也不会过早引入过重的实时系统复杂度 + +--- + +## 18. 下一步建议 + +如果这份技术方案方向确认无误,下一轮最值得继续做的是两件事之一: + +1. 把 `voice_sessions / voice_turns / voice_session_events` 进一步细化成数据库迁移草案 +2. 把 Phase A 的 API request/response schema 写成后端伪代码或 OpenAPI 草图 + +相比直接写实现代码,这两步能更稳地把范围钉住。