FunASR 模型注册机制深度指南:从自定义模型接入到安全加载实践
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
FunASR 是阿里巴巴达摩院开源的语音识别工具包,覆盖训练、推理、流式 ASR、VAD、标点、说话人分离以及 OpenAI 兼容/MCP 服务等完整链路。本文以仓库中 docs/model_registration_zh.md 为核心骨架,结合 funasr/register.py、funasr/auto/auto_model.py、funasr/download/download_model_from_hub.py 与 funasr/utils/dynamic_import.py 等源码,系统讲解模型注册表的运作原理、自定义模型接入的完整接口约定、两类模型加载路径的差异,以及trust_remote_code场景下的安全边界。读完本文,你将能独立把一个自定义模型注册进 FunASR,并判断何时该用直接注册构造、何时该走模型目录解析。
1. 先理解:注册到底做了什么
注册(Registration)在 FunASR 中的含义非常具体:它只是把「Python 类实现」与「配置名称」连接起来。它不会下载权重、不会自动兼容任意 Transformers 模型、不提供训练或导出能力,也不代表任何质量认证。这一点是整个模型注册体系的出发点,也是阅读后续所有内容前必须建立的心智模型。
注册表的实现集中在 funasr/register.py。该文件定义了一个RegisterTables数据类,并导出一个进程级单例tables:
- 注册键保存在进程级字典中(
model_classes、frontend_classes、encoder_classes等均为类属性字典); register()是一个装饰器工厂,返回原类(不会包装或修改类本身);- 装饰器通过
inspect.getfile与inspect.getsourcelines记录类的源码位置,作为注册元数据(*_meta表); - 同类名重复注册时,后者会覆盖前者,只记录 debug 日志而不报错——因此导入顺序会影响最终生效的实现。
从 funasr/register.py 可以看到register的完整逻辑:
def register(self, register_tables_key: str, key: str = None) -> callable: def decorator(target_class): if not hasattr(self, register_tables_key): setattr(self, register_tables_key, {}) logging.debug(f"New registry table added: {register_tables_key}") registry = getattr(self, register_tables_key) registry_key = key if key is not None else target_class.__name__ if registry_key in registry: logging.debug( f"Key {registry_key} already exists in {register_tables_key}, re-register" ) registry[registry_key] = target_class # ... 记录 (register_key, class_name, "path:line") 到 *_meta 表 return target_class return decorator注意其中一行:if not hasattr(self, register_tables_key)会动态创建新表。这意味着表名拼错不会报错,而是生成一张无人使用的表——这是排查「为什么我的注册没生效」时的经典陷阱。
1.1 注册键的命名规则
使用装饰器时的约定是:
@tables.register("model_classes", "YourUniqueModelName")其中tables来自funasr.register,第二个参数是区分大小写的精确注册键;省略第二个参数时,默认使用 Python 类名作为注册键。由于装饰器依赖inspect记录源码位置,示例类必须定义在可导入的.py文件中,而不能只在 REPL 会话里定义,也不能定义在动态生成的类上(否则无法追溯源码位置)。
1.2 同名覆盖与冲突检查
因为同名键会被静默覆盖,所以:
- 使用组织或项目专属的名称(例如
DocsEchoModelV1、MyTeamAsrV2); - 在注册前主动增加冲突检查,例如
if NAME in tables.model_classes: raise RuntimeError(...); - 不要给无关的自定义模型使用
SenseVoiceSmall、Paraformer、FunASRNano等已有名称——这会覆盖内置实现并破坏其他使用方。
调试时可使用:
tables.print("model") # 打印 model_classes 的注册元数据(注册名、类名、源码位置) tables.model_classes[name] # 取出当前生效的实现print方法遍历vars(self)中所有以_meta结尾的表并格式化输出(见 funasr/register.py),key参数用于过滤只查看包含该关键字的表。
1.3 不止一张表:组件级注册
model_classes只是众多注册表之一。从 funasr/register.py 可以看到,RegisterTables预定义了:
| 注册表 | 用途 |
|---|---|
model_classes | 完整模型(供AutoModel直接构造) |
frontend_classes | 前端特征提取器 |
encoder_classes/decoder_classes | 编码器 / 解码器组件 |
tokenizer_classes | 分词器 |
dataset_classes/index_ds_classes/batch_sampler_classes | 数据集与批采样 |
specaug_classes/normalize_classes/joint_network_classes/predictor_classes/stride_conv_classes/dataloader_classes | 其他组件级表 |
关键认知:注册 encoder 不等于注册完整模型。每个调用方都有自己的接口约定——AutoModel从model_classes取模型,而编码器表、前端表、分词器表分别服务于build_model内部的不同环节(详见 funasr/auto/auto_model.py 中 tokenizer/frontend/model 的构建顺序)。若你只注册了 encoder 却想用AutoModel(model="你的名字")构造,会得到 "is not registered" 的报错。
2. 最小本地接口示例:零依赖跑通注册
文档给出了一个可在任何环境(无需 GPU、无需权重、无需下载)运行的玩具示例,用于验证「注册 → AutoModel 构造 → generate 推理」整条接口链路。将以下代码保存为可导入的custom_model_demo.py,然后在已安装当前 checkout 的环境执行python custom_model_demo.py:
import torch from funasr import AutoModel from funasr.register import tables MODEL_NAME = "DocsEchoModelV1" if MODEL_NAME in tables.model_classes: raise RuntimeError(f"Registry collision: {MODEL_NAME}") @tables.register("model_classes", MODEL_NAME) class DocsEchoModel(torch.nn.Module): def __init__(self, **kwargs): super().__init__() self.anchor = torch.nn.Parameter(torch.zeros(1), requires_grad=False) def inference( self, data_in, data_lengths=None, key=None, tokenizer=None, frontend=None, **kwargs, ): results = [ {"key": sample_key, "text": str(value)} for sample_key, value in zip(key, data_in) ] return results, {} if __name__ == "__main__": model = AutoModel( model=MODEL_NAME, model_conf={}, device="cpu", disable_update=True, disable_pbar=True, ) result = model.generate(input=["hello", "world"], data_type="text") assert [row["text"] for row in result] == ["hello", "world"] assert all(isinstance(row["key"], str) for row in result) print([row["text"] for row in result])运行后终端输出应为['hello', 'world']。这个例子只回显文本、不执行语音识别,因此不需要权重、音频、模型下载或 GPU。
2.1 示例中的关键细节
为什么保留一个参数?当前 funasr/auto/auto_model.py 的inference方法在推理结束时执行device = next(model.parameters()).device来清理 CUDA 缓存。如果模型是零参数的空nn.Module,next(model.parameters())会直接抛StopIteration。因此示例在__init__中创建了一个torch.nn.Parameter占位参数:
self.anchor = torch.nn.Parameter(torch.zeros(1), requires_grad=False)为什么传model_conf={}?这是有意为之。看 funasr/auto/auto_model.py 的build_model:
if "model_conf" not in kwargs: logging.info("download models from model hub: {}".format(kwargs.get("hub", "ms"))) kwargs = download_model(**kwargs)只要model_conf键存在,build_model就会跳过 hub 下载与配置解析。所以这里的model是已注册的类名,而不是目录或 hub ID。
必须先自行导入模块。在直接构造路径中传remote_code不会触发导入——remote_code只在 hub 下载路径中被消费(见第 4 节)。因此示例中from funasr.register import tables之后的装饰器执行,就是完成注册的那一步。
构造参数的合并规则。解析后的 kwargs 会通过deep_update覆盖合并进model_conf,再一起传给构造器(funasr/auto/auto_model.py):
model_conf = {} deep_update(model_conf, kwargs.get("model_conf", {})) deep_update(model_conf, kwargs) model = model_class(**model_conf)这意味着已构造的 tokenizer/frontend、设备、词表大小、输入维度等都会被注入到model_conf中。因此自定义模型应像真实模型一样,接收相应的命名参数以及**kwargs兜底,避免因多余键而构造失败。
2.2 玩具示例的验证边界
这段代码不只是文档示例,tests/test_training_docs_contract.py中的test_no_download_toy_against_checkout测试会在临时文件中针对当前源码直接执行这段代码(设置HF_HUB_OFFLINE=1、TRANSFORMERS_OFFLINE=1保证离线),并断言输出包含['hello', 'world'](见 tests/test_training_docs_contract.py)。它只验证接口连通性,不验证任何语音模型、不下载权重、不跑 GPU。
3. 推理与训练接口约定
自定义模型必须遵循 AutoModel 对模型对象的接口约定,否则会在运行时以各种隐晦方式失败。下表总结了当前源码约定:
| 接口 | 当前源码约定 |
|---|---|
| 模型对象 | 通常为torch.nn.Module,需要支持.to(...)、.eval()、.parameters();构造配置由模型自己决定。 |
inference输入 | 不使用 VAD 时,AutoModel 把输入组织成data_in和key列表,在torch.no_grad()下调用model.inference(**batch, **kwargs)(见 funasr/auto/auto_model.py)。单条data_type="fbank"输入会直接传入特征对象并设data_lengths=input_len(funasr/auto/auto_model.py)。tokenizer/frontend 是已构造对象或None。 |
| 模型层返回值 | 返回二元组(results, meta_data):results是list[dict],meta_data是字典。ASR 结果使用字符串key、text并保持输入顺序与标识。不能只返回结果字典列表,否则 AutoModel 会把第一条当成整个 batch 的结果(见 funasr/auto/auto_model.py 的res[0]/res[1]解包逻辑)。 |
| 元数据 | 可包含load_data、extract_feat、batch_data_time。音频场景的batch_data_time是以秒为单位的正时长,不能填毫秒或零——零会导致 RTF 计时代码除零(funasr/auto/auto_model.py)。省略时使用内部-1哨兵值(如上述非音频示例),此时 RTF 不代表有效速度测量。 |
| 公开返回值 | AutoModel.generate(...)返回展开后的结果列表。时间戳等附加字段由模型自行决定,注册并不承诺这些字段。VAD、标点、说话人和流式集成还需要额外兼容实现与独立测试。 |
| 训练 | 实现可微forward,命名张量参数应匹配 dataset collator。训练器解包(loss, stats, weight)(见 funasr/train_utils/trainer_ds.py);SenseVoice 和 FunASRNano 使用force_gatherable聚合统计量。玩具模型故意不实现训练 forward。 |
| 导出 | 导出工具调用模型的export方法,再使用export_dummy_inputs、输入输出名称、动态轴等模型专属方法(见 funasr/utils/export_utils.py)。注册本身不会实现这些方法。 |
3.1 附加能力的责任边界
自定义模型仍需自行实现所需的音频加载、特征处理、分词、解码及 batching 逻辑。可以参考相近模型的接口实现,但不能在缺少实现的情况下宣称具有其能力。例如:
- 想接 VAD 长音频切分:需要在
AutoModel传入vad_model,并保证你的模型能消费 VAD 切出的每个片段; - 想接标点恢复:需要兼容
punc_model的文本接口; - 想接说话人分离:需要
spk_model及spk_embedding输出约定(见 funasr/auto/auto_model.py 中说话人嵌入的收集逻辑); - 训练还需适配数据集与损失函数,参见 docs/training_zh.md。
4. 加载已审查代码与权重:两条路径
文档强调,加载模型有两条不同的路径,理解它们的差异是避免踩坑的关键。
4.1 路径一:直接注册构造
先导入模块(触发注册),再传注册键和model_conf,就像第 2 节的玩具示例一样:
from funasr import AutoModel import my_custom_model # 先导入模块完成注册 model = AutoModel( model="MyRegisteredName", model_conf={...}, # 关键:存在该键即跳过 hub 解析 device="cpu", disable_update=True, )当需要加载权重时,提供兼容的 tokenizer/frontend/配置以及真实存在的init_param。该路径不执行 hub 代码导入,因此remote_code在这里无效——你必须自己先完成模块导入。
4.2 路径二:模型目录解析
传已审查的本地目录或 hub ID,不传model_conf。此时加载器(funasr/download/download_model_from_hub.py)会:
- 解析名称别名(
name_maps_ms/name_maps_hf); - 从 ModelScope 或 HuggingFace 下载(若非本地路径);
- 读取目录中的
configuration.json文件元数据或config.yaml; - 解析出模型类名、tokenizer、frontend 配置并加载权重。
简单的本地config.yaml目录通常还需要model.pt以及配置引用的分词器、前端文件(例如tokens.txt/tokens.json/bpe.model/am.mvn,见 funasr/download/download_model_from_hub.py 的config.yaml分支)。任意 HF 权重文件夹不会自动成为 FunASR 模型目录——缺少 FunASR 约定的配置结构就无法被解析。
第二条路径的 ModelScope 接口示例:
from funasr import AutoModel model = AutoModel( model="./models/custom-asr", hub="ms", trust_remote_code=True, remote_code="./custom_asr_model.py", device="cpu", disable_update=True, ) print(model.generate(input="data/audio/heldout.wav"))这不是「无需准备即可运行」的例子:
models/custom-asr必须已有兼容且经过审查的配置和权重,custom_asr_model.py必须注册配置文件中指定的精确键。
4.3 remote_code 的动态导入行为
当前download_from_ms在trust_remote_code=True时会调用import_module_from_path(funasr/download/download_model_from_hub.py),remote_code未指定时默认为模块名model。导入器(funasr/utils/dynamic_import.py)的行为:
- 支持模块/文件路径,也支持 URL(http 前缀会先
download_from_url下载); - 把文件所在目录加入
sys.path后,按文件基本名importlib.import_module; - 相对路径按工作目录解析,不会自动相对权重目录解析;
- 文件基本名冲突及 Python 导入缓存可能选中已加载模块,应使用独立模块名并验证当前类;
- 该辅助函数打印导入异常而不重新抛出——因此应检查错误日志与注册结果,导入失败时
AutoModel才会在查表阶段报 "not registered"。
4.4 hub="hf" 的路径差异
hub="hf"路径与 ModelScope 不同:当前 checkout 的download_from_hf在信任开关下会安装模型目录的requirements.txt,但不调用import_module_from_path(可对照 funasr/download/download_model_from_hub.py,其中没有 remote_code 导入逻辑)。也就是说,不能假定hub="hf"会执行remote_code。两种正确做法:
- 在
hub="hf"构造前显式导入已审查模块; - 使用完整配置的直接注册构造路径(路径一)。
两条路径的本地config.yaml回退对init_param的处理也不同:
- ModelScope:保留已有且存在的显式路径(
if "init_param" not in kwargs or not os.path.exists(kwargs["init_param"])才回退到目录内model.pt,见 funasr/download/download_model_from_hub.py); - Hugging Face:指定目录内
model.pt(无条件覆盖init_param,见 funasr/download/download_model_from_hub.py)。
因此必须检查解析后的真实路径,不要假定 checkpoint 覆盖行为始终一致。
5. 真实示例与边界
5.1 SenseVoiceSmall:完整集成的参考
SenseVoice 实现是展示「注册 → 训练 → 推理 → 导出」完整集成的范本,但它有自己的模型配置与分词器假设。若以它为参考,需保持其配置结构与 tokenizer 约定,不能只抄注册写法就宣称拥有同等能力。
5.2 FunASRNano 的本地实现覆盖
examples/industrial_data_pretraining/fun_asr_nano/demo1.py 使用trust_remote_code=True、remote_code="./model.py"、hub="ms",并要求在 recipe 工作目录下运行。其本地实现注册FunASRNano(@tables.register("model_classes", "FunASRNano")),并导入同目录的ctc、tools模块——这会覆盖内置实现funasr/models/fun_asr_nano/model.py。两者并非对所有功能可互换,尤其不能假定保留内置 LoRA,应检查实际加载的类和权重键(这也是仓库中多个test_fun_asr_nano_*测试反复验证的主题)。
5.3 MOSS 适配器:第三方模型集成
funasr/models/moss_transcribe_diarize/model.py 集成第三方 OpenMOSS 模型,其forward明确拒绝训练。这印证了一个边界:注册模型不意味着支持微调或导出,能力以具体实现为准。
5.4 历史文档入口
原始注册教程和通用教程保留为历史入口;若示例与本文存在差异,以本文说明的当前源码行为为准。
6. 安全与验证
6.1 trust_remote_code 的安全边界
trust_remote_code=True允许执行 Python 代码;hub 加载器还可能安装模型目录的requirements.txt(download_from_ms与download_from_hf中都有install_requirements调用)。因此:
- 本地目录也不天然可信——审查源码、依赖与权重序列化方式;
- 使用隔离环境(容器 / 虚拟环境);
- 不加载不可信的 pickle checkpoint;
- 不要把不可信 URL、模块名或配置直接接入流程;
- 远程 revision 行为不可靠时,保留带哈希的已审查本地快照;这里的
model_revision并非所有加载路径都能统一固定版本(对比 funasr/download/download_model_from_hub.py 的"master"默认值与snapshot_download的 revision 参数使用)。
6.2 加载前与加载后的检查清单
- 加载前检查当前注册键、类和源码位置(
tables.print("model")可辅助); - 确认模型、配置、分词器兼容;
- 检查缺失或多余权重:
AutoModel的ignore_init_mismatch默认为True,直接传入不存在的init_param只打印错误、不保证构造失败(见 funasr/auto/auto_model.py)——应自行验证 checkpoint 存在; - 训练、导出、部署前分别测试单条、多条、异常输入与目标流水线;
- 注意:FunASR 软件的 MIT 许可证不替代模型或上游组件的许可(如 SenseVoice、OpenMOSS 等各自的协议)。
6.3 专项契约测试
运行仓库中的专项语法、仓库链接和不下载模型的玩具接口测试:
python -m pytest -q tests/test_training_docs_contract.py该测试覆盖:文档相对链接有效性、代码块语法(Python AST / JSON / bash)、中英文档示例一致性、加载器与注册表源码契约(import_module_from_path只在download_from_ms中被调用、model_conf分支存在、model.inference(**batch, **kwargs)调用约定等,见 tests/test_training_docs_contract.py)。
需要明确的是:这些检查不认证任意自定义代码的正确性、真实 ASR 质量、GPU 训练、真实 checkpoint 恢复或导出兼容性——它们只是保证文档与当前源码的接口约定不脱节。
7. 快速决策:我该用哪条路径?
| 场景 | 推荐路径 | 关键参数 |
|---|---|---|
| 本地实验、接口连通性验证 | 直接注册构造 | model=注册键+model_conf={} |
| 已审查的本地模型目录 | 模型目录解析 | model=目录路径+ 目录含config.yaml/configuration.json+model.pt |
| 加载 ModelScope 远程模型 | 模型目录解析 | hub="ms"+trust_remote_code=True+remote_code=模块路径 |
| 加载 HuggingFace 远程模型 | 模型目录解析 + 显式导入 | hub="hf",构造前先 import 已审查模块(remote_code 不自动执行) |
无论走哪条路径,都请记住:注册只是「名称 ↔ 实现」的绑定,真正的能力边界由你的inference/forward/export实现决定,而安全性永远取决于你对所加载代码与权重的审查程度。
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考