# load_binary.py: standalone loader for Kea-B, the 1-bit Parakeet-TDT-0.6B-v2 export. # # MIT License # # Copyright (c) 2026 ajbarryiii # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # This license covers this file only. The model weights it loads are a modified # version of nvidia/parakeet-tdt-0.6b-v2 and are licensed CC-BY-4.0 (see LICENSE # and NOTICE in the model repository). """Load and run Kea-B, the 1-bit Parakeet-TDT-0.6B-v2 export, with only NeMo (nemo_toolkit[asr] and its torch), safetensors and soundfile. Standalone copy of the repository's binary_export.load_export, so the model can be used without the training code. python load_binary.py clip.wav [clip2.flac ...] # 16 kHz audio; prints one transcript per file import sys; sys.path.insert(0, "kea-b") # directory holding this repository's files from load_binary import load, transcribe model = load("kea-b", device="cuda") # or "cpu" print(transcribe(model, ["clip.wav"])) # list in -> list of str out Format "parakeet-binary-v1" (one directory): - export.safetensors: each of the 264 quantized encoder weights as ".codes", one bit per weight packed eight per byte (code j of a row is bit j % 8 of byte j // 8, least significant bit first; bit 1 = +1, bit 0 = -1; unused tail bits 0; uint8 [out, ceil(in/8)]; pointwise convolutions as their [out, in] matrix) and ".scale", one FP32 scale per output row; the weight is codes * scale. Every other state_dict tensor is FP16, except the feature-extraction constants preprocessor.featurizer.{window,fb} (FP32) and integer BatchNorm counters (int64). - manifest.json: format, SHA-256 of export.safetensors and the three tokenizer files, the NeMo model config, per-layer shapes and kinds, code balance, parameter and byte accounting, training provenance. - tokenizer/: the base model's SentencePiece files (tokenizer.model, tokenizer.vocab, vocab.txt). load() rebuilds a plain NeMo EncDecRNNTBPEModel in FP32 with the dequantized weights. It does not use 1-bit kernels and needs the same memory and compute as the FP32 original; no speed or energy benefit is claimed. Tested environment: NeMo 3.0.0, torch 2.11.0+cu128. The reported WERs were measured decoding on an NVIDIA RTX 5090 (CUDA, strict FP32). Loading on the CPU was tested, decoding on the CPU was not. """ from __future__ import annotations import contextlib import hashlib import json import math import sys from pathlib import Path import numpy as np import torch from safetensors.torch import load_file FORMAT = "parakeet-binary-v1" MANIFEST = "manifest.json" WEIGHTS_FILE = "export.safetensors" TOKENIZER_DIR = "tokenizer" # config.tokenizer key -> file name in the tokenizer directory TOKENIZER_FILES = {"model_path": "tokenizer.model", "vocab_path": "vocab.txt", "spe_tokenizer_vocab": "tokenizer.vocab"} MODEL_TARGET = "nemo.collections.asr.models.rnnt_bpe_models.EncDecRNNTBPEModel" LAYER_KINDS = ("linear", "pointwise_conv1d") SAMPLE_RATE = 16000 # As in the evaluation: inputs shorter than 30 ms are zero-padded to 30 ms (NeMo's per-feature # normalization fails on a one-frame input). Every reported test utterance is longer. MIN_DECODE_SAMPLES = 480 MAX_BATCH_SECONDS = 1200.0 # NeMo's greedy TDT decoder CUDA-graph mode used by the evaluation (native PyTorch CUDA graphs). DECODER_GRAPHS_MODE = "no_while_loops" _SHIFTS = torch.arange(8, dtype=torch.uint8) def unpack_codes(packed: torch.Tensor, in_features: int) -> torch.Tensor: """uint8 [out, ceil(in/8)] -> int8 [out, in] in {-1, +1}; rejects a wrong dtype or shape and nonzero tail bits.""" if packed.dtype != torch.uint8 or packed.dim() != 2 or packed.shape[1] != math.ceil(in_features / 8): raise ValueError(f"expected uint8 [out, {math.ceil(in_features / 8)}], got {packed.dtype} {tuple(packed.shape)}") bits = ((packed[..., None] >> _SHIFTS.to(packed.device)) & 1).flatten(1) if bits[:, in_features:].any(): raise ValueError("invalid packed codes: nonzero tail padding") return (2 * bits[:, :in_features].to(torch.int8) - 1).to(torch.int8) def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1 << 20), b""): digest.update(chunk) return digest.hexdigest() def load(directory: str | Path = ".", device: str | torch.device = "cpu"): """Rebuild the FP32 eval-mode NeMo EncDecRNNTBPEModel from a directory holding manifest.json, export.safetensors and tokenizer/. The manifest must name export.safetensors as the weights file and list exactly it and tokenizer/tokenizer.model, tokenizer/vocab.txt and tokenizer/tokenizer.vocab; each file's SHA-256 is checked against it. manifest.json itself is not hash-checked. """ from nemo.collections.asr.models import EncDecRNNTBPEModel from omegaconf import OmegaConf, open_dict directory = Path(directory).resolve() manifest = json.loads((directory / MANIFEST).read_text()) if manifest["format"] != FORMAT: raise ValueError(f"unknown export format {manifest['format']!r}") if manifest["file"] != WEIGHTS_FILE: raise ValueError(f"manifest names weights file {manifest['file']!r}, not {WEIGHTS_FILE!r}") expected_files = {WEIGHTS_FILE, *(f"{TOKENIZER_DIR}/{f}" for f in TOKENIZER_FILES.values())} if set(manifest["files"]) != expected_files or manifest["files"][WEIGHTS_FILE] != manifest["sha256"]: raise ValueError(f"manifest is inconsistent: files {sorted(manifest['files'])}") for rel, digest in manifest["files"].items(): if _sha256(directory / rel) != digest: raise ValueError(f"{directory / rel} does not match the manifest SHA-256") tensors = load_file(directory / WEIGHTS_FILE) state: dict[str, torch.Tensor] = {} for name, layer in manifest["quantized_layers"].items(): if layer["kind"] not in LAYER_KINDS: raise ValueError(f"{name}: unknown layer kind {layer['kind']!r}") out_features, in_features = layer["shape"] scale = tensors.pop(f"{name}.scale") if scale.dtype != torch.float32 or tuple(scale.shape) != (out_features,): raise ValueError(f"{name}.scale: expected float32 [{out_features}], got {scale.dtype} {tuple(scale.shape)}") # codes are +-1, so codes * scale is exact: every weight is +scale or -scale of its row. weight = unpack_codes(tensors.pop(f"{name}.codes"), in_features).float() * scale[:, None] if weight.shape != (out_features, in_features): raise ValueError(f"{name}: shape {tuple(weight.shape)} != {layer['shape']}") state[f"{name}.weight"] = weight.unsqueeze(2) if layer["kind"] == "pointwise_conv1d" else weight if layer["bias"]: state[f"{name}.bias"] = tensors.pop(f"{name}.bias") state.update((k, v.float() if v.is_floating_point() else v) for k, v in tensors.items()) cfg = OmegaConf.create(manifest["config"]) if cfg.get("target") != MODEL_TARGET: raise ValueError(f"unexpected model target {cfg.get('target')!r}") with open_dict(cfg): cfg.tokenizer.dir = str(directory / TOKENIZER_DIR) for key, filename in TOKENIZER_FILES.items(): cfg.tokenizer[key] = str(directory / TOKENIZER_DIR / filename) for ds in ("train_ds", "validation_ds", "test_ds"): # stored for reference; no dataloaders at load if ds in cfg: cfg[ds] = None model = EncDecRNNTBPEModel(cfg=cfg) model.load_state_dict(state, strict=True) # shape mismatches and missing/unexpected keys raise return model.to(device=device, dtype=torch.float32).eval() def _pin_decoder_graphs(model) -> None: """On CUDA, put NeMo's greedy TDT decoder in the evaluation's CUDA-graph mode (no_while_loops), when this NeMo version exposes it and the decoder config allows CUDA graphs.""" computer = getattr(getattr(getattr(model, "decoding", None), "decoding", None), "decoding_computer", None) if computer is None or not hasattr(computer, "force_cuda_graphs_mode") or not computer.allow_cuda_graphs: return mode = computer.cuda_graphs_mode if mode is None or str(mode.value) != DECODER_GRAPHS_MODE: computer.force_cuda_graphs_mode(DECODER_GRAPHS_MODE) @contextlib.contextmanager def _strict_fp32(device: torch.device): """Autocast off and TF32 off (matmul and cuDNN) for the duration; previous settings restored.""" matmul = torch.backends.cuda.matmul.allow_tf32 precision = torch.get_float32_matmul_precision() try: torch.backends.cuda.matmul.allow_tf32 = False torch.set_float32_matmul_precision("highest") with torch.autocast(device.type, enabled=False), torch.backends.cudnn.flags( enabled=True, benchmark=torch.backends.cudnn.benchmark, deterministic=torch.backends.cudnn.deterministic, allow_tf32=False): yield finally: torch.backends.cuda.matmul.allow_tf32 = matmul torch.set_float32_matmul_precision(precision) @contextlib.contextmanager def _inference_settings(model): """What NeMo's transcribe() sets (eval mode, dither 0, pad_to 0), restored on exit.""" featurizer = getattr(getattr(model, "preprocessor", None), "featurizer", None) saved = {k: getattr(featurizer, k) for k in ("dither", "pad_to") if hasattr(featurizer, k)} training = model.training try: for key in saved: setattr(featurizer, key, 0.0 if key == "dither" else 0) model.eval() with torch.no_grad(): yield finally: for key, value in saved.items(): setattr(featurizer, key, value) model.train(training) def read_audio(path: str | Path) -> np.ndarray: """float32 mono samples of a 16 kHz audio file; multichannel files are averaged to mono.""" import soundfile audio, rate = soundfile.read(str(path), dtype="float32", always_2d=True) if rate != SAMPLE_RATE: raise ValueError(f"{path}: {rate} Hz; convert with: ffmpeg -i {path} -ar 16000 -ac 1 out.wav") return audio.mean(axis=1) if audio.shape[1] > 1 else audio[:, 0] def _as_clip(item) -> np.ndarray: if isinstance(item, (str, Path)): clip = read_audio(item) else: clip = item.detach().cpu().numpy() if isinstance(item, torch.Tensor) else np.asarray(item) if clip.ndim != 1: raise ValueError(f"in-memory audio must be 1-D mono 16 kHz samples, got shape {clip.shape}") clip = clip.astype(np.float32, copy=False) if len(clip) < MIN_DECODE_SAMPLES: clip = np.pad(clip, (0, MIN_DECODE_SAMPLES - len(clip))) return clip def transcribe(model, paths_or_audio, batch_size: int = 16, max_batch_seconds: float = MAX_BATCH_SECONDS): """Greedy TDT transcripts (punctuated, cased), decoded as the reported evaluation decoded. paths_or_audio: one item or a list of items, each a path to a 16 kHz audio file or a 1-D float array/tensor of 16 kHz mono samples in [-1, 1]. Returns a str for a single item and a list of str (in input order) for a list. Decoding: preprocessor -> encoder -> model.decoding.rnnt_decoder_predictions_tensor with the model's own decoding config (greedy_batch TDT, durations 0-4, max 10 symbols per frame) in strict FP32 (autocast off, TF32 off for matmuls and cuDNN), dither 0; on CUDA in NeMo's no_while_loops CUDA-graph mode, as evaluated. Inputs are batched longest first, zero-padded, at most batch_size items and max_batch_seconds of audio per batch. Long audio: each input is decoded in a single pass with full attention; nothing is chunked. Memory grows roughly quadratically with input length. The model was trained on 1-30 s utterances; split long recordings (ideally at pauses) into segments of about 30 s or less. """ single = not isinstance(paths_or_audio, (list, tuple)) items = [paths_or_audio] if single else list(paths_or_audio) if not items: return [] params = list(model.parameters()) if any(p.dtype != torch.float32 for p in params): raise ValueError("decoding is specified in FP32; load() returns an FP32 model") device = params[0].device if device.type == "cuda": _pin_decoder_graphs(model) clips = [_as_clip(item) for item in items] order = sorted(range(len(clips)), key=lambda i: (-len(clips[i]), i)) batches, current, seconds = [], [], 0.0 for i in order: duration = len(clips[i]) / SAMPLE_RATE if current and (len(current) >= batch_size or seconds + duration > max_batch_seconds): batches.append(current) current, seconds = [], 0.0 current.append(i) seconds += duration batches.append(current) texts: list[str | None] = [None] * len(clips) with _strict_fp32(device), _inference_settings(model): for idx in batches: lengths = torch.tensor([len(clips[i]) for i in idx], dtype=torch.long) audio = torch.zeros(len(idx), int(lengths.max()), dtype=torch.float32) for row, i in enumerate(idx): audio[row, :len(clips[i])] = torch.from_numpy(clips[i]) enc, enc_len = model.forward(input_signal=audio.to(device), input_signal_length=lengths.to(device)) hyps = model.decoding.rnnt_decoder_predictions_tensor( encoder_output=enc, encoded_lengths=enc_len, return_hypotheses=False) if isinstance(hyps, tuple): hyps = hyps[0] for i, hyp in zip(idx, hyps): texts[i] = hyp.text if hasattr(hyp, "text") else str(hyp) return texts[0] if single else texts def main(argv: list[str]) -> None: if not argv or argv[0] in ("-h", "--help"): sys.exit("usage: python load_binary.py clip.wav [clip2.wav ...] (16 kHz audio)") from nemo.utils import logging as nemo_logging nemo_logging.setLevel(nemo_logging.ERROR) device = "cuda" if torch.cuda.is_available() else "cpu" model = load(Path(__file__).resolve().parent, device=device) for name, text in zip(argv, transcribe(model, argv)): print(text if len(argv) == 1 else f"{name}\t{text}") if __name__ == "__main__": main(sys.argv[1:])