npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@desert-ant-labs/voz

v3.5.0

Published

On-device speech recognition for JavaScript: transcripts with word-level timestamps, 25 languages. Runs in the browser on WebAssembly + ONNX Runtime Web (WebGPU, with the decode step on WebNN where the browser has it), and server-side in Node from one imp

Readme

@desert-ant-labs/voz

On-device speech recognition for JavaScript that runs in the browser and in Node. Give Voz audio and it returns a transcript with a start and an end on every word, across 25 languages. Audio stays local: nothing is uploaded, and there is no API key.

One entry, @desert-ant-labs/voz, and one Voz API. Unlike this project's other models there is no /native subpath: Voz has no native Node core, so the browser and the server run the same WebAssembly pipeline - the same windowing, lane-batched decode and splice as the Swift SDK - over ONNX Runtime.

# Browser
npm i @desert-ant-labs/voz onnxruntime-web

# Node
npm i @desert-ant-labs/voz onnxruntime-node

In the browser you install the runtime and nothing else. It is imported on demand, the way @litertjs/core is for this project's other models, and bundlers split it into its own chunk, so a page that never transcribes never downloads it.

import { Voz } from "@desert-ant-labs/voz";

const voz = await Voz.load();
const result = await voz.transcribe(file);   // a File, Blob, ArrayBuffer, or samples

result.text;            // "the transcript, as one string"
result.words[0];        // { text: "the", start: 0.08, end: 0.24 }
result.realtimeFactor;  // seconds of audio per second of wall clock

The file is read in pieces as the model works through it, so a five-minute recording and a five-hour one cost the same memory. See Memory for what that means in numbers.

Under Node you pass the runtime, because there it is onnxruntime-node, a native addon. A package that imports one of those cannot be bundled for a server, so this one does not.

import { Voz } from "@desert-ant-labs/voz";
import * as ort from "onnxruntime-node";

const voz = await Voz.load({ ort });

Passing ort works in the browser too, for an app that already bundles a runtime or wants a build other than onnxruntime-web/webgpu.

The bundle is downloaded from the Hugging Face Hub on first use and cached (the Cache API in a browser, disk in Node), so a reload does not fetch it again. Nothing model-sized ships in the npm tarball.

Word timestamps

Every word carries start and end in seconds from the beginning of the audio, which is what subtitles, transcript highlighting and clip-finding need:

const { words } = await voz.transcribe(file);

for (const { text, start, end } of words) {
  console.log(`${start.toFixed(2)} -> ${end.toFixed(2)}  ${text}`);
}

Times come from the frame that emitted each word rather than from a second alignment pass, so they cost nothing extra. Their resolution is one encoder frame, 80 ms.

What you can pass to transcribe

| Input | What happens | | --- | --- | | File, Blob | Streamed: read in pieces, any container WebCodecs can decode | | a path (Node 20+) | Streamed, the same way | | ArrayBuffer, Uint8Array | Decoded whole, since the bytes are already in memory | | Float32Array | Taken as mono 16 kHz samples | | { samples, sampleRate } | Resampled to 16 kHz |

A browser can hand over a <input type="file"> file directly:

input.addEventListener("change", async () => {
  const result = await voz.transcribe(input.files[0], {
    onProgress: (fraction) => console.log(`${Math.round(fraction * 100)}%`),
  });
  console.log(result.text);
});

Under Node, a path streams the same way a File does, which needs Node 20 or newer (fs.openAsBlob). WebCodecs is what decodes compressed audio, so on a Node build without it, convert to WAV first - ffmpeg -i in.mp4 -ac 1 -ar 16000 out.wav - which streams everywhere.

If a decoder ever returns less audio than the container says it holds, that is reported rather than transcribed: decodeAudioData can return a short buffer with no error, and a transcript that quietly stops a minute in is worse than a failure. In a video file, where the audio track is allowed to be shorter than the picture, it is a warning instead.

Memory

Constant in the length of the recording, which is the point of the streaming path above. Measured in Chromium on an M5, peak resident for the whole browser:

| audio | peak | | --- | --- | | 1 minute | 2.4 GB | | 29 minutes | 2.7 GB | | 29 minutes, before streaming | 7.7 GB |

About 1.2 GB of that floor is the model: 349 MB of weights, and roughly 900 MB that ONNX Runtime keeps for the compiled session (measured, and not something this package can configure away). The rest is the browser itself. What matters is the shape: the audio no longer contributes, so length is free.

Where it runs

In the browser the encoder runs on WebGPU and, where the browser exposes WebNN (Chromium today), the decode step runs on the Neural Engine. Both are detected; Voz.load({ webnn: false }) forces WebGPU alone.

Speed, ten minutes of audio: about 125x real time in Chromium on an M5, 38x on an M1, 255x on an M3 Ultra, and about 35x in Safari. Word error rate is level with the Core ML build the Apple SDK uses (2.83% against 2.66% over 300 LibriSpeech utterances, a difference a paired bootstrap cannot distinguish).

Under Node, onnxruntime-node runs on the CPU. That is the right choice for transcribing a file on a server, and it is slower per second of audio than the browser path on the same machine, because a browser reaches the GPU and onnxruntime-node's default execution provider does not.

The page needs no SharedArrayBuffer and no cross-origin isolation: the runtime is configured for a single wasm thread, which measured identical on Chromium and avoids a WebKit JIT pathology that costs 15% in Safari.

Loading the model

Voz.load() takes:

| Option | Default | | | --- | --- | --- | | ort | onnxruntime-web in the browser | an ONNX Runtime module of your own; required under Node | | ep | "auto" | "webgpu" or "wasm" to override where the graphs run | | modelBaseUrl | the Hub | serve the bundle yourself; must end in / | | revision | the SDK's pin | a different revision of the weights | | webnn | detected | run the decode step on WebNN | | wasmDir | the runtime's own | where onnxruntime-web's .wasm files are served from | | onProgress | none | download progress, as a fraction | | cache | true | cache the downloaded bundle |

To serve the weights yourself, copy the web/ directory of the weights repo next to your app and point modelBaseUrl at it. meta.json names the rest, so there is no file list to keep in sync here.

One instance holds three compiled graphs and about 1.2 GB of resident weights, so load it once and reuse it. A transcription holds the instance for its whole run, and concurrent calls queue rather than interleave.

Requirements

  • A browser with WebGPU. Tested on Chromium 135+ and Safari 26+. A browser whose adapter cannot compile the encoder's f16 shaders - which is what a machine with no GPU exposes - runs on the CPU instead, correctly and much more slowly.
  • Node 20+ for streaming from a path, with onnxruntime-node installed.

License

The SDK is source-available under the license in this package. The model weights carry their own license on the Hub.