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

transcribe-cpp

v0.1.3

Published

TypeScript/Node.js bindings for transcribe.cpp — a C/C++ speech-to-text library built on ggml

Readme

transcribe-cpp

TypeScript/Node.js bindings for transcribe.cpp, a C/C++ speech-to-text library built on ggml — Whisper, Parakeet, Moonshine, Voxtral, and more, on CPU, Metal, CUDA, and Vulkan.

npm install transcribe-cpp

A matching prebuilt native package is selected automatically for your platform (@transcribe-cpp/<platform>); there is nothing to compile and no environment variables to set.

Quickstart

import { TranscribeModel } from "transcribe-cpp";

const model = await TranscribeModel.load("whisper-tiny-Q5_K_M.gguf");

// 16 kHz mono float32 PCM (bring your own decoder).
const result = await model.transcribe(pcm, { timestamps: "segment" });

console.log(result.text);
console.log(result.language); // detected or requested
for (const seg of result.segments) {
  console.log(`[${seg.t0Ms}–${seg.t1Ms}ms] ${seg.text}`);
}

model.dispose();

Streaming

const session = model.createSession();
const stream = await session.stream({ commitPolicy: "stable_prefix" });

for (const chunk of pcmChunks) {
  await stream.feed(chunk);
  const { committed, tentative } = stream.text; // committed is append-only/stable
  render(committed, tentative);
}
await stream.finalize();
stream.reset();

Batch

const items = await session.runBatch([pcmA, pcmB]);
for (const item of items) {
  if (item.ok) console.log(item.result.text);
  else console.error(item.error.message); // per-utterance failure
}

Cancellation

const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);
try {
  await model.transcribe(pcm, { signal: ac.signal });
} catch (e) {
  if (e instanceof Aborted) console.log("partial:", e.partialResult?.text);
}

Family extensions

Per-family options are a typed, discriminated union passed as family:

// Whisper is a run-slot extension:
await model.transcribe(pcm, { family: { kind: "whisper", initialPrompt: "…" } });

// Streaming families (moonshine, parakeet, voxtral) are stream-slot:
const stream = await session.stream({ family: { kind: "moonshine" } });

model.accepts({ kind: "whisper" }); // does this model take that extension?

Resource management

TranscribeModel, Session, and Stream all implement Symbol.dispose, so using works (TypeScript 5.2+ / Node 22+):

using model = await TranscribeModel.load("model.gguf");
using session = model.createSession();
// disposed automatically at block exit

Disposing a model disposes its sessions; disposing a stream resets it (releasing the model lease). Disposal is idempotent and order-independent.

Backend selection

import { getAvailableBackends, backendAvailable } from "transcribe-cpp";

getAvailableBackends(); // [{ kind: "metal", name: "MTL0", description: "…" }, …]
backendAvailable("cuda"); // boolean — never throws

const model = await TranscribeModel.load("model.gguf", { backend: "metal" });

backend defaults to "auto" (best accelerator, else CPU). A missing Vulkan loader/driver degrades silently to CPU; an explicit backend that cannot be satisfied raises a BackendError.

Thread-safety

Your JavaScript runs on one thread, but run/feed/finalize dispatch the native compute to a libuv worker thread (via koffi's async calls), so the event loop stays responsive while inference runs.

The C library allows one compute in flight per model — a run, a runBatch, or an active stream — across all of its sessions. The binding enforces this: every compute call serializes through an internal model-wide mutex, and an active stream holds a model-wide lease for its whole lifetime. While a stream is active (after stream(), before finalize()/reset()), a run/runBatch/stream on any session of that model is refused with a Busy error rather than allowed to race. So to parallelize, load one model per worker; to share a model, finalize or reset the stream first. A single Session is single-use-at-a-time — don't call run/feed on the same session concurrently.

Hand-offs are ordered, not racy: finalize()/reset()/dispose() release the lease only after the native teardown runs on the shared queue, so the slot is never freed early. Issue the teardown before the next stream()/run() and it is correctly serialized — stream.reset(); const next = await session.stream() works without awaiting the (void) reset(). The reverse order — beginning before the teardown — is refused with Busy, by design.

Because the compute is genuinely on another thread, do not touch a session while a call against it is in flight — it is single-threaded in the C library:

  • Reading a stream's text/state/revision/lastStatus, or a session's limits/wasAborted, during an un-awaited feed/finalize/run throws.
  • reset() and dispose() are safe to call any time: the native teardown is deferred behind any in-flight call, so it never frees a session mid-compute.
  • Disposing a Session or TranscribeModel while a stream is still active releases the lease and invalidates the stream — its later calls throw rather than touch the freed handle.
  • The input PCM is borrowed, not copied: run/runBatch/feed hand the buffer to native code that reads it on the worker thread, so do not mutate it (e.g. reuse a scratch/capture buffer) until the returned promise resolves. Pass a fresh buffer per call, or await before overwriting.

The normal pattern is safe — await first, then read:

await stream.feed(chunk);
const { committed, tentative } = stream.text; // ✓ feed has completed

const p = stream.feed(chunk);
stream.text;                                  // ✗ throws: read while feed in flight
await p;

Since JavaScript has no destructors, a stream holds the model's lease until you finalize(), reset(), or let using dispose it — drop the reference without one and the model stays Busy until you dispose the session or model. Always finalize/reset (or use using).

Result strings are copied at the boundary; the objects you get back are fully owned and outlive the model.

Runtime support

The binding is one native artifact (koffi, an N-API addon) plus plain ESM, so any runtime with N-API support runs the same package — there is no per-runtime build.

  • Node ≥ 22 — the primary, fully tested target.
  • Deno ≥ 2 — runs the package unmodified via deno run (use --allow-ffi --allow-read --allow-env, or -A). Note: deno compile does not bundle native addons, so the compiled-binary path is unsupported.
  • Bun — not yet. Bun loads the addon and runs, but currently crashes in its own N-API finalizer (napi_reference_unref); this is an upstream Bun bug, fixed on Bun's side, not here.

Building from source

The universal fallback when no prebuilt package exists for your platform (a new arch, a custom backend, or a single self-contained library):

# from a transcribe.cpp checkout:
npm run build:native -- --source /path/to/transcribe.cpp
# add --self-contained to link ggml into one libtranscribe

# from a consumer project (the script ships in the package):
node node_modules/transcribe-cpp/scripts/build-from-source.mjs --source /path/to/transcribe.cpp

This drives CMake directly (the binding is FFI over a shared library, not an N-API addon, so cmake-js does not apply) and installs into prebuilds/<tuple>/, which the loader finds automatically. Requires cmake and a C/C++ toolchain. You can also point TRANSCRIBE_LIBRARY at any libtranscribe you built.

Packaging an app (Electron / Tauri)

The native code ships as a self-contained @transcribe-cpp/<platform> package (the shared library plus its sibling ggml libs / backend modules, in one directory). Because npm resolution is transitive, your app can locate that directory at build time — there is nothing special to wire through. artifactDir() returns it without loading the library (no dlopen), which is exactly what a bundler/pack step needs:

import { artifactDir } from "transcribe-cpp";
import * as fs from "node:fs";

const dir = artifactDir();
const lib =
  process.platform === "win32" ? "transcribe.dll"
  : process.platform === "darwin" ? "libtranscribe.dylib"
  : "libtranscribe.so";

// HARD-FAIL if the artifact is missing/empty: better to break the build than
// ship a silently broken installer that crashes at first transcribe() call.
if (!fs.existsSync(`${dir}/${lib}`)) {
  throw new Error(`native library not found in ${dir}; the platform package is not bundled`);
}
// copy `dir` into your app resources, or feed it to your bundler config…

The native files are real binaries, so they must not be packed into an asar archive. With electron-builder, unpack the platform packages:

// package.json → "build"
{ "asarUnpack": ["node_modules/@transcribe-cpp/**"] }

For a Tauri app whose native bits come from the Rust crate instead, see that crate's "Packaging a distributable" section — there the artifact dir is exposed to your build.rs as DEP_TRANSCRIBE_CPP_RUNTIME_DIR.

Waived requirements

  • Per-field ABI layout is not waived (unlike Rust/Swift): TypeScript has no compiler reading the C headers, so — like the Python ctypes binding — the generated struct layout is verified at load against both the captured C-compiler layout and the live library (transcribe_abi_struct_size/_align).
  • Buffer model load awaits transcribe_model_load_buffer in the C API; only file load is exposed today.

License

MIT. Bundled native packages include third-party license texts (ggml, miniz, and any bundled backend runtimes).