Skip to main content

自建语音转文字播放器:从 Whisper 到可点击跳转的逐字稿

本文基于项目 2026voice-to-text 整理,可直接作为技术博客草稿使用。

语音转文字播放器界面:左侧录音列表,右侧逐字稿与播放控制


一、项目背景

很多场景里,录音不只是「听」,还需要对照文字快速定位——法律取证、会议纪要、采访整理、客服质检都是如此。本项目最初的需求来自物业纠纷现场录音:一段长达一小时的 m4a,需要在特定时间点找到某句话,反复核对细节。

传统做法是:Whisper 转写 → 得到纯文本 → 手动对照播放器拖进度条。效率很低。

这个项目的目标是:上传录音 → 本地 Whisper 转写 → 在浏览器里「点文字即跳转播放」,并支持多版本转写、URL 直达某条录音,形成一套轻量、可自托管的语音证据审阅工具。


二、整体架构

系统采用经典的前后端分离结构:

设计原则:

  • 转写与 Web 解耦:核心逻辑在 transcribe_lib.py,CLI 和 API 共用同一套实现。
  • 本地优先:音频、文稿、数据库都在 server/data/,无需云存储。
  • 显式触发转写:上传只存文件,用户点击「开始转写」才跑 Whisper,避免误触消耗算力。

三、技术栈

层级技术说明
语音识别faster-whisperCTranslate2 加速的 Whisper,CPU/GPU 自适应
后端FastAPI + UvicornREST 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_HOTWORDSWHISPER_VAD_THRESHOLD 等微调。

6. 超时降级策略

服务端转写任务若在大模型上超时(默认 90s),会自动 fallback 到 WHISPER_FALLBACK_MODEL(默认 medium),避免长时间卡死。


五、数据模型

recordings — 录音主记录:

字段含义
idUUID hex
title文件名 stem
statusuploaded → pending → processing → ready / error
audio_path相对路径
whisper_model使用的模型

transcription_runs — 转写版本:

字段含义
version1, 2, 3…
statuspending / processing / ready / error
transcript_pathtranscripts/{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. 侧边栏与主区域独立滚动

RecordingSidebarTranscriptPlayer 各自有 scroll 容器;列表用 scrollIntoView({ block: 'nearest' }) 跟随选中项,不受右侧长文稿滚动影响。


十、性能与资源

模型大致内存1 小时中文音频(CPU)
base~1 GB较快,精度一般
medium~2–3 GB推荐平衡点
large-v3~5 GB+最准,很慢

首次运行会从 Hugging Face 下载模型权重,需联网。之后离线可用。


十一、已知限制与路线图

当前 TODO:

  1. URL 含录音 ID 的 deep link 已基本实现(recordingUrl.ts),可继续完善版本号参数
  2. 侧边栏滚动隔离已做,可进一步优化虚拟列表(录音很多时)

未做 / 非目标:

  • 同一录音并发转写
  • 版本间 diff 对比
  • 在线编辑文稿
  • 用户认证与多租户

十二、总结

这个项目用 约 700 行 Python + 400 行 React 搭出了一套实用的「语音证据播放器」:

  • faster-whisper 提供本地、隐私友好的转写
  • 分段 JSON + 点击 seek 解决「听 + 查」的核心痛点
  • 多版本转写 + SQLite 让重复实验有迹可循
  • FastAPI 后台任务 + 前端轮询 架构简单,单人维护成本低

如果你也在处理长录音、需要反复定位某句话,这套方案比直接用 Whisper WebUI 或云 API 更贴合「审阅」工作流——上传、转写、点读,三步闭环。


附录:环境变量速查

变量默认值说明
WHISPER_LANGUAGE空(自动)zh 强制中文
WHISPER_MODELmedium转写模型
WHISPER_FALLBACK_MODELmedium超时降级模型
WHISPER_FALLBACK_TIMEOUT_SECONDS90首次尝试超时
WHISPER_BEAM_SIZE8解码 beam size
WHISPER_VAD_FILTERtrue静音过滤
WHISPER_INITIAL_PROMPT中文模板解码偏置
CORS_ORIGINSlocalhost:5173跨域来源

如果你希望,我可以把这篇文档保存成项目里的 docs/blog.md,或按某个平台(掘金 / 知乎 / 公众号)的格式再改一版标题和摘要。