Kroko Community ASR validation

July 31, 2026 ยท View on GitHub

This report covers the native kroko_asr community-model implementation, public 64-L and 128-L Kroko packages, stateful streaming, word timestamps, multilingual behavior, and standalone Q8 GGUF loading. The original sherpa-onnx execution path is used as the reference.

Implementation scope

  • Native Kaldi-compatible filterbank, Conv2dSubsampling/ConvNeXt frontend, 19-layer Zipformer2, stateless RNN-T predictor, joiner, greedy search, and modified beam search.
  • Offline and stateful streaming sessions with partial transcripts.
  • Word timestamps derived from RNN-T emission frames.
  • Blank penalty, natural-text hotwords, and three-rule endpoint segmentation.
  • Public free German, English, Spanish, French, Italian, Hebrew, Dutch, Portuguese, Swedish, and Turkish packages.
  • Dynamic support for both public chunk layouts: 64-L (141/128) and 128-L (269/256).
  • Converted safetensors and movable standalone Q8 GGUF packages.
  • CLI, generic server task routes, and OpenAI-compatible transcription routes.
  • Model-manager installation with a language/size-specific output directory.

Test environment

Tests were run on 2026-07-27 with:

  • Windows 11 Pro Insider Preview 10.0.26220
  • AMD Ryzen 9 7950X3D, 16 cores / 32 logical processors
  • 63.1 GiB system RAM
  • NVIDIA RTX 3090 24,576 MiB, driver 591.86
  • CUDA 12.4 and CMake 3.29.2
  • sherpa-onnx 1.13.4 with CPUExecutionProvider

The native package paths were:

..\models\Kroko-ASR\Kroko-{DE,EN,ES,FR,IT,IW,NL,PT,SV,TR}-Community-64-L-Native
..\models\Kroko-ASR\Kroko-EN-Community-128-L-Native

The source packages were downloaded from Banafo/Kroko-ASR. Only packages whose header declares free=true were used.

Exact build commands

Configure the CUDA build with the validation probes enabled:

$cuda = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4"
$env:CUDA_PATH = $cuda
$env:CUDAToolkit_ROOT = $cuda
$env:CUDACXX = Join-Path $cuda "bin\nvcc.exe"
$env:NVCC_PREPEND_FLAGS = "-allow-unsupported-compiler"

.\scripts\build_windows.ps1 `
  -Preset windows-cuda-release -ConfigureOnly

cmake -S . -B build\windows-cuda-release `
  -DENGINE_BUILD_WARMBENCH=ON `
  -DCMAKE_CUDA_COMPILER="$env:CUDACXX"

The final build command was:

cmd /c '"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul && cmake --build build\windows-cuda-release --config Release --target audiocpp_cli audiocpp_server audiocpp_gguf kroko_asr_streaming_probe kroko_asr_encoder_probe -j 8'

The CLI, server, GGUF converter, and both Kroko probes built successfully.

Loader/package-catalog validation:

.\venv\Scripts\python.exe tools\check_loader_catalog_sync.py --self-test
.\venv\Scripts\python.exe tools\check_loader_catalog_sync.py

Result:

Ran 2 tests ... OK
active_loaders=37 commented_loaders=0 catalog_packages=48
ok: installable catalog families match registered loaders

Package conversion

Install the conversion and reference dependencies:

.\venv\Scripts\python.exe -m pip install `
  numpy onnx onnxruntime safetensors sherpa-onnx `
  soundfile huggingface_hub edge-tts psutil

Convert one free package through the model manager:

.\venv\Scripts\python.exe tools\model_manager_deprecated.py install `
  kroko_asr_community_converted `
  --source-file ..\models\Kroko-ASR\Kroko-DE-Community-64-L-Streaming-001.data `
  --models-root ..\models\Kroko-ASR --overwrite

The manager infers Kroko-DE-Community-64-L-Native; the language packages therefore do not overwrite each other. The direct equivalent is:

.\venv\Scripts\python.exe tools\community_models\convert_kroko_onnx.py `
  ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Streaming-001.data `
  ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native --overwrite

Each converted directory contains config.json, model.safetensors, and tokens.txt.

Original-model comparison

The test samples and manifest were generated with:

.\venv\Scripts\python.exe tests\kroko_asr\generate_multilingual_samples.py `
  --output ..\outputs\kroko_multilingual_samples

The matrix invokes the original sherpa-onnx greedy decoder and audio.cpp separately for every language. It reports exact transcript parity, normalized WER, ground-truth WER, word-timestamp delta, wall time, RTF, and process RSS.

.\venv\Scripts\python.exe tests\kroko_asr\kroko_multilingual_matrix.py `
  --cli build\windows-cuda-release\bin\audiocpp_cli.exe `
  --models-root ..\models\Kroko-ASR `
  --samples ..\outputs\kroko_multilingual_samples `
  --output ..\outputs\kroko_multilingual_results `
  --backend cpu

Per-request results:

LangAudio sReference ms / RTFaudio.cpp ms / RTFExactReference WERGround-truth WERTimestamp max deltaPeak RSS MiB
de5.544112.447 / 0.0203251.224 / 0.0453Yes000.200 us1345.3
en4.992114.649 / 0.0230251.630 / 0.0504Yes000.162 us1345.2
es4.800103.344 / 0.0215252.364 / 0.0526Yes000.172 us1345.0
fr5.688112.706 / 0.0198254.749 / 0.0448Yes000.229 us1345.2
it4.680102.171 / 0.0218248.670 / 0.0531Yes000.191 us1345.0
he5.040112.469 / 0.0223256.904 / 0.0510Yes000.162 us1345.1
nl5.400115.656 / 0.0214250.902 / 0.0465Yes000.153 us1345.5
pt5.136102.669 / 0.0200254.222 / 0.0495Yes000.305 us1345.0
sv4.968111.764 / 0.0225243.224 / 0.0490Yes00.16670.181 us1337.8
tr5.472103.432 / 0.0189252.660 / 0.0462Yes000.200 us1345.3

Reference WER compares audio.cpp with the original decoder. All ten requests are exact string matches and have zero reference WER. Swedish ground-truth WER is one token because both paths write Idag while the source prompt writes I dag; this is not a port mismatch. Word counts match for every request. The worst word-start difference is 0.305 microseconds, which is floating-point representation noise around the same 40 ms frame grid.

The complete machine-readable result and per-language artifacts are:

..\outputs\kroko_multilingual_results\multilingual_parity.json
..\outputs\kroko_multilingual_results\<language>_reference.json
..\outputs\kroko_multilingual_results\<language>_audiocpp.txt
..\outputs\kroko_multilingual_results\<language>_audiocpp_words.json

The optimized ONNX Runtime CPU reference is roughly 2.17-2.48 times faster in this short-request matrix. The native port runs about 19-22 times faster than real time on CPU.

The performance pass preserves the accumulation order used for every vocabulary score while:

  • evaluating the 512-element joiner tanh activation once per encoder frame instead of once for each of 650 vocabulary rows;
  • evaluating independent vocabulary rows in parallel and retaining the serial argmax order;
  • batching recurrent-state and constant transfers asynchronously, with one synchronization at each graph boundary;
  • reusing the padded feature-chunk buffer and decoding directly from the valid prefix of the encoder output.

Against the pre-optimization matrix recorded before this pass, the ten CPU requests improve by 1.804-1.957x, with a 1.881x arithmetic-mean speedup. A device-resident state experiment was rejected because it shifted several token emissions by one 40 ms frame. The retained implementation keeps the host-state round trip so that transcript and timestamp parity remain exact.

A focused five-run EN_3 measurement with the best native thread count found on this machine (--threads 8) measured 203.664 ms mean native session time and 88.704 ms mean sherpa-onnx time, a 2.296x ratio. Two further shortcuts were rejected during this pass: keeping recurrent states resident changed the transcript, while retaining allocator-managed graph constants changed the selected final tokens. Both therefore remain explicitly transferred at graph boundaries.

Modified beam, hotwords, blank penalty, and endpoints

The reference runner exposes the relevant sherpa-onnx controls:

.\venv\Scripts\python.exe tests\kroko_asr\kroko_reference_transcribe.py `
  ..\models\Kroko-ASR\extracted-en-128 ..\SAMPLES\EN_3.wav `
  --decoding-method modified_beam_search --max-active-paths 4 `
  --output ..\outputs\kroko_reference_en3_beam.json

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task asr --family kroko_asr `
  --model ..\models\Kroko-ASR\Kroko-EN-Community-128-L-Native `
  --backend cpu --threads 8 --audio ..\SAMPLES\EN_3.wav --language en `
  --request-option decoding_method=modified_beam_search `
  --request-option num_beams=4 `
  --text-out ..\outputs\kroko_native_en3_beam.txt

Both requests produced:

If you actually care about yourself, you could test numbers one by one until you sleep

With blank_penalty=1.0, the reference retained that text and native execution selected the adjacent punctuation-bearing path ending in sleep.. This is one token-boundary decision under the measured encoder floating-point drift, not a decoder-control failure. num_beams=1 remains identical to greedy search.

Natural-text hotword bias was tested with 32 paths and score 15 to make its effect visible. For example, hotwords=tomorrow changed the acoustically competing phrase from test numbers to tomorrow numbers; security and instruments likewise changed selected paths. The production default score remains 1.5. The implementation uses the package token table directly, because the public Kroko bundles do not include the SentencePiece model sherpa-onnx would otherwise require for natural-text hotword encoding.

Endpoint behavior was tested with EN_2.wav, three seconds of silence, and EN_3.wav concatenated into one 16 kHz stream:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task asr --mode streaming --family kroko_asr `
  --model ..\models\Kroko-ASR\Kroko-EN-Community-128-L-Native `
  --backend cpu --threads 8 `
  --audio ..\outputs\kroko_endpoint_two_utterances.wav --language en `
  --request-option enable_endpoint=true `
  --request-option rule2_min_trailing_silence_sec=1.2 `
  --text-out ..\outputs\kroko_endpoint_native.txt `
  --segments-out ..\outputs\kroko_endpoint_native_segments.json

The transcript preserved both utterances and the result contained two contiguous segments, [0,81920] and [81920,165974]. At the endpoint the decoder starts a new output segment and resets hotword state, while preserving the encoder states and each hypothesis's last two predictor tokens like sherpa-onnx. Emitted tokens and absolute word frames remain continuous in the final result. The sherpa-onnx reference produced the same words and endpoint split; its per-segment string join rendered security , If, while audio.cpp normalizes that boundary to security, If.

Encoder boundary parity

The probes feed two deterministic feature chunks through the native model and compare the frontend boundary and final encoder boundary with the source ONNX.

128-L English:

build\windows-cuda-release\bin\kroko_asr_streaming_probe.exe `
  ..\models\Kroko-ASR\Kroko-EN-Community-128-L-Native `
  ..\outputs\kroko_en128_streaming_final.f32 `
  ..\outputs\kroko_en128_embedding_final.f32

.\venv\Scripts\python.exe tests\kroko_asr\kroko_onnx_streaming_parity.py `
  ..\models\Kroko-ASR\extracted-en-128\encoder.onnx `
  ..\models\Kroko-ASR\Kroko-EN-Community-128-L-Native\config.json `
  ..\outputs\kroko_en128_streaming_final.f32 `
  --native-embedding ..\outputs\kroko_en128_embedding_final.f32

64-L Swedish uses the same commands with extracted-sv-64, Kroko-SV-Community-64-L-Native, and kroko_sv64_*.

Variant / boundaryChunk 1 cosineChunk 2 cosine
128-L frontend embedding0.9999590550.999957586
128-L final encoder0.9995737750.999731910
128-L Zipformer with native embedding0.9995806900.999710517
64-L frontend embedding0.9999548530.999955603
64-L final encoder0.9997395540.999864404
64-L Zipformer with native embedding0.9997241760.999878200

The remaining small numerical drift is expected from executing converted ONNX operators through ggml rather than ONNX Runtime. End-to-end greedy transcripts and timestamp frames nevertheless match exactly in the multilingual matrix.

Streaming and long-audio path

A 49.680-second 16 kHz Swedish sample was run through both modes:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task asr --mode streaming --family kroko_asr `
  --model ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native `
  --backend cpu --audio ..\outputs\kroko_sv_49s.wav --language sv `
  --text-out ..\outputs\kroko_sv_49s_streaming.txt `
  --words-out ..\outputs\kroko_sv_49s_streaming_words.json --log

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task asr --mode offline --family kroko_asr `
  --model ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native `
  --backend cpu --audio ..\outputs\kroko_sv_49s.wav --language sv `
  --text-out ..\outputs\kroko_sv_49s_offline.txt `
  --words-out ..\outputs\kroko_sv_49s_offline_words.json --log
ModeSession msRTFEncoder chunksPeak retained audio values
Streaming2051.9940.04134038,560
Offline1984.5660.039940n/a

The transcript files and word-timestamp JSON files are byte-identical. The stream accepted 794,880 samples but retained only 38,560 values at peak, demonstrating bounded 16 kHz waveform buffering. Relative to the original 3981.768 ms streaming and 3936.170 ms offline measurements, these paths are 1.940x and 1.983x faster.

The bounded resampler was separately tested with a 48 kHz stereo rendering of the 4.992-second English sample. Streaming and offline transcript/word JSON were byte-identical. The stream accepted the equivalent of 79,872 normalized 16 kHz samples while peaking at 48,000 normalized waveform values and 48,000 source values (one one-second stereo-mixdown input chunk), rather than retaining the full source waveform until finalization.

Standalone Q8 GGUF and CUDA

Build:

build\windows-cuda-release\bin\audiocpp_gguf.exe `
  --input ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native\model.safetensors `
  --root ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native `
  --family kroko_asr --type q8_0 --overwrite `
  --output ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native\Kroko-SV-Community-64-L-Q8.gguf

Run:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task asr --mode streaming --family kroko_asr `
  --model ..\models\Kroko-ASR\Kroko-SV-Community-64-L-Native\Kroko-SV-Community-64-L-Q8.gguf `
  --backend cuda --audio ..\outputs\kroko_multilingual_samples\sv.wav `
  --language sv --text-out ..\outputs\kroko_sv_q8_cuda.txt `
  --words-out ..\outputs\kroko_sv_q8_cuda_words.json --log

The 167,754,176-byte GGUF has SHA-256 2CBC4B2C69217D44FEDCD3B890AD563BA0E4FD9DDF78C105AFCC003D245EBB4F. It embeds the config, tokens, and package spec and runs from the file path with no external sidecars. Seven clean CUDA CLI process runs completed the 4.968-second sample in 223.328-244.833 ms, with a 232.341 ms median (RTF 0.0468). The pre-optimization validation run took 462.556 ms, so the median is 1.991x faster. Every run's transcript and word JSON are byte-identical to the native safetensors CPU result.

Server validation

Server configuration:

{
  "host": "127.0.0.1",
  "port": 18080,
  "backend": "cuda",
  "models": [{
    "id": "kroko-sv-stream",
    "family": "kroko_asr",
    "path": "../models/Kroko-ASR/Kroko-SV-Community-64-L-Native/Kroko-SV-Community-64-L-Q8.gguf",
    "task": "asr",
    "mode": "streaming"
  }]
}
build\windows-cuda-release\bin\audiocpp_server.exe `
  --config ..\outputs\kroko_server_test.json --log

curl.exe -N http://127.0.0.1:18080/v1/audio/transcriptions `
  -F "file=@../outputs/kroko_multilingual_samples/sv.wav" `
  -F "model=kroko-sv-stream" -F "language=sv" `
  -F "stream=true" -F "response_format=json"

GET /health returned status=ok, backend=cuda, and one model. Two long-lived-server requests both returned the exact reference transcript:

RequestWall msSession ms
1306.420293.179
2138.293125.561

An additional JSON transcription request forwarded decoding_method=modified_beam_search, num_beams=4, blank_penalty=0.5, and enable_endpoint=true. The Q8/CUDA server returned the expected Swedish transcript in 300.322 ms wall time (291.590 ms session time), and trace output confirmed all four request options reached the model.

After request 2 the server used 619.5 MiB RSS and 1478.1 MiB private memory. Reliable per-process CUDA VRAM is not available through Windows WDDM, so only the physical device capacity is reported. Artifacts are:

..\outputs\kroko_server_request_1.json
..\outputs\kroko_server_request_2.json
..\outputs\kroko_server_stdout_final.log
..\outputs\kroko_server_stderr_final.log

The generic /v1/tasks/run and /v1/tasks/stream result schemas preserve word_timestamps. The OpenAI-compatible transcription route returns its standard text/delta events and currently does not serialize the model's word array.

Backend coverage

BackendCoverage
CPUAll ten languages; original parity; timestamps; beam/blank/hotwords/endpoints; 64-L/128-L probes; bounded offline/streaming equality
CUDAStandalone Q8 CLI streaming; beam/blank/endpoints; two-request server session
VulkanNot tested
MetalNot tested

Known limitations

  • Conversion accepts only public packages whose header declares free=true. Commercial/encrypted models require Kroko's licensed runtime.
  • Each package recognizes one language. Automatic routing across packages is not implemented.
  • Native CPU execution currently prioritizes parity and is slower than the optimized ONNX Runtime reference.
  • Vulkan and Metal still require contributor testing.

Source references: