diff --git a/src/granite_switch/vllm/audio/asr.py b/src/granite_switch/vllm/audio/asr.py index 88eab82b..d141ca10 100644 --- a/src/granite_switch/vllm/audio/asr.py +++ b/src/granite_switch/vllm/audio/asr.py @@ -199,6 +199,7 @@ def _load_chunking(): # Sample rate every supported ASR front-end expects. _TARGET_SAMPLE_RATE = 16_000 +_NO_SPEECH_TEXT = "<|nospeech|>" # Audio item shapes vLLM may pass to a multimodal processor. AudioInput = Union[ @@ -320,7 +321,11 @@ def _run_pipeline( **call_kwargs, ) text = result["text"] if isinstance(result, dict) else str(result) - return text.strip() + text = text.strip() + # Some multilingual CTC checkpoints emit this control token for + # silence or non-speech. Treat it as an empty transcript so it cannot + # be inserted into the LLM prompt as if it were user speech. + return "" if text == _NO_SPEECH_TEXT else text # ── Module-level cache + convenience function ──────────────────────────────── diff --git a/tests/unit/test_asr.py b/tests/unit/test_asr.py index 7d26d750..698b3076 100644 --- a/tests/unit/test_asr.py +++ b/tests/unit/test_asr.py @@ -113,6 +113,14 @@ def test_transcribe_strips_and_uses_target_rate(self): passed = fake_pipe.call_args_list[-1][0][0] assert passed["sampling_rate"] == 16000 + def test_transcribe_drops_no_speech_control_token(self): + t = asr.ASRTranscriber(model_id="x", device="cpu") + t._pipeline = mock.Mock(return_value={"text": " <|nospeech|> "}) + + out = t.transcribe(np.zeros(1600, dtype=np.float32), sampling_rate=16000) + + assert out == "" + def test_load_is_idempotent_when_pipeline_set(self): # Once the pipeline is loaded, load() must early-return (no rebuild). t = asr.ASRTranscriber(model_id="x", device="cpu")