diff --git a/.github/workflows/test-funasr-onnx-package.yml b/.github/workflows/test-funasr-onnx-package.yml index 226421da6..18f0b4e54 100644 --- a/.github/workflows/test-funasr-onnx-package.yml +++ b/.github/workflows/test-funasr-onnx-package.yml @@ -86,7 +86,7 @@ jobs: import importlib.util from importlib.metadata import requires, version - assert version("funasr-onnx") == "0.4.10" + assert version("funasr-onnx") == "0.4.11" assert importlib.util.find_spec("torch") is None requirements = requires("funasr-onnx") or [] @@ -130,7 +130,7 @@ jobs: if: matrix.python-version == '3.12' uses: actions/upload-artifact@v7 with: - name: funasr-onnx-0.4.10 + name: funasr-onnx-0.4.11 path: dist/funasr-onnx/* if-no-files-found: error diff --git a/runtime/python/onnxruntime/README.md b/runtime/python/onnxruntime/README.md index e3d18c6d4..4982cc450 100644 --- a/runtime/python/onnxruntime/README.md +++ b/runtime/python/onnxruntime/README.md @@ -7,6 +7,12 @@ does not update the installed ONNX wrappers. Check their versions separately with `python -m pip show funasr-onnx funasr`; use the source installation below when a required wrapper fix is newer than the published PyPI package. +Version 0.4.11 keeps timestamps independent of padded samples in a batch +(#3871), bounds Contextual/Seaco hotwords to the exported ten-character +embedding input and skips whitespace-only entries (#3882), and preserves +terminal abbreviations and their timestamps (#3869). These are wrapper and +postprocessing fixes, not model-weight updates or ASR accuracy claims. + Version 0.4.10 restores direct librosa decoding for non-WAV audio in offline and streaming Paraformer, SenseVoice and offline VAD (#3865), without creating a WAV sidecar or treating every non-WAV input as MP3. Codec support still depends on the @@ -45,9 +51,9 @@ and postprocessing fixes, not a model-weight update or an ASR accuracy claim. install from pip ```shell -pip install -U "funasr-onnx>=0.4.10" +pip install -U "funasr-onnx>=0.4.11" # For the users in China, you could install with the command: -# pip install -U "funasr-onnx>=0.4.10" -i https://mirror.sjtu.edu.cn/pypi/web/simple +# pip install -U "funasr-onnx>=0.4.11" -i https://mirror.sjtu.edu.cn/pypi/web/simple # If you want to export .onnx file, you should install modelscope and funasr pip install -U modelscope funasr # For the users in China, you could install with the command: diff --git a/runtime/python/onnxruntime/setup.py b/runtime/python/onnxruntime/setup.py index 40290072f..22791fa14 100644 --- a/runtime/python/onnxruntime/setup.py +++ b/runtime/python/onnxruntime/setup.py @@ -13,7 +13,7 @@ def get_readme(): MODULE_NAME = "funasr_onnx" -VERSION_NUM = "0.4.10" +VERSION_NUM = "0.4.11" setuptools.setup( name=MODULE_NAME, diff --git a/tests/test_funasr_onnx_installed.py b/tests/test_funasr_onnx_installed.py index b6a74c78e..161870454 100644 --- a/tests/test_funasr_onnx_installed.py +++ b/tests/test_funasr_onnx_installed.py @@ -41,6 +41,71 @@ class InstalledOnnxPackageTest(unittest.TestCase): LOADERS = (funasr_onnx.Paraformer, ParaformerOnline, funasr_onnx.SenseVoiceSmall, funasr_onnx.Fsmn_vad) + def test_long_and_empty_hotwords_stay_within_embedding_bounds(self): + for cls in (funasr_onnx.ContextualParaformer, funasr_onnx.SeacoParaformer): + with self.subTest(wrapper=cls.__name__): + model = cls.__new__(cls) + model.vocab = {'a': 3, 'b': 4} + matrix, positions = model.proc_hotword(' ab abababababab ') + self.assertEqual(matrix.tolist(), [ + [3, 4, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 4, 3, 4, 3, 4, 3, 4, 3, 4], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]) + self.assertEqual(positions.tolist(), [1, 9, 0]) + for value in ('', ' \t\n'): + matrix, positions = model.proc_hotword(value) + self.assertEqual(matrix.tolist(), [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + self.assertEqual(positions.tolist(), [0]) + + def test_batched_timestamps_use_each_input_length(self): + def features(lengths): + return np.zeros((len(lengths), max(lengths), 4), dtype=np.float32), np.array(lengths) + + def acoustic_output(feats, lengths): + peaks = np.zeros((len(lengths), feats.shape[1] * 3), dtype=np.float32) + for row, length in enumerate(lengths): + peaks[row, np.linspace(5, length * 3 - 6, 3).round().astype(int)] = 1 + return None, None, np.zeros_like(peaks), peaks + + # Acoustic outputs are synthetic. The installed batching, timestamp and + # postprocessing code must keep the shorter sample independent of padding. + for cls in (funasr_onnx.Paraformer, funasr_onnx.ContextualParaformer, + funasr_onnx.SeacoParaformer): + with self.subTest(wrapper=cls.__name__): + model = cls.__new__(cls) + model.batch_size = 2 + model.plot_timestamp_to = '' + model.language = 'zh' + model.frontend = SimpleNamespace(opts=SimpleNamespace( + frame_opts=SimpleNamespace(samp_freq=16000))) + model.load_data = lambda values, fs: values + model.extract_feat = features + model.decode = lambda scores, lens: [['\u4f60', '\u597d'] for _ in range(model._rows)] + + def infer(feats, lengths, *unused): + model._rows = len(lengths) + return acoustic_output(feats, lengths) + + model.infer = infer + model.bb_infer = infer + model.vocab = {'a': 3} + model.eb_infer = lambda hotwords, lengths: [np.zeros((10, len(hotwords), 4), dtype=np.float32)] + args = () if cls is funasr_onnx.Paraformer else ('a',) + single = [model([length], *args)[0] for length in [17, 50]] + self.assertEqual([x['timestamp'] for x in single], [ + [[70, 470], [470, 945]], [[70, 670], [1450, 2925]], + ]) + self.assertEqual(model([17, 50], *args), single) + + def test_terminal_abbreviation_preserves_text_and_timestamps(self): + from funasr_onnx.utils.postprocess_utils import abbr_dispose + + self.assertEqual(abbr_dispose(['a', ' ', 'b']), ['AB']) + self.assertEqual(abbr_dispose(['a', ' ', 'b'], [[0, 100], [100, 200]]), + (['AB'], [[0, 200]])) + self.assertEqual(abbr_dispose(['a', ' ', 'b', ' ', 'c']), ['ABC']) + def test_lossless_non_wav_without_sidecar(self): samples = (np.arange(4800, dtype=np.int16) - 2400).astype(np.float32) / 32768 with tempfile.TemporaryDirectory() as directory: @@ -91,7 +156,7 @@ def test_invalid_audio_remains_visible_without_sidecar(self): def test_installed_distribution_version_and_origin(self): distribution = importlib.metadata.distribution('funasr-onnx') - self.assertEqual(distribution.version, '0.4.10') + self.assertEqual(distribution.version, '0.4.11') self.assertEqual( Path(funasr_onnx.__file__).resolve().parent, Path(distribution.locate_file('funasr_onnx')).resolve(), diff --git a/tests/test_funasr_onnx_release.py b/tests/test_funasr_onnx_release.py index cb57f6be5..1e73caf87 100644 --- a/tests/test_funasr_onnx_release.py +++ b/tests/test_funasr_onnx_release.py @@ -7,7 +7,7 @@ ROOT = Path(__file__).resolve().parents[1] PACKAGE_ROOT = ROOT / "runtime" / "python" / "onnxruntime" SETUP_PATH = PACKAGE_ROOT / "setup.py" -EXPECTED_VERSION = "0.4.10" +EXPECTED_VERSION = "0.4.11" REQUIREMENT_NAME_PATTERN = re.compile(r"^\s*([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)") @@ -44,7 +44,7 @@ def normalized_requirement_name(requirement): class FunASROnnxReleaseContractTest(unittest.TestCase): - def test_release_version_is_0_4_10(self): + def test_release_version_is_0_4_11(self): self.assertEqual(assigned_literal(read_setup_tree(), "VERSION_NUM"), EXPECTED_VERSION) def test_standalone_license_preserves_repository_notice(self): @@ -83,6 +83,9 @@ def test_readme_names_the_standalone_fix_release(self): self.assertIn("#3847", readme) self.assertIn("empty or sub-frame", readme) self.assertIn("preserves the streaming cache", readme) + self.assertIn("#3869", readme) + self.assertIn("#3871", readme) + self.assertIn("#3882", readme) def test_runtime_dependencies_keep_onnx_install_torch_free(self): requirements = setup_keyword_literal(read_setup_tree(), "install_requires")