654 lines
14 KiB
Markdown
654 lines
14 KiB
Markdown
# 技术方案:语音共创 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 与更细确认交互的基础
|
||
|
||
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/<session_id>/`
|
||
|
||
目录下可包括:
|
||
|
||
- `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 草图
|
||
|
||
相比直接写实现代码,这两步能更稳地把范围钉住。
|