Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Qwen3ASR] Add hotword parsing, and fix language parsing and training.#47111

Open
ebezzam wants to merge 8 commits into
huggingface:mainfrom
ebezzam:qwenasr_hotwords
Open

[Qwen3ASR] Add hotword parsing, and fix language parsing and training.#47111
ebezzam wants to merge 8 commits into
huggingface:mainfrom
ebezzam:qwenasr_hotwords

Conversation

@ebezzam

@ebezzam ebezzam commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

CI

What does this PR do?

A few patches for Qwen3 ASR:

  • add support for custom hotwords, e.g. passing custom vocabulary to help transcription like VibeVoice. Original model card and repo didn't show such examples... but a user comment on the new checkpoints brought up this feature not handled by current code. And looking into original code, I see this context arg.
  • language hint should be handled differently
  • while doing above, I noticed a training bug which was copied from other models, because AudioFlamingo3, GlmASR, and MusicFlamingo were affected by this refactor (I'll open a separate PR to fix them)

# - assistant turns: rendered verbatim, wrapped in `{% generation %}` for assistant token masking.
# Used to prefill the forced language ("language <NAME><asr_text>") via
# `apply_chat_template(..., continue_final_message=True)` and for training targets.
ASR_CHAT_TEMPLATE = (

@ebezzam ebezzam Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would need to update chat templates of Qwen/Qwen3-ASR-1.7B-hf and Qwen/Qwen3-ASR-0.6B-hf. I can see with model authors if we do this direction

OR we could go with this approach to not have to update the chat template (but perhaps unconventional approach with apply_chat_template)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo its best to update the template to avoid headaches. This cries workaround an potentially messy maintenance

audio: AudioInput | list[AudioInput],
language: str | list[str] | None = None,
**kwargs,
prompt: str | list[str] | None = None,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding prompt arg like VibeVoice ASR here

Comment on lines +563 to +575
# Following the original implementation, the language is forced by prefilling the assistant
# turn with "language <NAME><asr_text>" so that the model only generates the transcription.
if self._template_renders_assistant_turns():
for messages, lang in zip(conversations, languages):
prefill = f"language {lang}<asr_text>" if lang is not None else ""
messages.append({"role": "assistant", "content": [{"type": "text", "text": prefill}]})
return self.apply_chat_template(
conversations,
tokenize=True,
return_dict=True,
continue_final_message=True,
**kwargs,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we push the new chat template, we can call self.apply_chat_template with tokenize=True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please

Comment on lines +577 to +589
# Legacy chat templates (before assistant-turn rendering was added) cannot express the
# prefill, so splice the forced-language suffix into the rendered text manually.
text = self.apply_chat_template(conversations, tokenize=False, add_generation_prompt=True)
text = [
rendered if lang is None else f"{rendered}language {lang}<asr_text>"
for rendered, lang in zip(text, languages)
]

sampling_rate = kwargs.get(
"sampling_rate", kwargs.get("audio_kwargs", {}).get("sampling_rate", self.feature_extractor.sampling_rate)
)
audio_arrays = [load_audio(item, sampling_rate=sampling_rate) for item in audio_items]
return self(text=text, audio=audio_arrays, **kwargs)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise we could leave the chat template as-is, but we wouldn't call self.apply_chat_template with tokenize=True and need to call the processor __call__ manually -> this would be the least breaking approach

return_dict=True,
**kwargs,
# Following the original implementation, the language is forced by prefilling the assistant
# turn with "language <NAME><asr_text>" so that the model only generates the transcription.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm_token_type_ids = model_inputs.pop("mm_token_type_ids")
labels = model_inputs["input_ids"].clone()
labels[mm_token_type_ids != 0] = -100 # audio positions
for token_id in [

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug in preparing labels!

AudioFlamingo3 also affected here, and as a result GlmASR and MusicFlamingo which do modular from this processor.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep same comment util worthy!

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@ebezzam

ebezzam commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

run-slow: qwen3_asr

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Workflow Run ⚙️

This comment contains run-slow, running the specified jobs:

models: ["models/qwen3_asr"]
quantizations: []

@ebezzam ebezzam requested a review from vasqu July 6, 2026 15:15
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

CI Results

Workflow Run ⚙️

Commit Info

Context Commit Description
RUN d167b695 workflow commit (merge commit)
PR e4cf30a3 branch commit (from PR)
main 1da9d1d4 base commit (on main)

✅ No failing test specific to this PR 🎉 👏 !

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments. I would nudge towards updating the templates remotely tbh as it will get messy otherwise

Comment thread docs/source/en/model_doc/qwen3_asr.md Outdated
print(text)
```

The language can be forced through the chat template by *prefilling* the assistant turn with `language <NAME><asr_text>` and passing `continue_final_message=True`, which is what `apply_transcription_request` does under the hood:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we kind of combine it with the previous instead with a small comment?

Iiuc then its possible not only to pass to the chat template but also give it as prefix?

# - assistant turns: rendered verbatim, wrapped in `{% generation %}` for assistant token masking.
# Used to prefill the forced language ("language <NAME><asr_text>") via
# `apply_chat_template(..., continue_final_message=True)` and for training targets.
ASR_CHAT_TEMPLATE = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo its best to update the template to avoid headaches. This cries workaround an potentially messy maintenance

mm_token_type_ids = model_inputs.pop("mm_token_type_ids")
labels = model_inputs["input_ids"].clone()
labels[mm_token_type_ids != 0] = -100 # audio positions
for token_id in [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep same comment util worthy!

Comment on lines +542 to +553
if prompt is None:
prompts = [None] * batch_size
elif isinstance(prompt, str):
prompts = [prompt] * batch_size
elif isinstance(prompt, (list, tuple)):
if len(prompt) != batch_size:
raise ValueError(
f"Received {len(prompt)} prompt(s) for {batch_size} audio sample(s); counts must match."
)
prompts = list(prompt)
else:
raise TypeError("`prompt` must be a string, a sequence of strings, or `None`.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would check whether something similar already exists? this looks failry standard and should work for most standard primitives

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find one, but creating prepare_prompt_input in audio utils because this was copied from VibeVoice, and future audio LMs may use this!

audio_items = make_list_of_audio_chat_template(audio)
audio_items = list(make_list_of_audio_chat_template(audio))
if is_torch_available():
audio_items = [el.detach().cpu().numpy() if isinstance(el, torch.Tensor) else el for el in audio_items]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

@ebezzam ebezzam Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need, copied from vibevoice but we can keep as tensors

Comment on lines +563 to +575
# Following the original implementation, the language is forced by prefilling the assistant
# turn with "language <NAME><asr_text>" so that the model only generates the transcription.
if self._template_renders_assistant_turns():
for messages, lang in zip(conversations, languages):
prefill = f"language {lang}<asr_text>" if lang is not None else ""
messages.append({"role": "assistant", "content": [{"type": "text", "text": prefill}]})
return self.apply_chat_template(
conversations,
tokenize=True,
return_dict=True,
continue_final_message=True,
**kwargs,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please

@@ -36,7 +36,7 @@ class Qwen3ASRProcessorTest(ProcessorTesterMixin, unittest.TestCase):
@classmethod
@require_torch
def setUpClass(cls):
cls.checkpoint = "Qwen/Qwen3-ASR-0.6B-hf"
cls.checkpoint = "bezzam/Qwen3-ASR-0.6B-hf"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can open a PR and add a revision for now no? Ideally no revision before merge then

self.assertTrue(decoded.endswith("<|im_start|>assistant\nlanguage English<asr_text>"))

@require_torch
def test_apply_transcription_request_with_prompt(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we have both language and prompt? Should they interact in a specific order or should we make it mutually exclusive etc? I think it would make sense to allow both but lan -> prompt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both are allowed! as seen in the chat template example (input repeated below)

  • prompts are in the user role
  • language is in the assistant role

their model was trained to have language forcing in the assistant role, so I guess we need to keep it that way?

chat_template = [
    [
        # Context/hotwords as system message
        {"role": "system", "content": [{"type": "text", "text": "Vocabulary: Quilter, apostle, gospel."}]},
        {
            "role": "user",
            "content": [
                {
                    "type": "audio",
                    "path": "https://huggingface.co/datasets/bezzam/audio_samples/resolve/main/librispeech_mr_quilter.wav",
                },
            ],
        },
        # empty prefill since forcing language in the other sample
        {"role": "assistant", "content": [{"type": "text", "text": ""}]},
    ],
    [
        {
            "role": "user",
            "content": [
                {
                    "type": "audio",
                    "path": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav",
                },
            ],
        },
        {"role": "assistant", "content": [{"type": "text", "text": "language Chinese<asr_text>"}]},
    ],
]

@require_torch
def setUpClass(cls):
cls.checkpoint = "Qwen/Qwen3-ASR-0.6B-hf"
cls.revision = "refs/pr/3" # TODO: set to main after merge

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vasqu Opened PRs to the model cards:

When we are ready to merge:

  • I will tell model authors to merge
  • we set revision to main (or remove altogether cls.revision)

@ebezzam

ebezzam commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

run-slow: qwen3_asr

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Workflow Run ⚙️

This comment contains run-slow, running the specified jobs:

models: ["models/qwen3_asr"]
quantizations: []

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

CI Results

Workflow Run ⚙️

Commit Info

Context Commit Description
RUN b5cbed78 workflow commit (merge commit)
PR caf37300 branch commit (from PR)
main 4af901d5 base commit (on main)

✅ No failing test specific to this PR 🎉 👏 !

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, would be nice to wait for the merge on the hub before merging this one tho 🤗

Comment on lines +535 to +546
for prompt_text, audio_item in zip(prompts, audio_items):
messages = []
if lang is not None:
messages.append({"role": "system", "content": [{"type": "text", "text": lang}]})
if prompt_text is not None:
messages.append({"role": "system", "content": [{"type": "text", "text": prompt_text}]})
messages.append({"role": "user", "content": [_audio_content_item(audio_item)]})
conversations.append(messages)

# The language is forced by prefilling the assistant turn with "language <NAME><asr_text>"
# so that the model only generates the transcription text in the desired language
for messages, lang in zip(conversations, languages):
prefill = f"language {lang}<asr_text>" if lang is not None else ""
messages.append({"role": "assistant", "content": [{"type": "text", "text": prefill}]})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok one is the user and one is the assistant I think I missed that last time. Really weird template

Comment thread src/transformers/audio_utils.py Outdated
return make_list_of_audio(audio)


def prepare_prompt_input(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe even processing utils or similar? not really sure if its audio only but super nit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets be careful that the processor is not too heavy for CI?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make a dummy one imo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI checked with @ydshieh. He's working on a script/workflow to make tiny model copies for the processor tests (e.g. see the tiny-processor models here). So if it passes with the current checkpoint, we'll let him create the tiny processor in a follow-up PR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, feel free to merge then

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! waiting for the model card PRs to be merged

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: musicflamingo, qwen3_asr, vibevoice_asr

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 29020934031:1
Result: failure | Jobs: 12 | Tests: 169,853 | Failures: 2 | Duration: 16h 51m

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants