[Bugfix] Build the forced-aligner prompt without a chat template (word timestamps one bin late) (#7240)

Signed-off-by: Tianyao Wu <rayroy31@gmail.com>
This commit is contained in:
Tianyao Wu authored and GitHub committed 2026-09-08 19:27:38 +00:00
1 parent 5e4baea64d
commit b3dd45874a
3 files changed
+28 -13

No files matched your search

@@ -10,7 +10,21 @@ def test_build_prompt_has_boundary_timestamp_markers():
assert prompt.count("<timestamp>") == 4
assert "hello<timestamp><timestamp>world" 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}hello<timestamp><timestamp>world<timestamp><timestamp>"
assert "<|im_start|>" not in prompt
assert processor.build_prompt([]) == f"{processor.AUDIO_PLACEHOLDER}<timestamp><timestamp>"
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(
@@ -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
+10 -10
View File
@@ -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 ``<timestamp>`` markers
(start + end) that the model classifies into audio time bins.
Each word gets two trailing ``<timestamp>`` 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) ----------------