文章目录[隐藏]
test 语音转文本完整方案
一、方案概述
将 test 录课视频(MKV 格式)批量转录为带标点的中文文本。
- 输入:
U:\test\下 119 个 MKV 文件(共 93GB,约 83 小时音频) - 输出:
F:\test_transcripts\下 119 个同名 .txt 文件(共 115 万字) - 总耗时:约 80 分钟(含音频提取 + ASR + 标点恢复)
- 环境:Windows 10 + WSL Ubuntu 24.04 + RTX 4070 Ti SUPER 16GB
二、技术选型
模型对比(实测数据)
| 模型 | 中文精度 | GPU 实时率 | VRAM 占用 | 特点 |
|---|---|---|---|---|
| SenseVoice-Small ⭐ | ⭐⭐⭐⭐ | 51.8x | ~2GB | 最快,情绪/事件检测 |
| Paraformer-Large | ⭐⭐⭐⭐⭐ | 33.5x | ~3GB | 更准,内置标点,支持说话人分离 |
| Fun-ASR-Nano | ⭐⭐⭐⭐⭐ | 17x | ~4GB | LLM 级精度(需 transformers 兼容修复) |
最终选择:SenseVoice-Small + ct-punc
理由:
- 速度最快(51.8x 实时),83 小时音频纯 ASR 只需 ~12 分钟
- 中文识别准确率在 AISHELL-1/2 上超越 Whisper
- 标点恢复用 ct-punc 模型后处理(几乎不耗时)
- 说话人分离可选(加 cam++ 模型,但大部分单人课程不需要)
三、环境搭建
3.1 前置条件
Windows 10/11 + WSL2 Ubuntu 24.04
NVIDIA GPU(CUDA 12.x)
ffmpeg(Windows 或 WSL 内安装均可)3.2 WSL 内 Python 环境
# 创建虚拟环境
python3 -m venv ~/funasr_env
source ~/funasr_env/bin/activate
# 安装 PyTorch(根据 CUDA 版本选择)
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu124
# 安装 FunASR
pip install funasr -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
# 安装 ffmpeg(WSL 内)
sudo apt-get install -y ffmpeg3.3 验证安装
source ~/funasr_env/bin/activate
python3 -c "
import torch
print(f'PyTorch {torch.__version__}, CUDA: {torch.cuda.is_available()}')
import funasr
print(f'FunASR {funasr.__version__}')
"四、完整流程
步骤 1:挂载 Windows 盘
# WSL 内挂载网络盘/本地盘
sudo mkdir -p /mnt/u /mnt/f
sudo mount -t drvfs 'U:' /mnt/u # test 源文件
sudo mount -t drvfs 'F:' /mnt/f # 输出目录步骤 2:批量提取音频(ffmpeg 并行)
#!/bin/bash
# extract_all.sh — 从 MKV 批量提取 16kHz 单声道 WAV
OUTDIR="/mnt/f/test_audio"
mkdir -p "$OUTDIR"
# 生成文件列表
find /mnt/u/test -name '*.mkv' -type f | sort > /tmp/mkv_list.txt
TOTAL=$(wc -l < /tmp/mkv_list.txt)
echo "共 $TOTAL 个文件待提取"
# 单文件提取(自动跳过已存在的)
extract_one() {
local src="$1"
local rel="${src#/mnt/u/test/}"
local dst="$OUTDIR/${rel%.mkv}.wav"
mkdir -p "$(dirname "$dst")"
[ -f "$dst" ] && echo "SKIP: $rel" && return 0
ffmpeg -y -loglevel error -i "$src" -vn -acodec pcm_s16le -ar 16000 -ac 1 "$dst"
[ $? -eq 0 ] && echo "OK: $rel → $(du -h "$dst" | cut -f1)" || echo "FAIL: $rel"
}
export -f extract_one
export OUTDIR
# 8 并发提取(网络盘建议 4-8,本地盘可更高)
cat /tmp/mkv_list.txt | xargs -P 8 -I {} bash -c 'extract_one "$@"' _ {}说明:
- 输出格式:16kHz、单声道、16bit PCM WAV(ASR 标准格式)
- 8 并发时 119 个文件约 18 分钟完成
- 已存在的文件自动跳过(可断点续传)
步骤 3:批量 ASR 转录
#!/usr/bin/env python3
# batch_asr.py — SenseVoice-Small 批量转录(单进程安全版)
import time, os, sys, glob, subprocess, json
os.environ["MODELSCOPE_CACHE"] = "/home/tmzn/modelscope_cache"
AUDIO_DIR = "/mnt/f/test_audio"
OUTPUT_DIR = "/mnt/f/test_transcripts"
os.makedirs(OUTPUT_DIR, exist_ok=True)
wav_files = sorted(glob.glob(os.path.join(AUDIO_DIR, "**/*.wav"), recursive=True))
print(f"共 {len(wav_files)} 个文件待转录")
# 模型只加载一次
from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess
model = AutoModel(
model="iic/SenseVoiceSmall",
vad_model="fsmn-vad",
vad_kwargs={"max_single_segment_time": 30000},
device="cuda:0",
disable_update=True,
)
print("✅ 模型加载完成")
for i, wav_path in enumerate(wav_files):
rel = os.path.relpath(wav_path, AUDIO_DIR)
out_name = os.path.splitext(rel)[0]
out_txt = os.path.join(OUTPUT_DIR, out_name + ".txt")
os.makedirs(os.path.dirname(out_txt), exist_ok=True)
if os.path.exists(out_txt):
print(f"[{i+1}/{len(wav_files)}] SKIP: {rel}")
continue
r = subprocess.run(["ffprobe", "-v", "quiet", "-show_entries",
"format=duration", "-of", "csv=p=0", wav_path],
capture_output=True, text=True)
try:
audio_sec = float(r.stdout.strip())
except:
audio_sec = 0
t0 = time.time()
res = model.generate(
input=wav_path, cache={}, language="zh",
use_itn=True, batch_size_s=300,
merge_vad=True, merge_length_s=15,
)
raw_text = res[0]["text"]
rich_text = rich_transcription_postprocess(raw_text)
elapsed = time.time() - t0
with open(out_txt, "w", encoding="utf-8") as f:
f.write(rich_text)
speed = audio_sec / elapsed if elapsed > 0 else 0
print(f"[{i+1}/{len(wav_files)}] ✅ {rel} → {elapsed:.1f}s ({speed:.0f}x) {len(rich_text)}字")关键参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
model | iic/SenseVoiceSmall | 最快的中文 ASR 模型 |
vad_model | fsmn-vad | 语音活动检测,自动切分静音段 |
max_single_segment_time | 30000 | VAD 最大切片 30 秒 |
language | "zh" | 强制中文(比 auto 更准) |
use_itn | True | 逆文本归一化(数字、日期等) |
batch_size_s | 300 | 每批处理 300 秒音频 |
merge_vad | True | 合并相邻 VAD 片段 |
merge_length_s | 15 | 合并后最小 15 秒 |
五、并行策略(重要教训)
5.1 并行数量与 VRAM 的关系
GPU: RTX 4070 Ti SUPER 16GB
Windows 显示层占用: ~1-2GB(重启后)/ ~9.4GB(长时间使用后)
实际可用: 14-15GB(刚重启)/ ~7GB(长时间使用后)| 并行数 | VRAM 占用 | 稳定性 | 建议 |
|---|---|---|---|
| 1 路 | ~2GB | ✅ 绝对稳定 | 保守选择 |
| 2 路 | ~4-6GB | ✅ 稳定 | 推荐 |
| 3 路 | ~6-12GB | ⚠️ 大文件会爆 | 不推荐 |
| 4 路+ | ~8-16GB | ❌ 很容易崩 | 禁止 |
5.2 为什么 3 路会崩
每个 Worker:
模型本身: ~2GB(固定)
VAD 中间张量: ~1-4GB(随音频长度波动)
─────────────────
单 Worker 峰值: ~3-6GB
3 路并行峰值: 3 × 6GB = 18GB > 16GB → CUDA OOM大文件尤其危险:170 分钟的音频,VAD 产生的中间张量远大于 10 分钟的短音频。
5.3 推荐方案:2 路并行 + 大文件单独处理
# 2 路并行版本(推荐)
NUM_WORKERS = 2
chunks = [[] for _ in range(NUM_WORKERS)]
for i, f in enumerate(todo):
chunks[i % NUM_WORKERS].append(f)或者更稳妥:1 路 + 跳过标点后处理,先跑完 ASR,再批量标点恢复。
5.4 CUDA 崩溃后的补救
如果某些文件因 CUDA 错误失败,用 CPU 模式补完:
# 补完脚本只改一行
model = AutoModel(
model="iic/SenseVoiceSmall",
vad_model="fsmn-vad",
device="cpu", # ← 改成 CPU
disable_update=True,
)CPU 模式速度约 17x realtime(170 分钟音频约 10 分钟),但绝对稳定。
六、说话人分离(可选)
如果音频有多人对话,加上 cam++ 模型:
model = AutoModel(
model="iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
vad_model="fsmn-vad",
punc_model="ct-punc",
spk_model="cam++", # ← 加这个
device="cuda:0",
disable_update=True,
)
res = model.generate(
input=audio_file,
batch_size_s=300,
merge_vad=True,
merge_length_s=15,
# preset_spk_num=2, # ⚠️ 已知会卡住,不要用
)
# 输出格式: sentence_info 中包含 spk 字段
for item in res:
for sent in item.get("sentence_info", []):
print(f"Speaker {sent['spk']}: {sent['text']}")注意事项:
- cam++ 会多占 ~1GB VRAM
- 说话人数量是自动聚类的,不准确时需要后处理合并
- ⚠️
preset_spk_num参数已知会导致卡死,不要使用 - 单人课程不需要加 cam++
七、文件结构
F:\
├── test_audio\ # 中间文件(提取的 WAV,可删除)
│
└── test_transcripts\ # 最终输出 ⭐
├── _raw\ # 原始 ASR(无标点,可删除)
└── asr_summary.json # 统计摘要八、性能基准(实测数据)
音频提取
| 并发数 | 119 文件耗时 | 说明 |
|---|---|---|
| 4 并发 | ~12 分钟 | 网络盘安全速度 |
| 8 并发 | ~8 分钟 | 网络盘极限 |
ASR 转录
| 方案 | 83 小时音频耗时 | 实时率 |
|---|---|---|
| 单进程 GPU | ~96 分钟 | 52x |
| 2 路并行 GPU | ~20 分钟 | ~250x |
| 3 路并行 GPU | ~12 分钟 | ~415x(但大文件会崩) |
| CPU 单进程 | ~50 小时 | 17x |
标点恢复(ct-punc 后处理)
纯文本处理,119 个文件 < 1 秒完成。
九、快速复用命令
# 一键环境检查
source ~/funasr_env/bin/activate && python3 -c "import torch,funasr; print('OK')"
# 一键提取音频(替换路径即可)
bash ~/extract_all.sh
# 一键批量转录
source ~/funasr_env/bin/activate && python3 -u ~/batch_asr.py 2>&1 | tee ~/batch_asr.log
# 一键标点恢复
source ~/funasr_env/bin/activate && python3 -u ~/batch_punc.py 2>&1 | tee ~/batch_punc.log
# 查看进度
tail -f ~/batch_asr.log
# 查看 GPU 状态
nvidia-smi十、已知问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| CUDA OOM(大文件) | VAD 中间张量 + 多进程 VRAM 累积 | 降并行数到 2 或 1;大文件用 CPU |
| Windows 显示层占 9.4GB VRAM | 长时间使用后 DWM 内存泄漏 | 重启机器释放 VRAM |
preset_spk_num 参数卡死 | FunASR 已知 bug | 不使用该参数;后处理合并说话人 |
| Fun-ASR-Nano 加载失败 | transformers 缺少 Qwen3 支持 | 等 FunASR 更新;或手动升级 transformers |
| WSL 挂载网络盘超时 | 网络盘响应慢 | 用 mount -t drvfs 加 timeout |
| 说话人分离不准(5→2 人) | cam++ 自动聚类过度 | 后处理基于时间邻近性合并 |
十一、依赖版本(已验证可用)
Python: 3.12.3
PyTorch: 2.6.0+cu124
FunASR: 1.3.9
CUDA: 12.4 (WSL)
GPU Driver: 595.71
OS: Windows 10 + WSL2 Ubuntu 24.04