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

zelispeech

v0.3.1

Published

ZeliSpeech JavaScript SDK — self-hosted, low-latency streaming text-to-speech.

Readme

ZeliSpeech — JavaScript SDK

A small, zero-dependency client for ZeliSpeech — self-hosted, low-latency streaming text-to-speech. Point it at your running server and get audio back in a couple of lines. It is the JavaScript twin of the zeli-tts Python SDK, and talks the same authenticated /v1 API documented in the API reference.

Works in Node ≥ 18 and in the browser (plain fetch — no WebSocket needed).

Install

npm install zelispeech

Quickstart

import { ZeliSpeech, play, save, stream } from "zelispeech";

const client = new ZeliSpeech({
  baseUrl: "https://voice.zeligate.com",
  apiKey: "sk-zeli-...",                    // optional if the box runs open
});

// 1) Stream — first audio after the first sentence, not the whole passage
const audio = client.textToSpeech.stream("zeli-voice-1", "Hey there, this is Zeli.");
for await (const chunk of audio) {
  // mp3 bytes (Uint8Array) as they generate
}

// 2) Stream + play live (Node-only, needs ffplay)
await stream(client.textToSpeech.stream("zeli-voice-1", "Playing as I generate."));

// 3) One-shot — a finished clip
const clip = await client.textToSpeech.convert("zeli-voice-1", "Hello world.");
await save(clip, "hello.mp3");

Client

new ZeliSpeech({ baseUrl, apiKey, timeoutMs = 60000 })
  • baseUrlhttp://host:8000 or https://voice.zeligate.com; do not include a trailing /v1.
  • apiKey — sent as Authorization: Bearer … on every request. Required only when the box sets ZELI_API_KEY; a loopback/embedded box can run open.
  • client.capabilities() / client.health() / client.isReady() — engine info and readiness (handy while a box is warming up).

Synthesis — client.textToSpeech

client.textToSpeech.stream(voiceId, text, {
  modelId = "zeli-turbo", voiceSettings, outputFormat = "mp3_44100_128", timeoutMs,
}) // -> AudioStream

await client.textToSpeech.convert(voiceId, text, { ...same }) // -> Uint8Array
  • stream(...) returns an AudioStreamfor await it for encoded Uint8Array chunks; await audio.read() drains it to one blob. It exposes .outputFormat, .sampleRate, .voice (the resolved voice id), and .requestId.
  • convert(...) returns the finished clip in the requested outputFormat.
  • outputFormat accepts the full menu — mp3_*, wav_*, raw pcm_*, ulaw_8000, alaw_8000, opus_48000_* — see the output formats reference.
  • voiceSettings (stability/style/speed/…) maps onto Turbo delivery — see voice settings.

Voices — client.voices

await client.voices.list();                            // -> Voice[]
await client.voices.get("zeli-voice-1");               // -> Voice (404s on unknown ids)
await client.voices.add({ name, file, description });  // zero-shot clone (Turbo engine)
await client.voices.delete("custom-…");

file may be a path (Node), a Blob/File, a Uint8Array, or an ArrayBuffer. ~10–20 s of clean single-speaker audio works best.

Voice management requires a full API key — ephemeral tokens get 401 insufficient_scope here.

Ephemeral tokens — client.tokens

Never ship a real API key to a browser: anyone can read it from the page. The intended flow is that your backend (which holds a full apiKey) mints a short-lived, synthesis-only token and hands it to the page; the browser then talks to the API directly with that token.

await client.tokens.createEphemeral({ ttlSeconds: 300 })
// -> { token: "zsk_temp_…", expires_at: 1770000000, scope: "tts" }
  • ttlSeconds is clamped to 30–600 (default 300). expires_at is the epoch second the token dies — enforced server-side on every request, so a leaked token is usable for minutes at most.
  • The returned token has scope "tts": it can call synthesis and read endpoints (textToSpeech.stream/convert, voices.list/get) but not voices.add/delete or tokens.createEphemeral — those return 401 insufficient_scope.
  • Minting itself requires a full key; it fails with 401 insufficient_scope when attempted with a temp token, and 501 not_supported on a server without the portal keys table configured.

End-to-end example (Express backend + browser):

// backend — the real key stays here
import { ZeliSpeech } from "zelispeech";
const zeli = new ZeliSpeech({
  baseUrl: "https://voice.zeligate.com",
  apiKey: process.env.ZELISPEECH_API_KEY,
});
app.post("/api/tts-token", async (req, res) => {
  res.json(await zeli.tokens.createEphemeral({ ttlSeconds: 300 }));
});

// browser — only ever sees the temp token
const { token } = await (await fetch("/api/tts-token", { method: "POST" })).json();
const speech = new ZeliSpeech({ baseUrl: "https://voice.zeligate.com", apiKey: token });
const clip = await speech.textToSpeech.convert("zeli-voice-1", "Hello from the browser!");
new Audio(URL.createObjectURL(new Blob([clip], { type: "audio/mpeg" }))).play();

More examples

// Delivery control (stability / style / speed map onto Turbo knobs)
const expressive = await client.textToSpeech.convert(
  "zeli-voice-1", "Same words, very different delivery!",
  { voiceSettings: { stability: 0.2, style: 0.9, speed: 1.1 } },
);

// Raw PCM for your own audio pipeline (24 kHz mono int16, no container)
const pcm = await client.textToSpeech.convert("zeli-voice-1", "Raw samples.", {
  outputFormat: "pcm_24000",
});

// Stream straight to a file as it generates (Node)
import { createWriteStream } from "node:fs";
const out = createWriteStream("out.mp3");
for await (const chunk of client.textToSpeech.stream("zeli-voice-1", "Written as it speaks.")) {
  out.write(chunk);
}
out.end();

// Clone a voice, use it, remove it
const voice = await client.voices.add({ name: "My narrator", file: "reference.wav" });
await client.textToSpeech.convert(voice.id, "Cloned voice speaking.");
await client.voices.delete(voice.id);

// Robust error handling
import { APIError, ConnectionError } from "zelispeech";
try {
  await client.textToSpeech.convert("zeli-voice-1", "…");
} catch (err) {
  if (err instanceof APIError && err.status === "invalid_api_key") {
    // rotate/refresh the key
  } else if (err instanceof APIError && err.statusCode === 503) {
    // model still loading — retry shortly
  } else if (err instanceof ConnectionError) {
    // box unreachable / stopped
  }
}

Audio helpers

await save(bytes, path);       // write to disk (Node-only; pcm_* wrapped when .wav)
await play(clip);              // system player: afplay / ffplay / aplay (Node-only)
await stream(audioStream);     // play chunks live via ffplay; returns the bytes (Node-only)
pcmToWav(pcm, sampleRate);     // wrap raw PCM in a WAV container (works everywhere)

In the browser, build a blob and play it instead:

const clip = await client.textToSpeech.convert("zeli-voice-1", "Hi!");
new Audio(URL.createObjectURL(new Blob([clip], { type: "audio/mpeg" }))).play();

Errors

Everything the SDK throws derives from ZeliSpeechError:

| Exception | When | |---|---| | ConfigurationError | bad baseUrl, or a missing player | | ConnectionError | server unreachable (refused / DNS / timeout) | | APIError | non-2xx from an HTTP endpoint (.statusCode, .status, .body) | | GenerationError | the stream broke mid-synthesis |

Auth failures are APIErrors whose .status tells you why:

| .statusCode | .status | When | |---|---|---| | 401 | missing_api_key | auth required but no key sent | | 401 | invalid_api_key | unknown, revoked, or expired key/token | | 401 | insufficient_scope | an ephemeral token hit a management route | | 501 | not_supported | minting on a server without the keys table |

Migrating from 0.1.x

0.2.0 moves the SDK onto the authenticated /v1 API: stream(text, { voice })stream(voiceId, text), audio now arrives in outputFormat (mp3 by default) instead of raw PCM, engine-knob options are replaced by voiceSettings, and streaming no longer uses WebSocket (plain fetch everywhere).

License

MIT