This is an unofficial Core ML conversion of IBM's Granite Speech 5.0 470M TurboCTC, with 8-bit palettized weights and a valid-length (mask) input, for on-device speech recognition on Apple Silicon Macs.
Source: ibm-granite/granite-speech-5.0-470m-turboctc at revision 91dcb0605daf65f88dbb04fef015c423241d941d. This is the multilingual release of 21 October 2026: English, French, German, Spanish, Portuguese, Italian and Japanese, with native punctuation and capitalization.
All model credit belongs to IBM Granite. This repository changes the format (a fixed-shape Core ML program that applies IBM's attention and padding masks from a valid_frames input), the weight precision (8-bit), one tokenizer setting, and adds change notices. IBM has not reviewed or endorsed it.
The earlier swaynehales/granite-speech-5.0-470m-turboctc-coreml-* repositories run every call as 16,384 zero-padded frames without a mask, and their no-mask fp16 package (like the no-mask reference computation) drops the final sentence period on 119 of 141 clips in our corpus. This repository adds the mask:
valid_frames (int32 [1]): the number of real feature frames before padding. The graph reproduces IBM's four masking steps (after the input projection, on attention keys, before each depthwise convolution, and halving the length after each subsampling layer).valid_output_frames (int32 [1]) = floor(valid_frames / 4): read only that many frame_ids.model_type granite_speech5_coreml_ctc_masked. The pinned Granite-MLX release (7ad6b19) does not load it yet: its loader accepts only granite_speech5_coreml_ctc and passes a single input. Until a loader supports it, use the Python example below.IBM describes the model as 16 conformer blocks trained with CTC, with a 16,384-token multilingual BPE head. It uses 8× temporal subsampling (100 Hz to 12.5 Hz), block attention over 128 frames, and self-conditioned CTC from the middle layer: 470M parameters in total. For training data, intended use, benchmarks and the technical report, see the IBM model card at the pinned revision.
IBM releases this model under the Apache License 2.0. The pinned source revision declares license: apache-2.0 in its card metadata, and its README states:
License: Apache 2.0
Apache-2.0 section 2 grants the right to "reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form". Section 4 requires that redistribution include a copy of the licence and a notice of changes. Accordingly:
LICENSE is the verbatim Apache-2.0 text from apache.org (SHA-256 cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30). The source repository ships no separate LICENSE or NOTICE file at this revision.MODIFICATIONS.md states what was changed, and each changed file carries its own notice: the Core ML model's metadata has license: Apache-2.0 and a modification_notice field, and tokenizer_config.json has a _modification_notice key. Unchanged source files are byte-identical.Do not confuse this model with ibm-granite/granite-speech-5.0-470m-turboctc-nc, a different, English-only model under CC-BY-NC-SA-4.0. That model was not used here.
| Format | Core ML ML Program (GraniteSpeech.mlpackage), minimum macOS 15 |
Input features |
float16, [1, 16384, 320]: log-mel + delta features, frame-stacked (327.68 s of audio at 50 frames/s), zero-padded |
Input valid_frames |
int32, [1]: count T of real frames; must be 1 ≤ T ≤ 16384 |
Output frame_ids |
int32, [1, 4096]: argmax CTC token per 80 ms output frame; frames at and beyond valid_output_frames are undefined |
Output valid_output_frames |
int32, [1]: floor(T / 4) |
| Weights | 8-bit uniform palettization, per-grouped-channel, group size 1, weight threshold 2048; activations fp16 (valid-length/mask path fp32) |
| Frontend | not in the graph: compute features with the Hugging Face processor (Python) or Granite-MLX's GraniteFeatureExtractor (Swift) |
| Recommended compute units | CPU and GPU (cpu-gpu); Neural Engine placement was not measured |
coreml_config.json |
Granite-MLX schema plus inputs, outputs and mask_semantics; model_type granite_speech5_coreml_ctc_masked |
Required guard for consumers. Pass the true frame count, and check 1 <= T <= 16384 before calling (chunk longer audio). Outside that range the output count is not trustworthy: valid_frames 20000 returns valid_output_frames 5000 (more than the 4096 frames returned), and -1 returns -1. T of 1–3 returns 0 frames; treat it as an empty transcript. Passing a T larger than the real audio adds spurious trailing punctuation. Check that valid_output_frames == T // 4.
How it was produced. The graph is traced from a masked variant of Granite-MLX's explicit-ops PyTorch model (Granite-MLX commit 7ad6b19c698aac5aa8328c911e4a6eead456c5a1, classes reused unmodified, source state dict loaded strictly) that adds IBM's four masking steps, with padded attention keys filled with -1e4. It was converted with coremltools to fp16, keeping the length and mask computations in fp32 (coremltools otherwise lowers the integer length halving to a float16 operation that rounds lengths above 2048). The weights were then compressed with Granite-MLX quantize_granite_coreml.py --method palette-uniform --bits 8 --granularity per-channel --block-size 1 --weight-threshold 2048, run unmodified; the internal masks were checked exact against the expected prefix masks at 22 lengths after compression. Versions: coremltools 9.0 with torch 2.7.0 for conversion; reference inference with transformers 5.16.1 and torch 2.13.0. Run on macOS 27.0.1, Apple M2 Max, on 2026-11-13/14 (America/Los_Angeles). Full provenance is in CONVERSION.json.
| File | Bytes | SHA-256 |
|---|---|---|
GraniteSpeech.mlpackage/Data/com.apple.CoreML/weights/weight.bin |
691,635,072 | b7bc4907e8c6e519a5e54cf8e566b494a7a00b5287f52f8e7ade569d5451414d |
GraniteSpeech.mlpackage/Data/com.apple.CoreML/model.mlmodel |
399,015 | 6f5de012b1ea44d45a6f3dd4055be4697cffbd21441f0b39eb09d7d7856d1745 |
coreml_config.json |
see SHA256SUMS |
|
tokenizer.json, config.json, processor_config.json, generation_config.json |
byte-identical to the source revision | |
tokenizer_config.json |
source file plus two keys (see below) |
Every file's SHA-256 is listed in SHA256SUMS; check with shasum -a 256 -c SHA256SUMS.
Tokenizer deviation. tokenizer_config.json adds "clean_up_tokenization_spaces": false and a _modification_notice string (the Apache-2.0 §4(b) change notice; Python transformers and swift-transformers both load the file with it, tested); nothing else differs from the source file (source SHA-256 2f61ece3468bf7ca7a1b6dd3fd5b809814315bea9c298feea035fda4931b9f02). swift-transformers 1.3.3 turns that cleanup on when the key is absent, and it strips the space before ? ! : ;, which corrupts French typography. Python transformers already leaves it off for this tokenizer, so Python output is unchanged.
Tested with coremltools 9.0, transformers 5.16.1 and soundfile on macOS 27, on a local copy of this repository and 16 kHz mono audio of at most 327.68 s (this exact script was run on this folder):
import numpy as np, soundfile as sf, coremltools as ct
from transformers import AutoProcessor
repo = "/path/to/granite-speech-5.0-470m-turboctc-coreml-masked-8bit"
audio, sr = sf.read("audio.wav", dtype="float32") # 16 kHz mono
proc = AutoProcessor.from_pretrained(repo)
feats = proc(audio, sampling_rate=16000, return_tensors="np")["input_features"][0] # [T, 320]
T = len(feats)
assert 1 <= T <= 16384, "valid_frames must be 1..16384; chunk longer audio" # required range guard
x = np.zeros((1, 16384, 320), np.float16); x[0, :T] = feats # fixed shape, zero-padded
model = ct.models.MLModel(f"{repo}/GraniteSpeech.mlpackage", compute_units=ct.ComputeUnit.CPU_AND_GPU)
out = model.predict({"features": x, "valid_frames": np.array([T], dtype=np.int32)})
n = int(out["valid_output_frames"].reshape(-1)[0])
assert n == T // 4 # returned-count guard
ids = out["frame_ids"].reshape(-1)[:n] # frames beyond n are undefined
collapsed, prev = [], None
for t in ids.tolist(): # greedy CTC: merge repeats, drop blank (0)
if t != prev and t != 0: collapsed.append(t)
prev = t
text = proc.tokenizer.decode(collapsed, group_tokens=False).replace("<|nospeech|>", "").strip()
print(text)
Pass group_tokens=False when decoding IDs that are already collapsed; otherwise the tokenizer merges repeated tokens a second time. For longer audio, chunk it as Granite-MLX does: 286.72 s core windows with up to 20.48 s of context on each side; run each segment with its own T; trim the context frames from each segment's frame_ids in frame space (12.5 output frames per second); concatenate the retained frame IDs of all segments; then CTC-collapse once and decode.
Not yet supported by the pinned Granite-MLX release (7ad6b19); see "How this differs" above.
Method. The reference is IBM's model in transformers 5.16.1, fp32 on CPU, variable length with an attention mask, with greedy CTC (official inference). This package was run through the Python path above (Core ML, CPU and GPU), with zero-padded features and valid_frames = T.
The fixed corpus has 141 unique clips:
hf-internal-testing/librispeech_asr_dummy);Long-audio behaviour was tested separately on 4 constructed clips of 286.5–328 s. Text is compared after NFKC normalization, lower-casing and removing punctuation; WER is used for European languages, and CER with whitespace removed for Japanese. All rates are corpus-level per set. With 10 clips per FLEURS language these are small samples: they show conversion fidelity, not benchmark accuracy.
Results for this 8-bit package, in percent:
| Set (clips) | vs our masked fp16 package | vs official IBM inference | vs ground-truth transcripts (official IBM model in brackets) |
|---|---|---|---|
| LibriSpeech en (73) | 0.09 | 0.09 | 1.80 (1.71) |
| FLEURS fr (10) | 0.00 | 0.00 | 7.19 (7.19) |
| FLEURS de (10) | 0.00 | 0.00 | 2.86 (2.86) |
| FLEURS es (10) | 0.86 | 0.86 | 3.40 (3.40) |
| FLEURS pt (10) | 0.78 | 0.39 | 10.77 (10.77) |
| FLEURS it (10) | 0.43 | 0.43 | 2.55 (2.13) |
| FLEURS ja (10, CER) | 0.00 | 0.00 | 4.97 (4.97) |
Final sentence mark: matches official inference on 141/141 clips. Local recordings (4 clips): 0.35% against official inference.
Long audio. Using the masked package per segment with Granite-MLX's 286.72 s chunking, compared with chunked official inference: 0.00% on each of the four constructed boundary clips and 0.15% on a private 5-minute recording.
Size and speed. The weight file is 691,635,072 bytes. Python predict median over 5 rounds after a warm-up: 0.46 s for an 11 s clip and 0.47 s for a full 327.5 s window (the graph always runs all 16,384 frames), process peak RSS 1.84 GB. These timings were measured on a heavily loaded machine (1-minute load average about 27) and are indicative only.
"Il.", as official inference does; 30 s of silence returns "."; white and pink noise return the literal <|nospeech|> token. Filter <|nospeech|> and punctuation-only output in your application.valid_frames gives untrustworthy counts (see the required guard above).7ad6b19 rejects granite_speech5_coreml_ctc_masked; the Swift path is untested.SHA256SUMS yourself.ibm-granite/granite-speech-5.0-470m-turboctc