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: 18,720 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 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | """Processor for Jais-ASR.
`processor(text=..., audio=..., sampling_rate=...)` takes one `<|ifm|audio|>`
placeholder per clip and expands it to `<audio>` + one placeholder per encoder frame
+ `</audio>`, so the model's `masked_scatter` lines up exactly with the frames the
audio tower emits.
Audio input is standardized automatically: pass `sampling_rate=` with whatever rate
your file has (a scalar, or one rate per clip) and it is resampled to the model's
16 kHz; multi-channel audio is downmixed to mono and integer PCM is converted to
float. When `sampling_rate` is omitted the audio is assumed to already be 16 kHz.
A bare 2-D numpy array is always a batch of mono clips (leading dim = batch); pass
a multi-channel clip inside a list to have its channels downmixed.
Audio length is unrestricted at the long end: no maximum duration, no truncation.
The mel is extracted per clip at full length and long audio is chunked inside the
encoder; each clip expands to its own frame count (~13/sec). Clips must be longer
than `n_fft // 2` samples (200 samples = 12.5 ms at 16 kHz), the STFT minimum.
"""
import re
import numpy as np
import transformers
from transformers.feature_extraction_utils import BatchFeature
from transformers.processing_utils import ProcessorMixin
if tuple(int(p) for p in transformers.__version__.split(".")[:2]) < (4, 57):
raise ImportError(
"MawJais-stt requires transformers >= 4.57 (earlier versions lack the "
"WhisperFeatureExtractor attention-mask trim and corrupt the audio features); "
f"found transformers=={transformers.__version__}. "
"Upgrade with `pip install -U 'transformers>=4.57'`.")
def _audio_output_lengths(input_lengths):
"""Encoder output-frame count from the mel-frame count.
Three stride-2 stages plus per-100-frame windowing: 100 mel frames/sec -> 13 output
tokens/sec. Works elementwise on a python int or a torch / numpy integer tensor.
"""
input_lengths_leave = input_lengths % 100
feat_lengths = (input_lengths_leave - 1) // 2 + 1
return ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
def _resample(wave, orig_sr, target_sr):
"""Resample one mono float32 clip with the best library installed."""
try:
import soxr
return np.asarray(soxr.resample(wave, orig_sr, target_sr, quality="HQ"),
dtype=np.float32)
except ImportError:
pass
try:
import librosa
return np.asarray(librosa.resample(wave, orig_sr=orig_sr, target_sr=target_sr),
dtype=np.float32)
except ImportError:
pass
try:
from math import gcd
from scipy.signal import resample_poly
g = gcd(int(orig_sr), int(target_sr))
return np.asarray(resample_poly(wave, target_sr // g, orig_sr // g),
dtype=np.float32)
except ImportError:
pass
try:
import torch
import torchaudio.functional as _taf
return _taf.resample(torch.from_numpy(wave), orig_sr, target_sr).numpy()
except ImportError:
raise ImportError(
f"this audio is {orig_sr} Hz and the model expects {target_sr} Hz; install a "
"resampler (`pip install soxr`, or librosa / scipy / torchaudio) or resample "
"the audio yourself.")
def _standardize_audio(wave, sr, target_sr):
"""One clip -> float32 mono at `target_sr`.
Accepts any dtype (integer PCM is rescaled to [-1, 1]), mono or multi-channel
(channels are averaged), any sample rate (`sr=None` means already `target_sr`).
A clip that is already float mono at `target_sr` passes through untouched.
"""
a = np.asarray(wave)
# integer PCM -> [-1, 1] float FIRST: channel averaging below returns float and
# would otherwise hide the integer dtype, leaving the waveform at PCM amplitude
if np.issubdtype(a.dtype, np.unsignedinteger): # e.g. uint8 PCM (offset binary)
half = float(np.iinfo(a.dtype).max + 1) / 2.0
a = (a.astype(np.float32) - half) / half
elif np.issubdtype(a.dtype, np.integer): # e.g. int16 / int32 PCM
a = a.astype(np.float32) / float(np.iinfo(a.dtype).max + 1)
if a.ndim == 2:
ch_axis = 0 if a.shape[0] <= a.shape[1] else 1
if a.shape[ch_axis] > 8:
raise ValueError(
f"cannot tell channels from samples in audio of shape {tuple(a.shape)}; "
"pass each clip as 1-D mono or (channels, samples) / (samples, channels)")
a = a.mean(axis=ch_axis)
if a.ndim != 1:
raise ValueError(
f"each audio clip must be 1-D (mono) or 2-D (with a channel axis), "
f"got shape {tuple(a.shape)}")
a = a.astype(np.float32, copy=False)
if sr is not None and int(sr) != int(target_sr):
a = _resample(a, int(sr), int(target_sr))
return a
class JaisASRProcessor(ProcessorMixin):
attributes = ["feature_extractor", "tokenizer"]
tokenizer_class = "AutoTokenizer"
# transformers>=5 resolves the feature extractor from preprocessor_config.json
# (`feature_extractor_type`) and deprecates the class attribute; <5 still needs it.
if int(transformers.__version__.split(".")[0]) < 5:
feature_extractor_class = "WhisperFeatureExtractor"
valid_kwargs = ["chat_template", "audio_token", "boa_token", "eoa_token"]
#: fields this processor can produce (read by serving backends)
model_input_names = [
"input_ids", "attention_mask", "input_features", "feature_attention_mask",
]
def __init__(
self,
feature_extractor=None,
tokenizer=None,
chat_template=None,
audio_token="<|ifm|audio|>",
boa_token="<audio>",
eoa_token="</audio>",
**kwargs,
):
self.audio_token = audio_token
self.boa_token = boa_token # span delimiters; never scattered onto
self.eoa_token = eoa_token
self.audio_token_id = (
tokenizer.convert_tokens_to_ids(audio_token) if tokenizer is not None else None
)
super().__init__(feature_extractor, tokenizer, chat_template=chat_template)
# ---- ids some serving backends look for by these exact names -----------------
@property
def audio_token_ids(self):
return [self.audio_token_id]
@property
def audio_processor(self):
return self.feature_extractor
def __call__(
self,
text=None,
audio=None,
return_tensors=None,
return_mm_token_type_ids: bool = False,
**kwargs,
):
if text is None:
raise ValueError("`text` is required.")
# rate of the INCOMING audio; consumed here (audio is standardized to the
# feature extractor's rate below), never forwarded into the tokenizer
sr_kw = kwargs.pop("sampling_rate", None)
# ProcessorMixin.apply_chat_template(tokenize=True) always passes images= and
# videos=; they must not fall through **kwargs into the tokenizer
for key in ("images", "videos"):
if kwargs.pop(key, None) is not None:
raise ValueError(f"`{key}` is not supported: this is an audio-only model.")
if isinstance(text, str):
text = [text]
audio_inputs = {}
if audio is not None:
sr_target = int(self.feature_extractor.sampling_rate)
if isinstance(audio, np.ndarray):
if audio.ndim == 1:
audio = [audio] # one mono clip
else:
audio = list(audio) # leading dim is always the batch of clips
elif not isinstance(audio, (list, tuple)):
audio = [audio]
if len(audio) == 0:
raise ValueError("`audio` is empty; pass at least one clip or omit `audio=`.")
if isinstance(sr_kw, (list, tuple)):
if len(sr_kw) != len(audio):
raise ValueError(
f"sampling_rate has {len(sr_kw)} entries but {len(audio)} audio clips "
"were passed; give one rate per clip (or a single scalar).")
audio = [_standardize_audio(a, s, sr_target) for a, s in zip(audio, sr_kw)]
else:
audio = [_standardize_audio(a, sr_kw, sr_target) for a in audio]
min_samples = int(self.feature_extractor.n_fft) // 2 + 1
for i, a in enumerate(audio):
if len(a) < min_samples:
raise ValueError(
f"audio clip {i} has {len(a)} samples after standardization; the STFT "
f"needs at least {min_samples} samples "
f"({1000.0 * min_samples / sr_target:.1f} ms at {sr_target} Hz). Note a "
"bare 2-D array is a batch of mono clips; pass a multi-channel clip "
"inside a list.")
n_placeholders = sum(t.count(self.audio_token) for t in text)
if n_placeholders != len(audio):
raise ValueError(
f"text contains {n_placeholders} '{self.audio_token}' placeholders but "
f"{len(audio)} audio clips were passed; they must match one-to-one (in order).")
sr = self.feature_extractor.sampling_rate
# each clip runs through the feature extractor ALONE: its mel values and
# frame count never depend on which other clips share the batch
feats = [
np.asarray(
self.feature_extractor(
a,
sampling_rate=sr,
return_attention_mask=True,
padding="longest",
truncation=False,
)["input_features"]
)[0]
for a in audio
]
mel_lens = [f.shape[-1] for f in feats]
t_max = max(mel_lens)
input_features = np.zeros((len(feats), feats[0].shape[0], t_max), dtype=np.float32)
feature_attention_mask = np.zeros((len(feats), t_max), dtype=np.int32)
for i, (f, n) in enumerate(zip(feats, mel_lens)):
input_features[i, :, :n] = f
feature_attention_mask[i, :n] = 1
audio_inputs["input_features"] = input_features
audio_inputs["feature_attention_mask"] = feature_attention_mask
n_tokens = [int(_audio_output_lengths(int(n))) for n in mel_lens]
repl = iter(
f"{self.boa_token}{self.audio_token * n}{self.eoa_token}" for n in n_tokens
)
pattern = re.escape(self.audio_token)
text = [re.sub(pattern, lambda _m: next(repl), t) for t in text]
else:
n_placeholders = sum(t.count(self.audio_token) for t in text)
if n_placeholders:
raise ValueError(
f"text contains {n_placeholders} '{self.audio_token}' placeholders but no "
"audio was passed; pass `audio=` with one clip per placeholder.")
kwargs.setdefault("add_special_tokens", False)
text_inputs = self.tokenizer(text=text, return_tensors=return_tensors, **kwargs)
data = {**text_inputs, **audio_inputs}
# PreTrainedTokenizerFast emits `token_type_ids` by default. This model has no
# segment embeddings, and `generate()` rejects unknown kwargs outright, so
# `model.generate(**processor(...))` would raise. Drop it here so the processor's
# output is always directly usable.
data.pop("token_type_ids", None)
if return_mm_token_type_ids:
# 1 wherever the audio embeddings will be scattered, 0 elsewhere. Mirrors
# `input_ids` row for row (lists stay lists, so ragged batches work).
import torch
ids = data["input_ids"]
if isinstance(ids, torch.Tensor):
data["mm_token_type_ids"] = (ids == self.audio_token_id).long()
elif isinstance(ids, np.ndarray):
data["mm_token_type_ids"] = (ids == self.audio_token_id).astype(np.int64)
else:
data["mm_token_type_ids"] = [
[1 if t == self.audio_token_id else 0 for t in row] for row in ids
]
return BatchFeature(data=data, tensor_type=return_tensors)
def _get_num_multimodal_tokens(self, audio_lengths=None, **kwargs):
"""Placeholder count per clip, without running the feature extractor.
`audio_lengths` are MEL-frame counts (100/sec). Used by serving backends to
size the placeholder span before scheduling.
"""
if audio_lengths is None or len(audio_lengths) == 0:
return {"num_audio_tokens": []}
return {"num_audio_tokens": [int(_audio_output_lengths(int(n))) for n in audio_lengths]}
# ---- tokenizer passthroughs --------------------------------------------------
def batch_decode(self, *args, **kwargs):
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
return self.tokenizer.decode(*args, **kwargs)
# ---- reading the model's output ----------------------------------------------
def post_process_transcription(self, generated, *, input_lengths=None, strip_tag=True,
skip_special_tokens=True):
"""Decode generations and split off the leading `[...]` decision tag.
`generated` is what `model.generate(...)` returned (ids, incl. the prompt — a
batch or a single row) or already-decoded text. Pass `input_lengths` to trim
the prompt first: either the padded prompt width as a single int, or the true
prompt length per row (leading pad tokens are then skipped automatically, so
per-row lengths stay correct under left padding).
Returns a list of `Transcription(text, language, refused, tag, thinking,
truncated)`; see postprocess.py. `.truncated` is True when a row hit the
generation cap (no end-of-turn token in the generated ids -- raise
`max_new_tokens` for long-form audio). With `strip_tag=False` the tag is
left in `.text`.
"""
try:
from .postprocess import (split_text, BRACKET_OPEN_ID, BRACKET_CLOSE_ID,
LANGUAGE_TAG_IDS, THINK_OPEN_ID, THINK_CLOSE_ID)
except ImportError:
if __package__:
raise # Preserve dependency errors raised inside sibling modules.
from postprocess import (split_text, BRACKET_OPEN_ID, BRACKET_CLOSE_ID,
LANGUAGE_TAG_IDS, THINK_OPEN_ID, THINK_CLOSE_ID)
if isinstance(generated, str):
return [split_text(generated, strip=strip_tag)]
first = generated[0] if len(generated) else None
if isinstance(first, str):
return [split_text(t, strip=strip_tag) for t in generated]
if getattr(generated, "ndim", None) == 1 or (
first is not None and not hasattr(first, "__iter__")):
generated = [generated] # a single row of ids -> batch of one
pad_id = self.tokenizer.pad_token_id
bos_id = self.tokenizer.bos_token_id
eos_ids = {self.tokenizer.eos_token_id,
self.tokenizer.convert_tokens_to_ids("<|ifm|endoftext|>")}
eos_ids.discard(None)
rows = []
for i, seq in enumerate(generated):
seq = [int(t) for t in seq]
prefilled = None
thinking_open = False
if input_lengths is not None:
if isinstance(input_lengths, int):
n = input_lengths # padded prompt width (pads included)
else:
n = int(input_lengths[i]) # true per-row prompt length: skip the
if pad_id is not None: # leading pad run first (left padding)
lead = 0
while lead < len(seq) and seq[lead] == pad_id:
lead += 1
if lead and pad_id == bos_id:
lead -= 1 # that last one is the prompt's own BOS
n += lead
if (n >= 3 and seq[n - 1] == BRACKET_CLOSE_ID
and seq[n - 3] == BRACKET_OPEN_ID
and seq[n - 2] in LANGUAGE_TAG_IDS):
prefilled = LANGUAGE_TAG_IDS[seq[n - 2]] # tag prefilled into the prompt
for token_id in reversed(seq[:n]):
if token_id == THINK_CLOSE_ID:
break
if token_id == THINK_OPEN_ID:
thinking_open = True
break
seq = seq[n:]
if seq and seq[0] == THINK_OPEN_ID:
thinking_open = True
pending_thinking = thinking_open and THINK_CLOSE_ID not in seq
rows.append((self.tokenizer.decode(seq, skip_special_tokens=skip_special_tokens),
not any(t in eos_ids for t in seq), prefilled, pending_thinking))
import dataclasses
out = []
for t, cut, pre, pending_thinking in rows:
# A capped generation still inside <ifm|think> is reasoning, not a
# transcript. The prompt establishes the open block when supplied.
r = split_text(t + "</ifm|think>" if pending_thinking else t, strip=strip_tag)
if r.tag is None and pre is not None:
r = dataclasses.replace(r, tag=f"[{pre}]", language=pre)
out.append(dataclasses.replace(r, truncated=cut))
return out
def language_pin(self, language, force_bracket=True):
"""A `LanguagePin` for this repo: constrain the language tag while keeping the
model's own transcribe-or-abstain decision. `language` is a tag, an ISO code
covering several tags (`"ar"`), or a list of tags.
pin = processor.language_pin("ar")
out = model.generate(**inputs,
logits_processor=[pin.for_transformers(inputs["input_ids"].shape[1])])
"""
try:
from .language import LanguagePin
except ImportError:
if __package__:
raise # Preserve dependency errors raised inside sibling modules.
from language import LanguagePin
return LanguagePin(language, force_bracket=force_bracket)
__all__ = ["JaisASRProcessor"]
|