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

@pyai/sdk

v0.7.1

Published

Official TypeScript/JavaScript SDK for PyAI, speech-to-text (Hear), text-to-speech (Speak), realtime voice agents (Omni), and call compliance (Trace).

Readme

@pyai/sdk

Official TypeScript/JavaScript SDK for PyAI, the all-in-one voice AI platform: lightning-fast speech-to-text, ultra-realistic text-to-speech, end-to-end realtime voice agents, and automatic call compliance. Zero dependencies; runs in the browser and Node 18+.

PyAI products

  • Hear, Lightning-fast, telephony-native speech-to-text. Whisper-compatible transcription tuned for real phone-call audio, with live streaming partials so your app reacts mid-sentence, plus async batch transcription for big archives. POST /v1/audio/transcriptions
  • Speak, Ultra-realistic text-to-speech that starts speaking in tens of milliseconds. Stream lifelike, expressive voices, choose from 144 stock voices, or clone any voice instantly, for free. POST /v1/audio/speech
  • Omni (flagship), One API for a complete, end-to-end voice AI agent. A single WebSocket where your agent listens, thinks, and speaks, grounded in your knowledge bases and tools, with human-like turn-taking and instant barge-in, no STT, LLM, or TTS to stitch together yourself. wss://api.pyai.com/v1/omni
  • Trace (flagship), The compliance API that keeps your AI agents safe. Trace automatically checks every call for HIPAA, TCPA, and PII risks (plus your own brand-voice rules), flags the exact rule broken, redacts sensitive data, and seals each call with a tamper-evident audit trail, so a risky conversation never slips through. GET /v1/trace/interactions
  • Cue, reserved turn-detection and grounding fields on Hear streaming. Grounding is not active on the serving route yet.
  • AMD, Answering-machine detection that tells your dialer who or what answered, human, voicemail, IVR, iPhone/Google screening, dead number, fax, in a fraction of Twilio's dead-air dwell, with the reason. A one-line-TwiML Twilio Media Streams drop-in; billed per answered call (first 5,000/month free). wss://api.pyai.com/v1/amd/stream
  • Telephony, Instant managed phone numbers for your voice agents. Provision a US number and route live calls straight into an Omni agent, no carrier contracts, no telephony glue. POST /v1/telephony/numbers

The contract is https://api.pyai.com/openapi.json. This SDK wraps it ergonomically with typed errors, automatic retries, and a realtime helper.

Install

npm install @pyai/sdk

Quickstart

import PyAI from "@pyai/sdk";

const pyai = new PyAI({ apiKey: process.env.PYAI_API_KEY! });

// Text-to-speech
const audio = await pyai.audio.speech({ input: "Hello from PyAI.", voice: "stock_emma_en_gb" });
await Bun.write?.("hello.wav", audio); // or fs.writeFile in Node

// Text-to-speech, streamed, start playing/forwarding at the first chunk
// (tens of ms) instead of waiting for the whole clip. Use mp3 for smooth
// progressive playback.
const stream = await pyai.audio.speechStream({ input: "Hello from PyAI.", voice: "stock_emma_en_gb", response_format: "mp3" });
for await (const chunk of stream) writeToSpeakerOrResponse(chunk);

// Voices
const { data: voices } = await pyai.voices.list({ gender: "female" });

// Async transcription (safe retry with an idempotency key)
const job = await pyai.transcriptionJobs.create(
  { audio_url: "https://example.com/call.wav", diarize: true },
  { idempotencyKey: crypto.randomUUID() },
);
const done = await pyai.transcriptionJobs.get(job.job_id);

Use with MCP (AI coding agents)

Building this SDK with an AI coding agent (Cursor, Claude Code, Codex)? Add the PyAI MCP server (@pyai/mcp) so the agent can mint a free key and call PyAI as tools, no endpoint guessing, no human setup step:

// .cursor/mcp.json  ·  or:  claude mcp add pyai -- npx -y @pyai/mcp
{ "mcpServers": { "pyai": { "command": "npx", "args": ["-y", "@pyai/mcp"] } } }

With no key set, the server exposes create_sandbox_key, calls it, and adopts the minted key for the session, then get_started, list_voices, synthesize_speech, and the transcription tools work immediately. Full setup + a runnable client: the mcp-quickstart example.

Realtime (Omni)

omni.connect() opens an agentic-voice session and hides the wire protocol, including its frame-key asymmetry (your control frames are keyed on type, the server's frames are keyed on event). It sends a type-keyed configure the instant the socket opens and routes server frames to typed callbacks, so you can't trip the #1 Omni integration bug (a hand-rolled {"event":"configure"} is acked but silently dropped, giving you a connected session with zero turns):

Requires SDK 0.7.1 for --template omni. The runnable starter uses ESM, Node 20.19+ with an explicit ws transport, or Node 22+. MCP 0.5.0 requires Node 22+. The SDK's REST client continues to support Node 18+.

npm install @pyai/[email protected]
npx pyai init voice-demo --template omni
cd voice-demo
npm install

Initialization is offline; installing dependencies and running main.mjs are separate steps. For an existing checkout of this example, run npm install in its directory instead.

Inject PYAI_API_KEY through your environment, then supply a 24 kHz PCM16 mono WAV (at most 20 seconds) saying “Please look up the office opening time.”

node main.mjs caller.wav
# Optional interruption clip, sent while reply audio is queued:
node main.mjs caller.wav interruption.wav

The generated project contains the complete runnable source, including its Node 20 WebSocket import, WAV reader, one paced input stream, read-only tool, and bounded capture. It waits for configuration and greeting playback to drain. It sends caller PCM or silence in each slot, never overlapping silence timers.

Running it consumes Omni and Hear usage under the injected key. It saves a private report and WAV files. The report separates received audio from an answer recovered by Hear from captured bytes; synthesis text alone cannot pass. The playback sink is simulated. Interruption clears that queue; physical speaker playback remains a separate test. Capture uses two seconds of quiet after the queue drains, because the protocol has no reply-end marker.

This section and the CLI project are generated from the same tested example. Release readiness: https://pyai.com/agents/bot-release.json

Live 0x02 transcript bodies are plain UTF-8 caller-text deltas, not JSON. onTranscript receives the normalized { event:"transcript", role:"user", text, final:false, mode:"delta" } shape. Coalesce successive deltas for the current caller turn. Bounded direct JSON bodies remain accepted for older bridges.

Since version 0.5.1, the engine's four-field assistant synthesis advisory on 0x03 also reaches onTranscript with role: "assistant", final: true, and mode: "replace". This is text submitted for synthesis; it does not prove that playback completed. Caller transcripts continue to require the 0x02 carrier.

rate configures caller input. A rate: 16000 session still receives 24 kHz agent audio; rate: 8000 receives 8 kHz. Read hello.audio_out before playback. Omni has no commit frame—keep streaming silence during caller pauses. Use one paced input stream: send caller PCM when available, otherwise silence. Pause any separate silence timer while microphone or fixture frames are being sent; never interleave extra silence with active input.

For a client-executed lookup, declare side_effect: "read" in its tool definition:

const officeHoursTool = {
  name: "lookup_office_hours",
  description: "Read the office opening time.",
  side_effect: "read",
  parameters: { type: "object", properties: {} },
};
// Include officeHoursTool in configure.tools. In onToolCall, return the
// actual lookup result using session.toolResult(frame.call_id, { result }).

An omitted side_effect is treated as an action. Action results need a positive completion acknowledgement, for example { ok: true, receipt_id: actualReceiptId }, returned inside toolResult's result. Send that only after the operation has completed; a queued request or transport acknowledgement is insufficient. On failure, return { error: "Operation failed" } instead of claiming success.

From the browser, mint an ephemeral token server-side with pyai.omni.createSession({ allowedOrigins }) and pass it as token so the page never holds a secret key:

const omni = pyai.omni.connect({ token: session.token, configure: { voice_id, persona } });

Omni connects only to wss://api.pyai.com/v1/omni and is zero-state, no agent to create. sessionLabel is an optional opaque tag (never required). Need the raw socket? Use pyai.realtimeURL({ sessionLabel }) with pyai.realtimeSubprotocol() (or pyai.connectRealtime()). The raw URL helper accepts canonical format, rate, and api_key query parameters; retired connect aliases, model selectors, and token query names throw instead of being translated.

Streaming speech-to-text (Hear)

transcriptions.stream() hides the WebSocket frame protocol behind callbacks. It opens wss://api.pyai.com/v1/audio/transcriptions/stream?protocol=pyai-hear-v1 (key carried as the WS subprotocol, so it works in the browser), routes the wire frames to onConfigAck/onPartial/onFinal/onError, and gives you sendAudio, configureEndpointing(), commit(), and close():

const hear = pyai.audio.transcriptions.stream({
  sampleRate: 16000,
  endpointingMs: 800, // minimum trailing pause; may wait up to max(800, 1500) ms
  vocabulary: ["Nguyen", "SKU-99"],
  onConfigAck: (ack) => {
    if (ack.warnings.length) throw new Error(JSON.stringify(ack.warnings));
  },
  onPartial: (f) => console.log("…", f.text),
  onFinal: (f) => console.log("✓", f.text, f.endpoint_reason),
  onError: (e) => console.error(e),
});

micChunks.on("data", (pcm16) => hear.sendAudio(pcm16)); // keep sending silence through pauses
hear.configureEndpointing(950);                         // update without reconnecting
vad.on("end", () => hear.commit());                     // optional forced final
// hear.close() also flushes a final for any buffered audio

Streaming uses up to five sanitized vocabulary terms. To store organization suggestions, use a key with hear:configure and set explicit activation profiles first:

await pyai.hear.vocabulary.set({
  terms: ["Nguyen", "SKU-99"],
  enabledFor: ["batch", "hear_stream"],
});

Request-level terms come first. Stored suggestions fill remaining slots up to five. The effective list is fixed when a stream opens or a batch job is created. Organization Hear terms are not used by Omni and are not populated automatically from CRM or dialer data. A managed Agent can opt in with its own list:

const agent = await pyai.agents.create({
  name: "Front desk",
  vocabulary: ["Nguyen", "Acme Dental", "SKU-99"],
});

await pyai.agents.update(agent.agent_id, { vocabulary: [] });

The Agent list is sanitized to at most five effective terms and fixed when a new session starts. An empty list turns the feature off.

Frame types, WS close codes, and error codes are exported as named constants so you never hardcode a magic string:

import { HearFrameType, WSCloseCode, ErrorCode } from "@pyai/sdk";
HearFrameType.SpeechFinal; // "speech_final"
WSCloseCode.OverCapacity;  // 4429
ErrorCode.CreditExhausted; // "credit_exhausted"

Set grounding: true to turn the stream into Cue (turn detection + KB context): the SDK sends the grounding config on open and final/speech_final frames then carry a grounding array of top KB passages.

In Node, pass a WebSocket implementation if there's no global one: transcriptions.stream({ webSocket: (await import("ws")).WebSocket }).

Speak audio formats (incl. telephony G.711)

audio.speech encodes server-side into any of eight formats via response_format, the audio comes back already in the shape you need, so telephony callers can drop the hand-rolled resampler + μ-law encoder entirely:

// Twilio/SIP-ready in one param: raw 8 kHz mono μ-law, no client-side DSP.
const ulaw = await pyai.audio.speech({
  input: "Your appointment is confirmed.",
  voice: "stock_emma_en_gb",
  response_format: "g711_ulaw", // -> audio/basic, forced 8 kHz
});
// base64-encode `ulaw` straight into a Twilio media frame.

| response_format | sample rates (Hz) | Content-Type | |---|---|---| | wav (default) | 8000 / 16000 / 24000 / 48000 | audio/wav | | mp3 | 8000 / 16000 / 24000 / 48000 | audio/mpeg | | opus | 8000 / 16000 / 24000 / 48000 | audio/ogg | | aac | 8000 / 16000 / 24000 / 48000 | audio/aac | | flac | 8000 / 16000 / 24000 / 48000 | audio/flac | | pcm (raw int16 LE, no header) | 8000 / 16000 / 24000 / 48000 | audio/pcm | | g711_ulaw | 8000 (forced) | audio/basic | | g711_alaw | 8000 (forced) | audio/basic |

sample_rate is optional, omit it for the engine's native 24 kHz (g711_* is always 8 kHz). The set is typed (SpeechFormat) and exported as SPEECH_FORMATS / SPEECH_SAMPLE_RATES for dropdowns and validation. Any other value is a 400 unsupported_format; omit response_format for the default wav.

See examples/speak-telephony-formats for the full before/after: ~120 lines of resampler + μ-law replaced by one param, with Node (@pyai/twilio), Python, and raw-curl snippets.

AMD (answering-machine detection)

Already on Twilio? The usual path is one line of TwiML pointing the call's media at PyAI, no SDK needed. The answered_by_twilio field maps to Twilio's exact AnsweredBy enum, so your routing logic doesn't change:

<Response><Connect>
  <Stream url="wss://api.pyai.com/v1/amd/stream">
    <Parameter name="api_key" value="YOUR_PYAI_KEY"/>
    <Parameter name="aggressiveness" value="0.25"/>
    <Parameter name="webhook" value="https://you/amd-events"/>
  </Stream>
</Connect></Response>

(The key rides a <Parameter> because Twilio strips query strings from the <Stream> URL; PyAI verifies it from the start frame before processing any audio.)

From code, set the operating point and read decisions back:

// One aggressiveness dial: near 0 = human-safe, near 1 = fire "machine" fast.
await pyai.amd.config.set({ aggressiveness: 0.25, webhookUrl: "https://you/amd-events" });

const { data: decisions } = await pyai.amd.calls.list({ sessionLabel: "sales" });
const decision = await pyai.amd.calls.get("C_123");
// decision.answered_by = "human" | "voicemail" | "screening" | "sit_invalid" | ...
// decision.answered_by_twilio = "human" | "machine_start" | ...  (Twilio parity)
// decision.reason = "machine phrase: 'leave a message' @1.2s"

// Server-side helper if you fork the media yourself (Twilio Media Streams wire):
const stream = pyai.amd.stream({
  aggressiveness: 0.25,
  onDecision: (d) => console.log(d.answered_by, d.decision_ms, d.reason),
});

Billed per answered call, first 5,000 answered calls/month free, then $0.004/call; free when bundled with PyAI telephony/Omni.

More APIs: clones, telephony, trace

// Voice clones (Speak)
const { data: clones } = await pyai.clones.list();
const clone = await pyai.clones.create({ name: "Brand VO", file: refAudioBlob });
await pyai.clones.delete(clone.id);

// Managed phone numbers (Telephony)
const { data: avail } = await pyai.telephony.numbers.available({ areaCode: "415" });
const num = await pyai.telephony.numbers.buy({ phone_number: avail[0]!.phone_number, agent_id: "agent_123" });
await pyai.telephony.numbers.assign(num.id, "agent_123");
await pyai.telephony.numbers.release(num.id);

// Compliance (Trace)
const { data: calls } = await pyai.trace.interactions.list({ verdict: "FAIL" });
const detail = await pyai.trace.interactions.get(calls[0]!.id);
await pyai.trace.config.set({ agent_id: "agent_123", enabled: true });
const exposure = await pyai.trace.exposure(30);

// Per-call eval scorecard (timeline + quality metrics). These are additive and
// forward-compatible, present once the engine emits them, so reading them is
// always safe (the timeline reader returns [] until then).
const timeline = await pyai.trace.callTimeline(detail.id); // TraceTimelineTurn[]
const quality = detail.quality_metrics;                    // { wer?, ttfb_ms?, turn_p95_ms?, vaqi?, … }

Reproducible runs (evals)

audio.speech and audio.transcriptions.create take optional seed and temperature for deterministic eval runs. They're forward-compatible, honored once the engine supports them and otherwise ignored, so it's always safe to send:

await pyai.audio.speech({ input: "Hello", voice: "stock_emma_en_gb", seed: 42, temperature: 0 });
await pyai.audio.transcriptions.create({ file: wavBlob, seed: 42 });

Errors

Failures throw PyAIError with a stable code (branch on it, not the message):

import { PyAIError } from "@pyai/sdk";

try {
  await pyai.audio.speech({ input: "hi" });
} catch (err) {
  if (err instanceof PyAIError && err.code === "credit_exhausted") {
    // out of prepaid credit, add credit or use a sandbox key
  }
}

Common codes: unauthorized, forbidden, credit_exhausted, rate_limit_exceeded, concurrency_limit_exceeded, idempotency_conflict. 429/5xx are retried automatically (honoring Retry-After); tune with new PyAI({ apiKey, maxRetries }).

CLI (pyai)

The package provides a pyai executable for engineers, CI, and coding agents. Install version 0.5.0 with npm install -g @pyai/[email protected], then use pyai login for browser sign-in. Environment API keys work for unattended automation.

pyai speak "Your appointment is confirmed." -o confirmation.wav
pyai hear confirmation.wav --text-only
pyai login -p work
pyai whoami -j
pyai schema agents create -j
pyai agents create --data @agent.json --dry-run -j
pyai recipes speak
pyai init voice-project --template agent

The detailed CLI handbook covers installation, profiles, speech, transcription, Dub submission through download, Cast, resource configuration, JSON and stdin contracts, exit codes, and troubleshooting. For coding agents, use the raw integration guide and live OpenAPI contract.

pyai doctor checks the key, catalogs, and a Speak-to-Hear round trip; pyai smoke checks catalogs and synthesis. Both make real API calls.

Develop

npm install
npm test         # node --test, fetch injected (no network)
npm run build    # emits dist/ (incl. the pyai CLI bin)