[Qwen3ASR] Add hotword parsing, and fix language parsing and training.#47111
[Qwen3ASR] Add hotword parsing, and fix language parsing and training.#47111ebezzam wants to merge 8 commits into
Conversation
| # - 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 = ( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
adding prompt arg like VibeVoice ASR here
| # 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, | ||
| ) |
There was a problem hiding this comment.
if we push the new chat template, we can call self.apply_chat_template with tokenize=True
| # 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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| 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 [ |
There was a problem hiding this comment.
bug in preparing labels!
AudioFlamingo3 also affected here, and as a result GlmASR and MusicFlamingo which do modular from this processor.
There was a problem hiding this comment.
yep same comment util worthy!
|
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. |
|
run-slow: qwen3_asr |
|
This comment contains models: ["models/qwen3_asr"] |
vasqu
left a comment
There was a problem hiding this comment.
Some initial comments. I would nudge towards updating the templates remotely tbh as it will get messy otherwise
| 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: |
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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 [ |
There was a problem hiding this comment.
yep same comment util worthy!
| 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`.") |
There was a problem hiding this comment.
I would check whether something similar already exists? this looks failry standard and should work for most standard primitives
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
don't need, copied from vibevoice but we can keep as tensors
| # 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, | ||
| ) |
| @@ -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" | |||
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
@vasqu Opened PRs to the model cards:
- https://huggingface.co/Qwen/Qwen3-ASR-1.7B-hf/discussions/2
- https://huggingface.co/Qwen/Qwen3-ASR-0.6B-hf/discussions/3
When we are ready to merge:
- I will tell model authors to merge
- we set revision to main (or remove altogether cls.revision)
|
run-slow: qwen3_asr |
|
This comment contains models: ["models/qwen3_asr"] |
vasqu
left a comment
There was a problem hiding this comment.
LGTM, would be nice to wait for the merge on the hub before merging this one tho 🤗
| 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}]}) |
There was a problem hiding this comment.
Ah ok one is the user and one is the assistant I think I missed that last time. Really weird template
| return make_list_of_audio(audio) | ||
|
|
||
|
|
||
| def prepare_prompt_input( |
There was a problem hiding this comment.
maybe even processing utils or similar? not really sure if its audio only but super nit
There was a problem hiding this comment.
Lets be careful that the processor is not too heavy for CI?
There was a problem hiding this comment.
Sounds good, feel free to merge then
There was a problem hiding this comment.
thanks! waiting for the model card PRs to be merged
|
[For maintainers] Suggested jobs to run (before merge) run-slow: musicflamingo, qwen3_asr, vibevoice_asr |
CI recapDashboard: View test results in Grafana |
What does this PR do?
A few patches for Qwen3 ASR: