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

@puppetry.com/sdk

v0.1.1

Published

Official TypeScript SDK for the Puppetry API — create talking head videos from photos using AI

Readme

@puppetry.com/sdk

Official TypeScript SDK for the Puppetry API — create talking head videos from photos using AI.

Installation

npm install @puppetry.com/sdk

Quick Start

import { Puppetry } from "@puppetry.com/sdk";

const client = new Puppetry({ apiKey: "pk_..." });

// Create a video from text
const job = await client.videos.createFromText({
  text: "Welcome to our product demo!",
  image_url: "https://example.com/photo.jpg",
  voice: "puppetry-af_heart",
  idempotencyKey: "demo-video-1",
});

// Wait for completion (polls automatically, honoring the create Retry-After hint)
const video = await job.waitForCompletion();
console.log(video.url);

Features

  • Text-to-Video: Provide text → get a talking head video with AI voice
  • Audio-to-Video: Provide audio → lip-sync any portrait
  • 500+ Voices: Choose from built-in voices or clone your own
  • 29 Languages: Arabic, Chinese, English, French, German, Hindi, and more
  • Progress Events: Stream job events through status polling that follows API retry hints
  • Voice Cloning: Upload 6 seconds of audio to create a custom voice
  • TypeScript-first: Full type safety, JSDoc, IntelliSense support

API Reference

Create a Client

const client = new Puppetry({
  apiKey: "pk_...", // Required — get yours at puppetry.com/dashboard/settings/api-keys
  baseUrl: "https://www.puppetry.com/api/v1", // Optional
  timeout: 30000, // Optional, ms
  maxRetries: 2, // Optional, retries GET 429/5xx and idempotent POST 429/5xx
  retryDelayMs: 500, // Optional, exponential backoff base delay
  onRetry: (event) => console.warn(event.code, event.retryAfter), // Optional
});

The SDK only retries side-effecting POST calls when you pass an idempotencyKey. Use one for video creates and signed upload URL requests so a transient 429/5xx cannot create duplicate jobs or charges. onRetry runs before the SDK sleeps for a safe/idempotent retry and includes Developer API job, operation, request, retry-after, and status URL metadata when the server returns it.

Videos

// From text (text-to-speech + lip sync)
const job = await client.videos.createFromText({
  text: "Hello, world!",
  image_url: "https://example.com/photo.jpg",
  voice: "puppetry-af_heart",
  language: "en",
  expressiveness: 1.0,
  idempotencyKey: "text-video-demo-1",
});
console.log(job.source); // "text"
console.log(`First poll at ${job.nextPollAt ?? "the Retry-After time"}`);

Agents and launch checks can preflight video credits and active slots before reserving uploads or queueing a job:

const readiness = await client.videos.getReadiness();
if (!readiness.can_create) {
  console.log(readiness.reason, readiness.retryAfter);
  for (const blocker of readiness.blockers ?? []) {
    console.log(blocker.code, blocker.retryable);
  }
}

// Throws PuppetryRateLimitError with retryAfter for active-slot blockers, or
// PuppetryError(402) when the API key is out of video credits.
await client.videos.ensureReady();

readiness.blockers preserves the API's machine-readable recovery list. Agents can treat insufficient_video_credits as a buy-credits path and concurrent_job_limit_reached as a backoff path using retryAfter or retry_after_seconds.

For hosted audio uploads that will immediately feed videos.createFromAudio(), pass requireVideoReadiness: true so Puppetry fails before reserving upload quota if video credits or active slots are currently blocked.

If an idempotent retry replays an already completed job, the SDK normalizes the terminal URL onto url, videoUrl, resultUrl, downloadUrl, and outputUrl while preserving raw API fields such as download_url and idempotent_replay. Video job responses also preserve and normalize the create source across source, request_source, and requestSource, so agents can distinguish text jobs from audio/lip-sync jobs during status polling or idempotent replay. The seven-day lookup expiry is normalized across expires_at and expiresAt. Retryable video job responses expose both relative retry seconds and absolute poll timestamps as next_poll_at / nextPollAt. SDK waitForCompletion() and videos.stream() polling honors both forms, so agents can follow the server-paced poll schedule without custom sleep logic. Video create, replay, and status responses also normalize the public operation handle across operation_id / operationId, using the response operation-id headers when the body omits those aliases. waitForCompletion() also treats retryable status-poll errors as part of the same job lifecycle: it uses the error's preserved job identity, status URL, and retry timing to keep polling the original job until the caller timeout expires. The polling URL is normalized across status_url / statusUrl; when a queued create or replay response only exposes the URL in Location / Content-Location, the SDK fills the same aliases from those headers. Job helpers also derive their polling target from that canonical status URL when it is present, so idempotent replays and sparse header-only responses keep polling the accepted video job.

// From audio (lip sync only)
const job2 = await client.videos.createFromAudio({
  audio_url: "https://example.com/speech.mp3",
  image_url: "https://example.com/photo.jpg",
  idempotencyKey: "audio-video-demo-1",
});

// Get job status
const status = await client.jobs.get(job.id);

// Wait for completion (with polling)
const completed = await job.waitForCompletion({
  intervalMs: 2000, // Poll every 2s (default)
  timeoutMs: 300000, // 5 min timeout (default)
  // initialDelayMs defaults to the job's Retry-After hint when present
  onPoll: (latest) => {
    console.log(latest.status, latest.progress ?? 0);
  },
});

// The explicit resource helper is still available when you only have an ID.
const completedAgain = await client.jobs.waitForCompletion(job.id, {
  initialDelayMs: (job.retryAfter ?? 0) * 1000,
});

// Stream progress events through polling
for await (const event of client.videos.stream(job.id)) {
  console.log(`${event.type}: ${event.progress}%`);
  if (event.type === "completed") {
    console.log(`Video ready: ${event.video_url}`);
  }
}

Voices

// List available voices
const { data: voices } = await client.voices.list({
  language: "en",
  gender: "female",
  limit: 20,
});

// List built-in Puppetry voices for /tts/puppetry
const puppetryVoices = await client.voices.listPuppetry();
console.log(puppetryVoices.default_voice_id);
console.log(puppetryVoices.data[0]?.preview_text);

Text to Speech

const voices = await client.voices.listPuppetry();

const audio = await client.tts.createPuppetry({
  voice_id: voices.default_voice_id,
  text: "Create a voiceover before turning it into video.",
  speed: 1.0,
});

console.log(audio.audio_url);

Uploads

const upload = await client.uploads.createAudioUrl({
  contentType: "audio/mpeg",
  fileSize: 12345,
  idempotencyKey: "upload-demo-1",
});

await fetch(upload.uploadUrl, {
  method: upload.method,
  headers: upload.headers,
  body: audioBytes,
});

console.log(upload.readUrl);

// Or let the SDK request the signed URL and upload bytes in one call.
const uploaded = await client.uploads.uploadAudio({
  contentType: "audio/mpeg",
  sizeBytes: audioBytes.byteLength,
  body: audioBytes,
  idempotencyKey: "upload-demo-1",
});

const job = await client.videos.createFromAudio({
  audio_url: uploaded.readUrl,
  image_url: "https://example.com/photo.jpg",
});

The raw API fields (upload_url, read_url, expiry fields, and limits) stay available too; the SDK also exposes camelCase aliases for TypeScript and agent callers, including idempotentReplay when a retried upload reservation is replayed.

Puppets

// Create a reusable puppet (portrait)
const puppet = await client.puppets.create({
  imageUrl: "https://example.com/portrait.jpg",
  name: "Marketing Avatar",
});

// List puppets
const { data: puppets } = await client.puppets.list({ limit: 10 });

// Delete a puppet when it should no longer appear in your reusable library
await client.puppets.delete(puppet.id);

Usage

const usage = await client.usage.get();
console.log(
  `${usage.videoCredits?.balance ?? usage.creditsRemaining} video credits remaining`,
);
console.log(usage.videoGeneration?.blockers ?? []);
console.log(
  `${usage.videoCredits?.netUsed ?? usage.creditsUsed} video credits used this month`,
);

// Agent integrations can use the quota alias for a clearer tool name.
const quota = await client.quota.get();
console.log(`${quota.creditsRemaining} video credits remaining`);

Agent Tool Manifest

import {
  Puppetry,
  PUPPETRY_AGENT_TOOLS,
  callPuppetryAgentTool,
} from "@puppetry.com/sdk";

console.log(PUPPETRY_AGENT_TOOLS.map((tool) => tool.name));
// puppetry_create_video_from_text, puppetry_create_video_from_audio,
// puppetry_create_video_from_text_and_wait,
// puppetry_create_video_from_audio_and_wait, puppetry_create_audio_upload_url,
// puppetry_lipsync, puppetry_list_voices, puppetry_get_job_status,
// puppetry_wait_for_video, puppetry_get_video_readiness, puppetry_get_quota

const client = new Puppetry({ apiKey: process.env.PUPPETRY_API_KEY! });
const result = await callPuppetryAgentTool(
  client,
  "puppetry_create_video_from_text",
  {
    prompt: "Agent-generated launch update.",
    photo_url: "https://example.com/avatar.jpg",
    voice: "puppetry-af_heart",
    preflightReadiness: true,
  },
);

console.log(result.id);
console.log(result.readiness_checked, result.readiness?.credits_remaining);

await callPuppetryAgentTool(client, "puppetry_get_job_status", {
  taskId: result.taskId ?? result.id,
});

Direct video agent tools accept preflightReadiness / preflight_readiness when the caller wants to check video credits and active slots before queueing a credit-bearing job. Successful direct-create results include readiness_checked / readinessChecked and, when preflight ran, the normalized readiness object that was checked before the create POST. The create-and-wait agent tools perform that preflight by default and expose preflightReadiness: false only for callers that already checked readiness. Both result shapes include snake_case and camelCase readiness aliases so agents can read the same evidence regardless of their naming convention.

MCP stdio server

{
  "mcpServers": {
    "puppetry": {
      "command": "npx",
      "args": ["-y", "@puppetry.com/sdk"],
      "env": { "PUPPETRY_API_KEY": "pk_live_xxx" }
    }
  }
}

The puppetry-mcp bin serves the same tool manifest over MCP stdio and dispatches calls through the SDK. By default it uses https://www.puppetry.com/api/v1, the live Developer API route family. Set PUPPETRY_BASE_URL only when pointing agents at a preview or staging API host. When the live API returns a Puppetry error, MCP JSON-RPC errors include structured error.data with the API status/statusCode, Puppetry code, message, optional details, and optional retry_after_seconds/ retryAfterSeconds/retryAfter seconds so agents can back off instead of retrying blindly. Retry hints are normalized to seconds whether the API sends a numeric or HTTP-date Retry-After header, and fall back to top-level retry_after_seconds / retryAfterSeconds / retryAfter body aliases or matching details.* aliases if an intermediary strips the header. Video job responses also include next_poll_at / nextPollAt when the API can provide an absolute retry time, plus pollable video identity fields such as object, id, job_id / jobId, source, request_source / requestSource, status_url / statusUrl, and expires_at / expiresAt when the error belongs to a video job.

Error Handling

import { PuppetryError, PuppetryAuthError, PuppetryRateLimitError, PuppetryNetworkError } from '@puppetry.com/sdk';

try {
  await client.videos.createFromText({ ... });
} catch (err) {
  if (err instanceof PuppetryAuthError) {
    console.error('Invalid API key');
  } else if (err instanceof PuppetryRateLimitError) {
    console.error(`Rate limited. Retry after ${err.retryAfter}s`);
  } else if (err instanceof PuppetryNetworkError) {
    console.error(`Network error: ${err.message}`);
  } else if (err instanceof PuppetryError) {
    if (err.retryAfter) {
      console.error(`Try again after ${err.retryAfter}s`);
    }
    console.error(`API error ${err.status}: ${err.message}`);
  }
}

Requirements

  • Node.js 18+ (uses native fetch)
  • For Node.js 16, pass a fetch implementation: new Puppetry({ apiKey: '...', fetch: nodeFetch })

License

MIT