From 099c2f4404be97c28ce20aa980afd8295d8dab37 Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Wed, 4 Nov 2026 11:39:39 +0000 Subject: [PATCH] fix(auto): honor merge_length_s passed to generate() With merge_vad=True, inference_with_vad read merge_length_s from the construction kwargs before the call options were merged in, so a value passed to generate() (as in the SenseVoice README and demo) was silently ignored and the construction value or the 15 s default was used instead. Read the per-call value first and fall back to the construction value, then the 15 s default. Update the Python API docs accordingly. --- docs/python_api.md | 2 +- docs/python_api_zh.md | 2 +- funasr/auto/auto_model.py | 6 +-- tests/test_merge_vad_length.py | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/test_merge_vad_length.py diff --git a/docs/python_api.md b/docs/python_api.md index 160a66273..f95a5ba27 100644 --- a/docs/python_api.md +++ b/docs/python_api.md @@ -42,7 +42,7 @@ The wrapper accepts `**kwargs`, not a universally validated parameter schema. A | `batch_size_s` | `300` | VAD-path batching budget in seconds, based on longest padded segment times segment count, not a hard file-duration limit. CPU VAD processing uses single-segment batches. | | `batch_size_threshold_s` | `60` | VAD batching threshold for segment duration, not a VAD splitting setting. | | `merge_vad` | `False` | Enables merging detected VAD regions for this call. | -| `merge_length_s` | `15` | VAD merge target read before call overrides are merged into ASR config. Set this at construction in this checkout. | +| `merge_length_s` | `15` | Target length in seconds for merged VAD regions when `merge_vad=True`. A per-call value overrides the construction value. | | `cache` | No wrapper-managed session | Pass a caller-owned dictionary on every streaming call. It is not a hub/model download cache. | | `sentence_timestamp` | `False` | Requests sentence records in the VAD pipeline; depends on available timestamps and punctuation, with limited VAD fallback. | | `return_spk_res` | `True` | Includes clustered speaker results when VAD and a speaker model are configured. `False` does not avoid construction or segment embedding computation. | diff --git a/docs/python_api_zh.md b/docs/python_api_zh.md index 42d9a4a13..1f70e6d90 100644 --- a/docs/python_api_zh.md +++ b/docs/python_api_zh.md @@ -42,7 +42,7 @@ | `batch_size_s` | `300` | VAD 路径的秒级批处理预算,按最长补齐片段长度乘以片段数量估算,不是文件时长硬上限。CPU 的 VAD 路径按单片段处理。 | | `batch_size_threshold_s` | `60` | VAD 片段参与组批的时长阈值,不是 VAD 切分参数。 | | `merge_vad` | `False` | 为本次调用启用 VAD 区域合并。 | -| `merge_length_s` | `15` | VAD 合并目标,在本次参数合并进 ASR 配置之前读取。当前代码版本应在构建时设置它。 | +| `merge_length_s` | `15` | `merge_vad=True` 时合并后 VAD 片段的目标长度,单位为秒。本次调用传入的值会覆盖构建时设置的值。 | | `cache` | 包装层不维护会话 | 每次流式调用都传入调用方持有的字典。它不是模型下载缓存。 | | `sentence_timestamp` | `False` | 请求 VAD 流程输出句子记录,依赖实际时间戳与标点,部分情形可回退到 VAD 区域。 | | `return_spk_res` | `True` | 配置 VAD 与说话人模型时输出聚类结果。设为 `False` 并不能省去模型构建或片段说话人特征计算。 | diff --git a/funasr/auto/auto_model.py b/funasr/auto/auto_model.py index 8ec9fcbce..b8320dfa7 100644 --- a/funasr/auto/auto_model.py +++ b/funasr/auto/auto_model.py @@ -1043,10 +1043,10 @@ def inference_with_vad(self, input, input_len=None, **cfg): # FIX(gcf): concat the vad clips for sense vocie model for better aed if cfg.get("merge_vad", False): + # Call options are merged into kwargs only after this point. + merge_length_s = cfg.get("merge_length_s", kwargs.get("merge_length_s", 15)) for i in range(len(res)): - res[i]["value"] = merge_vad( - res[i]["value"], kwargs.get("merge_length_s", 15) * 1000 - ) + res[i]["value"] = merge_vad(res[i]["value"], merge_length_s * 1000) # step.2 compute asr model model = self.model diff --git a/tests/test_merge_vad_length.py b/tests/test_merge_vad_length.py new file mode 100644 index 000000000..f14fc952d --- /dev/null +++ b/tests/test_merge_vad_length.py @@ -0,0 +1,81 @@ +"""Regression tests for the merge_length_s option of AutoModel.generate().""" + +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np + + +VAD_SEGMENTS = [[0, 4000], [5000, 9000], [10000, 14000]] + + +class TestMergeVadLength(unittest.TestCase): + def _make_auto_model(self, **init_kwargs): + from funasr.auto.auto_model import AutoModel + + am = AutoModel.__new__(AutoModel) + am.model = MagicMock() + am.vad_model = MagicMock() + am.punc_model = None + am.punc_kwargs = {} + am.spk_model = None + am.spk_kwargs = {} + am.cb_model = None + am.spk_mode = None + 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, + **init_kwargs, + } + am._reset_runtime_configs = MagicMock() + + def mock_inference(data, input_len=None, model=None, kwargs=None, **cfg): + if model is am.vad_model: + return [{"key": "utt", "value": [list(s) for s in VAD_SEGMENTS]}] + return [{"text": "x"} for _ in data] + + am.inference = MagicMock(side_effect=mock_inference) + return am + + @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 _asr_segments(self, am, mock_prep, mock_load, mock_slice, **cfg): + mock_prep.return_value = (["utt"], [np.zeros(16000 * 14, dtype=np.float32)]) + mock_load.return_value = np.zeros(16000 * 14, dtype=np.float32) + mock_slice.side_effect = lambda speech, speech_lengths, segments: ( + [np.zeros(16000, dtype=np.float32) for _ in segments], + [16000 for _ in segments], + ) + + am.inference_with_vad("dummy_input", merge_vad=True, **cfg) + + segments = [] + for call in mock_slice.call_args_list: + segments.extend(seg for seg, _ in call.args[2]) + return sorted(segments) + + def test_default_merge_length_is_15_seconds(self): + am = self._make_auto_model() + self.assertEqual(self._asr_segments(am), [[0, 14000]]) + + def test_generate_merge_length_s_is_applied(self): + am = self._make_auto_model() + self.assertEqual(self._asr_segments(am, merge_length_s=10), [[0, 9000], [9000, 14000]]) + + def test_construction_merge_length_s_is_applied(self): + am = self._make_auto_model(merge_length_s=10) + self.assertEqual(self._asr_segments(am), [[0, 9000], [9000, 14000]]) + + def test_generate_merge_length_s_overrides_construction_value(self): + am = self._make_auto_model(merge_length_s=10) + self.assertEqual(self._asr_segments(am, merge_length_s=15), [[0, 14000]]) + + +if __name__ == "__main__": + unittest.main()