A Coding Guide to Cactus Whistle: Does a 16.9 MB Speech Model Hold Up Against Whisper Base? Word Error Rate, Speed, Streaming, Timestamps, Keywords, and Speech-to-Tool Calls on a CPU

Cactus Compute’s Whistle model, a 16.9 MB file running on CPU, transcribes seven languages and returns word timestamps. The model card claims…

By Vane October 22, 2026 4 min read

Cactus Compute’s Whistle model, a 16.9 MB file running on CPU, transcribes seven languages and returns word timestamps. The model card claims it beats Whisper base on LibriSpeech; we test it on seeded test-clean and test-other samples. We compare Whistle with Whisper tiny and base on identical clips, judged by paired speaker-cluster intervals with Holm correction. We find a lead over greedy Whisper base on test-other, no significant gap against beam search, and a speed advantage that narrows as clips grow. Beyond the headline, we probe a 30-second window where the pinned engine silently drops the middle of longer audio, plus streaming latency, timestamps, keyword biasing, other languages, noise, embeddings, and speech-to-tool calls. Our figures are one-run samples from an Apple M4, and published full-split numbers are quoted, not measured.

Setup and weights

We install cactus-needle 3.2.0 with its dependencies pinned and turn off its anonymous telemetry. The package downloads Whistle’s weights, its engine and Needle 3’s weights from Hugging Face repositories whose files are replaced in place, so we pin both repositories to fixed revisions and print SHA-256 hashes. The page() helper reads the datasets-server rows API with rate-limit backoff, and load_audio() resamples clips to 16 kHz. The first test-clean clip, 3.5 s of speech, transcribes in 33 ms of wall time, each word carrying its start, end and probability.

The code imports os, io, re, sys, json, math, time, random, hashlib, shutil, zipfile, platform, traceback, subprocess, collections, urllib.error, and urllib.request. We define RESULTS and VERDICTS as empty lists. The banner() function prints a title surrounded by equals signs. The section() function wraps a function to print the name, start a timer, run the function, save results to a JSON file, and print the elapsed time. If an error occurs, it saves the error type and message to RESULTS and prints the traceback.

We run the pip install command for cactus-needle 3.2.0, openai-whisper 20250625, jiwer 4.0.0, soundfile 0.14.0, and soxr 1.1.0. We set the NEEDLE_TELEMETRY environment variable to 0. We set HF_HUB_DISABLE_PROGRESS_BARS to 1. We import numpy, soundfile, soxr, and hf_hub_download. We define WHISTLE_REV as ca5287601bef25af26dcf2e1b2bdc0843a7c19e5 for Cactus-Compute/whistle dated 2026-10-10. We define NEEDLE3_REV as 93e88ede867e4311c39151285e74df513e4b835b for Cactus-Compute/needle3 dated 2026-10-10. We import needle and importlib.metadata. We define sha as a lambda function that returns the first 12 characters of the SHA-256 hash of a file. We get the CPU using platform.processor() or platform.machine(). If the system is Linux and /proc/cpuinfo exists, we get the model name from that file. If the system is Darwin, we run sysctl -n machdep.cpu.brand_string. We print the cactus-needle version, engine version, wheel tag, python version, soundfile version, soxr version, and numpy version. We print the hardware, number of cpus visible, whistle.cact size in MB and SHA-256, engine size in MB and SHA-256, and needle3.cact size in MB and SHA-256.

We set SR to 16000. We create a Whistle object W. We define the fetch() function to GET with retries. We define the page() function to get one page of a Hub dataset through the datasets-server rows API. We define load_audio() to download one row’s clip and return 16 kHz mono float32 samples and the SHA-256 of the original file. We get the first row from openslr/librispeech_asr, clean, test, 0, 1. We load the audio clip. We measure the time to transcribe the clip with word_timestamps set to True. We print the clip ID, length, reference text, output text, language, engine time to first token, decode tok/s, and wall time. We print the first three words with their start, end, and probability. We save the result to RESULTS.

Comparison method

We import jiwer, whisper, torch, EnglishTextNormalizer, and BasicTextNormalizer. We set NORM_EN to EnglishTextNormalizer. We set NORM_BASIC to BasicTextNormalizer. We set SEED to 2026. We set N_WHISTLE to 300. We set N_PAIRED to a dictionary with other as 100 and clean as 60. We set MAX_CLIP_S to 30.0. We define all_rows() to get all rows from a dataset. We define dataset_sha() to get the SHA of a dataset. We define wer_counts() to get errors and reference words for one normalised pair. We define sdi() to get substitutions, deletions, and insertions. We define corpus_wer() to get the word error rate.

What it means

The Whistle model is small enough to run on a CPU. It returns timestamps for each word. The test shows it beats greedy Whisper base on test-other but not beam search. The speed advantage drops as clips get longer. The engine drops the middle of longer audio in a 30-second window. Streaming latency, timestamps, keyword biasing, other languages, noise, embeddings, and speech-to-tool calls are also tested. The figures are one-run samples from an Apple M4. Full-split numbers are quoted, not measured.

Scroll to Top