前言
提供 Fun-ASR-Nano-2512-GGUF 本地部署与批量推理教程,涵盖 Conda 环境搭建、依赖安装及 Python 批量转录脚本。实现离线、高速的多格式音频(MP3/WAV/M4A)转文字并生成 SRT 字幕,是替代云端 API 的最佳免费方案。
本文使用的是 HaujetZhao/Fun-ASR-Nano-2512-GGUF 模型。原因,最简单,最快速。
下载仓库
HaujetZhao/Fun-ASR-Nano-2512-GGUF
modelscope download --model HaujetZhao/Fun-ASR-Nano-2512-GGUF --local_dir ./Fun-ASR-Nano-2512-GGUF
cd ./Fun-ASR-Nano-2512-GGUF
创建虚拟环境
conda 环境
conda create -n fun-asr-gguf python=3.12
conda activate fun-asr-gguf
venv 环境
python -m venv fun-asr-gguf
source fun-asr-gguf/bin/activate
安装依赖
注释掉 requirements.txt 中的 模型导出依赖,因为我们只需要运行模型,不需要导出模型。
pip install -r requirements.txt
网络差时,可以使用镜像源安装依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
运行 推理脚本
python 03-Inference.py
进阶版,批量推理
"""
批量语音识别脚本 - 对指定文件夹中的音频文件进行批量识别
使用方式:
python batch_asr.py
所有配置在下方 ================= 配置区域 ================= 中修改。
"""
"""
该脚本局限性:
1. 仅支持单线程批量处理,无法多文件并发处理。强行使用多线程会在第二步报错 ✗ 初始化失败: [ONNXRuntimeError] : 3 : NO_SUCHFILE : Load model from ./model/Fun-ASR-Nano-Encoder-Adaptor.fp32.onnx failed:Load model ./model/Fun-ASR-Nano-Encoder-Adaptor.fp32.onnx failed. File doesn't exist.
2. 难处理 AI “复读机” 现象。语音识别由 fun_asr_gguf 处理,较长的音频将会分批处理后合并返回结果。该脚本只能获取合并后的结果,较难检测 “重复文本”。也不能重试某个音频片段(只能整个重试)。未来想要处理 AI “复读机”,只能在 fun_asr_gguf 中添加相关逻辑。
"""
import os
import sys
import time
import dataclasses
from fun_asr_gguf import create_asr_engine
# ==================== Vulkan 选项 ====================
# os.environ["VK_ICD_FILENAMES"] = "none" # 禁止 Vulkan
# os.environ["GGML_VK_VISIBLE_DEVICES"] = "0" # 禁止 Vulkan 用独显(强制用集显)
# os.environ["GGML_VK_DISABLE_F16"] = "1" # 禁止 VulkanFP16 计算(Intel集显fp16有溢出问题)
# ==================== 输入 / 输出 配置 ====================
input_dir = "./input" # 音频文件输入文件夹
output_dir = "./output" # 识别结果输出文件夹
# 支持的音频格式(小写,含点号)
audio_extensions = [".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".wma", ".opus", ".webm"]
# ==================== 模型路径配置 ====================
model_dir = "./model"
encoder_onnx_path = f"{model_dir}/Fun-ASR-Nano-Encoder-Adaptor.fp32.onnx"
ctc_onnx_path = f"{model_dir}/Fun-ASR-Nano-CTC.int8.onnx"
decoder_gguf_path = f"{model_dir}/Fun-ASR-Nano-Decoder.q8_0.gguf"
tokens_path = f"{model_dir}/tokens.txt"
# ==================== 热词配置 ====================
hotwords_path = "" # 热词文件路径,留空字符串 "" 则不使用
similar_threshold = 0.6 # 热词模糊匹配阈值
max_hotwords = 10 # 最多提供给 LLM 的热词数量
# ==================== 识别 / 转录配置 ====================
language = None # 语言 (None=自动检测, "中文", "英文", "日文" 等)
context = "" # 上下文信息,留空则不使用
enable_ctc = True # 是否启用 CTC 辅助(提供时间戳和热词)
segment_size = 60.0 # 长音频分段大小(秒)
overlap = 4.0 # 分段之间的重叠(秒)
start_second = 0.0 # 开始时间(秒),None 表示从头开始
duration = None # 转录时长(秒),None 表示到音频结尾
srt = True # 是否同时生成 SRT 字幕文件
verbose = True # 是否打印详细信息
json_output = False # 是否同时输出 JSON 格式结果
# ==================== 语言说明 ====================
"""
Fun-ASR-Nano-2512
中文、英文、日文
Fun-ASR-MLT-Nano-2512
中文、英文、粤语、日文、韩文、越南语、印尼语、泰语、马来语、菲律宾语、阿拉伯语、
印地语、保加利亚语、克罗地亚语、捷克语、丹麦语、荷兰语、爱沙尼亚语、芬兰语、希腊语、
匈牙利语、爱尔兰语、拉脱维亚语、立陶宛语、马耳他语、波兰语、葡萄牙语、罗马尼亚语、
斯洛伐克语、斯洛文尼亚语、瑞典语
"""
# ==================== 执行区域 ====================
def collect_audio_files(root_dir, extensions):
"""递归收集指定目录下所有匹配扩展名的音频文件"""
files = []
if not os.path.isdir(root_dir):
print(f"[错误] 输入目录不存在: {root_dir}")
return files
for dirpath, _, filenames in os.walk(root_dir):
for fname in filenames:
ext = os.path.splitext(fname)[1].lower()
if ext in extensions:
files.append(os.path.join(dirpath, fname))
return sorted(files)
def ensure_output_path(input_audio_path, input_root, output_root):
"""
根据输入音频路径计算输出路径(保持目录结构)。
例如: input_root=./input, output_root=./output
input_audio_path=./input/meeting/rec1.mp3
-> ./output/meeting/rec1.txt
"""
rel = os.path.relpath(input_audio_path, input_root)
name_without_ext = os.path.splitext(rel)[0]
output_path = os.path.join(output_root, name_without_ext + ".txt")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
return output_path
def main():
print("=" * 70)
print("批量语音识别")
print("=" * 70)
# ---------- 收集音频文件 ----------
audio_files = collect_audio_files(input_dir, audio_extensions)
if not audio_files:
print(f"\n[提示] 在 {input_dir!r} 中没有找到匹配的音频文件。")
print(f" 支持的格式: {audio_extensions}")
return 1
print(f"\n共找到 {len(audio_files)} 个音频文件")
# ---------- 处理 hotwords_path ----------
hw_path = hotwords_path if hotwords_path.strip() else None
# ---------- 创建 ASR 引擎 ----------
print("\n初始化 ASR 引擎...")
engine = create_asr_engine(
encoder_onnx_path=encoder_onnx_path,
ctc_onnx_path=ctc_onnx_path,
decoder_gguf_path=decoder_gguf_path,
tokens_path=tokens_path,
hotwords_path=hw_path,
similar_threshold=similar_threshold,
max_hotwords=max_hotwords,
enable_ctc=enable_ctc,
verbose=verbose,
)
# ---------- 预跑一次(分配内存) ----------
print(f'\n预跑一遍,分配内存...')
engine.transcribe(
audio_files[0],
language=language,
context=context,
verbose=False,
duration=10.0,
)
# ---------- 批量识别 ----------
total = len(audio_files)
success_count = 0
fail_list = []
for idx, audio_path in enumerate(audio_files, 1):
print(f"\n{'=' * 70}")
print(f"[{idx}/{total}] 处理: {audio_path}")
print(f"{'=' * 70}")
output_txt = ensure_output_path(audio_path, input_dir, output_dir)
try:
result = engine.transcribe(
audio_path,
language=language,
context=context,
verbose=verbose,
segment_size=segment_size,
overlap=overlap,
start_second=start_second,
duration=duration,
srt=srt,
)
# 写入 txt 文件
with open(output_txt, "w", encoding="utf-8") as f:
f.write(result.text)
# 可选 JSON 输出
if json_output:
import json
json_path = os.path.splitext(output_txt)[0] + ".json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(dataclasses.asdict(result), f, ensure_ascii=False, indent=2)
# 如果开启了 srt 且未在同目录生成,将 srt 文件移到输出目录
if srt:
src_srt = os.path.splitext(audio_path)[0] + ".srt"
dst_srt = os.path.splitext(output_txt)[0] + ".srt"
if os.path.isfile(src_srt) and os.path.abspath(src_srt) != os.path.abspath(dst_srt):
try:
os.replace(src_srt, dst_srt)
except OSError:
pass
success_count += 1
print(f" -> 输出: {output_txt}")
except Exception as e:
fail_list.append((audio_path, str(e)))
print(f" [失败] {e}")
# ---------- 汇总 ----------
print(f"\n{'=' * 70}")
print(f"批量处理完成")
print(f"{'=' * 70}")
print(f" 总计: {total} 成功: {success_count} 失败: {len(fail_list)}")
if fail_list:
print(f"\n失败列表:")
for path, err in fail_list:
print(f" - {path}")
print(f" 错误: {err}")
# ---------- 清理 ----------
engine.cleanup()
return 0 if not fail_list else 1
if __name__ == "__main__":
exit(main())
喜欢的话,留下你的评论吧~