自建语音转文字播放器:从 Whisper 到可点击跳转的逐字稿
本文基于项目
2026voice-to-text整理,可直接作为技术博客草稿使用。

一、项目背景
很多场景里,录音不只是「听」,还需要对照文字快速定位——法律取证、会议纪要、采访整理、客服质检都是如此。本项目最初的需求来自物业纠纷现场录音:一段长达一小时的 m4a,需要在特定时间点找到某句话,反复核对细节。
传统做法是:Whisper 转写 → 得到纯文本 → 手动对照播放器拖进度条。效率很低。
这个项目的目标是:上传录音 → 本地 Whisper 转写 → 在浏览器里「点文字即跳转播放」,并支持多版本转写、URL 直达某条录音,形成一套轻量、可自托管的语音证据审阅工具。
二、整体架构
系统采用经典的前后端分离结构:
设计原则:
- 转写与 Web 解耦:核心逻辑在
transcribe_lib.py,CLI 和 API 共用同一套实现。 - 本地优先:音频、文稿、数据库都在
server/data/,无需云存储。 - 显式触发转写:上传只存文件,用户点击「开始转写」才跑 Whisper,避免误触消耗算力。
三、技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 语音识别 | faster-whisper | CTranslate2 加速的 Whisper,CPU/GPU 自适应 |
| 后端 | FastAPI + Uvicorn | REST API、文件上传、后台任务 |
| 存储 | SQLite + 本地文件 | recordings.db + uploads/ + transcripts/ |
| 前端 | React 19 + TypeScript + Vite 8 | 无 UI 框架,纯 CSS |
| 包管理 | pnpm (前端) / pip (后端) |
四、核心功能
1. 分段转写 JSON
Whisper 输出被整理为带时间戳的分段结构:
{
"audioFile": "20251221_134557.m4a",
"language": "zh",
"duration": 3721.5,
"segments": [
{ "start": 0.0, "end": 3.2, "text": "你好,看下材料。" },
{ "start": 3.2, "end": 8.1, "text": "你不是昨天过来了吗?" }
]
}
每个 segment 的 start / end 是播放器 seek 的基础。
2. 点击文稿跳转播放
TranscriptPlayer 的核心交互:
- 点击某段文字 →
audio.currentTime = seg.start→ 播放到seg.end自动暂停 - 播放过程中
timeupdate事件驱动当前段高亮,并scrollIntoView保持可见 - 底部 dock 提供 ±1 秒微调按钮,配合原生
<audio controls>使用
这是「证据审阅」场景下最高频的操作,比拖进度条精确一个数量级。
3. 多版本转写(v1, v2, …)
每次点击「开始转写」会创建新的 transcription_runs 记录:
- 版本号单调递增,失败也占号(便于审计)
- UI 下拉选择版本,默认展示最新 ready 版本
- 服务端记录每次运行的
duration_ms(墙钟耗时)
适用场景:换模型重跑、调参对比、修正识别错误后再转一次。
4. URL 直达录音
选中录音后,浏览器地址栏变为 /{recording_id}(32 位 hex UUID)。刷新页面或通过链接分享,可自动定位到该录音——适合把某条证据发给同事复核。
5. 中文识别优化
transcribe_lib.py 针对中文做了几项实用配置:
# 环境变量 WHISPER_LANGUAGE=zh 强制中文
# initial_prompt 引导口语风格
"这是一段中文普通话口语录音,可能包含日常用语、数字与专有名词。"
# VAD 过滤静音(Silero),beam_size=8
# CPU 使用 int8_float32,CUDA 使用 float16
还支持 WHISPER_HOTWORDS、WHISPER_VAD_THRESHOLD 等微调。
6. 超时降级策略
服务端转写任务若在大模型上超时(默认 90s),会自动 fallback 到 WHISPER_FALLBACK_MODEL(默认 medium),避免长时间卡死。
五、数据模型
recordings 表 — 录音主记录:
| 字段 | 含义 |
|---|---|
| id | UUID hex |
| title | 文件名 stem |
| status | uploaded → pending → processing → ready / error |
| audio_path | 相对路径 |
| whisper_model | 使用的模型 |
transcription_runs 表 — 转写版本:
| 字段 | 含义 |
|---|---|
| version | 1, 2, 3… |
| status | pending / processing / ready / error |
| transcript_path | transcripts/{id}_v{n}.json |
| duration_ms | 本次转写耗时 |
录音的 status 由 _sync_recording_from_runs() 从最新 run 同步,列表页无需额外查询。
六、API 一览
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/recordings | 录音列表 |
| POST | /api/recordings | 上传音频(最大 500MB) |
| POST | /api/recordings/{id}/transcribe | 启动转写(409 防并发) |
| GET | /api/recordings/{id}/transcription-runs | 版本列表 |
| GET | /api/recordings/{id}/transcript?version=N | 获取文稿 JSON |
| GET | /api/recordings/{id}/audio | 流式返回音频 |
| DELETE | /api/recordings/{id} | 删除录音及所有版本 |
前端通过 Vite dev server 把 /api 代理到 127.0.0.1:8000。
七、前端状态与轮询
App.tsx 在选中录音后每 2 秒轮询详情,直到没有 pending / processing 状态的 run:
用户上传 → status: uploaded
点击转写 → 创建 run vN (pending) → 后台线程 processing
→ 前端轮询 → ready → 加载 transcript JSON → 渲染播放器
转写进行中显示已等待时长(分:秒),首次运行会提示「会下载模型」。
八、两种使用方式
方式 A:完整 Web 应用(推荐)
# 1. 安装依赖
pip install -r requirements.txt
cd client-app && pnpm install
# 2. 启动后端(中文 + medium 模型)
WHISPER_LANGUAGE=zh WHISPER_MODEL=medium \
python -m uvicorn server.main:app --reload --host 127.0.0.1 --port 8000
# 3. 启动前端
cd client-app && pnpm dev # http://127.0.0.1:3010
浏览器打开后上传 m4a/mp3/wav 等,点击「开始转写」即可。
方式 B:纯 CLI + 静态预览
适合单次转写、不需要服务端:
python transcribe.py your.m4a -o client-app/public/your_transcript.json
cp your.m4a client-app/public/
cd client-app && pnpm dev
早期版本走这条路径;现在已演进为 API 驱动的完整应用。
九、实现亮点(可写进博客的「干货」)
1. 段内播放的边界控制
用 segmentPlayEndRef 记录当前段的 end,在 timeupdate 里检测 currentTime >= bound 则 pause,实现「点哪段听哪段」而不需要监听 ended 事件(整段音频结束才触发)。
2. 转写任务串行化
每个 recording_id 有独立 threading.Lock,加上 API 层 409 检查,保证同一录音不会并发跑两个 Whisper 进程——Whisper 本身就很吃 CPU/GPU。
3. subprocess 隔离
转写在子进程跑 transcribe.py,而不是在 FastAPI 进程内 import WhisperModel。好处:
- 避免模型加载污染 Web 进程内存
- 超时可直接 kill 子进程
- CLI 与 API 行为一致
4. 侧边栏与主区域独立滚动
RecordingSidebar 和 TranscriptPlayer 各自有 scroll 容器;列表用 scrollIntoView({ block: 'nearest' }) 跟随选中项,不受右侧长文稿滚动影响。
十、性能与资源
| 模型 | 大致内存 | 1 小时中文音频(CPU) |
|---|---|---|
| base | ~1 GB | 较快,精度一般 |
| medium | ~2–3 GB | 推荐平衡点 |
| large-v3 | ~5 GB+ | 最准,很慢 |
首次运行会从 Hugging Face 下载模型权重,需联网。之后离线可用。
十一、已知限制与路线图
当前 TODO:
- URL 含录音 ID 的 deep link 已基本实现(
recordingUrl.ts),可继续完善版本号参数 - 侧边栏滚动隔离已做,可进一步优化虚拟列表(录音很多时)
未做 / 非目标:
- 同一录音并发转写
- 版本间 diff 对比
- 在线编辑文稿
- 用户认证与多租户
十二、总结
这个项目用 约 700 行 Python + 400 行 React 搭出了一套实用的「语音证据播放器」:
- faster-whisper 提供本地、隐私友好的转写
- 分段 JSON + 点击 seek 解决「听 + 查」的核心痛点
- 多版本转写 + SQLite 让重复实验有迹可循
- FastAPI 后台任务 + 前端轮询 架构简单,单人维护成本低
如果你也在处理长录音、需要反复定位某句话,这套方案比直接用 Whisper WebUI 或云 API 更贴合「审阅」工作流——上传、转写、点读,三步闭环。
附录:环境变量速查
| 变量 | 默认值 | 说明 |
|---|---|---|
WHISPER_LANGUAGE | 空(自动) | 如 zh 强制中文 |
WHISPER_MODEL | medium | 转写模型 |
WHISPER_FALLBACK_MODEL | medium | 超时降级模型 |
WHISPER_FALLBACK_TIMEOUT_SECONDS | 90 | 首次尝试超时 |
WHISPER_BEAM_SIZE | 8 | 解码 beam size |
WHISPER_VAD_FILTER | true | 静音过滤 |
WHISPER_INITIAL_PROMPT | 中文模板 | 解码偏置 |
CORS_ORIGINS | localhost:5173 | 跨域来源 |
如果你希望,我可以把这篇文档保存成项目里的 docs/blog.md,或按某个平台(掘金 / 知乎 / 公众号)的格式再改一版标题和摘要。