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

@voicelabs/sdk

v0.1.0

Published

Official TypeScript SDK for the VoiceLabs API — text-to-speech and transcription over HTTP.

Readme

@voicelabs/sdk

The official TypeScript client for the VoiceLabs API — generate speech in your own cloned voices, transcribe audio, and read your voices, captures, and generations over HTTP.

Hand-written, not generated. Zero runtime dependencies. ESM and CommonJS, types included.

npm install @voicelabs/sdk

Quickstart

You need an API key. Sign in at app.voicelabs.now/connections, open the API keys tab, and create one. Tick voice:generate if the key needs to make audio — a key without it can read your account but cannot generate.

The secret is shown once, at creation. VoiceLabs stores a SHA-256 hash, so a lost key is replaced, not recovered.

import { VoiceLabs } from "@voicelabs/sdk";

const voicelabs = new VoiceLabs({ apiKey: process.env.VOICELABS_API_KEY! });

// Generate speech and wait for the audio (generation is asynchronous).
const generation = await voicelabs.generateSpeech({
  text: "The quick brown fox jumps over the lazy dog.",
  voice_name: "Narrator",
});

const { bytes, contentType } = await voicelabs.downloadAudio(generation);
await writeFile(`out.${contentType.split("/")[1]}`, bytes);

Field names are the wire names, snake_case and all. There is no camelCase translation layer, on purpose: a mapping is a second declaration of the contract that can drift from the first, and when it drifts you get a silently-undefined field rather than a type error. What you read in the OpenAPI document, in a curl response, and in these types is one set of names.

The six operations

| Method | Scope | What it does | | ------------------------------------ | ---------------- | -------------------------------------------------------------------- | | listVoices() | voice:read | Voice profiles on the account: cloned voices and presets | | listCaptures({ limit, offset }) | voice:read | Recent captures with their transcripts, most-recent first | | getGeneration(id) | voice:read | Poll one generation for its status and, once done, its audio URL | | createSpeech({ text, voice_name }) | voice:generate | Start generating speech; returns a handle immediately | | createTranscription({ audio }) | voice:generate | Transcribe a base64 audio clip; returns the transcript synchronously | | downloadAudio(generation) | — | Fetch the bytes a completed generation points at |

Plus two helpers over them: generateSpeech() (create + wait, in one call) and waitForGeneration(id).

Reading never draws on the account's audio allowance. Generating does.

Errors are typed, and two of them share a status on purpose

This is the part worth reading before you write a retry loop. The API returns two different problems under one HTTP status, twice:

| Status | Code | What it means | Retrying? | | ------ | --------------------- | ----------------------------------------- | --------------- | | 429 | rate_limit_exceeded | You are calling too fast | Yes — wait | | 429 | quota_exhausted | The account's audio allowance is spent | No — futile | | 403 | insufficient_scope | This key was not granted the scope | No — mint a key | | 403 | feature_not_enabled | The plan does not include this capability | No — upgrade |

A client that branches on response.status gets both pairs wrong, and the second mistake is expensive: retrying quota_exhausted on a backoff loop burns your rate budget forever without ever succeeding. So each meaning has its own class, and QuotaExhaustedError is deliberately not a subclass of RateLimitError.

import {
  InsufficientScopeError,
  QuotaExhaustedError,
  RateLimitError,
  VoiceLabsAPIError,
} from "@voicelabs/sdk";

try {
  await voicelabs.createSpeech({ text: "Hello." });
} catch (error) {
  if (error instanceof RateLimitError) {
    await sleep((error.retryAfterSeconds ?? 30) * 1000); // will succeed later
  } else if (error instanceof QuotaExhaustedError) {
    console.log(`Allowance spent. Upgrade at ${error.settingsUrl}`);
  } else if (error instanceof InsufficientScopeError) {
    console.log(`This key needs the "${error.requiredScope}" scope.`);
  } else if (error instanceof VoiceLabsAPIError) {
    console.log(error.code, error.detail, "retryable:", error.retryable);
  }
}

Every API error carries code (the stable token to branch on), status, detail, and retryable — which disagrees with the status where the status is misleading. A code this SDK version predates is kept verbatim on error.code and handled through VoiceLabsAPIError, never forced into a class it might not belong to.

VoiceLabsConnectionError is thrown when no HTTP response happened at all — a dropped connection, a DNS failure, a timeout. That is the case where a write may or may not have executed, which is what idempotency keys are for.

Retrying a write safely

await voicelabs.createSpeech({ text: "Charge me exactly once." }, { idempotencyKey: "order-9271" });

Replaying the same key with the same body returns the original result instead of generating again. Replaying it with a different body is an error, not a silent overwrite.

Rate limits

Responses to an API-key request carry IETF draft-11 RateLimit headers — 600 requests/hour per key by default. The SDK parses them for you:

const voicelabs = new VoiceLabs({
  apiKey: process.env.VOICELABS_API_KEY!,
  onRateLimit: ({ remaining, resetSeconds }) => {
    if (remaining !== null && remaining < 20) console.warn(`${remaining} left, ${resetSeconds}s`);
  },
});

The hook is not called when a response carries no budget — the OAuth 2.1 lane and keys with limiting switched off genuinely have no per-key counter. Absent means unknown, never zero.

Audio URLs

generation.audio_url is a signed, time-limited capability. The signature is the whole authorization: it carries no identity, so downloadAudio() fetches it without your API key — sending a long-lived credential to a URL that does not need one would leak it for nothing. Re-polling mints a fresh URL rather than reviving an expired one, so follow it promptly.

Configuration

new VoiceLabs({
  apiKey: "vl_sandbox_…",
  baseUrl: "https://app.voicelabs.now", // override for a staging host
  timeoutMs: 60_000, // per request; 0 disables (bring your own signal)
  onRateLimit: (info) => {},
  fetch: myFetch, // substitute the HTTP layer
});

Every method takes { signal, headers }; writes also take { idempotencyKey }. A caller-supplied header can never displace the API key.

Runtimes

Node 18+, Bun, Deno, Cloudflare Workers, and browsers. Nothing is imported from node:*, so the SDK runs unmodified at the edge. In a browser, only ever use a key you are willing to make public — the API serves a permissive CORS policy because it is credential-authenticated, which does not make a secret in client-side JavaScript any less exposed.

Versioning

Semantic versioning, tracking the API's /v1 contract, which is versioned separately and independently of this package. Additive API changes arrive in a minor release; a breaking change to this SDK's own surface is a major. Pre-1.0, minor releases may still change the SDK surface — pin an exact version if that matters to you.

Development

npm install
npm test        # unit tests over a stubbed transport
npm run build   # dual ESM + CJS build via two tsc passes, no bundler

The SDK is also dogfooded: platform/tests/integration/v1/the-published-sdk-drives-the-real-v1-handlers-end-to-end.test.ts runs this exact source against the real API route handlers in-process, with a real database and really-issued keys. A drift between these types and the API fails CI rather than your build.

License

MIT © Devino Solutions