diff --git a/README.md b/README.md
index fe73cb013..1ee25962e 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,7 @@ FunASR has open-sourced a large number of pre-trained models on industrial data.
| fsmn-kws
( [⭐](https://modelscope.cn/models/iic/speech_charctc_kws_phone-xiaoyun/summary) ) | keyword spotting,streaming | 5000 hours, Mandarin | 0.7M |
| fa-zh
( [⭐](https://modelscope.cn/models/damo/speech_timestamp_prediction-v1-16k-offline/summary) [🤗](https://huggingface.co/funasr/fa-zh) ) | timestamp prediction | 5000 hours, Mandarin | 38M |
| cam++
( [⭐](https://modelscope.cn/models/iic/speech_campplus_sv_zh-cn_16k-common/summary) [🤗](https://huggingface.co/funasr/campplus) ) | speaker verification/diarization | 5000 hours | 7.2M |
+| ERes2NetV2
([⭐](examples/industrial_data_pretraining/eres2netv2_sv/demo.py) [⭐](https://modelscope.cn/models/iic/speech_eres2netv2_sv_zh-cn_16k-common/summary) ) | speaker verification/diarization | 5000 hours | 17.8M |
| Whisper-large-v3
([⭐](https://www.modelscope.cn/models/iic/Whisper-large-v3/summary) [🍀](https://github.com/openai/whisper) ) | speech recognition, with timestamps, non-streaming | multilingual | 1550 M |
| Whisper-large-v3-turbo
([⭐](https://www.modelscope.cn/models/iic/Whisper-large-v3-turbo/summary) [🍀](https://github.com/openai/whisper) ) | speech recognition, with timestamps, non-streaming | multilingual | 809 M |
| Qwen-Audio
([⭐](examples/industrial_data_pretraining/qwen_audio/demo.py) [🤗](https://huggingface.co/Qwen/Qwen-Audio) ) | audio-text multimodal models (pretraining) | multilingual | 8B |
diff --git a/README_zh.md b/README_zh.md
index 33dc13f0e..55a57eeef 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -123,6 +123,7 @@ FunASR 开源了大量在工业数据上预训练模型,您可以在[模型许
| fsmn-kws
( [⭐](https://modelscope.cn/models/iic/speech_charctc_kws_phone-xiaoyun/summary) ) | 语音唤醒,实时 | 5000 小时,中文 | 0.7M |
| fa-zh
( [⭐](https://modelscope.cn/models/damo/speech_timestamp_prediction-v1-16k-offline/summary) [🤗](https://huggingface.co/funasr/fa-zh) ) | 字级别时间戳预测 | 50000 小时,中文 | 38M |
| cam++
( [⭐](https://modelscope.cn/models/iic/speech_campplus_sv_zh-cn_16k-common/summary) [🤗](https://huggingface.co/funasr/campplus) ) | 说话人确认/分割 | 5000 小时 | 7.2M |
+| ERes2NetV2
([⭐](examples/industrial_data_pretraining/eres2netv2_sv/demo.py) [⭐](https://modelscope.cn/models/iic/speech_eres2netv2_sv_zh-cn_16k-common/summary) ) | 说话人确认/分割 | 5000小时 | 17.8M |
| Whisper-large-v3
([⭐](https://www.modelscope.cn/models/iic/Whisper-large-v3/summary) [🍀](https://github.com/openai/whisper) ) | 语音识别,带时间戳输出,非实时 | 多语言 | 1550 M |
| Whisper-large-v3-turbo
([⭐](https://www.modelscope.cn/models/iic/Whisper-large-v3-turbo/summary) [🍀](https://github.com/openai/whisper) ) | 语音识别,带时间戳输出,非实时 | 多语言 | 809 M |
| Qwen-Audio
([⭐](examples/industrial_data_pretraining/qwen_audio/demo.py) [🤗](https://huggingface.co/Qwen/Qwen-Audio) ) | 音频文本多模态大模型(预训练) | 多语言 | 8B |
diff --git a/examples/industrial_data_pretraining/eres2netv2_sv/demo.py b/examples/industrial_data_pretraining/eres2netv2_sv/demo.py
new file mode 100644
index 000000000..e44720f32
--- /dev/null
+++ b/examples/industrial_data_pretraining/eres2netv2_sv/demo.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# -*- encoding: utf-8 -*-
+# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
+# MIT License (https://opensource.org/licenses/MIT)
+
+from funasr import AutoModel
+
+# ASR with speaker diarization using ERes2NetV2
+model = AutoModel(
+ model="iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
+ vad_model="iic/speech_fsmn_vad_zh-cn-16k-common-pytorch",
+ vad_kwargs={"max_single_segment_time": 60000},
+ punc_model="iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch",
+ spk_model="iic/speech_eres2netv2_sv_zh-cn_16k-common",
+ device="cuda:0",
+)
+
+res = model.generate(
+ input="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
+ batch_size_s=300,
+)
+
+for sentence in res:
+ print(f"[Speaker {sentence.get('spk', '?')}] {sentence['text']}")
diff --git a/examples/industrial_data_pretraining/eres2netv2_sv/demo_sv.py b/examples/industrial_data_pretraining/eres2netv2_sv/demo_sv.py
new file mode 100644
index 000000000..506327021
--- /dev/null
+++ b/examples/industrial_data_pretraining/eres2netv2_sv/demo_sv.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# -*- encoding: utf-8 -*-
+# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
+# MIT License (https://opensource.org/licenses/MIT)
+
+from funasr import AutoModel
+
+# Standalone speaker verification / embedding extraction
+model = AutoModel(
+ model="iic/speech_eres2netv2_sv_zh-cn_16k-common",
+ device="cuda:0",
+)
+
+# Extract speaker embedding
+res = model.generate(
+ input="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
+)
+
+embedding = res[0]["spk_embedding"]
+print(f"Speaker embedding shape: {embedding.shape}")
diff --git a/funasr/models/eres2net/eres2netv2.py b/funasr/models/eres2net/eres2netv2.py
new file mode 100644
index 000000000..4720ab8c9
--- /dev/null
+++ b/funasr/models/eres2net/eres2netv2.py
@@ -0,0 +1,226 @@
+# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
+# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+import math
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import funasr.models.sond.pooling.pooling_layers as pooling_layers
+from funasr.models.eres2net.fusion import AFF
+
+
+class ReLU(nn.Hardtanh):
+
+ def __init__(self, inplace=False):
+ super(ReLU, self).__init__(0, 20, inplace)
+
+ def __repr__(self):
+ inplace_str = "inplace" if self.inplace else ""
+ return self.__class__.__name__ + " (" + inplace_str + ")"
+
+
+class BasicBlockERes2NetV2(nn.Module):
+
+ def __init__(self, in_planes, planes, stride=1, baseWidth=26, scale=2, expansion=2):
+ super(BasicBlockERes2NetV2, self).__init__()
+ width = int(math.floor(planes * (baseWidth / 64.0)))
+ self.conv1 = nn.Conv2d(in_planes, width * scale, kernel_size=1, stride=stride, bias=False)
+ self.bn1 = nn.BatchNorm2d(width * scale)
+ self.nums = scale
+ self.expansion = expansion
+
+ convs = []
+ bns = []
+ for i in range(self.nums):
+ convs.append(nn.Conv2d(width, width, kernel_size=3, padding=1, bias=False))
+ bns.append(nn.BatchNorm2d(width))
+ self.convs = nn.ModuleList(convs)
+ self.bns = nn.ModuleList(bns)
+ self.relu = ReLU(inplace=True)
+
+ self.conv3 = nn.Conv2d(width * scale, planes * self.expansion, kernel_size=1, bias=False)
+ self.bn3 = nn.BatchNorm2d(planes * self.expansion)
+ self.shortcut = nn.Sequential()
+ if stride != 1 or in_planes != self.expansion * planes:
+ self.shortcut = nn.Sequential(
+ nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
+ nn.BatchNorm2d(self.expansion * planes),
+ )
+ self.stride = stride
+ self.width = width
+ self.scale = scale
+
+ def forward(self, x):
+ residual = x
+
+ out = self.conv1(x)
+ out = self.bn1(out)
+ out = self.relu(out)
+ spx = torch.split(out, self.width, 1)
+ for i in range(self.nums):
+ if i == 0:
+ sp = spx[i]
+ else:
+ sp = sp + spx[i]
+ sp = self.convs[i](sp)
+ sp = self.relu(self.bns[i](sp))
+ if i == 0:
+ out = sp
+ else:
+ out = torch.cat((out, sp), 1)
+
+ out = self.conv3(out)
+ out = self.bn3(out)
+
+ residual = self.shortcut(x)
+ out += residual
+ out = self.relu(out)
+
+ return out
+
+
+class BasicBlockERes2NetV2AFF(nn.Module):
+
+ def __init__(self, in_planes, planes, stride=1, baseWidth=26, scale=2, expansion=2):
+ super(BasicBlockERes2NetV2AFF, self).__init__()
+ width = int(math.floor(planes * (baseWidth / 64.0)))
+ self.conv1 = nn.Conv2d(in_planes, width * scale, kernel_size=1, stride=stride, bias=False)
+ self.bn1 = nn.BatchNorm2d(width * scale)
+ self.nums = scale
+ self.expansion = expansion
+
+ convs = []
+ fuse_models = []
+ bns = []
+ for i in range(self.nums):
+ convs.append(nn.Conv2d(width, width, kernel_size=3, padding=1, bias=False))
+ bns.append(nn.BatchNorm2d(width))
+ for j in range(self.nums - 1):
+ fuse_models.append(AFF(channels=width, r=4))
+
+ self.convs = nn.ModuleList(convs)
+ self.bns = nn.ModuleList(bns)
+ self.fuse_models = nn.ModuleList(fuse_models)
+ self.relu = ReLU(inplace=True)
+
+ self.conv3 = nn.Conv2d(width * scale, planes * self.expansion, kernel_size=1, bias=False)
+ self.bn3 = nn.BatchNorm2d(planes * self.expansion)
+ self.shortcut = nn.Sequential()
+ if stride != 1 or in_planes != self.expansion * planes:
+ self.shortcut = nn.Sequential(
+ nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),
+ nn.BatchNorm2d(self.expansion * planes),
+ )
+ self.stride = stride
+ self.width = width
+ self.scale = scale
+
+ def forward(self, x):
+ residual = x
+
+ out = self.conv1(x)
+ out = self.bn1(out)
+ out = self.relu(out)
+ spx = torch.split(out, self.width, 1)
+ for i in range(self.nums):
+ if i == 0:
+ sp = spx[i]
+ else:
+ sp = self.fuse_models[i - 1](sp, spx[i])
+ sp = self.convs[i](sp)
+ sp = self.relu(self.bns[i](sp))
+ if i == 0:
+ out = sp
+ else:
+ out = torch.cat((out, sp), 1)
+
+ out = self.conv3(out)
+ out = self.bn3(out)
+
+ residual = self.shortcut(x)
+ out += residual
+ out = self.relu(out)
+
+ return out
+
+
+class ERes2NetV2(nn.Module):
+ def __init__(
+ self,
+ block=BasicBlockERes2NetV2,
+ block_fuse=BasicBlockERes2NetV2AFF,
+ num_blocks=[3, 4, 6, 3],
+ m_channels=64,
+ feat_dim=80,
+ embedding_size=192,
+ baseWidth=26,
+ scale=2,
+ expansion=2,
+ pooling_func="TSTP",
+ two_emb_layer=False,
+ ):
+ super(ERes2NetV2, self).__init__()
+ self.in_planes = m_channels
+ self.feat_dim = feat_dim
+ self.embedding_size = embedding_size
+ self.stats_dim = int(feat_dim / 8) * m_channels * 8
+ self.two_emb_layer = two_emb_layer
+ self.baseWidth = baseWidth
+ self.scale = scale
+ self.expansion = expansion
+
+ self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
+ self.bn1 = nn.BatchNorm2d(m_channels)
+ self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=1)
+ self.layer2 = self._make_layer(block, m_channels * 2, num_blocks[1], stride=2)
+ self.layer3 = self._make_layer(block_fuse, m_channels * 4, num_blocks[2], stride=2)
+ self.layer4 = self._make_layer(block_fuse, m_channels * 8, num_blocks[3], stride=2)
+
+ self.layer3_ds = nn.Conv2d(
+ m_channels * 4 * self.expansion, m_channels * 8 * self.expansion,
+ kernel_size=3, padding=1, stride=2, bias=False,
+ )
+ self.fuse34 = AFF(channels=m_channels * 8 * self.expansion, r=4)
+
+ self.n_stats = 1 if pooling_func == "TAP" or pooling_func == "TSDP" else 2
+ self.pool = getattr(pooling_layers, pooling_func)(in_dim=self.stats_dim * self.expansion)
+ self.seg_1 = nn.Linear(self.stats_dim * self.expansion * self.n_stats, embedding_size)
+ if self.two_emb_layer:
+ self.seg_bn_1 = nn.BatchNorm1d(embedding_size, affine=False)
+ self.seg_2 = nn.Linear(embedding_size, embedding_size)
+ else:
+ self.seg_bn_1 = nn.Identity()
+ self.seg_2 = nn.Identity()
+
+ def _make_layer(self, block, planes, num_blocks, stride):
+ strides = [stride] + [1] * (num_blocks - 1)
+ layers = []
+ for stride in strides:
+ layers.append(
+ block(self.in_planes, planes, stride, baseWidth=self.baseWidth, scale=self.scale, expansion=self.expansion)
+ )
+ self.in_planes = planes * self.expansion
+ return nn.Sequential(*layers)
+
+ def forward(self, x):
+ x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
+ x = x.unsqueeze_(1)
+ out = F.relu(self.bn1(self.conv1(x)))
+ out1 = self.layer1(out)
+ out2 = self.layer2(out1)
+ out3 = self.layer3(out2)
+ out4 = self.layer4(out3)
+ out3_ds = self.layer3_ds(out3)
+ fuse_out34 = self.fuse34(out4, out3_ds)
+ stats = self.pool(fuse_out34)
+
+ embed_a = self.seg_1(stats)
+ if self.two_emb_layer:
+ out = F.relu(embed_a)
+ out = self.seg_bn_1(out)
+ embed_b = self.seg_2(out)
+ return embed_b
+ else:
+ return embed_a
diff --git a/funasr/models/eres2net/model.py b/funasr/models/eres2net/model.py
new file mode 100644
index 000000000..e2549a2b7
--- /dev/null
+++ b/funasr/models/eres2net/model.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# -*- encoding: utf-8 -*-
+# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
+# MIT License (https://opensource.org/licenses/MIT)
+# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
+
+import os
+import time
+import logging
+import torch
+import numpy as np
+
+from funasr.register import tables
+from funasr.models.campplus.utils import extract_feature
+from funasr.utils.load_utils import load_audio_text_image_video
+from funasr.models.eres2net.eres2netv2 import ERes2NetV2
+
+
+@tables.register("model_classes", "ERes2NetV2")
+@tables.register("model_classes", "iic/speech_eres2netv2_sv_zh-cn_16k-common")
+class ERes2NetV2SV(torch.nn.Module):
+ def __init__(
+ self,
+ feat_dim=80,
+ embedding_size=192,
+ m_channels=64,
+ baseWidth=26,
+ scale=2,
+ expansion=2,
+ num_blocks=[3, 4, 6, 3],
+ pooling_func="TSTP",
+ two_emb_layer=False,
+ **kwargs,
+ ):
+ super().__init__()
+ self.model = ERes2NetV2(
+ feat_dim=feat_dim,
+ embedding_size=embedding_size,
+ m_channels=m_channels,
+ baseWidth=baseWidth,
+ scale=scale,
+ expansion=expansion,
+ num_blocks=num_blocks,
+ pooling_func=pooling_func,
+ two_emb_layer=two_emb_layer,
+ )
+ self.embedding_size = embedding_size
+
+ model_path = kwargs.get("model_path", None)
+ init_param = kwargs.get("init_param", None)
+ if init_param is None and model_path is not None:
+ ckpt = os.path.join(model_path, "pretrained_eres2netv2.ckpt")
+ if os.path.exists(ckpt):
+ init_param = ckpt
+ if init_param is not None and os.path.exists(init_param):
+ self._load_pretrained(init_param)
+
+ def _load_pretrained(self, path):
+ state_dict = torch.load(path, map_location="cpu")
+ if "state_dict" in state_dict:
+ state_dict = state_dict["state_dict"]
+ missing, unexpected = self.model.load_state_dict(state_dict, strict=False)
+ if missing:
+ logging.warning(f"ERes2NetV2 missing keys: {missing[:5]}...")
+ logging.info(f"ERes2NetV2 loaded pretrained weights from {path}")
+
+ def forward(self, x):
+ return self.model(x)
+
+ def inference(
+ self,
+ data_in,
+ data_lengths=None,
+ key: list = None,
+ tokenizer=None,
+ frontend=None,
+ **kwargs,
+ ):
+ meta_data = {}
+ time1 = time.perf_counter()
+ audio_sample_list = load_audio_text_image_video(
+ data_in, fs=16000, audio_fs=kwargs.get("fs", 16000), data_type="sound"
+ )
+ time2 = time.perf_counter()
+ meta_data["load_data"] = f"{time2 - time1:0.3f}"
+ speech, speech_lengths, speech_times = extract_feature(audio_sample_list)
+ speech = speech.to(device=kwargs["device"])
+ time3 = time.perf_counter()
+ meta_data["extract_feat"] = f"{time3 - time2:0.3f}"
+ meta_data["batch_data_time"] = np.array(speech_times).sum().item() / 16000.0
+ results = [{"spk_embedding": self.forward(speech.to(torch.float32))}]
+ return results, meta_data
diff --git a/tests_models/run_all_tests.py b/tests_models/run_all_tests.py
index 9ad5abb05..bf7ee8acc 100644
--- a/tests_models/run_all_tests.py
+++ b/tests_models/run_all_tests.py
@@ -18,6 +18,7 @@ tests = [
"test_paraformer_streaming.py",
"test_qwen3_asr.py",
"test_glm_asr.py",
+ "test_eres2netv2.py",
]
SEP = "-" * 60
diff --git a/tests_models/test_eres2netv2.py b/tests_models/test_eres2netv2.py
new file mode 100644
index 000000000..6651d5892
--- /dev/null
+++ b/tests_models/test_eres2netv2.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""Test ERes2NetV2: speaker verification/diarization with ASR"""
+import sys
+import time
+
+
+def main():
+ from funasr import AutoModel
+
+ url_zh = "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
+
+ # Test 1: ERes2NetV2 as spk_model combined with VAD + ASR
+ print("[ERes2NetV2] Loading model with VAD + ASR + SPK...")
+ t0 = time.time()
+ model = AutoModel(
+ model="iic/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
+ vad_model="iic/speech_fsmn_vad_zh-cn-16k-common-pytorch",
+ vad_kwargs={"max_single_segment_time": 60000},
+ spk_model="iic/speech_eres2netv2_sv_zh-cn_16k-common",
+ device="cuda:0",
+ disable_update=True,
+ )
+ print("[ERes2NetV2] Model loaded in %.1fs" % (time.time() - t0))
+
+ # Test with speaker diarization
+ print("[ERes2NetV2] Running inference with speaker diarization...")
+ t0 = time.time()
+ res = model.generate(
+ input=url_zh,
+ batch_size_s=300,
+ )
+ print("[ERes2NetV2] Inference done in %.1fs" % (time.time() - t0))
+
+ if res and len(res) > 0 and "text" in res[0]:
+ print("[ERes2NetV2] Result: %s" % res[0]["text"])
+ if "spk" in res[0]:
+ print("[ERes2NetV2] Speaker: %s" % res[0]["spk"])
+ print("[ERes2NetV2] Test 1 PASSED")
+ else:
+ print("[ERes2NetV2] Test 1 FAILED - no text in result")
+ return 1
+
+ # Test 2: ERes2NetV2 standalone speaker embedding
+ print("[ERes2NetV2] Test 2: Standalone speaker embedding...")
+ t0 = time.time()
+ spk_model = AutoModel(
+ model="iic/speech_eres2netv2_sv_zh-cn_16k-common",
+ device="cuda:0",
+ disable_update=True,
+ )
+ res = spk_model.generate(input=url_zh)
+ print("[ERes2NetV2] Embedding done in %.1fs" % (time.time() - t0))
+
+ if res and len(res) > 0 and "spk_embedding" in res[0]:
+ emb = res[0]["spk_embedding"]
+ print("[ERes2NetV2] Embedding shape: %s" % str(emb.shape))
+ print("[ERes2NetV2] Test 2 PASSED")
+ else:
+ print("[ERes2NetV2] Test 2 FAILED - no spk_embedding in result")
+ return 1
+
+ print("[ERes2NetV2] All tests PASSED")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())