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
File size: 12,298 Bytes
1323995 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | """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 "",
)
|