# 技术方案:语音共创 Phase A MVP **Version**: 0.1 **Date**: 2026-04-19 **Status**: Draft / Companion to `voice-co-creation-mode-incremental-prd.md` --- ## 1. 目标 这份技术方案用于把 [语音共创模式增量 PRD](../product/voice-co-creation-mode-incremental-prd.md) 收敛成一个可实现的 Phase A MVP。 ## 0. 当前实现快照(2026-04-20) 远端 `main` 已经跑通以下 Phase A 主链路: - 独立 Voice Studio 入口页与最近会话恢复 - `voice_sessions / voice_turns / voice_session_events` 数据模型与 API - 文本 fallback 回合、录音上传回合、turn 轮询结果查询 - turn 级语音补发、失败重试、会话 abandon、finalize -> Story 持久化 - 会话事件记录与最近 turn / 最近事件展示 本轮新增收束: - 当 `transcript_confidence` 或 `intent_confidence` 偏低时,后端优先返回确认提示,而不是直接把这一轮写进故事正文 - 已补完整确认流:支持“按这个理解继续”“重说本轮”“改成文本输入” - 前端明确展示“本轮系统理解为”与“建议家长确认后再继续”提示 - 低置信度确认链路已有后端测试覆盖,可作为下一阶段继续验收真实 ASR Key 环境与更细确认交互的基础 - 已新增用户转写安全检查、assistant 输出柔性改写与 `safety_flags` 事件记录 - finalize 会生成更稳定的标题/摘要,并在条件允许时自动排队封面补全 job - 已新增 `voice session analytics` 聚合指标,可跟踪 turn 成功率、ASR/TTS 失败、低置信度触发、finalize 转化率、输入构成、语音时长、Provider 分布、确认率和平均置信度,并支持按转写 Provider 与会话状态筛选 - `voice session finalize` 现在会返回可追踪的 `generation_job_id`,让正式 Story 资产补全重新接回现有 generation trace 主干 - 语音共创触发的 `asset_generation` job 现在也支持沿用统一 generation job 的取消 / 重试控制 Phase A 的核心目标不是做“完全实时的语音陪伴”,而是验证以下最小闭环: 1. 孩子可以用语音发起一个故事 2. 孩子可以在中途用语音修正故事走向 3. 系统能用语音回应并继续讲述 4. 会话结束后可以保存为正式故事,进入现有故事库 这一步必须尽量复用现有统一生成工作流、Profile、Universe、Memory、TTS 与故事持久化主干,而不是重新搭一套平行系统。 --- ## 2. 非目标 Phase A 明确不做以下内容: - 不做 WebRTC / 全双工实时通话 - 不做复杂 barge-in(系统说到一半被自然打断) - 不做多人共创 - 不做绘本共创主链路 - 不做每回合即时插图生成 - 不把 Realtime 能力立刻并入当前 admin Provider 配置面板;ASR 已作为 Alpha 运营观测能力进入 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 初始设计时明显缺失、Alpha 已补齐的能力 - 语音输入识别(ASR / STT):已通过 `asr` Provider capability、demo fallback 和 `openai_asr` 适配器补齐,真实 Key 环境仍需验收。 - 会话级状态模型:已落地 `voice_sessions / voice_turns / voice_session_events`。 - “剧情修正”语义解析:Alpha 已支持 start / continue / correct 等回合意图。 - 会话级可观测事件:已支持 voice session analytics、事件列表和管理端 ASR 摘要。 - 从 voice session 保存为正式 Story 的收束服务:已支持 finalize 保存为 Story,并接回 generation job 资产补全。 --- ## 5. 核心设计原则 1. **会话先于结果** 语音共创首先是一个 session,不是一次性 generation request。 2. **最终结果仍然落到 Story** voice session 是过程对象,Story 才是最终可回看、可补资产、可沉淀记忆的正式对象。 3. **过程状态与结果状态分离** `voice_sessions` 管过程,`stories` 管正式结果,避免把会话噪音直接污染正式故事结构。 4. **先复用 `text` / `tts` 主干,再决定是否拆新 capability** 首版把复杂度压到最小,不急着把 realtime / barge-in 等新能力映射进 admin Provider 面板。ASR 现在只作为回合式转写能力进入 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 先接单一稳定供应商,并保留 demo fallback - 已并入当前 admin Provider CRUD 和运营摘要,但不引入 realtime 复杂配置 - 先通过配置文件或单独 service 封装真实 Key 环境差异 - 真实 Key 验收用 `SMOKE_REAL_ASR=1 ./scripts/demo_smoke.sh`,只在显式打开时调用外部 ASR 理由是: - 当前 admin Provider 已扩展到 `text/image/tts/storybook/asr` - Phase A Alpha 已把 ASR 纳入最小 Provider 能力,但仍保留 demo fallback,避免真实转写不可用时阻塞演示 - `openai_asr` 默认读取 `OPENAI_API_KEY`、可选 `OPENAI_API_BASE`、`VOICE_TRANSCRIPTION_MODEL` 和 `VOICE_TRANSCRIPTION_LANGUAGE` 真实 ASR 验收最小 `.env`: ```env ASR_PROVIDERS=["openai_asr", "demo"] OPENAI_API_KEY=sk-... OPENAI_API_BASE= VOICE_TRANSCRIPTION_MODE=provider VOICE_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe VOICE_TRANSCRIPTION_LANGUAGE=zh ``` 失败时优先看三处:上传接口响应、`turn_transcription_failed` 事件、Admin Provider analytics 的 `capability=asr` failure reasons。常见原因是 key 没进容器、401/403、429/额度不足、模型不可用、`OPENAI_API_BASE` 指向错误或音频格式不被接受。 ### 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 成本 当前 Alpha 已把 ASR 成本和调用摘要接入管理端 Provider analytics。短期这样可以让运营视角统一看到 text/image/tts/storybook/asr;中期如果语音共创继续扩大,应把 voice session analytics 保持为主视图,把 admin Provider analytics 只作为跨能力成本与失败原因摘要。 ### 13.3 服务复杂度自审(2026-05-06) 当前 Alpha 已经验证主链路,但服务边界开始接近需要拆分的程度: | 模块 | 当前职责 | 复杂度信号 | 建议拆分 | | --- | --- | --- | --- | | `voice_session_service.py` | 会话 CRUD、turn 创建、意图识别、故事 patch、低置信度确认、安全改写、TTS、finalize、analytics | 文件已接近 2000 行;同步处理状态机、AI 编排和响应序列化,单次改动容易波及多条路径 | 优先拆 `voice_turn_orchestrator.py`、`voice_session_analytics.py`、`voice_session_finalizer.py` | | `generation_jobs.py` + `admin_provider_analytics.py` | generation job/event、任务控制、provider stats、ops summary;管理端跨用户 Provider/ASR 摘要已拆到独立 service | `generation_jobs.py` 仍偏大,但 ASR 管理端摘要已不再继续塞进 generation job 模块 | 后续继续把 `generation_jobs.py` 内部 provider telemetry helper 拆为共享小模块,保留 generation job 主流程聚焦任务状态 | | `voice_transcription_service.py` | ASR mode 解析与 provider router 调用 | 仍较小,但失败元数据不足,admin ASR 失败只能从事件里读 `error` | 后续补 `VoiceTranscriptionAttempt` 风格的轻量结果结构,统一 provider、latency、cost、error | | 前端 `VoiceStudio.vue` | 页面状态、录音上传、会话列表、turn 展示、analytics 卡片、确认/重试/finalize | 视图文件承担了太多 workflow 判断;继续加实时能力会变得难测 | 拆出 `useVoiceSessionWorkflow`、`VoiceTurnCard`、`VoiceAnalyticsPanel` | 建议拆分顺序: 1. 先拆只读 analytics:风险最低,测试可以复用现有 `test_voice_sessions.py` 与 `test_admin_providers.py`。2026-05-06 已先拆出管理端 `admin_provider_analytics.py`。 2. 再拆 finalize:边界清晰,输入是 session,输出是 Story / generation job。 3. 最后拆 turn orchestrator:它耦合 ASR、意图、故事 patch、安全和 TTS,应等回归矩阵更稳定后再动。 暂不建议在 Phase A Alpha 末尾做的大改: - 不引入工作流引擎替代当前状态机。 - 不把 voice session 直接塞进 generation job 主模型。 - 不在 ASR 事件上新增迁移字段,除非要做精确延迟分布和供应商级 SLA。 触发必须拆分的信号: - 单个 voice turn 改动需要同时修改 3 个以上测试文件。 - 新增一个 analytics 字段需要读写多个无关 service。 - Voice Studio 引入实时或准实时能力前,仍没有可复用 composable。 --- ## 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 草图 相比直接写实现代码,这两步能更稳地把范围钉住。