一、引言
语音识别技术正在经历从"语音转文字"到"语音理解"的深刻演进。在实际业务场景中,仅仅把音频转成文字往往是不够的------会议记录需要区分"谁在何时说了什么",智能客服需要精准识别专业术语和人名地名,影视剧分析需要对话角色标注。这些需求催生了两个关键技术方向:热词定制(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精度通常弱于单说话人场景,而热词机制恰好为这一短板提供了有效的补偿手段------通过提前注入领域关键词,让模型在多说话人、嘈杂环境中依然能够准确"捕捉"到关键信息。
未来可在此基础上进一步扩展:支持更多国产算力芯片、接入流式语音识别、以及基于业务场景的热词自动挖掘与更新机制。