9223ceca50
- 规则重组:全局/ 下 8 个规则合并为 6 个(01+02→01,05+06→04),序号顺延 - 新增项目规则 05-多入口功能同步规范(UI/语音入口覆盖检查) - 新增 MCP 服务基础设施:Mcp/ 目录(DI 注册、端点扩展、动态工具描述符)、单元测试 - v1.3.0 工单文档:03 系列(会议任务拆分)、04(富文本描述与附件管理) - MCP 接口与前端集成指南:docs/manual/08、09
200 lines
7.0 KiB
Markdown
200 lines
7.0 KiB
Markdown
# 研发工单 v1.3.0 - 03-02 音频录制与转写
|
||
|
||
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||
>
|
||
> 依赖:03-01(API 契约)
|
||
|
||
---
|
||
|
||
## 一、目标
|
||
|
||
实现前端音频录制(MediaRecorder API)和后端音频转写(STT)能力,打通"录音 → 上传 → 转写文字 → 保存为会议纪要"的完整链路。
|
||
|
||
## 二、范围
|
||
|
||
**包含**:
|
||
- 前端录音控件(`AudioRecorder.vue`)
|
||
- 前端录音组合式函数(`useAudioRecorder.ts`)
|
||
- 录音状态管理(录制中/暂停/停止/时长显示)
|
||
- 音频上传 API 交互(POST multipart/form-data)
|
||
- 后端音频文件临时存储
|
||
- 后端 STT 转写服务(`SttService.cs`)
|
||
- 转写结果回写 `MeetingNotes`
|
||
|
||
**不包含**:
|
||
- 实时转写(边录边转)
|
||
- 音频持久存储(转写完成后删除音频文件)
|
||
- 音频云同步
|
||
- 语音指令输入(属于工单 02)
|
||
|
||
## 三、前置条件
|
||
|
||
| 条件 | 说明 |
|
||
|---|---|
|
||
| 03-01 完成 | `MeetingController`、`MeetingService` 基础骨架就绪 |
|
||
|
||
## 四、详细规格
|
||
|
||
### 4.1 前端录音控件
|
||
|
||
**AudioRecorder.vue**:
|
||
|
||
```
|
||
┌────────────────────────────────┐
|
||
│ 🎤 会议录音 │
|
||
│ │
|
||
│ ● 录制中... 00:15:23 │
|
||
│ ┌──────────────────────────┐ │
|
||
│ │ ▁▃▂▄▅▂▁▃▄▅▃▂▁▂▄▅▃▁ │ │ ← 简易波形
|
||
│ └──────────────────────────┘ │
|
||
│ │
|
||
│ [⏹ 停止录音] │
|
||
│ │
|
||
│ 录音时长限制:最长 2 小时 │
|
||
└────────────────────────────────┘
|
||
```
|
||
|
||
状态:
|
||
- **就绪**:显示录音按钮
|
||
- **录制中**:显示停止按钮 + 时长计时 + 简易波形条
|
||
- **已停止**:显示"提交转写" / "重新录制"
|
||
|
||
### 4.2 useAudioRecorder 组合式函数
|
||
|
||
```typescript
|
||
/// <summary>音频录制器组合式函数,封装 MediaRecorder API</summary>
|
||
export function useAudioRecorder() {
|
||
// 状态
|
||
const isRecording = ref(false)
|
||
const isPaused = ref(false)
|
||
const duration = ref(0) // 秒
|
||
const audioBlob = ref<Blob | null>(null)
|
||
const audioUrl = ref<string | null>(null) // 用于预览播放
|
||
|
||
// 方法
|
||
async function startRecording(): Promise<void> // 请求麦克风权限,开始录制
|
||
function stopRecording(): void // 停止录制,生成 Blob
|
||
function resetRecording(): void // 重置状态
|
||
function getAudioBlob(): Blob | null // 获取录制结果
|
||
|
||
// 内部
|
||
let mediaRecorder: MediaRecorder | null = null
|
||
let timerInterval: number | null = null
|
||
|
||
// 音频格式:webm(Chrome/Firefox)、mp4(Safari)
|
||
// 时长限制:最长 2 小时(7200 秒)
|
||
|
||
return { isRecording, isPaused, duration, audioBlob, audioUrl,
|
||
startRecording, stopRecording, resetRecording, getAudioBlob }
|
||
}
|
||
```
|
||
|
||
### 4.3 前端 API 模块
|
||
|
||
```typescript
|
||
// src/Hua.Todo.Web/src/api/meeting.ts
|
||
|
||
/// <summary>上传音频文件并请求转写</summary>
|
||
async function transcribeAudio(taskId: number, audioBlob: Blob): Promise<TranscribeResponse>
|
||
{
|
||
const formData = new FormData();
|
||
formData.append('audio', audioBlob, 'meeting.webm');
|
||
|
||
const apiBaseUrl = window.__API_BASE_URL__ || 'http://localhost:5173/api';
|
||
const resp = await fetch(`${apiBaseUrl}/meeting/${taskId}/transcribe`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
if (!resp.ok) throw new Error(`转写请求失败: ${resp.status}`);
|
||
return resp.json();
|
||
}
|
||
```
|
||
|
||
### 4.4 后端 SttService
|
||
|
||
```csharp
|
||
/// <summary>语音转写服务接口</summary>
|
||
public interface ISttService
|
||
{
|
||
/// <summary>将音频文件转写为文字</summary>
|
||
/// <returns>转写文字</returns>
|
||
Task<string> TranscribeAsync(Stream audioStream, string format, CancellationToken ct = default);
|
||
}
|
||
```
|
||
|
||
实现策略(按优先级):
|
||
|
||
| 平台/环境 | 实现方式 | 说明 |
|
||
|---|---|---|
|
||
| Windows MAUI | `Windows.Media.SpeechRecognition` 文件识别 | 系统自带,离线可用 |
|
||
| macOS/iOS | `SFSpeechRecognizer` 文件识别 | 需在线 |
|
||
| Android | `SpeechRecognizer` | 需在线 |
|
||
| Linux | WebKit 在线;本地 `whisper.cpp` 降级 | 多策略 |
|
||
| 通用服务端 | 扩展 `LlmClientService` 调用 Whisper API | Host 端部署 |
|
||
|
||
> 初期(v1.3.0)优先实现服务端 Whisper API 调用方式(通过 `LlmClientService` 扩展),后续版本各平台原生逐补。
|
||
|
||
### 4.5 音频上传处理流程
|
||
|
||
```
|
||
前端 AudioRecorder → stopRecording → Blob (webm/mp4)
|
||
↓
|
||
POST /api/meeting/{taskId}/transcribe (multipart/form-data)
|
||
↓
|
||
MeetingController.Transcribe()
|
||
↓ 保存音频临时文件到 meetings/ 目录
|
||
↓ 调用 ISttService.TranscribeAsync()
|
||
↓ 得到文字结果
|
||
↓ 删除临时音频文件
|
||
↓ 调用 MeetingService.SaveTranscript() 保存到数据库
|
||
↓
|
||
返回 TranscribeResponse { taskId, transcript, audioDuration }
|
||
```
|
||
|
||
### 4.6 错误处理
|
||
|
||
| 场景 | 处理 |
|
||
|---|---|
|
||
| 音频格式不支持 | 返回 400 "不支持的音频格式,支持 webm/wav/mp3" |
|
||
| 音频文件太大 | 返回 400 "音频文件过大,请控制录音在 2 小时以内" |
|
||
| STT 服务不可用 | 返回 503 "转写服务暂不可用,请稍后重试" |
|
||
| taskId 不是会议类型 | 返回 400 "该待办项不是会议类型" |
|
||
|
||
## 五、验收标准
|
||
|
||
| 验收项 | 验证方法 | 预期结果 |
|
||
|---|---|---|
|
||
| 录音按钮可用 | 点击录音按钮 | 浏览器弹出麦克风权限请求 |
|
||
| 录制过程 | 授权后开始录制 | 显示录音时长,波形条有变化 |
|
||
| 停止录音 | 点击停止按钮 | 时长停止,显示"提交转写"按钮 |
|
||
| 重新录制 | 点击"重新录制" | 状态重置,可再次录制 |
|
||
| 上传转写 | 提交录音文件 | 返回转写文字 |
|
||
| 纪要保存 | 转写完成后查看任务 | `meetingNotes` 字段有转写文字 |
|
||
| 权限拒绝 | 浏览器拒绝麦克风 | 提示"无法访问麦克风,请使用文字输入" |
|
||
| 浏览器不支持 | IE/Safari 旧版等 | 提示"当前浏览器不支持录音,请使用文字输入" |
|
||
|
||
## 六、风险
|
||
|
||
| 风险 | 影响 | 应对 |
|
||
|---|---|---|
|
||
| Safari 不支持 webm | 无法录制 | 使用 mp4 格式(Safari 支持);MIME type 自动适配 |
|
||
| STT 准确率不足 | 转写错误多 | 支持用户编辑修正转写结果 |
|
||
| 长音频转写耗时长 | 用户等待 | 前端显示转写进度或"转写中"加载状态 |
|
||
|
||
## 七、Touch List
|
||
|
||
| 文件路径 | 修改类型 |
|
||
|---|---|
|
||
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 |
|
||
| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 |
|
||
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 |
|
||
| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 |
|
||
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
|
||
|
||
---
|
||
|
||
**工单编号**:03-02
|
||
**标题**:音频录制与转写
|
||
**版本**:v1.3.0
|
||
**创建日期**:2026-06-16
|