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

@breeze.blue/sdk

v0.5.0

Published

ESM-first TypeScript SDK for the Breeze Blue Developer API.

Readme

@breeze.blue/sdk

ESM-first TypeScript SDK for the Breeze Blue Developer API. Covers text-to-speech, voice management, voice preview generation, history audio, models, and account usage.

The root entrypoint is safe to import in Node, browser, and edge runtimes. Local audio playback helpers live in the Node-only @breeze.blue/sdk/node subpath.

Install

pnpm add @breeze.blue/sdk

or:

npm install @breeze.blue/sdk

API Key

Create an API key in the Breeze Blue Developer Console, then export it:

export BREEZE_API_KEY=brz_...

The SDK sends the key with the xi-api-key header. It also sends x-breeze-sdk for server-side observability.

An API key is required for every REST method. The one exception is textToSpeech.realtime.connect(...) with a clientSecret, which lets a browser client run without a key (see the realtime section below).

Quickstart

import { BreezeBlueClient } from "@breeze.blue/sdk";
import { save } from "@breeze.blue/sdk/node";

const client = new BreezeBlueClient();

const audio = await client.textToSpeech.convert(
  "voc_...",
  { text: "Hello from Breeze Blue." },
  { outputFormat: "mp3" },
);

await save(audio, "hello.mp3");
console.log(audio.contentType);
console.log(audio.historyItemId);

new BreezeBlueClient() reads BREEZE_API_KEY by default and sends requests to https://api.breeze.blue. To point at another environment, pass baseUrl or set BREEZE_BASE_URL.

const client = new BreezeBlueClient({
  apiKey: "brz_...",
  baseUrl: "https://api.breeze.blue",
  timeout: 120_000,
});

Per-request options accept timeout, signal, and extra headers.

Text to Speech

const audio = await client.textToSpeech.convert("voc_...", {
  text: "Render this line.",
});

const audioStream = await client.textToSpeech.stream("voc_...", {
  text: "Stream this line.",
});

const enhanced = await client.textToSpeech.enhance({
  instruction: "Calm, warm, bedtime narration.",
  languageCode: "en",
});

Use async text-to-speech for long text, reference-heavy voices, or batch production where the caller should not hold an HTTP connection open:

const job = await client.textToSpeech.createJob(
  "voc_...",
  { text: "Render this longer script." },
  { outputFormat: "mp3" },
);

const status = await client.generationJobs.get(job.generationJobId);
if (status.status === "ready") {
  const audio = await client.generationJobs.downloadAudio(job.generationJobId);
  await save(audio, "async.mp3");
}

If the job is still active, downloadAudio(...) rejects with BreezeBlueGenerationNotReadyError; read error.retryAfter before retrying.

Use realtime text-to-speech when one WebSocket connection should handle multiple conversation turns. Realtime audio is fixed to raw pcm_s16le, 24000 Hz, mono, 16-bit frames.

Realtime needs a WebSocket implementation. Browsers, edge runtimes, and Node 22+ provide a global one; on Node 20, pass a constructor (for example the ws package's WebSocket) via new BreezeBlueClient({ webSocket }).

const connection = await client.textToSpeech.realtime.connect("voc_...", {
  modelId: "bluebell-v1",
});

connection.startTurn("turn_1");
connection.appendText("Hello from Breeze.");
connection.flush();
connection.endTurn();

const pcmChunks: Uint8Array[] = [];
for await (const message of connection) {
  if (message.type === "audio") {
    pcmChunks.push(message.audio);
    continue;
  }
  if (message.type === "error") {
    throw new Error(`Realtime TTS failed: ${message.code}: ${message.message}`);
  }
  if (message.type === "turn.done") {
    break; // all audio for this turn has been delivered
  }
}
connection.close();

Always consume the connection (or its audio() / events() iterators) while a turn is active, and call close() when you are done. When the SDK detects a connection failure — an invalid server frame, or more than 16 MiB of messages piling up unconsumed — it closes the connection and the iterator rejects with a BreezeBlueRealtimeError.

Events are a typed discriminated union (session.ready, turn.started, audio.started, turn.done, turn.cancelled, usage.committed, session.closed, error, pong), with camelCase fields such as turnId, historyItemId, and ttfaMs. Treat the union as non-exhaustive: the server may add event types, and the SDK delivers unknown JSON events unchanged — ignore event types you do not recognize instead of switching exhaustively.

For connections that sit idle between turns, send a keepalive ping inside the session's inactivityTimeoutSeconds window; the server answers with a pong event:

const keepalive = setInterval(() => connection.ping(), 30_000);
// ... run turns ...
clearInterval(keepalive);
connection.close();

To cut time to first audio, create the session ahead of time (for example while your app is still preparing the turn) and connect with its clientSecret when the first text is ready — only the WebSocket handshake remains:

const session = await client.textToSpeech.realtime.createSession("voc_...", {
  modelId: "bluebell-v1",
});

// Later, when the first text is ready:
const connection = await client.textToSpeech.realtime.connect("voc_...", {
  clientSecret: session.clientSecret,
});

The same clientSecret handoff lets a browser connect without ever seeing your API key: create the session on your server, hand session.clientSecret to the page, and build a key-less client there:

// Browser — no API key required for clientSecret connections.
const browserClient = new BreezeBlueClient();
const connection = await browserClient.textToSpeech.realtime.connect("voc_...", {
  clientSecret, // received from your server
});

Session parameters (modelId, languageCode, instructions, voiceSettings, inactivityTimeoutSeconds, enableLogging) are fixed when the session is created. connect ignores them when clientSecret or websocketUrl is provided and logs a warning.

If a realtime WebSocket is interrupted by a network change, service deployment, or upstream realtime worker restart, handle error.meta.reconnect === true or a session.closed event with reconnect === true by creating a new connection and starting a new turn from your own conversation state. Active turns are not resumed in place.

The API uses the default text-to-speech model when modelId is omitted. If you need to select a model explicitly, call client.models.list() and pass one of the returned modelId values.

All field names follow TypeScript conventions (modelId, voiceSettings, outputFormat, historyItemId, ...). The SDK translates them to the snake_case HTTP wire format on send and translates JSON responses back to camelCase on receive.

Audio responses expose convenient helpers:

await audio.arrayBuffer();
await audio.bytes();
await audio.blob();

audio.contentType;
audio.historyItemId;

Node playback helpers:

import { play, save, stream } from "@breeze.blue/sdk/node";

await play(audio);        // ffplay, with macOS afplay fallback
await save(audio, "x.mp3");
await stream(audioStream); // mpv

Streaming text-to-speech defaults to pcm to reduce time to first audio. Pass { outputFormat: "wav" } or { outputFormat: "mp3" } when you need that wire format explicitly.

Voices

Browse existing voices and inspect a single voice:

const voices = await client.voices.search({ search: "narrator" });
const firstVoiceId = voices.voices[0].voiceId;

const voice = await client.voices.get(firstVoiceId);
const settings = await client.voices.getSettings(firstVoiceId);

const randomVoice = await client.voices.random();
console.log(randomVoice.voiceId, randomVoice.name);

Breeze voice creation is always two steps: produce a preview, let the user accept it, then save the preview as a real voice. Two ways to produce a preview:

import { readFile } from "node:fs/promises";

// Option A — clone preview from an audio sample
const clonePreview = await client.voices.createClonePreview({
  name: "Demo voice",
  file: {
    data: await readFile("sample.wav"),
    filename: "sample.wav",
    contentType: "audio/wav",
  },
  text: "This is a short preview script.",
});
let generatedVoiceId = clonePreview.generatedVoiceId;

// Option B — design preview from a text description (no audio)
const design = await client.voices.createDesignPreview({
  voiceDescription: "Warm documentary narrator with clear articulation.",
});
generatedVoiceId = design.previews[0].generatedVoiceId;

files is also accepted with exactly one item.

Stream the preview so the user can audition it, then save the one they pick:

const audio = await client.voices.streamPreview(generatedVoiceId);
await save(audio, "preview.mp3");

const saved = await client.voices.savePreview({
  generatedVoiceId,
  voiceName: "Documentary narrator",
});

Edit, tune settings, or delete a saved voice:

await client.voices.edit(saved.voiceId, { name: "Renamed narrator" });
await client.voices.editSettings(saved.voiceId, { guidanceScale: 1.2 });
await client.voices.delete(saved.voiceId);

History, Models, and Account

const models = await client.models.list();
const balance = await client.account.balance();
const usage = await client.account.usage({ days: 7 });
const keyUsage = await client.account.usage({
  apiKeyId: "key_01hprod",
  clientType: "sdk",
});

const history = await client.history.list({ pageSize: 10 });
const item = await client.history.get(history.history[0].historyItemId);
const audio = await client.history.downloadAudio(item.historyItemId);

Errors

API failures throw BreezeBlueAPIError subclasses:

  • BreezeBlueAuthenticationError
  • BreezeBlueBadRequestError
  • BreezeBlueConflictError
  • BreezeBlueForbiddenError
  • BreezeBlueNotFoundError
  • BreezeBlueValidationError
  • BreezeBlueRateLimitError
  • BreezeBlueInsufficientCreditsError
  • BreezeBlueUpstreamError for upstream generation/storage/model failures, commonly 502/504
  • BreezeBlueServiceUnavailableError for capacity or service-configuration failures, commonly 503
import { BreezeBlueClient, BreezeBlueRateLimitError } from "@breeze.blue/sdk";

try {
  await new BreezeBlueClient().models.list();
} catch (error) {
  if (error instanceof BreezeBlueRateLimitError) {
    console.log(error.headers.get("retry-after"));
  }
}

Each API error exposes status, code, detail, meta, and headers.