Automatic Speech Recognition
Transformers
Safetensors
jais_asr
text-generation
jais
ifm
arabic
arabic-dialects
speech-to-text
code-switching
diacritization
language-identification
open-weights
custom_code
Instructions to use IFM/Jais-ASR with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use IFM/Jais-ASR with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="IFM/Jais-ASR", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("IFM/Jais-ASR", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download language.py from IFM/Jais-ASR: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/IFM/Jais-ASR/resolve/main/language.py
- Command line
-
hf download hf://IFM/Jais-ASR/language.py
-
curl -L -o language.py https://huggingface.co/IFM/Jais-ASR/resolve/main/language.py
12.3 kB
| """Choosing the transcription language in advance. | |
| By default Jais-ASR decides for itself, at one generated position, between abstaining | |
| (`[IFM-NoTranscript]`) and a language/dialect tag. This module lets a caller pin that | |
| language WITHOUT taking away the model's ability to abstain. | |
| Why that needs care | |
| ------------------- | |
| The obvious implementation -- restrict the decision to {pinned_tag, IF} -- silently | |
| changes the abstention rate: when the pinned tag is not the model's own first choice, | |
| the refusal token can outscore it even on clear speech, and the model abstains where | |
| it would have transcribed. So the two decisions are read separately, from the same | |
| untouched logits: | |
| abstain iff logit(IF) > max over ALL tags <- compare against all language tags | |
| else argmax over the ALLOWED tags <- the caller's pin | |
| The abstain/transcribe decision is compared against the model's OWN best tag, never the | |
| pinned one, so pinning does not change this comparison at a fixed decision prefix. Only which tag gets written changes. | |
| Two ways to use it, with different guarantees: | |
| constrained `LanguagePin` logits processor. Preserves the strict score comparison | |
| above (ties transcribe), not unconstrained sampling probabilities. Needs the | |
| processor to be passed to generate(). | |
| prefill write `[EG]` into the prompt (chat template `language_prefill=`). Works in any | |
| engine with no extra code, but the tag is then in the PROMPT, so | |
| `[IFM-NoTranscript]` can never be produced -- you always get text, even | |
| for silence. Use it when you want a transcript no matter what. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import math | |
| try: | |
| from .postprocess import ( | |
| LANGUAGE_TAG_IDS, | |
| BRACKET_OPEN_ID, | |
| BRACKET_CLOSE_ID, | |
| has_prefilled_tag, | |
| THINK_OPEN_ID, | |
| THINK_CLOSE_ID, | |
| ) | |
| except ImportError: | |
| if __package__: | |
| raise | |
| from postprocess import ( | |
| LANGUAGE_TAG_IDS, | |
| BRACKET_OPEN_ID, | |
| BRACKET_CLOSE_ID, | |
| has_prefilled_tag, | |
| THINK_OPEN_ID, | |
| THINK_CLOSE_ID, | |
| ) | |
| __all__ = [ | |
| "SUPPORTED_LANGUAGES", | |
| "resolve_language", | |
| "LanguagePin", | |
| "prefill_for", | |
| "resolve_language_controls", | |
| ] | |
| REFUSAL_DECISION_ID = 5126 # 'IF', the continuation of [IFM-NoTranscript] | |
| TAG_TO_ID = {t: i for i, t in LANGUAGE_TAG_IDS.items()} | |
| #: the 19 country codes plus MSA -- everything the model treats as Arabic | |
| ARABIC_TAGS = ( | |
| "msa", | |
| "EG", | |
| "SA", | |
| "MA", | |
| "IQ", | |
| "JO", | |
| "SY", | |
| "QA", | |
| "DZ", | |
| "LB", | |
| "OM", | |
| "YE", | |
| "TN", | |
| "MR", | |
| "KW", | |
| "SD", | |
| "LY", | |
| "AE", | |
| "PS", | |
| "BH", | |
| ) | |
| #: ISO 639-1 -> the tags that count as that language. `ar` deliberately maps to ALL the | |
| #: Arabic tags: pinning "Arabic" should let the model keep choosing the dialect. | |
| ISO_TO_TAGS = { | |
| "ar": ARABIC_TAGS, | |
| "en": ("en",), | |
| "fr": ("fr",), | |
| "es": ("es",), | |
| "it": ("it",), | |
| } | |
| #: what vLLM's SupportsTranscription advertises on /v1/audio/transcriptions -- | |
| #: the ISO codes plus every concrete Jais-ASR tag, so dialect pinning is reachable | |
| SUPPORTED_LANGUAGES = { | |
| "ar": "Arabic", | |
| "en": "English", | |
| "fr": "French", | |
| "es": "Spanish", | |
| "it": "Italian", | |
| "msa": "Arabic (Modern Standard)", | |
| **{t: f"Arabic ({t})" for t in ARABIC_TAGS if t != "msa"}, | |
| } | |
| def resolve_language(language): | |
| """Normalise a caller's language spec into the set of allowed tags. | |
| Accepts an ISO 639-1 code ("ar"), a model tag ("EG", "msa"), a list of either, or | |
| None for "no restriction". Raises on anything unrecognised rather than silently | |
| falling back to auto -- a typo that quietly disables pinning is worse than an error. | |
| """ | |
| if language is None: | |
| return None | |
| if not isinstance(language, (str, list, tuple)): | |
| raise ValueError( | |
| "language must be a tag, ISO code, non-empty list/tuple, or None" | |
| ) | |
| specs = [language] if isinstance(language, str) else list(language) | |
| if not specs: | |
| raise ValueError("language list must not be empty; use None for no restriction") | |
| tags = [] | |
| for s in specs: | |
| if not isinstance(s, str): | |
| raise ValueError("each language must be a string tag or ISO code") | |
| s = s.strip() | |
| if s in ISO_TO_TAGS: | |
| tags.extend(ISO_TO_TAGS[s]) | |
| elif s in TAG_TO_ID: | |
| tags.append(s) | |
| elif s.lower() in ISO_TO_TAGS: | |
| tags.extend(ISO_TO_TAGS[s.lower()]) | |
| else: | |
| raise ValueError( | |
| f"unknown language {s!r}. Use an ISO 639-1 code " | |
| f"({', '.join(sorted(ISO_TO_TAGS))}) or a model tag " | |
| f"({', '.join(sorted(TAG_TO_ID))})." | |
| ) | |
| seen, out = set(), [] | |
| for t in tags: | |
| if t not in seen: | |
| seen.add(t) | |
| out.append(t) | |
| return out | |
| def prefill_for(language): | |
| """The literal prefix for the PREFILL path, e.g. 'EG' -> '[EG]'. | |
| Only a single concrete tag can be prefilled -- you cannot write a set into a prompt. | |
| Returns None when the spec resolves to more than one tag. The prefill ends at ']': | |
| the space before the first transcript word is the model's own to produce. | |
| """ | |
| tags = resolve_language(language) | |
| if not tags or len(tags) != 1: | |
| return None | |
| return f"[{tags[0]}]" | |
| class LanguagePin: | |
| """Pin the language while leaving the abstention decision untouched. | |
| Engine-agnostic: `apply(generated_ids, logits)` is the whole rule. Thin adapters for | |
| transformers (`__call__`) and for any engine that hands over (token_ids, logits). | |
| pin = LanguagePin("EG") # or "ar", or ["EG", "MA"] | |
| model.generate(**inputs, logits_processor=[pin.for_transformers(prompt_len)]) | |
| """ | |
| def __init__(self, language, force_bracket=True): | |
| self.tags = resolve_language(language) | |
| self.allowed_ids = [TAG_TO_ID[t] for t in self.tags] if self.tags else None | |
| self.all_tag_ids = list(LANGUAGE_TAG_IDS) | |
| self.force_bracket = force_bracket | |
| def apply(self, generated_ids, logits, *, thinking=False): | |
| """Constrain the decision zone in-place and return the score vector. | |
| Prompt tokens must be excluded. If the prompt opens a thinking block, | |
| pass thinking=True: scores are untouched until the generated closing | |
| marker, and the decision zone starts immediately after that marker. | |
| The adapters infer this flag from the prompt automatically. | |
| """ | |
| if self.allowed_ids is None: | |
| return logits | |
| if thinking: | |
| for index, token_id in enumerate(generated_ids): | |
| if int(token_id) == THINK_CLOSE_ID: | |
| generated_ids = generated_ids[index + 1 :] | |
| break | |
| else: | |
| return logits | |
| n_generated = len(generated_ids) | |
| if n_generated > 2: | |
| return logits | |
| if n_generated == 0: | |
| return _keep_only(logits, BRACKET_OPEN_ID) if self.force_bracket else logits | |
| # With force_bracket=False, non-tag output must remain untouched. | |
| if int(generated_ids[0]) != BRACKET_OPEN_ID: | |
| return logits | |
| if n_generated == 2: | |
| if int(generated_ids[1]) in self.allowed_ids: | |
| return _keep_only(logits, BRACKET_CLOSE_ID) | |
| return logits # Let [IFM-NoTranscript] continue naturally. | |
| # Read both decisions before masking. Refusal must strictly beat every tag; | |
| # ties transcribe. This is a deterministic score rule, not preservation of | |
| # the probability distribution of unconstrained sampling. | |
| best_tag = max(self.all_tag_ids, key=lambda i: float(logits[i])) | |
| abstains = float(logits[REFUSAL_DECISION_ID]) > float(logits[best_tag]) | |
| keep_id = ( | |
| REFUSAL_DECISION_ID | |
| if abstains | |
| else max(self.allowed_ids, key=lambda i: float(logits[i])) | |
| ) | |
| return _keep_only(logits, keep_id) | |
| def for_transformers(self, prompt_len): | |
| """Bind to the padded prompt width; supports batch and expanded beam rows.""" | |
| if not isinstance(prompt_len, int) or prompt_len <= 0: | |
| raise ValueError("prompt_len must be the positive padded prompt width") | |
| outer = self | |
| class _Proc: | |
| def __call__(self, input_ids, scores): | |
| if input_ids.shape[1] < prompt_len: | |
| raise ValueError("input_ids is shorter than the bound prompt_len") | |
| if outer.allowed_ids is None: | |
| return scores | |
| # Stateless normal-prompt fast path after the decision zone. | |
| if input_ids.shape[1] - prompt_len > 2: | |
| if bool((input_ids[:, prompt_len - 1] == THINK_CLOSE_ID).all()): | |
| return scores | |
| for b in range(scores.shape[0]): | |
| prompt = input_ids[b, :prompt_len] | |
| if has_prefilled_tag(prompt): | |
| continue | |
| outer.apply( | |
| input_ids[b, prompt_len:], | |
| scores[b], | |
| thinking=has_open_thinking(prompt), | |
| ) | |
| return scores | |
| return _Proc() | |
| def _keep_only(logits, token_id): | |
| keep = logits[token_id].clone() if hasattr(logits, "clone") else logits[token_id] | |
| if not math.isfinite(float(keep)): | |
| raise ValueError( | |
| "language pin selected a token without a finite score; check conflicting " | |
| "token constraints or invalid logits" | |
| ) | |
| logits[:] = float("-inf") | |
| logits[token_id] = keep | |
| return logits | |
| def has_open_thinking(prompt_ids): | |
| """Whether the final thinking delimiter in the prompt is an opening one.""" | |
| # Tensor reversal copies the prompt and iteration unbinds every element. | |
| # Index backward so a final closing delimiter costs constant work. | |
| for index in range(len(prompt_ids) - 1, -1, -1): | |
| token_id = int(prompt_ids[index]) | |
| if token_id == THINK_CLOSE_ID: | |
| return False | |
| if token_id == THINK_OPEN_ID: | |
| return True | |
| return False | |
| #: The exact prompt contract, rendered here so engines that build their own prompt | |
| #: (vLLM's transcription endpoint) do not have to run the jinja template. Kept in | |
| #: exact agreement with chat_template.jinja: the system turn is rendered only when | |
| #: there is system content. | |
| GENERATION_PROMPT = ( | |
| "<|ifm|begin_of_text|>" | |
| "{system_turn}" | |
| "<|ifm|im_start|>user\n{audio}<|ifm|im_end|>" | |
| "<|ifm|im_start|>assistant\n<ifm|think>\n</ifm|think>{prefill}" | |
| ) | |
| SYSTEM_TURN = "<|ifm|im_start|>system\n{system}<|ifm|im_end|>" | |
| AUDIO_TOKEN = "<|ifm|audio|>" | |
| def resolve_language_controls( | |
| language=None, language_prefill=None, *, enable_thinking=False | |
| ): | |
| """Validate mutually exclusive pin/prefill options and canonicalize prefill.""" | |
| if language is not None and language_prefill is not None: | |
| raise ValueError("Use either language (pinning) or language_prefill, not both") | |
| if language is not None: | |
| resolve_language(language) | |
| if language_prefill is not None: | |
| if not isinstance(language_prefill, str): | |
| raise ValueError("language_prefill must be one concrete language tag") | |
| tags = resolve_language(language_prefill) | |
| if len(tags) != 1: | |
| raise ValueError( | |
| "language_prefill must resolve to one tag; use language for a family" | |
| ) | |
| if enable_thinking: | |
| raise ValueError( | |
| "language_prefill cannot be combined with enable_thinking=True" | |
| ) | |
| language_prefill = tags[0] | |
| return language, language_prefill | |
| def build_prompt(system="", language_prefill=None, audio_token=AUDIO_TOKEN): | |
| """Render the ASR prompt; only language_prefill inserts a decision tag.""" | |
| _, tag = resolve_language_controls(language_prefill=language_prefill) | |
| return GENERATION_PROMPT.format( | |
| system_turn=SYSTEM_TURN.format(system=system) if system else "", | |
| audio=audio_token, | |
| prefill=f"[{tag}]" if tag else "", | |
| ) | |