Verified locally on 2026-10-10. This is a release candidate, not a published npm release. Tests distinguish real model inference from deterministic lifecycle and capture doubles. No personal microphone audio was recorded.
The initial pnpm 12.4.2 refactor was verified on the same date, placing the SDK
in packages/whistle (@tiny-stt/whistle); at that stage, examples shared its tooling.
The existing dependency versions were imported without upgrades. Frozen installs,
including reuse with --offline, formatting, typecheck, build, all 14 contract
tests, and all four real-model integration tests on both Node 22 and 24 passed.
Chromium, Firefox, and WebKit passed the relocated browser suite. The Node example,
the browser example's /demo/ production build, the clean npm-installed consumer,
and a pnpm-produced tarball's network-denied Node consumer also passed. No engine,
model, public API, or asset-lock changes were needed. The detailed measurements
below and in evidence.json remain the original pre-refactor baseline.
Windows and remote CI were not executed locally.
The follow-up migration replaced Prettier with Biome 2.5.15 and added the private
@tiny-stt/documentation workspace. All public APIs now carry TypeDoc comments,
including properties, defaults, units, cancellation, ownership and cleanup contracts.
TypeDoc 0.28.20 validates the four entry points and renders the static site; its
compiler is isolated from Whistle's compiler. A deliberate missing-comment probe
confirmed the strict public coverage gate fails correctly, then restored the source.
Biome lint/format/import checks, SDK typechecking/build, 14 contract tests, four
Node 22 real-model integration tests, and Chromium's real-model/cache/capture-helper
suite passed after the lint changes. The clean installed-tarball Node/network-denied
and production-browser /demo/ consumer passed again. Vendored runtime and model
hashes are unchanged; generated support files were rebuilt and prepared assets refreshed.
The previous platform/performance measurements below remain the original baseline.
The docs check typechecked the embedded examples and traversed 33 generated pages
and 1,378 local links/assets. Chromium navigation/search passed under /api/ with
external requests blocked. The dev server's guide watch and termination were tested.
The documentation job is configured in CI; remote CI has not been run here.
The browser example now lives in the private @tiny-stt/demo package, using React
and React DOM 19.3.0, Vite 8.3.4 and @vitejs/plugin-react 6.1.2 (stable versions
checked on 2026-10-10). It consumes Whistle's public exports through workspace:*;
React is not an SDK dependency. The Node example now lives in packages/documentation/examples/node.
On macOS arm64, Chromium 156.0.8078.4, Firefox 157.0 and WebKit 27.2 passed real
speech/file transcription and timestamp bounds in the production /demo/ build
with external hosts blocked. Fixture-backed capture tests passed pointer/keyboard
control, manual and 30-second automatic stop, pending/denied permission,
cancellation, pagehide/reload, and track/context/worker cleanup. Development tests
exercise StrictMode, real inference and unmount while permission is pending.
All three also verified actual Vite Fast Refresh, retained transcript state, worker
disposal and model reload. These checks never access physical microphone hardware.
The updated consumer test installs the React app with the packed SDK in a temporary
project using pnpm's offline store, then builds and runs real speech under /demo/.
It also retains installed-CLI offline preparation, network-denied Node speech,
default bundled browser worker/WASM and plain browser ESM checks. npm installation
coverage remains in test:packed-node. Model/runtime revisions and the performance
baseline below are unchanged. CI includes the demo in its desktop-browser matrix;
remote CI remains unexecuted locally.
The requested candidate pair worked; no revision change was needed:
| Artifact | Immutable revision | Bytes | SHA-256 |
|---|---|---|---|
Official needle.js |
c7c415a3d1b3d929014bc6e866d51ebb971f7089 |
62,823 | f3f7366dcad9555b792ee519d2518f3c506038bcb2ffd179e76e850000749359 |
Official needle.wasm |
same runtime revision | 903,655 | c19b9ddf9c7de4eb4f37e5f1811c5bbea9f099041d2a27284daf89789ee8523d |
whistle.cact |
b358ddadd89b7a713b5aa131f23032d3cca1b251 |
16,919,407 | b6e02f048568ac5d01a2042556c658061e699acbc0aa2a1439f52f3d461dffeb |
asset-lock.json contains immutable official URLs, repository
names, header/license hashes and the adapted loader hash. Source distributions:
Needle runtime,
Whistle model.
No needle3.cact was acquired. No website worker/frontend was copied.
The pinned header and loader exports were inspected. tests/abi.mjs reproduces:
needle_load(pointer, BigInt(byteLength)) returns 0; a JS number throws TypeError.needle_models() returns 2 (speech). Bad model bytes return -1 with last-error text.word, start, end,
probability, ttft_ms and decode_tps. Silence returns empty language/text.needle_load copies or takes ownership.The only loader adaptation is disabling original Node filesystem/process hooks
and adding an ESM default export. Verified WASM bytes are always passed through
wasmBinary. Original/adapted files and exact licenses are retained. The deployed
loader is 62,960 bytes. There is no runtime fetching/evaluation of JavaScript text.
| Environment/gate | Actual result |
|---|---|
| macOS arm64 Node 22.22.2 | Real speech, timestamps, silence, repeated calls, instance isolation, abort/recovery, disposal, local paths and typed failures passed |
| macOS arm64 Node 24.21.0 | Same four integration tests passed; a Node 24 test-runner flag inheritance issue was found and fixed |
| Linux arm64 Node 22.12.0, Debian container | Clean tarball install, installed CLI offline preparation, real speech/repeated calls and cancellation recovery passed with Docker --network none |
| Linux arm64 Node 24.21.0, Alpine container | Same packed-consumer test passed with Docker --network none |
| Chromium 156.0.8078.4 | Real model, self-hosted external-host blockade, CSP, cache reuse/corruption/denial, active abort recovery and helpers passed |
| Firefox 157.0 (Playwright) | Real model, self-hosting, CSP, cache reuse/denial and helpers passed |
| WebKit 27.2 (Playwright) | Real model, self-hosting, CSP, IndexedDB reuse/corruption/denial and helpers passed |
| Installed Chrome 155.0.8059.40 | Initial direct-runtime real-speech compatibility spike passed |
| Production Vite 8 consumer | Installed tarball under /demo/, self-hosted real speech/file helper/timestamps, bundled default worker/WASM with model mirror, no external hosts passed |
| Plain browser ESM consumer | Installed package modules and CLI directory initialize/transcribe silence without a bundler |
| Consumer TypeScript | All four installed public entry points compile with strict declarations |
| Contract/installer suite | 14 tests passed: validation, snapshots/offsets, queue/abort/stale events, allocation/output failures, manifest checks, import safety, checksums, interruption, retries, reuse and unrelated-file preservation |
| Connected acquisition | Actual CLI first download passed; actual default browser model download and speech passed, with requests restricted to the pinned model's redirect chain |
| Tarball | Required loader/WASM/workers/types/CLI/lock/notices present; no weights, fixture recordings, caches, postinstall or production dependencies |
Node API-denied subprocess tests terminate immediately if fetch, HTTP(S), TCP/TLS, DNS, UDP or WebSocket APIs are attempted; worker threads inherit that preload. The Linux containers additionally enforce an OS-level networking ban. Asset preparation happened before denial. A clean child process exits after disposal, which checks that no worker keeps Node alive.
The browser tests block external origins in self-hosted mode and assert zero such
requests. Persistence tests make model/WASM URLs unavailable after an initial load,
then create another instance. Corrupt model bytes in IndexedDB are replaced from
the configured source. Denied persistence is injected into a real worker; inference
still uses the official model. The default-URL contract test serves verified model
bytes through a route fixture; test:browser:download separately verifies an actual
connected download without that fixture.
Audio tests decode a generated 48 kHz stereo PCM16 WAV, average channels to about 0.5, and assert exactly 16,000 output samples for one second. Corrupt/31-second files and decoding aborts are checked. Capture tests use an injected MediaRecorder and stream with real Web Audio decoding: manual stop, 20 ms automatic stop (320 samples), cancellation, permission denial, and late permission cleanup. These do not test physical microphone hardware or all browser recorder codecs.
Host: Apple M2, 16 GiB RAM, macOS arm64. Fixture: 11-second English JFK excerpt, 176,000 samples, word timestamps enabled; all figures are individual local runs, not an SLA or an accuracy benchmark. Browser and Node processes may have shared host load. evidence.json records representative measurements.
| Measurement | Observed |
|---|---|
| Fresh CLI directory, actual pinned-model transfer + verification/copies | 1.16 s wall time on this connection/CDN state |
| Node 22 cold local initialization | approximately 69 ms in the first SDK run |
| Node 22 warm service time | approximately 527–681 ms across 10 requests in that run |
| Node 24 cold local initialization | 103 ms |
| Node 24 warm service time | 507–600 ms across 10 requests |
| Chromium self-hosted cold / persisted warm initialization | 115 / 51 ms |
| Chromium speech worker service | 538 ms; first-token 276 ms; decoder 129 tokens/s |
| Firefox self-hosted cold / persisted warm initialization | 196 / 129 ms |
| Firefox speech worker service | 663 ms |
| WebKit self-hosted cold / persisted warm initialization | 134 / 55 ms |
| WebKit speech worker service | 563 ms |
| Chromium actual upstream download + initialization | 954 ms; subsequent speech service 545 ms |
The ABI harness retained the model, allocated maximum-size PCM plus a 1 MiB output, and transcribed the fixture ten times. WASM memory stayed at 76,808,192 bytes for all ten calls. The original spike with a smaller PCM allocation plateaued at 70,647,808 bytes. Memory growth need not shrink. The SDK Node integration also samples process RSS after every call. After three warmup calls, it requires growth from any earlier measured minimum to remain below 64 MiB. Decreases from garbage collection are not counted as growth; later increases after a decrease still count. Failures include every RSS sample, the measured growth, budget, Node version and platform. This replaces an unordered maximum-minus-minimum check that could fail when memory was reclaimed. The separate ABI check still requires WASM memory to plateau. RSS includes main/worker V8 overhead and is not a precise allocation/leak profiler. Disposal destroys the worker's entire runtime.
The upstream native “11 ms” headline is not a browser SDK result and is not used as a performance promise here.
All criteria have passing evidence on the executed targets, with platform/codec coverage bounded by the limitations below.
| ID | Status and evidence |
|---|---|
| AC-01 | Pass — independent source, official locked runtime, only Whistle weights |
| AC-02 | Pass — actual speech on both adapters and three browser engines |
| AC-03 | Pass — public declarations, validation/result/error tests and real timestamps |
| AC-04 | Pass — network API denial plus Linux containers with networking disabled, including recovery |
| AC-05 | Pass — real CLI acquisition and both adapters consume its directory directly |
| AC-06 | Pass — checksum/interruption/retry/reuse/unrelated-file tests |
| AC-07 | Pass — default pinned source assertion and actual download; self-hosted external-host block |
| AC-08 | Pass — FIFO lifecycle tests, repeated model calls, separate real instances |
| AC-09 | Pass — abort/recovery/stale events/disposal tests and clean child exits |
| AC-10 | Pass — invocation-time snapshots, offsets, caller buffer tests |
| AC-11 | Pass — stereo 48 kHz to mono 16 kHz, corrupt/oversized decode rejection |
| AC-12 | Pass — deterministic manual/automatic/cancel and late-permission cleanup; no hardware claim |
| AC-13 | Pass — hash-keyed verified persistence, reuse, corruption and storage-denial fallback |
| AC-14 | Pass — clean tarball Node/Linux, production Vite /demo/, plain ESM and types |
| AC-15 | Pass — tarball and package scripts inspected; no weights/postinstall |
| AC-16 | Pass — exact upstream licenses, original/adapted provenance, tarball and output notices |
| AC-17 | Pass — README, API, deployment guide, examples and measured compatibility note |
| AC-18 | Pass — no continuous API, TTS, telemetry, hosted service or publication |
whistle command name for
collisions. @tiny-stt/whistle is already the decided package identity.The API/deployment guides, runnable Node example, and this compatibility evidence
live in packages/documentation. evidence.json is preserved byte-for-byte; its
fixture path is relative to packages/whistle. The SDK tarball retains its
quick-start README, documented declarations, asset lock, licenses and notices.
After this move, TypeDoc's strict check and generated-site browser test passed:
33 pages, 1,408 local links/assets, embedded Node code and the JSON evidence download.
The relocated Node example transcribed the licensed fixture. All 14 SDK contract
tests, workspace typechecks, Biome checks and clean consumers passed. The packed
SDK contains 60 files with no docs/ or examples/; Node 22/24 Linux containers
passed repeated inference and cancellation recovery with --network none using
the revised package mount. These structural checks do not replace the original
performance measurements above.
Original SDK and documentation code retain this repository's MIT license. Official runtime and model artifacts retain their exact Apache-2.0 licenses and notices in the package and CLI-generated distribution. See the SDK notice. The SDK is independently written and is not official Cactus software.
Generated documentation has no inference engine, model weights, recording controls, analytics or third-party font/CDN dependencies. Viewing it never requests microphone access or downloads speech assets. Repository links become available after the owner separately commits and pushes the local implementation.