Instructions to use litert-community/Haas-ja-v1.0 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use litert-community/Haas-ja-v1.0 with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Haas-ja-v1.0 for LiteRT
This repository is a conversion of third-intelligence/Haas-ja-v1.0 (Japanese speech-to-text) to LiteRT, made with litert-torch 0.9.4 and ai-edge-quantizer 0.10.0 for LiteRT 2.3.0. On a Galaxy S26 its float16 encoder, compiled ahead of time for the phone's NPU (Qualcomm SM8850), transcribes 184Γ faster than real time; its float16 encoders on the GPU 63Γ and its int8 encoders on the CPU 46Γ (the 650 utterances of the FLEURS Japanese test set, one after another, measured 2026-11-08 on a warm phone; the NPU runs with the host pipeline on, the GPU and CPU runs serial, see Performance). On those 650 utterances the character error rate (CER strict, defined under Accuracy) on the Galaxy S26's NPU is 3.7009 %, against 3.6886 % for our float32 PyTorch run of the same audio. Speed is still being tuned (updates follow).
third-intelligence/Haas-ja-v1.0 follows NVIDIA's Parakeet-TDT: a FastConformer encoder (24 layers, hidden size 1,024) and a Token-and-Duration Transducer (TDT) decoder whose prediction network is two layers of Kimi Delta Attention instead of an LSTM (its card). The checkpoint (Haas-ja-v1.0.nemo) stores its weights in bfloat16. The encoder graphs are built by the same code as litert-community/parakeet-ultra's from Haas-ja-v1.0's weights (with Haas-ja-v1.0's encoder input scale of 32 folded into the subsampling's linear layer); the prediction network is a new graph that carries the attention state between steps, and the Android runners are built from parakeet-ultra's runner, with that state handled generically. The speeds on this card are for this repository's .tflite files run with LiteRT 2.3.0's C API, not for LiteRT-LM or for any other conversion of this model.
Files
The first release's files are unchanged. Added after it: the encoder for all four window lengths in one file, compiled for the Qualcomm SM8850; the 15 s encoder and mel files; and the runner haas_asr_runner_p8 with its build script.
| file | bytes | what | backend |
|---|---|---|---|
haas_ja_encoder_all_f16_sm8850.tflite |
1,335,000,480 | the f16 encoder for all four window lengths (10, 15, 20, 30 s) in one file, compiled ahead of time for the Qualcomm SM8850 (QAIRT 2.50.0.260828): the four graphs are one compiled program with their weights stored once (1.34 GB, where the four compiled one per file come to 4.92 GB) |
NPU (SM8850) |
haas_ja_encoder_10s_i8.tflite |
616,306,528 | encoder for windows up to 10 s; int8 weights, one scale per output channel, in the linear layers and the two 1Γ1 convolutions of the subsampling, with int8 activations computed at run time; the depthwise convolutions and the 24 relative-position tables stay float32 | CPU |
haas_ja_encoder_20s_i8.tflite |
640,882,528 | the same, up to 20 s | CPU |
haas_ja_encoder_30s_i8.tflite |
665,458,528 | the same, up to 30 s | CPU |
haas_ja_encoder_10s_f16.tflite |
1,194,675,168 | encoder for windows up to 10 s; float16 weights in the linear and convolution layers, LayerNorm inputs rescaled so that float16 arithmetic does not overflow | GPU, NPU |
haas_ja_encoder_15s_f16.tflite |
1,207,061,472 | the same, up to 15 s (240,640 samples); the GPU block below uses the 10 / 20 / 30 s files only | NPU |
haas_ja_encoder_20s_f16.tflite |
1,219,251,168 | the same, up to 20 s | GPU, NPU |
haas_ja_encoder_30s_f16.tflite |
1,243,827,168 | the same, up to 30 s | GPU, NPU |
haas_ja_mel_10s.tflite |
1,608,068 | log-mel front end, 10 s of 16 kHz audio, with the checkpoint's own window and mel filterbank values | CPU |
haas_ja_mel_15s.tflite |
1,932,644 | the same, 15 s (240,640 samples) | CPU |
haas_ja_mel_20s.tflite |
2,252,068 | the same, 20 s | CPU |
haas_ja_mel_30s.tflite |
2,896,068 | the same, 30 s | CPU |
haas_ja_decoder_i8.tflite |
6,479,920 | TDT prediction network (predict, two Kimi Delta Attention layers) and joint network (joint), int8 weights |
CPU |
tokenizer.model |
279,711 | the original's SentencePiece tokenizer (the .nemo member 74b21e79e00d473eb171526fc011af5b_tokenizer.model, byte for byte) |
host |
haas_asr_runner_p8.cc, build_android_p8.sh |
the command-line runner for Android (LiteRT C API) that runs the one-file encoder, the 15 s window length and the three-stage pipeline below, and the script that builds it and haas_asr_runner |
||
haas_asr_runner.cc, build_android.sh |
a command-line runner for Android (LiteRT C API) and its build script: parakeet-ultra's runner (ultra_asr_runner_p.cc) with the decoder state read from the predict signature, the vocabulary size read from joint, no end token and 30 s windows of 480,000 samples (the runner reads the model files from its arguments) |
||
tdt_text.py |
turns the runner's token ids into text with tokenizer.model |
host |
Each encoder graph holds the whole encoder; the window lengths only save the work of padding short audio. The files each path below runs (encoder, mel and decoder files) come to 1.35 GB on the Galaxy S26's NPU (the sm8850 file), 4.88 GB on the NPU when the four f16 files are compiled on the phone, 3.67 GB on the GPU (three f16 files) and 1.94 GB on the CPU and for the desktop script (the i8 files).
How it works
audio (16 kHz mono) β 30 s windows β mel front end β encoder β greedy TDT decoding β text
- Windows. The audio is cut into consecutive windows of 480,000 samples (30 s); the last one is shorter. A window under 320 samples (two mel frames) is dropped and has no text. Each window is transcribed on its own, and the window texts are joined with no separator. Long audio is cut at fixed 30 s points, not at pauses (the original's Haas-ASR code,
transcribe_longform, cuts at quiet points); we did not measure what happens to a word that crosses a cut. No utterance of the FLEURS test set is longer than 30 s (the longest is 28.2 s), so the accuracy below is for single windows. - Buckets. Each window goes to the smallest graph pair that holds it: up to 160,000 samples β the 10 s mel and encoder, up to 240,640 β 15 s, up to 320,000 β 20 s, else 30 s. The paths that run the first release's three files (GPU, CPU, the desktop script) have no 15 s pair: a window over 160,000 samples goes to 20 s or 30 s there. The shorter graphs only skip the work on padding: in float32 they gave the same tokens as our PyTorch reference on all 650 FLEURS utterances (131 / 489 / 30 of them on the 10 / 20 / 30 s graphs, and the 351 of the 489 that the 15 s graph holds on it too).
- Mel front end (
haas_ja_mel_*, inputsaudio[1, samples] float32 andn_valid[1] int32): NeMo'sAudioToMelSpectrogramPreprocessoras this checkpoint configures it β pre-emphasis 0.97, STFT (512 points, 400-sample window, hop 160), 128 mel bins, log with a 2^-24 guard, per-bin normalization over the valid frames. The window and the filterbank are the values stored in the checkpoint (bfloat16 values: they differ from a computed Hann window by up to 1.94e-3 and from librosa's Slaney filterbank by up to 1.17e-4). Outputsfeatures[1, 128, T],mel_mask[1, T] andframe_mask[1, T / 8] (T = 1,000 / 1,504 / 2,000 / 3,000 for 10 / 15 / 20 / 30 s). - One file, four graphs. In the
sm8850file each window length is a signature of its own (enc10,enc15,enc20,enc30) with the inputs and output of the encoder below: the fourf16graphs, compiled for the NPU of that chip, so the phone compiles nothing (haas_asr_runner_p8 --enc-all). - Encoder (
haas_ja_encoder_*, inputsfeaturesandmel_maskas the mel graph gives them): outputenc[1, T / 8, 640], the FastConformer output already projected by the joint network's encoder layer (1,024 β 640); rows past the valid frames are undefined. - Decoding (on the host,
haas_ja_decoder_i8):predict(token[1, 1] int32 and four state tensors:kda0_rec,kda1_rec[1, 5, 64, 64] andkda0_conv,kda1_conv[1, 960, 4], float32) βdec_out[1, 640] and the new state askda0_rec_outand so on;joint(oneencrow [1, 640],dec_out) β 3,078 logits = 3,073 token logits (3072 = blank) + 5 duration logits (0β4 frames). Start from blank with zero state; at each step take the arg-max token and duration; a blank with duration 0 moves one frame; a non-blank token is emitted and fed topredict; after ten tokens emitted on one frame the decoder moves one frame; stop at the last valid frame (the sum offrame_mask). There is no end token and no step limit (NeMo 2.7.3's greedy TDT decoding). - Pipelining (the Android runners'
--pipeline). With--pipeline 2(haas_asr_runner_p8) the mel front end runs on one thread, the encoder on a second and the decoding on the main thread, window after window in the input order, with up to two windows (--pipeline-depth) waiting between two stages, also across files. With--pipeline 1(both runners) a second thread runs the mel front end and the encoder of the next windows (up to two ahead) while the main thread decodes;--pipeline 0runs the three stages one after another on the main thread, window after window. Each window's decoding still starts from blank with zero state, so the setting does not change the tokens: on the Galaxy S26's NPU thesm8850file gave the same tokens, frames and durations under--pipeline 2as under--pipeline 1, and the first release's threef16files the same under--pipeline 1as under--pipeline 0, on all 650 utterances; on the GPU and the CPU,haas_asr_runner_p8under--pipeline 1gave the same ashaas_asr_runnerunder--pipeline 0. With a single window there is nothing to overlap; the setting can only matter over runs of several windows or files (the NPU comparisons under Performance). - Text. Turn each window's token ids into SentencePiece pieces with
tokenizer.modeland decode them (decode_pieces; byte-fallback pieces become their bytes), then remove a space that comes before a punctuation mark of the model's vocabulary, as NeMo does; join the windows and strip the result (tdt_text.py).
Use it
1. Download
Each path below runs one encoder form with the mel files, the decoder and the tokenizer; the runners are built from the repository's sources. This block downloads the Galaxy S26 NPU path:
pip install -U huggingface_hub
hf download litert-community/Haas-ja-v1.0 --local-dir Haas-ja-v1.0 --include haas_ja_encoder_all_f16_sm8850.tflite \
--include "haas_ja_mel_*.tflite" --include haas_ja_decoder_i8.tflite --include tokenizer.model \
--include "*.cc" --include "*.sh" --include "*.py"
cd Haas-ja-v1.0
The repeated --include needs the current hf (checked with huggingface_hub 2.4.0; 0.36 keeps only the last --include).
For another path, replace the first --include with "haas_ja_encoder_*s_f16.tflite" (the four f16 files: the NPU compiled on the phone, and the GPU, which runs three of them) or "haas_ja_encoder_*s_i8.tflite" (the three i8 files: the CPU, and the desktop script below). Without any --include it downloads every file.
2. Your audio
The desktop script (3a) and the Android runners (3b) read 16 kHz mono 16-bit WAV, and the Android runners read a folder with an index file. The block below writes those WAVs and the index from any format ffmpeg reads; replace sample1.wav sample2.wav with your files.
mkdir -p audio && : > audio/index.tsv && i=0
for f in sample1.wav sample2.wav; do
id=$(printf %03d $i)
ffmpeg -loglevel error -y -i "$f" -ac 1 -ar 16000 -c:a pcm_s16le "audio/$id.wav"
printf '%s\t%s\t%s.wav\n' "$id" "$(basename "$f")" "$id" >> audio/index.tsv
i=$((i + 1))
done
The accuracy and speed tables below use the FLEURS files as asr-ja-bench rebuilds them from google/fleurs (16-bit PCM, 16 kHz); the block above writes their samples unchanged. For a 32-bit float file it rounds the samples to 16-bit PCM, which can change a transcript by a token (seen on another model's conversion; not measured on this one).
3a. On a desktop (Python, CPU)
Save this as transcribe.py in the Haas-ja-v1.0 folder (it reads the three i8 encoders: download them with the "haas_ja_encoder_*s_i8.tflite" include above):
import sys
import time
import wave
import numpy as np
from ai_edge_litert.interpreter import Interpreter
from tdt_text import Text
BUCKETS = (10, 20, 30) # seconds: each 30 s window goes to the smallest graph that holds it
mel = {b: Interpreter(f"haas_ja_mel_{b}s.tflite", num_threads=4).get_signature_runner() for b in BUCKETS}
enc = {b: Interpreter(f"haas_ja_encoder_{b}s_i8.tflite", num_threads=4).get_signature_runner() for b in BUCKETS}
dec = Interpreter("haas_ja_decoder_i8.tflite", num_threads=1)
predict, joint = dec.get_signature_runner("predict"), dec.get_signature_runner("joint")
STATE = {"kda0_rec": (1, 5, 64, 64), "kda0_conv": (1, 960, 4), "kda1_rec": (1, 5, 64, 64), "kda1_conv": (1, 960, 4)}
text = Text("tokenizer.model")
BLANK, WINDOW = 3072, 480000
def read_wav(path): # 16 kHz mono 16-bit PCM
with wave.open(path) as w:
assert (w.getframerate(), w.getnchannels(), w.getsampwidth()) == (16000, 1, 2), path
return np.frombuffer(w.readframes(w.getnframes()), "<i2").astype(np.float32) / 32768
def transcribe_window(x):
n = len(x)
b = next(b for b in BUCKETS if n <= b * 16000)
audio = np.zeros((1, b * 16000), np.float32)
audio[0, :n] = x
m = mel[b](audio=audio, n_valid=np.array([n], np.int32))
e = enc[b](features=m["features"], mel_mask=m["mel_mask"])["enc"][0]
frames = int(m["frame_mask"].sum())
out = predict(token=np.array([[BLANK]], np.int32), **{k: np.zeros(s, np.float32) for k, s in STATE.items()})
ids, t, run = [], 0, 0
while t < frames:
logits = joint(enc_frame=e[t:t + 1], dec_out=out["dec_out"])["logits"][0]
token, d = int(np.argmax(logits[:BLANK + 1])), int(np.argmax(logits[BLANK + 1:]))
if token == BLANK and d == 0:
d = 1
if token != BLANK:
ids.append(token)
out = predict(token=np.array([[token]], np.int32), **{k: out[k + "_out"] for k in STATE})
if d == 0:
run += 1
if run >= 10:
d = 1
if d > 0:
run = 0
t += d
return ids
for path in sys.argv[1:]:
x = read_wav(path)
t0 = time.perf_counter()
wins = [transcribe_window(x[s:s + WINDOW]) for s in range(0, len(x), WINDOW) if len(x[s:s + WINDOW]) >= 320]
print(f"{path}\t{len(x) / 16000:.1f} s audio\t{time.perf_counter() - t0:.2f} s\t" + text.windows(wins))
pip install ai-edge-litert==2.3.0 sentencepiece numpy
python transcribe.py audio/*.wav
3b. On an Android phone (native runners)
Build the runners with the Android NDK r29; the script needs curl, unzip and shasum or sha256sum (we built them on macOS; the path below is Android Studio's default there). The script downloads the LiteRT 2.3.0 C headers (litert_cc_sdk.zip from LiteRT's GitHub release) and the runtime libraries (Google's Maven), and checks their sha256.
NDK=$HOME/Library/Android/sdk/ndk/29.0.13113456 bash build_android_p8.sh
Copy the runners, the mel and decoder files and your audio to the phone:
adb shell mkdir -p /data/local/tmp/haas-ja
adb push haas_asr_runner_p8 haas_asr_runner libs/libLiteRt.so haas_ja_mel_*.tflite haas_ja_decoder_i8.tflite audio /data/local/tmp/haas-ja/
adb shell chmod 755 /data/local/tmp/haas-ja/haas_asr_runner_p8 /data/local/tmp/haas-ja/haas_asr_runner
Then run one of the four paths below. Each writes out.jsonl, which the block after them turns into text.
NPU, Galaxy S26 (Qualcomm SM8850). The sm8850 file is compiled for that chip, and the phone loads it without compiling. The block pushes LiteRT's Qualcomm libraries (libLiteRtDispatch_Qualcomm.so and libLiteRtCompilerPlugin_Qualcomm.so, from the litert-npu-runtime-qualcomm package that build_android_p8.sh fetches) and the QNN libraries of the Qualcomm AI Runtime SDK (QAIRT) 2.50.0.260828, the version the file was compiled with, which you download from Qualcomm; copy these eight files flat into a folder qnn/: from lib/aarch64-android/, libQnnHtp.so, libQnnHtpPrepare.so, libQnnHtpV81Stub.so, libQnnHtpV81CalculatorStub.so, libQnnSystem.so, libQnnIr.so and libQnnSaver.so; from lib/hexagon-v81/unsigned/, libQnnHtpV81Skel.so. Without libLiteRtClGlAccelerator.so on the phone (the GPU block pushes it) the run prints GPU accelerator could not be loaded and registered on stderr and goes on.
adb push libs/libLiteRtDispatch_Qualcomm.so libs/libLiteRtCompilerPlugin_Qualcomm.so qnn/. haas_ja_encoder_all_f16_sm8850.tflite /data/local/tmp/haas-ja/
adb shell 'cd /data/local/tmp/haas-ja && LD_LIBRARY_PATH=. ADSP_LIBRARY_PATH="/data/local/tmp/haas-ja;/vendor/dsp/cdsp;/vendor/lib/rfsa/adsp;/system/lib/rfsa/adsp;/dsp" \
./haas_asr_runner_p8 --accel npu+cpu --libdir . --enc-all haas_ja_encoder_all_f16_sm8850.tflite \
--mel haas_ja_mel_30s.tflite --mel10 haas_ja_mel_10s.tflite --mel20 haas_ja_mel_20s.tflite \
--mel-bucket 15=haas_ja_mel_15s.tflite \
--dec haas_ja_decoder_i8.tflite --mel-threads 2 --dec-threads 1 --pipeline 2 \
--fixtures audio --count $(grep -c . audio/index.tsv) --rounds 0 --dump none --out out' > out.jsonl
NPU compiled on the phone (the f16 files). The sm8850 file is compiled for that chip only. haas_asr_runner_p8 compiles each f16 encoder for the phone's Hexagon NPU the first time (LiteRT's Qualcomm compiler plugin, on-device JIT) and keeps the result in --cache-dir. It needs the libraries of the block above; these V81 libraries are for the Galaxy S26's Hexagon NPU, and we have run this path on the Galaxy S26 only. Download its encoders with their own --include (above). The block passes --window-samples = 30 / 20 / 15 / 10 Γ 16,000, the longest window each compile run cuts; each graph holds at least that (the 15 s graph holds up to 240,640 samples). Compile one encoder per process (on a Galaxy S26 a process that compiled one encoder peaked at 5.1β6.3 GB of memory, see Notes): the loop stops at the first encoder that does not compile, and the last command runs all four from the cache only when all four compiled. Run this block with other apps closed and before any GPU run (the compile needs up to 6.3 GB, see Notes):
adb push libs/libLiteRtDispatch_Qualcomm.so libs/libLiteRtCompilerPlugin_Qualcomm.so qnn/. haas_ja_encoder_*s_f16.tflite /data/local/tmp/haas-ja/
NPU_ENV='LD_LIBRARY_PATH=. ADSP_LIBRARY_PATH="/data/local/tmp/haas-ja;/vendor/dsp/cdsp;/vendor/lib/rfsa/adsp;/system/lib/rfsa/adsp;/dsp"'
compiled=0
for b in 30 20 15 10; do
adb shell "cd /data/local/tmp/haas-ja && $NPU_ENV ./haas_asr_runner_p8 --accel npu+cpu --libdir . --cache-dir cache \
--model haas_ja_encoder_${b}s_f16.tflite --mel haas_ja_mel_${b}s.tflite --window-samples $((b * 16000)) \
--dec haas_ja_decoder_i8.tflite --fixtures audio --count 1 --rounds 0 --dump none --out out_compile" > compile_$b.log &&
grep -q '^done wall_ms=' compile_$b.log || { echo "the ${b} s encoder did not compile (see compile_$b.log): stopped" >&2; break; }
compiled=$((compiled + 1))
done
[ "$compiled" = 4 ] && adb shell "cd /data/local/tmp/haas-ja && $NPU_ENV ./haas_asr_runner_p8 --accel npu+cpu --libdir . --cache-dir cache \
--model haas_ja_encoder_30s_f16.tflite --mel haas_ja_mel_30s.tflite \
--model10 haas_ja_encoder_10s_f16.tflite --mel10 haas_ja_mel_10s.tflite \
--model20 haas_ja_encoder_20s_f16.tflite --mel20 haas_ja_mel_20s.tflite \
--mel-bucket 15=haas_ja_mel_15s.tflite --enc-bucket 15=haas_ja_encoder_15s_f16.tflite \
--dec haas_ja_decoder_i8.tflite --mel-threads 2 --dec-threads 1 --pipeline 2 \
--fixtures audio --count \$(grep -c . audio/index.tsv) --rounds 0 --dump none --out out" > out.jsonl
GPU. The first release's three f16 files with haas_asr_runner, serial (--pipeline 0), as in the performance table:
adb push libs/libLiteRtClGlAccelerator.so haas_ja_encoder_10s_f16.tflite haas_ja_encoder_20s_f16.tflite haas_ja_encoder_30s_f16.tflite /data/local/tmp/haas-ja/
adb shell 'cd /data/local/tmp/haas-ja && LD_LIBRARY_PATH=. ./haas_asr_runner --accel gpu --gpu-precision fp16 --libdir . \
--model haas_ja_encoder_30s_f16.tflite --mel haas_ja_mel_30s.tflite \
--model10 haas_ja_encoder_10s_f16.tflite --mel10 haas_ja_mel_10s.tflite \
--model20 haas_ja_encoder_20s_f16.tflite --mel20 haas_ja_mel_20s.tflite \
--dec haas_ja_decoder_i8.tflite --mel-threads 2 --dec-threads 1 --pipeline 0 \
--fixtures audio --count $(grep -c . audio/index.tsv) --rounds 0 --dump none --out out' > out.jsonl
CPU. The first release's three i8 files with haas_asr_runner, the encoder at 8 threads, serial (--pipeline 0), as in the performance table; --threads 8 is what we ran on the Galaxy S26, set it for your phone. Without libLiteRtClGlAccelerator.so on the phone (the GPU block pushes it) the run prints GPU accelerator could not be loaded and registered on stderr and goes on.
adb push haas_ja_encoder_10s_i8.tflite haas_ja_encoder_20s_i8.tflite haas_ja_encoder_30s_i8.tflite /data/local/tmp/haas-ja/
adb shell 'cd /data/local/tmp/haas-ja && LD_LIBRARY_PATH=. ./haas_asr_runner --accel cpu --libdir . \
--model haas_ja_encoder_30s_i8.tflite --mel haas_ja_mel_30s.tflite \
--model10 haas_ja_encoder_10s_i8.tflite --mel10 haas_ja_mel_10s.tflite \
--model20 haas_ja_encoder_20s_i8.tflite --mel20 haas_ja_mel_20s.tflite \
--dec haas_ja_decoder_i8.tflite --threads 8 --mel-threads 2 --dec-threads 1 --pipeline 0 \
--fixtures audio --count $(grep -c . audio/index.tsv) --rounds 0 --dump none --out out' > out.jsonl
Text.
pip install sentencepiece
python tdt_text.py tokenizer.model out.jsonl
Each line of out.jsonl that starts with {"kind":"utt" holds one file's token ids (tok), their encoder frame (frame = window index Γ 375 + the frame within the window; 375 = the encoder frames of a 30 s window) and duration (dur), and the stage times in ms. The burst_summary line gives rtf_wall, the run's wall clock from the first window's start to the last window's end over the audio time (the model load and the warm-up file excluded), and rtf_burst, the summed stage times over the audio time, which counts the overlapped time twice under --pipeline 1 and --pipeline 2.
To remove everything from the phone: adb shell rm -rf /data/local/tmp/haas-ja.
Accuracy
Character error rate (%) on the 650 utterances of the FLEURS Japanese test set as asr-ja-bench v1.1 rebuilds it (two reference texts corrected by the benchmark), scored with jacer 1.2 (metrics v1.2) as the original's card scores: CER strict (the primary metric), CER flex (kanji / kana choice ignored) and CER kana (readings). The reference is our float32 PyTorch run of a reimplementation of the original's modules (the NeMo 2.7.3 preprocessor arithmetic in PyTorch and the FastConformer encoder as transformers 5.20.2's ParakeetEncoder, both checked against NeMo's own modules on a Mac's CPU, and the Kimi Delta Attention prediction network rewritten in plain PyTorch from flash-linear-attention 0.5.2, whose Triton kernels do not run on macOS) on the same audio, one 30 s window per utterance, greedy TDT decoding; the original's card reports 3.67 / 2.98 / 2.34 on this set (bfloat16, on a GPU).
| backend | CER strict | CER flex | CER kana |
|---|---|---|---|
| PyTorch reference (float32, Mac CPU) | 3.6886 | 3.0182 | 2.3829 |
Galaxy S26, NPU (sm8850 file) |
3.7009 | 3.0029 | 2.3438 |
Galaxy S26, NPU (the first release's three f16 files, compiled on the phone) |
3.7009 | 3.0029 | 2.3438 |
Galaxy S26, GPU (three f16 files, float16 arithmetic) |
3.6825 | 3.0029 | 2.3737 |
Galaxy S26, CPU (three i8 files) |
3.6978 | 3.0091 | 2.3553 |
We did not run the four f16 files compiled on the phone (with the 15 s one) on the 650 utterances; the sm8850 file holds the same four graphs, compiled ahead of time.
Performance
| phone | backend | RTF | Γ real time | encoder per call, 30 / 20 / 15 / 10 s window (ms) | mel, 30 s (ms) | TDT decoding of a 30 s window (ms) | peak memory |
|---|---|---|---|---|---|---|---|
| Galaxy S26 | NPU (Hexagon, sm8850 file, --pipeline 2) |
0.005434 | 184Γ | 182.4 / 78.1 / 46.4 / 41.4 | 4.5 | 12.1 | 1.50 GB (1,463,664 kB) |
| Galaxy S26 | NPU (Hexagon, the first release's three f16 files compiled on the phone, --pipeline 1) |
0.007489 | 134Γ | 187.2 / 80.5 / β / 46.8 | 3.9 | 12.4 | 4.95 GB (4,838,264 kB) |
| Galaxy S26 | GPU (three f16 files, fp16 arithmetic, --pipeline 0) |
0.015935 | 63Γ | 157.4 / 106.6 / β / 60.7 | 3.6 | 8.9 | 7.73 GB (7,553,244 kB) |
| Galaxy S26 | CPU (three i8 files, 8 threads, --pipeline 0) |
0.021774 | 46Γ | 282.3 / 211.2 / β / 108.6 | 4.8 | 18.6 | 4.49 GB (4,388,868 kB) |
- RTF =
rtf_wall: the wall clock of a run from the first window's start to the last window's end over the audio time, the model load and one warm-up utterance excluded, for the 650 FLEURS Japanese test utterances (8,511.1 s of audio) run one after another in one process with the settings of the row (plus a per-window dump of the outputs for scoring; the mel graphs at 2 threads, the decoder at 1). Thesm8850row ran the four window lengths withhaas_asr_runner_p8, the windows going 131 / 351 / 138 / 30 to the 10 / 15 / 20 / 30 s graphs; the other rows are the first release's files withhaas_asr_runner, the windows going 131 / 489 / 30 to the 10 / 20 / 30 s graphs. The two NPU rows are the mean of two runs (thesm8850file: 0.005102 and 0.005766; the threef16files: 0.007303 and 0.007676), the GPU and CPU rows one run each. The fourf16files compiled on the phone were not timed on the 650 utterances. --pipeline 2against--pipeline 1on the NPU with thesm8850file, four runs in one session in the order 1, 2, 2, 1: 0.005478, 0.005102, 0.005766, 0.006607; a third--pipeline 2run after them gave 0.005794.--pipeline 1against--pipeline 0with the first release's threef16files: 0.007489 against 0.008936, the means of two runs each, in one session in the order 0, 1, 1, 0.- Later runs. The GPU and CPU rows' settings, run again in a later session: 0.017771 and 0.021059 on the GPU, 0.023977 on the CPU.
- Heat. The runs are those of a warm phone. The
sm8850runs ran at Android's thermal status 3, with the clocks of the two CPU clusters capped in all of their readings (down to 1.23 / 1.27 GHz, of 3.63 / 4.74 GHz). The GPU run started from a phone at rest, but the GPU clock was capped at 578 MHz (of 1,300 MHz, thermal power level 9) in 50 of its 72 readings; the CPU run ran with the clocks of the two CPU clusters capped at 1.56 / 1.50 GHz (of 3.63 / 4.74 GHz) in 52 of its 70 readings (thermal status 1β2). - Per call: the median of 20 calls after 3 warm-up calls, input write and output read included, on FLEURS Japanese test utterance 9518252661993015549 (28.2 s: its 30 s window with 353 valid encoder frames, and its first 20 s, 15 s (240,640 samples) and 10 s), the mel graphs at 2 threads and the decoder at 1, each run started from a phone at rest (thermal status 0, no CPU frequency cap). During the timed calls the phone capped the clocks of the two CPU clusters, in the CPU run down to 2.00 / 1.98 GHz (of 3.63 / 4.74 GHz), so the CPU numbers are those of a capped CPU; the NPU and GPU runs also saw CPU caps during their timed calls (down to 2.90 / 2.88 and 2.75 / 2.67 GHz, and in the
sm8850file's run down to 3.19 / 3.40 GHz, from its second warm-up call on). "β": the first release's files have no 15 s graph. - Peak memory: the runner's VmHWM during the 650-utterance run (GB = kB Γ 1024 / 10βΉ; for the NPU rows, the larger of their two runs).
- Galaxy S26 (SM-S942Q, Android 16); LiteRT 2.3.0 from Google's Maven; on the NPU the QNN libraries of QAIRT 2.50.0.260828 for Hexagon v81; 2026-11-08.
Notes
- First NPU run,
sm8850file. The phone compiles nothing: LiteRT logs that the model is pre-compiled and applies no compiler plugin, and on a Galaxy S26 the runner loaded the file's four graphs in 0.62β0.85 s (nine runs), each graph on the NPU as one partition. Over a whole run the runner's peak resident memory was 1.49β1.50 GB (VmHWM 1,455,892β1,469,272 kB, the same nine runs). - First NPU run, the
f16files. On a Galaxy S26 the on-device compile took 56β132 s for the 30 s encoder, 141β235 s for the 10 s encoder and 59β95 s for the 20 s encoder (four runs ofhaas_asr_runner, one encoder per process), and the three compiled encoders take 3.70 GB in the cache folder (3,611,529 kB); a later run loaded each of the three from the cache in 0.9β2.9 s (eight runs). A process that compiled one encoder peaked at 5.4β6.3 GB of resident memory (VmHWM 5,257,204β6,117,668 kB over the twelve compiles of the four runs), and the phone's available memory went down to 1.28 GB (1,249,244 kB) in one 30 s encoder's process, the lowest of the twelve. Withhaas_asr_runner_p8and the four files (the on-phone compile block under Use it, one run) the compiles took 89 / 93 / 50 / 207 s for the 30 / 20 / 15 / 10 s encoders, each compile process peaked at 5.1β5.4 GB (VmHWM 4,946,976β5,321,916 kB), the four compiled encoders take 4.92 GB in the cache folder (4,806,380 kB), and the last command loaded them in 1.8β2.1 s. - GPU and fp16. In the checkpoint's encoder the squared deviation inside a LayerNorm reaches 103,163 over the 217 windows of our range check (the first block's output LayerNorm), beyond float16's largest value 65,504. The
f16encoders scale 99 of the 120 LayerNorm inputs by 2^-k (k = 1β4 per norm) and their epsilon by 4^-k, which left the float32 output bit-identical in our check and keeps every square in range (1,023.8 at most over the same windows); the GPU (float16 arithmetic) and the NPU run these files. The 15 s file was built with the same k per norm. - int8 decoder. The decoder's weights are int8, one scale per output channel, and its linear layers take int8 activations computed at run time (dynamic range), as the
i8encoders do. Against the float32 decoder on our reference's encoder output (Mac CPU) it gives the same token sequence on 634 of the 650 FLEURS utterances, and CER 3.6856 / 3.0060 / 2.3668 against 3.6886 / 3.0182 / 2.3829. - int8 encoders. The
i8encoders are lossy: on a Mac CPU (with the int8 decoder) their CER on the 650 utterances is 3.6978 / 3.0091 / 2.3553 against the reference's 3.6886 / 3.0182 / 2.3829 (CER strict 0.0092 points above it, CER flex and kana below it), and 90 of the 650 transcripts differ from the reference's.
License
The model files are under the Apache License 2.0, as third-intelligence/Haas-ja-v1.0 is; its card says: "The model weights and the inference code are released under the Apache 2.0 License. Third-party components retain their respective licenses." The runners and the scripts (haas_asr_runner_p8.cc, build_android_p8.sh, haas_asr_runner.cc, build_android.sh, tdt_text.py) are under the Apache License 2.0.
- Downloads last month
- -
Model tree for litert-community/Haas-ja-v1.0
Base model
third-intelligence/Haas-ja-v1.0