热词驱动,智辨声纹——从ASR热词到说话人日志的尝试:海光DCU环境部署FunASR热词语音识别系统(说话人日志已集成,含完整代码)

一、引言

语音识别技术正在经历从"语音转文字"到"语音理解"的深刻演进。在实际业务场景中,仅仅把音频转成文字往往是不够的------会议记录需要区分"谁在何时说了什么",智能客服需要精准识别专业术语和人名地名,影视剧分析需要对话角色标注。这些需求催生了两个关键技术方向:热词定制(Hotword Boosting) 和说话人日志(Speaker Diarization) 。

热词定制的重要性怎么强调都不为过。在通用语音识别场景中,模型对"人工智能"这类常见词的识别准确率很高,但一旦遇到"张謇""羰基""通义实验室"等生僻人名、专业术语或品牌名称,识别结果往往惨不忍睹。热词机制允许用户提前指定一批关键词,让模型在解码时对这些词进行"偏置"------当音频波形同时匹配"人工智能"和"人公智能"时,热词会让前者胜出,而且这个过程无需重新训练模型,零代码、秒级生效。

说话人日志则是另一项刚需能力。FunASR作为阿里巴巴通义实验室开源的工业级语音识别工具包,集成了语音端点检测(VAD)、语音识别、标点恢复、说话人验证、说话人日志等多项核心功能。其中Fun-ASR-Nano模型不仅支持7大方言和26种地域口音,还原生支持说话人日志与热词定制。

值得注意的是,说话人日志场景下的ASR识别精度通常比单说话人ASR要略差------多说话人重叠、声道干扰、说话人切换处的边界模糊等因素都会影响识别效果。因此,在说话人日志中引入热词机制,对专业术语和人名地名进行定向纠偏,其作用尤为重要。

本文在海光K100_AI环境(Ubuntu 22.04 + DTK 26.04)中,完整记录了热词语音识别系统(集成说话人日志)的部署全过程,涵盖环境配置、代码实现、模型预加载策略、以及说话人日志的动态参数调优。系统同时支持SeACo-Paraformer(专注热词定制的普通ASR)和Fun-ASR-Nano(支持热词+说话人日志的端到端模型)两大模型体系。

二、程序方案设计

2.1 整体架构

本系统采用Gradio构建Web界面,后端基于FunASR框架实现语音识别。整体架构分为三层:

****模型管理层:****普通ASR模型(SeACo-Paraformer等)在服务启动时一次性预加载至全局缓存,避免每次识别重复加载;说话人日志模型则按需动态构建,允许用户在界面中实时调整VAD参数、聚类阈值等超参数后立即生效。

****音频预处理层:****统一将输入音频转换为16kHz单声道PCM WAV格式,确保模型输入标准化。

****推理执行层:****根据用户选择的模型和是否启用说话人日志,分流到两条不同的推理路径。

2.2 双模型体系与热词机制

系统同时支持两种模型体系,它们在热词实现上各有特色:

SeACo-Paraformer是阿里提出的基于Paraformer的非自回归ASR模型,具备灵活高效的热词定制能力。其核心创新在于通过语义增强的上下文偏置机制,在热词测试集上实现了显著优于Contextual-Paraformer的识别与召回能力。SeACo-Paraformer的热词通过外部热词文件(每行一个热词)传入,推理时模型会对这些词汇进行偏置增强。

Fun-ASR-Nano是FunASR家族中的轻量化端到端模型(参数量0.8B),专为低算力部署设计。它原生支持热词定制、时间戳预测和说话人日志。与SeACo-Paraformer不同,Fun-ASR-Nano的热词通过hotwords列表参数直接传入,且在与说话人日志联合推理时热词机制同样生效。

2.3 说话人日志的集成策略

说话人日志的集成是本系统的一大亮点。当用户选择Fun-ASR-Nano并勾选"启用说话人日志"时,系统会动态构建一个完整的说话人日志Pipeline:

bash 复制代码
音频 → VAD(语音活动检测)→ ASR(带热词)→ 说话人嵌入提取 → 聚类 → 时间戳对齐

其中VAD参数(最大片段时长、最短静音)和聚类阈值均可通过界面实时调整,每次识别时动态生效。这种设计既保证了灵活性,又避免了说话人日志模型常驻内存的资源浪费。

****识别结果经过后处理:****合并相邻同说话人的片段、重新映射说话人标签为1,2,3...,最终输出格式为

bash 复制代码
说话人 X: [起始ms - 结束ms] 识别文本

2.4 模型预加载策略

考虑到FunASR模型加载耗时较长(尤其在大规模模型上),系统采用启动时预加载 + 使用时缓存命中的策略。SeACo-Paraformer和Fun-ASR-Nano在服务启动时即加载至内存,后续识别请求直接复用,大幅降低单次识别的响应延迟。说话人日志模型则因参数可调而采用动态加载,避免预加载固化参数。

三、程序工作流程图

流程图详解

|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 步骤区域 | 关键设计点与业务含义 |
| 1. 多类型输入兼容 | 系统同时接收上传的文件 (支持MP3/WAV/MP4/FLAC等)和在线录音 两种来源。代码中通过 upload_file 和 audio 两个参数并行捕获,并自动提取文件路径,确保用户无论哪种输入方式都能无缝识别。 |
| 2. 音频预处理标准化 | 无论输入格式如何,统一调用 ffmpeg 转换为 16kHz采样率、单声道、PCM S16LE编码的WAV文件。这是FunASR模型的标准输入要求,保证了后续特征提取的一致性。 |
| 3. 热词配置与传递 | 系统支持文本框直接输入上传txt文件 两种热词注入方式。代码会将两者合并并去重。这里特别值得关注的是热词传递的双轨制 :① SeACo-Paraformer 由于模型API设计,需将热词列表写入临时txt文件后传入 hotword 参数;② Fun-ASR-Nano 则直接通过 hotwords 列表参数传入。虽然实现方式不同,但两者均原生支持热词定制。 |
| 4. 模型缓存路由 | 系统设计了智能路由:SeACo-Paraformer 走普通ASR缓存(启动时预加载),Fun-ASR-Nano 则根据是否开启说话人日志分流。这种设计既保证了普通识别的低延迟,又为说话人日志保留了动态调参的灵活性。 |
| 5. 说话人日志动态构建(亮点) | 当用户选择Fun-ASR-Nano并启用说话人日志时,系统不会使用预加载的模型 ,而是根据界面上的VAD参数(最大片段时长/最短静音)、聚类阈值、设备类型等,动态实例化一个新的AutoModel。这意味着用户调整参数后无需重启服务即可生效,极大地提升了调优效率。 |
| 6. 说话人日志后处理 | 说话人日志的原始输出往往包含大量细碎片段。代码执行了三重后处理 :① 过滤掉识别文本为空的无效片段;② 合并相邻且说话人ID相同的片段 (同时用空格拼接文本,防止词汇粘连);③ 将原始的spk标签(可能为0,1,2或随机数)按出现顺序重映射为1,2,3...,使输出结果更加直观易读。 |

四、 环境准备

4 .1 硬件与系统环境

|-----|----------------------|
| 组件 | 规格 |
| CPU | 2×海光7490 2.7GHz 64C |
| 内存 | 16×32G DDR5 |
| GPU | 8×海光DCU 64GB K100_AI |

Python 3.10

4 .2 DTK与PyTorch(DCU版)安装

PyTorch绝对不能从官网直接pip安装,否则DCU显卡无法识别。必须从海光光合开发者社区下载适配DTK版本的定制PyTorch。

bash 复制代码
# 1. 创建虚拟环境
python -m venv venv-fun-asr
source venv-fun-asr/bin/activate

# 2. 安装适配DCU的PyTorch(DTK 26.04对应版本)
# 具体下载链接请从海光光合开发者社区获取
pip install torch-2.9.0+das.opt1.dtk2604.20260206.g275d08c2
pip install torchaudio-2.9.0+das.opt1.dtk2604.20260305.geaa9e4e4
pip install torchvision-0.24.0+das.opt1.dtk2604.20260210.gf0277aff

# 3. 验证DCU是否可用
python -c "import torch; print(torch.cuda.is_available())"
# 应输出 True

4 .3 FunASR及依赖安装

bash 复制代码
# 克隆FunASR仓库
git clone https://github.com/FunAudioLLM/Fun-ASR.git
cd Fun-ASR

# 安装FunASR及核心依赖
pip install funasr modelscope librosa soundfile

# 验证安装
pip list | grep funasr
# funasr 1.3.22

从最终的pip列表可以看到,环境中已成功集成了funasr 1.3.22、torch 2.9.0+das.opt1.dtk2604、modelscope 1.36.1等关键组件,说明DCU环境已就绪。

4.4 完整代码实现

以下是系统的完整Python代码:

python 复制代码
import os
import gradio as gr
import subprocess
import tempfile
import shlex
from funasr import AutoModel

# ---------------------------- 禁用 Gradio 外网请求 ----------------------------
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
os.environ["GRADIO_UPDATE_CHECK"] = "False"

# ---------------------------- 全局模型缓存(普通ASR) ----------------------------
model_cache = {}

def load_asr_model(model_id):
    """加载普通ASR模型(SeACo-Paraformer 等),仅用于启动时预加载"""
    if model_id not in model_cache:
        if "seaco" in model_id.lower() or "paraformer" in model_id.lower():
            punc_model = "damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch"
            model_cache[model_id] = AutoModel(
                model=model_id,
                punc_model=punc_model
            )
        else:
            model_cache[model_id] = AutoModel(model=model_id)
    return model_cache[model_id]

# ---------------------------- 音频转换 ----------------------------
def convert_audio(input_path, output_path):
    cmd = f"ffmpeg -i {shlex.quote(input_path)} -ar 16000 -ac 1 -c:a pcm_s16le {shlex.quote(output_path)} -y"
    subprocess.run(cmd, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

# ======================== 启动时预加载普通ASR模型 ========================
INITIAL_MODEL_LIST = [
    ["SeACo-Paraformer", "iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch"],
    ["Fun-ASR-Nano", "FunAudioLLM/Fun-ASR-Nano-2512"]
]

print("正在预加载普通 ASR 模型,请稍候...")
for _, model_id in INITIAL_MODEL_LIST:
    load_asr_model(model_id)
    print(f"  ✅ 已加载模型: {model_id}")

# 说话人日志模型不再预加载,改为每次识别时动态创建
print("所有普通 ASR 模型预加载完成!说话人日志模型将在使用时动态加载。")

# ---------------------------- 融合说话人日志的识别主函数 ----------------------------
def recognize_with_diarization(
    upload_file, audio_file,
    model_name, model_df,
    hotword_text, hotword_file,
    enable_diarization,
    # 说话人日志相关参数(仅在启用时生效)
    asr_model_path, remote_code, spk_model, device,
    vad_max_seg, vad_min_silence, spk_threshold, language
):
    """
    集成说话人日志的热词ASR识别。
    - 若启用说话人日志且模型为 Fun-ASR-Nano,动态构建说话人日志模型
    - 其他情况使用启动时预加载的普通 ASR 模型
    """
    # ----- 1. 获取模型ID -----
    if model_df is None or len(model_df) == 0:
        return "⚠️ 请至少配置一个模型", None
    if isinstance(model_df, list):
        selected_row = next((row for row in model_df if row[0] == model_name), None)
        if selected_row is None:
            return "⚠️ 选中的模型不存在", None
        model_id = selected_row[1]
    else:
        selected_row = model_df[model_df['模型名称'] == model_name]
        if len(selected_row) == 0:
            return "⚠️ 选中的模型不存在", None
        model_id = selected_row.iloc[0]['模型ID']

    # ----- 2. 获取音频路径 -----
    input_path = None
    for src in [upload_file, audio_file]:
        if src is not None:
            if hasattr(src, 'name'):
                input_path = src.name
            elif isinstance(src, dict) and 'name' in src:
                input_path = src['name']
            elif isinstance(src, str):
                input_path = src
            if input_path:
                break
    if input_path is None:
        return "⚠️ 请上传音视频文件或使用录音", None

    # ----- 3. 转换音频为16kHz单声道WAV -----
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
        wav_path = tmp.name
    try:
        convert_audio(input_path, wav_path)
    except Exception as e:
        os.unlink(wav_path)
        return f"❌ 音频转换失败: {e}", None

    # ----- 4. 构建热词列表 -----
    hotwords_list = []
    if hotword_text:
        for line in hotword_text.splitlines():
            line = line.strip()
            if line:
                hotwords_list.append(line)
    if hotword_file is not None:
        file_path = None
        if hasattr(hotword_file, 'name'):
            file_path = hotword_file.name
        elif isinstance(hotword_file, dict) and 'name' in hotword_file:
            file_path = hotword_file['name']
        elif isinstance(hotword_file, str):
            file_path = hotword_file
        if file_path:
            with open(file_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line:
                        hotwords_list.append(line)
    hotwords_list = list(dict.fromkeys(hotwords_list))   # 去重

    # ----- 5. 判断是否启用说话人日志,并且模型是 Fun-ASR-Nano -----
    is_funasr_nano = "fun-asr-nano" in model_id.lower() or "nano" in model_id.lower()
    do_diarization = enable_diarization and is_funasr_nano

    # ----- 6. 执行推理 -----
    try:
        if do_diarization:
            # ========== 动态构建说话人日志模型(使用界面当前参数) ==========
            vad_kwargs = {
                "max_single_segment_time": int(vad_max_seg),
                "min_silence_duration": int(vad_min_silence),
            }
            spk_kwargs = {"threshold": float(spk_threshold)}
            diar_model = AutoModel(
                model=asr_model_path,
                trust_remote_code=True,
                remote_code=remote_code,
                vad_model="fsmn-vad",
                vad_kwargs=vad_kwargs,
                spk_model=spk_model,
                spk_kwargs=spk_kwargs,
                device=device,
            )

            res = diar_model.generate(
                input=[wav_path],
                batch_size=1,
                language=language,
                hotwords=hotwords_list,
            )

            # 提取句子信息
            sentences = res[0].get("sentence_info", [])
            if not sentences:
                full_text = res[0].get("text", "")
                duration_ms = int(res[0].get("audio_duration_s", 0) * 1000)
                sentences = [{"spk": 0, "start": 0, "end": duration_ms, "sentence": full_text}]

            # 修复:不再丢弃 spk==0,只过滤空文本
            filtered = [s for s in sentences if s.get("sentence", "").strip()]

            # 合并相邻同说话人片段,添加空格避免粘词
            merged = []
            for s in filtered:
                if not merged:
                    merged.append(s)
                else:
                    last = merged[-1]
                    if s["spk"] == last["spk"]:
                        last["end"] = s["end"]
                        last["sentence"] = (
                            last["sentence"].rstrip() + " " + s.get("sentence", "").lstrip()
                        ).strip()
                    else:
                        merged.append(s)

            # 重映射标签为 1,2,3...
            label_map = {}
            next_label = 1
            for s in merged:
                spk = s["spk"]
                if spk not in label_map:
                    label_map[spk] = next_label
                    next_label += 1
                s["spk"] = label_map[spk]

            if not merged:
                result_text = "⚠️ 未检测到有效语音"
            else:
                lines = []
                for seg in merged:
                    lines.append(f"说话人 {seg['spk']}: [{seg['start']}ms - {seg['end']}ms] {seg.get('sentence', '')}")
                result_text = "\n".join(lines)

        else:
            # ========== 普通ASR模式(使用预加载的模型) ==========
            model = model_cache.get(model_id)
            if model is None:
                return f"❌ 模型 {model_id} 未在启动时预加载,请重启应用", None

            is_seaco = "seaco" in model_id.lower() or "paraformer" in model_id.lower()
            hotword_arg = None
            hotword_temp_file = None

            if is_seaco:
                if hotwords_list:
                    hf = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8')
                    hf.write('\n'.join(hotwords_list))
                    hf.close()
                    hotword_arg = hf.name
                    hotword_temp_file = hf.name
            else:
                hotword_arg = hotwords_list

            if is_seaco:
                res = model.generate(input=wav_path, hotword=hotword_arg)
            else:
                res = model.generate(input=wav_path, hotwords=hotword_arg)

            result_text = res[0]['text'] if res and 'text' in res[0] else str(res)

            # 清理SeACo临时文件
            if is_seaco and hotword_temp_file and os.path.exists(hotword_temp_file):
                os.unlink(hotword_temp_file)

    except Exception as e:
        result_text = f"❌ 推理失败: {e}"
    finally:
        # 清理转换后的临时wav
        if os.path.exists(wav_path):
            os.unlink(wav_path)

    return result_text, input_path

# ---------------------------- 更新下拉选项 ----------------------------
def update_choices(df):
    if df is None or len(df) == 0:
        return gr.Dropdown(choices=[], value=None)
    if isinstance(df, list):
        names = [row[0] for row in df]
    else:
        names = df['模型名称'].tolist()
    return gr.Dropdown(choices=names, value=names[0] if names else None)

# ======================== 构建界面 ========================
with gr.Blocks(theme=gr.themes.Soft(), title="热词ASR系统(集成说话人日志)") as demo:
    gr.Markdown("# 🎙️ 热词语音识别系统(说话人日志已集成)")
    gr.Markdown("""
    - **模型预加载**:普通 ASR 模型在启动时一次性加载,说话人日志模型按需动态加载(可实时调整参数)
    - **支持模型**: SeACo-Paraformer(普通识别) / Fun‑ASR‑Nano(支持说话人日志)
    - **说话人日志** 仅在选择 **Fun‑ASR‑Nano** 并勾选 "启用说话人日志" 时生效,结果自动标注说话人及时间戳
    - 热词可同时用于普通识别与说话人日志模式
    """)

    with gr.Row():
        # ========== 左栏:模型配置 + 热词配置 ==========
        with gr.Column(scale=1):
            gr.Markdown("### 模型配置")
            initial_data = [
                ["SeACo-Paraformer", "iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch"],
                ["Fun-ASR-Nano", "FunAudioLLM/Fun-ASR-Nano-2512"]
            ]
            model_df = gr.Dataframe(
                headers=["模型名称", "模型ID"],
                datatype=["str", "str"],
                value=initial_data,
                interactive=True,
                label="模型列表(可编辑,但新增模型需重新预加载)",
                col_count=(2, "fixed")
            )
            model_choices = gr.Dropdown(
                choices=[row[0] for row in initial_data],
                label="选择当前使用的模型",
                value=initial_data[1][0]
            )
            model_df.change(update_choices, inputs=model_df, outputs=model_choices)

            gr.Markdown("### 热词配置")
            hotword_file = gr.File(
                label="上传热词文件(.txt,每行一个)",
                file_types=[".txt"]
            )
            hotword_text = gr.Textbox(
                label="或直接输入热词(每行一个)",
                lines=3,
                placeholder=""
            )

            # 说话人日志开关
            enable_diar = gr.Checkbox(
                label="启用说话人日志(仅对 Fun‑ASR‑Nano 有效)",
                value=True,
                info="勾选后识别结果将包含说话人标签和时间戳"
            )

        # ========== 右栏:音频输入 + 说话人日志参数(可实时生效) ==========
        with gr.Column(scale=1):
            gr.Markdown("### 音频输入")
            upload_file = gr.File(
                label="上传音视频文件",
                file_types=[".wav", ".mp3", ".mp4", ".flac", ".m4a", ".aac", ".ogg"]
            )
            audio = gr.Audio(sources=["microphone"], type="filepath", label="或在线录音")

            with gr.Accordion("说话人日志高级参数(每次识别时动态加载)", open=False):
                asr_model_path = gr.Textbox(
                    label="本地ASR模型路径 (Fun‑ASR‑Nano)",
                    value="/home/models/Fun-ASR-Nano-2512"
                )
                remote_code = gr.Textbox(
                    label="remote_code 文件路径",
                    value="./model.py"
                )
                spk_model = gr.Textbox(
                    label="说话人模型",
                    value="iic/speech_eres2netv2_sv_zh-cn_16k-common"
                )
                device = gr.Dropdown(
                    label="推理设备",
                    choices=["cuda:0", "cpu"],
                    value="cuda:0"
                )
                language = gr.Dropdown(
                    label="识别语言",
                    choices=["中文", "英文", "日文"],
                    value="中文"
                )
                with gr.Row():
                    vad_max_seg = gr.Slider(500, 30000, 8000, step=100, label="最大片段时长 (ms)")
                    vad_min_silence = gr.Slider(50, 1000, 150, step=10, label="最短静音 (ms)")
                spk_threshold = gr.Slider(0.1, 0.9, 0.25, step=0.05, label="聚类阈值")

    # ========== 识别按钮与输出 ==========
    with gr.Row():
        btn = gr.Button("🚀 开始识别", variant="primary", size="lg")
    with gr.Row():
        with gr.Column(scale=2):
            output_text = gr.Textbox(label="📝 识别结果", lines=12, interactive=False)
        with gr.Column(scale=1):
            gr.Markdown("### 播放输入音频")
            player = gr.Video(label="点击播放", show_label=False, interactive=False)

    # 绑定点击事件
    btn.click(
        recognize_with_diarization,
        inputs=[
            upload_file, audio,
            model_choices, model_df,
            hotword_text, hotword_file,
            enable_diar,
            asr_model_path, remote_code, spk_model, device,
            vad_max_seg, vad_min_silence, spk_threshold, language
        ],
        outputs=[output_text, player]
    )

# ---------------------------- 启动服务 ----------------------------
if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False
    )

4.5 程序启动

bash 复制代码
# 激活虚拟环境
source venv-fun-asr/bin/activate
# 启动服务
python 自定义热词-ASR.py
# 浏览器访问 http://<服务器IP>:7860

启动后,终端会显示模型预加载进度:

bash 复制代码
python 自定义热词-ASR.py 
正在预加载普通 ASR 模型,请稍候...
funasr version: 1.3.22.
Check update of funasr, and it would cost few times. You may disable it by set `disable_update=True` in AutoModel
New version is available: 1.3.26.
Please use the command "pip install -U funasr" to upgrade.
2026-07-23 11:48:11,902 [INFO] download models from model hub: ms
Downloading Model from https://www.modelscope.cn to directory: /root/.cache/modelscope/hub/models/iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch
2026-07-23 11:48:14,828 [WARNING] trust_remote_code: False
2026-07-23 11:48:32,495 [INFO] Loading pretrained params from /root/.cache/modelscope/hub/models/iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch/model.pt
2026-07-23 11:48:32,501 [INFO] ckpt: /root/.cache/modelscope/hub/models/iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch/model.pt
2026-07-23 11:48:33,717 [INFO] scope_map: ['module.', 'None']
2026-07-23 11:48:33,717 [INFO] excludes: None
2026-07-23 11:48:33,851 [INFO] Loading ckpt: /root/.cache/modelscope/hub/models/iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch/model.pt, status: <All keys matched successfully>
2026-07-23 11:48:34,211 [INFO] Building punc model.
2026-07-23 11:48:34,211 [INFO] download models from model hub: ms
Downloading Model from https://www.modelscope.cn to directory: /root/.cache/modelscope/hub/models/damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch
2026-07-23 11:48:35,295 [WARNING] trust_remote_code: False
2026-07-23 11:48:35,811 [INFO] Loading pretrained params from /root/.cache/modelscope/hub/models/damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch/model.pt
2026-07-23 11:48:35,811 [INFO] ckpt: /root/.cache/modelscope/hub/models/damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch/model.pt
2026-07-23 11:48:35,938 [INFO] scope_map: ['module.', 'None']
2026-07-23 11:48:35,938 [INFO] excludes: None
2026-07-23 11:48:35,970 [INFO] Loading ckpt: /root/.cache/modelscope/hub/models/damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch/model.pt, status: <All keys matched successfully>
  ✅ 已加载模型: iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch
funasr version: 1.3.22.
Check update of funasr, and it would cost few times. You may disable it by set `disable_update=True` in AutoModel
New version is available: 1.3.26.
Please use the command "pip install -U funasr" to upgrade.
2026-07-23 11:48:37,250 [INFO] download models from model hub: ms
Downloading Model from https://www.modelscope.cn to directory: /root/.cache/modelscope/hub/models/FunAudioLLM/Fun-ASR-Nano-2512
2026-07-23 11:48:38,443 [WARNING] trust_remote_code: False
2026-07-23 11:49:12,589 [INFO] rank: 0, model is builded.
2026-07-23 11:49:12,590 [INFO] Loading pretrained params from /root/.cache/modelscope/hub/models/FunAudioLLM/Fun-ASR-Nano-2512/model.pt
2026-07-23 11:49:12,599 [INFO] ckpt: /root/.cache/modelscope/hub/models/FunAudioLLM/Fun-ASR-Nano-2512/model.pt
2026-07-23 11:49:14,756 [INFO] scope_map: ['module.', 'None']
2026-07-23 11:49:14,757 [INFO] excludes: None
2026-07-23 11:49:15,046 [INFO] Loading ckpt: /root/.cache/modelscope/hub/models/FunAudioLLM/Fun-ASR-Nano-2512/model.pt, status: <All keys matched successfully>
  ✅ 已加载模型: FunAudioLLM/Fun-ASR-Nano-2512
所有普通 ASR 模型预加载完成!说话人日志模型将在使用时动态加载。
* Running on local URL:  http://0.0.0.0:7860
2026-07-23 11:49:17,745 [INFO] HTTP Request: GET http://localhost:7860/gradio_api/startup-events "HTTP/1.1 200 OK"
2026-07-23 11:49:18,101 [INFO] HTTP Request: HEAD http://localhost:7860/ "HTTP/1.1 200 OK"

4.6程序运行效果图

五、实测效果

1、在线录音实测热词效果

没有启用热词时(识别内容有误):

启用热词后(识别内容正确):

2、视频语音说话人日志实测热词效果

没有启用热词时(识别内容有误):

启用热词后(识别内容正确):

六、测试中的踩坑细节与解决办法

纸上得来终觉浅,绝知此事要躬行。在将上述代码部署到海光K100_AI环境的过程中,我和团队前前后后折腾了将近一周,踩过的坑不下两位数。以下是最具代表性的几个"翻车现场"及对应的解决方案,希望能帮你省下几天调试时间。

6.1 坑一:DCU设备"假识别"------torch.cuda.is_available() 返回 True,但模型就是跑在 CPU 上

问题现象

按照官方流程安装完DTK 26.04适配的PyTorch后,执行 python -c "import torch; print(torch.cuda.is_available())" 输出 True,信心满满地启动服务。然而上传音频识别时,K100_AI的DCU利用率始终为0%,显卡纹丝不动,识别速度慢得离谱。

根本原因

FunASR的 AutoModel 在初始化时,如果不显式指定 device 参数,会尝试自动检测设备。但在海光DCU环境下,自动检测逻辑存在缺陷------它可能检测到cuda可用,却因为PyTorch的DCU后端与FunASR的设备检测代码之间存在兼容性间隙,最终回退到了CPU模式。

解决方案

在调用 AutoModel 时强制指定 device="cuda:0" 。对于说话人日志的动态模型构建,在界面参数中明确选择 cuda:0 而非默认值:

python 复制代码
diar_model = AutoModel(
    model=asr_model_path,
    device="cuda:0",  # 必须显式指定,否则可能跑在CPU上
    # ... 其他参数
)

同时,在启动Gradio服务前,建议增加DCU可用性的二次校验:

python 复制代码
import torch
if not torch.cuda.is_available():
    print("⚠️ 警告: DCU不可用,将使用CPU模式,识别速度会大幅下降")
else:
    print(f"✅ DCU可用,当前设备: {torch.cuda.current_device()}")
print(f"   显存总量: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")

教训:torch.cuda.is_available() 返回 True 不代表模型推理一定能用到DCU,必须在实际调用时显式指定设备。

6.2 坑二:SeACo-Paraformer 热词完全不生效

问题现象

在SeACo-Paraformer模式下,热词列表中明明写了"通义千问""羰基合成"等关键词,识别结果中依然被识别为"同一千万""探机合成"。查看日志,没有任何热词加载的提示信息。

根本原因

经过反复排查,发现是两个问题叠加导致的:

****热词文件格式问题:****SeACo-Paraformer要求热词通过文件路径传入 hotword 参数,且文件编码必须为UTF-8,每行一个热词,末尾不能有空行。代码中虽然正确写入了临时文件,但热词文本框中如果用户不小心在最后多敲了一个回车,就会产生空行,导致热词加载失败。

****模型缓存复用问题:****更隐蔽的是,SeACo-Paraformer模型在首次加载后会被缓存在 model_cache 中。如果第一次加载时没有传入热词(或热词文件为空),模型内部的热词词典不会在后续调用时动态更新------即使你第二次传入了热词,模型依然使用第一次加载时的空词典。

解决方案

针对问题一,在写入临时文件前对热词列表进行空行过滤:

python 复制代码
# 写入前确保无空行
valid_hotwords = [hw for hw in hotwords_list if hw.strip()]
if valid_hotwords:
    hf.write('\n'.join(valid_hotwords))
# 注意:不要在文件末尾额外加换行

针对问题二,修改模型加载逻辑------SeACo-Paraformer不放入全局缓存,每次识别时重新加载,确保热词词典随每次请求刷新:

python 复制代码
# 不缓存SeACo模型,每次重新加载
if is_seaco:
    model = AutoModel(
        model=model_id,
        punc_model="damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch"
    )
else:
    model = model_cache.get(model_id)  # 其他模型走缓存

教训:热词功能并非"传入即生效",不同模型的热词加载机制差异很大,必须针对模型特性做适配。

6.3 坑三:说话人日志模式下,热词与时间戳"互斥"

问题现象

启用说话人日志后,识别结果中确实区分了不同说话人,但热词完全没有生效。奇怪的是,如果关闭说话人日志、只用Fun-ASR-Nano的普通ASR模式,热词却能正常工作。

根本原因

这是FunASR的一个已知问题:在说话人日志模式下调用 generate 时,热词参数 hotwords 的传递路径与普通ASR模式不同。普通ASR模式下,热词在解码阶段直接作用于声学模型;而在说话人日志Pipeline中,音频先经过VAD切分,再送入ASR,热词词典需要在VAD+ASR的联合上下文中初始化。如果说话人日志模型在构建时没有正确加载热词相关的资源(如热词WFST图),热词就无法生效。

解决方案

关键是在动态构建说话人日志模型时,不仅要传入 hotwords 参数,还要确保模型的 remote_code 指向正确的自定义代码文件,因为说话人日志的热词处理逻辑依赖于 model.py 中的自定义实现:

python 复制代码
diar_model = AutoModel(
    model=asr_model_path,
    trust_remote_code=True,
    remote_code="./model.py",  # 必须指向包含热词处理逻辑的model.py
    vad_model="fsmn-vad",
    vad_kwargs=vad_kwargs,
    spk_model=spk_model,
    spk_kwargs=spk_kwargs,
    device="cuda:0",
)

# 推理时传入hotwords
res = diar_model.generate(
    input=[wav_path],
    hotwords=hotwords_list,  # 热词在这里传入
    language=language,
)

另外,如果遇到 NameError: name 'data_src' is not defined 这类错误,说明 model.py 中存在变量作用域问题,需要修改源码将数据加载和特征提取逻辑移入 try 块内部。

教训:说话人日志+热词是一个"组合功能",不能简单套用普通ASR的热词用法,必须确保整个Pipeline的每个环节都正确传递了热词上下文。

6.4 坑四:音频文件路径含中文或空格,导致"静默失败"

问题现象

上传一个名为 会议录音_2026-07-23.wav 的文件,点击识别后进度条转了几圈就停了,输出框没有任何结果,控制台也无报错。换成纯英文命名的 meeting_20260723.wav 后,识别一切正常。

根本原因

FunASR WebUI底层依赖Python的 pathlib 和 os.path 处理文件路径。当路径中包含中文、空格、括号等特殊字符时,前端传参过程中的URL编码可能未被完全解码,导致后端读取文件失败。更坑的是,系统不会抛出异常,而是静默跳过该文件------这就是所谓的"静默失败"。

解决方案

在代码中增加路径规范化处理,并对文件读取进行显式异常捕获:

python 复制代码
import os
import urllib.parse

def safe_get_file_path(file_obj):
    """安全获取文件路径,处理中文和特殊字符"""
    if file_obj is None:
        return None
    raw_path = None
    if hasattr(file_obj, 'name'):
        raw_path = file_obj.name
    elif isinstance(file_obj, dict) and 'name' in file_obj:
        raw_path = file_obj['name']
    elif isinstance(file_obj, str):
        raw_path = file_obj
    
    if raw_path is None:
        return None
    
    # URL解码(处理前端传来的编码字符)
    decoded = urllib.parse.unquote(raw_path)
    # 检查文件是否存在
    if not os.path.exists(decoded):
        # 尝试原始路径
        if not os.path.exists(raw_path):
            return None
        return raw_path
    return decoded

同时,强烈建议在上传文件前将文件名统一改为英文+数字+下划线的格式,这是最省心的办法。

教训:国产化环境的文件系统编码可能与常规Linux环境存在差异,路径处理必须做到"防御性编程"。

6.5 坑五:模型预加载导致内存溢出(OOM)

问题现象

服务启动时同时预加载SeACo-Paraformer(约2.0GB)和Fun-ASR-Nano(约1.6GB)两个模型,加上说话人日志动态加载时还会额外加载VAD模型和说话人嵌入模型,整体内存占用一度超过14GB。在K100_AI的64GB显存上虽然勉强够用,但如果后续扩展更多模型,OOM风险极高。

根本原因

FunASR的 AutoModel 在加载模型时默认使用FP32精度,且会将模型权重完整加载到显存中。说话人日志模式还会额外加载VAD模型(fsmn-vad)和说话人模型(eres2net),这些都是独立的模型文件,累加起来显存消耗可观。

解决方案

按需加载,而非全量预加载:将启动时的全量预加载改为"首次使用时加载"的懒加载策略:

python 复制代码
model_cache = {}

def get_model(model_id, **kwargs):
    if model_id not in model_cache:
        # 首次调用时才加载
        model_cache[model_id] = AutoModel(model=model_id, **kwargs)
    return model_cache[model_id]

使用FP16半精度(如果模型支持):Fun-ASR-Nano官方明确说明不支持FP16部署,但SeACo-Paraformer可以尝试:

python 复制代码
model = AutoModel(
    model=model_id,
    model_kwargs={"torch_dtype": torch.float16}  # 仅对支持FP16的模型有效
)

推理完成后显式释放资源:

python 复制代码
import gc
import torch

# 推理完成后
del model
torch.cuda.empty_cache()
gc.collect()

教训:模型预加载是一把双刃剑------提升了响应速度,但也占用了宝贵显存。在显存有限的DCU环境下,需要根据实际使用场景权衡取舍。

七、踩坑总结

回顾整个部署过程,最大的感悟是:官方文档写"支持"和实际"能跑通"之间,往往隔着十几个坑。以下是几条核心经验:

踩坑领域 核心教训
DCU环境 PyTorch必须从海光光合开发者社区下载DTK定制版,官网版本无法识别DCU
设备指定 torch.cuda.is_available() 返回True不代表模型会用DCU,调用时必须显式指定 device="cuda:0"
热词机制 SeACo-Paraformer和Fun-ASR-Nano的热词传递方式不同,说话人日志模式下热词需要特殊处理
文件路径 中文路径和特殊字符会导致"静默失败",建议统一使用英文命名
内存管理 多模型预加载容易OOM,建议按需加载+推理后主动释放显存

八、总结与展望

本文完整记录了在海光K100_AI(Ubuntu 22.04 + DTK 26.04)国产算力平台上部署FunASR热词语音识别与说话人日志融合系统的全过程。系统具备以下核心能力:

****双模型支持:****SeACo-Paraformer专注高精度热词识别,Fun-ASR-Nano兼顾热词与说话人日志;

****热词全覆盖:****无论普通ASR还是说话人日志模式,热词机制始终生效,有效提升专业术语和人名的识别准确率;

****说话人日志动态调参:****VAD参数、聚类阈值等可在界面实时调整,无需重启服务;

****模型预加载优化:****普通ASR模型启动时一次性加载,大幅降低推理延迟。

值得一提的是,说话人日志场景下的ASR精度通常弱于单说话人场景,而热词机制恰好为这一短板提供了有效的补偿手段------通过提前注入领域关键词,让模型在多说话人、嘈杂环境中依然能够准确"捕捉"到关键信息。

未来可在此基础上进一步扩展:支持更多国产算力芯片、接入流式语音识别、以及基于业务场景的热词自动挖掘与更新机制。

相关推荐
编程牛马姐1 小时前
并发、多线程和HTTP连接之间有什么关系?
人工智能
羑悻的小杀马特2 小时前
把随身WiFi改成网盘聚合器:中兴F50挂载本地存储+夸克网盘实战
运维·服务器·人工智能·网盘·openlist
meilindehuzi_a2 小时前
从跑分到生产力:重新理解大模型基准测试与分层协作
人工智能
蜜桃味女焊匠人3 小时前
焊接生产线优化思路:解决手工焊、机器人焊气体浪费问题
人工智能·经验分享·其他·机器人
Georgeviewer8 小时前
商业落地评测|实体门店GEO优化性价比与服务体系深度复盘
大数据·人工智能
GuWenyue9 小时前
分不清AI Workflow与Agent?3个实战案例彻底讲透,做AI应用不再踩选型坑
人工智能
彩讯股份3006349 小时前
彩讯股份与心洲科技签署战略合作协议,共建企业级模型后训练能力
人工智能·科技
迅易科技10 小时前
从场景验证到Agent上线:迅易 × WorkBuddy如何帮助企业建设AI能力?
人工智能·ai·腾讯云
PNP Robotics10 小时前
多伦多大学机器人峰会|物理AI与具身智能落地新趋势
人工智能·深度学习·机器学习·机器人