便笺: Fun-ASR 本地部署与批量推理

发布于 2026-08-10 20:12 更新于 2026-08-10 22:34 1366 字 7 min read

kissablecho avatar

kissablecho

kissablecho 的个人博客 / 记录生活,分享技术 / 喜欢二次元和白丝。

提供 Fun-ASR-Nano-2512-GGUF 本地部署与批量推理教程,涵盖 Conda 环境搭建、依赖安装及 Python 批量转录脚本。实现离线、高速的多格式音频(MP3/WAV/M4A)转文字并生成 SRT 字幕,是替代云端 API 的最佳免费方案。

前言

提供 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())

喜欢的话,留下你的评论吧~