Repository navigation
model: Add NVIDIA Canary-1B-v2 to Transformers #46825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
36767d1
403a6df
c9a82bf
86a5cf1
210d2ee
9c2e8b3
769b816
d471d82
4537446
e4bd5a9
5784717
11a160a
e029972
3afbfaa
60c4ffc
43b3a1a
f9cfb7e
65e9ecb
3a59a9e
a0c120e
26d6acb
aa463c9
7fbcd26
bc48e52
2ae352f
b18b94c
1af705d
0240976
aedeb7e
0993cdb
83af420
1fa2ec6
e751207
bec2944
646fc61
f7b6e99
5dc16a3
cb50a15
3904da9
88dd3d6
077e790
5cfdb93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| <!--Copyright 2026 The HuggingFace Team. All rights reserved. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with | ||
| the License. You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
| an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
| specific language governing permissions and limitations under the License. | ||
|
|
||
| ⚠️ Note that this file is in Markdown but contains specific syntax for our doc-builder (similar to MDX) that may not be | ||
| rendered properly in your Markdown viewer. | ||
|
|
||
| --> | ||
| *This model was published in HF papers on 2025-09-17 and contributed to Hugging Face Transformers on 2026-07-29.* | ||
|
|
||
| <div class="flex flex-wrap space-x-1"> | ||
| <img alt="SDPA" src="https://img.shields.io/badge/SDPA-DE3412?style=flat&logo=pytorch&logoColor=white"> | ||
| </div> | ||
|
|
||
| # Canary | ||
|
|
||
| ## Overview | ||
|
|
||
| Canary-1B-v2 was proposed in [Canary-1B-v2 & Parakeet-TDT-0.6B-v3: Efficient and High-Performance Models for Multilingual ASR and AST](https://huggingface.co/papers/2509.14128) by Monica Sekoyan, Nithin Rao Koluguri, Nune Tadevosyan, Piotr Zelasko, Travis Bartley, Nikolay Karpov, Jagadeesh Balam, and Boris Ginsburg. | ||
|
|
||
| The abstract from the paper is the following: | ||
|
|
||
| *This report introduces Canary-1B-v2, a fast, robust multilingual model for Automatic Speech Recognition (ASR) and Speech-to-Text Translation (AST). Built with a FastConformer encoder and Transformer decoder, it supports 25 European languages. The model was trained on 1.7M hours of total data samples, including Granary and NeMo ASR Set 3.0, with non-speech audio added to reduce hallucinations for ASR and AST. We describe its two-stage pre-training and fine-tuning process with dynamic data balancing, as well as experiments with an nGPT encoder. Results show nGPT scales well with massive data, while FastConformer excels after fine-tuning. For timestamps, Canary-1B-v2 uses the NeMo Forced Aligner (NFA) with an auxiliary CTC model, providing reliable segment-level timestamps for ASR and AST. Evaluations show Canary-1B-v2 outperforms Whisper-large-v3 on English ASR while being 10× faster, and delivers competitive multilingual ASR and AST performance against larger models like Seamless-M4T-v2-large and LLM-based systems. We also release Parakeet-TDT-0.6B-v3, a successor to v2, offering multilingual ASR across the same 25 languages with just 600M parameters.* | ||
|
|
||
| Canary reuses the [Fast Conformer](https://huggingface.co/papers/2305.05084) encoder from [Parakeet](./parakeet.md) (loaded through [`ParakeetEncoder`] / [`ParakeetEncoderConfig`]) and pairs it with a Transformer decoder that uses fixed sinusoidal positional embeddings, cross-attention to the encoder outputs and tied input/output embeddings. The task is selected through a decoder prompt prefix built by [`CanaryProcessor`] of the form `<|startofcontext|> <|startoftranscript|> <|emo:undefined|> <source_lang> <target_lang> <pnc|nopnc> <|noitn|> <|notimestamp|> <|nodiarize|>`, where `source_lang == target_lang` selects transcription and otherwise selects translation. | ||
|
|
||
| The original implementation can be found in [NVIDIA NeMo](https://github.com/NVIDIA/NeMo). A model checkpoint is available at [harshaljanjani/canary-1b-v2-hf](https://huggingface.co/harshaljanjani/canary-1b-v2-hf). | ||
|
|
||
|
ebezzam marked this conversation as resolved.
Resolved
|
||
| This model was contributed by [Harshal Janjani](https://huggingface.co/harshaljanjani). | ||
|
|
||
| ## Usage | ||
|
ebezzam marked this conversation as resolved.
Resolved
|
||
|
|
||
| ### Transcription | ||
|
|
||
| The simplest way to transcribe audio is with `apply_transcription_request`, which builds the multitask decoder prompt for you (it is a convenience wrapper for `apply_chat_template`). | ||
|
|
||
| ```python | ||
| from datasets import load_dataset, Audio | ||
| from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq | ||
|
|
||
| processor = AutoProcessor.from_pretrained("harshaljanjani/canary-1b-v2-hf") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO (when HF model merged): set checkpoint here and elsewhere to |
||
| model = AutoModelForSpeechSeq2Seq.from_pretrained("harshaljanjani/canary-1b-v2-hf", device_map="auto") | ||
|
|
||
| ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") | ||
| ds = ds.cast_column("audio", Audio(sampling_rate=processor.feature_extractor.sampling_rate)) | ||
|
|
||
| inputs = processor.apply_transcription_request(audio=ds[0]["audio"]["array"], source_language="en").to(model.device) | ||
| generated_ids = model.generate(**inputs, max_new_tokens=128) | ||
| print(processor.decode(generated_ids, skip_special_tokens=True)[0]) | ||
| ``` | ||
|
|
||
| ### Translation | ||
|
|
||
| Set `target_language` to a different language than `source_language` for speech-to-text translation. | ||
|
|
||
| ```python | ||
| inputs = processor.apply_transcription_request( | ||
| audio=ds[0]["audio"]["array"], source_language="en", target_language="de" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw more fmi but what are the defaults here? would it use en as default?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. default for source_language is "en", and target_language is None (as default is to not translate) |
||
| ).to(model.device) | ||
| generated_ids = model.generate(**inputs, max_new_tokens=128) | ||
| print(processor.decode(generated_ids, skip_special_tokens=True)[0]) | ||
| ``` | ||
|
|
||
| ### Batch inference | ||
|
|
||
| Pass a list of audios and, optionally, a list of `source_language` / `target_language`. | ||
|
|
||
| ```python | ||
| audios = [ds[0]["audio"]["array"], ds[1]["audio"]["array"]] | ||
| inputs = processor.apply_transcription_request( | ||
| audio=audios, source_language="en", target_language=["en", "de"] | ||
|
ebezzam marked this conversation as resolved.
Resolved
|
||
| ).to(model.device) | ||
| generated_ids = model.generate(**inputs, max_new_tokens=128) | ||
| for text in processor.decode(generated_ids, skip_special_tokens=True): | ||
| print(text) | ||
| ``` | ||
|
|
||
| ### Torch compile | ||
|
|
||
| For autoregressive transcription, `torch.compile` accelerates the per-token forward passes inside `generate` by providing a `CompileConfig` object. | ||
|
|
||
| ```python | ||
| from transformers import CompileConfig | ||
|
ebezzam marked this conversation as resolved.
Resolved
|
||
|
|
||
| inputs = processor.apply_transcription_request(audio=ds[0]["audio"]["array"], source_language="en").to(model.device) | ||
| compile_config = CompileConfig() | ||
|
|
||
| # Warmup | ||
| for _ in range(3): | ||
| _ = model.generate(**inputs, max_new_tokens=128, cache_implementation="static", compile_config=compile_config) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cache implementation is not necessary if we have compile config no?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think because my Qwen3 ASR example from here was copied, and because I probably saw that type of usage in the docstrings here. @vasqu should we remove on another note: @harshaljanjani I'm getting the following warning when running a compile example can you look into it? below is my script from transformers import CompileConfig
from datasets import load_dataset, Audio
from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq
processor = AutoProcessor.from_pretrained("harshaljanjani/canary-1b-v2-hf")
model = AutoModelForSpeechSeq2Seq.from_pretrained("harshaljanjani/canary-1b-v2-hf", device_map="auto")
ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation")
ds = ds.cast_column("audio", Audio(sampling_rate=processor.feature_extractor.sampling_rate))
inputs = processor.apply_transcription_request(audio=ds[0]["audio"]["array"], source_language="en").to(model.device)
compile_config = CompileConfig()
# Warmup
for _ in range(3):
# _ = model.generate(**inputs, max_new_tokens=128, cache_implementation="static", compile_config=compile_config)
_ = model.generate(**inputs, max_new_tokens=128, compile_config=compile_config)
# Apply model
generated_ids = model.generate(**inputs, max_new_tokens=128, compile_config=compile_config)
# generated_ids = model.generate(**inputs, max_new_tokens=128, cache_implementation="static", compile_config=compile_config)
print(processor.decode(generated_ids, skip_special_tokens=True)[0])
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's from removing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep can confirm 👍 are you able to try with multi-GPU, e.g. running I get the following warning 4 times
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup here's the output I got 🤗
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raised #48364
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree this is weird, though I have no idea about where it's going. We also have the I think Cyril is cache maintainer so it's better to ask him
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @Cyrilvallez (sorry for all the pings 😬)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @harshaljanjani thanks for the explanations and opening the other PR! @vasqu how should we proceed with this PR so it isn't blocked: should we leave
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't block this PR! We can keep the static cache docs etc (and only update when we change the logic if we decide to do it) |
||
|
|
||
| # Apply model | ||
| generated_ids = model.generate(**inputs, max_new_tokens=128, cache_implementation="static", compile_config=compile_config) | ||
| print(processor.decode(generated_ids, skip_special_tokens=True)[0]) | ||
| ``` | ||
|
|
||
| ### Training | ||
|
|
||
| Canary can be trained with the loss outputted by the model. Build the decoder sequence from the multitask prompt followed by the target text, and mask the prompt in the labels. | ||
|
|
||
| ```python | ||
| import torch | ||
|
|
||
| model.train() | ||
| audio = ds[0]["audio"]["array"] | ||
| transcription = "mister Quilter is the apostle of the middle classes, and we are glad to welcome his gospel." | ||
|
|
||
| prompt = processor.apply_transcription_request(audio=audio, source_language="en").to(model.device) | ||
| target_ids = processor(audio=audio, text=transcription)["decoder_input_ids"].to(model.device) | ||
| decoder_input_ids = torch.cat([prompt["decoder_input_ids"], target_ids], dim=1) | ||
| labels = decoder_input_ids.clone() | ||
| labels[:, : prompt["decoder_input_ids"].shape[1]] = -100 | ||
|
ebezzam marked this conversation as resolved.
Resolved
|
||
|
|
||
| outputs = model( | ||
| input_features=prompt["input_features"], | ||
| attention_mask=prompt["attention_mask"], | ||
| decoder_input_ids=decoder_input_ids, | ||
| labels=labels, | ||
| ) | ||
| outputs.loss.backward() | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > Segment-level timestamps for Canary-1B-v2 are produced by the external NeMo Forced Aligner (NFA) with an auxiliary CTC model, not by the decoder, so they are not part of the `generate` output. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe would put it somewhere above close to the initial description. Feel like users would oftetimes not get to the end to see this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b18b94c by |
||
|
|
||
| ## CanaryConfig | ||
|
|
||
| [[autodoc]] CanaryConfig | ||
|
|
||
| ## CanaryProcessor | ||
|
|
||
| [[autodoc]] CanaryProcessor | ||
|
|
||
| ## CanaryModel | ||
|
|
||
| [[autodoc]] CanaryModel | ||
| - forward | ||
|
|
||
| ## CanaryForConditionalGeneration | ||
|
|
||
| [[autodoc]] CanaryForConditionalGeneration | ||
| - forward | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably cc @eustlb just in case |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -445,6 +445,104 @@ def make_list_of_audio_chat_template( | |
| return make_list_of_audio(audio) | ||
|
|
||
|
|
||
| def make_audio_chat_template_content(audio_item) -> dict: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. new utility to handle str input (for url or path) and array input |
||
| """ | ||
| Build a chat-template content dict for a single audio item. | ||
|
|
||
| Args: | ||
| audio_item (`str` or array-like): | ||
| A single audio item. Strings are treated as local paths or URLs; other values (numpy/torch arrays) are | ||
| forwarded directly. | ||
|
|
||
| Returns: | ||
| `dict`: A chat-template content dict, e.g. `{"type": "audio", "path": ...}` for strings or | ||
| `{"type": "audio", "audio": ...}` otherwise. | ||
| """ | ||
| if isinstance(audio_item, str): | ||
| return {"type": "audio", "path": audio_item} | ||
| return {"type": "audio", "audio": audio_item} | ||
|
|
||
|
|
||
| def resolve_language(language: str | None, code_to_name: dict[str, str], return_code: bool = True) -> str | None: | ||
| """ | ||
| Map a language code or name to its canonical form, with validation. | ||
|
|
||
| Accepts either a language code (e.g. ``"zh"``, ``"en"``) or a full name (e.g. ``"Chinese"``, ``"English"``) and | ||
| returns the canonical code or name depending on ``return_code``. ``None`` passes through unchanged (auto-detect). | ||
|
|
||
| Args: | ||
| language (`str` or `None`): | ||
| The language code or full name to resolve. ``None`` is returned unchanged. | ||
| code_to_name (`dict[str, str]`): | ||
| Mapping from language code to full language name for the model's supported languages. | ||
| return_code (`bool`, *optional*, defaults to `True`): | ||
| Whether to return the canonical language ``code``. If ``False``, returns the full language ``name``. | ||
|
|
||
| Returns: | ||
| `str` or `None`: The canonical language code or name, or ``None`` if ``language`` is ``None``. | ||
|
|
||
| Raises: | ||
| `ValueError`: If the language is not recognized. | ||
| """ | ||
| if language is None: | ||
| return None | ||
|
|
||
| language_lower = language.lower() | ||
| # Try code lookup first, then full-name lookup (both case-insensitive) | ||
| for code, name in code_to_name.items(): | ||
| if language_lower == code.lower() or language_lower == name.lower(): | ||
| return code if return_code else name | ||
|
|
||
| raise ValueError( | ||
| f"Unsupported language: {language!r}. Use a language code " | ||
| f"(e.g. 'en', 'zh') or full name (e.g. 'English', 'Chinese'). " | ||
| f"Supported codes: {sorted(code_to_name.keys())}. " | ||
| f"Supported names: {sorted(set(code_to_name.values()))}." | ||
| ) | ||
|
|
||
|
|
||
| def prepare_language_inputs( | ||
| language: str | list[str] | None, | ||
| batch_size: int, | ||
| code_to_name: dict[str, str], | ||
| allow_broadcast: bool = False, | ||
| return_code: bool = True, | ||
| ) -> list[str | None]: | ||
| """ | ||
| Broadcast and validate a language argument to match ``batch_size``. | ||
|
|
||
| Accepts language codes (e.g. ``"zh"``, ``"en"``) or full names (e.g. ``"Chinese"``, ``"English"``). Each value is | ||
| resolved to its canonical form via [`resolve_language`]. | ||
|
|
||
| Args: | ||
| language (`str`, `list[str]`, or `None`): | ||
| The language hint(s). A single value is broadcast to the whole batch; a list must match ``batch_size`` | ||
| (unless ``allow_broadcast`` is set). ``None`` disables language hints for the whole batch. | ||
| batch_size (`int`): | ||
| The number of samples in the batch. | ||
| code_to_name (`dict[str, str]`): | ||
| Mapping from language code to full language name for the model's supported languages. | ||
| allow_broadcast (`bool`, *optional*, defaults to `False`): | ||
| Whether a single-element list may be broadcast to the whole batch. | ||
| return_code (`bool`, *optional*, defaults to `True`): | ||
| Whether to return canonical language ``code``s. If ``False``, returns full language ``name``s. | ||
|
|
||
| Returns: | ||
| `list[str | None]`: The resolved language for each sample. | ||
| """ | ||
| if language is None: | ||
| return [None] * batch_size | ||
| if isinstance(language, str): | ||
| return [resolve_language(language, code_to_name, return_code)] * batch_size | ||
| if isinstance(language, (list, tuple)): | ||
| if allow_broadcast and len(language) == 1 and batch_size > 1: | ||
| return [resolve_language(language[0], code_to_name, return_code)] * batch_size | ||
| if len(language) != batch_size: | ||
| raise ValueError(f"Got {len(language)} language(s) for {batch_size} sample(s); counts must match.") | ||
| return [resolve_language(lang, code_to_name, return_code) for lang in language] | ||
| raise TypeError("`language` must be a string, a list of strings, or `None`.") | ||
|
|
||
|
|
||
| def hertz_to_mel(freq: float | np.ndarray, mel_scale: str = "htk") -> float | np.ndarray: | ||
| """ | ||
| Convert frequency from hertz to mels. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1876,6 +1876,17 @@ def tokenizer(self, proto): | |
| return tokenizer | ||
|
|
||
|
|
||
| class CanaryConverter(ParakeetConverter): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
| def __init__(self, vocab_file=None, *args): | ||
| super().__init__(vocab_file, *args) | ||
| # Only the `<|...|>` (type 4) control tokens are special; other type-4 pieces (digits, `<pad>`) are plain text. | ||
| self.special_tokens = { | ||
| piece.piece | ||
| for piece in self.proto.pieces | ||
| if piece.type == 4 and piece.piece.startswith("<|") and piece.piece.endswith("|>") | ||
| } | ||
|
|
||
|
|
||
| def bytes_to_unicode(): | ||
| """ | ||
| Returns list of utf-8 byte and a mapping to unicode strings. We specifically avoids mapping to whitespace/control | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok so this is the simplification tied to the doc change? Would it change anything if we still passed the kwarg?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no the docchange is something separate, just added a comment for the reason this change is to create a this common utility shared across multiple processors |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Copyright 2026 The HuggingFace Team. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from ...utils import _LazyModule | ||
| from ...utils.import_utils import define_import_structure | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
| from .configuration_canary import * | ||
| from .modeling_canary import * | ||
| from .processing_canary import * | ||
| else: | ||
| import sys | ||
|
|
||
| _file = globals()["__file__"] | ||
| sys.modules[__name__] = _LazyModule(__name__, _file, define_import_structure(_file), module_spec=__spec__) |

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: let's check with NVIDIA about adding the Transformers checkpoint to their model card, as NeMo and Transformers files can exist in the same repo!
Can you open a PR on the HF Hub to add the Transformers file and usage? Like this and this as one PR. And if you can mention this PR + indicate that your HF PR is a draft so that they don't merge it just yet. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Raised https://huggingface.co/nvidia/canary-1b-v2/discussions/27
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yea what's the state here? Any response or should we use "revision" for now instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when we're ready to merge, we can ask @nithinraok!