chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from fnmatch import fnmatch
|
||||
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
|
||||
def gather_test_cases(test_dir, pattern, list_tests):
|
||||
case_list = []
|
||||
for dirpath, dirnames, filenames in os.walk(test_dir):
|
||||
for file in filenames:
|
||||
if fnmatch(file, pattern):
|
||||
case_list.append(file)
|
||||
|
||||
test_suite = unittest.TestSuite()
|
||||
|
||||
for case in case_list:
|
||||
test_case = unittest.defaultTestLoader.discover(start_dir=test_dir, pattern=case)
|
||||
test_suite.addTest(test_case)
|
||||
if hasattr(test_case, "__iter__"):
|
||||
for subcase in test_case:
|
||||
if list_tests:
|
||||
print(subcase)
|
||||
else:
|
||||
if list_tests:
|
||||
print(test_case)
|
||||
return test_suite
|
||||
|
||||
|
||||
def main(args):
|
||||
runner = unittest.TextTestRunner()
|
||||
test_suite = gather_test_cases(os.path.abspath(args.test_dir), args.pattern, args.list_tests)
|
||||
if not args.list_tests:
|
||||
result = runner.run(test_suite)
|
||||
if len(result.failures) > 0:
|
||||
sys.exit(len(result.failures))
|
||||
if len(result.errors) > 0:
|
||||
sys.exit(len(result.errors))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser("test runner")
|
||||
parser.add_argument("--list_tests", action="store_true", help="list all tests")
|
||||
parser.add_argument("--pattern", default="test_*.py", help="test file pattern")
|
||||
parser.add_argument("--test_dir", default="tests", help="directory to be tested")
|
||||
parser.add_argument("--disable_profile", action="store_true", help="disable profiling")
|
||||
args = parser.parse_args()
|
||||
print(f"working dir: {os.getcwd()}")
|
||||
main(args)
|
||||
@@ -0,0 +1,610 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestConformerInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_aishell1(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_conformer_asr_nat-zh-cn-16k-aishell1-vocab4234-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_aishell2(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_conformer_asr_nat-zh-cn-16k-aishell2-vocab5212-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
class TestData2vecInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_transformer(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_data2vec_pretrain-zh-cn-aishell2-16k-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://modelscope.oss-cn-beijing.aliyuncs.com/test/audios/asr_example.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "每一天都要快乐喔"
|
||||
|
||||
def test_paraformer(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_data2vec_pretrain-paraformer-zh-cn-aishell2-16k",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://modelscope.oss-cn-beijing.aliyuncs.com/test/audios/asr_example.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "每一天都要快乐喔"
|
||||
|
||||
|
||||
class TestMfccaInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_alimeeting(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="NPU-ASLP/speech_mfcca_asr-zh-cn-16k-alimeeting-vocab4950",
|
||||
model_revision="v3.0.0",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://pre.modelscope.cn/api/v1/models/NPU-ASLP/speech_mfcca_asr-zh-cn-16k-alimeeting-vocab4950/repo?Revision=master&FilePath=example/asr_example_mc.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
class TestParaformerInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_paraformer_large_contextual_common(self):
|
||||
param_dict = dict()
|
||||
param_dict["hotword"] = (
|
||||
"https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/hotword.txt"
|
||||
)
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large-contextual_asr_nat-zh-cn-16k-common-vocab8404",
|
||||
param_dict=param_dict,
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_hotword.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "国务院发展研究中心市场经济研究所副所长邓郁松认为"
|
||||
|
||||
def test_paraformer_large_aishell1(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large_asr_nat-zh-cn-16k-aishell1-vocab8404-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型"
|
||||
|
||||
def test_paraformer_large_aishell2(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large_asr_nat-zh-cn-16k-aishell2-vocab8404-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型"
|
||||
|
||||
def test_paraformer_large_common(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型"
|
||||
|
||||
def test_paraformer_large_online_common(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online",
|
||||
model_revision="v1.0.6",
|
||||
update_model=False,
|
||||
mode="paraformer_fake_streaming",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型"
|
||||
|
||||
def test_paraformer_online_common(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer_asr_nat-zh-cn-16k-common-vocab8404-online",
|
||||
model_revision="v1.0.6",
|
||||
update_model=False,
|
||||
mode="paraformer_fake_streaming",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型"
|
||||
|
||||
def test_paraformer_tiny_commandword(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-tiny-commandword_asr_nat-zh-cn-16k-vocab544-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh_command.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_paraformer_8k(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer_asr_nat-zh-cn-8k-common-vocab8358-tensorflow1",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_8K.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_paraformer_aishell1(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer_asr_nat-zh-cn-16k-aishell1-vocab4234-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_paraformer_aishell2(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer_asr_nat-zh-cn-16k-aishell2-vocab5212-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
class TestParaformerBertInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_aishell1(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformerbert_asr_nat-zh-cn-16k-aishell1-vocab4234-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://modelscope.oss-cn-beijing.aliyuncs.com/test/audios/asr_example.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_aishell2(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformerbert_asr_nat-zh-cn-16k-aishell2-vocab5212-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://modelscope.oss-cn-beijing.aliyuncs.com/test/audios/asr_example.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
class TestUniasrInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_uniasr_2pass_cantonese_chs_16k_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-cantonese-CHS-16k-common-vocab1468-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_cantonese-CHS.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_cantonese_chs_16k_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-cantonese-CHS-16k-common-vocab1468-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_cantonese-CHS.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_cn_dialect_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-cn-dialect-16k-vocab8358-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_cn_dialect_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-cn-dialect-16k-vocab8358-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_de_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-de-16k-common-vocab3690-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_de.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_de_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-de-16k-common-vocab3690-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_de.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_en_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-en-16k-common-vocab1080-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_en.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_en_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-en-16k-common-vocab1080-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_en.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_es_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-es-16k-common-vocab3445-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_es.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_es_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-es-16k-common-vocab3445-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_es.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_fa_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-fa-16k-common-vocab1257-pytorch-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_fa.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_fa_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-fa-16k-common-vocab1257-pytorch-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_fa.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_fr_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-fr-16k-common-vocab3472-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_fr.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_fr_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-fr-16k-common-vocab3472-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_fr.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_id_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-id-16k-common-vocab1067-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_id.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_id_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-id-16k-common-vocab1067-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_id.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ja_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ja-16k-common-vocab93-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ja.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ja_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ja-16k-common-vocab93-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ja.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ko_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ko-16k-common-vocab6400-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ko.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ko_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ko-16k-common-vocab6400-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ko.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_minnan_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-minnan-16k-common-vocab3825",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_pt_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-pt-16k-common-vocab1617-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_pt.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_pt_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-pt-16k-common-vocab1617-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_pt.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ru_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ru-16k-common-vocab1664-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ru.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_ru_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-ru-16k-common-vocab1664-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_ru.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_vi_common_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-vi-16k-common-vocab1001-pytorch-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_vi.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_vi_common_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-vi-16k-common-vocab1001-pytorch-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_vi.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_8k_common_vocab3445_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-8k-common-vocab3445-pytorch-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_8k_common_vocab3445_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-8k-common-vocab3445-pytorch-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_8k_common_vocab8358_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-8k-common-vocab8358-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_8k_common_vocab8358_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-8k-common-vocab8358-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_16k_common_vocab8358_offline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-16k-common-vocab8358-tensorflow1-offline",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "offline"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
def test_uniasr_2pass_zhcn_16k_common_vocab8358_online(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_UniASR_asr_2pass-zh-cn-16k-common-vocab8358-tensorflow1-online",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav",
|
||||
param_dict={"decoding_model": "normal"},
|
||||
)
|
||||
logger.info("asr inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,36 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestParaformerInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_inference_pipeline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.auto_speech_recognition,
|
||||
model="damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
|
||||
model_revision="v1.2.1",
|
||||
vad_model="damo/speech_fsmn_vad_zh-cn-16k-common-pytorch",
|
||||
vad_model_revision="v1.1.8",
|
||||
punc_model="damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch",
|
||||
punc_model_revision="v1.1.6",
|
||||
ngpu=1,
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
|
||||
)
|
||||
logger.info("asr_vad_punc inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == "欢迎大家来体验达摩院推出的语音识别模型。"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,154 @@
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import funasr
|
||||
from funasr.auto.auto_model import AutoModel
|
||||
|
||||
|
||||
class TestAutoModel(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.base_kwargs = {
|
||||
"model": "damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
|
||||
"vad_model": "fsmn-vad",
|
||||
"punc_model": "ct-punc",
|
||||
"device": "cpu",
|
||||
"batch_size": 1,
|
||||
"disable_update": True,
|
||||
}
|
||||
|
||||
def test_merge_thr_in_cb_model(self):
|
||||
kwargs = self.base_kwargs.copy()
|
||||
kwargs["spk_model"] = "cam++"
|
||||
merge_thr = 0.5
|
||||
kwargs["spk_kwargs"] = {"cb_kwargs": {"merge_thr": merge_thr}}
|
||||
model = AutoModel(**kwargs)
|
||||
self.assertEqual(model.cb_model.model_config['merge_thr'], merge_thr)
|
||||
# res = model.generate(input="/test.wav",
|
||||
# batch_size_s=300)
|
||||
|
||||
def test_progress_callback_called(self):
|
||||
class DummyModel:
|
||||
def __init__(self):
|
||||
self.param = torch.nn.Parameter(torch.zeros(1))
|
||||
|
||||
def parameters(self):
|
||||
return iter([self.param])
|
||||
|
||||
def eval(self):
|
||||
pass
|
||||
|
||||
def inference(self, data_in=None, **kwargs):
|
||||
results = [{"text": str(d)} for d in data_in]
|
||||
return results, {"batch_data_time": 1}
|
||||
|
||||
am = AutoModel.__new__(AutoModel)
|
||||
am.model = DummyModel()
|
||||
am.kwargs = {"batch_size": 2, "disable_pbar": True}
|
||||
|
||||
progress = []
|
||||
|
||||
res = AutoModel.inference(
|
||||
am,
|
||||
["a", "b", "c"],
|
||||
progress_callback=lambda idx, total: progress.append((idx, total)),
|
||||
)
|
||||
|
||||
self.assertEqual(len(progress), 2)
|
||||
self.assertEqual(progress, [(2, 3), (3, 3)])
|
||||
|
||||
def test_import_submodules_records_failures(self):
|
||||
old_errors = dict(funasr._IMPORT_ERRORS)
|
||||
old_tracebacks = dict(funasr._IMPORT_ERROR_TRACEBACKS)
|
||||
package_name = "funasr_import_probe_pkg"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
package_dir = Path(tmp_dir) / package_name
|
||||
package_dir.mkdir()
|
||||
(package_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
(package_dir / "broken.py").write_text(
|
||||
'raise RuntimeError("boom optional dependency")\n', encoding="utf-8"
|
||||
)
|
||||
sys.path.insert(0, tmp_dir)
|
||||
try:
|
||||
funasr._IMPORT_ERRORS.clear()
|
||||
funasr._IMPORT_ERROR_TRACEBACKS.clear()
|
||||
package = importlib.import_module(package_name)
|
||||
|
||||
imported = funasr.import_submodules(package)
|
||||
|
||||
failed_module = f"{package_name}.broken"
|
||||
self.assertEqual(imported, {})
|
||||
self.assertIn(failed_module, funasr._IMPORT_ERRORS)
|
||||
self.assertIn("boom optional dependency", funasr._IMPORT_ERRORS[failed_module])
|
||||
self.assertIn(failed_module, funasr._IMPORT_ERROR_TRACEBACKS)
|
||||
finally:
|
||||
sys.path.remove(tmp_dir)
|
||||
for module_name in list(sys.modules):
|
||||
if module_name == package_name or module_name.startswith(package_name + "."):
|
||||
sys.modules.pop(module_name, None)
|
||||
funasr._IMPORT_ERRORS.clear()
|
||||
funasr._IMPORT_ERRORS.update(old_errors)
|
||||
funasr._IMPORT_ERROR_TRACEBACKS.clear()
|
||||
funasr._IMPORT_ERROR_TRACEBACKS.update(old_tracebacks)
|
||||
|
||||
def test_import_submodules_strict_import_raises(self):
|
||||
old_strict_import = funasr._STRICT_IMPORT
|
||||
package_name = "funasr_import_strict_probe_pkg"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
package_dir = Path(tmp_dir) / package_name
|
||||
package_dir.mkdir()
|
||||
(package_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
(package_dir / "broken.py").write_text(
|
||||
'raise RuntimeError("strict optional dependency")\n', encoding="utf-8"
|
||||
)
|
||||
sys.path.insert(0, tmp_dir)
|
||||
try:
|
||||
funasr._STRICT_IMPORT = True
|
||||
package = importlib.import_module(package_name)
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "strict optional dependency"):
|
||||
funasr.import_submodules(package)
|
||||
finally:
|
||||
funasr._STRICT_IMPORT = old_strict_import
|
||||
sys.path.remove(tmp_dir)
|
||||
for module_name in list(sys.modules):
|
||||
if module_name == package_name or module_name.startswith(package_name + "."):
|
||||
sys.modules.pop(module_name, None)
|
||||
|
||||
def test_unregistered_model_reports_import_failures(self):
|
||||
old_errors = dict(funasr._IMPORT_ERRORS)
|
||||
try:
|
||||
funasr._IMPORT_ERRORS.clear()
|
||||
funasr._IMPORT_ERRORS["funasr.models.fake_model"] = (
|
||||
"ModuleNotFoundError: No module named 'fake_dep'"
|
||||
)
|
||||
|
||||
with self.assertRaises(RuntimeError) as context:
|
||||
AutoModel.build_model(
|
||||
model="missing_model",
|
||||
model_conf={},
|
||||
device="cpu",
|
||||
ncpu=1,
|
||||
)
|
||||
|
||||
message = str(context.exception)
|
||||
self.assertIn("missing_model", message)
|
||||
self.assertIn("funasr.models.fake_model", message)
|
||||
self.assertIn("fake_dep", message)
|
||||
self.assertIn("FUNASR_IMPORT_DEBUG", message)
|
||||
self.assertIn("FUNASR_STRICT_IMPORT", message)
|
||||
finally:
|
||||
funasr._IMPORT_ERRORS.clear()
|
||||
funasr._IMPORT_ERRORS.update(old_errors)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,41 @@
|
||||
import io
|
||||
import sys
|
||||
import types
|
||||
from contextlib import redirect_stdout
|
||||
from unittest.mock import patch
|
||||
|
||||
from funasr import cli
|
||||
|
||||
|
||||
class DummyAutoModel:
|
||||
instances = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.instances.append(self)
|
||||
|
||||
def generate(self, **kwargs):
|
||||
return [{"text": "hello"}]
|
||||
|
||||
|
||||
def test_cli_passes_hub_to_auto_model(tmp_path):
|
||||
audio_path = tmp_path / "sample.wav"
|
||||
audio_path.write_bytes(b"not a real wav")
|
||||
fake_torch = types.SimpleNamespace(
|
||||
cuda=types.SimpleNamespace(is_available=lambda: False),
|
||||
)
|
||||
|
||||
DummyAutoModel.instances = []
|
||||
argv = ["funasr", "--hub", "hf", str(audio_path)]
|
||||
|
||||
with (
|
||||
patch.object(sys, "argv", argv),
|
||||
patch.dict(sys.modules, {"torch": fake_torch}),
|
||||
patch("funasr.AutoModel", DummyAutoModel),
|
||||
redirect_stdout(io.StringIO()) as stdout,
|
||||
):
|
||||
cli.main()
|
||||
|
||||
assert DummyAutoModel.instances[0].kwargs["hub"] == "hf"
|
||||
assert stdout.getvalue().strip() == "hello"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.models.fsmn_vad_streaming import model as vad_model
|
||||
|
||||
|
||||
class TestFsmnVadDynamicSilence(unittest.TestCase):
|
||||
def _run_inference(self, **kwargs):
|
||||
vad = vad_model.FsmnVADStreaming.__new__(vad_model.FsmnVADStreaming)
|
||||
vad.vad_opts = SimpleNamespace(speech_to_sil_time_thres=100)
|
||||
vad.forward = lambda **batch: []
|
||||
|
||||
cache = {
|
||||
"frontend": {},
|
||||
"prev_samples": torch.empty(0),
|
||||
"encoder": {},
|
||||
"stats": SimpleNamespace(
|
||||
vad_state_machine=vad_model.VadStateMachine.kVadInStateInSpeechSegment,
|
||||
max_end_sil_frame_cnt_thresh=200,
|
||||
speech_noise_thres=0.6,
|
||||
),
|
||||
}
|
||||
frontend = SimpleNamespace(fs=16000, frame_shift=10, lfr_n=1)
|
||||
|
||||
def fake_extract_fbank(*args, **kwargs):
|
||||
cache["frontend"]["waveforms"] = torch.zeros(1, 16000)
|
||||
return torch.zeros(1, 1, 80), torch.tensor([100])
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
vad_model,
|
||||
"load_audio_text_image_video",
|
||||
return_value=[torch.zeros(16000)],
|
||||
),
|
||||
patch.object(vad_model, "extract_fbank", side_effect=fake_extract_fbank),
|
||||
):
|
||||
vad_model.FsmnVADStreaming.inference(
|
||||
vad,
|
||||
torch.zeros(16000),
|
||||
frontend=frontend,
|
||||
cache=cache,
|
||||
key=["utt"],
|
||||
chunk_size=1000,
|
||||
is_final=False,
|
||||
device="cpu",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return cache
|
||||
|
||||
def test_explicit_max_end_silence_time_keeps_fixed_threshold_by_default(self):
|
||||
cache = self._run_inference(max_end_silence_time=300)
|
||||
|
||||
self.assertEqual(cache["stats"].max_end_sil_frame_cnt_thresh, 200)
|
||||
self.assertNotIn("_dynamic_accumulated_ms", cache)
|
||||
|
||||
def test_explicit_dynamic_silence_still_enables_schedule(self):
|
||||
cache = self._run_inference(max_end_silence_time=300, dynamic_silence=True)
|
||||
|
||||
self.assertEqual(cache["stats"].max_end_sil_frame_cnt_thresh, 1900)
|
||||
self.assertEqual(cache["_dynamic_accumulated_ms"], 1000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,54 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEVICE_UTILS_PATH = REPO_ROOT / "funasr" / "models" / "fun_asr_nano" / "device_utils.py"
|
||||
PACKAGE_MODEL_PATH = REPO_ROOT / "funasr" / "models" / "fun_asr_nano" / "model.py"
|
||||
EXAMPLE_MODEL_PATH = (
|
||||
REPO_ROOT / "examples" / "industrial_data_pretraining" / "fun_asr_nano" / "model.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_device_utils():
|
||||
spec = importlib.util.spec_from_file_location("fun_asr_nano_device_utils", DEVICE_UTILS_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_resolve_autocast_device_type_keeps_npu_backend():
|
||||
device_utils = _load_device_utils()
|
||||
|
||||
assert device_utils.resolve_autocast_device_type("npu:1") == "npu"
|
||||
assert device_utils.resolve_autocast_device_type("npu") == "npu"
|
||||
assert device_utils.resolve_autocast_device_type("cuda:0") == "cuda"
|
||||
assert device_utils.resolve_autocast_device_type("xpu:0") == "xpu"
|
||||
assert device_utils.resolve_autocast_device_type("mps") == "mps"
|
||||
assert device_utils.resolve_autocast_device_type("cpu") == "cpu"
|
||||
assert device_utils.resolve_autocast_device_type("unknown:0") == "cpu"
|
||||
|
||||
|
||||
def test_device_type_from_value_treats_none_as_cpu():
|
||||
device_utils = _load_device_utils()
|
||||
|
||||
assert device_utils._device_type_from_value(None) == "cpu"
|
||||
|
||||
|
||||
def test_resolve_autocast_device_type_accepts_device_like_objects():
|
||||
device_utils = _load_device_utils()
|
||||
|
||||
class DeviceLike:
|
||||
type = "npu"
|
||||
|
||||
assert device_utils.resolve_autocast_device_type(DeviceLike()) == "npu"
|
||||
|
||||
|
||||
def test_fun_asr_nano_autocast_calls_use_shared_resolver():
|
||||
old_inline_fallback = 'device_type if device_type in ["cuda", "xpu", "mps"] else "cpu"'
|
||||
|
||||
for path in (PACKAGE_MODEL_PATH, EXAMPLE_MODEL_PATH):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert old_inline_fallback not in source
|
||||
assert source.count("resolve_autocast_device_type(") >= 2
|
||||
@@ -0,0 +1,103 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import torch
|
||||
|
||||
from funasr.models.fun_asr_nano import model as nano_model
|
||||
|
||||
|
||||
class _FakeLLM:
|
||||
def __init__(self):
|
||||
self.config = SimpleNamespace(pad_token_id=None, eos_token_id=0)
|
||||
self.calls = 0
|
||||
|
||||
def to(self, _dtype):
|
||||
return self
|
||||
|
||||
def generate(self, **_kwargs):
|
||||
self.calls += 1
|
||||
return torch.tensor([[10 + self.calls]], dtype=torch.long)
|
||||
|
||||
|
||||
class _FakeTokenizer:
|
||||
def batch_decode(self, generated_ids, **_kwargs):
|
||||
return [f"text-{int(generated_ids[0, -1])}"]
|
||||
|
||||
|
||||
class _FakeCTCDecoder:
|
||||
def __call__(self, encoder_out, encoder_out_lens):
|
||||
logits = torch.tensor(
|
||||
[[[0.0, 2.0, 0.0], [0.0, 0.0, 3.0]]],
|
||||
dtype=torch.float32,
|
||||
)
|
||||
return logits, encoder_out_lens
|
||||
|
||||
|
||||
class _FakeCTC:
|
||||
def log_softmax(self, decoder_out):
|
||||
return decoder_out
|
||||
|
||||
|
||||
class _FakeCTCTokenizer:
|
||||
def decode(self, token_ids):
|
||||
return "".join(str(token_id) for token_id in token_ids)
|
||||
|
||||
def encode(self, text):
|
||||
return [1] if text else []
|
||||
|
||||
|
||||
def test_ctc_decoder_multi_segment_input_uses_single_segment_fallback(monkeypatch):
|
||||
instance = object.__new__(nano_model.FunASRNano)
|
||||
instance.ctc_decoder = _FakeCTCDecoder()
|
||||
instance.ctc = _FakeCTC()
|
||||
instance.ctc_tokenizer = _FakeCTCTokenizer()
|
||||
instance.blank_id = 0
|
||||
instance.llm = _FakeLLM()
|
||||
|
||||
prepare_calls = []
|
||||
|
||||
def fake_inference_prepare(
|
||||
data_in,
|
||||
data_lengths=None,
|
||||
key=None,
|
||||
tokenizer=None,
|
||||
frontend=None,
|
||||
**_kwargs,
|
||||
):
|
||||
if len(data_in) > 1:
|
||||
raise NotImplementedError("batch decoding is not implemented")
|
||||
prepare_calls.append((data_in[0], key[0]))
|
||||
return (
|
||||
torch.zeros(1, 2, 4),
|
||||
{"assistant": [f"label-{data_in[0]}"]},
|
||||
{"attention_mask": torch.ones(1, 2, dtype=torch.long)},
|
||||
torch.empty(1, 0, dtype=torch.long),
|
||||
{
|
||||
"encoder_out": torch.zeros(1, 2, 3),
|
||||
"encoder_out_lens": torch.tensor([2]),
|
||||
"batch_data_time": 1.0,
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(instance, "inference_prepare", fake_inference_prepare)
|
||||
monkeypatch.setattr(
|
||||
nano_model,
|
||||
"forced_align",
|
||||
lambda _logits, target_ids, _blank_id: [
|
||||
{"token": int(target_ids[0]), "start_time": 0.0, "end_time": 1.0}
|
||||
],
|
||||
)
|
||||
|
||||
results, meta = instance.inference_llm(
|
||||
["seg-a", "seg-b"],
|
||||
key=["key-a", "key-b"],
|
||||
tokenizer=_FakeTokenizer(),
|
||||
frontend=None,
|
||||
device="cpu",
|
||||
llm_dtype="fp32",
|
||||
)
|
||||
|
||||
assert prepare_calls == [("seg-a", "key-a"), ("seg-b", "key-b")]
|
||||
assert [result["key"] for result in results] == ["key-a", "key-b"]
|
||||
assert [result["text"] for result in results] == ["text-11", "text-12"]
|
||||
assert all("timestamps" in result for result in results)
|
||||
assert meta["batch_data_time"] == 2.0
|
||||
@@ -0,0 +1,153 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SERVICE_PATH = (
|
||||
REPO_ROOT
|
||||
/ "examples"
|
||||
/ "industrial_data_pretraining"
|
||||
/ "fun_asr_nano"
|
||||
/ "serve_vllm.py"
|
||||
)
|
||||
|
||||
|
||||
def load_service_module(monkeypatch):
|
||||
fastapi_stub = types.ModuleType("fastapi")
|
||||
|
||||
class FastAPIStub:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def on_event(self, *args, **kwargs):
|
||||
return lambda func: func
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return lambda func: func
|
||||
|
||||
def websocket(self, *args, **kwargs):
|
||||
return lambda func: func
|
||||
|
||||
fastapi_stub.FastAPI = FastAPIStub
|
||||
fastapi_stub.File = lambda *args, **kwargs: None
|
||||
fastapi_stub.Form = lambda *args, **kwargs: None
|
||||
fastapi_stub.UploadFile = object
|
||||
fastapi_stub.WebSocket = object
|
||||
fastapi_stub.WebSocketDisconnect = Exception
|
||||
|
||||
responses_stub = types.ModuleType("fastapi.responses")
|
||||
|
||||
class JSONResponseStub:
|
||||
def __init__(self, content=None):
|
||||
self.content = content
|
||||
|
||||
responses_stub.JSONResponse = JSONResponseStub
|
||||
|
||||
funasr_stub = types.ModuleType("funasr")
|
||||
funasr_stub.AutoModel = object
|
||||
|
||||
nano_stub = types.ModuleType("funasr.models.fun_asr_nano.inference_vllm")
|
||||
nano_stub.FunASRNanoVLLM = object
|
||||
|
||||
vad_stub = types.ModuleType("funasr.models.fsmn_vad_streaming.dynamic_vad")
|
||||
vad_stub.DynamicStreamingVAD = object
|
||||
|
||||
monkeypatch.setitem(sys.modules, "fastapi", fastapi_stub)
|
||||
monkeypatch.setitem(sys.modules, "fastapi.responses", responses_stub)
|
||||
monkeypatch.setitem(sys.modules, "uvicorn", types.ModuleType("uvicorn"))
|
||||
monkeypatch.setitem(sys.modules, "soundfile", types.ModuleType("soundfile"))
|
||||
monkeypatch.setitem(sys.modules, "torch", types.ModuleType("torch"))
|
||||
monkeypatch.setitem(sys.modules, "funasr", funasr_stub)
|
||||
monkeypatch.setitem(sys.modules, "funasr.models", types.ModuleType("funasr.models"))
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"funasr.models.fun_asr_nano",
|
||||
types.ModuleType("funasr.models.fun_asr_nano"),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "funasr.models.fun_asr_nano.inference_vllm", nano_stub
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"funasr.models.fsmn_vad_streaming",
|
||||
types.ModuleType("funasr.models.fsmn_vad_streaming"),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "funasr.models.fsmn_vad_streaming.dynamic_vad", vad_stub
|
||||
)
|
||||
|
||||
module_name = "serve_vllm_under_test"
|
||||
sys.modules.pop(module_name, None)
|
||||
spec = importlib.util.spec_from_file_location(module_name, SERVICE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_openai_verbose_json_keeps_segment_speaker(monkeypatch):
|
||||
module = load_service_module(monkeypatch)
|
||||
|
||||
response = module.build_openai_verbose_json(
|
||||
{
|
||||
"duration": 1.2,
|
||||
"text": "hello",
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 1.2,
|
||||
"text": "hello",
|
||||
"words": [{"word": "hello", "start": 0.0, "end": 1.2}],
|
||||
"speaker": "SPK0",
|
||||
}
|
||||
],
|
||||
},
|
||||
language="en",
|
||||
)
|
||||
|
||||
assert response["segments"][0]["speaker"] == "SPK0"
|
||||
|
||||
|
||||
def test_process_audio_downmixes_stereo_before_resampling(monkeypatch):
|
||||
module = load_service_module(monkeypatch)
|
||||
captured = {}
|
||||
|
||||
librosa_stub = types.ModuleType("librosa")
|
||||
|
||||
def fake_resample(audio, orig_sr, target_sr):
|
||||
assert audio.ndim == 1
|
||||
captured["resample_input"] = audio.copy()
|
||||
assert orig_sr == 8000
|
||||
assert target_sr == 16000
|
||||
return np.repeat(audio, 2)
|
||||
|
||||
librosa_stub.resample = fake_resample
|
||||
monkeypatch.setitem(sys.modules, "librosa", librosa_stub)
|
||||
|
||||
class EngineStub:
|
||||
def generate(self, inputs, **kwargs):
|
||||
captured["engine_input"] = inputs[0]
|
||||
return [{"text": "ok"}]
|
||||
|
||||
module._engine = EngineStub()
|
||||
stereo_audio = np.column_stack(
|
||||
[np.zeros(8000, dtype=np.float32), np.ones(8000, dtype=np.float32)]
|
||||
)
|
||||
|
||||
result = module.process_audio(
|
||||
stereo_audio,
|
||||
sr=8000,
|
||||
use_vad=False,
|
||||
use_spk=False,
|
||||
use_timestamp=False,
|
||||
)
|
||||
|
||||
assert np.allclose(captured["resample_input"], 0.5)
|
||||
assert captured["engine_input"].shape == (16000,)
|
||||
assert np.allclose(captured["engine_input"], 0.5)
|
||||
assert result["duration"] == 1.0
|
||||
assert result["text"] == "ok"
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Unit tests for Fun-ASR-Nano vLLM repetition-penalty handling.
|
||||
|
||||
Regression guard for issue #2948: a repetition penalty other than 1.0 is
|
||||
incompatible with vLLM prompt-embeds mode and aborts the engine with a CUDA
|
||||
"scatter gather index out of bounds" assertion. The serving paths must never
|
||||
forward such a value to ``SamplingParams`` while ``enable_prompt_embeds=True``.
|
||||
|
||||
The helper is dependency-free, so these tests run without a GPU or vLLM.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from funasr.models.fun_asr_nano import vllm_utils
|
||||
from funasr.models.fun_asr_nano.vllm_utils import (
|
||||
NEUTRAL_REPETITION_PENALTY,
|
||||
resolve_repetition_penalty,
|
||||
)
|
||||
|
||||
|
||||
class TestResolveRepetitionPenalty(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reset the once-per-process warning flag so each test is independent.
|
||||
vllm_utils._warned_prompt_embeds = False
|
||||
|
||||
def test_neutral_value_passes_through(self):
|
||||
self.assertEqual(resolve_repetition_penalty(1.0), 1.0)
|
||||
|
||||
def test_none_maps_to_neutral(self):
|
||||
self.assertEqual(
|
||||
resolve_repetition_penalty(None), NEUTRAL_REPETITION_PENALTY
|
||||
)
|
||||
|
||||
def test_nonneutral_is_clamped_in_prompt_embeds_mode(self):
|
||||
# The exact value that triggers the #2948 crash.
|
||||
self.assertEqual(
|
||||
resolve_repetition_penalty(1.3, prompt_embeds=True),
|
||||
NEUTRAL_REPETITION_PENALTY,
|
||||
)
|
||||
|
||||
def test_nonneutral_preserved_for_token_prompts(self):
|
||||
# Regular token-prompt decoding can safely apply the penalty.
|
||||
self.assertEqual(
|
||||
resolve_repetition_penalty(1.3, prompt_embeds=False), 1.3
|
||||
)
|
||||
|
||||
def test_warns_once_in_prompt_embeds_mode(self):
|
||||
with self.assertLogs(vllm_utils.logger, level=logging.WARNING) as cm:
|
||||
resolve_repetition_penalty(1.3, prompt_embeds=True)
|
||||
# Subsequent clamps must not emit additional warnings.
|
||||
resolve_repetition_penalty(1.5, prompt_embeds=True)
|
||||
self.assertEqual(len(cm.records), 1)
|
||||
self.assertIn("2948", cm.output[0])
|
||||
|
||||
def test_no_warning_when_value_is_safe(self):
|
||||
# Capture records directly (assertNoLogs is only available on 3.10+).
|
||||
records = []
|
||||
|
||||
class _Collect(logging.Handler):
|
||||
def emit(self, record):
|
||||
records.append(record)
|
||||
|
||||
handler = _Collect(level=logging.WARNING)
|
||||
vllm_utils.logger.addHandler(handler)
|
||||
try:
|
||||
resolve_repetition_penalty(1.0, prompt_embeds=True)
|
||||
resolve_repetition_penalty(1.3, prompt_embeds=False)
|
||||
finally:
|
||||
vllm_utils.logger.removeHandler(handler)
|
||||
self.assertEqual(records, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,115 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_generate_subtitle_module():
|
||||
script_path = Path(__file__).resolve().parents[1] / "examples" / "subtitle" / "generate_subtitle.py"
|
||||
spec = importlib.util.spec_from_file_location("generate_subtitle", script_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_generate_subtitle_requests_sentence_timestamps_and_writes_segmented_srt(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
module = load_generate_subtitle_module()
|
||||
captured = {}
|
||||
|
||||
class FakeAutoModel:
|
||||
def __init__(self, **kwargs):
|
||||
captured["model_kwargs"] = kwargs
|
||||
|
||||
def generate(self, **kwargs):
|
||||
captured["generate_kwargs"] = kwargs
|
||||
if kwargs.get("sentence_timestamp") and kwargs.get("output_timestamp"):
|
||||
return [
|
||||
{
|
||||
"text": "<|zh|>First segment Second segment",
|
||||
"sentence_info": [
|
||||
{"start": 0, "end": 3500, "text": "<|zh|>First segment"},
|
||||
{"start": 3500, "end": 7200, "sentence": "Second segment", "spk": 1},
|
||||
],
|
||||
}
|
||||
]
|
||||
return [{"text": "<|zh|>First segment Second segment"}]
|
||||
|
||||
fake_funasr = types.ModuleType("funasr")
|
||||
fake_funasr.AutoModel = FakeAutoModel
|
||||
monkeypatch.setitem(sys.modules, "funasr", fake_funasr)
|
||||
|
||||
input_path = tmp_path / "input.mp4"
|
||||
input_path.write_bytes(b"fake media")
|
||||
output_path = tmp_path / "sub.srt"
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"generate_subtitle.py",
|
||||
str(input_path),
|
||||
"-o",
|
||||
str(output_path),
|
||||
"--lang",
|
||||
"zh",
|
||||
"--device",
|
||||
"cpu",
|
||||
"--spk",
|
||||
],
|
||||
)
|
||||
|
||||
module.main()
|
||||
|
||||
assert captured["model_kwargs"]["punc_model"] == "ct-punc"
|
||||
assert captured["generate_kwargs"]["language"] == "zh"
|
||||
assert captured["generate_kwargs"]["sentence_timestamp"] is True
|
||||
assert captured["generate_kwargs"]["output_timestamp"] is True
|
||||
assert captured["generate_kwargs"]["return_time_stamps"] is True
|
||||
assert output_path.read_text(encoding="utf-8") == (
|
||||
"1\n"
|
||||
"00:00:00,000 --> 00:00:03,500\n"
|
||||
"First segment\n\n"
|
||||
"2\n"
|
||||
"00:00:03,500 --> 00:00:07,200\n"
|
||||
"[Speaker 1] Second segment\n\n"
|
||||
)
|
||||
|
||||
|
||||
def test_generate_subtitle_falls_back_to_timestamp_bounds_when_sentence_info_missing(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
module = load_generate_subtitle_module()
|
||||
|
||||
class FakeAutoModel:
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def generate(self, **kwargs):
|
||||
return [
|
||||
{
|
||||
"text": "<|zh|>Full text",
|
||||
"timestamp": [[250, 1000], [1000, 3200]],
|
||||
}
|
||||
]
|
||||
|
||||
fake_funasr = types.ModuleType("funasr")
|
||||
fake_funasr.AutoModel = FakeAutoModel
|
||||
monkeypatch.setitem(sys.modules, "funasr", fake_funasr)
|
||||
|
||||
input_path = tmp_path / "input.mp4"
|
||||
input_path.write_bytes(b"fake media")
|
||||
output_path = tmp_path / "sub.srt"
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
["generate_subtitle.py", str(input_path), "-o", str(output_path), "--device", "cpu"],
|
||||
)
|
||||
|
||||
module.main()
|
||||
|
||||
assert output_path.read_text(encoding="utf-8") == (
|
||||
"1\n"
|
||||
"00:00:00,250 --> 00:00:03,200\n"
|
||||
"Full text\n\n"
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Unit tests for GLM-ASR vLLM result-key deduplication.
|
||||
|
||||
These tests exercise ``GLMASRVLLMEngine.generate`` without a GPU or a real
|
||||
vLLM installation: the vLLM entry points are stubbed in ``sys.modules`` and the
|
||||
audio/encoder/engine collaborators are mocked, so only the result-key wiring is
|
||||
under test. The pure ``_dedup_keys`` helper is also tested directly.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def _install_vllm_stub():
|
||||
"""Install a minimal ``vllm`` stub so ``inference_vllm`` imports without GPU."""
|
||||
|
||||
class _SamplingParams:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
class _EmbedsPrompt:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
vllm_mod = types.ModuleType("vllm")
|
||||
vllm_mod.SamplingParams = _SamplingParams
|
||||
vllm_mod.LLM = object
|
||||
inputs_mod = types.ModuleType("vllm.inputs")
|
||||
inputs_mod.EmbedsPrompt = _EmbedsPrompt
|
||||
data_mod = types.ModuleType("vllm.inputs.data")
|
||||
data_mod.EmbedsPrompt = _EmbedsPrompt
|
||||
|
||||
sys.modules["vllm"] = vllm_mod
|
||||
sys.modules["vllm.inputs"] = inputs_mod
|
||||
sys.modules["vllm.inputs.data"] = data_mod
|
||||
|
||||
|
||||
class DedupKeysHelperTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_install_vllm_stub()
|
||||
from funasr.models.glm_asr import inference_vllm
|
||||
|
||||
self.inference_vllm = inference_vllm
|
||||
# Reset the warn-once flag so warning-related assertions are deterministic.
|
||||
inference_vllm._warned_dup_keys = False
|
||||
|
||||
def test_collision_free_input_unchanged(self):
|
||||
keys = ["a", "b", "c"]
|
||||
self.assertEqual(self.inference_vllm._dedup_keys(keys), ["a", "b", "c"])
|
||||
|
||||
def test_repeated_keys_get_deterministic_suffix(self):
|
||||
keys = ["seg", "seg", "other", "seg"]
|
||||
self.assertEqual(
|
||||
self.inference_vllm._dedup_keys(keys),
|
||||
["seg", "seg_1", "other", "seg_2"],
|
||||
)
|
||||
|
||||
def test_suffix_does_not_clash_with_existing_key(self):
|
||||
# A naive "seg" -> "seg_1" scheme would re-collide with the literal
|
||||
# "seg_1" input; the result must stay globally unique.
|
||||
keys = ["seg", "seg_1", "seg"]
|
||||
result = self.inference_vllm._dedup_keys(keys)
|
||||
self.assertEqual(result, ["seg", "seg_1", "seg_2"])
|
||||
self.assertEqual(len(set(result)), len(result))
|
||||
|
||||
def test_empty_input(self):
|
||||
self.assertEqual(self.inference_vllm._dedup_keys([]), [])
|
||||
|
||||
def test_sentinel_keys_preserved(self):
|
||||
keys = ["sample_0", "sample_1", "sample_0"]
|
||||
self.assertEqual(
|
||||
self.inference_vllm._dedup_keys(keys),
|
||||
["sample_0", "sample_1", "sample_0_1"],
|
||||
)
|
||||
|
||||
def test_does_not_mutate_input(self):
|
||||
keys = ["seg", "seg"]
|
||||
self.inference_vllm._dedup_keys(keys)
|
||||
self.assertEqual(keys, ["seg", "seg"])
|
||||
|
||||
|
||||
class DedupResultKeysTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_install_vllm_stub()
|
||||
from funasr.models.glm_asr.inference_vllm import GLMASRVLLMEngine
|
||||
|
||||
# Build an engine without running __init__ (no model load / GPU needed).
|
||||
engine = GLMASRVLLMEngine.__new__(GLMASRVLLMEngine)
|
||||
engine.device = "cpu"
|
||||
engine._encode_audio = mock.Mock(return_value="audio_embeds")
|
||||
engine._build_prompt_embeds = mock.Mock(
|
||||
return_value=mock.Mock(float=lambda: "embeds")
|
||||
)
|
||||
engine.tokenizer = mock.Mock()
|
||||
engine.tokenizer.decode = mock.Mock(return_value="hello world")
|
||||
self.engine = engine
|
||||
|
||||
def _set_outputs(self, n):
|
||||
token_out = types.SimpleNamespace(token_ids=[1, 2, 3])
|
||||
outs = [types.SimpleNamespace(outputs=[token_out]) for _ in range(n)]
|
||||
self.engine.vllm_engine = mock.Mock()
|
||||
self.engine.vllm_engine.generate = mock.Mock(return_value=outs)
|
||||
|
||||
def test_duplicate_basenames_get_unique_keys(self):
|
||||
self._set_outputs(2)
|
||||
results = self.engine.generate(["spk1/segment.wav", "spk2/segment.wav"])
|
||||
self.assertEqual([r["key"] for r in results], ["segment", "segment_1"])
|
||||
# No transcript is dropped when results are folded into a {key: text} dict.
|
||||
self.assertEqual(len({r["key"] for r in results}), len(results))
|
||||
|
||||
def test_distinct_basenames_unchanged(self):
|
||||
self._set_outputs(2)
|
||||
results = self.engine.generate(["a.wav", "b.wav"])
|
||||
self.assertEqual([r["key"] for r in results], ["a", "b"])
|
||||
|
||||
def test_single_input_key(self):
|
||||
self._set_outputs(1)
|
||||
results = self.engine.generate("only.wav")
|
||||
self.assertEqual(results, [{"key": "only", "text": "hello world"}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Unit tests for the GLM-ASR vLLM fp16 degraded-output guard.
|
||||
|
||||
Regression guard: ``fp16`` can produce degraded or garbage transcription for
|
||||
GLM-ASR (numerical overflow in the audio embedding path), mirroring the
|
||||
documented Fun-ASR-Nano behaviour. Requesting it must warn once so users are
|
||||
not silently handed poor output, while leaving the requested value untouched.
|
||||
|
||||
The helper is dependency-free, so these tests run without a GPU, torch, or
|
||||
vLLM.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from funasr.models.glm_asr import vllm_utils
|
||||
from funasr.models.glm_asr.vllm_utils import warn_if_degraded_dtype
|
||||
|
||||
|
||||
class TestWarnIfDegradedDtype(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reset the once-per-process warning flag so each test is independent.
|
||||
vllm_utils._warned_fp16 = False
|
||||
|
||||
def test_returns_value_unchanged(self):
|
||||
for dtype in ("bf16", "fp16", "fp32", "something-else"):
|
||||
self.assertEqual(warn_if_degraded_dtype(dtype), dtype)
|
||||
|
||||
def test_fp16_warns(self):
|
||||
with self.assertLogs(vllm_utils.logger, level=logging.WARNING) as cm:
|
||||
warn_if_degraded_dtype("fp16")
|
||||
self.assertEqual(len(cm.records), 1)
|
||||
self.assertIn("fp16", cm.output[0])
|
||||
|
||||
def test_fp16_warns_only_once(self):
|
||||
with self.assertLogs(vllm_utils.logger, level=logging.WARNING) as cm:
|
||||
warn_if_degraded_dtype("fp16")
|
||||
# Subsequent calls must not emit additional warnings.
|
||||
warn_if_degraded_dtype("fp16")
|
||||
self.assertEqual(len(cm.records), 1)
|
||||
|
||||
def test_safe_values_do_not_warn(self):
|
||||
# Capture records directly (assertNoLogs is only available on 3.10+).
|
||||
records = []
|
||||
|
||||
class _Collect(logging.Handler):
|
||||
def emit(self, record):
|
||||
records.append(record)
|
||||
|
||||
handler = _Collect(level=logging.WARNING)
|
||||
vllm_utils.logger.addHandler(handler)
|
||||
try:
|
||||
warn_if_degraded_dtype("bf16")
|
||||
warn_if_degraded_dtype("fp32")
|
||||
finally:
|
||||
vllm_utils.logger.removeHandler(handler)
|
||||
self.assertEqual(records, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Unit tests for GLM-ASR vLLM sampling-parameter handling.
|
||||
|
||||
These tests exercise ``GLMASRVLLMEngine.generate`` without a GPU or a real
|
||||
vLLM installation: the vLLM entry points are stubbed in ``sys.modules`` and the
|
||||
audio/encoder/engine collaborators are mocked, so only the sampling-parameter
|
||||
wiring is under test.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def _install_vllm_stub():
|
||||
"""Install a minimal ``vllm`` stub whose SamplingParams records kwargs."""
|
||||
|
||||
captured = {}
|
||||
|
||||
class _RecordingSamplingParams:
|
||||
def __init__(self, **kwargs):
|
||||
captured.clear()
|
||||
captured.update(kwargs)
|
||||
|
||||
class _EmbedsPrompt:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
vllm_mod = types.ModuleType("vllm")
|
||||
vllm_mod.SamplingParams = _RecordingSamplingParams
|
||||
vllm_mod.LLM = object
|
||||
inputs_mod = types.ModuleType("vllm.inputs")
|
||||
inputs_mod.EmbedsPrompt = _EmbedsPrompt
|
||||
data_mod = types.ModuleType("vllm.inputs.data")
|
||||
data_mod.EmbedsPrompt = _EmbedsPrompt
|
||||
|
||||
sys.modules["vllm"] = vllm_mod
|
||||
sys.modules["vllm.inputs"] = inputs_mod
|
||||
sys.modules["vllm.inputs.data"] = data_mod
|
||||
return captured
|
||||
|
||||
|
||||
class GLMASRSamplingParamsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.captured = _install_vllm_stub()
|
||||
from funasr.models.glm_asr.inference_vllm import GLMASRVLLMEngine
|
||||
|
||||
# Build an engine without running __init__ (no model load / GPU needed).
|
||||
engine = GLMASRVLLMEngine.__new__(GLMASRVLLMEngine)
|
||||
engine.device = "cpu"
|
||||
engine._encode_audio = mock.Mock(return_value="audio_embeds")
|
||||
engine._build_prompt_embeds = mock.Mock(
|
||||
return_value=mock.Mock(float=lambda: "embeds")
|
||||
)
|
||||
|
||||
token_out = types.SimpleNamespace(token_ids=[1, 2, 3])
|
||||
vllm_output = types.SimpleNamespace(outputs=[token_out])
|
||||
engine.vllm_engine = mock.Mock()
|
||||
engine.vllm_engine.generate = mock.Mock(return_value=[vllm_output])
|
||||
engine.tokenizer = mock.Mock()
|
||||
engine.tokenizer.decode = mock.Mock(return_value="hello world")
|
||||
self.engine = engine
|
||||
|
||||
def test_defaults_preserve_greedy_behavior(self):
|
||||
results = self.engine.generate("a.wav")
|
||||
self.assertEqual(results, [{"key": "a", "text": "hello world"}])
|
||||
self.assertEqual(self.captured["temperature"], 0.0)
|
||||
self.assertEqual(self.captured["top_p"], 1.0)
|
||||
self.assertEqual(self.captured["top_k"], -1)
|
||||
self.assertEqual(self.captured["repetition_penalty"], 1.0)
|
||||
self.assertEqual(self.captured["max_tokens"], 500)
|
||||
|
||||
def test_caller_sampling_params_are_forwarded(self):
|
||||
self.engine.generate(
|
||||
"a.wav", max_new_tokens=128, temperature=0.7, top_p=0.9, top_k=20
|
||||
)
|
||||
self.assertEqual(self.captured["max_tokens"], 128)
|
||||
self.assertEqual(self.captured["temperature"], 0.7)
|
||||
self.assertEqual(self.captured["top_p"], 0.9)
|
||||
self.assertEqual(self.captured["top_k"], 20)
|
||||
|
||||
def test_non_positive_top_k_is_normalized_to_disabled(self):
|
||||
self.engine.generate("a.wav", top_k=0)
|
||||
self.assertEqual(self.captured["top_k"], -1)
|
||||
|
||||
def test_repetition_penalty_is_forced_neutral_in_prompt_embeds_mode(self):
|
||||
# A non-neutral repetition_penalty would crash vLLM prompt-embeds mode
|
||||
# (issue #2948), so it must be coerced back to 1.0 rather than forwarded.
|
||||
self.engine.generate("a.wav", repetition_penalty=1.3)
|
||||
self.assertEqual(self.captured["repetition_penalty"], 1.0)
|
||||
|
||||
def test_neutral_repetition_penalty_passes_through(self):
|
||||
self.engine.generate("a.wav", repetition_penalty=1.0)
|
||||
self.assertEqual(self.captured["repetition_penalty"], 1.0)
|
||||
|
||||
|
||||
class SafeRepetitionPenaltyTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_install_vllm_stub()
|
||||
import funasr.models.glm_asr.inference_vllm as mod
|
||||
|
||||
self.mod = mod
|
||||
# Reset the process-wide warn-once flag between tests.
|
||||
mod._warned_rep_penalty = False
|
||||
|
||||
def test_neutral_and_none_map_to_one(self):
|
||||
self.assertEqual(self.mod._safe_repetition_penalty(1.0), 1.0)
|
||||
self.assertEqual(self.mod._safe_repetition_penalty(None), 1.0)
|
||||
|
||||
def test_non_neutral_is_coerced_and_warns_once(self):
|
||||
with self.assertLogs(self.mod.logger, level="WARNING") as cm:
|
||||
self.assertEqual(self.mod._safe_repetition_penalty(1.5), 1.0)
|
||||
self.assertTrue(any("2948" in line for line in cm.output))
|
||||
self.assertTrue(self.mod._warned_rep_penalty)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,23 @@
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from funasr.utils.kws_utils import query_token_set, split_mixed_label, symbol_str
|
||||
|
||||
|
||||
class KwsUtilsTest(unittest.TestCase):
|
||||
def test_symbol_regex_strips_punctuation_and_whitespace(self):
|
||||
self.assertEqual(re.sub(symbol_str, "", "abc!@# 中文\t★"), "abc中文")
|
||||
|
||||
def test_split_mixed_label_lowercases_ascii_words(self):
|
||||
self.assertEqual(split_mixed_label("Hello中文"), ["hello", "中", "文"])
|
||||
|
||||
def test_query_token_set_strips_symbols_before_character_lookup(self):
|
||||
symbol_table = {"a": 1, "中": 2, "<unk>": 0}
|
||||
tokens, token_ids = query_token_set("A!中", symbol_table, {})
|
||||
|
||||
self.assertEqual(tokens, ("a", "中"))
|
||||
self.assertEqual(token_ids, (1, 2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestTransformerInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_inference_pipeline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.language_score_prediction,
|
||||
model="damo/speech_transformer_lm_zh-cn-common-vocab8404-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(text_in="hello 大 家 好 呀")
|
||||
logger.info("lm inference result: {0}".format(rec_result))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_mcp_server_example_has_glama_ready_dockerfile():
|
||||
example_dir = Path(__file__).resolve().parents[1] / "examples" / "mcp_server"
|
||||
dockerfile = example_dir / "Dockerfile"
|
||||
|
||||
assert dockerfile.is_file()
|
||||
|
||||
dockerfile_text = dockerfile.read_text()
|
||||
assert "pip install" in dockerfile_text
|
||||
assert "funasr" in dockerfile_text
|
||||
assert "COPY funasr_mcp.py /app/funasr_mcp.py" in dockerfile_text
|
||||
assert 'CMD ["python", "/app/funasr_mcp.py"]' in dockerfile_text
|
||||
|
||||
|
||||
def test_mcp_server_example_has_glama_metadata():
|
||||
example_dir = Path(__file__).resolve().parents[1] / "examples" / "mcp_server"
|
||||
metadata_path = example_dir / "glama.json"
|
||||
|
||||
metadata = json.loads(metadata_path.read_text())
|
||||
|
||||
assert metadata["repository"] == "https://github.com/modelscope/FunASR"
|
||||
assert metadata["runtime"] == "python"
|
||||
assert metadata["license"] == "MIT"
|
||||
assert metadata["mcpServers"]["funasr"]["command"] == "python"
|
||||
assert metadata["mcpServers"]["funasr"]["args"] == ["/app/funasr_mcp.py"]
|
||||
|
||||
|
||||
def test_repository_root_has_glama_metadata_for_mcp_directory_scanners():
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
metadata = json.loads((repo_root / "glama.json").read_text())
|
||||
|
||||
assert metadata["repository"] == "https://github.com/modelscope/FunASR"
|
||||
assert metadata["runtime"] == "python"
|
||||
assert metadata["license"] == "MIT"
|
||||
assert metadata["mcpServers"]["funasr"]["command"] == "python"
|
||||
assert metadata["mcpServers"]["funasr"]["args"] == [
|
||||
"examples/mcp_server/funasr_mcp.py"
|
||||
]
|
||||
|
||||
|
||||
def test_mcp_server_readme_has_glama_submission_checklist():
|
||||
example_dir = Path(__file__).resolve().parents[1] / "examples" / "mcp_server"
|
||||
readme = (example_dir / "README.md").read_text()
|
||||
|
||||
assert "### Glama submission checklist" in readme
|
||||
assert "https://github.com/modelscope/FunASR" in readme
|
||||
assert "`examples/mcp_server`" in readme
|
||||
assert "`python /app/funasr_mcp.py`" in readme
|
||||
assert "`transcribe_audio`" in readme
|
||||
assert "https://glama.ai/mcp/servers/modelscope/FunASR/badges/score.svg" in readme
|
||||
@@ -0,0 +1,72 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _install_fake_rapidfuzz():
|
||||
rapidfuzz = types.ModuleType("rapidfuzz")
|
||||
distance = types.ModuleType("rapidfuzz.distance")
|
||||
|
||||
class Levenshtein:
|
||||
@staticmethod
|
||||
def distance(left, right):
|
||||
left = list(left)
|
||||
right = list(right)
|
||||
dp = list(range(len(right) + 1))
|
||||
for i, lval in enumerate(left, 1):
|
||||
prev = dp[0]
|
||||
dp[0] = i
|
||||
for j, rval in enumerate(right, 1):
|
||||
old = dp[j]
|
||||
dp[j] = min(
|
||||
dp[j] + 1,
|
||||
dp[j - 1] + 1,
|
||||
prev + (0 if lval == rval else 1),
|
||||
)
|
||||
prev = old
|
||||
return dp[-1]
|
||||
|
||||
distance.Levenshtein = Levenshtein
|
||||
rapidfuzz.distance = distance
|
||||
sys.modules.setdefault("rapidfuzz", rapidfuzz)
|
||||
sys.modules.setdefault("rapidfuzz.distance", distance)
|
||||
|
||||
|
||||
def _load_metrics_common():
|
||||
_install_fake_rapidfuzz()
|
||||
module_path = Path(__file__).resolve().parents[1] / "funasr" / "metrics" / "common.py"
|
||||
spec = importlib.util.spec_from_file_location("metrics_common_probe", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["metrics_common_probe"] = module
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_cer_and_wer_return_none_when_reference_length_is_zero():
|
||||
common = _load_metrics_common()
|
||||
calc = common.ErrorCalculator(
|
||||
char_list=["<blank>", "a", "b", "c", "h", "e", "l", "o", "w", "r", "d", " "],
|
||||
sym_space=" ",
|
||||
sym_blank="<blank>",
|
||||
report_cer=True,
|
||||
report_wer=True,
|
||||
)
|
||||
|
||||
assert calc.calculate_cer(["abc"], [" "]) is None
|
||||
assert calc.calculate_wer(["hello world"], [" "]) is None
|
||||
|
||||
|
||||
def test_cer_and_wer_keep_normal_levenshtein_semantics():
|
||||
common = _load_metrics_common()
|
||||
calc = common.ErrorCalculator(
|
||||
char_list=["<blank>", "a", "b", "c", "x", "y", " "],
|
||||
sym_space=" ",
|
||||
sym_blank="<blank>",
|
||||
report_cer=True,
|
||||
report_wer=True,
|
||||
)
|
||||
|
||||
assert calc.calculate_cer(["abc"], ["axc"]) == 1 / 3
|
||||
assert calc.calculate_wer(["foo bar baz"], ["foo qux baz"]) == 1 / 3
|
||||
@@ -0,0 +1,163 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
_MODULE_PATH = Path(__file__).resolve().parents[1] / "funasr" / "utils" / "postprocess_hotwords.py"
|
||||
_SPEC = importlib.util.spec_from_file_location("postprocess_hotwords", _MODULE_PATH)
|
||||
postprocess_hotwords = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules["postprocess_hotwords"] = postprocess_hotwords
|
||||
assert _SPEC.loader is not None
|
||||
_SPEC.loader.exec_module(postprocess_hotwords)
|
||||
|
||||
HotwordMatch = postprocess_hotwords.HotwordMatch
|
||||
PostprocessHotwordMatcher = postprocess_hotwords.PostprocessHotwordMatcher
|
||||
apply_postprocess_hotwords_to_results = postprocess_hotwords.apply_postprocess_hotwords_to_results
|
||||
build_postprocess_hotword_matcher = postprocess_hotwords.build_postprocess_hotword_matcher
|
||||
parse_hotword_file = postprocess_hotwords.parse_hotword_file
|
||||
parse_postprocess_hotwords = postprocess_hotwords.parse_postprocess_hotwords
|
||||
|
||||
|
||||
class TestPostprocessHotwordParsing(unittest.TestCase):
|
||||
def test_parse_list_and_dict(self):
|
||||
explicit, fuzzy = parse_postprocess_hotwords(["科大讯飞", "东方财富"])
|
||||
self.assertEqual(explicit, {})
|
||||
self.assertEqual(fuzzy, ["科大讯飞", "东方财富"])
|
||||
|
||||
explicit, fuzzy = parse_postprocess_hotwords({"科大迅飞": "科大讯飞", "东方财富": "东方财富"})
|
||||
self.assertEqual(explicit, {"科大迅飞": "科大讯飞"})
|
||||
self.assertEqual(fuzzy, ["东方财富"])
|
||||
|
||||
def test_parse_inline_mapping(self):
|
||||
explicit, fuzzy = parse_postprocess_hotwords(["撒贝你=>撒贝宁", "康辉"])
|
||||
self.assertEqual(explicit, {"撒贝你": "撒贝宁"})
|
||||
self.assertEqual(fuzzy, ["康辉"])
|
||||
|
||||
def test_parse_hotword_file(self):
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as f:
|
||||
f.write("# comment\n")
|
||||
f.write("科大讯飞\n")
|
||||
f.write("科大迅飞=>科大讯飞\n")
|
||||
path = f.name
|
||||
|
||||
try:
|
||||
explicit, fuzzy = parse_hotword_file(path)
|
||||
self.assertEqual(explicit, {"科大迅飞": "科大讯飞"})
|
||||
self.assertEqual(fuzzy, ["科大讯飞"])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class TestPostprocessHotwordMatcher(unittest.TestCase):
|
||||
def test_explicit_replace(self):
|
||||
matcher = PostprocessHotwordMatcher(
|
||||
explicit_map={"撒贝你": "撒贝宁"},
|
||||
enable_fuzzy=False,
|
||||
)
|
||||
text, matches = matcher.apply_text("我非常喜欢撒贝你说的新闻")
|
||||
self.assertEqual(text, "我非常喜欢撒贝宁说的新闻")
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0].replacement, "撒贝宁")
|
||||
self.assertEqual(matches[0].score, 1.0)
|
||||
|
||||
def test_fuzzy_replace_with_optional_deps(self):
|
||||
try:
|
||||
import pypinyin # noqa: F401
|
||||
import rapidfuzz # noqa: F401
|
||||
except ImportError:
|
||||
self.skipTest("pypinyin and rapidfuzz are required for fuzzy tests")
|
||||
|
||||
matcher = PostprocessHotwordMatcher(
|
||||
fuzzy_targets=["科大讯飞"],
|
||||
threshold=0.75,
|
||||
)
|
||||
text, matches = matcher.apply_text("科大迅飞的语音识别很强")
|
||||
self.assertIn("科大讯飞", text)
|
||||
self.assertTrue(matches)
|
||||
self.assertEqual(matches[0].replacement, "科大讯飞")
|
||||
|
||||
def test_missing_fuzzy_dependency_raises(self):
|
||||
with mock.patch.object(
|
||||
postprocess_hotwords,
|
||||
"_require_pypinyin",
|
||||
side_effect=ImportError("missing pypinyin"),
|
||||
):
|
||||
with self.assertRaises(ImportError):
|
||||
PostprocessHotwordMatcher(
|
||||
fuzzy_targets=["科大讯飞"],
|
||||
threshold=0.85,
|
||||
)
|
||||
|
||||
def test_invalid_threshold_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PostprocessHotwordMatcher(explicit_map={"a": "b"}, threshold=1.5)
|
||||
|
||||
def test_sentence_info_and_timestamp_preserved(self):
|
||||
matcher = PostprocessHotwordMatcher(
|
||||
explicit_map={"撒贝你": "撒贝宁"},
|
||||
enable_fuzzy=False,
|
||||
)
|
||||
result = {
|
||||
"text": "撒贝你主持节目",
|
||||
"timestamp": [[0, 100], [100, 200]],
|
||||
"sentence_info": [
|
||||
{"text": "撒贝你主持", "sentence": "撒贝你主持", "start": 0, "end": 1000},
|
||||
{"text": "节目", "sentence": "节目", "start": 1000, "end": 1500},
|
||||
],
|
||||
}
|
||||
matcher.apply_result(result, return_matches=True)
|
||||
self.assertEqual(result["text"], "撒贝宁主持节目")
|
||||
self.assertEqual(result["sentence_info"][0]["text"], "撒贝宁主持")
|
||||
self.assertEqual(result["sentence_info"][0]["sentence"], "撒贝宁主持")
|
||||
self.assertEqual(result["timestamp"], [[0, 100], [100, 200]])
|
||||
self.assertEqual(result["postprocess_hotword_matches"][0]["replacement"], "撒贝宁")
|
||||
|
||||
def test_matcher_reused_for_multiple_results(self):
|
||||
build_calls = {"count": 0}
|
||||
original_build = build_postprocess_hotword_matcher
|
||||
|
||||
def counting_build(*args, **kwargs):
|
||||
build_calls["count"] += 1
|
||||
return original_build(*args, **kwargs)
|
||||
|
||||
results = [
|
||||
{"text": "撒贝你主持", "timestamp": [1]},
|
||||
{"text": "康灰播报", "timestamp": [2]},
|
||||
]
|
||||
cfg = {
|
||||
"postprocess_hotwords": {"撒贝你": "撒贝宁", "康灰": "康辉"},
|
||||
"return_postprocess_hotword_matches": True,
|
||||
}
|
||||
with mock.patch.object(
|
||||
postprocess_hotwords,
|
||||
"build_postprocess_hotword_matcher",
|
||||
side_effect=counting_build,
|
||||
):
|
||||
updated = apply_postprocess_hotwords_to_results(results, cfg)
|
||||
|
||||
self.assertEqual(build_calls["count"], 1)
|
||||
self.assertEqual(updated[0]["text"], "撒贝宁主持")
|
||||
self.assertEqual(updated[1]["text"], "康辉播报")
|
||||
self.assertEqual(len(updated[0]["postprocess_hotword_matches"]), 1)
|
||||
|
||||
def test_noop_when_not_configured(self):
|
||||
results = [{"text": "不变", "timestamp": [1]}]
|
||||
updated = apply_postprocess_hotwords_to_results(results, {})
|
||||
self.assertIs(updated, results)
|
||||
self.assertEqual(updated[0]["text"], "不变")
|
||||
|
||||
|
||||
class TestHotwordMatch(unittest.TestCase):
|
||||
def test_as_dict(self):
|
||||
match = HotwordMatch("a", "b", 0.9, 1, 2)
|
||||
self.assertEqual(
|
||||
match.as_dict(),
|
||||
{"original": "a", "replacement": "b", "score": 0.9, "start": 1, "end": 2},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Tests for issue #2839: punc_model=None should not cause UnboundLocalError."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestPuncModelNone(unittest.TestCase):
|
||||
"""Test that inference_with_vad works when punc_model is None."""
|
||||
|
||||
def _make_auto_model(self, punc_model=None, spk_model=None, spk_mode=None):
|
||||
"""Create a minimal AutoModel instance with mocked dependencies."""
|
||||
from funasr.auto.auto_model import AutoModel
|
||||
|
||||
am = AutoModel.__new__(AutoModel)
|
||||
am.model = MagicMock()
|
||||
am.vad_model = MagicMock()
|
||||
am.punc_model = punc_model
|
||||
am.punc_kwargs = {}
|
||||
am.spk_model = spk_model
|
||||
am.cb_model = None
|
||||
am.spk_mode = spk_mode
|
||||
am.vad_kwargs = {}
|
||||
am.kwargs = {
|
||||
"batch_size_s": 300,
|
||||
"batch_size_threshold_s": 60,
|
||||
"device": "cpu",
|
||||
"disable_pbar": True,
|
||||
"frontend": MagicMock(fs=16000),
|
||||
"fs": 16000,
|
||||
}
|
||||
am._reset_runtime_configs = MagicMock()
|
||||
return am
|
||||
|
||||
def _setup_mocks(self, am, mock_slice, mock_load, mock_prep):
|
||||
"""Configure standard mocks for a single-segment VAD + ASR flow."""
|
||||
# VAD returns one segment [0, 16000ms]
|
||||
vad_result = [{"key": "test_utt", "value": [[0, 16000]]}]
|
||||
# ASR returns text with timestamps
|
||||
asr_result = [{"text": "hello world", "timestamp": [[0, 500], [500, 1000]]}]
|
||||
|
||||
call_count = [0]
|
||||
results_seq = [vad_result, asr_result]
|
||||
|
||||
def mock_inference(data, input_len=None, model=None, kwargs=None, **cfg):
|
||||
idx = call_count[0]
|
||||
call_count[0] += 1
|
||||
if idx < len(results_seq):
|
||||
return results_seq[idx]
|
||||
return [{"text": ""}]
|
||||
|
||||
am.inference = MagicMock(side_effect=mock_inference)
|
||||
mock_prep.return_value = (["test_utt"], [np.zeros(16000, dtype=np.float32)])
|
||||
mock_load.return_value = np.zeros(16000, dtype=np.float32)
|
||||
mock_slice.return_value = ([np.zeros(16000, dtype=np.float32)], [16000])
|
||||
|
||||
@patch("funasr.auto.auto_model.slice_padding_audio_samples")
|
||||
@patch("funasr.auto.auto_model.load_audio_text_image_video")
|
||||
@patch("funasr.auto.auto_model.prepare_data_iterator")
|
||||
def test_punc_model_none_basic(self, mock_prep, mock_load, mock_slice):
|
||||
"""Basic inference with punc_model=None should not raise UnboundLocalError."""
|
||||
am = self._make_auto_model(punc_model=None)
|
||||
self._setup_mocks(am, mock_slice, mock_load, mock_prep)
|
||||
|
||||
results = am.inference_with_vad("dummy_input")
|
||||
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]["text"], "hello world")
|
||||
self.assertEqual(results[0]["key"], "test_utt")
|
||||
|
||||
@patch("funasr.auto.auto_model.slice_padding_audio_samples")
|
||||
@patch("funasr.auto.auto_model.load_audio_text_image_video")
|
||||
@patch("funasr.auto.auto_model.prepare_data_iterator")
|
||||
def test_sentence_timestamp_with_punc_model_none(self, mock_prep, mock_load, mock_slice):
|
||||
"""sentence_timestamp=True with punc_model=None should not crash."""
|
||||
am = self._make_auto_model(punc_model=None)
|
||||
self._setup_mocks(am, mock_slice, mock_load, mock_prep)
|
||||
|
||||
# This path previously caused UnboundLocalError on punc_res
|
||||
results = am.inference_with_vad("dummy_input", sentence_timestamp=True)
|
||||
|
||||
self.assertEqual(len(results), 1)
|
||||
# sentence_info should be empty list since punc_res is unavailable
|
||||
self.assertEqual(results[0].get("sentence_info"), [])
|
||||
|
||||
@patch("funasr.auto.auto_model.slice_padding_audio_samples")
|
||||
@patch("funasr.auto.auto_model.load_audio_text_image_video")
|
||||
@patch("funasr.auto.auto_model.prepare_data_iterator")
|
||||
def test_punc_model_with_value_still_works(self, mock_prep, mock_load, mock_slice):
|
||||
"""When punc_model is provided, punc_res should still be used normally."""
|
||||
punc_mock = MagicMock()
|
||||
am = self._make_auto_model(punc_model=punc_mock)
|
||||
|
||||
vad_result = [{"key": "test_utt", "value": [[0, 16000]]}]
|
||||
asr_result = [{"text": "hello world", "timestamp": [[0, 500], [500, 1000]]}]
|
||||
punc_result = [{"text": "Hello, world.", "punc_array": [1, 2]}]
|
||||
|
||||
call_count = [0]
|
||||
results_seq = [vad_result, asr_result, punc_result]
|
||||
|
||||
def mock_inference(data, input_len=None, model=None, kwargs=None, **cfg):
|
||||
idx = call_count[0]
|
||||
call_count[0] += 1
|
||||
return results_seq[idx]
|
||||
|
||||
am.inference = MagicMock(side_effect=mock_inference)
|
||||
mock_prep.return_value = (["test_utt"], [np.zeros(16000, dtype=np.float32)])
|
||||
mock_load.return_value = np.zeros(16000, dtype=np.float32)
|
||||
mock_slice.return_value = ([np.zeros(16000, dtype=np.float32)], [16000])
|
||||
|
||||
results = am.inference_with_vad("dummy_input")
|
||||
|
||||
self.assertEqual(len(results), 1)
|
||||
# Text should be updated with punctuated version
|
||||
self.assertEqual(results[0]["text"], "Hello, world.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestTransformerInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_inference_pipeline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.punctuation,
|
||||
model="damo/punc_ct-transformer_zh-cn-common-vocab272727-pytorch",
|
||||
model_revision="v1.1.7",
|
||||
)
|
||||
inputs = "./egs_modelscope/punctuation/punc_ct-transformer_zh-cn-common-vocab272727-pytorch/data/punc_example.txt"
|
||||
rec_result = inference_pipeline(text_in=inputs)
|
||||
logger.info("punctuation inference result: {0}".format(rec_result))
|
||||
|
||||
def test_vadrealtime_inference_pipeline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.punctuation,
|
||||
model="damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727",
|
||||
)
|
||||
inputs = "跨境河流是养育沿岸|人民的生命之源长期以来为帮助下游地区防灾减灾中方技术人员|在上游地区极为恶劣的自然条件下克服巨大困难甚至冒着生命危险|向印方提供汛期水文资料处理紧急事件中方重视印方在跨境河流问题上的关切|愿意进一步完善双方联合工作机制|凡是|中方能做的我们|都会去做而且会做得更好我请印度朋友们放心中国在上游的|任何开发利用都会经过科学|规划和论证兼顾上下游的利益"
|
||||
vads = inputs.split("|")
|
||||
rec_result_all = "outputs:"
|
||||
param_dict = {"cache": []}
|
||||
for vad in vads:
|
||||
rec_result = inference_pipeline(text_in=vad, param_dict=param_dict)
|
||||
rec_result_all += rec_result["text"]
|
||||
logger.info("punctuation inference result: {0}".format(rec_result_all))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,91 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from funasr.models.qwen3_asr import model as qwen3_model
|
||||
|
||||
|
||||
def test_qwen3_asr_dependency_check_reports_transformers_mismatch(monkeypatch):
|
||||
def fake_version(package_name):
|
||||
versions = {
|
||||
"qwen-asr": "0.0.6",
|
||||
"transformers": "5.5.0",
|
||||
}
|
||||
return versions[package_name]
|
||||
|
||||
def fake_requires(package_name):
|
||||
assert package_name == "qwen-asr"
|
||||
return ["transformers==4.57.6", "accelerate==1.12.0"]
|
||||
|
||||
monkeypatch.setattr(qwen3_model, "_package_version", fake_version)
|
||||
monkeypatch.setattr(qwen3_model, "_package_requires", fake_requires)
|
||||
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
qwen3_model._check_qwen3_asr_dependencies()
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "Qwen3-ASR dependency mismatch" in message
|
||||
assert "qwen-asr==0.0.6 requires transformers==4.57.6" in message
|
||||
assert "active environment has transformers==5.5.0" in message
|
||||
assert 'pip install -U "qwen-asr==0.0.6" "transformers==4.57.6" accelerate' in message
|
||||
|
||||
|
||||
def test_qwen3_asr_dependency_check_accepts_matching_transformers(monkeypatch):
|
||||
def fake_version(package_name):
|
||||
versions = {
|
||||
"qwen-asr": "0.0.6",
|
||||
"transformers": "4.57.6",
|
||||
}
|
||||
return versions[package_name]
|
||||
|
||||
def fake_requires(package_name):
|
||||
assert package_name == "qwen-asr"
|
||||
return ["transformers==4.57.6", "accelerate==1.12.0"]
|
||||
|
||||
monkeypatch.setattr(qwen3_model, "_package_version", fake_version)
|
||||
monkeypatch.setattr(qwen3_model, "_package_requires", fake_requires)
|
||||
|
||||
qwen3_model._check_qwen3_asr_dependencies()
|
||||
|
||||
|
||||
def test_qwen3_asr_dependency_check_matches_requirement_names_case_insensitively(monkeypatch):
|
||||
def fake_version(package_name):
|
||||
versions = {
|
||||
"qwen-asr": "0.0.6",
|
||||
"transformers": "5.5.0",
|
||||
}
|
||||
return versions[package_name]
|
||||
|
||||
def fake_requires(package_name):
|
||||
assert package_name == "qwen-asr"
|
||||
return ["Transformers==4.57.6"]
|
||||
|
||||
monkeypatch.setattr(qwen3_model, "_package_version", fake_version)
|
||||
monkeypatch.setattr(qwen3_model, "_package_requires", fake_requires)
|
||||
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
qwen3_model._check_qwen3_asr_dependencies()
|
||||
|
||||
assert "requires transformers==4.57.6" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_qwen3_asr_dependency_check_does_not_crash_on_non_pep440_transformers(monkeypatch):
|
||||
def fake_version(package_name):
|
||||
versions = {
|
||||
"qwen-asr": "0.0.6",
|
||||
"transformers": "custom-main",
|
||||
}
|
||||
return versions[package_name]
|
||||
|
||||
def fake_requires(package_name):
|
||||
assert package_name == "qwen-asr"
|
||||
return ["transformers==4.57.6"]
|
||||
|
||||
monkeypatch.setattr(qwen3_model, "_package_version", fake_version)
|
||||
monkeypatch.setattr(qwen3_model, "_package_requires", fake_requires)
|
||||
|
||||
qwen3_model._check_qwen3_asr_dependencies()
|
||||
@@ -0,0 +1,56 @@
|
||||
import ast
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
EXAMPLE_DIR = ROOT / "examples" / "industrial_data_pretraining" / "qwen3_asr"
|
||||
SERVER_PATH = EXAMPLE_DIR / "serve_qwen3_asr_ws.py"
|
||||
NOTE_PATHS = [
|
||||
EXAMPLE_DIR / "serve_qwen3_asr_ws_notes.md",
|
||||
EXAMPLE_DIR / "serve_qwen3_asr_ws_notes_en.md",
|
||||
]
|
||||
|
||||
|
||||
class Qwen3AsrWebsocketExampleTest(unittest.TestCase):
|
||||
def test_notes_do_not_contain_review_placeholders(self):
|
||||
forbidden_phrases = ["以下是我的个人理解", "The following is my personal understanding"]
|
||||
for note_path in NOTE_PATHS:
|
||||
text = note_path.read_text(encoding="utf-8")
|
||||
for phrase in forbidden_phrases:
|
||||
with self.subTest(path=note_path.name, phrase=phrase):
|
||||
self.assertNotIn(phrase, text)
|
||||
|
||||
def test_handler_logs_unexpected_exceptions(self):
|
||||
tree = ast.parse(SERVER_PATH.read_text(encoding="utf-8"), filename=str(SERVER_PATH))
|
||||
handle_client = next(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.AsyncFunctionDef) and node.name == "handle_client"
|
||||
)
|
||||
|
||||
catches_generic_exception = False
|
||||
logs_exception = False
|
||||
for node in ast.walk(handle_client):
|
||||
if not isinstance(node, ast.ExceptHandler):
|
||||
continue
|
||||
if isinstance(node.type, ast.Name) and node.type.id == "Exception":
|
||||
catches_generic_exception = True
|
||||
for child in ast.walk(node):
|
||||
if not isinstance(child, ast.Call):
|
||||
continue
|
||||
func = child.func
|
||||
if (
|
||||
isinstance(func, ast.Attribute)
|
||||
and isinstance(func.value, ast.Name)
|
||||
and func.value.id == "logging"
|
||||
and func.attr == "exception"
|
||||
):
|
||||
logs_exception = True
|
||||
|
||||
self.assertTrue(catches_generic_exception)
|
||||
self.assertTrue(logs_exception)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
BENCHMARK_PATH = (
|
||||
REPO_ROOT
|
||||
/ "examples"
|
||||
/ "industrial_data_pretraining"
|
||||
/ "fun_asr_nano"
|
||||
/ "realtime_ws_benchmark.py"
|
||||
)
|
||||
|
||||
|
||||
def load_benchmark_module():
|
||||
module_name = "realtime_ws_benchmark_under_test"
|
||||
sys.modules.pop(module_name, None)
|
||||
spec = importlib.util.spec_from_file_location(module_name, BENCHMARK_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_final_message_does_not_contribute_to_response_lag(monkeypatch):
|
||||
module = load_benchmark_module()
|
||||
messages = iter(
|
||||
[
|
||||
{"partial": "hello", "duration_ms": 1000},
|
||||
{"is_final": True, "sentences": [{"text": "hello"}], "duration_ms": 1200},
|
||||
{"event": "stopped"},
|
||||
]
|
||||
)
|
||||
timestamps = iter([1.2, 2.0, 2.1])
|
||||
|
||||
async def fake_receive_message(ws, timeout):
|
||||
return next(messages)
|
||||
|
||||
monkeypatch.setattr(module, "receive_message", fake_receive_message)
|
||||
monkeypatch.setattr(module.time, "perf_counter", lambda: next(timestamps))
|
||||
metrics = {
|
||||
"messages": 0,
|
||||
"result_messages": 0,
|
||||
"partial_messages": 0,
|
||||
"final_messages": 0,
|
||||
"events": {},
|
||||
"first_update_ms": None,
|
||||
"final_update_ms": None,
|
||||
"final_after_stop_ms": None,
|
||||
"response_lag_ms": [],
|
||||
"stopped": False,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
asyncio.run(module.recv_results(object(), metrics, 0.0, {"value": 1.5}, 1.0))
|
||||
|
||||
assert metrics["result_messages"] == 2
|
||||
assert metrics["partial_messages"] == 1
|
||||
assert metrics["final_messages"] == 1
|
||||
assert metrics["response_lag_ms"] == [200.0]
|
||||
assert metrics["final_update_ms"] == 2000.0
|
||||
assert metrics["final_after_stop_ms"] == 500.0
|
||||
@@ -0,0 +1,157 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SERVICE_PATH = REPO_ROOT / "examples" / "industrial_data_pretraining" / "fun_asr_nano" / "serve_realtime_ws.py"
|
||||
|
||||
|
||||
def load_service_module():
|
||||
websockets_stub = types.SimpleNamespace(
|
||||
exceptions=types.SimpleNamespace(ConnectionClosed=Exception),
|
||||
serve=lambda *args, **kwargs: None,
|
||||
)
|
||||
sys.modules.setdefault("websockets", websockets_stub)
|
||||
|
||||
module_name = "serve_realtime_ws_under_test"
|
||||
sys.modules.pop(module_name, None)
|
||||
spec = importlib.util.spec_from_file_location(module_name, SERVICE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_cli_defaults_disable_speaker_and_bound_partial_window():
|
||||
module = load_service_module()
|
||||
|
||||
assert hasattr(module, "build_arg_parser")
|
||||
args = module.build_arg_parser().parse_args([])
|
||||
|
||||
assert args.enable_spk is False
|
||||
assert args.partial_window_sec == 15.0
|
||||
|
||||
|
||||
def test_websocket_keepalive_kwargs_are_configurable():
|
||||
module = load_service_module()
|
||||
|
||||
args = module.build_arg_parser().parse_args(
|
||||
[
|
||||
"--ws-ping-interval", "35",
|
||||
"--ws-ping-timeout", "70",
|
||||
"--ws-close-timeout", "12",
|
||||
"--ws-max-size", "12345",
|
||||
]
|
||||
)
|
||||
|
||||
assert module.build_websocket_serve_kwargs(args) == {
|
||||
"max_size": 12345,
|
||||
"ping_interval": 35.0,
|
||||
"ping_timeout": 70.0,
|
||||
"close_timeout": 12.0,
|
||||
}
|
||||
|
||||
|
||||
def test_websocket_keepalive_kwargs_can_disable_ping():
|
||||
module = load_service_module()
|
||||
|
||||
args = module.build_arg_parser().parse_args(
|
||||
[
|
||||
"--ws-ping-interval", "0",
|
||||
"--ws-ping-timeout", "0",
|
||||
]
|
||||
)
|
||||
|
||||
kwargs = module.build_websocket_serve_kwargs(args)
|
||||
assert kwargs["ping_interval"] is None
|
||||
assert kwargs["ping_timeout"] is None
|
||||
|
||||
|
||||
def test_create_speaker_tracker_skips_spk_when_disabled():
|
||||
module = load_service_module()
|
||||
args = types.SimpleNamespace(enable_spk=False, device="cuda:0")
|
||||
|
||||
assert module.create_speaker_tracker(object(), args) is None
|
||||
|
||||
|
||||
def test_partial_decode_audio_uses_recent_window():
|
||||
module = load_service_module()
|
||||
|
||||
class DummyVad:
|
||||
current_speech_start = 0
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
sample_rate = 16000
|
||||
session = module.RealtimeASRSession(
|
||||
vllm_engine=object(),
|
||||
asr_kwargs={},
|
||||
vad=DummyVad(),
|
||||
spk_tracker=None,
|
||||
sample_rate=sample_rate,
|
||||
chunk_ms=960,
|
||||
partial_window_sec=2.0,
|
||||
)
|
||||
session.audio_buffer = np.arange(sample_rate * 5, dtype=np.float32)
|
||||
|
||||
audio, start_ms = session.get_partial_decode_audio()
|
||||
|
||||
assert start_ms == 3000
|
||||
np.testing.assert_array_equal(audio, session.audio_buffer[-sample_rate * 2:])
|
||||
|
||||
|
||||
def test_partial_decode_audio_keeps_short_current_segment():
|
||||
module = load_service_module()
|
||||
|
||||
class DummyVad:
|
||||
current_speech_start = 1500
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
sample_rate = 16000
|
||||
session = module.RealtimeASRSession(
|
||||
vllm_engine=object(),
|
||||
asr_kwargs={},
|
||||
vad=DummyVad(),
|
||||
spk_tracker=None,
|
||||
sample_rate=sample_rate,
|
||||
chunk_ms=960,
|
||||
partial_window_sec=10.0,
|
||||
)
|
||||
session.audio_buffer = np.arange(sample_rate * 5, dtype=np.float32)
|
||||
|
||||
audio, start_ms = session.get_partial_decode_audio()
|
||||
assert start_ms == 1500
|
||||
np.testing.assert_array_equal(audio, session.audio_buffer[int(sample_rate * 1.5):])
|
||||
|
||||
|
||||
def test_empty_partial_response_preserves_zero_speech_start():
|
||||
module = load_service_module()
|
||||
|
||||
class DummyVad:
|
||||
current_speech_start = 0
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
sample_rate = 16000
|
||||
session = module.RealtimeASRSession(
|
||||
vllm_engine=object(),
|
||||
asr_kwargs={},
|
||||
vad=DummyVad(),
|
||||
spk_tracker=None,
|
||||
sample_rate=sample_rate,
|
||||
chunk_ms=960,
|
||||
)
|
||||
session.audio_buffer = np.zeros(sample_rate, dtype=np.float32)
|
||||
session.last_partial_text = ""
|
||||
|
||||
response = session._build_response(is_final=False)
|
||||
|
||||
assert response["partial_start_ms"] == 0
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Unit tests for SenseVoice Tokenizer handling of special-token strings.
|
||||
|
||||
Regression guard for issue #3110: ASR models such as Fun-ASR-Nano occasionally
|
||||
emit special-token strings (e.g. ``<|no|>``, a language tag) as part of the
|
||||
transcription text. Re-encoding that text via ``tiktoken.Encoding.encode``
|
||||
crashes by default (``disallowed_special="all"``), which takes down the whole
|
||||
batch on a single bad sample during forced alignment / loss computation.
|
||||
``Tokenizer.encode`` now treats special-token strings as ordinary text unless
|
||||
the caller explicitly opts into a stricter policy.
|
||||
|
||||
These tests build a minimal in-memory tiktoken encoding, so they run without
|
||||
the Fun-ASR-Nano ``multilingual.tiktoken`` vocab (which ships with the model,
|
||||
not this repo) and without a GPU or model download.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import tiktoken
|
||||
|
||||
from funasr.models.sense_voice.whisper_lib.tokenizer import Tokenizer
|
||||
|
||||
|
||||
def _build_test_encoding() -> tiktoken.Encoding:
|
||||
"""A minimal byte-level BPE encoding with the specials Tokenizer needs.
|
||||
|
||||
Every single byte is its own token, so arbitrary text can be encoded
|
||||
without the model's multilingual.tiktoken vocab file.
|
||||
"""
|
||||
mergeable_ranks = {bytes([b]): b for b in range(256)}
|
||||
n_vocab = len(mergeable_ranks)
|
||||
|
||||
# Specials required by Tokenizer.__post_init__ (startoftranscript/translate/
|
||||
# transcribe) plus a few language tags. "<|no|>" is the Norwegian tag that
|
||||
# triggered #3110.
|
||||
required_specials = [
|
||||
"<|startoftranscript|>",
|
||||
"<|no|>",
|
||||
"<|zh|>",
|
||||
"<|en|>",
|
||||
"<|translate|>",
|
||||
"<|transcribe|>",
|
||||
"<|startoflm|>",
|
||||
"<|startofprev|>",
|
||||
"<|nospeech|>",
|
||||
"<|notimestamps|>",
|
||||
"<|0.00|>",
|
||||
]
|
||||
special_tokens = {}
|
||||
for tok in required_specials:
|
||||
special_tokens[tok] = n_vocab
|
||||
n_vocab += 1
|
||||
|
||||
return tiktoken.Encoding(
|
||||
name="test-encoding",
|
||||
pat_str=(
|
||||
r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| """
|
||||
r"""?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
|
||||
),
|
||||
mergeable_ranks=mergeable_ranks,
|
||||
special_tokens=special_tokens,
|
||||
)
|
||||
|
||||
|
||||
def _build_test_tokenizer() -> Tokenizer:
|
||||
return Tokenizer(encoding=_build_test_encoding(), num_languages=1)
|
||||
|
||||
|
||||
class TestTokenizerEncodeSpecialTokens(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tokenizer = _build_test_tokenizer()
|
||||
self.no_id = self.tokenizer.special_tokens["<|no|>"]
|
||||
|
||||
def test_plain_text_encodes(self):
|
||||
ids = self.tokenizer.encode("hello world")
|
||||
self.assertIsInstance(ids, list)
|
||||
self.assertTrue(ids)
|
||||
|
||||
def test_language_tag_in_text_does_not_raise(self):
|
||||
# The exact failure from issue #3110: ASR output containing "<|no|>"
|
||||
# used to crash with "disallowed special token '<|no|>'".
|
||||
ids = self.tokenizer.encode("hello <|no|> world")
|
||||
self.assertIsInstance(ids, list)
|
||||
self.assertTrue(ids)
|
||||
|
||||
def test_nospeech_tag_in_text_does_not_raise(self):
|
||||
ids = self.tokenizer.encode("<|nospeech|> something")
|
||||
self.assertIsInstance(ids, list)
|
||||
self.assertTrue(ids)
|
||||
|
||||
def test_language_tag_alone_does_not_raise(self):
|
||||
ids = self.tokenizer.encode("<|no|>")
|
||||
self.assertIsInstance(ids, list)
|
||||
self.assertTrue(ids)
|
||||
|
||||
def test_caller_disallowed_special_is_respected(self):
|
||||
# A caller that explicitly wants the strict behaviour must still get
|
||||
# it (setdefault must not override an explicit kwargs).
|
||||
with self.assertRaises(ValueError):
|
||||
self.tokenizer.encode("<|no|>", disallowed_special="all")
|
||||
|
||||
def test_allowed_special_all_keeps_special_token_ids(self):
|
||||
# funasr/models/sense_voice/whisper_lib/decoding.py calls
|
||||
# tokenizer.encode(prompt, allowed_special="all"); the special token
|
||||
# must still be encoded as its token id, not as raw bytes.
|
||||
ids = self.tokenizer.encode("<|no|>", allowed_special="all")
|
||||
self.assertEqual(ids, [self.no_id])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestXVectorInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_inference_pipeline(self):
|
||||
inference_sv_pipline = pipeline(
|
||||
task=Tasks.speaker_verification,
|
||||
model="damo/speech_xvector_sv-zh-cn-cnceleb-16k-spk3465-pytorch",
|
||||
)
|
||||
|
||||
# the same speaker
|
||||
rec_result = inference_sv_pipline(
|
||||
audio_in=(
|
||||
"https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/sv_example_enroll.wav",
|
||||
"https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/sv_example_same.wav",
|
||||
)
|
||||
)
|
||||
assert (
|
||||
abs(rec_result["scores"][0] - 0.85) < 0.1 and abs(rec_result["scores"][1] - 0.14) < 0.1
|
||||
)
|
||||
logger.info(f"Similarity {rec_result['scores']}")
|
||||
|
||||
# different speaker
|
||||
rec_result = inference_sv_pipline(
|
||||
audio_in=(
|
||||
"https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/sv_example_enroll.wav",
|
||||
"https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/sv_example_different.wav",
|
||||
)
|
||||
)
|
||||
assert abs(rec_result["scores"][0] - 0.0) < 0.1 and abs(rec_result["scores"][1] - 1.0) < 0.1
|
||||
logger.info(f"Similarity {rec_result['scores']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,58 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestTimestampPredictionPipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_inference_pipeline(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.speech_timestamp,
|
||||
model="damo/speech_timestamp_prediction-v1-16k-offline",
|
||||
model_revision="v1.1.0",
|
||||
)
|
||||
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_timestamps.wav",
|
||||
text_in="一 个 东 太 平 洋 国 家 为 什 么 跑 到 西 太 平 洋 来 了 呢",
|
||||
)
|
||||
print(rec_result)
|
||||
logger.info("punctuation inference result: {0}".format(rec_result))
|
||||
assert rec_result == {
|
||||
"text": "<sil> 0.000 0.380;一 0.380 0.560;个 0.560 0.800;东 0.800 0.980;太 0.980 1.140;平 1.140 1.260;洋 1.260 1.440;国 1.440 1.680;家 1.680 1.920;<sil> 1.920 2.040;为 2.040 2.200;什 2.200 2.320;么 2.320 2.500;跑 2.500 2.680;到 2.680 2.860;西 2.860 3.040;太 3.040 3.200;平 3.200 3.380;洋 3.380 3.500;来 3.500 3.640;了 3.640 3.800;呢 3.800 4.150;<sil> 4.150 4.440;",
|
||||
"timestamp": [
|
||||
[380, 560],
|
||||
[560, 800],
|
||||
[800, 980],
|
||||
[980, 1140],
|
||||
[1140, 1260],
|
||||
[1260, 1440],
|
||||
[1440, 1680],
|
||||
[1680, 1920],
|
||||
[2040, 2200],
|
||||
[2200, 2320],
|
||||
[2320, 2500],
|
||||
[2500, 2680],
|
||||
[2680, 2860],
|
||||
[2860, 3040],
|
||||
[3040, 3200],
|
||||
[3200, 3380],
|
||||
[3380, 3500],
|
||||
[3500, 3640],
|
||||
[3640, 3800],
|
||||
[3800, 4150],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,85 @@
|
||||
import unittest
|
||||
|
||||
from modelscope.pipelines import pipeline
|
||||
from modelscope.utils.constant import Tasks
|
||||
from modelscope.utils.logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class TestFSMNInferencePipelines(unittest.TestCase):
|
||||
def test_funasr_path(self):
|
||||
import funasr
|
||||
import os
|
||||
|
||||
logger.info("run_dir:{0} ; funasr_path: {1}".format(os.getcwd(), funasr.__file__))
|
||||
|
||||
def test_8k(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.voice_activity_detection,
|
||||
model="damo/speech_fsmn_vad_zh-cn-8k-common",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/vad_example_8k.wav"
|
||||
)
|
||||
logger.info("vad inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == [
|
||||
[0, 1960],
|
||||
[2870, 6730],
|
||||
[7960, 10180],
|
||||
[12140, 14830],
|
||||
[15740, 19400],
|
||||
[20220, 24230],
|
||||
[25540, 27290],
|
||||
[30070, 30970],
|
||||
[32070, 34280],
|
||||
[35990, 37050],
|
||||
[39400, 41020],
|
||||
[41810, 47320],
|
||||
[48120, 52150],
|
||||
[53560, 58310],
|
||||
[59290, 62210],
|
||||
[63110, 66420],
|
||||
[67300, 68280],
|
||||
[69670, 71770],
|
||||
[73100, 75550],
|
||||
[76850, 78500],
|
||||
[79380, 83280],
|
||||
[85000, 92320],
|
||||
[93560, 94110],
|
||||
[94990, 95620],
|
||||
[96940, 97590],
|
||||
[98400, 100530],
|
||||
[101600, 104890],
|
||||
[108780, 110900],
|
||||
[112020, 113460],
|
||||
[114210, 115030],
|
||||
]
|
||||
|
||||
def test_16k(self):
|
||||
inference_pipeline = pipeline(
|
||||
task=Tasks.voice_activity_detection,
|
||||
model="damo/speech_fsmn_vad_zh-cn-16k-common-pytorch",
|
||||
)
|
||||
rec_result = inference_pipeline(
|
||||
audio_in="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/vad_example.wav"
|
||||
)
|
||||
logger.info("vad inference result: {0}".format(rec_result))
|
||||
assert rec_result["text"] == [
|
||||
[70, 2340],
|
||||
[2620, 6200],
|
||||
[6480, 23670],
|
||||
[23950, 26250],
|
||||
[26780, 28990],
|
||||
[29950, 31430],
|
||||
[31750, 37600],
|
||||
[38210, 46900],
|
||||
[47310, 49630],
|
||||
[49910, 56460],
|
||||
[56740, 59540],
|
||||
[59820, 70450],
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user