|
|
@@ -0,0 +1,165 @@
|
|
|
+## Context
|
|
|
+
|
|
|
+当前 `DialogueChatView.vue` 是一个 1041 行的单文件组件,所有对话逻辑(状态管理、录音模拟、评估生成、UI 渲染)全部内联在一个文件中,使用硬编码的 `dialogueScript` 数组驱动对话轮次。
|
|
|
+
|
|
|
+现有架构问题:
|
|
|
+- 无真实录音能力,录音按钮仅切换 `isRecording` 布尔值
|
|
|
+- AI 回复来自本地脚本数组,无 LLM 调用
|
|
|
+- 评估数据由 `generateMockEvaluation()` 随机生成
|
|
|
+- 无异常处理(网络断开、请求超时、消息重发)
|
|
|
+- 无消息状态管理(无法区分消息是发送中、流式中还是失败)
|
|
|
+- UI 和业务逻辑完全耦合,难以测试和维护
|
|
|
+
|
|
|
+技术约束:
|
|
|
+- 前端:Vue 3 + TypeScript + Composition API,优先使用浏览器原生 API
|
|
|
+- 后端:独立项目 `cococlass-english-speaking-api`(Python + FastAPI)
|
|
|
+- LLM 通过 One-Hub 统一网关调用(Chat Completions API)
|
|
|
+- ASR 和发音评估使用 Azure Speech
|
|
|
+
|
|
|
+## Goals / Non-Goals
|
|
|
+
|
|
|
+**Goals:**
|
|
|
+- 将对话引擎从 UI 组件中解耦为独立的 composable + service 三层架构
|
|
|
+- 实现完整的消息状态机(loading / done / error)
|
|
|
+- 支持 LLM 流式输出(fetch streaming),逐 token 渲染
|
|
|
+- 实现异常处理:请求失败 → 显示重试按钮 → 重新发送
|
|
|
+- 封装录音采集(MediaRecorder)为独立 composable,含权限处理
|
|
|
+- 设计 API 抽象层,支持 mock adapter 和真实 API adapter 无缝切换
|
|
|
+- 后端实现对话编排服务,提供 session/speak/report 三个接口
|
|
|
+
|
|
|
+**Non-Goals:**
|
|
|
+- 不重新设计 UI 外观——保留现有的视觉样式,仅重构逻辑层
|
|
|
+- 不涉及离线模式或本地缓存
|
|
|
+- 不做自动重试 / 指数退避(v1 先不做,反馈后迭代)
|
|
|
+- 不做服务端 TTS(v1 用前端 Web Speech API)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 前端架构
|
|
|
+
|
|
|
+### Decision 1: 三层架构(Service → Composable → Component)
|
|
|
+
|
|
|
+**选择**: `llmService`(通信层)→ `useDialogueEngine`(状态编排层)→ `DialogueChatView`(UI 层)
|
|
|
+
|
|
|
+**理由**:
|
|
|
+- Service 层不依赖 Vue,可单元测试,也可在其他场景复用
|
|
|
+- Composable 层用 Vue 的 ref/reactive 管理状态
|
|
|
+- Component 层变为纯粹的展示层
|
|
|
+
|
|
|
+### Decision 2: 使用 fetch + ReadableStream
|
|
|
+
|
|
|
+**选择**: `fetch()` + `response.body.getReader()` 实现流式读取
|
|
|
+
|
|
|
+**理由**: EventSource 只支持 GET,fetch streaming 支持 POST + AbortController + 自定义 headers
|
|
|
+
|
|
|
+### Decision 3: 消息状态简化为 3 个
|
|
|
+
|
|
|
+**选择**: `loading | done | error`,不细分内部阶段
|
|
|
+
|
|
|
+**理由**:
|
|
|
+- 用户不关心是 ASR 阶段还是 LLM 阶段,只关心"在处理/好了/出错了"
|
|
|
+- 错误处理统一:出错 → 显示重试按钮 → 重新调 /speak
|
|
|
+- 第一版先简单,反馈后再迭代更精细的状态
|
|
|
+
|
|
|
+### Decision 4: 录音使用浏览器原生 MediaRecorder API
|
|
|
+
|
|
|
+**选择**: 直接封装 `MediaRecorder`,不引入第三方录音库
|
|
|
+
|
|
|
+**理由**: 现代浏览器兼容性已足够,避免额外依赖
|
|
|
+
|
|
|
+### Decision 5: API Adapter 对接后端 3 个接口
|
|
|
+
|
|
|
+**选择**: `DialogueAPI` 接口对应后端的 session / speak / report
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface DialogueAPI {
|
|
|
+ createSession(config: SessionConfig): Promise<SessionInfo>
|
|
|
+ speak(sessionId: string, audioBlob: Blob, signal: AbortSignal): AsyncGenerator<SSEEvent>
|
|
|
+ getReport(sessionId: string): Promise<Report>
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**实现**: `MockDialogueAPI`(开发用)和 `RealDialogueAPI`(对接真实后端)
|
|
|
+通过前端 prop `mode: 'preview' | 'real'` 切换
|
|
|
+
|
|
|
+### Decision 6: TTS 使用前端 Web Speech API(v1)
|
|
|
+
|
|
|
+**选择**: AI 消息 `done` 后调用 `speechSynthesis.speak()` 朗读
|
|
|
+
|
|
|
+**理由**: 零后端改动,3 行代码立即可用。质量不够后续升级 Azure TTS
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 后端架构
|
|
|
+
|
|
|
+### 项目:cococlass-english-speaking-api
|
|
|
+
|
|
|
+独立编排服务,串联 Azure Speech 和 One-Hub,管理对话 session 状态。
|
|
|
+
|
|
|
+```
|
|
|
+前端
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+cococlass-english-speaking-api(编排层)
|
|
|
+ ├──→ Azure Speech ASR + 发音评估
|
|
|
+ ├──→ One-Hub LLM 流式对话(Chat Completions)
|
|
|
+ ├──→ MySQL session + 消息 + 评估结果
|
|
|
+ └──→ S3 音频文件存储
|
|
|
+```
|
|
|
+
|
|
|
+### 技术栈
|
|
|
+
|
|
|
+| 层 | 选择 |
|
|
|
+|----|------|
|
|
|
+| 框架 | FastAPI + uvicorn |
|
|
|
+| LLM | Chat Completions via One-Hub(openai SDK) |
|
|
|
+| ASR | Azure Speech(纯转录模式) |
|
|
|
+| 发音评估 | Azure Speech Pronunciation Assessment(后台) |
|
|
|
+| SSE | FastAPI StreamingResponse |
|
|
|
+| 数据库 | MySQL + asyncmy + SQLAlchemy 2.0 |
|
|
|
+| 存储 | S3(boto3) |
|
|
|
+| 配置 | pydantic-settings + python-dotenv |
|
|
|
+
|
|
|
+### Decision 7: 不使用 Assistants API
|
|
|
+
|
|
|
+**理由**: 口语对话是简单一问一答,不需要 Thread/Run 机制。对话历史要存自己 DB,Assistants 的 Thread 管理是重复功能。
|
|
|
+
|
|
|
+### Decision 8: 后端统一编排,前端只发一个请求
|
|
|
+
|
|
|
+**理由**: 前端不应知道后端内部有几步(ASR/LLM/评估)。音频只上传一次,错误处理统一。
|
|
|
+
|
|
|
+### Decision 9: 对话中不展示评分
|
|
|
+
|
|
|
+**理由**: 保证练习流畅度。发音评估后台静默执行,结果页才展示。
|
|
|
+
|
|
|
+### Decision 10: Protocol + 依赖注入
|
|
|
+
|
|
|
+ASRProvider / LLMProvider / PronunciationAssessor / AudioStorage 四个 Protocol 接口,具体实现可替换可测试。
|
|
|
+
|
|
|
+### Decision 11: 不用 Redis
|
|
|
+
|
|
|
+**理由**: 3 轮对话只有 3 次 DB 查询,不值得加缓存层。
|
|
|
+
|
|
|
+### API 设计
|
|
|
+
|
|
|
+```
|
|
|
+POST /api/speaking/dialogue/session → JSON { sessionId, aiMessage }
|
|
|
+POST /api/speaking/dialogue/speak → SSE { transcript, token, done }
|
|
|
+GET /api/speaking/dialogue/report → 延后(结果页方案待确认)
|
|
|
+```
|
|
|
+
|
|
|
+### 数据模型
|
|
|
+
|
|
|
+- **DialogueSession**: id, user_id, topic, role_config, total_rounds, current_round, status, system_prompt, created_at, completed_at
|
|
|
+- **DialogueMessage**: id, session_id, round, role, content, audio_url, created_at
|
|
|
+- **PronunciationEvaluation**: id, message_id, session_id, round, status, accuracy/fluency/completeness/prosody scores, word_analysis, created_at, completed_at
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Risks / Trade-offs
|
|
|
+
|
|
|
+- **[MediaRecorder 格式兼容性]** → Safari 不支持 webm,需 fallback 到 mp4/wav
|
|
|
+- **[流式解析差异]** → 在 adapter 层做格式解析,engine 层只关心 token string
|
|
|
+- **[录音权限拒绝]** → composable 暴露 permissionState,UI 显示引导提示
|
|
|
+- **[内存泄漏]** → 在 composable 的 `onUnmounted` 中统一清理录音 Blob
|
|
|
+- **[后台评估未完成]** → /report 接口最多等 5 秒,超时返回部分结果
|