GigaAM-v3 e2e CTC, Core ML

A Core ML conversion of Sber's GigaAM-v3 v3_e2e_ctc Russian speech recognition model. It outputs punctuated, normalized Russian text. It was converted for echo99, a macOS call recorder that transcribes on device. The weights are the upstream ones, stored in fp16 (the fp16 variant) or as per-channel int8 (the int8 variant); the Results section names the variant of this upload. Nothing was retrained or fine-tuned.

Licence and attribution

MIT, like the original. The model was made by Sber / SaluteDevices (the GigaChat team):

MIT License

Copyright (c) 2024 GigaChat Team

The full licence text is in the upstream repository's LICENSE file at that commit.

Evaluation audio: FLEURS ru_ru test split (CC-BY 4.0), at dataset revision 70bb2e84b976b7e960aa89f1c648e09c59f894dd.

Files

File Contents
GigaAM.mlmodelc/ The compiled model (macOS 14+). This is what the app loads.
GigaAM.mlpackage/ The same model, uncompiled.
gigaam-melfb.f32 Mel filterbank: 64 × 161 float32 little-endian, row-major [mel][bin], the table stored in the checkpoint.
gigaam-window.f32 Analysis window: 320 float32 little-endian, the table stored in the checkpoint.
gigaam-vocab.txt 256 SentencePiece pieces, UTF-8, one per line; line number = token id.
requirements-linux.lock The exact Python packages of the Linux conversion run (not used by the app).
variant.json int8 variant only: the quantization settings and the sha256 of the fp16 package it was made from.
compiled.json The sha256 of every file of GigaAM.mlmodelc and of the package it was compiled from (not used by the app).

Input and output

Audio is 16 kHz mono float. One prediction covers at most 25.0 s (400,000 samples).

Features are computed outside the model, with the upstream preprocessor's parameters (steps 2–4):

  1. Input shorter than 1,600 samples (0.1 s) is zero-padded at the end to 1,600 samples.
  2. Frames: ⌊(n − 320) / 160⌋ + 1 frames of 320 samples, hop 160, no centering.
  3. Each frame is multiplied by gigaam-window.f32, then a 320-point real DFT gives a power spectrum (re² + im²) of 161 bins.
  4. Mel: multiply by gigaam-melfb.f32 (64 × 161), then log(clamp(x, 1e-9, 1e9)).

Load the window and the filterbank from the files. They are the checkpoint's own tables, which need not equal a textbook Hann window or a freshly built mel filterbank.

Model inputs:

  • features: [1, 64, 2499] float32, the log-mel frames [mel][frame], zero-padded after the real frames.
  • feature_lengths: [1] int32, the real frame count.

Model outputs:

  • logits: [1, 625, 257] float32, CTC log-probabilities (log-softmax). Class 256 is the blank; class 0 is <unk>.
  • logits_length: [1] int32, the number of valid output frames. One output frame is 40 ms.

Frames past logits_length are padding and must be ignored. The encoder masks the padding in attention, so a padded clip matches the same clip run at its own length (see Results).

Decoding (greedy CTC): take the argmax of each valid frame, collapse repeats, drop the blank (256) and <unk> (0), join the pieces, read ▁ as a space and trim. A token spans its run of frames: start = firstFrame × 0.04 s, end = (lastFrame + 1) × 0.04 s.

Conversion

Upstream builds the encoder's attention mask only for batches larger than one (gigaam/encoder.py, if audio_signal.shape[0] > 1:). With one clip padded to 25 s, real frames would then attend to the padding. The export wrapper builds the mask for every batch size; nothing else changes, and unpadded input gives the same result as upstream.

The conversion runs on two machines. PyTorch 2.7 publishes no Intel macOS wheels, and Core ML can only compile and predict on macOS. From the echo99 repository root:

Linux (x86_64, CPU only, Python 3.12): the PyTorch half. It checks the preprocessor's parameters, writes the side files, records unpadded upstream reference outputs on FLEURS, checks the patched model against them, exports with torch.export, and converts with coremltools 9 (fp16, minimum_deployment_target=macOS14). The ops that compute the output length and the masks from feature_lengths stay fp32: fp16 is exact only up to 2,048, and upstream computes the length in floating point, so in fp16 2,499 frames would report 626 output frames instead of 625. The CTC head (the projection onto the 257 classes, its transpose, the softmax and the log) also stays fp32: in fp16 a log-softmax can give −inf. Both subgraphs are selected by what they depend on, not by op name, and a static check fails the conversion if any of their values is fp16. Boolean operators on the masks are converted as Core ML logical ops, and the export fails, listing every node, if any op in the exported graph has no coremltools converter.

Once tools/gigaam/requirements-linux.lock is committed, install from it, so the run uses exactly the packages of the verified run:

python3.12 -m venv tools/gigaam/.venv
uv pip install --python tools/gigaam/.venv "setuptools>=77" "packaging>=24.2" wheel
uv pip install --python tools/gigaam/.venv --no-build-isolation-package gigaam -r tools/gigaam/requirements-linux.lock
tools/gigaam/.venv/bin/python -I tools/gigaam/export_linux.py   # ends with ALL PYTORCH CHECKS PASSED

The first install, before a lock exists, uses the version ranges instead: the same commands with -r tools/gigaam/requirements-linux.txt.

GigaAM must be built without build isolation, in a venv that already has setuptools>=77 and packaging>=24.2. Its pyproject.toml declares license = "MIT" as a licence expression, and an isolated build fails with "ImportError: Cannot import packaging.licenses".

A passing run writes out/requirements-linux.lock: every installed package at its exact version, and GigaAM at the pinned commit. Copy it to tools/gigaam/requirements-linux.lock and commit it; the run prints whether the committed lock still matches the environment. The run also hashes the checkpoint and the tokenizer that gigaam.load_model downloads. While EXPECTED_CHECKPOINT_SHA256 and EXPECTED_TOKENIZER_SHA256 in common.py are empty, it prints both hashes for pinning; once they are set, any other file fails the run before it is loaded.

Then copy the results to the Mac, from the Mac's repository root:

rsync -a <linux-host>:<path>/recc/tools/gigaam/{out,reference} tools/gigaam/

Mac (Python 3.12, no torch): the Core ML half. It checks that the package is the one the Linux run recorded (sha256 of model.mlmodel and weight.bin), compiles GigaAM.mlmodelc, compares Core ML with the PyTorch references, and writes tools/gigaam/results/<arch>-<variant>.{json,md}. In this order, on the Intel Mac (it has Xcode, which provides coremlcompiler):

python3.12 -m venv tools/gigaam/.venv
tools/gigaam/.venv/bin/pip install -r tools/gigaam/requirements-mac.txt
tools/gigaam/.venv/bin/python -I tools/gigaam/finish_mac.py --write-fixtures   # ends with ALL CORE ML CHECKS PASSED
tools/gigaam/.venv/bin/python -I tools/gigaam/quantize_mac.py                  # out-int8/: per-channel int8 weights
tools/gigaam/.venv/bin/python -I tools/gigaam/finish_mac.py --variant int8
tools/gigaam/.venv/bin/python -I tools/gigaam/pack_artifacts.py                # dist/ for the Apple-silicon run

finish_mac.py runs every check on the compiled GigaAM.mlmodelc, the bundle the app downloads.

  • One compile per package. coremlcompiler does not produce byte-identical bundles (its coremldata.bin differs between runs), so the first run records the bundle in compiled.json and later runs reuse it while it matches, on this Mac and on the Apple-silicon Mac (which runs with --shipped-bundle: never compile). So the bundle checked on both Macs is the bundle uploaded. --recompile replaces it; then repeat every later step, including the Apple-silicon run.
  • Bars, per compute-unit setting: at most max(1, ⌊2 % of the valid frames⌋) argmax mismatches against unpadded PyTorch on each duration clip and on the test fixture; the WER within 0.3 points; max abs logsumexp ≤ 1e-2; every log-probability finite; the fixture predicted twice gives identical ids. logits_length is swept at 13 lengths.
  • Compute units: on Intel the bars run on CPU_ONLY (what the app uses: CPU_AND_GPU returned NaN from the second prediction on); on Apple silicon on ALL, CPU_AND_NE and CPU_AND_GPU. The sweep also runs CPU_ONLY. --compute-units replaces the bars list. Each setting runs in a child process, so a Core ML crash fails that setting and the run goes on.
  • Results files (committed) also hold the load times, the prediction latencies and the compute plan (ops per device).
  • --write-fixtures (Intel only; refused on Apple silicon) also writes echo99Tests/Fixtures/GigaAM/ and the results below.

quantize_mac.py applies coremltools.optimize.coreml.linear_quantize_weights (linear symmetric, int8, per output channel) to out/GigaAM.mlpackage. It quantizes every model parameter of rank ≥ 2 with more than 2,048 elements: the conv and linear weights, the CTC head's conv weight included. coremltools never compresses rank-1 constants, so biases and norms keep their precision. Constants computed during the conversion, such as the rotary cos/sin tables [625, 1, 1, 48], are not parameters (their names lack torch.export's p_) and stay as they are; variant.json lists them. Only stored weights change: the length path and the head still compute in fp32. The script checks all of this on the quantized program.

For a run on another Mac, pack_artifacts.py writes dist/gigaam-artifacts-<commit>.tar (out/, out-int8/, reference/, with the compiled bundles) and dist/SHA256SUMS; tar -xf in tools/gigaam/ restores the same paths. It refuses a variant unless the Intel run passed on that exact package (results/x86_64-<variant>.json) and the bundle matches compiled.json. APPLE_SILICON_TEST.md is the step-by-step run on Apple silicon.

Both halves reuse cached downloads and finished stages. For a clean run, delete tools/gigaam/out, tools/gigaam/out-int8 and tools/gigaam/reference.

Upload. Upload either out/ (fp16) or out-int8/ (int8), whichever variant the results favour, without dotfiles and without the uncompiled package. For int8, first run finish_mac.py --variant int8 --write-fixtures on the Intel Mac, so the fixtures and this card describe that variant (it reuses the bundle the Apple-silicon run checked). With hf from huggingface_hub 1.x (each --exclude adds a pattern; 0.x keeps only the last one):

hf upload echo99app/gigaam-v3-e2e-ctc-coreml tools/gigaam/out . --exclude ".*" --exclude "*/.*" --exclude "*.mlpackage/*"

Then manifest.py --commit <sha> writes the app's pinned file list (echo99/Models/GigaAMManifest.swift); add --models-dir tools/gigaam/out-int8 for the int8 variant.

Provenance

Recorded by export_linux.py on Linux (Python 3.12.15, torch 2.7.0+cpu, coremltools 9.0). gigaam.load_model("v3_e2e_ctc") at GigaAM 7447938d791c4f3e643386ee22c33777004293a5 downloads these files:

File Source Bytes sha256
checkpoint https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/v3_e2e_ctc.ckpt 442404646 73e27ec8f904e486ab063d5bb7c6ab701838b7e5a967ee79b71ccc5aefde146f
tokenizer https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/v3_e2e_ctc_tokenizer.model 240941 0b9a1960898fbfdf5424ab852ea17445eb3da960fba23e977ff100eb0054fbc8

The Linux environment is requirements-linux.lock (sha256 b210f7574d7dd6808d9c108c828a4fc97b0c697c205fb33ddcbba5701e70a372), written by the same run (uv pip freeze).

Results

Measured 2026-10-13 on Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (x86_64, macOS 15.8.1) with the compiled GigaAM.mlmodelc of the fp16 variant, coremltools 9.0, compute units CPU_ONLY, against unmodified upstream PyTorch (2.7.0+cpu, fp32) run on each clip at its own length. The patched column is the export wrapper in PyTorch, padded to 25 s.

Clip Length Valid frames Patched PyTorch, padded Core ML fp16, padded Core ML mismatches (allowed) Core ML max abs logsumexp
fleurs-1s 1.00 s 25 100.00 % 100.00 % 0 (1) 2.6e-07
fleurs-3s 3.00 s 75 100.00 % 100.00 % 0 (1) 3.8e-07
fleurs-7s 7.00 s 175 100.00 % 100.00 % 0 (3) 2.0e-07
fleurs-15s 15.00 s 375 100.00 % 99.73 % 1 (7) 2.4e-07
fleurs-24.9s 24.90 s 623 100.00 % 100.00 % 0 (12) 3.8e-07
synthetic-25s 25.00 s 625 100.00 % 100.00 % 0 (12) 5.1e-07

Per-frame argmax agreement over the valid frames. Bars: patched ≥ 99.5 %; Core ML at most max(1, ⌊2 % of the valid frames⌋) mismatches at every length; abs logsumexp ≤ 1e-2; every Core ML log-probability finite; the same model predicting twice gives identical ids.

logits_length matched the expected frame count at 13 feature lengths (9, 10, 100, 399, 1000, 2047, 2048, 2049, 2050, 2051, 2402, 2498, 2499) on CPU_ONLY.

WER, FLEURS ru_ru test, 100 utterances
PyTorch, unpadded 4.16 %
Core ML fp16 4.16 %

WER is on lowercased text without punctuation, "ё" read as "е" (bar: within 0.3 points). 100/100 Core ML hypotheses are identical to PyTorch's.

Test fixture: FLEURS 18363036360322471934.wav (sentence 1908, CC-BY 4.0), 4.0 s from 0.50 s. Core ML vs unpadded PyTorch: 0 mismatches (2 allowed); text identical. Numpy extractor port vs the model's preprocessor: max abs diff 1.4e-04 where the reference is ≥ −15, 5.7e-06 below.

Per compute unit, timings and the compute plan: tools/gigaam/results/x86_64-fp16.md in the echo99 repository.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for echo99app/gigaam-v3-e2e-ctc-coreml

Finetuned
(12)
this model