From b3dd45874a750f7edfa39bb02262804228e5ff7b Mon Sep 17 00:00:00 2001 From: Tianyao Wu <54675599+twu3202@users.noreply.github.com> Date: Wed, 9 Sep 2026 03:27:38 +0800 Subject: [PATCH] [Bugfix] Build the forced-aligner prompt without a chat template (word timestamps one bin late) (#7240) Signed-off-by: Tianyao Wu --- .../utils/test_qwen3_force_align_processor.py | 16 ++++++++++++++- .../deploy/qwen3_tts_forced_aligner.yaml | 5 +++-- .../utils/qwen3_force_align_processor.py | 20 +++++++++---------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/tests/utils/test_qwen3_force_align_processor.py b/tests/utils/test_qwen3_force_align_processor.py index 4a928d0e5..f846ac7a8 100644 --- a/tests/utils/test_qwen3_force_align_processor.py +++ b/tests/utils/test_qwen3_force_align_processor.py @@ -10,7 +10,21 @@ def test_build_prompt_has_boundary_timestamp_markers(): assert prompt.count("") == 4 assert "helloworld" in prompt - assert prompt.endswith("<|im_start|>assistant\n") + # Official aligner format: audio placeholder + words, no chat template + # (a leading "<|im_start|>user\n" shifts the predicted markers one bin late). + assert prompt == f"{processor.AUDIO_PLACEHOLDER}helloworld" + assert "<|im_start|>" not in prompt + assert processor.build_prompt([]) == f"{processor.AUDIO_PLACEHOLDER}" + + +def test_build_prompt_matches_official_encode_timestamp(): + pytest.importorskip("qwen_asr") + from qwen_asr.inference.qwen3_forced_aligner import Qwen3ForceAlignProcessor + + text = "It's 3 o'clock, 你好 world." + word_list, official_prompt = Qwen3ForceAlignProcessor().encode_timestamp(text, "english") + + assert processor.build_prompt(list(word_list)) == official_prompt @pytest.mark.parametrize( diff --git a/vllm_omni/deploy/qwen3_tts_forced_aligner.yaml b/vllm_omni/deploy/qwen3_tts_forced_aligner.yaml index 2aecdbf9b..b600ff1e1 100644 --- a/vllm_omni/deploy/qwen3_tts_forced_aligner.yaml +++ b/vllm_omni/deploy/qwen3_tts_forced_aligner.yaml @@ -5,8 +5,9 @@ # --forced-aligner sets just the model path. # # The stage runs the model with runner="pooling" + pooling_task="token_classify". -# Prompt tokens, chat template and word segmentation are Qwen-specific and live -# in qwen3_force_align_processor (not configurable here). +# Prompt tokens and word segmentation are Qwen-specific and live in +# qwen3_force_align_processor (not configurable here); the prompt is the same +# string qwen_asr's encode_timestamp builds, with no chat template. forced_aligner: model: Qwen/Qwen3-ForcedAligner-0.6B diff --git a/vllm_omni/utils/qwen3_force_align_processor.py b/vllm_omni/utils/qwen3_force_align_processor.py index e70d270db..76c5b9084 100644 --- a/vllm_omni/utils/qwen3_force_align_processor.py +++ b/vllm_omni/utils/qwen3_force_align_processor.py @@ -65,18 +65,18 @@ _LANG_ALIASES = { def build_prompt(words: list[str]) -> str: - """Wrap segmented words in the Qwen3 aligner prompt template. + """Build the Qwen3 aligner prompt exactly as qwen_asr's + ``Qwen3ForceAlignProcessor.encode_timestamp`` does: the audio placeholder + followed by the words, each with two trailing ```` markers + (start + end) that the model classifies into audio time bins. - Each word gets two trailing ```` markers (start + end); the - model classifies each marker into an audio time bin. + No chat template. The official aligner feeds this string straight to the + tokenizer; wrapping it in ``<|im_start|>user ... <|im_start|>assistant`` + puts three tokens in front of the audio and shifts almost every predicted + marker one 80 ms bin later than the official output. """ - if not words: - # Pad with one timestamp so the decoder always has something to - # read; an empty result still surfaces as "[]" upstream. - body = TIMESTAMP_TOKEN - else: - body = f"{TIMESTAMP_TOKEN}{TIMESTAMP_TOKEN}".join(words) + f"{TIMESTAMP_TOKEN}{TIMESTAMP_TOKEN}" - return f"<|im_start|>user\n{AUDIO_PLACEHOLDER}{body}<|im_end|>\n<|im_start|>assistant\n" + body = f"{TIMESTAMP_TOKEN}{TIMESTAMP_TOKEN}".join(words) + f"{TIMESTAMP_TOKEN}{TIMESTAMP_TOKEN}" + return f"{AUDIO_PLACEHOLDER}{body}" # --- word segmentation (port of Qwen3ForceAlignProcessor) ----------------