Downloads · 30 days
199
48% of all-time downloads
litert-community/granite-speech-5.0-470m-turboctc
granite-speech-5.0-470m-turboctc is a automatic speech recognition model from litert-community. Use it when you need speech turned into text. It is set up for litert. The card lists the license as apache-2.0.
ibm-granite/granite-speech-5.0-470m-turboctc converted to LiteRT (.tflite) for on-device speech recognition. A 473M CTC conformer: one encoder forward, no decoder loop, no KV cache — audio features in, token ids out,…
Downloads · 30 days
199
48% of all-time downloads
All-time downloads
416
Public
Repo size
1.5 GB
Likes
1
Public
Click a slice to open those files.
.tflite1.5 GB · 100%
From the Hugging Face model README
ibm-granite/granite-speech-5.0-470m-turboctc converted to LiteRT (.tflite) for on-device speech recognition. A 473M CTC conformer: one encoder forward, no decoder loop, no KV cache — audio features in, token ids out, and the whole "decoder" on the host is ten lines of CTC collapse plus a tokenizer.
| File | Recipe | Size | |
|---|---|---|---|
granite_speech_ctc_wi8fc.tflite | int8 dynamic-range (all linears, incl. pointwise convs) | 518 MB | mobile + desktop |
granite_speech_ctc_fp16.tflite | fp16 weights, float compute | 994 MB | desktop |
Verified on a Galaxy S26 (SM-S942Q) with real inputs: transcripts from both files match the fp32 PyTorch model exactly on every signature, and the int8 logits are bit-identical between Apple M4 Max and the S26 (max abs diff 0.0 — the dynamic int8 path is deterministic across devices). Use int8 on device: fp16 is ~3× slower on CPU because XNNPACK repacks fp16 weights to fp32.
This is a CPU model. The mobile GPU delegate does not compile the graph (measured on S26: Failed to compile model; the relative-position gather lowers to GATHER_ND, which mobile GPUs decline) — that is expected, not a defect, and CPU is faster than real-time by a wide margin anyway.
Three fixed audio windows, batch 1. Input input_features float32 [1, 50×s, 320]; outputs ids int32 [1, T] (argmax over the CTC vocab, already computed in-graph) and logits float32 [1, T, 16384].
| Signature | audio window | input shape | output T |
|---|---|---|---|
transcribe_5s | ≤ 5 s | [1, 250, 320] | 62 |
transcribe_10s | ≤ 10 s | [1, 500, 320] | 125 |
transcribe_30s | ≤ 30 s | [1, 1500, 320] | 375 |
Zero-pad the 16 kHz waveform to the smallest window that fits and run that signature; trailing silence decodes to CTC blanks. The encoder's attention is block-local (128 frames ≈ 2.56 s), so padding can shift block boundaries: on 20 LibriSpeech clips, 19 transcribed identically to the unpadded model and one changed a single word. Longer audio: chunk at pauses and concatenate transcripts.
16 kHz mono float32 → log-mel → normalize → delta → stack:
n_fft 512, win_length 400, hop_length 160, 80 mels (torchaudio MelSpectrogram defaults otherwise);log10 (clip at 1e-10), then floor at max − 8.0 per utterance, then x/4 + 1;torchaudio.functional.compute_deltas, win 3) → 160 channels;[T', 320], T' = mel_frames / 2.The usage snippet below is the exact contract, mirroring the upstream repo's processor.
Take the ids output, collapse repeats, drop blanks (id 0), decode with the bundled tokenizer.json. That's the entire decoder.
import numpy as np, torch, torchaudio
from ai_edge_litert.interpreter import Interpreter
from tokenizers import Tokenizer
it = Interpreter(model_path="granite_speech_ctc_wi8fc.tflite", num_threads=8)
tok = Tokenizer.from_file("tokenizer.json")
mel = torchaudio.transforms.MelSpectrogram(16000, n_fft=512, win_length=400,
hop_length=160, n_mels=80)
def features(wave): # wave: float32 [T], 16 kHz mono
x = torch.from_numpy(wave)[None]
n_frames = 2 * -(-(x.shape[1] // 160) // 2)
need = (n_frames - 1) * 160 + 1
if x.shape[1] < need:
x = torch.nn.functional.pad(x, (0, need - x.shape[1]))
m = mel(x)[..., :n_frames].clip(min=1e-10).log10()
m = torch.maximum(m, m.amax(dim=(-2, -1), keepdim=True) - 8.0) / 4 + 1
m = torch.cat((m, torchaudio.functional.compute_deltas(m, win_length=3)), dim=-2)
m = m.transpose(-2, -1).contiguous()
return m.reshape(1, -1, 2 * m.shape[-1]).numpy() # [1, T', 320]
def transcribe(wave):
sec = next(s for s in (5, 10, 30) if len(wave) <= s * 16000)
padded = np.zeros(sec * 16000, np.float32); padded[:len(wave)] = wave
out = it.get_signature_runner(f"transcribe_{sec}s")(input_features=features(padded))
ids = next(v for v in out.values() if v.dtype == np.int32)[0]
collapsed, prev = [], -1
for i in ids:
if i != prev and i != 0:
collapsed.append(int(i))
prev = i
return tok.decode(collapsed).strip()
wave, sr = torchaudio.load("speech.wav") # resample to 16 kHz mono first
print(transcribe(wave[0].numpy()))
On 20 LibriSpeech dev-clean clips (2–30 s): int8 and fp16 transcripts match the fp32 PyTorch model exactly, 20/20 each, and corpus WER is identical to fp32 under the same windows (4.24%; 3.79% unpadded — the gap is the fixed-window effect above, +2 words in 448). This is parity evidence on a small fixture set, not an ASR benchmark; for task quality see the base model card.
Latency per window (median; the model processes the whole window regardless of speech length, so this is also the worst case). Real fixture audio, outputs read every run.
Apple M4 Max (CPU/XNNPACK, ai_edge_litert Interpreter, 8 threads, median of 20 warm runs):
| Variant | 5 s | 10 s | 30 s | |
|---|---|---|---|---|
| int8 | 49.6 ms | 88.3 ms | 238.8 ms | ≈ 126× real-time |
| fp16 | 128.1 ms | 230.9 ms | 605.8 ms |
Galaxy S26, SM-S942Q (CPU, LiteRT CompiledModel via litert 2.2.0, median of 20 warm runs, thermal NONE→NONE):
| Variant | 5 s | 10 s | 30 s | load | |
|---|---|---|---|---|---|
| int8 | 106.4 ms | 220.2 ms | 666.4 ms | 2.6 s | ≈ 45× real-time |
| fp16 | 266.2 ms | 549.9 ms | 1765.3 ms | 5.8 s |
Encoder lane — a direct multi-signature litert_torch trace (no LLM export machinery). Two notes for reproducers:
GraniteSpeech5ForCTC, while the shipped .py files expect the older packaged naming. The conversion loads through the remote code with a full-key state-dict rename (strict load, all 550 tensors).einsum(q, Emb(dists)) constant-folds under torch.export into a per-layer, per-block-length fp32 constant (~0.5 GB across signatures) that dynamic int8 can't reach. Rewriting it as gather(q @ W.T, dists) keeps one shared [1025, 128] table and is bitwise-identical in eager — int8 goes 923 → 518 MB.Scripts and full notes: hf-to-litertlm.
Apache-2.0, inherited from the base model; LICENSE is included. (Note the separate -nc sibling of the base model is CC-BY-NC-SA — this conversion is of the Apache-2.0 release only.)
Modification notice: these files are converted, not original. The weights were exported to LiteRT and quantized (int8 dynamic-range / fp16); the in-graph argmax output was added. No fine-tuning or weight modification beyond quantization was performed.