@breeze.blue/sdk
v0.5.0
Published
ESM-first TypeScript SDK for the Breeze Blue Developer API.
Maintainers
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/sdkor:
npm install @breeze.blue/sdkAPI 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); // mpvStreaming 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:
BreezeBlueAuthenticationErrorBreezeBlueBadRequestErrorBreezeBlueConflictErrorBreezeBlueForbiddenErrorBreezeBlueNotFoundErrorBreezeBlueValidationErrorBreezeBlueRateLimitErrorBreezeBlueInsufficientCreditsErrorBreezeBlueUpstreamErrorfor upstream generation/storage/model failures, commonly 502/504BreezeBlueServiceUnavailableErrorfor 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.
