Initial release — OpenMontage: the first open-source agentic video production system

11 production pipelines, 47 tools, 124 agent skills.
Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and
free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
calesthio
2026-03-29 08:25:17 -07:00
commit a3e735cc7a
1147 changed files with 240221 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
---
name: acestep
description: AI music generation with ACE-Step 1.5 — background music, vocal tracks, covers, stem extraction for video production. Use when generating music, soundtracks, jingles, or working with audio stems. Triggers include background music, soundtrack, jingle, music generation, stem extraction, cover, style transfer, or musical composition tasks.
---
# ACE-Step 1.5 Music Generation
Open-source music generation (MIT license) via `tools/music_gen.py`. Runs on RunPod serverless.
Requires `RUNPOD_API_KEY` and `RUNPOD_ACESTEP_ENDPOINT_ID` in `.env` (run `--setup` to create endpoint).
## Quick Reference
```bash
# Basic generation
python tools/music_gen.py --prompt "Upbeat tech corporate" --duration 60 --output bg.mp3
# With musical control
python tools/music_gen.py --prompt "Calm ambient piano" --duration 30 --bpm 72 --key "D Major" --output ambient.mp3
# Scene presets (video production)
python tools/music_gen.py --preset corporate-bg --duration 60 --output bg.mp3
python tools/music_gen.py --preset tension --duration 20 --output problem.mp3
python tools/music_gen.py --preset cta --brand digital-samba --duration 15 --output cta.mp3
# Vocals with lyrics
python tools/music_gen.py --prompt "Indie pop jingle" --lyrics "[verse]\nBuild it better\nShip it faster" --duration 30 --output jingle.mp3
# Cover / style transfer
python tools/music_gen.py --cover --reference theme.mp3 --prompt "Jazz piano version" --duration 60 --output jazz_cover.mp3
# Stem extraction
python tools/music_gen.py --extract vocals --input mixed.mp3 --output vocals.mp3
# List presets
python tools/music_gen.py --list-presets
```
## Creating a Song (Step by Step)
### 1. Instrumental background track (simplest)
```bash
python tools/music_gen.py --prompt "Upbeat indie rock, driving drums, jangly guitar" --duration 60 --bpm 120 --key "G Major" --output track.mp3
```
### 2. Song with vocals and lyrics
Write lyrics in a temp file or pass inline. Use structure tags to control song sections.
```bash
# Write lyrics to a file first (recommended for longer songs)
cat > /tmp/lyrics.txt << 'LYRICS'
[Verse 1]
Walking through the morning light
Coffee in my hand feels right
Another day to build and dream
Nothing's ever what it seems
[Chorus - anthemic]
WE KEEP MOVING FORWARD
Through the noise and doubt
We keep moving forward
That's what it's about
[Verse 2]
Screens are glowing late at night
Shipping code until it's right
The deadline's close but so are we
Almost there, just wait and see
[Chorus - bigger]
WE KEEP MOVING FORWARD
Through the noise and doubt
We keep moving forward
That's what it's about
[Outro - fade]
(Moving forward...)
LYRICS
# Generate the song
python tools/music_gen.py \
--prompt "Upbeat indie rock anthem, male vocal, driving drums, electric guitar, studio polish" \
--lyrics "$(cat /tmp/lyrics.txt)" \
--duration 60 \
--bpm 128 \
--key "G Major" \
--output my_song.mp3
```
### 3. Using a preset for video background
```bash
python tools/music_gen.py --preset tension --duration 20 --output problem_scene.mp3
```
### Key tips for good results
- **Caption = overall style** (genre, instruments, mood, production quality)
- **Lyrics = temporal structure** (verse/chorus flow, vocal delivery)
- **UPPERCASE in lyrics** = high vocal intensity
- **Parentheses** = background vocals: "We rise (together)"
- **Keep 6-10 syllables per line** for natural rhythm
- **Don't describe the melody in the caption** — describe the *sound* and *feeling*
- **Use `--seed`** to lock randomness when iterating on prompt/lyrics
## Scene Presets
| Preset | BPM | Key | Use Case |
|--------|-----|-----|----------|
| `corporate-bg` | 110 | C Major | Professional background, presentations |
| `upbeat-tech` | 128 | G Major | Product launches, tech demos |
| `ambient` | 72 | D Major | Overview slides, reflective content |
| `dramatic` | 90 | D Minor | Reveals, announcements |
| `tension` | 85 | A Minor | Problem statements, challenges |
| `hopeful` | 120 | C Major | Solution reveals, resolutions |
| `cta` | 135 | E Major | Call to action, closing energy |
| `lofi` | 85 | F Major | Screen recordings, coding demos |
## Task Types
### text2music (default)
Generate music from text prompt + optional lyrics.
### cover
Style transfer from reference audio. Control blend with `--cover-strength` (0.0-1.0):
- **0.2** — Loose style inspiration (more creative freedom)
- **0.5** — Balanced style transfer
- **0.7** — Close to original structure (default)
- **1.0** — Maximum fidelity to source
### extract
Stem separation — isolate individual tracks from mixed audio.
Tracks: `vocals`, `drums`, `bass`, `guitar`, `piano`, `keyboard`, `strings`, `brass`, `woodwinds`, `other`
### repaint (future)
Regenerate a specific time segment within existing audio while preserving the rest.
### lego (future, requires base model)
Generate individual instrument tracks within an existing audio context.
### complete (future, requires base model)
Extend partial compositions by adding specified instruments.
## Prompt Engineering
### Caption Writing — Layer Dimensions
Write captions by layering multiple descriptive dimensions rather than single-word descriptions.
**Dimensions to include:**
- **Genre/Style**: pop, rock, jazz, electronic, lo-fi, synthwave, orchestral
- **Emotion/Mood**: melancholic, euphoric, dreamy, nostalgic, intimate, tense
- **Instruments**: acoustic guitar, synth pads, 808 drums, strings, brass, piano
- **Timbre**: warm, crisp, airy, punchy, lush, polished, raw
- **Era**: "80s synth-pop", "modern indie", "classical romantic"
- **Production**: lo-fi, studio-polished, live recording, cinematic
- **Vocal**: breathy, powerful, falsetto, raspy, spoken word (or "instrumental")
**Good**: "Slow melancholic piano ballad with intimate female vocal, warm strings building to powerful chorus, studio-polished production"
**Bad**: "Sad song"
### Key Principles
1. **Specificity over vagueness** — describe instruments, mood, production style
2. **Avoid contradictions** — don't request "classical strings" and "hardcore metal" simultaneously
3. **Repetition reinforces priority** — repeat important elements for emphasis
4. **Sparse captions = more creative freedom** — detailed captions constrain the model
5. **Use metadata params for BPM/key** — don't write "120 BPM" in the caption, use `--bpm 120`
### Lyrics Formatting
**Structure tags** (use in lyrics, not caption):
```
[Intro]
[Verse]
[Chorus]
[Bridge]
[Outro]
[Instrumental]
[Guitar Solo]
[Build]
[Drop]
[Breakdown]
```
**Vocal control** (prefix lines or sections):
```
[raspy vocal]
[whispered]
[falsetto]
[powerful belting]
[harmonies]
[ad-lib]
```
**Energy indicators:**
- UPPERCASE = high intensity ("WE RISE ABOVE")
- Parentheses = background vocals ("We rise (together)")
- Keep 6-10 syllables per line within sections for natural rhythm
**Example — Tech Product Jingle:**
```
[Verse]
Build it better, ship it faster
Every feature tells a story
[Chorus - anthemic]
THIS IS YOUR PLATFORM
Your vision, your stage
Digital Samba, every page
[Outro - fade]
(Build it better...)
```
## Video Production Integration
### Music for Scene Types
| Scene | Preset | Duration | Notes |
|-------|--------|----------|-------|
| Title | `dramatic` or `ambient` | 3-5s | Short, mood-setting |
| Problem | `tension` | 10-15s | Dark, unsettling |
| Solution | `hopeful` | 10-15s | Relief, optimism |
| Demo | `lofi` or `corporate-bg` | 30-120s | Non-distracting, matches demo length |
| Stats | `upbeat-tech` | 8-12s | Building credibility |
| CTA | `cta` | 5-10s | Maximum energy, punchy |
| Credits | `ambient` | 5-10s | Gentle fade-out |
### Timing Workflow
1. Plan scene durations first (from voiceover script)
2. Generate music to match: `--duration <scene_seconds>`
3. Music duration is precise (within 0.1s of requested)
4. For background music spanning multiple scenes: generate one long track
### Combining with Voiceover
Background music should be mixed at 10-20% volume in Remotion:
```tsx
<Audio src={staticFile('voiceover.mp3')} volume={1} />
<Audio src={staticFile('bg-music.mp3')} volume={0.15} />
```
For music under narration: use instrumental presets (`corporate-bg`, `ambient`, `lofi`).
For music-forward scenes (title, CTA): can use higher volume or vocal tracks.
### Brand Consistency
Use `--brand <name>` to load hints from `brands/<name>/brand.json`.
Use `--cover --reference brand_theme.mp3` to create variations of a brand's sonic identity.
For consistent sound across a project: fix the seed (`--seed 42`) and vary only duration/prompt.
## Technical Details
- **Output**: 48kHz MP3/WAV/FLAC
- **Duration range**: 10-600 seconds
- **BPM range**: 30-300
- **Inference**: ~2-3s on GPU (turbo, 8 steps), ~40-60s on Mac MPS
- **Turbo model**: 8 steps, no CFG needed, fast and good quality
- **Shift parameter**: 3.0 recommended for turbo (improves quality)
### When NOT to use ACE-Step
- **Voice cloning** — use Qwen3-TTS or ElevenLabs instead
- **Sound effects** — use ElevenLabs SFX (`tools/sfx.py`)
- **Speech/narration** — use voiceover tools, not music gen
- **Stem extraction from video** — extract audio first with FFmpeg, then use `--extract`
+302
View File
@@ -0,0 +1,302 @@
---
name: agents
description: Build voice AI agents with ElevenLabs. Use when creating voice assistants, customer service bots, interactive voice characters, or any real-time voice conversation experience.
license: MIT
compatibility: Requires internet access and an ElevenLabs API key (ELEVENLABS_API_KEY).
metadata: {"openclaw": {"requires": {"env": ["ELEVENLABS_API_KEY"]}, "primaryEnv": "ELEVENLABS_API_KEY"}}
---
# ElevenLabs Agents Platform
Build voice AI agents with natural conversations, multiple LLM providers, custom tools, and easy web embedding.
> **Setup:** See [Installation Guide](references/installation.md) for CLI and SDK setup.
## Quick Start with CLI
The ElevenLabs CLI is the recommended way to create and manage agents:
```bash
# Install CLI and authenticate
npm install -g @elevenlabs/cli
elevenlabs auth login
# Initialize project and create an agent
elevenlabs agents init
elevenlabs agents add "My Assistant" --template complete
# Push to ElevenLabs platform
elevenlabs agents push
```
**Available templates:** `complete`, `minimal`, `voice-only`, `text-only`, `customer-service`, `assistant`
### Python
```python
from elevenlabs import ElevenLabs
client = ElevenLabs()
agent = client.conversational_ai.agents.create(
name="My Assistant",
enable_versioning=True,
conversation_config={
"agent": {
"first_message": "Hello! How can I help?",
"language": "en",
"prompt": {
"prompt": "You are a helpful assistant. Be concise and friendly.",
"llm": "gemini-2.0-flash",
"temperature": 0.7
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
### JavaScript
```javascript
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
const client = new ElevenLabsClient();
const agent = await client.conversationalAi.agents.create({
name: "My Assistant",
enableVersioning: true,
conversationConfig: {
agent: {
firstMessage: "Hello! How can I help?",
language: "en",
prompt: {
prompt: "You are a helpful assistant.",
llm: "gemini-2.0-flash",
temperature: 0.7
}
},
tts: { voiceId: "JBFqnCBsd6RMkjVDRZzb" }
}
});
```
### cURL
```bash
curl -X POST "https://api.elevenlabs.io/v1/convai/agents/create?enable_versioning=true" \
-H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "My Assistant", "conversation_config": {"agent": {"first_message": "Hello!", "language": "en", "prompt": {"prompt": "You are helpful.", "llm": "gemini-2.0-flash"}}, "tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}}}'
```
## Starting Conversations
**Server-side (Python):** Get signed URL for client connection:
```python
signed_url = client.conversational_ai.conversations.get_signed_url(
agent_id="your-agent-id",
environment="staging",
)
```
**Client-side (JavaScript):**
```javascript
import { Conversation } from "@elevenlabs/client";
const conversation = await Conversation.startSession({
agentId: "your-agent-id",
environment: "staging",
onMessage: (msg) => console.log("Agent:", msg.message),
onUserTranscript: (t) => console.log("User:", t.message),
onError: (e) => console.error(e)
});
```
**React Hook:**
```typescript
import { useConversation } from "@elevenlabs/react";
const conversation = useConversation({ onMessage: (msg) => console.log(msg) });
// Get a signed URL for the target environment from your backend, then:
await conversation.startSession({ signedUrl: token });
```
## Configuration
| Provider | Models |
|----------|--------|
| OpenAI | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo` |
| Anthropic | `claude-sonnet-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4`, `claude-haiku-4-5`, `claude-3-7-sonnet`, `claude-3-5-sonnet`, `claude-3-haiku` |
| Google | `gemini-3.1-flash-lite-preview`, `gemini-3-pro-preview`, `gemini-3-flash-preview`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-2.0-flash`, `gemini-2.0-flash-lite` |
| ElevenLabs | `glm-45-air-fp8`, `qwen3-30b-a3b`, `gpt-oss-120b` |
| Custom | `custom-llm` (bring your own endpoint) |
Use `GET /v1/convai/llm/list` to inspect the current model catalog, including deprecation state, token/context limits, and capability flags such as image-input support.
**Popular voices:** `JBFqnCBsd6RMkjVDRZzb` (George), `EXAVITQu4vr4xnSDxMaL` (Sarah), `onwK4e9ZLuTAKqWW03F9` (Daniel), `XB0fDUnXU5powFXDhCwa` (Charlotte)
**Turn eagerness:** `patient` (waits longer for user to finish), `normal`, or `eager` (responds quickly)
See [Agent Configuration](references/agent-configuration.md) for all options.
## Tools
Extend agents with webhook, client, or built-in system tools. Tools are defined inside `conversation_config.agent.prompt`:
Workspace environment variables can resolve per-environment server tool URLs, headers, and auth connections, and runtime system variables such as `{{system__conversation_history}}` can pass full conversation context into tool calls when needed.
```python
"prompt": {
"prompt": "You are a helpful assistant that can check the weather.",
"llm": "gemini-2.0-flash",
"tools": [
# Webhook: server-side API call
{"type": "webhook", "name": "get_weather", "description": "Get weather",
"api_schema": {"url": "https://api.example.com/weather", "method": "POST",
"request_body_schema": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}},
# Client: runs in the browser
{"type": "client", "name": "show_product", "description": "Display a product",
"parameters": {"type": "object", "properties": {"productId": {"type": "string"}}, "required": ["productId"]}}
],
"built_in_tools": {
"end_call": {},
"transfer_to_number": {"transfers": [{"transfer_destination": {"type": "phone", "phone_number": "+1234567890"}, "condition": "User asks for human support"}]}
}
}
```
**Client tools** run in browser:
```javascript
clientTools: {
show_product: async ({ productId }) => {
document.getElementById("product").src = `/products/${productId}`;
return { success: true };
}
}
```
See [Client Tools Reference](references/client-tools.md) for complete documentation.
## Widget Embedding
```html
<elevenlabs-convai agent-id="your-agent-id"></elevenlabs-convai>
<script src="https://unpkg.com/@elevenlabs/convai-widget-embed" async type="text/javascript"></script>
```
Customize with attributes: `avatar-image-url`, `action-text`, `start-call-text`, `end-call-text`.
See [Widget Embedding Reference](references/widget-embedding.md) for all options.
## Outbound Calls
Make outbound phone calls using your agent via Twilio integration:
### Python
```python
response = client.conversational_ai.twilio.outbound_call(
agent_id="your-agent-id",
agent_phone_number_id="your-phone-number-id",
to_number="+1234567890",
call_recording_enabled=True
)
print(f"Call initiated: {response.conversation_id}")
```
### JavaScript
```javascript
const response = await client.conversationalAi.twilio.outboundCall({
agentId: "your-agent-id",
agentPhoneNumberId: "your-phone-number-id",
toNumber: "+1234567890",
callRecordingEnabled: true,
});
```
### cURL
```bash
curl -X POST "https://api.elevenlabs.io/v1/convai/twilio/outbound-call" \
-H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
-d '{"agent_id": "your-agent-id", "agent_phone_number_id": "your-phone-number-id", "to_number": "+1234567890", "call_recording_enabled": true}'
```
See [Outbound Calls Reference](references/outbound-calls.md) for configuration overrides and dynamic variables.
## Managing Agents
### Using CLI (Recommended)
```bash
# List agents and check status
elevenlabs agents list
elevenlabs agents status
# Import agents from platform to local config
elevenlabs agents pull # Import all agents
elevenlabs agents pull --agent <agent-id> # Import specific agent
# Push local changes to platform
elevenlabs agents push # Upload configurations
elevenlabs agents push --dry-run # Preview changes first
# Add tools
elevenlabs tools add-webhook "Weather API"
elevenlabs tools add-client "UI Tool"
```
### Project Structure
The CLI creates a project structure for managing agents:
```
your_project/
├── agents.json # Agent definitions
├── tools.json # Tool configurations
├── tests.json # Test configurations
├── agent_configs/ # Individual agent configs
├── tool_configs/ # Individual tool configs
└── test_configs/ # Individual test configs
```
### SDK Examples
```python
# List
agents = client.conversational_ai.agents.list()
# Get
agent = client.conversational_ai.agents.get(agent_id="your-agent-id")
# Update (partial - only include fields to change)
client.conversational_ai.agents.update(agent_id="your-agent-id", name="New Name")
client.conversational_ai.agents.update(agent_id="your-agent-id",
conversation_config={
"agent": {"prompt": {"prompt": "New instructions", "llm": "claude-sonnet-4"}}
})
# Delete
client.conversational_ai.agents.delete(agent_id="your-agent-id")
```
See [Agent Configuration](references/agent-configuration.md) for all configuration options and SDK examples.
## Error Handling
```python
try:
agent = client.conversational_ai.agents.create(...)
except Exception as e:
print(f"API error: {e}")
```
Common errors: **401** (invalid key), **404** (not found), **422** (invalid config), **429** (rate limit)
## References
- [Installation Guide](references/installation.md) - SDK setup and migration
- [Agent Configuration](references/agent-configuration.md) - All config options and CRUD examples
- [Client Tools](references/client-tools.md) - Webhook, client, and system tools
- [Widget Embedding](references/widget-embedding.md) - Website integration
- [Outbound Calls](references/outbound-calls.md) - Twilio phone call integration
@@ -0,0 +1,606 @@
# Agent Configuration
Complete reference for configuring conversational AI agents.
## Configuration Structure
```python
agent = client.conversational_ai.agents.create(
name="My Agent",
conversation_config={
"agent": {
"first_message": "Hello!",
"language": "en",
"prompt": { # LLM, system prompt, tools, and knowledge base
"prompt": "You are helpful.",
"llm": "gemini-2.0-flash",
"tools": [...],
"built_in_tools": {...}
}
},
"tts": {...}, # Voice and TTS model settings
"asr": {...}, # Speech recognition settings
"turn": {...}, # Turn-taking behavior
"conversation": {...}, # Duration, events, monitoring
"vad": {...}, # Voice activity detection config
"language_presets": {...} # Language-specific overrides
},
platform_settings={...} # Auth, call limits
)
```
## conversation_config
Controls the real-time conversation behavior.
### agent
```python
conversation_config={
"agent": {
"first_message": "Hello! How can I help you today?",
"language": "en",
"disable_first_message_interruptions": False,
"prompt": {
"prompt": "You are a helpful assistant.",
"llm": "gemini-2.0-flash",
"temperature": 0.7
}
}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `first_message` | string | `""` | What the agent says when conversation starts |
| `language` | string | `"en"` | ISO 639-1 language code (en, es, fr, etc.) |
| `disable_first_message_interruptions` | bool | `false` | Prevent user from interrupting the first message |
| `hinglish_mode` | bool | `false` | When enabled and language is Hindi, agent responds in Hinglish |
| `dynamic_variables` | object | - | Config with `dynamic_variable_placeholders` containing key-value pairs |
| `prompt` | object | - | LLM configuration (see prompt section below) |
### tts (Text-to-Speech)
```python
conversation_config={
"tts": {
"voice_id": "JBFqnCBsd6RMkjVDRZzb",
"model_id": "eleven_flash_v2_5",
"stability": 0.5,
"similarity_boost": 0.8,
"speed": 1.0,
"optimize_streaming_latency": 3,
"expressive_mode": True
}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `voice_id` | string | `"cjVigY5qzO86Huf0OWal"` | Voice to use |
| `model_id` | string | - | TTS model (see below) |
| `stability` | float | `0.5` | 0-1, lower = more expressive |
| `similarity_boost` | float | `0.8` | 0-1, higher = closer to original voice |
| `speed` | float | `1.0` | 0.7-1.2, speech speed multiplier |
| `optimize_streaming_latency` | int | - | 0-4, higher = faster but lower quality |
| `expressive_mode` | bool | `true` | Enable expressive voice generation |
| `agent_output_audio_format` | string | - | Output audio codec format |
| `pronunciation_dictionary_locators` | array | - | Pronunciation overrides |
**Available TTS models for agents:**
| Model ID | Languages | Latency |
|----------|-----------|---------|
| `eleven_flash_v2_5` | 32 | ~75ms (recommended) |
| `eleven_flash_v2` | English | ~75ms |
| `eleven_turbo_v2_5` | 32 | ~250-300ms |
| `eleven_turbo_v2` | English | ~250-300ms |
| `eleven_multilingual_v2` | 29 | Standard |
| `eleven_v3_conversational` | 70+ | Standard |
### asr (Automatic Speech Recognition)
```python
conversation_config={
"asr": {
"quality": "high",
"keywords": ["ElevenLabs", "TechCorp"],
"user_input_audio_format": "pcm_16000"
}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `quality` | string | `"high"` | Transcription quality level |
| `provider` | string | `"elevenlabs"` | ASR provider (`elevenlabs` or `scribe_realtime`) |
| `keywords` | array | - | Words to boost recognition accuracy |
| `user_input_audio_format` | string | - | Input audio format (e.g., `pcm_16000`, `ulaw_8000`) |
### turn (Turn-Taking)
```python
conversation_config={
"turn": {
"turn_timeout": 7,
"turn_eagerness": "normal",
"silence_end_call_timeout": -1
}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `turn_timeout` | number | `7` | Seconds to wait before re-engaging the user |
| `turn_eagerness` | string | `"normal"` | How quickly agent responds: `patient`, `normal`, or `eager` |
| `silence_end_call_timeout` | number | `-1` | Seconds of silence before ending call (-1 = disabled) |
| `initial_wait_time` | number | - | Seconds to wait for user to start speaking |
| `spelling_patience` | string | `"auto"` | Entity detection patience: `auto` or `off` |
| `speculative_turn` | bool | `false` | Enable speculative turn detection |
| `soft_timeout_config` | object | - | Configures a message if user is silent (see below) |
**soft_timeout_config:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `timeout_seconds` | number | `-1` | Seconds before soft timeout (-1 = disabled) |
| `message` | string | `"Hhmmmm...yeah."` | What agent says on timeout |
| `use_llm_generated_message` | bool | `false` | Let LLM generate the timeout message |
## prompt (nested in conversation_config.agent)
Configures the LLM behavior. This object lives at `conversation_config.agent.prompt`:
```python
conversation_config={
"agent": {
"prompt": {
"prompt": "You are a helpful customer service agent...",
"llm": "gemini-2.0-flash",
"temperature": 0.7,
"max_tokens": 500,
"tools": [...],
"built_in_tools": {...},
"knowledge_base": [...]
}
}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `prompt` | string | `""` | System prompt defining agent behavior |
| `llm` | string | - | Model ID (see LLM providers below) |
| `temperature` | float | `0` | 0-1, higher = more creative |
| `max_tokens` | int | `-1` | Max tokens for LLM response (-1 = unlimited) |
| `reasoning_effort` | string | - | Reasoning depth: `none`, `minimal`, `low`, `medium`, `high` (model-dependent) |
| `thinking_budget` | int | - | Max thinking tokens for reasoning models |
| `tools` | array | - | Webhook and client tool definitions |
| `built_in_tools` | object | - | System tools (end_call, transfer, etc.) |
| `tool_ids` | array | - | References to pre-configured tools |
| `knowledge_base` | array | - | Documents for RAG |
| `custom_llm` | object | - | Custom LLM endpoint config |
| `timezone` | string | - | IANA timezone (e.g., `America/New_York`) |
| `backup_llm_config` | object | - | Fallback LLM configuration |
| `cascade_timeout_seconds` | number | `8` | Seconds before cascading to backup LLM (2-15) |
| `mcp_server_ids` | array | - | MCP server IDs to connect |
| `native_mcp_server_ids` | array | - | Native MCP server IDs |
| `ignore_default_personality` | bool | - | Skip default personality instructions |
Workspace environment variables let one agent configuration span multiple deployments. Use
`{{system_env__label}}` in server tool and MCP server URLs, `{ "env_var_label": "orders_api_key" }`
for secret-backed tool headers, and `{ "env_var_label": "orders_oauth" }` in `auth_connection`
to resolve per-environment auth connections at runtime.
### LLM Providers
| Provider | Model IDs |
|----------|-----------|
| OpenAI | `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo` |
| Anthropic | `claude-sonnet-4-6`, `claude-sonnet-4-5`, `claude-sonnet-4`, `claude-haiku-4-5`, `claude-3-7-sonnet`, `claude-3-5-sonnet`, `claude-3-haiku` |
| Google | `gemini-3.1-flash-lite-preview`, `gemini-3-pro-preview`, `gemini-3-flash-preview`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-2.0-flash`, `gemini-2.0-flash-lite` |
| ElevenLabs | `glm-45-air-fp8`, `qwen3-30b-a3b`, `gpt-oss-120b` (hosted, ultra-low latency) |
| Custom | `custom-llm` (requires custom_llm config) |
Use `GET /v1/convai/llm/list` to inspect the current model catalog, including deprecation state, token/context limits, and capability flags such as image-input support.
### Custom LLM
The `custom_llm` field is nested inside `conversation_config.agent.prompt`:
```python
conversation_config={
"agent": {
"prompt": {
"prompt": "You are helpful.",
"llm": "custom-llm",
"custom_llm": {
"url": "https://your-llm-endpoint.com/v1/chat/completions",
"model_id": "your-model-id",
"api_key": {"secret_id": "your-secret-id"},
"api_type": "chat_completions" # or "responses"
}
}
}
}
```
## platform_settings
Platform-level configuration for security, limits, summaries, and widget behavior.
```python
platform_settings={
"summary_language": "en",
"widget": {
"show_agent_status": True,
"show_conversation_id": True
},
"auth": {
"enable_auth": True,
"allowlist": [{"hostname": "example.com"}]
},
"call_limits": {
"agent_concurrency_limit": 10,
"daily_limit": 100
}
}
```
### Top-Level Fields
| Field | Type | Description |
|-------|------|-------------|
| `summary_language` | string | Language for conversation analysis outputs such as summaries, titles, evaluation rationales, and data collection rationales. If omitted, ElevenLabs infers it from the conversation. |
| `widget` | object | Hosted widget and shareable page configuration. See the widget table below for selected options. |
| `auth` | object | Authentication and origin restrictions for agent access |
| `call_limits` | object | Concurrency and daily usage limits |
| `guardrails` | object | Built-in safety and policy controls for agent interactions |
| `privacy` | object | Recording, retention, and conversation history redaction settings |
### auth
| Field | Type | Description |
|-------|------|-------------|
| `enable_auth` | bool | Require signed URLs/tokens for connections |
| `allowlist` | array | Allowed origins for CORS |
| `shareable_token` | string | Public conversation token |
### call_limits
| Field | Type | Description |
|-------|------|-------------|
| `agent_concurrency_limit` | int | Max simultaneous conversations (default: -1, unlimited) |
| `daily_limit` | int | Max conversations per day (default: 100000) |
| `bursting_enabled` | bool | Allow exceeding limits at 2x cost (default: true) |
### guardrails
Use `platform_settings.guardrails` to configure built-in safety controls for user input and agent behavior. The fields below cover the current schema additions that are most relevant in agent configs.
| Field | Type | Description |
|-------|------|-------------|
| `version` | string | Guardrail config version. Use `"1"` for the current schema. |
| `focus` | object | Keeps the agent on-topic and aligned with the configured task. |
| `prompt_injection` | object | Detects prompt injection and instruction override attempts. |
| `custom` | object | Configures user-defined response validation guardrails. |
| `content` | object | Configures category-specific content moderation guardrails. |
**focus / prompt_injection:**
| Field | Type | Description |
|-------|------|-------------|
| `is_enabled` | bool | Enables the guardrail. |
**content:**
| Field | Type | Description |
|-------|------|-------------|
| `execution_mode` | string | Guardrail execution mode: `streaming` or `blocking`. |
| `config` | object | Category threshold settings for content moderation. |
**content.config:**
| Field | Type | Description |
|-------|------|-------------|
| `sexual` | object | Threshold settings for sexual content. |
| `violence` | object | Threshold settings for violent content. |
| `harassment` | object | Threshold settings for harassment. |
| `self_harm` | object | Threshold settings for self-harm content. |
| `profanity` | object | Threshold settings for profanity. |
| `religion_or_politics` | object | Threshold settings for religion or politics content. |
| `medical_and_legal_information` | object | Threshold settings for medical or legal information. |
**content.config.\<category\>:**
| Field | Type | Description |
|-------|------|-------------|
| `is_enabled` | bool | Enables moderation for the category. |
| `threshold` | number or string | Category threshold as a numeric score or one of `low`, `medium`, or `high`. |
Blocking content guardrails and custom guardrails support a `trigger_action` that either ends
the session immediately or retries the response. Retry removes the blocked reply, injects your
feedback as a system message, and re-generates up to 3 times before the platform falls back to
ending the session. Feedback templates can use `{{trigger_reason}}` and `{{agent_message}}`.
### privacy
Use `platform_settings.privacy` to control recording, retention, and redaction behavior. The redaction-specific field is:
| Field | Type | Description |
|-------|------|-------------|
| `conversation_history_redaction` | object | Redacts configured entity types from stored transcripts, audio, and analysis. |
**conversation_history_redaction:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | bool | `false` | Whether conversation history redaction is enabled |
| `entities` | array | - | Entity types to redact. Use parent types such as `name` or specific values such as `name.name_given`, `email_address`, `contact_number`, `dob`, and `age`. |
### widget
Use `platform_settings.widget` to configure the hosted widget and shareable page defaults. For client-side embed attributes, see the widget embedding reference.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `dismissible` | bool | `false` | Whether the widget can be dismissed by the user |
| `show_agent_status` | bool | `false` | Whether to show working, done, or error status while tools are running |
| `show_conversation_id` | bool | `true` | Whether to show the conversation ID after disconnection |
| `strip_audio_tags` | bool | `true` | Whether to strip audio markup from messages |
| `syntax_highlight_theme` | string | auto | Code block syntax highlighting theme (`light` or `dark`); omit it to let the widget auto-detect |
### conversation (inside conversation_config)
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `max_duration_seconds` | int | `600` | Max conversation duration |
| `text_only` | bool | `false` | Text-only mode (avoids audio pricing) |
| `monitoring_enabled` | bool | `false` | Enable real-time WebSocket monitoring |
## Additional Top-Level Fields
| Field | Type | Description |
|-------|------|-------------|
| `tags` | array | Classification labels for filtering (e.g., `["production"]`, `["test"]`) |
| `workflow` | object | Conversation flow definition and tool interaction sequences |
## Knowledge Base / RAG
Knowledge base is configured inside `conversation_config.agent.prompt`:
```python
agent = client.conversational_ai.agents.create(
name="Support Agent",
conversation_config={
"agent": {
"prompt": {
"prompt": "You are a support agent. Use the knowledge base to answer questions.",
"llm": "gemini-2.0-flash",
"knowledge_base": [
{"type": "file", "id": "doc-id", "name": "Product Guide", "usage_mode": "auto"}
],
"rag": {
"enabled": True,
"embedding_model": "qwen3_embedding_4b",
"max_documents_length": 50000,
"max_retrieved_rag_chunks_count": 20
}
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
`rag.embedding_model` supports `e5_mistral_7b_instruct`, `multilingual_e5_large_instruct`, and `qwen3_embedding_4b`.
## CRUD Operations
### Using CLI (Recommended)
```bash
# Initialize project
elevenlabs agents init
# Create agent from template
elevenlabs agents add "My Agent" --template complete
elevenlabs agents add "Support Bot" --template customer-service
# List agents
elevenlabs agents list
# Check status
elevenlabs agents status
# Push local changes to platform
elevenlabs agents push
elevenlabs agents push --dry-run # Preview changes first
# Import agents from platform
elevenlabs agents pull # Import all
elevenlabs agents pull --agent <agent-id> # Import specific agent
elevenlabs agents pull --update # Override local configs
# View available templates
elevenlabs agents templates list
elevenlabs agents templates show <template-name>
# Add tools
elevenlabs tools add-webhook "API Tool"
elevenlabs tools add-client "UI Tool"
# Generate widget code
elevenlabs agents widget <agent-id>
```
### SDK: List Agents
```python
agents = client.conversational_ai.agents.list()
for agent in agents.agents:
print(f"{agent.name}: {agent.agent_id}")
```
```javascript
const agents = await client.conversationalAi.agents.list();
```
```bash
curl -X GET "https://api.elevenlabs.io/v1/convai/agents" -H "xi-api-key: $ELEVENLABS_API_KEY"
```
### SDK: Get Agent
```python
agent = client.conversational_ai.agents.get(agent_id="your-agent-id")
```
```javascript
const agent = await client.conversationalAi.agents.get("your-agent-id");
```
```bash
curl -X GET "https://api.elevenlabs.io/v1/convai/agents/your-agent-id" -H "xi-api-key: $ELEVENLABS_API_KEY"
```
### SDK: Update Agent
Only include fields you want to change. All other settings remain unchanged.
**Python:**
```python
# Update name
client.conversational_ai.agents.update(agent_id="id", name="New Name")
# Update TTS voice
client.conversational_ai.agents.update(agent_id="id", conversation_config={
"tts": {"voice_id": "EXAVITQu4vr4xnSDxMaL", "model_id": "eleven_flash_v2_5"}
})
# Update prompt/LLM (nested in agent)
client.conversational_ai.agents.update(agent_id="id", conversation_config={
"agent": {"prompt": {"prompt": "New instructions.", "llm": "claude-sonnet-4", "temperature": 0.8}}
})
# Update first message
client.conversational_ai.agents.update(agent_id="id", conversation_config={
"agent": {"first_message": "Welcome back!"}
})
# Update platform settings
client.conversational_ai.agents.update(agent_id="id", platform_settings={
"auth": {"enable_auth": True, "allowlist": [{"hostname": "myapp.com"}]}
})
```
**JavaScript:**
```javascript
await client.conversationalAi.agents.update("id", { name: "New Name" });
await client.conversationalAi.agents.update("id", {
conversationConfig: { tts: { voiceId: "EXAVITQu4vr4xnSDxMaL" } }
});
await client.conversationalAi.agents.update("id", {
conversationConfig: { agent: { prompt: { prompt: "New instructions.", llm: "claude-sonnet-4" } } }
});
```
**cURL:**
```bash
curl -X PATCH "https://api.elevenlabs.io/v1/convai/agents/your-agent-id" \
-H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "New Name"}'
```
#### Updatable Fields
| Section | Fields |
|---------|--------|
| Root | `name`, `tags` |
| `conversation_config.agent` | `first_message`, `language`, `disable_first_message_interruptions`, `dynamic_variables` |
| `conversation_config.agent.prompt` | `prompt`, `llm`, `temperature`, `max_tokens`, `reasoning_effort`, `tools`, `built_in_tools`, `knowledge_base`, `custom_llm`, `timezone` |
| `conversation_config.tts` | `voice_id`, `model_id`, `stability`, `similarity_boost`, `speed`, `optimize_streaming_latency`, `expressive_mode` |
| `conversation_config.asr` | `quality`, `provider`, `keywords`, `user_input_audio_format` |
| `conversation_config.turn` | `turn_timeout`, `turn_eagerness`, `silence_end_call_timeout`, `soft_timeout_config` |
| `conversation_config.conversation` | `max_duration_seconds`, `text_only`, `monitoring_enabled` |
| `platform_settings` | `summary_language`, `guardrails`, `privacy` |
| `platform_settings.widget` | `dismissible`, `show_agent_status`, `show_conversation_id`, `strip_audio_tags`, `syntax_highlight_theme` |
| `platform_settings.auth` | `enable_auth`, `allowlist` |
| `platform_settings.call_limits` | `agent_concurrency_limit`, `daily_limit`, `bursting_enabled` |
### SDK: Delete Agent
```python
client.conversational_ai.agents.delete(agent_id="your-agent-id")
```
```javascript
await client.conversationalAi.agents.delete("your-agent-id");
```
```bash
curl -X DELETE "https://api.elevenlabs.io/v1/convai/agents/your-agent-id" -H "xi-api-key: $ELEVENLABS_API_KEY"
```
## CI/CD Integration
Use the CLI in your deployment pipeline:
```bash
# Set API key as environment variable
export ELEVENLABS_API_KEY="your-api-key"
# Push changes (non-interactive)
elevenlabs agents push
```
## Example Configurations
### Customer Support Agent
```python
agent = client.conversational_ai.agents.create(
name="Support Agent",
conversation_config={
"agent": {
"first_message": "Hi! Thanks for calling TechCorp support.",
"language": "en",
"prompt": {
"prompt": "You are a customer support agent. Be helpful, professional, concise.",
"llm": "gemini-2.0-flash",
"temperature": 0.5,
"built_in_tools": {
"end_call": {},
"transfer_to_number": {
"transfers": [{"transfer_destination": {"type": "phone", "phone_number": "+1234567890"}, "condition": "User asks for human support"}]
}
}
}
},
"tts": {"voice_id": "XB0fDUnXU5powFXDhCwa", "model_id": "eleven_flash_v2_5"},
"turn": {"turn_eagerness": "normal", "turn_timeout": 7},
"conversation": {"max_duration_seconds": 900}
}
)
```
### Low-Latency Assistant
```python
agent = client.conversational_ai.agents.create(
name="Quick Assistant",
conversation_config={
"agent": {
"first_message": "Hey! What do you need?",
"prompt": {
"prompt": "Fast, efficient assistant. Brief answers.",
"llm": "gemini-2.0-flash",
"temperature": 0.3,
"max_tokens": 100
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb", "model_id": "eleven_flash_v2_5", "optimize_streaming_latency": 4},
"turn": {"turn_eagerness": "eager", "turn_timeout": 3}
}
)
```
@@ -0,0 +1,545 @@
# Client Tools
Extend your agent with custom capabilities. Tools let the agent take actions beyond just talking.
## Tool Types
| Type | Execution | Use Case |
|------|-----------|----------|
| **Webhook** | Server-side via HTTP | Database queries, API calls, secure operations |
| **Client** | Browser-side JavaScript | UI updates, local storage, navigation |
| **System** | Built-in ElevenLabs | End call, transfer, standard actions |
## Where Tools Live
Tools are defined inside `conversation_config.agent.prompt`. Webhook and client tools go in the `tools` array. System tools go in `built_in_tools`:
```python
conversation_config={
"agent": {
"prompt": {
"prompt": "You are helpful.",
"llm": "gemini-2.0-flash",
"tools": [...], # Webhook and client tools
"built_in_tools": {...} # System tools (end_call, transfer, etc.)
}
}
}
```
## Webhook Tools
Execute server-side logic when the agent needs external data or actions.
### Basic Webhook
```python
agent = client.conversational_ai.agents.create(
name="Weather Assistant",
conversation_config={
"agent": {
"prompt": {
"prompt": "You are a helpful assistant that can check the weather.",
"llm": "gemini-2.0-flash",
"tools": [{
"type": "webhook",
"name": "get_weather",
"description": "Get current weather for a city. Use when user asks about weather.",
"api_schema": {
"url": "https://api.example.com/weather",
"method": "POST",
"request_headers": {
"Authorization": "Bearer {{API_KEY}}"
},
"request_body_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g., 'San Francisco'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}
}
}]
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
### Webhook Request Format
When the agent calls a webhook tool, ElevenLabs sends:
```json
{
"tool_call_id": "call_abc123",
"tool_name": "get_weather",
"parameters": {
"city": "San Francisco",
"units": "fahrenheit"
},
"conversation_id": "conv_xyz789"
}
```
### Webhook Response Format
Your server should respond with:
```json
{
"result": "The weather in San Francisco is 68°F and sunny."
}
```
Or for structured data:
```json
{
"result": {
"temperature": 68,
"condition": "sunny",
"humidity": 45
}
}
```
### Webhook with Authentication
```python
# Inside conversation_config.agent.prompt.tools:
{
"type": "webhook",
"name": "lookup_order",
"description": "Look up order status by order ID",
"response_timeout_secs": 10,
"api_schema": {
"url": "https://api.mystore.com/orders/lookup",
"method": "POST",
"request_headers": {
"Authorization": "Bearer {{ORDER_API_KEY}}",
"X-Store-ID": "store_123"
},
"request_body_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order ID (e.g., ORD-12345)"
}
},
"required": ["order_id"]
}
}
}
```
Use workspace environment variables to keep a single server tool configuration working across
staging and production. `{{system_env__label}}` works in server tool URLs, secret environment
variables can populate `request_headers`, and auth-connection environment variables can populate
`api_schema.auth_connection`. The same environment-variable resolution model also applies to MCP
server connections.
```json
{
"api_schema": {
"url": "https://{{system_env__api_host}}.example.com/orders",
"method": "GET",
"request_headers": {
"X-Api-Key": { "env_var_label": "orders_api_key" }
},
"auth_connection": { "env_var_label": "orders_oauth" }
}
}
```
Workspace auth connections support OAuth2 client credentials, OAuth2 JWT, private key JWT,
basic auth, bearer auth, and custom header auth.
System dynamic variables are also available in tool parameters and headers. Use
`{{system__conversation_history}}` when a webhook or sub-agent needs the full conversation
context as a lazily evaluated JSON history object with user, agent, and tool entries.
### Webhook Tool Options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `response_timeout_secs` | int | `20` | Timeout in seconds (5-120) |
| `disable_interruptions` | bool | `false` | Prevent user interruptions during tool execution |
| `execution_mode` | string | `"immediate"` | `immediate`, `post_tool_speech`, or `async` |
| `tool_call_sound` | string | - | Sound during execution: `typing`, `elevator1`-`elevator4` |
| `force_pre_tool_speech` | bool | `false` | Force agent to speak before executing tool |
| `tool_error_handling_mode` | string | `"auto"` | `auto`, `summarized`, `passthrough`, or `hide` |
**Note:** The default `api_schema.method` is `GET`. Always set `"method": "POST"` explicitly for webhook tools that send request bodies.
### Server Implementation (Node.js)
```javascript
app.post("/webhook/get_weather", async (req, res) => {
const { parameters, conversation_id } = req.body;
const { city, units = "fahrenheit" } = parameters;
// Fetch weather from your data source
const weather = await weatherService.get(city, units);
res.json({
result: `It's ${weather.temp}°${units === "celsius" ? "C" : "F"} and ${weather.condition} in ${city}.`,
});
});
```
### Server Implementation (Python)
```python
@app.post("/webhook/get_weather")
async def get_weather(request: Request):
data = await request.json()
city = data["parameters"]["city"]
units = data["parameters"].get("units", "fahrenheit")
# Fetch weather from your data source
weather = weather_service.get(city, units)
return {
"result": f"It's {weather['temp']}°{'C' if units == 'celsius' else 'F'} and {weather['condition']} in {city}."
}
```
## Client Tools
Execute JavaScript in the user's browser. Useful for UI updates, navigation, or accessing browser APIs.
### Defining Client Tools
Client tools are registered when starting a conversation:
```javascript
import { Conversation } from "@elevenlabs/client";
const conversation = await Conversation.startSession({
agentId: "your-agent-id",
clientTools: {
show_product: async ({ productId }) => {
// Update UI to show product
const modal = document.getElementById("product-modal");
modal.innerHTML = await fetchProductCard(productId);
modal.showModal();
return { success: true, message: "Showing product" };
},
navigate_to: async ({ page }) => {
// Navigate to a page
window.location.href = `/${page}`;
return { success: true };
},
save_preference: async ({ key, value }) => {
// Store in localStorage
localStorage.setItem(key, value);
return { saved: true };
},
},
});
```
### Registering Client Tools with Agent
Tell the agent about available client tools in `conversation_config.agent.prompt.tools`:
```python
agent = client.conversational_ai.agents.create(
name="Shopping Assistant",
conversation_config={
"agent": {
"prompt": {
"prompt": """You are a shopping assistant.
When users want to see a product, use show_product.
When users want to go somewhere, use navigate_to.""",
"llm": "gemini-2.0-flash",
"tools": [
{
"type": "client",
"name": "show_product",
"description": "Display a product card to the user",
"parameters": {
"type": "object",
"properties": {
"productId": {
"type": "string",
"description": "Product ID to display"
}
},
"required": ["productId"]
}
},
{
"type": "client",
"name": "navigate_to",
"description": "Navigate user to a different page",
"parameters": {
"type": "object",
"properties": {
"page": {
"type": "string",
"enum": ["cart", "checkout", "account", "home"],
"description": "Page to navigate to"
}
},
"required": ["page"]
}
}
]
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}
}
)
```
### Client Tool Options
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `expects_response` | bool | `false` | Whether the tool returns data to the agent |
### Client Tool Return Values
Return data that the agent can use in conversation:
```javascript
clientTools: {
check_cart: async () => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
return {
itemCount: cart.length,
total: cart.reduce((sum, item) => sum + item.price, 0),
items: cart.map((item) => item.name),
};
};
}
```
The agent receives this data and can say: "You have 3 items in your cart totaling $45.99."
## System Tools (built_in_tools)
Built-in tools provided by ElevenLabs. These are configured in `conversation_config.agent.prompt.built_in_tools` (not in the `tools` array):
```python
"built_in_tools": {
"end_call": {},
"transfer_to_number": {...},
"transfer_to_agent": {...},
"language_detection": {},
"skip_turn": {},
"voicemail_detection": {...},
"play_keypad_touch_tone": {}
}
```
Current API schemas also expose `agent_prompt_change`, `memory_entry_create`, `memory_entry_delete`, `memory_entry_search`, and `memory_entry_update` in `built_in_tools`.
### end_call
Ends the current conversation:
```python
"built_in_tools": {
"end_call": {}
}
```
The agent can say "Goodbye!" and then end the call programmatically.
### transfer_to_number
Transfer to a phone number (requires telephony integration):
```python
"built_in_tools": {
"transfer_to_number": {
"transfers": [{
"transfer_destination": {"type": "phone", "phone_number": "+1234567890"},
"condition": "User asks to speak with a human agent"
}]
}
}
```
### transfer_to_agent
Transfer to another ElevenLabs agent:
```python
"built_in_tools": {
"transfer_to_agent": {
"transfers": [{
"agent_id": "other-agent-id",
"condition": "User asks about sales"
}]
}
}
```
## Best Practices
### Tool Descriptions
Write clear descriptions so the LLM knows when to use tools:
```python
# Good - specific and actionable
"description": "Look up order status. Use when customer asks about their order, delivery, or shipping."
# Bad - vague
"description": "Order tool"
```
### Parameter Descriptions
Help the LLM extract correct values:
```python
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order ID in format ORD-XXXXX (e.g., ORD-12345)"
},
"email": {
"type": "string",
"description": "Customer email address for verification"
}
}
}
```
### Error Handling
Configure how tool errors are shared with the agent using `tool_error_handling_mode`:
| Mode | Behavior |
|------|----------|
| `auto` | ElevenLabs automatically decides how to handle errors |
| `summarized` | Errors are summarized before being sent to the agent |
| `passthrough` | Full error details are passed to the agent |
| `hide` | Errors are hidden from the agent |
Return helpful error messages:
```javascript
// Server webhook
app.post("/webhook/lookup_order", async (req, res) => {
const { order_id } = req.body.parameters;
const order = await db.orders.find(order_id);
if (!order) {
return res.json({
result: {
error: true,
message: `Order ${order_id} not found. Please verify the order ID.`,
},
});
}
res.json({ result: order });
});
```
### Timeouts
Set reasonable timeouts for webhooks using `response_timeout_secs` (5-120 seconds, default 20):
```python
{
"type": "webhook",
"name": "slow_operation",
"description": "Run a slow operation",
"response_timeout_secs": 30,
"api_schema": {
"url": "https://api.example.com/slow-operation",
"method": "POST"
}
}
```
## Complete Example
```python
agent = client.conversational_ai.agents.create(
name="E-commerce Assistant",
conversation_config={
"agent": {
"first_message": "Hi! How can I help you today?",
"language": "en",
"prompt": {
"prompt": """You are an e-commerce support assistant.
Available actions:
- lookup_order: Check order status
- show_product: Display products to customer
- end_call: End conversation politely
- transfer_to_number: Transfer to human support
Always verify order ID before lookup. Offer transfer for complex issues.""",
"llm": "gemini-2.0-flash",
"tools": [
# Webhook: Server-side order lookup
{
"type": "webhook",
"name": "lookup_order",
"description": "Look up order status by order ID or email",
"api_schema": {
"url": "https://api.mystore.com/orders/lookup",
"method": "POST",
"request_headers": {"Authorization": "Bearer {{API_KEY}}"},
"request_body_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"email": {"type": "string"}
}
}
}
},
# Client: Browser-side product display
{
"type": "client",
"name": "show_product",
"description": "Display product details to the customer",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"}
},
"required": ["product_id"]
}
}
],
"built_in_tools": {
"end_call": {},
"transfer_to_number": {
"transfers": [{
"transfer_destination": {"type": "phone", "phone_number": "+1234567890"},
"condition": "User asks for human support"
}]
}
}
}
},
"tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb", "model_id": "eleven_flash_v2_5"}
}
)
```
@@ -0,0 +1,131 @@
# Installation
## CLI (Recommended)
The ElevenLabs CLI is the recommended way to create and manage agents:
```bash
npm install -g @elevenlabs/cli
# or
pnpm add -g @elevenlabs/cli
# or
yarn global add @elevenlabs/cli
```
Requires Node.js 16.0.0 or higher.
### Authentication
```bash
elevenlabs auth login # Authenticate with API key
elevenlabs auth whoami # Verify current login status
elevenlabs auth logout # Remove stored credentials
```
API keys are securely stored in `~/.agents/api_keys.json`.
### Quick Start
```bash
# Initialize a new project
elevenlabs agents init
# Create an agent from template
elevenlabs agents add "My Assistant" --template complete
# Push to ElevenLabs platform
elevenlabs agents push
```
## JavaScript / TypeScript SDK
For programmatic access and client-side integration:
```bash
npm install @elevenlabs/elevenlabs-js
```
> **Important:** Always use `@elevenlabs/elevenlabs-js`. The old `elevenlabs` npm package (v1.x) is deprecated and should not be used.
```javascript
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
// Option 1: Environment variable (recommended)
// Set ELEVENLABS_API_KEY in your environment
const client = new ElevenLabsClient();
// Option 2: Pass directly
const client = new ElevenLabsClient({ apiKey: "your-api-key" });
```
### Migrating from deprecated packages
If you have old packages installed, remove them:
```bash
# Remove deprecated packages
npm uninstall elevenlabs
# Install the current packages
npm install @elevenlabs/elevenlabs-js
# For client-side/browser usage, also install:
npm install @elevenlabs/client # Browser client
npm install @elevenlabs/react # React hooks
```
**Import changes:**
```javascript
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { Conversation } from "@elevenlabs/client";
import { useConversation } from "@elevenlabs/react";
```
## Python
```bash
pip install elevenlabs
```
```python
from elevenlabs import ElevenLabs
# Option 1: Environment variable (recommended)
# Set ELEVENLABS_API_KEY in your environment
client = ElevenLabs()
# Option 2: Pass directly
client = ElevenLabs(api_key="your-api-key")
```
## cURL / REST API
Set your API key as an environment variable:
```bash
export ELEVENLABS_API_KEY="your-api-key"
```
Include in requests via the `xi-api-key` header:
```bash
curl -X POST "https://api.elevenlabs.io/v1/convai/agents/create" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Agent", "conversation_config": {"agent": {"prompt": {"prompt": "You are helpful.", "llm": "gemini-2.0-flash"}}, "tts": {"voice_id": "JBFqnCBsd6RMkjVDRZzb"}}}'
```
## Getting an API Key
1. Sign up at [elevenlabs.io](https://elevenlabs.io)
2. Go to [API Keys](https://elevenlabs.io/app/settings/api-keys)
3. Click **Create API Key**
4. Copy and store securely
Or use the `setup-api-key` skill for guided setup.
## Environment Variables
| Variable | Description |
|----------|-------------|
| `ELEVENLABS_API_KEY` | Your ElevenLabs API key (required) |
@@ -0,0 +1,170 @@
# Outbound Calls
Make outbound phone calls using your ElevenLabs agent via Twilio integration.
## Prerequisites
1. A configured ElevenLabs agent
2. A Twilio phone number linked to your agent (obtain `agent_phone_number_id` from ElevenLabs dashboard)
3. Your ElevenLabs API key
## Basic Usage
See the [main agents skill](../SKILL.md#outbound-calls) for basic Python, JavaScript, and cURL examples.
## Request Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `agent_id` | string | Yes | The ID of your ElevenLabs agent |
| `agent_phone_number_id` | string | Yes | The ID of the Twilio phone number linked to your agent |
| `to_number` | string | Yes | The destination phone number (E.164 format) |
| `conversation_initiation_client_data` | object | No | Override conversation settings for this call |
| `call_recording_enabled` | boolean | No | Whether to let Twilio record the call |
| `telephony_call_config` | object | No | Telephony call settings like ringing timeout |
## Response
```json
{
"success": true,
"message": "Call initiated successfully",
"conversation_id": "conv_abc123",
"callSid": "CA1234567890abcdef"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Whether the call was initiated successfully |
| `message` | string | Status message |
| `conversation_id` | string | ElevenLabs conversation ID for tracking |
| `callSid` | string | Twilio Call SID for reference |
## Customizing the Call
Override agent settings for a specific call using `conversation_initiation_client_data`:
### Python
```python
response = client.conversational_ai.twilio.outbound_call(
agent_id="your-agent-id",
agent_phone_number_id="your-phone-number-id",
to_number="+1234567890",
call_recording_enabled=True,
conversation_initiation_client_data={
"conversation_config_override": {
"agent": {
"first_message": "Hello! This is a reminder about your appointment tomorrow.",
"language": "en"
},
"tts": {
"voice_id": "JBFqnCBsd6RMkjVDRZzb"
}
},
"dynamic_variables": {
"customer_name": "John",
"appointment_time": "2:00 PM"
}
}
)
```
### JavaScript
```javascript
const response = await client.conversationalAi.twilio.outboundCall({
agentId: "your-agent-id",
agentPhoneNumberId: "your-phone-number-id",
toNumber: "+1234567890",
callRecordingEnabled: true,
conversationInitiationClientData: {
conversationConfigOverride: {
agent: {
firstMessage: "Hello! This is a reminder about your appointment tomorrow.",
language: "en",
},
tts: {
voiceId: "JBFqnCBsd6RMkjVDRZzb",
},
},
dynamicVariables: {
customer_name: "John",
appointment_time: "2:00 PM",
},
},
});
```
## Configuration Overrides
### Agent Settings
| Option | Type | Description |
|--------|------|-------------|
| `first_message` | string | Custom greeting for this call |
| `language` | string | Language code (e.g., "en", "es", "fr") |
| `prompt` | object | Override agent prompt and LLM settings |
### TTS Settings
| Option | Type | Description |
|--------|------|-------------|
| `voice_id` | string | Voice ID to use for this call |
| `stability` | number | Voice stability (0.0-1.0) |
| `similarity_boost` | number | Voice similarity boost (0.0-1.0) |
| `speed` | number | Speech speed multiplier |
### Telephony Call Configuration
| Option | Type | Description |
|--------|------|-------------|
| `ringing_timeout_secs` | integer | How long to ring the recipient before giving up (default: `60`) |
### Dynamic Variables
Pass custom data to your agent's prompt using `dynamic_variables`. Reference them in your agent's prompt with `{{variable_name}}` syntax.
When assigning dynamic variables, you can use the `sanitize` option to remove sensitive values from tool responses before they are sent to the LLM and transcript, while still allowing variable assignment:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `sanitize` | boolean | `false` | If true, the assignment's value is removed from tool responses before sending to LLM/transcript but still processed for variable assignment |
## Complete Example
```python
from elevenlabs import ElevenLabs
client = ElevenLabs()
# Make personalized outbound calls
customers = [
{"name": "Alice", "phone": "+1234567890", "balance": "$150.00"},
{"name": "Bob", "phone": "+0987654321", "balance": "$75.50"},
]
for customer in customers:
try:
response = client.conversational_ai.twilio.outbound_call(
agent_id="payment-reminder-agent",
agent_phone_number_id="your-phone-number-id",
to_number=customer["phone"],
call_recording_enabled=True,
conversation_initiation_client_data={
"conversation_config_override": {
"agent": {
"first_message": f"Hello {customer['name']}, this is a friendly reminder about your account."
}
},
"dynamic_variables": {
"customer_name": customer["name"],
"balance": customer["balance"]
}
}
)
print(f"Called {customer['name']}: {response.conversation_id}")
except Exception as e:
print(f"Failed to call {customer['name']}: {e}")
```
@@ -0,0 +1,365 @@
# Widget Embedding
Add a voice AI agent to any website with the ElevenLabs conversation widget.
## Basic Embed
```html
<elevenlabs-convai agent-id="your-agent-id"></elevenlabs-convai>
<script src="https://unpkg.com/@elevenlabs/convai-widget-embed" async type="text/javascript"></script>
```
This creates a floating button that users can click to start a voice conversation.
> **Note:** Widgets currently require public agents with authentication disabled. For authenticated flows, use the SDKs.
## Widget Attributes
### Required
| Attribute | Description |
|-----------|-------------|
| `agent-id` | Your ElevenLabs agent ID |
| `signed-url` | Alternative to `agent-id` when using signed URLs |
### Appearance
| Attribute | Description | Default |
|-----------|-------------|---------|
| `avatar-image-url` | URL for agent avatar image | ElevenLabs logo |
| `avatar-orb-color-1` | Primary orb gradient color | `#2792dc` |
| `avatar-orb-color-2` | Secondary orb gradient color | `#9ce6e6` |
### Text Labels
| Attribute | Description | Default |
|-----------|-------------|---------|
| `action-text` | Tooltip when hovering | "Talk to AI" |
| `start-call-text` | Button to start call | "Start call" |
| `end-call-text` | Button to end call | "End call" |
| `expand-text` | Expand chat button | "Open" |
| `collapse-text` | Collapse chat button | "Close" |
| `listening-text` | Listening state label | "Listening..." |
| `speaking-text` | Speaking state label | "Assistant speaking" |
### Behavior
| Attribute | Description | Default |
|-----------|-------------|---------|
| `variant` | Widget style: `compact` or `expanded` | `compact` |
| `server-location` | Server region (`us`, `eu-residency`, `in-residency`, `global`) | `us` |
| `dismissible` | Allow the user to minimize the widget | `false` |
| `disable-banner` | Hide "Powered by ElevenLabs" | `false` |
## Examples
### Custom Avatar
```html
<elevenlabs-convai
agent-id="your-agent-id"
avatar-image-url="https://example.com/your-avatar.png"
></elevenlabs-convai>
```
### Custom Colors
```html
<elevenlabs-convai
agent-id="your-agent-id"
avatar-orb-color-1="#ff6b6b"
avatar-orb-color-2="#ffd93d"
></elevenlabs-convai>
```
### Custom Text
```html
<elevenlabs-convai
agent-id="your-agent-id"
action-text="Chat with our AI assistant"
start-call-text="Begin conversation"
end-call-text="Hang up"
></elevenlabs-convai>
```
### Expanded Variant
```html
<elevenlabs-convai
agent-id="your-agent-id"
variant="expanded"
></elevenlabs-convai>
```
### Full Customization
```html
<elevenlabs-convai
agent-id="your-agent-id"
avatar-image-url="https://example.com/support-agent.png"
avatar-orb-color-1="#4f46e5"
avatar-orb-color-2="#818cf8"
action-text="Talk to Support"
start-call-text="Start voice chat"
end-call-text="End conversation"
expand-text="Open assistant"
collapse-text="Minimize"
></elevenlabs-convai>
```
## CSS Customization
The widget uses Shadow DOM but exposes CSS custom properties:
```css
elevenlabs-convai {
--elevenlabs-convai-widget-width: 400px;
--elevenlabs-convai-widget-height: 600px;
}
```
### Positioning
By default, the widget appears in the bottom-right corner. Override with CSS:
```css
elevenlabs-convai {
position: fixed;
bottom: 20px;
right: 20px;
/* Or position differently */
left: 20px;
right: auto;
}
```
### Z-Index
```css
elevenlabs-convai {
z-index: 9999;
}
```
## JavaScript Control
Access the widget element to control it programmatically:
```html
<elevenlabs-convai id="my-widget" agent-id="your-agent-id"></elevenlabs-convai>
<script>
const widget = document.getElementById("my-widget");
// Start a conversation
widget.startConversation();
// End the conversation
widget.endConversation();
// Listen for events
widget.addEventListener("conversationStarted", () => {
console.log("Conversation started");
});
widget.addEventListener("conversationEnded", () => {
console.log("Conversation ended");
});
</script>
```
### Custom Trigger Button
Hide the default widget and use your own button:
```html
<style>
elevenlabs-convai {
display: none;
}
</style>
<button onclick="document.getElementById('widget').startConversation()">
Talk to AI
</button>
<elevenlabs-convai id="widget" agent-id="your-agent-id"></elevenlabs-convai>
```
## Authentication
For agents with authentication enabled, pass a signed URL:
```html
<elevenlabs-convai id="widget" agent-id="your-agent-id"></elevenlabs-convai>
<script>
async function startAuthenticatedConversation() {
// Get signed URL from your backend
const response = await fetch("/api/get-signed-url");
const { signedUrl } = await response.json();
const widget = document.getElementById("widget");
widget.setAttribute("signed-url", signedUrl);
widget.startConversation();
}
</script>
```
Your backend:
```python
@app.get("/api/get-signed-url")
def get_signed_url():
signed_url = client.conversational_ai.conversations.get_signed_url(
agent_id="your-agent-id"
)
return {"signedUrl": signed_url.signed_url}
```
## Mobile Considerations
### Responsive Positioning
```css
/* Desktop: bottom-right */
elevenlabs-convai {
position: fixed;
bottom: 20px;
right: 20px;
}
/* Mobile: full-width bottom */
@media (max-width: 768px) {
elevenlabs-convai {
bottom: 0;
right: 0;
left: 0;
--elevenlabs-convai-widget-width: 100%;
}
}
```
### Touch-Friendly
The widget is touch-optimized by default. For better mobile UX:
```css
@media (max-width: 768px) {
elevenlabs-convai {
/* Larger touch target */
transform: scale(1.1);
transform-origin: bottom right;
}
}
```
## Multiple Widgets
You can have multiple widgets for different agents:
```html
<elevenlabs-convai
agent-id="support-agent-id"
action-text="Support"
style="right: 20px"
></elevenlabs-convai>
<elevenlabs-convai
agent-id="sales-agent-id"
action-text="Sales"
style="right: 100px"
></elevenlabs-convai>
```
## Framework Integration
### React
```jsx
function App() {
useEffect(() => {
// Load widget script
const script = document.createElement("script");
script.src = "https://unpkg.com/@elevenlabs/convai-widget-embed";
script.async = true;
document.body.appendChild(script);
return () => document.body.removeChild(script);
}, []);
return (
<div>
<elevenlabs-convai agent-id="your-agent-id"></elevenlabs-convai>
</div>
);
}
```
### Vue
```vue
<template>
<div>
<elevenlabs-convai agent-id="your-agent-id"></elevenlabs-convai>
</div>
</template>
<script setup>
import { onMounted } from "vue";
onMounted(() => {
const script = document.createElement("script");
script.src = "https://unpkg.com/@elevenlabs/convai-widget-embed";
script.async = true;
document.body.appendChild(script);
});
</script>
```
### Next.js
```jsx
import Script from "next/script";
export default function Page() {
return (
<>
<Script
src="https://unpkg.com/@elevenlabs/convai-widget-embed"
strategy="lazyOnload"
/>
<elevenlabs-convai agent-id="your-agent-id"></elevenlabs-convai>
</>
);
}
```
## Troubleshooting
### Widget Not Appearing
1. Check that the agent ID is correct
2. Verify the script is loaded (check Network tab)
3. Check for JavaScript errors in console
4. Ensure no CSS is hiding the widget
### Audio Issues
1. Ensure HTTPS (microphone requires secure context)
2. Check browser permissions for microphone
3. Test in a supported browser (Chrome, Firefox, Safari, Edge)
### CORS Errors
If using authentication, ensure your domain is in the agent's allowlist:
```python
platform_settings={
"auth": {
"enable_auth": True,
"allowlist": ["https://yourdomain.com"]
}
}
```
+308
View File
@@ -0,0 +1,308 @@
---
name: ai-video-gen
description: |
Generate AI videos from text prompts using the HeyGen API. Use when: (1) Generating videos from text descriptions, (2) Creating AI-generated video clips for content production, (3) Image-to-video generation with a reference image, (4) Choosing between video generation providers (VEO, Kling, Sora, Runway, Seedance), (5) Working with HeyGen's /v1/workflows/executions endpoint for video generation.
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# Video Generation (HeyGen API)
Generate AI videos from text prompts. Supports multiple providers (VEO 3.1, Kling, Sora, Runway, Seedance), configurable aspect ratios, and optional reference images for image-to-video generation.
## Authentication
All requests require the `X-Api-Key` header. Set the `HEYGEN_API_KEY` environment variable.
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workflow_type": "GenerateVideoNode", "input": {"prompt": "A drone shot flying over a coastal city at sunset"}}'
```
## Default Workflow
1. Call `POST /v1/workflows/executions` with `workflow_type: "GenerateVideoNode"` and your prompt
2. Receive a `execution_id` in the response
3. Poll `GET /v1/workflows/executions/{id}` every 10 seconds until status is `completed`
4. Use the returned `video_url` from the output
## Execute Video Generation
### Endpoint
`POST https://api.heygen.com/v1/workflows/executions`
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `workflow_type` | string | Y | Must be `"GenerateVideoNode"` |
| `input.prompt` | string | Y | Text description of the video to generate |
| `input.provider` | string | | Video generation provider (default: `"veo_3_1"`). See Providers below. |
| `input.aspect_ratio` | string | | Aspect ratio (default: `"16:9"`). Common values: `"16:9"`, `"9:16"`, `"1:1"` |
| `input.reference_image_url` | string | | Reference image URL for image-to-video generation |
| `input.tail_image_url` | string | | Tail image URL for last-frame guidance |
| `input.config` | object | | Provider-specific configuration overrides |
### Providers
| Provider | Value | Description |
|----------|-------|-------------|
| VEO 3.1 | `"veo_3_1"` | Google VEO 3.1 (default, highest quality) |
| VEO 3.1 Fast | `"veo_3_1_fast"` | Faster VEO 3.1 variant |
| VEO 3 | `"veo3"` | Google VEO 3 |
| VEO 3 Fast | `"veo3_fast"` | Faster VEO 3 variant |
| VEO 2 | `"veo2"` | Google VEO 2 |
| Kling Pro | `"kling_pro"` | Kling Pro model |
| Kling V2 | `"kling_v2"` | Kling V2 model |
| Sora V2 | `"sora_v2"` | OpenAI Sora V2 |
| Sora V2 Pro | `"sora_v2_pro"` | OpenAI Sora V2 Pro |
| Runway Gen-4 | `"runway_gen4"` | Runway Gen-4 |
| Seedance Lite | `"seedance_lite"` | Seedance Lite |
| Seedance Pro | `"seedance_pro"` | Seedance Pro |
| LTX Distilled | `"ltx_distilled"` | LTX Distilled (fastest) |
### curl
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": "A drone shot flying over a coastal city at golden hour, cinematic lighting",
"provider": "veo_3_1",
"aspect_ratio": "16:9"
}
}'
```
### TypeScript
```typescript
interface GenerateVideoInput {
prompt: string;
provider?: string;
aspect_ratio?: string;
reference_image_url?: string;
tail_image_url?: string;
config?: Record<string, any>;
}
interface ExecuteResponse {
data: {
execution_id: string;
status: "submitted";
};
}
async function generateVideo(input: GenerateVideoInput): Promise<string> {
const response = await fetch("https://api.heygen.com/v1/workflows/executions", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
workflow_type: "GenerateVideoNode",
input,
}),
});
const json: ExecuteResponse = await response.json();
return json.data.execution_id;
}
```
### Python
```python
import requests
import os
def generate_video(
prompt: str,
provider: str = "veo_3_1",
aspect_ratio: str = "16:9",
reference_image_url: str | None = None,
tail_image_url: str | None = None,
) -> str:
payload = {
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": prompt,
"provider": provider,
"aspect_ratio": aspect_ratio,
},
}
if reference_image_url:
payload["input"]["reference_image_url"] = reference_image_url
if tail_image_url:
payload["input"]["tail_image_url"] = tail_image_url
response = requests.post(
"https://api.heygen.com/v1/workflows/executions",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json",
},
json=payload,
)
data = response.json()
return data["data"]["execution_id"]
```
### Response Format
```json
{
"data": {
"execution_id": "node-gw-v1d2e3o4",
"status": "submitted"
}
}
```
## Check Status
### Endpoint
`GET https://api.heygen.com/v1/workflows/executions/{execution_id}`
### curl
```bash
curl -X GET "https://api.heygen.com/v1/workflows/executions/node-gw-v1d2e3o4" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### Response Format (Completed)
```json
{
"data": {
"execution_id": "node-gw-v1d2e3o4",
"status": "completed",
"output": {
"video": {
"video_url": "https://resource.heygen.ai/generated/video.mp4",
"video_id": "abc123"
},
"asset_id": "asset-xyz789"
}
}
}
```
## Polling for Completion
```typescript
async function generateVideoAndWait(
input: GenerateVideoInput,
maxWaitMs = 600000,
pollIntervalMs = 10000
): Promise<{ video_url: string; video_id: string; asset_id: string }> {
const executionId = await generateVideo(input);
console.log(`Submitted video generation: ${executionId}`);
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const response = await fetch(
`https://api.heygen.com/v1/workflows/executions/${executionId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
switch (data.status) {
case "completed":
return {
video_url: data.output.video.video_url,
video_id: data.output.video.video_id,
asset_id: data.output.asset_id,
};
case "failed":
throw new Error(data.error?.message || "Video generation failed");
case "not_found":
throw new Error("Workflow not found");
default:
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
}
throw new Error("Video generation timed out");
}
```
## Usage Examples
### Simple Text-to-Video
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": "A person walking through a sunlit park, shallow depth of field"
}
}'
```
### Image-to-Video
```json
{
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": "Animate this product photo with a slow zoom and soft particle effects",
"reference_image_url": "https://example.com/product-photo.png",
"provider": "kling_pro"
}
}
```
### Vertical Format for Social Media
```json
{
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": "A trendy coffee shop interior, camera slowly panning across the counter",
"aspect_ratio": "9:16",
"provider": "veo_3_1"
}
}
```
### Fast Generation with LTX
```json
{
"workflow_type": "GenerateVideoNode",
"input": {
"prompt": "Abstract colorful shapes morphing and flowing",
"provider": "ltx_distilled"
}
}
```
## Best Practices
1. **Be descriptive in prompts** — include camera movement, lighting, style, and mood details
2. **Default to VEO 3.1** for highest quality; use `ltx_distilled` or `veo3_fast` when speed matters
3. **Use reference images** for image-to-video generation — great for animating product photos or still images
4. **Video generation is the slowest workflow** — allow up to 5 minutes, poll every 10 seconds
5. **Aspect ratio matters** — use `9:16` for social media stories/reels, `16:9` for landscape, `1:1` for square
6. **Output includes `asset_id`** — use this to reference the generated video in other HeyGen workflows
7. **Output URLs are temporary** — download or save generated videos promptly
+117
View File
@@ -0,0 +1,117 @@
---
name: avatar-video
description: |
Create AI avatar videos with precise control over avatars, voices, scripts, scenes, and backgrounds using HeyGen's v2 API. Use when: (1) Choosing a specific avatar and voice for a video, (2) Writing exact scripts for an avatar to speak, (3) Building multi-scene videos with different backgrounds per scene, (4) Creating transparent WebM videos for compositing, (5) Using talking photos as video presenters, (6) Integrating HeyGen avatars with Remotion, (7) Batch video generation with exact specs, (8) Brand-consistent production videos with precise control.
homepage: https://docs.heygen.com/reference/create-a-video
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# Avatar Video
Create AI avatar videos with full control over avatars, voices, scripts, scenes, and backgrounds. Build single or multi-scene videos with exact configuration using HeyGen's `/v2/video/generate` API.
## Authentication
All requests require the `X-Api-Key` header. Set the `HEYGEN_API_KEY` environment variable.
```bash
curl -X GET "https://api.heygen.com/v2/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
## Tool Selection
If HeyGen MCP tools are available (`mcp__heygen__*`), **prefer them** over direct HTTP API calls — they handle authentication and request formatting automatically.
| Task | MCP Tool | Fallback (Direct API) |
|------|----------|----------------------|
| Check video status / get URL | `mcp__heygen__get_video` | `GET /v2/videos/{video_id}` |
| List account videos | `mcp__heygen__list_videos` | `GET /v2/videos` |
| Delete a video | `mcp__heygen__delete_video` | `DELETE /v2/videos/{video_id}` |
Video generation (`POST /v2/video/generate`) and avatar/voice listing are done via direct API calls — see reference files below.
## Default Workflow
1. **List avatars**`GET /v2/avatars` → pick an avatar, preview it, note `avatar_id` and `default_voice_id`. See [avatars.md](references/avatars.md)
2. **List voices** (if needed) — `GET /v2/voices` → pick a voice matching the avatar's gender/language. See [voices.md](references/voices.md)
3. **Write the script** — Structure scenes with one concept each. See [scripts.md](references/scripts.md)
4. **Generate the video**`POST /v2/video/generate` with avatar, voice, script, and background per scene. See [video-generation.md](references/video-generation.md)
5. **Poll for completion**`GET /v2/videos/{video_id}` until status is `completed`. See [video-status.md](references/video-status.md)
## Quick Reference
| Task | Read |
|------|------|
| List and preview avatars | [avatars.md](references/avatars.md) |
| List and select voices | [voices.md](references/voices.md) |
| Write and structure scripts | [scripts.md](references/scripts.md) |
| Generate video (single or multi-scene) | [video-generation.md](references/video-generation.md) |
| Add custom backgrounds | [backgrounds.md](references/backgrounds.md) |
| Add captions / subtitles | [captions.md](references/captions.md) |
| Add text overlays | [text-overlays.md](references/text-overlays.md) |
| Create transparent WebM video | [video-generation.md](references/video-generation.md) (WebM section) |
| Use templates | [templates.md](references/templates.md) |
| Create avatar from photo | [photo-avatars.md](references/photo-avatars.md) |
| Check video status / download | [video-status.md](references/video-status.md) |
| Upload assets (images, audio) | [assets.md](references/assets.md) |
| Use with Remotion | [remotion-integration.md](references/remotion-integration.md) |
| Set up webhooks | [webhooks.md](references/webhooks.md) |
## When to Use This Skill vs Create Video
This skill is for **precise control** — you choose the avatar, write the exact script, configure each scene.
If the user just wants to **describe a video idea** and let AI handle the rest (script, avatar, visuals), use the **create-video** skill instead.
| User Says | Create Video Skill | This Skill |
|-----------|:------------------:|:----------:|
| "Make me a video about X" | ✓ | |
| "Create a product demo" | ✓ | |
| "I want avatar Y to say exactly Z" | | ✓ |
| "Multi-scene video with different backgrounds" | | ✓ |
| "Transparent WebM for compositing" | | ✓ |
| "Use this specific voice for my script" | | ✓ |
| "Batch generate videos with exact specs" | | ✓ |
## Reference Files
### Core Video Creation
- [references/avatars.md](references/avatars.md) - Listing avatars, styles, avatar_id selection
- [references/voices.md](references/voices.md) - Listing voices, locales, speed/pitch
- [references/scripts.md](references/scripts.md) - Writing scripts, pauses, pacing
- [references/video-generation.md](references/video-generation.md) - POST /v2/video/generate and multi-scene videos
### Video Customization
- [references/backgrounds.md](references/backgrounds.md) - Solid colors, images, video backgrounds
- [references/text-overlays.md](references/text-overlays.md) - Adding text with fonts and positioning
- [references/captions.md](references/captions.md) - Auto-generated captions and subtitles
### Advanced Features
- [references/templates.md](references/templates.md) - Template listing and variable replacement
- [references/photo-avatars.md](references/photo-avatars.md) - Creating avatars from photos
- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events
### Integration
- [references/remotion-integration.md](references/remotion-integration.md) - Using HeyGen in Remotion compositions
### Foundation
- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs
- [references/assets.md](references/assets.md) - Uploading images, videos, audio
- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios
- [references/quota.md](references/quota.md) - Credit system and usage limits
## Best Practices
1. **Preview avatars before generating** — Download `preview_image_url` so the user can see the avatar before committing
2. **Use avatar's default voice** — Most avatars have a `default_voice_id` pre-matched for natural results
3. **Fallback: match gender manually** — If no default voice, ensure avatar and voice genders match
4. **Use test mode for development** — Set `test: true` to avoid consuming credits (output will be watermarked)
5. **Set generous timeouts** — Video generation often takes 5-15 minutes, sometimes longer
6. **Validate inputs** — Check avatar and voice IDs exist before generating
@@ -0,0 +1,339 @@
---
name: assets
description: Uploading images, videos, and audio for use in HeyGen video generation
---
# Asset Upload and Management
HeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.
## Upload Flow
Asset uploads are a single-step process: POST the raw file binary directly to the upload endpoint. The Content-Type header must match the file's MIME type.
## Uploading an Asset
**Endpoint:** `POST https://upload.heygen.com/v1/asset`
### Request
| Header | Required | Description |
|--------|:--------:|-------------|
| `X-Api-Key` | ✓ | Your HeyGen API key |
| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |
The request body is the raw binary file data. No JSON or form fields are needed.
### Response
| Field | Type | Description |
|-------|------|-------------|
| `code` | number | Status code (`100` = success) |
| `data.id` | string | Unique asset ID for use in video generation |
| `data.name` | string | Asset name |
| `data.file_type` | string | `image`, `video`, or `audio` |
| `data.url` | string | Accessible URL for the uploaded file |
| `data.image_key` | string \| null | Key for creating uploaded photo avatars (images only) |
| `data.folder_id` | string | Folder ID (empty if not in a folder) |
| `data.meta` | string \| null | Asset metadata |
| `data.created_ts` | number | Unix timestamp of creation |
### curl
```bash
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./background.jpg'
```
### TypeScript
```typescript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
name: string;
file_type: string;
url: string;
image_key: string | null;
folder_id: string;
meta: string | null;
created_ts: number;
};
msg: string | null;
message: string | null;
}
async function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: fileBuffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
// Usage
const asset = await uploadAsset("./background.jpg", "image/jpeg");
console.log(`Uploaded asset: ${asset.id}`);
console.log(`Asset URL: ${asset.url}`);
```
### TypeScript (with streams for large files)
```typescript
import fs from "fs";
import path from "path";
import { stat } from "fs/promises";
async function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileStats = await stat(resolvedPath);
const fileStream = fs.createReadStream(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
"Content-Length": fileStats.size.toString(),
},
body: fileStream as any,
// @ts-ignore - duplex is needed for streaming
duplex: "half",
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
### Python
```python
import requests
import os
def upload_asset(file_path: str, content_type: str) -> dict:
with open(file_path, "rb") as f:
response = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": content_type
},
data=f
)
data = response.json()
if data.get("code") != 100:
raise Exception(data.get("message", "Upload failed"))
return data["data"]
# Usage
asset = upload_asset("./background.jpg", "image/jpeg")
print(f"Uploaded asset: {asset['id']}")
print(f"Asset URL: {asset['url']}")
```
## Supported Content Types
| Type | Content-Type | Use Case |
|------|--------------|----------|
| JPEG | `image/jpeg` | Backgrounds, talking photos |
| PNG | `image/png` | Backgrounds, overlays |
| MP4 | `video/mp4` | Video backgrounds |
| WebM | `video/webm` | Video backgrounds |
| MP3 | `audio/mpeg` | Custom audio input |
| WAV | `audio/wav` | Custom audio input |
## Uploading from URL
If your asset is already hosted online:
```typescript
async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse["data"]> {
// 1. Validate and download the file
const url = new URL(sourceUrl);
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs are supported");
}
const sourceResponse = await fetch(sourceUrl);
const buffer = Buffer.from(await sourceResponse.arrayBuffer());
// 2. Upload directly to HeyGen
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: buffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
## Using Uploaded Assets
### As Background Image
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello, this is a video with a custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: asset.url, // Use the URL from the upload response
},
},
],
};
```
### As Talking Photo Source
```typescript
const talkingPhotoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: asset.id, // Use the ID from the upload response
},
voice: {
type: "text",
input_text: "Hello from my talking photo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### As Audio Input
```typescript
const audioConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: asset.url, // Use the URL from the upload response
},
},
],
};
```
## Complete Upload Workflow
```typescript
async function createVideoWithCustomBackground(
backgroundPath: string,
script: string
): Promise<string> {
// 1. Upload background
console.log("Uploading background...");
const background = await uploadAsset(backgroundPath, "image/jpeg");
// 2. Create video config
const config = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: background.url,
},
},
],
dimension: { width: 1920, height: 1080 },
};
// 3. Generate video
console.log("Generating video...");
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const { data } = await response.json();
return data.video_id;
}
```
## Asset Limitations
- **File size**: 10MB maximum
- **Image dimensions**: Recommended to match video dimensions
- **Audio duration**: Should match expected video length
- **Retention**: Assets may be deleted after a period of inactivity
## Best Practices
1. **Optimize images** - Resize to match video dimensions before uploading
2. **Use appropriate formats** - JPEG for photos, PNG for graphics with transparency
3. **Validate before upload** - Check file type and size locally first
4. **Handle upload errors** - Implement retry logic for failed uploads
5. **Cache asset IDs** - Reuse assets across multiple video generations
@@ -0,0 +1,586 @@
---
name: avatars
description: Listing avatars, avatar styles, and avatar_id selection for HeyGen
---
# HeyGen Avatars
Avatars are the AI-generated presenters in HeyGen videos. You can use public avatars provided by HeyGen or create custom avatars.
## Previewing Avatars Before Generation
Always preview avatars before generating a video to ensure they match user preferences. Each avatar has preview URLs that can be opened directly in the browser - no downloading required.
### List Avatars and Show Previews
```typescript
async function listAndPreviewAvatars(openInBrowser = true): Promise<void> {
const response = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data } = await response.json();
for (const avatar of data.avatars.slice(0, 5)) {
console.log(`\n${avatar.avatar_name} (${avatar.gender})`);
console.log(` ID: ${avatar.avatar_id}`);
console.log(` Preview: ${avatar.preview_image_url}`);
}
// Preview URLs can be opened directly in any browser
for (const avatar of data.avatars.slice(0, 3)) {
console.log(`Open in browser: ${avatar.preview_image_url}`);
}
}
```
### Workflow: Preview Before Generate
1. **List available avatars** - get names, genders, and preview URLs
2. **Show preview URLs to user** - share `preview_image_url` for visual check
3. **User selects** preferred avatar by name or ID
4. **Get avatar details** for `default_voice_id`
5. **Generate video** with selected avatar
### Preview Fields in API Response
| Field | Description |
|-------|-------------|
| `preview_image_url` | Static image of the avatar (JPG) - publicly accessible URL |
| `preview_video_url` | Short video clip showing avatar animation |
Both URLs are publicly accessible - no authentication needed to view.
## Listing Available Avatars
### curl
```bash
curl -X GET "https://api.heygen.com/v2/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Avatar {
avatar_id: string;
avatar_name: string;
gender: "male" | "female";
preview_image_url: string;
preview_video_url: string;
}
interface AvatarsResponse {
error: null | string;
data: {
avatars: Avatar[];
talking_photos: TalkingPhoto[];
};
}
async function listAvatars(): Promise<Avatar[]> {
const response = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: AvatarsResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.avatars;
}
```
### Python
```python
import requests
import os
def list_avatars() -> list:
response = requests.get(
"https://api.heygen.com/v2/avatars",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["avatars"]
```
## Response Format
```json
{
"error": null,
"data": {
"avatars": [
{
"avatar_id": "josh_lite3_20230714",
"avatar_name": "Josh",
"gender": "male",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/..."
},
{
"avatar_id": "angela_expressive_20231010",
"avatar_name": "Angela",
"gender": "female",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/..."
}
],
"talking_photos": []
}
}
```
## Avatar Types
### Public Avatars
HeyGen provides a library of public avatars that anyone can use:
```typescript
// List only public avatars
const avatars = await listAvatars();
const publicAvatars = avatars.filter((a) => !a.avatar_id.startsWith("custom_"));
```
### Private/Custom Avatars
Custom avatars created from your own training footage:
```typescript
const customAvatars = avatars.filter((a) => a.avatar_id.startsWith("custom_"));
```
## Avatar Styles
Avatars support different rendering styles:
| Style | Description |
|-------|-------------|
| `normal` | Full body shot, standard framing |
| `closeUp` | Close-up on face, more expressive |
| `circle` | Avatar in circular frame (talking head) |
| `voice_only` | Audio only, no video rendering |
### When to Use Each Style
| Use Case | Recommended Style |
|----------|-------------------|
| Full-screen presenter video | `normal` |
| Personal/intimate content | `closeUp` |
| Picture-in-picture overlay | `circle` |
| Small corner widget | `circle` |
| Podcast/audio content | `voice_only` |
| Motion graphics with avatar overlay | `normal` or `closeUp` + transparent bg |
### Using Avatar Styles
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal", // "normal" | "closeUp" | "circle" | "voice_only"
},
voice: {
type: "text",
input_text: "Hello, world!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Circle Style for Talking Heads
Circle style is ideal for overlay compositions:
```typescript
// Circle avatar for picture-in-picture
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "circle",
},
voice: { ... },
background: {
type: "color",
value: "#00FF00", // Green for chroma key, or use webm endpoint
},
}
```
## Searching and Filtering Avatars
### By Gender
```typescript
function filterByGender(avatars: Avatar[], gender: "male" | "female"): Avatar[] {
return avatars.filter((a) => a.gender === gender);
}
const maleAvatars = filterByGender(avatars, "male");
const femaleAvatars = filterByGender(avatars, "female");
```
### By Name
```typescript
function searchByName(avatars: Avatar[], query: string): Avatar[] {
const lowerQuery = query.toLowerCase();
return avatars.filter((a) =>
a.avatar_name.toLowerCase().includes(lowerQuery)
);
}
const results = searchByName(avatars, "josh");
```
## Avatar Groups
Avatars are organized into groups for better management.
### List Avatar Groups
```bash
curl -X GET "https://api.heygen.com/v2/avatar_group.list?include_public=true" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_public` | bool | false | Include public avatars in results |
#### TypeScript
```typescript
interface AvatarGroupItem {
id: string;
name: string;
created_at: number;
num_looks: number;
preview_image: string;
group_type: string;
train_status: string;
default_voice_id: string | null;
}
interface AvatarGroupListResponse {
error: null | string;
data: {
avatar_group_list: AvatarGroupItem[];
};
}
async function listAvatarGroups(
includePublic = true
): Promise<AvatarGroupListResponse["data"]> {
const params = new URLSearchParams({
include_public: includePublic.toString(),
});
const response = await fetch(
`https://api.heygen.com/v2/avatar_group.list?${params}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: AvatarGroupListResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
### Get Avatars in a Group
```bash
curl -X GET "https://api.heygen.com/v2/avatar_group/{group_id}/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
## Using Avatars in Video Generation
### Basic Avatar Usage
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to our product demo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
### Multiple Scenes with Different Avatars
```typescript
const multiSceneConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hi, I'm Josh. Let me introduce my colleague.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
{
character: {
type: "avatar",
avatar_id: "angela_expressive_20231010",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! I'm Angela. Nice to meet you!",
voice_id: "2d5b0e6a8c3f47d9a1b2c3d4e5f60718",
},
},
],
};
```
## Using Avatar's Default Voice
Many avatars have a `default_voice_id` that's pre-matched for natural results. **This is the recommended approach** rather than manually selecting voices.
### Recommended Flow
```
1. GET /v2/avatars → Get list of avatar_ids
2. GET /v2/avatar/{id}/details → Get default_voice_id for chosen avatar
3. POST /v2/video/generate → Use avatar_id + default_voice_id
```
### Get Avatar Details (v2 API)
Given an `avatar_id`, fetch its details including the default voice:
```bash
curl -X GET "https://api.heygen.com/v2/avatar/{avatar_id}/details" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
#### Response Format
```json
{
"error": null,
"data": {
"type": "avatar",
"id": "josh_lite3_20230714",
"name": "Josh",
"gender": "male",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/...",
"premium": false,
"is_public": true,
"default_voice_id": "1bd001e7e50f421d891986aad5158bc8",
"tags": ["AVATAR_IV"]
}
}
```
#### TypeScript
```typescript
interface AvatarDetails {
type: "avatar";
id: string;
name: string;
gender: "male" | "female";
preview_image_url: string;
preview_video_url: string;
premium: boolean;
is_public: boolean;
default_voice_id: string | null;
tags: string[];
}
async function getAvatarDetails(avatarId: string): Promise<AvatarDetails> {
const response = await fetch(
`https://api.heygen.com/v2/avatar/${avatarId}/details`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
// Usage: Get default voice for a known avatar
const details = await getAvatarDetails("josh_lite3_20230714");
if (details.default_voice_id) {
console.log(`Using ${details.name} with default voice: ${details.default_voice_id}`);
} else {
console.log(`${details.name} has no default voice, select manually`);
}
```
#### Complete Example: Generate Video with Any Avatar's Default Voice
```typescript
async function generateWithAvatarDefaultVoice(
avatarId: string,
script: string
): Promise<string> {
// 1. Get avatar details to find default voice
const avatar = await getAvatarDetails(avatarId);
if (!avatar.default_voice_id) {
throw new Error(`Avatar ${avatar.name} has no default voice`);
}
// 2. Generate video with the avatar's default voice
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatar.id,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id,
},
}],
dimension: { width: 1920, height: 1080 },
});
return videoId;
}
```
### Why Use Default Voice?
1. **Guaranteed gender match** - Avatar and voice are pre-paired
2. **Natural lip sync** - Default voices are optimized for the avatar
3. **Simpler code** - No need to fetch and match voices separately
4. **Better quality** - HeyGen has tested this combination
## Selecting the Right Avatar
### Avatar Categories
HeyGen avatars fall into distinct categories. Match the category to your use case:
| Category | Examples | Best For |
|----------|----------|----------|
| **Business/Professional** | Josh, Angela, Wayne | Corporate videos, product demos, training |
| **Casual/Friendly** | Lily, various lifestyle avatars | Social media, informal content |
| **Themed/Seasonal** | Holiday-themed, costume avatars | Specific campaigns, seasonal content |
| **Expressive** | Avatars with "expressive" in name | Engaging storytelling, dynamic content |
### Selection Guidelines
**For business/professional content:**
- Choose avatars with neutral attire (business casual or formal)
- Avoid themed or seasonal avatars (holiday costumes, casual clothing)
- Preview the avatar to verify professional appearance
- Consider your audience demographics when selecting gender and appearance
**For casual/social content:**
- More flexibility in avatar choice
- Themed avatars can work for specific campaigns
- Match avatar energy to content tone
### Common Mistakes to Avoid
1. **Using themed avatars for business content** - A holiday-themed avatar looks unprofessional in a product demo
2. **Not previewing before generation** - Always check the preview URL to verify appearance
3. **Ignoring avatar style** - A `circle` style avatar may not work for full-screen presentations
4. **Mismatched voice gender** - Always use the avatar's `default_voice_id` or match genders manually
### Selection Checklist
Before generating a video:
- [ ] Previewed avatar image/video in browser
- [ ] Avatar appearance matches content tone (professional vs casual)
- [ ] Avatar style (`normal`, `closeUp`, `circle`) fits the video format
- [ ] Voice gender matches avatar gender
- [ ] Using `default_voice_id` when available
## Helper Functions
### Get Avatar by ID
```typescript
async function getAvatarById(avatarId: string): Promise<Avatar | null> {
const avatars = await listAvatars();
return avatars.find((a) => a.avatar_id === avatarId) || null;
}
```
### Validate Avatar ID
```typescript
async function isValidAvatarId(avatarId: string): Promise<boolean> {
const avatar = await getAvatarById(avatarId);
return avatar !== null;
}
```
### Get Random Avatar
```typescript
async function getRandomAvatar(gender?: "male" | "female"): Promise<Avatar> {
let avatars = await listAvatars();
if (gender) {
avatars = avatars.filter((a) => a.gender === gender);
}
const randomIndex = Math.floor(Math.random() * avatars.length);
return avatars[randomIndex];
}
```
## Common Avatar IDs
Some commonly used public avatar IDs (availability may vary):
| Avatar ID | Name | Gender |
|-----------|------|--------|
| `josh_lite3_20230714` | Josh | Male |
| `angela_expressive_20231010` | Angela | Female |
| `wayne_20240422` | Wayne | Male |
| `lily_20230614` | Lily | Female |
Always verify avatar availability by calling the list endpoint before using.
@@ -0,0 +1,295 @@
---
name: backgrounds
description: Solid colors, images, and video backgrounds for HeyGen videos
---
# Video Backgrounds
HeyGen supports various background types to customize the appearance of your avatar videos.
## Background Types
| Type | Description |
|------|-------------|
| `color` | Solid color background |
| `image` | Static image background |
| `video` | Looping video background |
## Color Backgrounds
The simplest option - use a solid color:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello with a colored background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#FFFFFF", // White background
},
},
],
};
```
### Common Color Values
| Color | Hex Value | Use Case |
|-------|-----------|----------|
| White | `#FFFFFF` | Clean, professional |
| Black | `#000000` | Dramatic, cinematic |
| Blue | `#0066CC` | Corporate, trustworthy |
| Green | `#00FF00` | Chroma key (for compositing) |
| Gray | `#808080` | Neutral, modern |
### Using Transparent/Green Screen
For compositing in post-production:
```typescript
background: {
type: "color",
value: "#00FF00", // Green screen
}
```
## Image Backgrounds
Use a static image as background:
### From URL
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Check out this custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/my-background.jpg",
},
},
],
};
```
### From Uploaded Asset
First upload your image, then use the asset URL:
```typescript
// 1. Upload the image
const assetId = await uploadFile("./background.jpg", "image/jpeg");
// 2. Use in video config
const videoConfig = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: `https://files.heygen.ai/asset/${assetId}`,
},
},
],
};
```
### Image Requirements
- **Formats**: JPEG, PNG
- **Recommended size**: Match video dimensions (e.g., 1920x1080 for 1080p)
- **Aspect ratio**: Should match video aspect ratio
- **File size**: Under 10MB recommended
## Video Backgrounds
Use a looping video as background:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Dynamic video background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "video",
url: "https://example.com/background-loop.mp4",
},
},
],
};
```
### Video Requirements
- **Format**: MP4 (H.264 codec recommended)
- **Looping**: Video will loop if shorter than avatar content
- **Audio**: Background video audio is typically muted
- **File size**: Under 100MB recommended
## Different Backgrounds Per Scene
Use different backgrounds for each scene:
```typescript
const multiBackgroundConfig = {
video_inputs: [
// Scene 1: Office background
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Let me start with an introduction.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/office-bg.jpg",
},
},
// Scene 2: Product showcase
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "closeUp",
},
voice: {
type: "text",
input_text: "Now let me show you our product.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/product-bg.jpg",
},
},
// Scene 3: Call to action
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Get started today!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
};
```
## Background Helper Functions
### TypeScript
```typescript
type BackgroundType = "color" | "image" | "video";
interface Background {
type: BackgroundType;
value?: string;
url?: string;
}
function createColorBackground(hexColor: string): Background {
return { type: "color", value: hexColor };
}
function createImageBackground(imageUrl: string): Background {
return { type: "image", url: imageUrl };
}
function createVideoBackground(videoUrl: string): Background {
return { type: "video", url: videoUrl };
}
// Preset backgrounds
const backgrounds = {
white: createColorBackground("#FFFFFF"),
black: createColorBackground("#000000"),
greenScreen: createColorBackground("#00FF00"),
corporate: createColorBackground("#0066CC"),
};
```
## Best Practices
1. **Match dimensions** - Background should match video dimensions
2. **Consider avatar position** - Leave space where avatar will appear
3. **Use contrasting colors** - Ensure avatar is visible against background
4. **Optimize file sizes** - Compress images/videos for faster processing
5. **Test with green screen** - For professional post-production workflows
6. **Keep backgrounds simple** - Avoid distracting elements behind the avatar
## Common Issues
### Background Not Showing
```typescript
// Wrong: missing url/value
background: {
type: "image"
}
// Correct
background: {
type: "image",
url: "https://example.com/bg.jpg"
}
```
### Aspect Ratio Mismatch
If your background doesn't match the video dimensions, it may be cropped or stretched. Always match your background aspect ratio to your video dimensions:
```typescript
// For 1920x1080 video
// Use 1920x1080 background image
// For 1080x1920 portrait video
// Use 1080x1920 background image
```
### Video Background Audio
Background video audio is typically muted to avoid conflicting with the avatar's voice. If you need background music, add it as a separate audio track in post-production.
@@ -0,0 +1,277 @@
---
name: captions
description: Auto-generated captions and subtitle options for HeyGen videos
---
# Video Captions
HeyGen can automatically generate captions (subtitles) for your videos, improving accessibility and engagement.
## Enabling Captions
Captions can be enabled when generating a video:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! This video will have automatic captions.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
// Caption settings (availability varies by plan)
caption: true,
};
```
## Caption Configuration Options
```typescript
interface CaptionConfig {
// Enable/disable captions
enabled: boolean;
// Caption style
style?: {
font_family?: string;
font_size?: number;
font_color?: string;
background_color?: string;
position?: "top" | "bottom";
};
// Language for caption generation
language?: string;
}
```
## Caption Styles
### Basic Captions
```typescript
const config = {
video_inputs: [...],
caption: true, // Enable with default styling
};
```
### Styled Captions
```typescript
const config = {
video_inputs: [...],
caption: {
enabled: true,
style: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
position: "bottom",
},
},
};
```
## Multi-Language Captions
For videos in different languages, captions are generated based on the voice language:
```typescript
// Spanish video with Spanish captions
const spanishConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "¡Hola! Este video tendrá subtítulos en español.",
voice_id: "spanish_voice_id",
},
},
],
caption: true,
};
```
## Working with SRT Files
### SRT File Format
Standard SRT format:
```srt
1
00:00:00,000 --> 00:00:03,000
Hello! This video will have
2
00:00:03,000 --> 00:00:06,000
automatic captions generated.
3
00:00:06,000 --> 00:00:09,000
They sync with the audio.
```
### Using Custom SRT
For video translation, you can provide your own SRT:
```typescript
const translationConfig = {
input_video_id: "original_video_id",
output_languages: ["es-ES", "fr-FR"],
srt_key: "path/to/custom.srt", // Custom SRT file
srt_role: "input", // "input" or "output"
};
```
## Caption Positioning
### Bottom (Default)
Standard position for most videos:
```typescript
caption: {
enabled: true,
style: {
position: "bottom"
}
}
```
### Top
For videos where bottom space is occupied:
```typescript
caption: {
enabled: true,
style: {
position: "top"
}
}
```
## Accessibility Best Practices
1. **Always enable captions** - Improves accessibility for deaf/hard-of-hearing viewers
2. **Use high contrast** - White text on dark background or vice versa
3. **Readable font size** - At least 24px for standard video, larger for mobile
4. **Don't cover important content** - Position captions away from key visual elements
5. **Sync timing** - Ensure captions match audio timing accurately
## Caption Helper Functions
```typescript
interface CaptionStyle {
font_family: string;
font_size: number;
font_color: string;
background_color: string;
position: "top" | "bottom";
}
const captionPresets: Record<string, CaptionStyle> = {
default: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
position: "bottom",
},
minimal: {
font_family: "Arial",
font_size: 28,
font_color: "#FFFFFF",
background_color: "transparent",
position: "bottom",
},
bold: {
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.9)",
position: "bottom",
},
branded: {
font_family: "Roboto",
font_size: 30,
font_color: "#00D1FF",
background_color: "rgba(26, 26, 46, 0.9)",
position: "bottom",
},
};
function createCaptionConfig(preset: keyof typeof captionPresets) {
return {
enabled: true,
style: captionPresets[preset],
};
}
```
## Social Media Caption Considerations
### TikTok / Instagram Reels
- Position captions in center or upper portion
- Avoid bottom 20% (covered by UI elements)
- Use larger font sizes for mobile viewing
```typescript
const socialCaptions = {
enabled: true,
style: {
font_size: 42,
position: "top", // Avoid bottom UI elements
},
};
```
### YouTube
- Standard bottom captions work well
- YouTube also supports closed captions upload
### LinkedIn
- Captions highly recommended (many watch without sound)
- Professional styling preferred
## Limitations
- Caption styles may be limited depending on your subscription tier
- Some advanced caption features may require the web interface
- Multi-speaker caption detection may have limited availability
- Caption accuracy depends on audio quality and speech clarity
## Integration with Video Translation
When using video translation, captions are automatically handled:
```typescript
// Video translation includes caption generation
const translationConfig = {
input_video_id: "original_video_id",
output_languages: ["es-ES"],
// Captions generated in target language
};
```
See the **video-translate** skill for more details on video translation.
@@ -0,0 +1,314 @@
---
name: dimensions
description: Resolution options (720p/1080p) and aspect ratios for HeyGen videos
---
# Video Dimensions and Resolution
HeyGen supports various video dimensions and aspect ratios to fit different platforms and use cases.
## Standard Resolutions
### Landscape (16:9)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 1280 | 720 | Standard quality, faster processing |
| 1080p | 1920 | 1080 | High quality, most common |
### Portrait (9:16)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 1280 | Mobile-first content |
| 1080p | 1080 | 1920 | High quality vertical |
### Square (1:1)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 720 | Social media posts |
| 1080p | 1080 | 1080 | High quality square |
## Setting Dimensions
### TypeScript
```typescript
// Landscape 1080p
const landscapeConfig = {
video_inputs: [...],
dimension: {
width: 1920,
height: 1080
}
};
// Portrait 1080p
const portraitConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1920
}
};
// Square 1080p
const squareConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1080
}
};
```
### curl
```bash
# Landscape 1080p
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [...],
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
## Dimension Helper Functions
```typescript
type AspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "4:5";
type Quality = "720p" | "1080p";
interface Dimensions {
width: number;
height: number;
}
function getDimensions(aspectRatio: AspectRatio, quality: Quality): Dimensions {
const configs: Record<AspectRatio, Record<Quality, Dimensions>> = {
"16:9": {
"720p": { width: 1280, height: 720 },
"1080p": { width: 1920, height: 1080 },
},
"9:16": {
"720p": { width: 720, height: 1280 },
"1080p": { width: 1080, height: 1920 },
},
"1:1": {
"720p": { width: 720, height: 720 },
"1080p": { width: 1080, height: 1080 },
},
"4:3": {
"720p": { width: 960, height: 720 },
"1080p": { width: 1440, height: 1080 },
},
"4:5": {
"720p": { width: 576, height: 720 },
"1080p": { width: 864, height: 1080 },
},
};
return configs[aspectRatio][quality];
}
// Usage
const youTubeDimensions = getDimensions("16:9", "1080p");
const tikTokDimensions = getDimensions("9:16", "1080p");
const instagramDimensions = getDimensions("1:1", "1080p");
```
## Platform-Specific Recommendations
### YouTube
```typescript
const youtubeConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape
};
```
### TikTok / Instagram Reels / YouTube Shorts
```typescript
const shortFormConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1920 }, // 9:16 portrait
};
```
### Instagram Feed Post
```typescript
const instagramFeedConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1080 }, // 1:1 square
};
```
### LinkedIn
```typescript
const linkedinConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred
};
```
### Twitter/X
```typescript
const twitterConfig = {
video_inputs: [...],
dimension: { width: 1280, height: 720 }, // 16:9, 720p is common
};
```
## Avatar IV Dimensions
For Avatar IV (photo-based avatars), dimensions are set via orientation:
```typescript
type VideoOrientation = "portrait" | "landscape" | "square";
function getAvatarIVDimensions(orientation: VideoOrientation): Dimensions {
switch (orientation) {
case "portrait":
return { width: 720, height: 1280 };
case "landscape":
return { width: 1280, height: 720 };
case "square":
return { width: 720, height: 720 };
}
}
```
## Custom Dimensions
HeyGen supports custom dimensions within limits:
```typescript
const customConfig = {
video_inputs: [...],
dimension: {
width: 1600,
height: 900 // Custom 16:9 at non-standard resolution
}
};
```
### Dimension Constraints
- **Minimum**: 128px on any side
- **Maximum**: 4096px on any side
- **Must be even numbers**: Both width and height must be divisible by 2
```typescript
function validateDimensions(width: number, height: number): boolean {
if (width < 128 || height < 128) {
throw new Error("Dimensions must be at least 128px");
}
if (width > 4096 || height > 4096) {
throw new Error("Dimensions cannot exceed 4096px");
}
if (width % 2 !== 0 || height % 2 !== 0) {
throw new Error("Dimensions must be even numbers");
}
return true;
}
```
## Resolution vs. Credit Cost
Higher resolutions may consume more credits:
| Resolution | Relative Cost |
|------------|---------------|
| 720p | Base rate |
| 1080p | ~1.5x base rate |
Consider using 720p for drafts and testing, then 1080p for final output.
## Background Considerations
Match background image/video dimensions to your video dimensions:
```typescript
// For 1080p landscape video
const config = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: "https://example.com/1920x1080-background.jpg" // Match video dimensions
}
}
],
dimension: { width: 1920, height: 1080 }
};
```
## Creating a Video Config Factory
```typescript
interface VideoConfigOptions {
script: string;
avatarId: string;
voiceId: string;
platform: "youtube" | "tiktok" | "instagram_feed" | "instagram_story" | "linkedin";
quality?: "720p" | "1080p";
}
function createVideoConfig(options: VideoConfigOptions) {
const platformDimensions: Record<string, Dimensions> = {
youtube: { width: 1920, height: 1080 },
tiktok: { width: 1080, height: 1920 },
instagram_feed: { width: 1080, height: 1080 },
instagram_story: { width: 1080, height: 1920 },
linkedin: { width: 1920, height: 1080 },
};
const dimension = platformDimensions[options.platform];
// Scale down for 720p if requested
if (options.quality === "720p") {
dimension.width = Math.round((dimension.width * 720) / 1080);
dimension.height = Math.round((dimension.height * 720) / 1080);
}
return {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: options.avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: options.script,
voice_id: options.voiceId,
},
},
],
dimension,
};
}
// Usage
const tiktokVideo = createVideoConfig({
script: "Hey everyone! Check this out!",
avatarId: "josh_lite3_20230714",
voiceId: "1bd001e7e50f421d891986aad5158bc8",
platform: "tiktok",
quality: "1080p",
});
```
@@ -0,0 +1,853 @@
---
name: photo-avatars
description: Creating avatars from photos (talking photos) for HeyGen
---
# Photo Avatars (Talking Photos)
Photo avatars allow you to animate a static photo and make it speak. This is useful for creating personalized video content from portraits, headshots, or any suitable image.
## Creating a Photo Avatar from an Uploaded Image
The workflow is: **Upload Image → Create Avatar Group → Use in Video**
### Step 1: Upload the Image
Upload a portrait photo using the asset upload endpoint. The response includes an `image_key` which you'll use in the next step.
```bash
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./portrait.jpg'
```
Response:
```json
{
"code": 100,
"data": {
"id": "741299e941764988b432ed3a6757878f",
"name": "741299e941764988b432ed3a6757878f",
"file_type": "image",
"url": "https://resource2.heygen.ai/image/.../original.jpg",
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg"
}
}
```
> **Important:** Save the `image_key` field (not the `id`). The `image_key` is the S3 path used to create the photo avatar.
See [assets.md](assets.md) for full upload details.
### Step 2: Create Photo Avatar Group
Use the `image_key` from the upload response to create a photo avatar group. This processes the image and creates a usable photo avatar.
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/create`
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/avatar_group/create" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg",
"name": "My Photo Avatar"
}'
```
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `image_key` | string | ✓ | S3 image key from upload response |
| `name` | string | ✓ | Display name for the avatar |
| `generation_id` | string | | If using AI-generated photo (see below) |
Response:
```json
{
"error": null,
"data": {
"id": "045c260bc0364727b2cbe50442c3a5bf",
"image_url": "https://files2.heygen.ai/...",
"created_at": 1771798135.777256,
"name": "My Photo Avatar",
"status": "pending",
"group_id": "045c260bc0364727b2cbe50442c3a5bf",
"is_motion": false,
"business_type": "uploaded"
}
}
```
The `id` (same as `group_id`) is your `talking_photo_id` for video generation.
### Step 3: Wait for Processing
The photo avatar starts with `status: "pending"` and transitions to `"completed"` within seconds. Poll the status endpoint:
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`
```bash
curl "https://api.heygen.com/v2/photo_avatar/045c260bc0364727b2cbe50442c3a5bf" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
Wait until `status` is `"completed"` before using in video generation.
### Step 4: Use in Video Generation
Use the photo avatar `id` as `talking_photo_id`:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: "045c260bc0364727b2cbe50442c3a5bf",
},
voice: {
type: "text",
input_text: "Hello! This is my photo avatar speaking.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
## TypeScript: Complete Workflow
```typescript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
image_key: string;
url: string;
};
}
interface PhotoAvatarResponse {
error: string | null;
data: {
id: string;
group_id: string;
image_url: string;
name: string;
status: string;
is_motion: boolean;
business_type: string;
};
}
async function createPhotoAvatar(
imagePath: string,
name: string
): Promise<string> {
// 1. Upload image
const resolvedPath = path.resolve(imagePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const uploadResponse = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "image/jpeg",
},
body: fileBuffer,
});
const uploadJson: AssetUploadResponse = await uploadResponse.json();
if (uploadJson.code !== 100) {
throw new Error("Upload failed");
}
const imageKey = uploadJson.data.image_key;
// 2. Create avatar group
const createResponse = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_key: imageKey, name }),
}
);
const createJson: PhotoAvatarResponse = await createResponse.json();
if (createJson.error) {
throw new Error(createJson.error);
}
const photoAvatarId = createJson.data.id;
// 3. Wait for processing
await waitForPhotoAvatar(photoAvatarId);
return photoAvatarId;
}
async function waitForPhotoAvatar(id: string): Promise<void> {
for (let i = 0; i < 30; i++) {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: PhotoAvatarResponse = await response.json();
if (json.data.status === "completed") return;
if (json.data.status === "failed") {
throw new Error("Photo avatar processing failed");
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Photo avatar processing timed out");
}
async function createVideoFromPhoto(
photoPath: string,
script: string,
voiceId: string
): Promise<string> {
// 1. Create photo avatar
const talkingPhotoId = await createPhotoAvatar(photoPath, "Video Avatar");
// 2. Generate video
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: talkingPhotoId,
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
},
],
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
## Python: Complete Workflow
```python
import requests
import os
import time
def create_photo_avatar(image_path: str, name: str) -> str:
api_key = os.environ["HEYGEN_API_KEY"]
# 1. Upload image
with open(image_path, "rb") as f:
upload_resp = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": api_key,
"Content-Type": "image/jpeg",
},
data=f,
)
upload_data = upload_resp.json()
if upload_data.get("code") != 100:
raise Exception("Upload failed")
image_key = upload_data["data"]["image_key"]
# 2. Create avatar group
create_resp = requests.post(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
headers={
"X-Api-Key": api_key,
"Content-Type": "application/json",
},
json={"image_key": image_key, "name": name},
)
create_data = create_resp.json()
if create_data.get("error"):
raise Exception(create_data["error"])
photo_avatar_id = create_data["data"]["id"]
# 3. Wait for processing
for _ in range(30):
status_resp = requests.get(
f"https://api.heygen.com/v2/photo_avatar/{photo_avatar_id}",
headers={"X-Api-Key": api_key},
)
status = status_resp.json()["data"]["status"]
if status == "completed":
return photo_avatar_id
if status == "failed":
raise Exception("Photo avatar processing failed")
time.sleep(2)
raise Exception("Photo avatar processing timed out")
```
## Listing Existing Talking Photos
Retrieve all talking photos in your account:
**Endpoint:** `GET https://api.heygen.com/v1/talking_photo.list`
```bash
curl "https://api.heygen.com/v1/talking_photo.list" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
Response:
```json
{
"code": 100,
"data": [
{
"id": "ef0ed70f72c6497793e5e36e434d2aea",
"image_url": "https://files2.heygen.ai/talking_photo/.../image.WEBP",
"circle_image": ""
}
]
}
```
Each `id` can be used as `talking_photo_id` in video generation.
## Adding Photos to an Existing Group
Add additional photo looks to an existing avatar group:
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/add`
```typescript
async function addPhotosToGroup(
groupId: string,
imageKeys: string[],
name: string
): Promise<void> {
const response = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/add",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
group_id: groupId,
image_keys: imageKeys,
name,
}),
}
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}
```
## Training a Photo Avatar Group
Train the avatar group for improved animation quality:
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/train`
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/train" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"group_id": "045c260bc0364727b2cbe50442c3a5bf"}'
```
Check training status:
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/train/status/{group_id}`
## Avatar IV Video Generation
Avatar IV is HeyGen's latest photo avatar technology with improved quality and natural motion. It generates a video directly from an uploaded image, bypassing the avatar group creation step.
**Endpoint:** `POST https://api.heygen.com/v2/video/av4/generate`
```bash
curl -X POST "https://api.heygen.com/v2/video/av4/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg",
"script": "Hello! This is Avatar IV with enhanced quality.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"video_orientation": "landscape",
"video_title": "My Avatar IV Video"
}'
```
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `image_key` | string | ✓ | S3 image key from asset upload |
| `script` | string | ✓ | Text for the avatar to speak |
| `voice_id` | string | ✓ | Voice to use |
| `video_orientation` | string | | `"portrait"`, `"landscape"`, or `"square"` |
| `video_title` | string | | Title for the video |
| `fit` | string | | `"cover"` or `"contain"` |
| `custom_motion_prompt` | string | | Motion/expression description |
| `enhance_custom_motion_prompt` | boolean | | Enhance the motion prompt with AI |
### TypeScript
```typescript
interface AvatarIVRequest {
image_key: string;
script: string;
voice_id: string;
video_orientation?: "portrait" | "landscape" | "square";
video_title?: string;
fit?: "cover" | "contain";
custom_motion_prompt?: string;
enhance_custom_motion_prompt?: boolean;
}
interface AvatarIVResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateAvatarIVVideo(
config: AvatarIVRequest
): Promise<string> {
const response = await fetch(
"https://api.heygen.com/v2/video/av4/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const json: AvatarIVResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Avatar IV Options
| Orientation | Dimensions | Use Case |
|-------------|------------|----------|
| `portrait` | 720x1280 | TikTok, Stories |
| `landscape` | 1280x720 | YouTube, Web |
| `square` | 720x720 | Instagram Feed |
| Fit | Description |
|-----|-------------|
| `cover` | Fill the frame, may crop edges |
| `contain` | Fit entire image, may show background |
### Custom Motion Prompts
```typescript
const videoId = await generateAvatarIVVideo({
image_key: "image/.../original.jpg",
script: "Let me tell you about our product.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
custom_motion_prompt: "nodding head and smiling",
enhance_custom_motion_prompt: true,
});
```
## Generating AI Photo Avatars
Generate synthetic photo avatars from text descriptions instead of uploading a photo.
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/photo/generate`
> **IMPORTANT: All 8 fields are REQUIRED.** The API will reject requests missing any field.
> When a user asks to "generate an AI avatar of a professional man", you need to ask for or select values for ALL fields below.
### Required Fields (ALL must be provided)
| Field | Type | Allowed Values |
|-------|------|----------------|
| `name` | string | Name for the generated avatar |
| `age` | enum | `"Young Adult"`, `"Early Middle Age"`, `"Late Middle Age"`, `"Senior"`, `"Unspecified"` |
| `gender` | enum | `"Woman"`, `"Man"`, `"Unspecified"` |
| `ethnicity` | enum | `"White"`, `"Black"`, `"Asian American"`, `"East Asian"`, `"South East Asian"`, `"South Asian"`, `"Middle Eastern"`, `"Pacific"`, `"Hispanic"`, `"Unspecified"` |
| `orientation` | enum | `"square"`, `"horizontal"`, `"vertical"` |
| `pose` | enum | `"half_body"`, `"close_up"`, `"full_body"` |
| `style` | enum | `"Realistic"`, `"Pixar"`, `"Cinematic"`, `"Vintage"`, `"Noir"`, `"Cyberpunk"`, `"Unspecified"` |
| `appearance` | string | Text prompt describing appearance (clothing, mood, lighting, etc). Max 1000 chars |
### curl Example
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/photo/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sarah Product Demo",
"age": "Young Adult",
"gender": "Woman",
"ethnicity": "White",
"orientation": "horizontal",
"pose": "half_body",
"style": "Realistic",
"appearance": "Professional woman with a friendly smile, wearing a navy blue blazer over a white blouse, soft studio lighting, clean neutral background"
}'
```
Response:
```json
{
"error": null,
"data": {
"generation_id": "6a7f7f2795de4599bec7cf1e06babe30"
}
}
```
### Check Generation Status
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/generation/{generation_id}`
The response includes multiple generated images to choose from:
```json
{
"error": null,
"data": {
"id": "6a7f7f2795de4599bec7cf1e06babe30",
"status": "success",
"image_url_list": [
"https://resource2.heygen.ai/photo_generation/.../image1.jpg",
"https://resource2.heygen.ai/photo_generation/.../image2.jpg",
"https://resource2.heygen.ai/photo_generation/.../image3.jpg",
"https://resource2.heygen.ai/photo_generation/.../image4.jpg"
],
"image_key_list": [
"photo_generation/.../image1.jpg",
"photo_generation/.../image2.jpg",
"photo_generation/.../image3.jpg",
"photo_generation/.../image4.jpg"
]
}
}
```
### TypeScript
```typescript
interface GeneratePhotoAvatarRequest {
name: string;
age: "Young Adult" | "Early Middle Age" | "Late Middle Age" | "Senior" | "Unspecified";
gender: "Woman" | "Man" | "Unspecified";
ethnicity: "White" | "Black" | "Asian American" | "East Asian" | "South East Asian" | "South Asian" | "Middle Eastern" | "Pacific" | "Hispanic" | "Unspecified";
orientation: "square" | "horizontal" | "vertical";
pose: "half_body" | "close_up" | "full_body";
style: "Realistic" | "Pixar" | "Cinematic" | "Vintage" | "Noir" | "Cyberpunk" | "Unspecified";
appearance: string;
}
interface GeneratePhotoAvatarResponse {
error: string | null;
data: {
generation_id: string;
};
}
interface PhotoGenerationStatus {
error: string | null;
data: {
id: string;
status: "pending" | "processing" | "success" | "failed";
msg: string | null;
image_url_list?: string[];
image_key_list?: string[];
};
}
async function generatePhotoAvatar(
config: GeneratePhotoAvatarRequest
): Promise<string> {
const response = await fetch(
"https://api.heygen.com/v2/photo_avatar/photo/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const json: GeneratePhotoAvatarResponse = await response.json();
if (json.error) {
throw new Error(`Photo avatar generation failed: ${json.error}`);
}
return json.data.generation_id;
}
async function waitForPhotoGeneration(
generationId: string
): Promise<string[]> {
for (let i = 0; i < 60; i++) {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/generation/${generationId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: PhotoGenerationStatus = await response.json();
if (json.error) throw new Error(json.error);
if (json.data.status === "success") {
return json.data.image_key_list!;
}
if (json.data.status === "failed") {
throw new Error(json.data.msg ?? "Photo generation failed");
}
await new Promise((r) => setTimeout(r, 5000));
}
throw new Error("Photo generation timed out");
}
```
### AI Photo → Avatar Group → Video
Use a generated AI photo to create an avatar group, then generate a video:
```typescript
// 1. Generate AI photo
const generationId = await generatePhotoAvatar({
name: "Product Demo Host",
age: "Young Adult",
gender: "Woman",
ethnicity: "Unspecified",
orientation: "horizontal",
pose: "half_body",
style: "Realistic",
appearance: "Professional woman, navy blazer, friendly smile, soft lighting",
});
// 2. Wait for generation and pick first result
const imageKeys = await waitForPhotoGeneration(generationId);
const selectedImageKey = imageKeys[0];
// 3. Create avatar group from the AI photo
const createResponse = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
image_key: selectedImageKey,
name: "Product Demo Host",
generation_id: generationId,
}),
}
);
const { data } = await createResponse.json();
const talkingPhotoId = data.id;
// 4. Generate video (after status is "completed")
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "talking_photo",
talking_photo_id: talkingPhotoId,
},
voice: {
type: "text",
input_text: "Welcome to our product demo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
}],
dimension: { width: 1920, height: 1080 },
});
```
### Pre-Generation Checklist
Before calling the AI generation API, ensure you have values for ALL fields:
| # | Field | Question to Ask / Default |
|---|-------|---------------------------|
| 1 | `name` | What should we call this avatar? |
| 2 | `age` | Young Adult / Early Middle Age / Late Middle Age / Senior? |
| 3 | `gender` | Woman / Man? |
| 4 | `ethnicity` | Which ethnicity? (see enum values above) |
| 5 | `orientation` | horizontal (landscape) / vertical (portrait) / square? |
| 6 | `pose` | half_body (recommended) / close_up / full_body? |
| 7 | `style` | Realistic (recommended) / Cinematic / other? |
| 8 | `appearance` | Describe clothing, expression, lighting, background |
**If the user only provides a vague request** like "create a professional looking man", ask them to specify the missing fields OR make reasonable defaults (e.g., "Early Middle Age", "Realistic" style, "half_body" pose, "horizontal" orientation).
### Appearance Prompt Tips
The `appearance` field is a text prompt - be descriptive:
**Good prompts:**
- "Professional woman with shoulder-length brown hair, wearing a light blue button-down shirt, warm friendly smile, soft studio lighting, clean white background"
- "Young man with short black hair, casual tech startup style, wearing a dark hoodie, confident expression, modern office background with plants"
**Avoid:**
- Vague descriptions: "a nice person"
- Conflicting attributes
- Requesting specific real people
## Managing Photo Avatars
### Get Photo Avatar Details
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`
```typescript
async function getPhotoAvatar(id: string): Promise<PhotoAvatarResponse> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
return response.json();
}
```
### Delete Photo Avatar
**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar/{id}`
```typescript
async function deletePhotoAvatar(id: string): Promise<void> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{
method: "DELETE",
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
}
);
if (!response.ok) {
throw new Error("Failed to delete photo avatar");
}
}
```
### Delete Photo Avatar Group
**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar_group/{group_id}`
```typescript
async function deletePhotoAvatarGroup(groupId: string): Promise<void> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar_group/${groupId}`,
{
method: "DELETE",
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
}
);
if (!response.ok) {
throw new Error("Failed to delete photo avatar group");
}
}
```
## API Reference
| Endpoint | Method | Description |
|----------|--------|-------------|
| `upload.heygen.com/v1/asset` | POST | Upload image (returns `image_key`) |
| `/v2/photo_avatar/avatar_group/create` | POST | Create photo avatar from `image_key` |
| `/v2/photo_avatar/avatar_group/add` | POST | Add photos to existing group |
| `/v2/photo_avatar/train` | POST | Train avatar group |
| `/v2/photo_avatar/train/status/{group_id}` | GET | Check training status |
| `/v2/photo_avatar/{id}` | GET | Get photo avatar details/status |
| `/v2/photo_avatar/{id}` | DELETE | Delete photo avatar |
| `/v2/photo_avatar_group/{id}` | DELETE | Delete avatar group |
| `/v2/photo_avatar/photo/generate` | POST | Generate AI photo from text |
| `/v2/photo_avatar/generation/{id}` | GET | Check AI generation status |
| `/v2/video/av4/generate` | POST | Avatar IV video from `image_key` |
| `/v1/talking_photo.list` | GET | List all existing talking photos |
| `/v2/video/generate` | POST | Generate video with `talking_photo_id` |
## Photo Requirements
### Technical Requirements
| Aspect | Requirement |
|--------|-------------|
| Format | JPEG, PNG |
| Resolution | Minimum 512x512px |
| File size | Under 10MB |
| Face visibility | Clear, front-facing |
### Quality Guidelines
1. **Lighting** - Even, natural lighting on face
2. **Expression** - Neutral or slight smile
3. **Background** - Simple, uncluttered
4. **Face position** - Centered, not cut off
5. **Clarity** - Sharp, in focus
6. **Angle** - Straight-on or slight angle
## Best Practices
1. **Use high-quality photos** - Better input = better output
2. **Front-facing portraits** - Work best for animation
3. **Neutral expressions** - Allow for more natural animation
4. **Use Avatar IV for best quality** - Latest generation technology
5. **Train avatar groups** - Improves animation quality
6. **Reuse photo avatar IDs** - Once created, use the same `talking_photo_id` across multiple videos
## Limitations
- Photo quality significantly affects output
- Side-profile photos have limited support
- Full-body photos may not animate properly
- Some expressions may look unnatural
- Processing time varies by complexity
@@ -0,0 +1,195 @@
---
name: quota
description: Credit system, usage limits, and checking remaining quota for HeyGen
---
# HeyGen Quota and Credits
HeyGen uses a credit-based system for video generation. Understanding quota management helps prevent failed video generation requests.
## Checking Remaining Quota
### curl
```bash
curl -X GET "https://api.heygen.com/v2/user/remaining_quota" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface QuotaResponse {
error: null | string;
data: {
remaining_quota: number;
used_quota: number;
};
}
const response = await fetch("https://api.heygen.com/v2/user/remaining_quota", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data }: QuotaResponse = await response.json();
console.log(`Remaining credits: ${data.remaining_quota}`);
```
### Python
```python
import requests
import os
response = requests.get(
"https://api.heygen.com/v2/user/remaining_quota",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()["data"]
print(f"Remaining credits: {data['remaining_quota']}")
```
## Response Format
```json
{
"error": null,
"data": {
"remaining_quota": 450,
"used_quota": 50
}
}
```
## Credit Consumption
Different operations consume different amounts of credits:
| Operation | Credit Cost | Notes |
|-----------|-------------|-------|
| Standard video (1 min) | ~1 credit per minute | Varies by resolution |
| 720p video | Base rate | Standard quality |
| 1080p video | ~1.5x base rate | Higher quality |
| Video translation | Varies | Depends on video length |
| Streaming avatar | Per session | Real-time usage |
## Pre-Generation Quota Check
Always verify sufficient quota before generating videos:
```typescript
async function generateVideoWithQuotaCheck(videoConfig: VideoConfig) {
// Check quota first
const quotaResponse = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: quota } = await quotaResponse.json();
// Estimate required credits (rough estimate: 1 credit per minute)
const estimatedMinutes = videoConfig.estimatedDuration / 60;
const requiredCredits = Math.ceil(estimatedMinutes);
if (quota.remaining_quota < requiredCredits) {
throw new Error(
`Insufficient credits. Need ${requiredCredits}, have ${quota.remaining_quota}`
);
}
// Proceed with video generation
return generateVideo(videoConfig);
}
```
## Quota Management Best Practices
### 1. Monitor Usage Regularly
```typescript
async function logQuotaUsage() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
console.log({
remaining: data.remaining_quota,
used: data.used_quota,
percentUsed: (
(data.used_quota / (data.remaining_quota + data.used_quota)) *
100
).toFixed(1),
});
}
```
### 2. Set Up Alerts
```typescript
const QUOTA_WARNING_THRESHOLD = 50;
async function checkQuotaWithAlert() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.remaining_quota < QUOTA_WARNING_THRESHOLD) {
// Send alert (email, Slack, etc.)
await sendAlert(`Low HeyGen quota: ${data.remaining_quota} credits remaining`);
}
return data;
}
```
### 3. Use Test Mode for Development
When available, use test mode to avoid consuming credits during development:
```typescript
const videoConfig = {
test: true, // Use test mode during development
video_inputs: [...],
};
// Test videos may have watermarks but don't consume credits
```
## Subscription Tiers
Different subscription tiers have different quota allocations and features:
| Tier | Features |
|------|----------|
| Free | Limited credits, basic features |
| Creator | More credits, standard avatars |
| Team | Higher limits, team collaboration |
| Enterprise | Custom limits, API access, priority support |
API access typically requires Enterprise tier or higher.
## Error Handling for Quota Issues
```typescript
async function handleQuotaError(error: any) {
if (error.message.includes("quota") || error.message.includes("credit")) {
console.error("Quota exceeded. Consider:");
console.error("1. Upgrading your subscription");
console.error("2. Waiting for quota reset");
console.error("3. Purchasing additional credits");
// Check current quota
const quota = await getQuota();
console.error(`Current remaining: ${quota.remaining_quota}`);
}
throw error;
}
```
@@ -0,0 +1,705 @@
---
name: remotion-integration
description: Using HeyGen avatar videos in Remotion compositions
---
# HeyGen + Remotion Integration
This guide covers workflows for generating HeyGen avatar videos and using them in Remotion compositions.
## Quick Start
```typescript
// 1. Get avatar with default voice
const avatar = await getAvatarDetails(avatarId);
// 2. Generate video (MP4 with background - most common)
const videoId = await generateVideo({
video_inputs: [{
character: { type: "avatar", avatar_id: avatar.id, avatar_style: "normal" },
voice: { type: "text", input_text: script, voice_id: avatar.default_voice_id },
background: { type: "color", value: "#1a1a2e" },
}],
dimension: { width: 1920, height: 1080 },
});
// 3. Poll for completion (10-15+ min)
// 4. Use in Remotion with motion graphics overlaid on top
```
## Overview
A typical workflow:
1. Generate avatar video with HeyGen
2. Wait for completion and get video URL
3. Download or use URL directly in Remotion
4. Compose with other elements (backgrounds, overlays, animations)
## Choosing the Right Output Format
| Your Composition | Recommended | Why |
|------------------|-------------|-----|
| Avatar as presenter with overlays | MP4 + background | Simpler, overlays go on top |
| Loom-style (avatar over screen recording) | WebM + `closeUp`, mask in Remotion | Need transparency, apply circle mask in CSS |
| Avatar overlaid ON other video/content | WebM (transparent) | Need to see through to content behind |
| Full-screen avatar | MP4 + background | Standard approach |
**Use MP4 with background for most cases.** Use WebM when you need to see content *behind* the avatar.
**Note:** WebM only supports `normal` and `closeUp` styles. For circular framing, use CSS `border-radius: 50%` in Remotion.
## Recommended: Parallel Development Workflow
HeyGen video generation takes **10-15+ minutes**. Don't wait - work in parallel:
1. **Start HeyGen generation** - save `video_id` to a file, exit immediately
2. **Build Remotion composition** - use a placeholder or the avatar's `preview_video_url` (a short loop)
3. **Check HeyGen status** periodically or when done building
4. **Swap placeholder** for real video URL once ready
**Estimate duration from script**: ~150 words/minute speech rate, so `wordCount / 150 * 60 * fps` gives approximate frames.
**Composition tip**: Design components to work with or without the avatar video, so motion graphics can be tested independently.
## Dimension Alignment
**Critical**: Match HeyGen output dimensions to your Remotion composition.
### Common Dimension Presets
```typescript
// Shared dimension constants for both HeyGen and Remotion
const DIMENSIONS = {
landscape_1080p: { width: 1920, height: 1080 },
landscape_720p: { width: 1280, height: 720 },
portrait_1080p: { width: 1080, height: 1920 },
portrait_720p: { width: 720, height: 1280 },
square_1080p: { width: 1080, height: 1080 },
square_720p: { width: 720, height: 720 },
} as const;
type DimensionPreset = keyof typeof DIMENSIONS;
```
### HeyGen Video Generation
```typescript
// Generate HeyGen video with specific dimensions
async function generateHeyGenVideo(
script: string,
avatarId: string,
voiceId: string,
preset: DimensionPreset
): Promise<string> {
const dimension = DIMENSIONS[preset];
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: "#00FF00", // Green screen for compositing
},
},
],
dimension,
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### Remotion Composition Setup
```tsx
// remotion/src/Root.tsx
import { Composition } from "remotion";
import { AvatarComposition } from "./AvatarComposition";
const DIMENSIONS = {
landscape_1080p: { width: 1920, height: 1080 },
// ... same as above
};
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="AvatarVideo"
component={AvatarComposition}
durationInFrames={300} // Will be set dynamically
fps={30}
width={DIMENSIONS.landscape_1080p.width}
height={DIMENSIONS.landscape_1080p.height}
defaultProps={{
avatarVideoUrl: "",
}}
/>
</>
);
};
```
## Generating Avatar Video for Remotion
### Standard: MP4 with Background
Most Remotion compositions work best with MP4 + background. Overlays and motion graphics go on top:
```typescript
async function generateAvatarForRemotion(
script: string,
avatarId: string,
voiceId: string,
options: {
style?: "normal" | "closeUp" | "circle";
backgroundColor?: string;
} = {}
): Promise<string> {
const { style = "normal", backgroundColor = "#1a1a2e" } = options;
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: style,
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: backgroundColor,
},
}],
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### Transparent Background (WebM)
Only use when you need to see content *behind* the avatar (e.g., avatar overlaid on screen recording):
```typescript
// Use /v1/video.webm endpoint for transparent background
// Note: Different structure than /v2/video/generate
const response = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required: avatar pose ID
avatar_style: "normal", // Required: "normal" or "closeUp" only
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
});
```
## Using HeyGen Video in Remotion
### Important: Use OffthreadVideo for Frame-Accurate Rendering
**Always use `OffthreadVideo` instead of `Video`** for HeyGen avatar videos. The basic `Video` component uses the browser's video decoder which isn't frame-accurate, causing jitter during rendering. `OffthreadVideo` extracts frames via FFmpeg for smooth, accurate playback.
`OffthreadVideo` is included in the core `remotion` package - no additional install needed.
### Basic Usage
```tsx
// remotion/src/AvatarComposition.tsx
import { OffthreadVideo, useVideoConfig } from "remotion";
interface AvatarCompositionProps {
avatarVideoUrl: string;
}
export const AvatarComposition: React.FC<AvatarCompositionProps> = ({
avatarVideoUrl,
}) => {
return (
<div style={{ flex: 1, backgroundColor: "#1a1a2e" }}>
<OffthreadVideo
src={avatarVideoUrl}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
);
};
```
### WebM with Transparent Background (Recommended)
Using WebM from `/v1/video.webm` - no chroma keying needed:
```tsx
import { OffthreadVideo, AbsoluteFill, Sequence } from "remotion";
export const AvatarWithMotionGraphics: React.FC<{
avatarWebmUrl: string
}> = ({ avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Layer 1: Your background/content */}
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
<YourMotionGraphics />
</AbsoluteFill>
{/* Layer 2: Avatar with transparent background - use OffthreadVideo for frame-accurate rendering */}
<OffthreadVideo
src={avatarWebmUrl}
transparent
style={{
position: "absolute",
bottom: 0,
right: 0,
width: "50%",
height: "auto",
}}
/>
{/* Layer 3: Overlays on top of avatar */}
<Sequence from={30}>
<AnimatedTitle text="Welcome!" />
</Sequence>
</AbsoluteFill>
);
};
```
### Loom-Style: Circle Avatar Over Screen Recording
Use `closeUp` style + WebM, then apply circular mask in Remotion:
```tsx
import { OffthreadVideo, AbsoluteFill } from "remotion";
export const LoomStyleComposition: React.FC<{
screenRecordingUrl: string;
avatarWebmUrl: string; // Generated with avatar_style: "closeUp" via /v1/video.webm
}> = ({ screenRecordingUrl, avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Screen recording fills the frame */}
<OffthreadVideo src={screenRecordingUrl} style={{ width: "100%", height: "100%" }} />
{/* Avatar with circular mask - transparent bg shows screen behind */}
<OffthreadVideo
src={avatarWebmUrl}
transparent
style={{
position: "absolute",
bottom: 40,
left: 40,
width: 180,
height: 180,
borderRadius: "50%", // Circular mask applied in CSS
overflow: "hidden",
objectFit: "cover",
}}
/>
</AbsoluteFill>
);
};
```
**Note:** WebM doesn't support `circle` style - use `normal` or `closeUp` and apply circular masking via CSS.
### Legacy: Green Screen with Chroma Key
If using MP4 with green background (not recommended - use WebM instead):
```tsx
// Note: True chroma key requires WebGL or post-processing
// WebM transparent background is much simpler
<OffthreadVideo
src={avatarVideoUrl}
style={{
mixBlendMode: "multiply", // Basic compositing only
}}
/>
```
### Layered Composition
```tsx
import { OffthreadVideo, Sequence, useVideoConfig, Img } from "remotion";
interface LayeredAvatarProps {
avatarVideoUrl: string;
backgroundUrl: string;
logoUrl: string;
title: string;
}
export const LayeredAvatarComposition: React.FC<LayeredAvatarProps> = ({
avatarVideoUrl,
backgroundUrl,
logoUrl,
title,
}) => {
const { fps } = useVideoConfig();
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{/* Layer 1: Background */}
<Img
src={backgroundUrl}
style={{
position: "absolute",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
{/* Layer 2: Avatar video - use OffthreadVideo to prevent jitter */}
<OffthreadVideo
src={avatarVideoUrl}
style={{
position: "absolute",
bottom: 0,
right: 0,
width: "40%",
height: "auto",
}}
/>
{/* Layer 3: Title (appears after 1 second) */}
<Sequence from={fps}>
<div
style={{
position: "absolute",
top: 50,
left: 50,
color: "white",
fontSize: 48,
fontWeight: "bold",
}}
>
{title}
</div>
</Sequence>
{/* Layer 4: Logo */}
<Img
src={logoUrl}
style={{
position: "absolute",
top: 20,
right: 20,
width: 100,
height: "auto",
}}
/>
</div>
);
};
```
## Complete Workflow
### Generate and Compose
```typescript
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
async function generateAvatarVideoForRemotion(
script: string,
outputPath: string
) {
// 1. Generate HeyGen video
console.log("Generating HeyGen avatar video...");
const videoId = await generateHeyGenVideo(
script,
"josh_lite3_20230714",
"1bd001e7e50f421d891986aad5158bc8",
"landscape_1080p"
);
// 2. Wait for completion
console.log("Waiting for HeyGen video...");
const avatarVideoUrl = await waitForVideo(videoId);
console.log(`HeyGen video ready: ${avatarVideoUrl}`);
// 3. Get video duration for Remotion
const avatarDuration = await getVideoDuration(avatarVideoUrl);
const durationInFrames = Math.ceil(avatarDuration * 30); // 30 fps
// 4. Bundle Remotion project
console.log("Bundling Remotion project...");
const bundleLocation = await bundle({
entryPoint: "./remotion/src/index.ts",
});
// 5. Select composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id: "AvatarVideo",
inputProps: {
avatarVideoUrl,
},
});
// 6. Render final video
console.log("Rendering final composition...");
await renderMedia({
composition: {
...composition,
durationInFrames,
},
serveUrl: bundleLocation,
codec: "h264",
outputLocation: outputPath,
inputProps: {
avatarVideoUrl,
},
});
console.log(`Final video rendered: ${outputPath}`);
return outputPath;
}
```
### Dynamic Duration with calculateMetadata
```tsx
// remotion/src/AvatarComposition.tsx
import { CalculateMetadataFunction } from "remotion";
export const calculateAvatarMetadata: CalculateMetadataFunction<
AvatarCompositionProps
> = async ({ props }) => {
// Fetch video duration from HeyGen video
const duration = await getVideoDurationInSeconds(props.avatarVideoUrl);
return {
durationInFrames: Math.ceil(duration * 30),
fps: 30,
width: 1920,
height: 1080,
};
};
// In Root.tsx
<Composition
id="AvatarVideo"
component={AvatarComposition}
calculateMetadata={calculateAvatarMetadata}
defaultProps={{
avatarVideoUrl: "",
}}
/>
```
## Best Practices
### 1. Use Green Screen for Flexibility
Generate HeyGen videos with green screen background when you want to composite:
```typescript
background: {
type: "color",
value: "#00FF00", // Pure green for chroma key
}
```
### 2. Match Frame Rates
HeyGen default is 25 fps. Consider this when setting Remotion fps:
```typescript
// Option 1: Match HeyGen's 25 fps
fps: 25
// Option 2: Use 30 fps with playback rate adjustment
<OffthreadVideo
src={avatarVideoUrl}
playbackRate={25/30} // Slow down slightly to match
/>
```
### 3. URL vs Download: When to Use Each
**Use URL directly** when:
- Previewing in Remotion Studio (`npm run dev`)
- URL won't expire before render completes
- You want faster iteration during development
```tsx
// Direct URL usage - simpler, faster for dev
<OffthreadVideo src={avatarVideoUrl} />
```
**Download first** when:
- URL has expiration (HeyGen URLs expire after ~24 hours)
- Rendering will happen later or repeatedly
- Network reliability is a concern
- You need offline rendering
```typescript
// Download with retry for reliability
async function downloadVideoWithRetry(
url: string,
outputPath: string,
maxRetries = 5
): Promise<string> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const buffer = await response.arrayBuffer();
await fs.promises.writeFile(outputPath, Buffer.from(buffer));
return outputPath;
} catch (error) {
const delay = 2000 * Math.pow(2, attempt);
console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Download failed after retries");
}
// Use local file in Remotion
const localPath = await downloadVideoWithRetry(avatarVideoUrl, "./public/avatar.mp4");
```
**Hybrid approach** (recommended for production):
```typescript
// Save both URL and local path in metadata
const metadata = {
videoUrl: result.video_url, // For quick preview
localPath: "./public/avatar.mp4", // For reliable rendering
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // URL expiration
};
// In Remotion component, prefer local if available
const videoSrc = fs.existsSync(localPath) ? staticFile("avatar.mp4") : avatarVideoUrl;
```
### 4. Handle Avatar Positioning
Common avatar positions in compositions:
```typescript
const AVATAR_POSITIONS = {
fullscreen: { width: "100%", height: "100%", position: "center" },
bottomRight: { width: "40%", bottom: 0, right: 0 },
bottomLeft: { width: "40%", bottom: 0, left: 0 },
pictureInPicture: { width: "25%", bottom: 20, right: 20 },
leftThird: { width: "33%", left: 0, height: "100%" },
};
```
## Output Formats
### HeyGen Output
- Format: MP4 (H.264)
- Audio: AAC
- Resolution: As specified in request
### Remotion Output
- Codec: H.264 (default), VP8, VP9, ProRes
- Match or exceed HeyGen quality settings
```typescript
await renderMedia({
codec: "h264",
crf: 18, // High quality
// ...
});
```
## Troubleshooting
### Video Not Playing in Remotion
1. Check URL accessibility (CORS issues)
2. Verify video format compatibility
3. Try downloading locally first
### Dimension Mismatch
Ensure both HeyGen and Remotion use identical dimensions:
```typescript
// Shared config
const VIDEO_CONFIG = {
width: 1920,
height: 1080,
fps: 30,
};
// HeyGen
dimension: { width: VIDEO_CONFIG.width, height: VIDEO_CONFIG.height }
// Remotion
<Composition width={VIDEO_CONFIG.width} height={VIDEO_CONFIG.height} />
```
### Video Jitter During Rendering
If avatar video appears jittery or stuttery in rendered output:
1. **Use `OffthreadVideo` instead of `Video`** - The basic `Video` component uses the browser's video decoder which isn't frame-accurate
2. Update imports (no additional install needed - it's in core `remotion`):
```tsx
// Before (causes jitter)
import { Video } from "remotion";
// After (frame-accurate)
import { OffthreadVideo } from "remotion";
```
3. For WebM with transparency, add the `transparent` prop:
```tsx
<OffthreadVideo src={avatarWebmUrl} transparent />
```
### Audio Sync Issues
If avatar audio drifts:
- Verify source video frame rate
- Check for encoding issues
- Consider re-encoding with consistent settings
@@ -0,0 +1,322 @@
---
name: scripts
description: Writing effective scripts for HeyGen AI avatar videos
---
# Writing Scripts for HeyGen Videos
Scripts for AI avatar videos have different requirements than scripts for human presenters. This guide covers best practices for writing scripts that sound natural and render well.
## Script Basics
### Speech Rate and Duration
Typical speech is approximately **150 words per minute** at normal speed (1.0x). Use this as a rough estimate for planning script length.
| Script Length | Approximate Duration |
|---------------|---------------------|
| 75 words | 30 seconds |
| 150 words | 1 minute |
| 300 words | 2 minutes |
| 450 words | 3 minutes |
| 750 words | 5 minutes |
```typescript
// Estimate video duration from script
function estimateDuration(script: string, speed: number = 1.0): number {
const words = script.split(/\s+/).filter(w => w.length > 0).length;
const wordsPerMinute = 150 * speed;
return words / wordsPerMinute * 60; // seconds
}
// Estimate frames for Remotion
function estimateFrames(script: string, fps: number = 30, speed: number = 1.0): number {
const durationSeconds = estimateDuration(script, speed);
return Math.ceil(durationSeconds * fps);
}
```
### Sentence Structure
**Keep sentences short.** AI voices handle shorter sentences more naturally.
| Guideline | Example |
|-----------|---------|
| **Good**: 10-20 words per sentence | "Our platform helps teams collaborate. It syncs in real-time across all devices." |
| **Avoid**: 30+ word run-on sentences | "Our platform helps teams collaborate more effectively by providing real-time synchronization across all devices while also offering offline support and automatic conflict resolution." |
### Punctuation Affects Delivery
| Punctuation | Effect |
|-------------|--------|
| Period `.` | Full stop, natural pause |
| Comma `,` | Brief pause |
| Question mark `?` | Rising intonation |
| Exclamation `!` | Emphasis (use sparingly) |
| Ellipsis `...` | Trailing off, slight pause |
## Adding Pauses with Break Tags
Use SSML-style `<break>` tags for precise pause control:
```
<break time="Xs"/>
```
Where `X` is seconds (e.g., `0.5s`, `1s`, `1.5s`, `2s`).
### Formatting Rules
| Rule | Correct | Incorrect |
|------|---------|-----------|
| Space before tag | `word <break time="1s"/>` | `word<break time="1s"/>` |
| Space after tag | `<break time="1s"/> word` | `<break time="1s"/>word` |
| Use seconds with "s" | `<break time="1.5s"/>` | `<break time="1500ms"/>` |
| Self-closing tag | `<break time="1s"/>` | `<break time="1s"></break>` |
### When to Use Pauses
| Situation | Recommended Pause | Example |
|-----------|-------------------|---------|
| After greeting | 0.5-1s | `Hello! <break time="0.5s"/> Welcome to...` |
| Between sections | 1-1.5s | `...that's feature one. <break time="1.5s"/> Now let's look at...` |
| Before key point | 0.5s | `The most important thing is <break time="0.5s"/> consistency.` |
| For dramatic effect | 1.5-2s | `And the winner is... <break time="2s"/> you!` |
| After question | 1s | `Sound good? <break time="1s"/> Let's get started.` |
| List items | 0.5s | `First, speed. <break time="0.5s"/> Second, reliability.` |
### Pause Duration Guide
| Duration | Feel | Use For |
|----------|------|---------|
| 0.3-0.5s | Brief breath | Between clauses, light emphasis |
| 0.5-1s | Natural pause | Sentence breaks, transitions |
| 1-1.5s | Deliberate pause | Section changes, setup for key points |
| 1.5-2s | Dramatic | Reveals, important announcements |
| 2s+ | Long pause | Use sparingly, can feel unnatural |
### Examples
```typescript
// Section transitions
const script = `
Welcome to our product overview. <break time="1s"/>
Today I'll cover three key features. <break time="0.5s"/>
First, let's look at the dashboard. <break time="1.5s"/>
As you can see, it's designed for simplicity. <break time="0.5s"/>
Every action is just one click away.
`;
// Building suspense
const announcement = `
We've been working on something special. <break time="1s"/>
After months of development... <break time="1.5s"/>
I'm excited to announce <break time="0.5s"/> our new AI assistant.
`;
// List with rhythm
const features = `
Our platform offers three core benefits. <break time="0.5s"/>
Speed. <break time="0.5s"/>
Reliability. <break time="0.5s"/>
And simplicity. <break time="1s"/>
Let me show you each one.
`;
```
### Consecutive Breaks
Multiple consecutive breaks are combined:
```typescript
// These two breaks:
"Hello <break time=\"1s\"/> <break time=\"0.5s\"/> world"
// Are treated as a single 1.5s pause
```
## Script Structure Templates
### Product Demo (60 seconds, ~150 words)
```typescript
const productDemo = `
Hi, I'm [Name], and I'm excited to show you [Product]. <break time="1s"/>
[Product] helps you [main benefit] in just [timeframe]. <break time="0.5s"/>
Here's how it works. <break time="1s"/>
First, [step 1]. <break time="0.5s"/>
Then, [step 2]. <break time="0.5s"/>
And finally, [step 3]. <break time="1s"/>
What used to take [old time] now takes [new time]. <break time="0.5s"/>
Ready to get started? <break time="0.5s"/>
Visit [website] today.
`;
```
### Tutorial Introduction (90 seconds, ~225 words)
```typescript
const tutorial = `
Welcome to this tutorial on [topic]. <break time="0.5s"/>
I'm [Name], and I'll guide you through everything you need to know. <break time="1s"/>
By the end of this video, you'll be able to [outcome 1], [outcome 2], and [outcome 3]. <break time="1s"/>
Let's start with the basics. <break time="1.5s"/>
[Section 1 content - 2-3 sentences] <break time="1s"/>
Now that you understand [concept], let's move on to [next topic]. <break time="1.5s"/>
[Section 2 content - 2-3 sentences] <break time="1s"/>
And finally, let's cover [last topic]. <break time="1.5s"/>
[Section 3 content - 2-3 sentences] <break time="1s"/>
That's everything you need to get started. <break time="0.5s"/>
If you have questions, leave a comment below. <break time="0.5s"/>
Thanks for watching!
`;
```
### Announcement (30 seconds, ~75 words)
```typescript
const announcement = `
Big news! <break time="0.5s"/>
We're thrilled to announce [announcement]. <break time="1s"/>
This means [benefit 1] and [benefit 2] for all our users. <break time="0.5s"/>
Starting [date], you'll be able to [new capability]. <break time="1s"/>
Head to [location] to learn more. <break time="0.5s"/>
We can't wait to hear what you think!
`;
```
## Writing Tips for AI Voices
### Do
- **Write conversationally** - Read it aloud to check flow
- **Use contractions** - "We're" not "We are", "It's" not "It is"
- **Break up long sentences** - Split at natural pause points
- **Spell out abbreviations** - "API" may sound like "a pee eye"
- **Add pauses for emphasis** - Guide the listener's attention
- **End sections clearly** - Don't trail off mid-thought
### Avoid
- **Jargon without context** - Explain technical terms
- **Long parentheticals** - Move to separate sentences
- **Ambiguous pronunciations** - "read" (present) vs "read" (past)
- **Excessive exclamation marks** - One per script is usually enough
- **Run-on sentences** - Break into digestible chunks
- **Dense information** - Space out facts with pauses
### Pronunciation Hints
For words that might be mispronounced, spell phonetically or add hints:
```typescript
// Technical terms
const script1 = "Our API (A-P-I) handles authentication...";
// Ambiguous words
const script2 = "I read (red) the documentation yesterday...";
// Brand names
const script3 = "Welcome to HeyGen (hey-jen)...";
```
## Multi-Scene Scripts
When splitting scripts across scenes (for different backgrounds or avatars):
```typescript
const multiSceneVideo = {
video_inputs: [
{
// Scene 1: Introduction
character: { type: "avatar", avatar_id: "josh_lite3_20230714", avatar_style: "normal" },
voice: {
type: "text",
input_text: "Welcome to our quarterly update. <break time=\"1s\"/> I'm Josh, and I'll walk you through the highlights.",
voice_id: "voice_id_here",
},
background: { type: "color", value: "#1a1a2e" },
},
{
// Scene 2: Main content (different background)
character: { type: "avatar", avatar_id: "josh_lite3_20230714", avatar_style: "normal" },
voice: {
type: "text",
input_text: "Let's start with revenue. <break time=\"0.5s\"/> We grew 25 percent quarter over quarter. <break time=\"1s\"/> Here's what drove that growth.",
voice_id: "voice_id_here",
},
background: { type: "image", url: "https://..." },
},
// ... more scenes
],
};
```
### Scene Transition Tips
- End each scene with a complete thought
- Start new scenes with brief context
- Maintain consistent tone across scenes
- Use pauses at scene starts to let visuals register
## Testing Your Script
Before generating the full video:
1. **Read aloud** - Time yourself, check for awkward phrasing
2. **Count words** - Verify expected duration
3. **Check break tags** - Ensure proper spacing and syntax
4. **Preview with short clip** - Generate a 10-second test if unsure about pronunciation
```typescript
// Test a small portion first
const testScript = script.split('.').slice(0, 2).join('.') + '.';
const testVideoId = await generateVideo({
video_inputs: [{
character: { type: "avatar", avatar_id: avatarId, avatar_style: "normal" },
voice: { type: "text", input_text: testScript, voice_id: voiceId },
}],
dimension: { width: 1280, height: 720 }, // Lower res for test
});
```
## Voice Speed Adjustment
Adjust delivery speed in the voice configuration:
```typescript
voice: {
type: "text",
input_text: script,
voice_id: "voice_id",
speed: 1.1, // Slightly faster (range: 0.5 - 2.0)
}
```
| Speed | Effect | Use Case |
|-------|--------|----------|
| 0.8-0.9 | Slower, deliberate | Complex topics, older audiences |
| 1.0 | Normal | General use |
| 1.1-1.2 | Slightly faster | Energetic content, younger audiences |
| 1.3+ | Fast | Use sparingly, may reduce clarity |
See [voices.md](voices.md) for full voice configuration options.
@@ -0,0 +1,433 @@
---
name: templates
description: Template listing and variable replacement for HeyGen videos
---
# Video Templates
HeyGen templates allow you to create reusable video structures with variable placeholders, enabling personalized video generation at scale.
## Listing Templates
### curl
```bash
curl -X GET "https://api.heygen.com/v2/templates" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Template {
template_id: string;
name: string;
thumbnail_url: string;
variables: TemplateVariable[];
}
interface TemplateVariable {
name: string;
type: "text" | "image" | "audio";
properties?: {
max_length?: number;
default_value?: string;
};
}
interface TemplatesResponse {
error: null | string;
data: {
templates: Template[];
};
}
async function listTemplates(): Promise<Template[]> {
const response = await fetch("https://api.heygen.com/v2/templates", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: TemplatesResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.templates;
}
```
### Python
```python
import requests
import os
def list_templates() -> list:
response = requests.get(
"https://api.heygen.com/v2/templates",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["templates"]
```
## Response Format
```json
{
"error": null,
"data": {
"templates": [
{
"template_id": "template_abc123",
"name": "Product Announcement",
"thumbnail_url": "https://files.heygen.ai/...",
"variables": [
{
"name": "product_name",
"type": "text",
"properties": {
"max_length": 50
}
},
{
"name": "presenter_script",
"type": "text",
"properties": {
"max_length": 500
}
},
{
"name": "product_image",
"type": "image"
}
]
}
]
}
}
```
## Getting Template Details
### curl
```bash
curl -X GET "https://api.heygen.com/v2/template/{template_id}" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
async function getTemplate(templateId: string): Promise<Template> {
const response = await fetch(
`https://api.heygen.com/v2/template/${templateId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
## Generating Video from Template
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `variables` | object | ✓ | Key-value pairs matching template variables |
| `test` | boolean | | Test mode (watermarked, no credits) |
| `title` | string | | Video name for organization |
| `callback_id` | string | | Custom ID for webhook tracking |
| `callback_url` | string | | URL for completion notification |
**Note:** The `variables` object keys must match the template's defined variable names. Check template details to see which variables are defined.
### curl
```bash
curl -X POST "https://api.heygen.com/v2/template/{template_id}/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test": false,
"variables": {
"product_name": "SuperWidget Pro",
"presenter_script": "Introducing our latest innovation!",
"product_image": "https://example.com/product.jpg"
}
}'
```
### TypeScript
```typescript
interface TemplateGenerateRequest {
variables: Record<string, string>; // Required
test?: boolean;
title?: string;
callback_id?: string;
callback_url?: string;
}
interface TemplateGenerateResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateFromTemplate(
templateId: string,
variables: Record<string, string>,
test: boolean = false
): Promise<string> {
const response = await fetch(
`https://api.heygen.com/v2/template/${templateId}/generate`,
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ test, variables }),
}
);
const json: TemplateGenerateResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Python
```python
def generate_from_template(template_id: str, variables: dict, test: bool = False) -> str:
response = requests.post(
f"https://api.heygen.com/v2/template/{template_id}/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json={
"test": test,
"variables": variables
}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["video_id"]
```
## Variable Types
### Text Variables
For dynamic text content:
```typescript
const variables = {
customer_name: "John Smith",
product_name: "SuperWidget Pro",
price: "$99.99",
cta_text: "Order Now!",
};
```
### Image Variables
For dynamic images (backgrounds, product shots):
```typescript
const variables = {
product_image: "https://example.com/product.jpg",
logo: "https://example.com/logo.png",
background: "https://example.com/bg.jpg",
};
```
### Audio Variables
For custom audio content:
```typescript
const variables = {
background_music: "https://example.com/music.mp3",
custom_voiceover: "https://example.com/voiceover.mp3",
};
```
## Batch Video Generation
Generate multiple personalized videos from a template:
```typescript
interface PersonalizationData {
name: string;
email: string;
company: string;
customMessage: string;
}
async function batchGenerateVideos(
templateId: string,
recipients: PersonalizationData[]
): Promise<string[]> {
const videoIds: string[] = [];
for (const recipient of recipients) {
const variables = {
recipient_name: recipient.name,
company_name: recipient.company,
personalized_message: recipient.customMessage,
};
const videoId = await generateFromTemplate(templateId, variables);
videoIds.push(videoId);
// Rate limiting: add delay between requests
await new Promise((r) => setTimeout(r, 1000));
}
return videoIds;
}
// Usage
const recipients = [
{
name: "John Smith",
email: "john@example.com",
company: "Acme Inc",
customMessage: "Thanks for your interest in our product!",
},
{
name: "Jane Doe",
email: "jane@example.com",
company: "Tech Corp",
customMessage: "We'd love to show you a demo!",
},
];
const videoIds = await batchGenerateVideos("template_abc123", recipients);
```
## Template Validation
Validate variables before generating:
```typescript
function validateTemplateVariables(
template: Template,
variables: Record<string, string>
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
for (const templateVar of template.variables) {
const value = variables[templateVar.name];
// Check if required variable is provided
if (!value) {
errors.push(`Missing required variable: ${templateVar.name}`);
continue;
}
// Check text length limits
if (templateVar.type === "text" && templateVar.properties?.max_length) {
if (value.length > templateVar.properties.max_length) {
errors.push(
`Variable "${templateVar.name}" exceeds max length of ${templateVar.properties.max_length}`
);
}
}
// Validate image URLs
if (templateVar.type === "image") {
try {
new URL(value);
} catch {
errors.push(`Variable "${templateVar.name}" is not a valid URL`);
}
}
}
return {
valid: errors.length === 0,
errors,
};
}
```
## Complete Template Workflow
```typescript
async function createPersonalizedVideo(
templateId: string,
personalization: Record<string, string>
): Promise<string> {
// 1. Get template details
const template = await getTemplate(templateId);
console.log(`Using template: ${template.name}`);
// 2. Validate variables
const validation = validateTemplateVariables(template, personalization);
if (!validation.valid) {
throw new Error(`Validation errors: ${validation.errors.join(", ")}`);
}
// 3. Generate video
console.log("Generating video...");
const videoId = await generateFromTemplate(templateId, personalization);
console.log(`Video ID: ${videoId}`);
// 4. Wait for completion
const videoUrl = await waitForVideo(videoId);
console.log(`Video ready: ${videoUrl}`);
return videoUrl;
}
// Usage
const videoUrl = await createPersonalizedVideo("template_abc123", {
customer_name: "John Smith",
product_name: "SuperWidget Pro",
offer_details: "Get 20% off your first order!",
});
```
## Best Practices
1. **Design for flexibility** - Create templates with generic placeholders
2. **Set reasonable limits** - Define max lengths for text variables
3. **Validate inputs** - Check variable values before generating
4. **Use test mode** - Test with `test: true` to verify before production
5. **Implement rate limiting** - Add delays for batch generation
6. **Cache template data** - Reduce API calls by caching template details
7. **Error handling** - Gracefully handle generation failures
## Use Cases
- **Sales outreach** - Personalized prospect videos
- **Customer onboarding** - Welcome videos with customer name
- **Product updates** - Announcements with dynamic content
- **Training** - Customized training modules
- **Marketing campaigns** - Targeted promotional videos
@@ -0,0 +1,316 @@
---
name: text-overlays
description: Adding text overlays with fonts and positioning to HeyGen videos
---
# Text Overlays
Add text overlays to your HeyGen videos for titles, captions, lower thirds, and other on-screen text elements.
## Basic Text Overlay
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to our presentation!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
// Text overlay configuration (if supported in your API tier)
// Note: Availability varies by plan
};
```
## Text Overlay Configuration
Text overlays typically support these properties:
```typescript
interface TextOverlay {
text: string;
x: number; // X position (pixels or percentage)
y: number; // Y position (pixels or percentage)
width?: number; // Text box width
height?: number; // Text box height
font_family?: string;
font_size?: number;
font_color?: string;
background_color?: string;
text_align?: "left" | "center" | "right";
duration?: {
start: number; // Start time in seconds
end: number; // End time in seconds
};
}
```
## Positioning Text
### Coordinate System
- **Origin**: Top-left corner (0, 0)
- **X-axis**: Increases to the right
- **Y-axis**: Increases downward
- **Units**: Typically pixels or percentage of video dimensions
### Common Positions
For a 1920x1080 video:
| Position | X | Y | Description |
|----------|---|---|-------------|
| Top-left | 50 | 50 | Upper left corner |
| Top-center | 960 | 50 | Top center |
| Top-right | 1870 | 50 | Upper right corner |
| Center | 960 | 540 | Dead center |
| Bottom-left | 50 | 1030 | Lower third left |
| Bottom-center | 960 | 1030 | Lower third center |
### Position Helper Function
```typescript
interface Position {
x: number;
y: number;
}
function getTextPosition(
location: "top-left" | "top-center" | "top-right" | "center" | "bottom-left" | "bottom-center" | "bottom-right",
videoWidth: number,
videoHeight: number,
padding: number = 50
): Position {
const positions: Record<string, Position> = {
"top-left": { x: padding, y: padding },
"top-center": { x: videoWidth / 2, y: padding },
"top-right": { x: videoWidth - padding, y: padding },
"center": { x: videoWidth / 2, y: videoHeight / 2 },
"bottom-left": { x: padding, y: videoHeight - padding },
"bottom-center": { x: videoWidth / 2, y: videoHeight - padding },
"bottom-right": { x: videoWidth - padding, y: videoHeight - padding },
};
return positions[location];
}
```
## Font Styling
### Available Font Properties
```typescript
const textStyle = {
font_family: "Arial",
font_size: 48,
font_color: "#FFFFFF",
font_weight: "bold",
background_color: "rgba(0, 0, 0, 0.5)",
text_align: "center",
};
```
### Common Font Families
| Font | Style | Use Case |
|------|-------|----------|
| Arial | Sans-serif | Clean, universal |
| Helvetica | Sans-serif | Modern, professional |
| Times New Roman | Serif | Traditional, formal |
| Georgia | Serif | Elegant, readable |
| Roboto | Sans-serif | Modern, digital |
| Open Sans | Sans-serif | Friendly, accessible |
## Common Text Overlay Patterns
### Title Card
```typescript
const titleOverlay = {
text: "Product Demo",
x: 960,
y: 540,
font_family: "Arial",
font_size: 72,
font_color: "#FFFFFF",
text_align: "center",
duration: {
start: 0,
end: 3,
},
};
```
### Lower Third (Name/Title)
```typescript
const lowerThirdOverlay = {
text: "John Smith\nCEO, Company Inc.",
x: 100,
y: 900,
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 102, 204, 0.9)",
text_align: "left",
duration: {
start: 2,
end: 8,
},
};
```
### Call to Action
```typescript
const ctaOverlay = {
text: "Visit example.com",
x: 960,
y: 1000,
font_family: "Arial",
font_size: 42,
font_color: "#FFD700",
text_align: "center",
duration: {
start: 25,
end: 30,
},
};
```
## Creating Text Overlay Templates
```typescript
interface TextOverlayTemplate {
name: string;
style: Partial<TextOverlay>;
}
const templates: TextOverlayTemplate[] = [
{
name: "title",
style: {
font_family: "Arial",
font_size: 72,
font_color: "#FFFFFF",
text_align: "center",
},
},
{
name: "subtitle",
style: {
font_family: "Arial",
font_size: 42,
font_color: "#CCCCCC",
text_align: "center",
},
},
{
name: "lower-third",
style: {
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
text_align: "left",
},
},
{
name: "caption",
style: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.5)",
text_align: "center",
},
},
];
function createTextOverlay(
text: string,
templateName: string,
position: Position,
duration?: { start: number; end: number }
): TextOverlay {
const template = templates.find((t) => t.name === templateName);
if (!template) {
throw new Error(`Template "${templateName}" not found`);
}
return {
text,
x: position.x,
y: position.y,
...template.style,
duration,
};
}
```
## Timing Text Overlays
Coordinate text appearance with your script:
```typescript
// Script with timing markers
const script = `
Hello and welcome. [0:00 - 0:03]
Let me show you our features. [0:03 - 0:08]
First, we have analytics. [0:08 - 0:15]
Get started today! [0:15 - 0:20]
`;
// Matching text overlays
const overlays = [
{
text: "Welcome",
duration: { start: 0, end: 3 },
...titleStyle,
},
{
text: "Feature Overview",
duration: { start: 3, end: 8 },
...subtitleStyle,
},
{
text: "Analytics Dashboard",
duration: { start: 8, end: 15 },
...lowerThirdStyle,
},
{
text: "www.example.com",
duration: { start: 15, end: 20 },
...ctaStyle,
},
];
```
## Best Practices
1. **Readability** - Use sufficient contrast between text and background
2. **Size** - Ensure text is large enough to read on mobile devices
3. **Duration** - Give viewers enough time to read (rule of thumb: 3 seconds minimum)
4. **Positioning** - Don't overlap with the avatar's face
5. **Consistency** - Use consistent fonts and styles throughout
6. **Accessibility** - Consider color-blind friendly palettes
## Limitations
- Text overlay support varies by subscription tier
- Some advanced styling options may not be available via API
- Complex animations may require post-production tools
- For auto-generated captions, see [captions.md](captions.md)
@@ -0,0 +1,770 @@
---
name: video-generation
description: POST /v2/video/generate workflow and multi-scene videos for HeyGen
---
# Video Generation
## Table of Contents
- [Video Output Formats](#video-output-formats)
- [Basic Video Generation](#basic-video-generation)
- [Request Fields](#request-fields)
- [Video Configuration Options](#video-configuration-options)
- [Multi-Scene Videos](#multi-scene-videos)
- [Using Different Character Types](#using-different-character-types)
- [Voice Input Types](#voice-input-types)
- [Complete Workflow Example](#complete-workflow-example)
- [Error Handling](#error-handling)
- [Script Length Limits](#script-length-limits)
- [Adding Pauses to Scripts](#adding-pauses-to-scripts)
- [Test Mode](#test-mode)
- [Production-Ready Workflow](#production-ready-workflow)
- [Transparent Background Videos (WebM)](#transparent-background-videos-webm)
- [Best Practices](#best-practices)
---
The `/v2/video/generate` endpoint is the primary way to create AI avatar videos with HeyGen.
## Video Output Formats
| Endpoint | Format | Use Case |
|----------|--------|----------|
| `/v2/video/generate` | MP4 | **Standard** - videos with background (most common) |
| `/v1/video.webm` | WebM | Transparent background - only when needed |
Use MP4 with background for most cases. WebM is only needed when you want to see content *behind* the avatar (e.g., overlaying avatar on a screen recording).
## Basic Video Generation
### curl
```bash
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [
{
"character": {
"type": "avatar",
"avatar_id": "josh_lite3_20230714",
"avatar_style": "normal"
},
"voice": {
"type": "text",
"input_text": "Hello! Welcome to HeyGen.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8"
}
}
],
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
## Request Fields
### Top-Level Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `video_inputs` | array | ✓ | Array of 1-50 video input objects |
| `dimension` | object | | Video dimensions `{width, height}` |
| `title` | string | | Video name for organization |
| `test` | boolean | | Test mode (watermarked, no credits) |
| `caption` | boolean | | Enable auto-captions |
| `callback_id` | string | | Custom ID for webhook tracking |
| `callback_url` | string | | URL for completion notification |
| `folder_id` | string | | Storage folder ID |
### video_inputs[].character Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | ✓ | `"avatar"` or `"talking_photo"` |
| `avatar_id` | string | ✓* | Avatar ID (*required when type is "avatar") |
| `talking_photo_id` | string | ✓* | Photo ID (*required when type is "talking_photo") |
| `avatar_style` | string | | `"normal"`, `"closeUp"`, or `"circle"` |
| `scale` | number | | Avatar scale factor |
| `offset` | object | | Position offset `{x, y}` |
### video_inputs[].voice Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | ✓ | `"text"`, `"audio"`, or `"silence"` |
| `voice_id` | string | ✓* | Voice ID (*required when type is "text") |
| `input_text` | string | ✓* | Script text (*required when type is "text") |
| `audio_url` | string | ✓* | Audio URL (*required when type is "audio") |
| `duration` | number | ✓* | Duration in seconds (*required when type is "silence") |
| `speed` | number | | Speech speed 0.5-2.0 (default 1.0) |
| `pitch` | number | | Voice pitch -20 to 20 (default 0) |
### video_inputs[].background Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | | `"color"`, `"image"`, or `"video"` |
| `value` | string | | Hex color (when type is "color") |
| `url` | string | | Image/video URL (when type is "image"/"video") |
| `fit` | string | | `"cover"` or `"contain"` |
### TypeScript
```typescript
// Required fields have no '?' - optional fields have '?'
interface VideoInput {
character: {
type: "avatar" | "talking_photo"; // Required
avatar_id?: string; // Required when type="avatar"
talking_photo_id?: string; // Required when type="talking_photo"
avatar_style?: "normal" | "closeUp" | "circle";
scale?: number;
offset?: { x: number; y: number };
};
voice: {
type: "text" | "audio" | "silence"; // Required
input_text?: string; // Required when type="text"
voice_id?: string; // Required when type="text"
audio_url?: string; // Required when type="audio"
duration?: number; // Required when type="silence"
speed?: number;
pitch?: number;
};
background?: {
type?: "color" | "image" | "video";
value?: string;
url?: string;
fit?: "cover" | "contain";
};
}
interface VideoGenerateRequest {
video_inputs: VideoInput[]; // Required
dimension?: { width: number; height: number };
test?: boolean;
title?: string;
caption?: boolean;
callback_id?: string;
callback_url?: string;
folder_id?: string;
}
interface VideoGenerateResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateVideo(config: VideoGenerateRequest): Promise<string> {
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json: VideoGenerateResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Python
```python
import requests
import os
def generate_video(config: dict) -> str:
response = requests.post(
"https://api.heygen.com/v2/video/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json=config
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["video_id"]
```
## Video Configuration Options
### Full Configuration Example
```typescript
const fullConfig: VideoGenerateRequest = {
// Test mode (no credits consumed, watermarked output)
test: false,
// Video title (for organization)
title: "Product Demo Video",
// Video dimensions
dimension: {
width: 1920,
height: 1080,
},
// Video scenes/inputs
video_inputs: [
{
// Avatar configuration
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
// Voice configuration
voice: {
type: "text",
input_text: "Welcome to our product demonstration!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.0,
pitch: 0,
},
// Background configuration
background: {
type: "color",
value: "#FFFFFF",
},
},
],
};
```
## Multi-Scene Videos
Create videos with multiple scenes:
```typescript
const multiSceneConfig = {
video_inputs: [
// Scene 1: Introduction
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Today I'll show you three key features.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
// Scene 2: Feature 1
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "closeUp",
},
voice: {
type: "text",
input_text: "First, let's look at our dashboard.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/dashboard-bg.jpg",
},
},
// Scene 3: Conclusion
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Thanks for watching! Try it today.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
## Using Different Character Types
### Avatar
```typescript
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal"
}
}
```
### Talking Photo
```typescript
{
character: {
type: "talking_photo",
talking_photo_id: "your_talking_photo_id"
}
}
```
## Voice Input Types
### Text-to-Speech
```typescript
{
voice: {
type: "text",
input_text: "Your script here",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.0, // 0.5 - 2.0
pitch: 0 // -20 to 20
}
}
```
### Custom Audio
```typescript
{
voice: {
type: "audio",
audio_url: "https://example.com/your-audio.mp3"
}
}
```
## Complete Workflow Example
```typescript
async function createVideo(script: string, avatarId: string, voiceId: string) {
// 1. Generate video
console.log("Starting video generation...");
const videoId = await generateVideo({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: "#FFFFFF",
},
},
],
dimension: { width: 1920, height: 1080 },
});
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
console.log("Waiting for video completion...");
const videoUrl = await waitForVideo(videoId);
console.log(`Video ready: ${videoUrl}`);
return videoUrl;
}
// Helper function for polling
async function waitForVideo(videoId: string): Promise<string> {
const maxAttempts = 60;
const pollInterval = 10000; // 10 seconds
for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.status === "completed") {
return data.video_url;
} else if (data.status === "failed") {
throw new Error(data.failure_message || "Video generation failed");
}
await new Promise((r) => setTimeout(r, pollInterval));
}
throw new Error("Video generation timed out");
}
```
## Error Handling
```typescript
async function generateVideoSafe(config: VideoGenerateRequest) {
try {
const videoId = await generateVideo(config);
return { success: true, videoId };
} catch (error) {
// Common errors
if (error.message.includes("quota")) {
console.error("Insufficient credits");
} else if (error.message.includes("avatar")) {
console.error("Invalid avatar ID");
} else if (error.message.includes("voice")) {
console.error("Invalid voice ID");
} else if (error.message.includes("script")) {
console.error("Script too long or invalid");
}
return { success: false, error: error.message };
}
}
```
## Script Length Limits
| Tier | Max Characters |
|------|----------------|
| Free | ~500 |
| Creator | ~1,500 |
| Team | ~3,000 |
| Enterprise | ~5,000+ |
## Adding Pauses to Scripts
Use `<break>` tags to add pauses in your script:
```typescript
const script = "Welcome to our demo. <break time=\"1s\"/> Let me show you the features.";
```
**Format:** `<break time="Xs"/>` where X is seconds (e.g., `1s`, `1.5s`, `0.5s`)
**Important:** Break tags must have spaces before and after them.
See [voices.md](voices.md) for detailed break tag documentation.
## Test Mode
Use test mode during development:
```typescript
const config = {
test: true, // Watermarked output, no credits consumed
video_inputs: [...],
};
```
## Production-Ready Workflow
Complete example using avatar's default voice (recommended), proper timeouts, and retry logic:
```typescript
interface VideoGenerationResult {
videoId: string;
videoUrl: string;
duration: number;
avatarId: string;
voiceId: string;
avatarName: string;
}
async function generateAvatarVideo(
script: string,
options: {
avatarId?: string; // Specific avatar, or will pick first available
width?: number;
height?: number;
} = {}
): Promise<VideoGenerationResult> {
const { width = 1920, height = 1080 } = options;
let { avatarId } = options;
// 1. List avatars if no specific one provided
if (!avatarId) {
console.log("Listing available avatars...");
const listResponse = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const listData = await listResponse.json();
if (!listData.data?.avatars?.length) {
throw new Error("No avatars available");
}
avatarId = listData.data.avatars[0].avatar_id;
}
// 2. Get avatar details including default_voice_id
console.log(`Getting details for avatar: ${avatarId}`);
const detailsResponse = await fetch(
`https://api.heygen.com/v2/avatar/${avatarId}/details`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: avatar } = await detailsResponse.json();
if (!avatar.default_voice_id) {
throw new Error(`Avatar ${avatar.name} has no default voice - select voice manually`);
}
console.log(`Using avatar: ${avatar.name} with default voice: ${avatar.default_voice_id}`);
// 3. Generate video using avatar's default voice
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatar.id, // from details response
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id, // pre-matched default voice
speed: 1.0,
},
background: {
type: "color",
value: "#1a1a2e",
},
}],
dimension: { width, height },
});
console.log(`Video ID: ${videoId}`);
// 3. Wait for completion (20 minute timeout - generation can take 15+ min)
console.log("Waiting for video generation (typically 5-15 minutes, can be longer)...");
const result = await waitForVideo(
videoId,
process.env.HEYGEN_API_KEY!,
(status, elapsed) => {
console.log(` [${Math.round(elapsed / 1000)}s] ${status}`);
},
1200000 // 20 minute timeout for safety
);
return {
videoId,
videoUrl: result.video_url!,
duration: result.duration!,
avatarId: avatar.id,
voiceId: avatar.default_voice_id,
avatarName: avatar.name,
};
}
// Usage - let it pick an avatar automatically
const result = await generateAvatarVideo(
"Hello! Welcome to our product demonstration."
);
console.log(`Video ready: ${result.videoUrl}`);
// Or specify a known avatar_id
const result2 = await generateAvatarVideo(
"Hello! Welcome to our product demonstration.",
{ avatarId: "josh_lite3_20230714" }
);
```
## Transparent Background Videos (WebM)
Use WebM **only when you need transparency** - i.e., when the avatar should be overlaid on other video content and you need to see through to what's behind.
**Don't need WebM for:**
- Avatar with motion graphics/text overlaid ON TOP of avatar
- Picture-in-picture with solid background
- Standard presenter videos
**Do need WebM for:**
- Avatar overlaid on screen recording
- Avatar floating over video background
- True alpha-channel compositing
### WebM Request Fields
**Note:** The WebM endpoint (`/v1/video.webm`) uses a different structure than `/v2/video/generate`.
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `avatar_pose_id` | string | ✓ | Avatar pose ID (from avatar details) |
| `avatar_style` | string | ✓ | `"normal"` or `"closeUp"` only (no circle) |
| `input_text` | string | ✓* | Script text (*required if not using input_audio) |
| `voice_id` | string | ✓* | Voice ID (*required with input_text) |
| `input_audio` | string | ✓* | Audio URL (*required if not using input_text) |
| `dimension` | object | | `{width, height}` (default: 1280x720) |
**Either** (`input_text` + `voice_id`) **OR** `input_audio` must be provided, but not both.
### curl
```bash
curl -X POST "https://api.heygen.com/v1/video.webm" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"avatar_pose_id": "josh_lite3_20230714",
"avatar_style": "normal",
"input_text": "Hello! This video has a transparent background.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
### TypeScript
```typescript
interface WebMVideoRequest {
avatar_pose_id: string; // Required
avatar_style: "normal" | "closeUp"; // Required (no circle support)
input_text?: string; // Required if not using input_audio
voice_id?: string; // Required with input_text
input_audio?: string; // Required if not using input_text
dimension?: { width: number; height: number };
}
async function generateTransparentVideo(
script: string,
avatarPoseId: string,
voiceId: string
): Promise<string> {
const response = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required
avatar_style: "normal", // Required: "normal" or "closeUp"
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### When to Use WebM vs MP4
| Scenario | Format | Why |
|----------|--------|-----|
| Avatar with overlays on top | **MP4** | Overlays go on top, don't need transparency |
| Standard presenter | **MP4** | Simpler, more compatible |
| Loom-style (avatar over screen recording) | **WebM** + `normal`/`closeUp` | Need transparency, crop to circle in post |
| Avatar floating over video content | **WebM** | Need to see content behind avatar |
**Note:** WebM only supports `normal` and `closeUp` styles. Circle style is not supported for WebM - apply circular masking in your video editor/Remotion instead.
### WebM Example: Loom-Style (Avatar Over Screen Recording)
Generate with `normal` or `closeUp` style (circle not supported for WebM):
```typescript
// Generate avatar with transparent background
const videoId = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" },
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required
avatar_style: "closeUp", // Required: "normal" or "closeUp" only
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
}).then(r => r.json()).then(d => d.data.video_id);
```
Apply circular masking in Remotion:
```tsx
import { Video, AbsoluteFill } from "remotion";
export const LoomStyleVideo: React.FC<{
screenRecordingUrl: string;
avatarWebmUrl: string;
}> = ({ screenRecordingUrl, avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Screen recording as base layer */}
<Video src={screenRecordingUrl} style={{ width: "100%", height: "100%" }} />
{/* Avatar with circular mask applied in CSS */}
<Video
src={avatarWebmUrl}
style={{
position: "absolute",
bottom: 20,
left: 20,
width: 150,
height: 150,
borderRadius: "50%", // Circular mask
overflow: "hidden",
objectFit: "cover",
}}
/>
</AbsoluteFill>
);
};
```
### Note on Status Polling
WebM videos use the same status endpoint as MP4:
```typescript
// Same polling as regular videos
const status = await getVideoStatus(videoId);
// status.video_url will be a .webm file
```
## Best Practices
1. **Preview avatars before generating** - Download `preview_image_url` so user can see what the avatar looks like before committing to a video (see [avatars.md](avatars.md))
2. **Use avatar's default voice** - Most avatars have a `default_voice_id` that's pre-matched for natural results (see [avatars.md](avatars.md))
2. **Fallback: match gender manually** - If no default voice, ensure avatar and voice genders match (see [voices.md](voices.md))
3. **Validate inputs** - Check avatar and voice IDs before generating
4. **Use test mode** - Test configurations without consuming credits
5. **Set generous timeouts** - Use 15-20 minutes; generation often takes 10-15 min, sometimes longer
6. **Consider async patterns** - For long videos, save video_id and check status later (see [video-status.md](video-status.md))
7. **Handle errors gracefully** - Implement proper error handling
8. **Monitor progress** - Implement polling with progress feedback
9. **Optimize scripts** - Keep scripts concise and natural
10. **Consider dimensions** - Match dimensions to your use case (see [dimensions.md](dimensions.md))
@@ -0,0 +1,502 @@
---
name: video-status
description: Polling patterns, status types, and retrieving download URLs for HeyGen videos
---
# Video Status and Polling
After generating a video, you need to poll for status until the video is complete. HeyGen processes videos asynchronously.
## MCP Tool (Preferred)
If the HeyGen MCP server is connected, use `mcp__heygen__get_video` with the `videoId` parameter. It returns status, video_url, thumbnail_url, duration, title, gif_url, captioned_video_url, and other metadata in a single call.
## Checking Video Status (Direct API)
### curl
```bash
curl -X GET "https://api.heygen.com/v2/videos/YOUR_VIDEO_ID" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface VideoStatusResponse {
error: null | string;
data: {
id: string;
status: "pending" | "processing" | "completed" | "failed";
video_url?: string;
thumbnail_url?: string;
duration?: number;
title?: string;
created_at?: string;
completed_at?: string;
gif_url?: string;
captioned_video_url?: string;
subtitle_url?: string;
folder_id?: string;
output_language?: string;
failure_code?: string;
failure_message?: string;
};
}
async function getVideoStatus(videoId: string): Promise<VideoStatusResponse["data"]> {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: VideoStatusResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
### Python
```python
import requests
import os
def get_video_status(video_id: str) -> dict:
response = requests.get(
f"https://api.heygen.com/v2/videos/{video_id}",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]
```
## Video Status Types
| Status | Description |
|--------|-------------|
| `pending` | Video is queued for processing |
| `processing` | Video is being generated |
| `completed` | Video is ready for download |
| `failed` | Video generation failed |
## Expected Generation Times
Video generation typically takes **5-15 minutes**, but can exceed 20 minutes during peak load or for longer scripts.
| Factor | Impact |
|--------|--------|
| Script length | Longer scripts = significantly longer processing |
| Resolution | 1080p takes longer than 720p |
| Avatar complexity | Some avatars render faster |
| Queue load | Peak hours may cause 15-20+ minute waits |
| Multiple scenes | Each scene adds processing time |
**Recommendations**:
- Set timeout to **15-20 minutes** (900,000-1,200,000 ms) for safety
- For scripts > 2 minutes of speech, expect 15+ minutes
- Consider async patterns (save video_id, check later) for long videos
## Response Format
### Completed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "completed",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"title": "My Video",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:38:00Z",
"gif_url": "https://files.heygen.ai/gif/abc123.gif",
"captioned_video_url": null,
"subtitle_url": null,
"folder_id": null,
"output_language": "en"
}
}
```
### Failed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "failed",
"failure_code": "script_too_long",
"failure_message": "Script too long for selected avatar"
}
}
```
## Polling Implementation
### Basic Polling
```typescript
async function waitForVideo(
videoId: string,
maxWaitMs = 600000, // 10 minutes
pollIntervalMs = 5000 // 5 seconds
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await getVideoStatus(videoId);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
case "pending":
case "processing":
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
break;
}
}
throw new Error("Video generation timed out");
}
```
### Polling with Progress Callback
```typescript
type ProgressCallback = (status: string, elapsed: number) => void;
async function waitForVideoWithProgress(
videoId: string,
onProgress?: ProgressCallback,
maxWaitMs = 600000,
pollIntervalMs = 5000
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const elapsed = Date.now() - startTime;
const status = await getVideoStatus(videoId);
onProgress?.(status.status, elapsed);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
default:
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
throw new Error("Video generation timed out");
}
// Usage
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`Status: ${status}, Elapsed: ${Math.round(elapsed / 1000)}s`);
}
);
```
### Python Polling
```python
import time
from typing import Optional, Callable
def wait_for_video(
video_id: str,
max_wait_seconds: int = 600,
poll_interval: int = 5,
on_progress: Optional[Callable[[str, int], None]] = None
) -> str:
start_time = time.time()
while time.time() - start_time < max_wait_seconds:
elapsed = int(time.time() - start_time)
status_data = get_video_status(video_id)
status = status_data["status"]
if on_progress:
on_progress(status, elapsed)
if status == "completed":
return status_data["video_url"]
elif status == "failed":
raise Exception(status_data.get("failure_message", "Video generation failed"))
time.sleep(poll_interval)
raise Exception("Video generation timed out")
# Usage
def progress_callback(status: str, elapsed: int):
print(f"Status: {status}, Elapsed: {elapsed}s")
video_url = wait_for_video(video_id, on_progress=progress_callback)
```
## Downloading the Video
Once the video is complete, download it. **Important**: The video URL may not be immediately available after status shows "completed". Use retry logic with backoff.
### TypeScript (with retry)
```typescript
import fs from "fs";
import path from "path";
async function downloadVideoWithRetry(
videoUrl: string,
outputPath = "./output/video.mp4",
maxRetries = 5,
initialDelayMs = 2000
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
console.log(`Video downloaded to ${outputPath}`);
return;
} catch (error) {
lastError = error as Error;
const delay = initialDelayMs * Math.pow(2, attempt); // Exponential backoff
console.log(`Download attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`Failed to download after ${maxRetries} attempts: ${lastError?.message}`);
}
```
### Python (with retry)
```python
import requests
import time
def download_video_with_retry(
video_url: str,
output_path: str,
max_retries: int = 5,
initial_delay: float = 2.0
) -> None:
last_error = None
for attempt in range(max_retries):
try:
response = requests.get(video_url, stream=True, timeout=60)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Video downloaded to {output_path}")
return
except Exception as e:
last_error = e
delay = initial_delay * (2 ** attempt) # Exponential backoff
print(f"Download attempt {attempt + 1} failed, retrying in {delay}s...")
time.sleep(delay)
raise Exception(f"Failed to download after {max_retries} attempts: {last_error}")
```
### Simple Download (no retry)
For quick scripts where you'll retry manually:
```typescript
async function downloadVideo(videoUrl: string, outputPath = "./output/video.mp4") {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
}
```
## Complete Workflow Example
```typescript
async function generateAndDownloadVideo(config: VideoConfig): Promise<string> {
// 1. Generate video
const generateResponse = await fetch(
"https://api.heygen.com/v2/video/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const { data: generateData } = await generateResponse.json();
const videoId = generateData.video_id;
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`[${Math.round(elapsed / 1000)}s] Status: ${status}`);
}
);
// 3. Download
const outputPath = `./output/${videoId}.mp4`;
await downloadVideo(videoUrl, outputPath);
return outputPath;
}
```
## Resumable Status Checking
For long-running generations, save the video_id and check status later rather than keeping a process waiting.
### Save State After Generation
```typescript
interface PendingVideo {
videoId: string;
createdAt: string;
script: string;
avatarId: string;
voiceId: string;
}
async function startVideoGeneration(config: VideoGenerateRequest): Promise<PendingVideo> {
const videoId = await generateVideo(config);
const pending: PendingVideo = {
videoId,
createdAt: new Date().toISOString(),
script: config.video_inputs[0].voice.input_text!,
avatarId: config.video_inputs[0].character.avatar_id!,
voiceId: config.video_inputs[0].voice.voice_id!,
};
// Save to file for later retrieval
fs.writeFileSync("pending-video.json", JSON.stringify(pending, null, 2));
console.log(`Video generation started. ID: ${videoId}`);
console.log("Check status later with: checkVideoStatus()");
return pending;
}
```
### Check Status Later
```typescript
async function checkVideoStatus(): Promise<void> {
if (!fs.existsSync("pending-video.json")) {
console.log("No pending video found");
return;
}
const pending: PendingVideo = JSON.parse(
fs.readFileSync("pending-video.json", "utf-8")
);
const elapsed = Date.now() - new Date(pending.createdAt).getTime();
console.log(`Checking video ${pending.videoId} (started ${Math.round(elapsed / 60000)} min ago)...`);
const status = await getVideoStatus(pending.videoId);
switch (status.status) {
case "completed":
console.log(`Video ready: ${status.video_url}`);
console.log(`Duration: ${status.duration}s`);
// Clean up pending file
fs.unlinkSync("pending-video.json");
// Save result
fs.writeFileSync("video-result.json", JSON.stringify({
...pending,
videoUrl: status.video_url,
thumbnailUrl: status.thumbnail_url,
duration: status.duration,
title: status.title,
createdAt: status.created_at,
completedAt: status.completed_at,
}, null, 2));
break;
case "failed":
console.error(`Video failed: ${status.failure_message}`);
fs.unlinkSync("pending-video.json");
break;
default:
console.log(`Status: ${status.status} - check again in a few minutes`);
}
}
```
### CLI-Friendly Pattern
```typescript
// generate-video.ts - Start generation and exit
async function main() {
const pending = await startVideoGeneration(config);
console.log(`\nVideo ID saved. Run 'npx tsx check-status.ts' to check progress.`);
process.exit(0); // Exit immediately, don't wait
}
// check-status.ts - Check and optionally wait
async function main() {
const args = process.argv.slice(2);
const shouldWait = args.includes("--wait");
if (shouldWait) {
// Poll until complete (with 20 min timeout)
const result = await waitForVideo(pending.videoId, apiKey, onProgress, 1200000);
console.log(`Done: ${result.video_url}`);
} else {
// Just check once and report
await checkVideoStatus();
}
}
```
## Alternative: Using Webhooks
Instead of polling, you can use webhooks to receive notifications when videos complete. See [webhooks.md](webhooks.md) for details. Webhooks are ideal for production systems where you don't want to maintain polling connections.
## Best Practices
1. **Use exponential backoff** - Increase poll intervals for long-running jobs
2. **Set reasonable timeouts** - Most videos complete within 10 minutes
3. **Handle failures gracefully** - Check error messages for actionable feedback
4. **Consider webhooks** - For production systems, webhooks are more efficient than polling
5. **Cache video URLs** - Downloaded video URLs are valid for a limited time
@@ -0,0 +1,505 @@
---
name: voices
description: Listing voices, locales, speed/pitch configuration for HeyGen
---
# HeyGen Voices
HeyGen provides a wide variety of AI voices for different languages, accents, and styles. Voices convert your text script into natural-sounding speech.
## Listing Available Voices
### curl
```bash
curl -X GET "https://api.heygen.com/v2/voices" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Voice {
voice_id: string;
name: string;
language: string;
gender: "male" | "female";
preview_audio: string;
support_pause: boolean;
emotion_support: boolean;
}
interface VoicesResponse {
error: null | string;
data: {
voices: Voice[];
};
}
async function listVoices(): Promise<Voice[]> {
const response = await fetch("https://api.heygen.com/v2/voices", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: VoicesResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.voices;
}
```
### Python
```python
import requests
import os
def list_voices() -> list:
response = requests.get(
"https://api.heygen.com/v2/voices",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["voices"]
```
## Response Format
```json
{
"error": null,
"data": {
"voices": [
{
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"name": "Sara",
"language": "English",
"gender": "female",
"preview_audio": "https://files.heygen.ai/...",
"support_pause": true,
"emotion_support": true
},
{
"voice_id": "de8b5d78f2e0485f88d1e9f5c8e7f9a6",
"name": "Paul",
"language": "English",
"gender": "male",
"preview_audio": "https://files.heygen.ai/...",
"support_pause": true,
"emotion_support": false
}
]
}
}
```
## Supported Languages
HeyGen supports many languages including:
| Language | Code | Notes |
|----------|------|-------|
| English (US) | en-US | Multiple voice options |
| English (UK) | en-GB | British accent |
| Spanish | es-ES | Spain Spanish |
| Spanish (Latin) | es-MX | Mexican Spanish |
| French | fr-FR | France French |
| German | de-DE | Standard German |
| Portuguese | pt-BR | Brazilian Portuguese |
| Chinese (Mandarin) | zh-CN | Simplified Chinese |
| Japanese | ja-JP | Standard Japanese |
| Korean | ko-KR | Standard Korean |
| Italian | it-IT | Standard Italian |
| Dutch | nl-NL | Standard Dutch |
| Polish | pl-PL | Standard Polish |
| Arabic | ar-SA | Saudi Arabic |
## Using Voices in Video Generation
### Basic Voice Usage
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Welcome to our presentation.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Voice with Speed Adjustment
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "This is spoken at a faster pace.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.2, // 1.0 is normal, range: 0.5 - 2.0
},
},
],
};
```
### Voice with Pitch Adjustment
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "This has a higher pitch.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
pitch: 10, // Range: -20 to 20
},
},
],
};
```
## Adding Pauses with Break Tags
HeyGen supports SSML-style `<break>` tags to add pauses in scripts.
### Break Tag Format
```
<break time="Xs"/>
```
Where `X` is the duration in seconds (e.g., `1s`, `1.5s`, `0.5s`).
### Requirements
| Rule | Example |
|------|---------|
| Use seconds with "s" suffix | `<break time="1.5s"/>` ✓ |
| Must have space before tag | `word <break time="1s"/>` ✓ |
| Must have space after tag | `<break time="1s"/> word` ✓ |
| Self-closing tag | `<break time="1s"/>` ✓ |
**Incorrect:** `word<break time="1s"/>word` (no spaces)
**Correct:** `word <break time="1s"/> word`
### Examples
```typescript
// Single pause
const script1 = "Hello and welcome. <break time=\"1s\"/> Let me introduce our product.";
// Multiple pauses
const script2 = "First point. <break time=\"1.5s\"/> Second point. <break time=\"1s\"/> Third point.";
// Pause at start (dramatic opening)
const script3 = "<break time=\"0.5s\"/> Welcome to our presentation.";
// Longer pause for emphasis
const script4 = "And the winner is... <break time=\"2s\"/> You!";
```
### Full Example
```typescript
const scriptWithPauses = `
Welcome to our product demo. <break time="1s"/>
Today I'll show you three key features. <break time="0.5s"/>
First, let's look at the dashboard. <break time="1.5s"/>
As you can see, it's incredibly intuitive.
`;
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: scriptWithPauses,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Consecutive Breaks
Multiple consecutive break tags are automatically combined:
```typescript
// These two breaks:
"Hello <break time=\"1s\"/> <break time=\"0.5s\"/> world"
// Are treated as a single 1.5s pause
```
### Best Practices
1. **Use for emphasis** - Add pauses before important points
2. **Keep pauses reasonable** - 0.5s to 2s is typical; longer feels unnatural
3. **Match natural speech** - Add pauses where a human would breathe or pause
4. **Test the output** - Listen to generated audio to verify timing feels right
## Using Custom Audio Instead of TTS
Instead of text-to-speech, you can provide your own audio:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: "https://example.com/my-audio.mp3",
},
},
],
};
```
## Filtering Voices
### By Language
```typescript
function filterByLanguage(voices: Voice[], language: string): Voice[] {
return voices.filter((v) =>
v.language.toLowerCase().includes(language.toLowerCase())
);
}
const englishVoices = filterByLanguage(voices, "english");
const spanishVoices = filterByLanguage(voices, "spanish");
```
### By Gender
```typescript
function filterByGender(voices: Voice[], gender: "male" | "female"): Voice[] {
return voices.filter((v) => v.gender === gender);
}
const femaleVoices = filterByGender(voices, "female");
```
### By Features
```typescript
function filterByFeatures(
voices: Voice[],
options: { supportPause?: boolean; emotionSupport?: boolean }
): Voice[] {
return voices.filter((v) => {
if (options.supportPause !== undefined && v.support_pause !== options.supportPause) {
return false;
}
if (options.emotionSupport !== undefined && v.emotion_support !== options.emotionSupport) {
return false;
}
return true;
});
}
const expressiveVoices = filterByFeatures(voices, { emotionSupport: true });
```
## Voice Selection Helper
```typescript
interface VoiceSelectionCriteria {
language?: string;
gender?: "male" | "female";
supportPause?: boolean;
emotionSupport?: boolean;
}
async function findVoice(criteria: VoiceSelectionCriteria): Promise<Voice | null> {
const voices = await listVoices();
const filtered = voices.filter((v) => {
if (criteria.language && !v.language.toLowerCase().includes(criteria.language.toLowerCase())) {
return false;
}
if (criteria.gender && v.gender !== criteria.gender) {
return false;
}
if (criteria.supportPause !== undefined && v.support_pause !== criteria.supportPause) {
return false;
}
if (criteria.emotionSupport !== undefined && v.emotion_support !== criteria.emotionSupport) {
return false;
}
return true;
});
return filtered[0] || null;
}
// Usage
const voice = await findVoice({
language: "english",
gender: "female",
emotionSupport: true,
});
```
## Multi-Language Videos
Create videos with different languages per scene:
```typescript
const multiLanguageConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Welcome to our global product launch.",
voice_id: "english_voice_id",
},
},
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hola! Bienvenidos al lanzamiento global de nuestro producto.",
voice_id: "spanish_voice_id",
},
},
],
};
```
## Matching Voice to Avatar
### Recommended: Use Avatar's Default Voice
Many avatars have a `default_voice_id` that's pre-matched. **This is the best approach.**
```typescript
// Using v2 API to get avatar with default voice
const response = await fetch(
"https://api.heygen.com/v2/avatar_group.list?include_public=true",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
// Find avatar with a default voice
const avatar = data.avatar_group_list.find((a: any) => a.default_voice_id);
if (avatar) {
const videoConfig = {
video_inputs: [{
character: { type: "avatar", avatar_id: avatar.id },
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id, // Pre-matched voice
},
}],
};
}
```
See [avatars.md](avatars.md) for complete examples.
### Fallback: Match Gender Manually
If avatar has no default voice, match genders manually:
```typescript
interface AvatarVoicePair {
avatarId: string;
voiceId: string;
gender: "male" | "female";
}
async function findMatchingAvatarAndVoice(
preferredGender?: "male" | "female"
): Promise<AvatarVoicePair> {
const [avatars, voices] = await Promise.all([
listAvatars(),
listVoices(),
]);
// Default to male if no preference
const gender = preferredGender || "male";
// Find avatar with matching gender
const avatar = avatars.find((a) => a.gender === gender);
if (!avatar) {
throw new Error(`No ${gender} avatar available`);
}
// Find voice with matching gender AND language
const voice = voices.find(
(v) => v.gender === gender && v.language.toLowerCase().includes("english")
);
if (!voice) {
throw new Error(`No ${gender} English voice available`);
}
return {
avatarId: avatar.avatar_id,
voiceId: voice.voice_id,
gender,
};
}
```
## Best Practices
1. **Match voice gender to avatar** - Always pair male voices with male avatars, female with female
2. **Match voice to content** - Use professional voices for business content
3. **Test voice previews** - Listen to preview audio before selecting
4. **Consider locale** - Match voice accent to target audience
5. **Use natural pacing** - Adjust speed for clarity, typically 0.9-1.1x
6. **Add pauses** - Use SSML breaks for more natural speech flow
7. **Validate availability** - Always verify voice_id exists before using
@@ -0,0 +1,393 @@
---
name: webhooks
description: Registering webhook endpoints and event types for HeyGen
---
# Webhooks
Webhooks allow HeyGen to notify your application when events occur, such as video completion. This is more efficient than polling for status updates.
## Overview
Instead of repeatedly checking video status, webhooks push notifications to your server when:
- Video generation completes
- Video generation fails
- Translation completes
- Avatar training completes
- Other async operations finish
## Setting Up a Webhook Endpoint
Your webhook endpoint should:
1. Accept POST requests
2. Return 200 status quickly
3. Handle events asynchronously
### Express.js Example
```typescript
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
// Webhook endpoint
app.post("/webhook/heygen", async (req, res) => {
// Acknowledge receipt immediately
res.status(200).send("OK");
// Process event asynchronously
processWebhookEvent(req.body).catch(console.error);
});
async function processWebhookEvent(event: HeyGenWebhookEvent) {
console.log(`Received event: ${event.event_type}`);
switch (event.event_type) {
case "avatar_video.success":
await handleVideoSuccess(event);
break;
case "avatar_video.fail":
await handleVideoFailure(event);
break;
case "video_translate.success":
await handleTranslationSuccess(event);
break;
default:
console.log(`Unknown event type: ${event.event_type}`);
}
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});
```
### Python Flask Example
```python
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
@app.route("/webhook/heygen", methods=["POST"])
def heygen_webhook():
event = request.json
# Acknowledge immediately
response = jsonify({"status": "received"})
# Process asynchronously
thread = threading.Thread(
target=process_webhook_event,
args=(event,)
)
thread.start()
return response, 200
def process_webhook_event(event):
event_type = event.get("event_type")
print(f"Received event: {event_type}")
if event_type == "avatar_video.success":
handle_video_success(event)
elif event_type == "avatar_video.fail":
handle_video_failure(event)
elif event_type == "video_translate.success":
handle_translation_success(event)
if __name__ == "__main__":
app.run(port=3000)
```
## Webhook Event Types
| Event Type | Description |
|------------|-------------|
| `avatar_video.success` | Video generation completed |
| `avatar_video.fail` | Video generation failed |
| `video_translate.success` | Translation completed |
| `video_translate.fail` | Translation failed |
| `instant_avatar.success` | Instant avatar created |
| `instant_avatar.fail` | Instant avatar creation failed |
## Event Payload Structure
### Video Success Event
```typescript
interface VideoSuccessEvent {
event_type: "avatar_video.success";
event_data: {
video_id: string;
video_url: string;
thumbnail_url: string;
duration: number;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.success",
"event_data": {
"video_id": "abc123",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"callback_id": "your_custom_id"
}
}
```
### Video Failure Event
```typescript
interface VideoFailureEvent {
event_type: "avatar_video.fail";
event_data: {
video_id: string;
error: string;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.fail",
"event_data": {
"video_id": "abc123",
"error": "Script too long for selected avatar",
"callback_id": "your_custom_id"
}
}
```
## Registering a Webhook URL
Configure your webhook URL through the HeyGen dashboard or API:
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `url` | string | ✓ | Your webhook endpoint URL |
| `events` | array | ✓ | Event types to subscribe to |
| `secret` | string | | Shared secret for signature verification |
### Via API
```bash
curl -X POST "https://api.heygen.com/v1/webhook/endpoint.add" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook/heygen",
"events": ["avatar_video.success", "avatar_video.fail"]
}'
```
### TypeScript
```typescript
interface WebhookConfig {
url: string; // Required
events: string[]; // Required
secret?: string;
}
async function registerWebhook(config: WebhookConfig): Promise<void> {
const response = await fetch("https://api.heygen.com/v1/webhook/endpoint.add", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}
```
## Using Callback IDs
Track which video triggered a webhook with callback IDs:
### Include Callback ID in Video Generation
```typescript
const videoConfig = {
video_inputs: [...],
callback_id: "order_12345", // Your custom identifier
};
```
### Handle in Webhook
```typescript
async function handleVideoSuccess(event: VideoSuccessEvent) {
const { video_id, video_url, callback_id } = event.event_data;
if (callback_id) {
// Look up your original request
const order = await getOrderByCallbackId(callback_id);
await updateOrderWithVideo(order.id, video_url);
}
}
```
## Webhook Security
### Verify Webhook Signatures
If HeyGen provides signature verification:
```typescript
import crypto from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post("/webhook/heygen", (req, res) => {
const signature = req.headers["x-heygen-signature"] as string;
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process event...
});
```
### Validate Event Origin
```typescript
function isValidHeygenEvent(event: any): boolean {
// Check required fields
if (!event.event_type || !event.event_data) {
return false;
}
// Check event type is known
const validEventTypes = [
"avatar_video.success",
"avatar_video.fail",
"video_translate.success",
"video_translate.fail",
];
return validEventTypes.includes(event.event_type);
}
```
## Handling Webhook Failures
Implement retry logic and error handling:
```typescript
async function processWebhookEvent(event: HeyGenWebhookEvent) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await handleEvent(event);
return;
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
// Store failed event for manual review
await storeFailedEvent(event);
}
```
## Webhook vs Polling Comparison
| Aspect | Webhook | Polling |
|--------|---------|---------|
| Latency | Immediate | Depends on interval |
| Efficiency | High (push) | Low (repeated requests) |
| Complexity | Requires endpoint | Simpler to implement |
| Reliability | Needs retry handling | Guaranteed delivery |
| Cost | Lower API usage | Higher API usage |
## Testing Webhooks
### Local Development with ngrok
```bash
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhook/heygen
```
### Webhook Testing Tool
```typescript
// Test webhook locally
async function simulateWebhook(event: HeyGenWebhookEvent) {
const response = await fetch("http://localhost:3000/webhook/heygen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
console.log(`Response: ${response.status}`);
}
// Simulate success event
await simulateWebhook({
event_type: "avatar_video.success",
event_data: {
video_id: "test_123",
video_url: "https://example.com/test.mp4",
thumbnail_url: "https://example.com/test.jpg",
duration: 30,
callback_id: "test_callback",
},
});
```
## Best Practices
1. **Respond quickly** - Return 200 within 5 seconds, process async
2. **Handle duplicates** - Same event may be sent multiple times
3. **Implement retries** - Handle temporary processing failures
4. **Log everything** - Store webhook payloads for debugging
5. **Use callback IDs** - Track requests through the system
6. **Secure endpoints** - Verify signatures, use HTTPS
7. **Monitor health** - Track webhook success rates
8. **Queue processing** - Use job queues for heavy processing
+171
View File
@@ -0,0 +1,171 @@
---
name: beautiful-mermaid
description: Render Mermaid diagrams as SVG and PNG using the Beautiful Mermaid library. Use when the user asks to render a Mermaid diagram.
---
# Beautiful Mermaid Diagram Rendering
Render Mermaid diagrams as SVG and PNG images using the Beautiful Mermaid library.
## Dependencies
This skill requires the `agent-browser` skill for PNG rendering. Load it before proceeding with PNG capture.
## Supported Diagram Types
- **Flowchart** - Process flows, decision trees, CI/CD pipelines
- **Sequence** - API calls, OAuth flows, database transactions
- **State** - State machines, connection lifecycles
- **Class** - UML class diagrams, design patterns
- **Entity-Relationship** - Database schemas, data models
## Available Themes
Default, Dracula, Solarized, Zinc Dark, Tokyo Night, Tokyo Night Storm, Tokyo Night Light, Catppuccin Latte, Nord, Nord Light, GitHub Dark, GitHub Light, One Dark.
If no theme is specified, use `default`.
## Common Syntax Patterns
### Flowchart Edge Labels
Use pipe syntax for edge labels:
```mermaid
A -->|label| B
A ---|label| B
```
Avoid space-dash syntax which can cause incomplete renders:
```mermaid
A -- label --> B # May cause issues
```
### Node Labels with Special Characters
Wrap labels containing special characters in quotes:
```mermaid
A["Label with (parens)"]
B["Label with / slash"]
```
## Workflow
### Step 1: Generate or Validate Mermaid Code
If the user provides a description rather than code, generate valid Mermaid syntax. Consult `references/mermaid-syntax.md` for full syntax details.
### Step 2: Render SVG
Run the rendering script to produce an SVG file:
```bash
bun run scripts/render.ts --code "graph TD; A-->B" --output diagram --theme default
```
Or from a file:
```bash
bun run scripts/render.ts --input diagram.mmd --output diagram --theme tokyo-night
```
Alternative runtimes:
```bash
npx tsx scripts/render.ts --code "..." --output diagram
deno run --allow-read --allow-write --allow-net scripts/render.ts --code "..." --output diagram
```
This produces `<output>.svg` in the current working directory.
### Step 3: Create HTML Wrapper
Run the HTML wrapper script to prepare for screenshot:
```bash
bun run scripts/create-html.ts --svg diagram.svg --output diagram.html
```
This creates a minimal HTML file that displays the SVG with proper padding and background.
### Step 4: Capture High-Resolution PNG with agent-browser
Use the agent-browser CLI to capture a high-quality screenshot. Refer to the `agent-browser` skill for full CLI documentation.
```bash
# Set 4K viewport for high-resolution capture
agent-browser set viewport 3840 2160
# Open the HTML wrapper
agent-browser open "file://$(pwd)/diagram.html"
# Wait for render to complete
agent-browser wait 1000
# Capture full-page screenshot
agent-browser screenshot --full diagram.png
# Close browser
agent-browser close
```
For even higher resolution on complex diagrams, increase the viewport further or use the `--padding` option when creating the HTML wrapper to give the diagram more space.
### Step 5: Clean Up Intermediary Files
After rendering, remove all intermediary files. Only the final `.svg` and `.png` should remain.
Files to clean up:
- The HTML wrapper file (e.g., `diagram.html`)
- Any temporary `.mmd` files created to hold diagram code
- Any other files created during the rendering process
```bash
rm diagram.html
```
If a temporary `.mmd` file was created, remove it as well.
## Output
Both outputs are always produced:
- **SVG**: Vector format, infinitely scalable, small file size
- **PNG**: High-resolution raster, captured at 4K (3840×2160) viewport with minimum 1200px diagram width
Files are saved to the current working directory unless the user explicitly specifies a different path.
## Theme Selection Guide
| Theme | Background | Best For |
|-------|------------|----------|
| default | Light grey | General use |
| dracula | Dark purple | Dark mode preference |
| tokyo-night | Dark blue | Modern dark aesthetic |
| tokyo-night-storm | Darker blue | Higher contrast |
| nord | Dark arctic | Muted, calm visuals |
| nord-light | Light arctic | Light mode with soft tones |
| github-dark | GitHub dark | Matches GitHub UI |
| github-light | GitHub light | Matches GitHub UI |
| catppuccin-latte | Warm light | Soft pastel aesthetic |
| solarized | Tan/cream | Solarized colour scheme |
| one-dark | Atom dark | Atom editor aesthetic |
| zinc-dark | Neutral dark | Minimal, no colour bias |
## Troubleshooting
### Theme not applied
Check the render script output for the `bg` and `fg` values, or inspect the SVG's opening tag for `--bg` and `--fg` CSS custom properties.
### Diagram appears cut off or incomplete
- Check edge label syntax — use `-->|label|` pipe notation, not `-- label -->`
- Verify all node IDs are unique
- Check for unclosed brackets in node labels
### Render produces empty or malformed SVG
- Validate Mermaid syntax at https://mermaid.live before rendering
- Check for special characters that need escaping (wrap in quotes)
- Ensure flowchart direction is specified (`graph TD`, `graph LR`, etc.)
@@ -0,0 +1,235 @@
# Mermaid Syntax Reference
Quick reference for generating valid Mermaid diagram code.
## Flowchart
```mermaid
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Action 1]
B -->|No| D[Action 2]
C --> E[End]
D --> E
```
### Direction
- `TD` / `TB` - Top to bottom
- `BT` - Bottom to top
- `LR` - Left to right
- `RL` - Right to left
### Node Shapes
- `A[Text]` - Rectangle
- `A(Text)` - Rounded rectangle
- `A([Text])` - Stadium/pill
- `A[[Text]]` - Subroutine
- `A[(Text)]` - Cylinder (database)
- `A((Text))` - Circle
- `A>Text]` - Asymmetric
- `A{Text}` - Diamond (decision)
- `A{{Text}}` - Hexagon
- `A[/Text/]` - Parallelogram
- `A[\Text\]` - Parallelogram alt
- `A[/Text\]` - Trapezoid
- `A[\Text/]` - Trapezoid alt
### Edge Styles
- `A --> B` - Arrow
- `A --- B` - Line
- `A -.-> B` - Dotted arrow
- `A ==> B` - Thick arrow
- `A -->|text| B` - Arrow with label (preferred)
- `A ---|text| B` - Line with label (preferred)
**Important**: Always use pipe syntax `-->|label|` for edge labels. The space-dash syntax `-- label -->` can cause incomplete renders.
### Subgraphs
```mermaid
graph TD
subgraph Group1 [Label]
A --> B
end
subgraph Group2
C --> D
end
B --> C
```
## Sequence Diagram
```mermaid
sequenceDiagram
participant A as Alice
participant B as Bob
A->>B: Hello
B-->>A: Hi there
A->>+B: Start process
B-->>-A: Done
```
### Arrow Types
- `->>` - Solid arrow
- `-->>` - Dashed arrow
- `-x` - Solid with x
- `--x` - Dashed with x
- `-)` - Solid open arrow
- `--)` - Dashed open arrow
### Activations
- `+` after arrow activates participant
- `-` after arrow deactivates participant
### Notes and Boxes
```mermaid
sequenceDiagram
Note over A,B: Shared note
Note right of A: Side note
rect rgb(200, 220, 255)
A->>B: In a box
end
```
### Loops and Conditionals
```mermaid
sequenceDiagram
loop Every minute
A->>B: Ping
end
alt Success
B-->>A: Pong
else Failure
B-->>A: Error
end
opt Optional
A->>B: Extra step
end
```
## State Diagram
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Processing : start
Processing --> Done : complete
Processing --> Error : fail
Error --> Idle : reset
Done --> [*]
```
### Composite States
```mermaid
stateDiagram-v2
state Active {
[*] --> Running
Running --> Paused : pause
Paused --> Running : resume
}
Idle --> Active : activate
Active --> Idle : deactivate
```
### Notes
```mermaid
stateDiagram-v2
State1 : Description here
note right of State1
Additional info
end note
```
## Class Diagram
```mermaid
classDiagram
class Animal {
+String name
+int age
+makeSound() void
}
class Dog {
+bark() void
}
Animal <|-- Dog : extends
```
### Relationships
- `<|--` - Inheritance
- `*--` - Composition
- `o--` - Aggregation
- `-->` - Association
- `--` - Link (solid)
- `..>` - Dependency
- `..|>` - Realisation
- `..` - Link (dashed)
### Cardinality
```mermaid
classDiagram
Customer "1" --> "*" Order
Order "1" --> "1..*" LineItem
```
### Visibility
- `+` Public
- `-` Private
- `#` Protected
- `~` Package/Internal
## Entity-Relationship Diagram
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
PRODUCT }|..|{ LINE-ITEM : "ordered in"
```
### Relationship Types
- `||` - Exactly one
- `|{` - One or more
- `o{` - Zero or more
- `o|` - Zero or one
### Identifying vs Non-identifying
- `--` - Identifying (solid)
- `..` - Non-identifying (dashed)
### Attributes
```mermaid
erDiagram
CUSTOMER {
string id PK
string name
string email UK
}
ORDER {
int id PK
string customer_id FK
date created_at
}
```
## Styling
### CSS Classes
```mermaid
graph TD
A:::highlight --> B
classDef highlight fill:#f96,stroke:#333
```
### Inline Styles
```mermaid
graph TD
A --> B
style A fill:#bbf,stroke:#333
```
## Tips
1. **Escape special characters**: Use quotes for labels with special chars: `A["Label with (parens)"]`
2. **Multi-line labels**: Use `<br/>` for line breaks
3. **Comments**: Use `%%` for comments that won't render
4. **IDs vs Labels**: Node IDs should be simple, labels can be complex: `node1["Complex Label Here"]`
@@ -0,0 +1,177 @@
#!/usr/bin/env -S npx tsx
/**
* Create an HTML wrapper for an SVG to enable high-quality PNG capture
*
* Usage:
* bun run create-html.ts --svg diagram.svg --output diagram.html
* bun run create-html.ts --svg diagram.svg --output diagram.html --padding 40
*
* Runtimes:
* bun run create-html.ts ...
* npx tsx create-html.ts ...
* deno run --allow-read --allow-write create-html.ts ...
*/
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { resolve, basename } from "node:path";
interface Args {
svg: string;
output: string;
padding: number;
background?: string;
}
function parseArgs(): Args {
const args = process.argv.slice(2);
const result: Partial<Args> = { padding: 40 };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = args[i + 1];
switch (arg) {
case "--svg":
case "-s":
result.svg = next;
i++;
break;
case "--output":
case "-o":
result.output = next;
i++;
break;
case "--padding":
case "-p":
result.padding = parseInt(next, 10) || 40;
i++;
break;
case "--background":
case "-b":
result.background = next;
i++;
break;
case "--help":
case "-h":
printHelp();
process.exit(0);
}
}
if (!result.svg) {
console.error("Error: --svg is required");
printHelp();
process.exit(1);
}
if (!result.output) {
console.error("Error: --output is required");
printHelp();
process.exit(1);
}
return result as Args;
}
function printHelp(): void {
console.log(`
SVG to HTML Wrapper
Creates a minimal HTML file for screenshot capture of SVG diagrams.
Usage:
create-html.ts --svg <file.svg> --output <file.html> [options]
Options:
-s, --svg <file> Input SVG file
-o, --output <file> Output HTML file
-p, --padding <pixels> Padding around SVG (default: 40)
-b, --background <color> Background colour (auto-detected from SVG)
-h, --help Show this help
Examples:
create-html.ts --svg diagram.svg --output diagram.html
create-html.ts --svg diagram.svg --output diagram.html --padding 60
create-html.ts --svg diagram.svg --output diagram.html --background "#1a1b26"
`);
}
function extractBackgroundFromSvg(svgContent: string): string | null {
// Try to extract background from SVG style or rect
const bgMatch = svgContent.match(/background(?:-color)?:\s*([^;"\s]+)/i);
if (bgMatch) return bgMatch[1];
// Check for a background rect
const rectMatch = svgContent.match(
/<rect[^>]*fill="([^"]+)"[^>]*(?:width="100%"|height="100%")/i
);
if (rectMatch) return rectMatch[1];
// Check style attribute on svg element
const svgStyleMatch = svgContent.match(
/<svg[^>]*style="[^"]*background(?:-color)?:\s*([^;"\s]+)/i
);
if (svgStyleMatch) return svgStyleMatch[1];
return null;
}
function main(): void {
const args = parseArgs();
const svgPath = resolve(args.svg);
if (!existsSync(svgPath)) {
console.error(`SVG file not found: ${svgPath}`);
process.exit(1);
}
const svgContent = readFileSync(svgPath, "utf-8");
// Determine background colour
const background =
args.background ?? extractBackgroundFromSvg(svgContent) ?? "#ffffff";
// Create HTML wrapper optimised for high-resolution screenshot
// SVG renders at natural size with generous padding, no constraints
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${basename(args.svg, ".svg")}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
background: ${background};
}
.container {
padding: ${args.padding}px;
display: inline-block;
background: ${background};
}
.container svg {
display: block;
min-width: 1200px;
height: auto;
}
</style>
</head>
<body>
<div class="container">
${svgContent}
</div>
</body>
</html>`;
const outputPath = resolve(args.output);
writeFileSync(outputPath, html, "utf-8");
console.log(`HTML wrapper written to: ${outputPath}`);
console.log(`Background colour: ${background}`);
}
main();
@@ -0,0 +1,221 @@
#!/usr/bin/env -S npx tsx
/**
* Render a Mermaid diagram to SVG using Beautiful Mermaid
*
* Usage:
* bun run render.ts --input diagram.mmd --output diagram --theme tokyo-night
* bun run render.ts --code "graph TD; A-->B" --output diagram
*
* Runtimes:
* bun run render.ts ...
* npx tsx render.ts ...
* deno run --allow-read --allow-write --allow-net render.ts ...
*
* Output:
* Produces <output>.svg
*/
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { resolve } from "node:path";
const THEMES = [
"default",
"dracula",
"solarized",
"zinc-dark",
"tokyo-night",
"tokyo-night-storm",
"tokyo-night-light",
"catppuccin-latte",
"nord",
"nord-light",
"github-dark",
"github-light",
"one-dark",
] as const;
type Theme = (typeof THEMES)[number];
interface Args {
input?: string;
code?: string;
output: string;
theme: Theme;
}
function parseArgs(): Args {
const args = process.argv.slice(2);
const result: Partial<Args> = { theme: "default" };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = args[i + 1];
switch (arg) {
case "--input":
case "-i":
result.input = next;
i++;
break;
case "--code":
case "-c":
result.code = next;
i++;
break;
case "--output":
case "-o":
result.output = next;
i++;
break;
case "--theme":
case "-t":
if (next && THEMES.includes(next as Theme)) {
result.theme = next as Theme;
} else {
console.error(`Invalid theme: ${next}`);
console.error(`Available themes: ${THEMES.join(", ")}`);
process.exit(1);
}
i++;
break;
case "--help":
case "-h":
printHelp();
process.exit(0);
}
}
if (!result.input && !result.code) {
console.error("Error: Either --input or --code is required");
printHelp();
process.exit(1);
}
if (!result.output) {
console.error("Error: --output is required");
printHelp();
process.exit(1);
}
return result as Args;
}
function printHelp(): void {
console.log(`
Beautiful Mermaid Renderer
Renders Mermaid diagrams to SVG.
Usage:
render.ts --input <file.mmd> --output <basename> [--theme <theme>]
render.ts --code "<mermaid code>" --output <basename> [--theme <theme>]
Options:
-i, --input <file> Input Mermaid file (.mmd)
-c, --code <string> Mermaid code as string
-o, --output <name> Output base name (without extension)
-t, --theme <theme> Theme name (default: default)
-h, --help Show this help
Available themes:
${THEMES.join(", ")}
Output:
Produces <output>.svg
Examples:
render.ts -i diagram.mmd -o diagram -t tokyo-night
render.ts -c "graph TD; A-->B" -o simple
`);
}
function detectRuntime(): "bun" | "deno" | "node" {
if (typeof (globalThis as any).Bun !== "undefined") return "bun";
if (typeof (globalThis as any).Deno !== "undefined") return "deno";
return "node";
}
async function ensurePackage(name: string): Promise<any> {
const runtime = detectRuntime();
try {
if (runtime === "deno") {
return await import(`npm:${name}`);
}
return await import(name);
} catch {
console.error(`${name} not found. Installing...`);
const { execSync } = await import("node:child_process");
try {
if (runtime === "bun") {
execSync(`bun add ${name}`, { stdio: "inherit" });
} else if (runtime === "deno") {
return await import(`npm:${name}`);
} else {
execSync(`npm install ${name}`, { stdio: "inherit" });
}
return await import(name);
} catch (installError) {
console.error(`Failed to install ${name}:`, installError);
process.exit(1);
}
}
}
function getThemeConfig(themeName: Theme): { bg: string; fg: string } {
const themeConfigs: Record<Theme, { bg: string; fg: string }> = {
default: { bg: "#f5f5f5", fg: "#333333" },
dracula: { bg: "#282a36", fg: "#f8f8f2" },
solarized: { bg: "#fdf6e3", fg: "#657b83" },
"zinc-dark": { bg: "#18181b", fg: "#fafafa" },
"tokyo-night": { bg: "#1a1b26", fg: "#a9b1d6" },
"tokyo-night-storm": { bg: "#24283b", fg: "#a9b1d6" },
"tokyo-night-light": { bg: "#d5d6db", fg: "#343b58" },
"catppuccin-latte": { bg: "#eff1f5", fg: "#4c4f69" },
nord: { bg: "#2e3440", fg: "#eceff4" },
"nord-light": { bg: "#eceff4", fg: "#2e3440" },
"github-dark": { bg: "#0d1117", fg: "#c9d1d9" },
"github-light": { bg: "#ffffff", fg: "#24292f" },
"one-dark": { bg: "#282c34", fg: "#abb2bf" },
};
return themeConfigs[themeName];
}
async function main(): Promise<void> {
const args = parseArgs();
let mermaidCode: string;
if (args.input) {
const inputPath = resolve(args.input);
if (!existsSync(inputPath)) {
console.error(`Input file not found: ${inputPath}`);
process.exit(1);
}
mermaidCode = readFileSync(inputPath, "utf-8");
} else {
mermaidCode = args.code!;
}
console.log(`Rendering diagram with theme: ${args.theme}`);
const beautifulMermaid = await ensurePackage("beautiful-mermaid");
const renderMermaid = beautifulMermaid.renderMermaid;
const THEMES = beautifulMermaid.THEMES;
const themeConfig = THEMES?.[args.theme] ?? getThemeConfig(args.theme);
console.log(`Using theme: bg=${themeConfig.bg}, fg=${themeConfig.fg}`);
const svg = await renderMermaid(mermaidCode, themeConfig);
const svgPath = resolve(`${args.output}.svg`);
writeFileSync(svgPath, svg, "utf-8");
console.log(`SVG written to: ${svgPath}`);
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
+264
View File
@@ -0,0 +1,264 @@
---
name: bfl-api
description: BFL FLUX API integration guide covering endpoints, async polling patterns, rate limiting, error handling, webhooks, and regional endpoints with Python and TypeScript code examples.
metadata:
author: Black Forest Labs
version: "1.0.0"
tags: flux, bfl, api, integration, webhooks, rate-limiting
---
# BFL API Integration Guide
Use this skill when integrating BFL FLUX APIs into applications for image generation, editing, and processing.
## First: Check API Key
**Before generating images, verify your API key is set:**
```bash
echo $BFL_API_KEY
```
If empty or you see "Not authenticated" errors, see [API Key Setup](#api-key-setup) below.
## Important: Image URLs Expire in 10 Minutes
Result URLs from the API are temporary. Download images immediately after generation completes - do not store or cache the URLs themselves.
## When to Use
- Setting up BFL API client
- Implementing async polling patterns
- Handling rate limits and errors
- Configuring webhooks for production
- Selecting regional endpoints
- Building production-ready integrations
## Quick Reference
### Base Endpoints
| Region | Endpoint | Use Case |
| ------ | ----------------------- | --------------------------- |
| Global | `https://api.bfl.ai` | Default, automatic failover |
| EU | `https://api.eu.bfl.ai` | GDPR compliance |
| US | `https://api.us.bfl.ai` | US data residency |
### Model Endpoints & Pricing
> **Credit pricing:** 1 credit = $0.01 USD. FLUX.2 uses megapixel-based pricing (cost scales with resolution).
#### FLUX.2 Models
| Model | Path | 1st MP | +MP | 1MP T2I | 1MP I2I | Best For |
| ----------------- | --------------------- | ------ | ---- | ------- | ------- | ---------------------------------- |
| FLUX.2 [klein] 4B | `/v1/flux-2-klein-4b` | 1.4c | 0.1c | $0.014 | $0.015 | Real-time, high volume |
| FLUX.2 [klein] 9B | `/v1/flux-2-klein-9b` | 1.5c | 0.2c | $0.015 | $0.017 | Balanced quality/speed |
| FLUX.2 [pro] | `/v1/flux-2-pro` | 3c | 1.5c | $0.03 | $0.045 | Production, fast turnaround |
| FLUX.2 [max] | `/v1/flux-2-max` | 7c | 3c | $0.07 | $0.10 | Maximum quality |
| FLUX.2 [flex] | `/v1/flux-2-flex` | 5c | 5c | $0.05 | $0.10 | Typography, adjustable controls |
| FLUX.2 [dev] | - | - | - | Free | Free | Local development (non-commercial) |
> **Pricing formula:** `(firstMP + (outputMP-1) * mpPrice) + (inputMP * mpPrice)` in cents
#### FLUX.1 Models
| Model | Path | Price/Image | Best For |
| -------------------- | ------------------------ | ----------- | ----------------------------- |
| FLUX.1 Kontext [pro] | `/v1/flux-kontext` | $0.04 | Image editing with context |
| FLUX.1 Kontext [max] | `/v1/flux-kontext-max` | $0.08 | Max quality editing |
| FLUX1.1 [pro] | `/v1/flux-pro-1.1` | $0.04 | Standard T2I, fast & reliable |
| FLUX1.1 [pro] Ultra | `/v1/flux-pro-1.1-ultra` | $0.06 | Ultra high-resolution |
| FLUX1.1 [pro] Raw | `/v1/flux-pro-1.1-raw` | $0.06 | Candid photography feel |
| FLUX.1 Fill [pro] | `/v1/flux-pro-1.0-fill` | $0.05 | Inpainting |
> **Tip:** All FLUX.2 models support image editing via the `input_image` parameter - no separate editing endpoint needed. Use [bfl.ai/pricing](https://bfl.ai/pricing) calculator for exact costs at different resolutions.
### Image Input for Editing
**Preferred: Use URLs directly** - simpler and more convenient than base64.
**Single image editing:**
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: $BFL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Change the background to a sunset",
"input_image": "https://example.com/photo.jpg"
}'
```
**Multi-reference editing:**
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: $BFL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "The person from image 1 in the environment from image 2",
"input_image": "https://example.com/person.jpg",
"input_image_2": "https://example.com/background.jpg"
}'
```
The API fetches URLs automatically. Both URL and base64 work, but URLs are recommended when available.
### Multi-Reference I2I
FLUX.2 models support multiple input images for combining elements, style transfer, and character consistency:
| Model | Max References |
| --------------------- | -------------- |
| FLUX.2 [klein] | 4 images |
| FLUX.2 [pro/max/flex] | 8 images |
**Parameters:** `input_image`, `input_image_2`, `input_image_3`, ... `input_image_8`
**Prompt pattern:** Reference images by number in your prompt:
- "The subject from image 1 in the environment from image 2"
- "Apply the style of image 2 to the scene in image 1"
- "The person from image 1 wearing the outfit from image 2, in the pose from image 3"
> For detailed multi-reference patterns (character consistency, style transfer, pose guidance), see `flux-best-practices/rules/multi-reference-editing.md`
### Rate Limits
| Tier | Concurrent Requests |
| ------------------------- | ------------------- |
| Standard (most endpoints) | 24 |
### Polling vs Webhooks
| Approach | Use When |
| ------------ | ------------------------------------------------------------------------------------ |
| **Polling** | Scripts, CLI tools, local development, single requests, simple integrations |
| **Webhooks** | Production apps, high volume, server-to-server, when you need immediate notification |
**Start with polling** - it's simpler and works everywhere. Switch to webhooks when you need to scale or want event-driven architecture.
### Key Behaviors
- **Polling**: Response includes `polling_url` for async results
- **URL Expiration**: Result URLs expire after 10 minutes
- **Webhook Support**: Configure `webhook_url` for production workloads
## API Key Setup
**Required**: The `BFL_API_KEY` environment variable must be set before using the API.
### Quick Check
```bash
echo $BFL_API_KEY
```
### If Not Set
1. **Get a key**: Go to https://dashboard.bfl.ai/get-started → Click **"Create Key"** → Select organization
2. **Save to `.env`** (recommended for persistence):
```bash
echo 'BFL_API_KEY=bfl_your_key_here' >> .env
echo '.env' >> .gitignore # Don't commit secrets
```
See [references/api-key-setup.md](references/api-key-setup.md) for detailed setup instructions.
## Authentication
```bash
x-key: YOUR_API_KEY
```
## Basic Request Flow
```
1. POST request to model endpoint
└─> Response: { "polling_url": "..." }
2. GET polling_url (repeat until complete)
└─> Response: { "status": "Pending" | "Ready" | "Error", ... }
3. When Ready, download result URL
└─> URL expires in 10 minutes - download immediately
```
## Related
- **Prompting best practices** (T2I, I2I, typography, colors): see the **flux-best-practices** skill
- **Multi-reference patterns** (character consistency, style transfer, pose guidance): see `flux-best-practices/rules/multi-reference-editing.md`
## References
- [references/api-key-setup.md](references/api-key-setup.md) - **API key creation and configuration**
- [references/endpoints.md](references/endpoints.md) - Complete endpoint documentation
- [references/polling-patterns.md](references/polling-patterns.md) - Async polling implementation
- [references/rate-limiting.md](references/rate-limiting.md) - Rate limit handling strategies
- [references/error-handling.md](references/error-handling.md) - Error codes and recovery
- [references/webhook-integration.md](references/webhook-integration.md) - Webhook setup and security
### Code Examples
> **Note:** cURL examples are preferred by default as they work universally without requiring Python or Node.js. Use language-specific clients when building production applications.
- [references/code-examples/curl-examples.sh](references/code-examples/curl-examples.sh) - **cURL examples (recommended)**
- [references/code-examples/python-client.py](references/code-examples/python-client.py) - Python client
- [references/code-examples/typescript-client.ts](references/code-examples/typescript-client.ts) - TypeScript client
## Quick Start Example
### 1. Submit Generation Request
```bash
curl -s -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: $BFL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "A serene mountain landscape at sunset", "width": 1024, "height": 1024}'
```
Response:
```json
{ "id": "abc123", "polling_url": "https://api.bfl.ai/v1/get_result?id=abc123" }
```
### 2. Poll for Result
```bash
curl -s "POLLING_URL" -H "x-key: $BFL_API_KEY"
```
Response when ready:
```json
{ "status": "Ready", "result": { "sample": "https://...", "seed": 1234 } }
```
### 3. Download Image
```bash
curl -s -o output.png "IMAGE_URL"
```
> **Tip:** Result URLs expire in 10 minutes. Download immediately after status becomes `Ready`.
### 4. Multi-Reference Example
Combine elements from multiple images:
```bash
curl -s -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: $BFL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "The cat from image 1 sitting in the cozy room from image 2",
"input_image": "https://example.com/cat.jpg",
"input_image_2": "https://example.com/room.jpg",
"width": 1024,
"height": 1024
}'
```
Reference images by number in your prompt. See [Multi-Reference I2I](#multi-reference-i2i) for limits and patterns.
@@ -0,0 +1,55 @@
---
name: api-key-setup
description: How to obtain and configure a BFL API key
---
# API Key Setup
> **Important:** Always verify your API key before attempting image generation. Missing or invalid keys result in "Not authenticated" errors.
## Quick Validation
Run this first to check if your key is configured and valid:
```bash
# Check if key is set
[ -z "$BFL_API_KEY" ] && echo "Error: BFL_API_KEY not set" || echo "OK: Key configured"
```
If not set, follow the steps below.
## Get a Key
1. Go to **https://dashboard.bfl.ai/get-started**
2. Click **"Create Key"**
3. Select organization (ask user if multiple options)
4. Copy the key (starts with `bfl_`)
## For Agents Making Direct API Calls
When `BFL_API_KEY` is not set in the current session:
1. **Check for existing `.env`**:
```bash
grep BFL_API_KEY .env 2>/dev/null
```
2. **If found, export it**:
```bash
export BFL_API_KEY=$(grep BFL_API_KEY .env | cut -d '=' -f2)
```
3. **If not found, ask the user** for their key:
> "I need a BFL API key to generate images. Please:
> 1. Go to https://dashboard.bfl.ai/get-started
> 2. Click 'Create Key' and copy it
> 3. Paste it here"
4. **Save and export**:
```bash
echo 'BFL_API_KEY=bfl_provided_key' >> .env
echo '.env' >> .gitignore
export BFL_API_KEY=bfl_provided_key
```
Now `$BFL_API_KEY` is available for direct curl/API calls in the session.
@@ -0,0 +1,160 @@
#!/bin/bash
#
# BFL FLUX API - cURL Examples
#
# These examples demonstrate how to use the BFL FLUX API with cURL.
# Replace YOUR_API_KEY with your actual API key from https://dashboard.bfl.ai
#
API_KEY="${BFL_API_KEY:-YOUR_API_KEY}"
BASE_URL="https://api.bfl.ai"
# -----------------------------------------------------------------------------
# FIRST: Verify API Key is Set
# -----------------------------------------------------------------------------
# Always check this before making requests to avoid "Not authenticated" errors
if [ "$API_KEY" = "YOUR_API_KEY" ] || [ -z "$API_KEY" ]; then
echo "Error: BFL_API_KEY not set"
echo ""
echo "To fix:"
echo " 1. Get a key at https://dashboard.bfl.ai/get-started"
echo " 2. Run: export BFL_API_KEY=your_key_here"
exit 1
fi
echo "OK: API key configured"
# -----------------------------------------------------------------------------
# Example 1: Basic Image Generation with FLUX.2 Pro
# -----------------------------------------------------------------------------
echo "=== Submitting generation request ==="
RESPONSE=$(curl -s -X POST "${BASE_URL}/v1/flux-2-pro" \
-H "x-key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A serene mountain landscape at golden hour, dramatic lighting",
"width": 1024,
"height": 1024
}')
echo "Response: ${RESPONSE}"
# Extract polling URL
POLLING_URL=$(echo "${RESPONSE}" | grep -o '"polling_url":"[^"]*"' | cut -d'"' -f4)
echo "Polling URL: ${POLLING_URL}"
# -----------------------------------------------------------------------------
# Example 2: Poll for Result
# -----------------------------------------------------------------------------
echo ""
echo "=== Polling for result ==="
while true; do
RESULT=$(curl -s "${POLLING_URL}" -H "x-key: ${API_KEY}")
STATUS=$(echo "${RESULT}" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
echo "Status: ${STATUS}"
if [ "${STATUS}" = "Ready" ]; then
IMAGE_URL=$(echo "${RESULT}" | grep -o '"sample":"[^"]*"' | cut -d'"' -f4)
echo "Image URL: ${IMAGE_URL}"
break
elif [ "${STATUS}" = "Error" ]; then
echo "Generation failed!"
echo "${RESULT}"
exit 1
fi
sleep 2
done
# -----------------------------------------------------------------------------
# Example 3: Download the Image
# -----------------------------------------------------------------------------
echo ""
echo "=== Downloading image ==="
curl -s -o output.png "${IMAGE_URL}"
echo "Saved to output.png"
# =============================================================================
# ONE-LINER EXAMPLES (for quick reference)
# =============================================================================
# Submit request (returns polling_url):
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-pro" \
# -H "x-key: YOUR_API_KEY" \
# -H "Content-Type: application/json" \
# -d '{"prompt": "A red apple", "width": 1024, "height": 1024}'
# Poll for result (replace POLLING_URL):
# curl -s "POLLING_URL" -H "x-key: YOUR_API_KEY"
# Download image (replace IMAGE_URL):
# curl -s -o output.png "IMAGE_URL"
# =============================================================================
# IMAGE-TO-IMAGE EDITING
# =============================================================================
# Preferred: Pass image URLs directly - simpler and more convenient than base64.
# The API fetches URLs automatically. Both URL and base64 work.
echo ""
echo "=== Image-to-Image Edit Example ==="
# Edit an image using its URL directly
I2I_RESPONSE=$(curl -s -X POST "${BASE_URL}/v1/flux-2-pro" \
-H "x-key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Change the background to a sunset beach",
"input_image": "https://example.com/photo.jpg"
}')
echo "I2I Response: ${I2I_RESPONSE}"
# Multi-reference example (combine elements from multiple images)
# curl -s -X POST "${BASE_URL}/v1/flux-2-max" \
# -H "x-key: ${API_KEY}" \
# -H "Content-Type: application/json" \
# -d '{
# "prompt": "Person from image 1 wearing outfit from image 2 in setting from image 3",
# "input_image": "https://example.com/person.jpg",
# "input_image_2": "https://example.com/outfit.jpg",
# "input_image_3": "https://example.com/location.jpg"
# }'
# =============================================================================
# MODEL ENDPOINT EXAMPLES
# =============================================================================
# FLUX.2 [klein] 4B - Fastest
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-klein-4b" ...
# FLUX.2 [klein] 9B - Fast with better quality
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-klein-9b" ...
# FLUX.2 [pro] - Production balanced
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-pro" ...
# FLUX.2 [max] - Highest quality
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-max" ...
# FLUX.2 [flex] - Best for typography
# curl -s -X POST "https://api.bfl.ai/v1/flux-2-flex" ...
# =============================================================================
# REGIONAL ENDPOINTS
# =============================================================================
# Global (default): https://api.bfl.ai
# EU (GDPR): https://api.eu.bfl.ai
# US: https://api.us.bfl.ai
@@ -0,0 +1,476 @@
"""
BFL FLUX API Python Client
A complete, production-ready Python client for the BFL FLUX API.
Includes rate limiting, retry logic, webhook support, and async operations.
Usage:
from bfl_client import BFLClient
client = BFLClient("your-api-key")
result = client.generate("flux-2-pro", "A beautiful sunset")
print(f"Image URL: {result['url']}")
"""
import os
import time
import hmac
import hashlib
import logging
from typing import Optional, Dict, Any, List, Callable
from dataclasses import dataclass
from threading import Semaphore, Lock
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Exceptions ---
class BFLError(Exception):
"""Base exception for BFL API errors."""
def __init__(self, message: str, status_code: int = None, error_code: str = None):
self.message = message
self.status_code = status_code
self.error_code = error_code
super().__init__(message)
class AuthenticationError(BFLError):
"""API key or authentication issue."""
pass
class InsufficientCreditsError(BFLError):
"""Account needs more credits."""
pass
class RateLimitError(BFLError):
"""Too many concurrent requests."""
def __init__(self, message: str, retry_after: int = 5):
super().__init__(message, 429, "rate_limit_exceeded")
self.retry_after = retry_after
class ValidationError(BFLError):
"""Invalid request parameters."""
pass
class GenerationError(BFLError):
"""Generation failed."""
pass
# --- Data Classes ---
@dataclass
class GenerationResult:
"""Result of a successful generation."""
id: str
url: str
width: int
height: int
raw: Dict[str, Any]
# --- Client ---
class BFLClient:
"""
Production-ready BFL FLUX API client.
Features:
- Rate limiting with semaphore
- Automatic retries with exponential backoff
- Webhook support
- Batch processing
- Async operations
Example:
client = BFLClient("your-api-key")
result = client.generate("flux-2-pro", "A sunset over mountains")
client.download(result.url, "sunset.png")
"""
BASE_URLS = {
"global": "https://api.bfl.ai",
"eu": "https://api.eu.bfl.ai",
"us": "https://api.us.bfl.ai",
}
RATE_LIMITS = {
"default": 24,
"flux-kontext-max": 6,
}
def __init__(
self,
api_key: str,
region: str = "global",
max_concurrent: int = None,
timeout: int = 120,
):
"""
Initialize the BFL client.
Args:
api_key: Your BFL API key
region: API region ("global", "eu", "us")
max_concurrent: Max concurrent requests (default: 24)
timeout: Default polling timeout in seconds
"""
self.api_key = api_key
self.base_url = self.BASE_URLS.get(region, self.BASE_URLS["global"])
self.timeout = timeout
self.max_concurrent = max_concurrent or self.RATE_LIMITS["default"]
self.semaphore = Semaphore(self.max_concurrent)
self.headers = {
"x-key": api_key,
"Content-Type": "application/json",
}
def generate(
self,
model: str,
prompt: str,
width: int = 1024,
height: int = 1024,
seed: int = None,
safety_tolerance: int = 2,
output_format: str = "png",
webhook_url: str = None,
webhook_secret: str = None,
timeout: int = None,
**kwargs,
) -> GenerationResult:
"""
Generate an image from a text prompt.
Args:
model: Model to use (e.g., "flux-2-pro", "flux-2-max")
prompt: Text description of the image
width: Image width (multiple of 16)
height: Image height (multiple of 16)
seed: Random seed for reproducibility
safety_tolerance: 0 (strict) to 5 (permissive)
output_format: "png" or "jpeg"
webhook_url: URL for async notification
webhook_secret: Secret for webhook signature
timeout: Polling timeout override
**kwargs: Additional model-specific parameters
Returns:
GenerationResult with image URL and metadata
"""
# Validate dimensions
self._validate_dimensions(width, height)
# Build payload
payload = {
"prompt": prompt,
"width": width,
"height": height,
"safety_tolerance": safety_tolerance,
"output_format": output_format,
**kwargs,
}
if seed is not None:
payload["seed"] = seed
if webhook_url:
payload["webhook_url"] = webhook_url
if webhook_secret:
payload["webhook_secret"] = webhook_secret
# Rate-limited request
with self.semaphore:
return self._submit_and_poll(model, payload, timeout or self.timeout)
def generate_i2i(
self,
model: str,
prompt: str,
input_image: str,
additional_images: List[str] = None,
**kwargs,
) -> GenerationResult:
"""
Generate an image from another image (image-to-image).
Preferred: Pass image URLs directly - simpler and more convenient than base64.
The API fetches URLs automatically. Both URL and base64 work.
Args:
model: Model to use (e.g., "flux-2-pro", "flux-2-max")
prompt: Edit instructions
input_image: Image URL (preferred) or base64
additional_images: List of additional reference image URLs or base64
**kwargs: Additional parameters
Returns:
GenerationResult with edited image
Example:
result = client.generate_i2i(
"flux-2-pro",
"Change the background to a sunset",
"https://example.com/photo.jpg" # URL is simpler!
)
"""
payload = {
"prompt": prompt,
"input_image": input_image,
**kwargs,
}
# Add additional images
if additional_images:
for i, img in enumerate(additional_images[:7], start=2):
payload[f"input_image_{i}"] = img
with self.semaphore:
return self._submit_and_poll(model, payload, self.timeout)
def generate_batch(
self,
model: str,
prompts: List[str],
max_workers: int = None,
**kwargs,
) -> List[GenerationResult]:
"""
Generate multiple images concurrently.
Args:
model: Model to use
prompts: List of prompts
max_workers: Number of concurrent workers
**kwargs: Parameters applied to all generations
Returns:
List of GenerationResult objects
"""
max_workers = max_workers or min(len(prompts), self.max_concurrent)
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(self.generate, model, prompt, **kwargs): prompt
for prompt in prompts
}
for future in as_completed(futures):
try:
result = future.result()
results.append(result)
except Exception as e:
logger.error(f"Generation failed: {e}")
results.append(None)
return results
def download(self, url: str, output_path: str) -> str:
"""
Download a generated image.
Args:
url: Image URL (expires in 10 minutes)
output_path: Local path to save the image
Returns:
Path to saved file
"""
response = requests.get(url, timeout=60)
response.raise_for_status()
with open(output_path, "wb") as f:
f.write(response.content)
return output_path
def _submit_and_poll(
self,
model: str,
payload: Dict[str, Any],
timeout: int,
) -> GenerationResult:
"""Submit request and poll for result."""
endpoint = f"{self.base_url}/v1/{model}"
# Submit with retry
response = self._request_with_retry(
"POST",
endpoint,
json=payload,
)
polling_url = response["polling_url"]
generation_id = response.get("id", polling_url.split("=")[-1])
# Poll for result
result = self._poll(polling_url, timeout)
return GenerationResult(
id=generation_id,
url=result["sample"],
width=result.get("width", payload.get("width")),
height=result.get("height", payload.get("height")),
raw=result,
)
def _poll(self, polling_url: str, timeout: int) -> Dict[str, Any]:
"""Poll until completion or timeout."""
start_time = time.time()
delay = 1.0
while time.time() - start_time < timeout:
response = self._request_with_retry("GET", polling_url)
status = response.get("status")
if status == "Ready":
return response.get("result", response)
elif status == "Error":
error = response.get("error", "Generation failed")
raise GenerationError(error)
# Exponential backoff (cap at 5 seconds)
time.sleep(delay)
delay = min(delay * 1.5, 5.0)
raise TimeoutError(f"Generation timed out after {timeout}s")
def _request_with_retry(
self,
method: str,
url: str,
max_retries: int = 3,
**kwargs,
) -> Dict[str, Any]:
"""Make HTTP request with retry logic."""
last_exception = None
for attempt in range(max_retries):
try:
response = requests.request(
method,
url,
headers=self.headers,
timeout=30,
**kwargs,
)
return self._handle_response(response)
except RateLimitError as e:
logger.warning(f"Rate limited, waiting {e.retry_after}s")
time.sleep(e.retry_after * (attempt + 1))
last_exception = e
except BFLError as e:
if e.status_code and e.status_code >= 500:
wait_time = 2 ** attempt
logger.warning(f"Server error, retrying in {wait_time}s")
time.sleep(wait_time)
last_exception = e
else:
raise
raise last_exception
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""Process API response and raise appropriate errors."""
if response.status_code == 200:
return response.json()
try:
error_data = response.json()
except:
error_data = {"message": response.text}
error_code = error_data.get("error", "unknown")
message = error_data.get("message", "Unknown error")
if response.status_code == 401:
raise AuthenticationError(message, 401, error_code)
elif response.status_code == 402:
raise InsufficientCreditsError(message, 402, error_code)
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
raise RateLimitError(message, retry_after)
elif response.status_code == 400:
raise ValidationError(message, 400, error_code)
else:
raise BFLError(message, response.status_code, error_code)
def _validate_dimensions(self, width: int, height: int):
"""Validate image dimensions."""
if width % 16 != 0:
raise ValidationError(f"Width {width} must be a multiple of 16")
if height % 16 != 0:
raise ValidationError(f"Height {height} must be a multiple of 16")
if width * height > 4_000_000:
raise ValidationError(f"Total pixels ({width}x{height}) exceeds 4MP limit")
if width < 64 or height < 64:
raise ValidationError("Minimum dimension is 64 pixels")
# --- Webhook Verification ---
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
"""
Verify a webhook signature from BFL.
Args:
payload: Raw request body
signature: X-BFL-Signature header value
secret: Your webhook secret
Returns:
True if signature is valid
"""
if not signature or not signature.startswith("sha256="):
return False
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
provided = signature[7:] # Remove 'sha256=' prefix
return hmac.compare_digest(expected, provided)
# --- Example Usage ---
if __name__ == "__main__":
# Get API key from environment
api_key = os.environ.get("BFL_API_KEY")
if not api_key:
print("Set BFL_API_KEY environment variable")
exit(1)
# Create client
client = BFLClient(api_key)
# Generate a single image
print("Generating image...")
result = client.generate(
model="flux-2-pro",
prompt="A serene mountain landscape at golden hour, dramatic lighting",
width=1024,
height=1024,
)
print(f"Generated: {result.url}")
# Download the image
client.download(result.url, "output.png")
print("Saved to output.png")
@@ -0,0 +1,480 @@
/**
* BFL FLUX API TypeScript Client
*
* A complete, production-ready TypeScript client for the BFL FLUX API.
* Includes rate limiting, retry logic, webhook support, and async operations.
*
* Usage:
* import { BFLClient } from './bfl-client';
*
* const client = new BFLClient('your-api-key');
* const result = await client.generate('flux-2-pro', 'A beautiful sunset');
* console.log(`Image URL: ${result.url}`);
*/
import * as crypto from "crypto";
// --- Types ---
export interface GenerationResult {
id: string;
url: string;
width: number;
height: number;
raw: Record<string, unknown>;
}
export interface GenerateOptions {
width?: number;
height?: number;
seed?: number;
safetyTolerance?: number;
outputFormat?: "png" | "jpeg";
webhookUrl?: string;
webhookSecret?: string;
timeout?: number;
steps?: number; // For flex model
guidance?: number; // For flex model
}
export interface I2IOptions extends GenerateOptions {
additionalImages?: string[];
}
export type Region = "global" | "eu" | "us";
// --- Errors ---
export class BFLError extends Error {
constructor(
message: string,
public statusCode?: number,
public errorCode?: string
) {
super(message);
this.name = "BFLError";
}
}
export class AuthenticationError extends BFLError {
constructor(message: string) {
super(message, 401, "authentication_error");
this.name = "AuthenticationError";
}
}
export class InsufficientCreditsError extends BFLError {
constructor(message: string) {
super(message, 402, "insufficient_credits");
this.name = "InsufficientCreditsError";
}
}
export class RateLimitError extends BFLError {
constructor(
message: string,
public retryAfter: number = 5
) {
super(message, 429, "rate_limit_exceeded");
this.name = "RateLimitError";
}
}
export class ValidationError extends BFLError {
constructor(message: string) {
super(message, 400, "validation_error");
this.name = "ValidationError";
}
}
export class GenerationError extends BFLError {
constructor(message: string) {
super(message, undefined, "generation_error");
this.name = "GenerationError";
}
}
// --- Rate Limiter ---
class Semaphore {
private permits: number;
private waiting: Array<() => void> = [];
constructor(permits: number) {
this.permits = permits;
}
async acquire(): Promise<void> {
if (this.permits > 0) {
this.permits--;
return;
}
return new Promise((resolve) => {
this.waiting.push(resolve);
});
}
release(): void {
if (this.waiting.length > 0) {
const next = this.waiting.shift();
next?.();
} else {
this.permits++;
}
}
}
// --- Client ---
export class BFLClient {
private static readonly BASE_URLS: Record<Region, string> = {
global: "https://api.bfl.ai",
eu: "https://api.eu.bfl.ai",
us: "https://api.us.bfl.ai",
};
private static readonly RATE_LIMITS: Record<string, number> = {
default: 24,
"flux-kontext-max": 6,
};
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
private readonly timeout: number;
private readonly semaphore: Semaphore;
/**
* Create a new BFL client.
*
* @param apiKey - Your BFL API key
* @param region - API region ("global", "eu", "us")
* @param maxConcurrent - Max concurrent requests (default: 24)
* @param timeout - Default polling timeout in milliseconds
*/
constructor(
private readonly apiKey: string,
region: Region = "global",
maxConcurrent: number = 24,
timeout: number = 120000
) {
this.baseUrl = BFLClient.BASE_URLS[region];
this.timeout = timeout;
this.semaphore = new Semaphore(maxConcurrent);
this.headers = {
"x-key": apiKey,
"Content-Type": "application/json",
};
}
/**
* Generate an image from a text prompt.
*/
async generate(
model: string,
prompt: string,
options: GenerateOptions = {}
): Promise<GenerationResult> {
const {
width = 1024,
height = 1024,
seed,
safetyTolerance = 2,
outputFormat = "png",
webhookUrl,
webhookSecret,
timeout = this.timeout,
steps,
guidance,
} = options;
// Validate dimensions
this.validateDimensions(width, height);
// Build payload
const payload: Record<string, unknown> = {
prompt,
width,
height,
safety_tolerance: safetyTolerance,
output_format: outputFormat,
};
if (seed !== undefined) payload.seed = seed;
if (webhookUrl) payload.webhook_url = webhookUrl;
if (webhookSecret) payload.webhook_secret = webhookSecret;
if (steps !== undefined) payload.steps = steps;
if (guidance !== undefined) payload.guidance = guidance;
// Rate-limited request
await this.semaphore.acquire();
try {
return await this.submitAndPoll(model, payload, timeout);
} finally {
this.semaphore.release();
}
}
/**
* Generate an image from another image (image-to-image).
*
* Preferred: Pass image URLs directly - simpler and more convenient than base64.
* The API fetches URLs automatically. Both URL and base64 work.
*
* @param model - Model to use (e.g., "flux-2-pro", "flux-2-max")
* @param prompt - Edit instructions
* @param inputImage - Image URL (preferred) or base64
* @param options - Additional options including more reference image URLs or base64
*
* @example
* const result = await client.generateI2I(
* "flux-2-pro",
* "Change the background to a sunset",
* "https://example.com/photo.jpg" // URL is simpler!
* );
*/
async generateI2I(
model: string,
prompt: string,
inputImage: string,
options: I2IOptions = {}
): Promise<GenerationResult> {
const { additionalImages = [], ...rest } = options;
const payload: Record<string, unknown> = {
prompt,
input_image: inputImage,
};
// Add additional images
additionalImages.slice(0, 7).forEach((img, i) => {
payload[`input_image_${i + 2}`] = img;
});
await this.semaphore.acquire();
try {
return await this.submitAndPoll(model, payload, rest.timeout ?? this.timeout);
} finally {
this.semaphore.release();
}
}
/**
* Generate multiple images concurrently.
*/
async generateBatch(
model: string,
prompts: string[],
options: GenerateOptions = {}
): Promise<Array<GenerationResult | Error>> {
const tasks = prompts.map((prompt) =>
this.generate(model, prompt, options).catch((e) => e)
);
return Promise.all(tasks);
}
/**
* Download a generated image.
*/
async download(url: string): Promise<ArrayBuffer> {
const response = await fetch(url);
if (!response.ok) {
throw new BFLError(`Failed to download: ${response.status}`);
}
return response.arrayBuffer();
}
private async submitAndPoll(
model: string,
payload: Record<string, unknown>,
timeout: number
): Promise<GenerationResult> {
const endpoint = `${this.baseUrl}/v1/${model}`;
// Submit request
const submitResponse = await this.requestWithRetry("POST", endpoint, payload);
const pollingUrl = submitResponse.polling_url as string;
const generationId =
(submitResponse.id as string) ?? pollingUrl.split("=").pop() ?? "unknown";
// Poll for result
const result = await this.poll(pollingUrl, timeout);
return {
id: generationId,
url: result.sample as string,
width: (result.width as number) ?? (payload.width as number),
height: (result.height as number) ?? (payload.height as number),
raw: result,
};
}
private async poll(
pollingUrl: string,
timeout: number
): Promise<Record<string, unknown>> {
const startTime = Date.now();
let delay = 1000;
while (Date.now() - startTime < timeout) {
const response = await this.requestWithRetry("GET", pollingUrl);
const status = response.status as string;
if (status === "Ready") {
return (response.result as Record<string, unknown>) ?? response;
} else if (status === "Error") {
throw new GenerationError((response.error as string) ?? "Generation failed");
}
// Exponential backoff (cap at 5 seconds)
await this.sleep(delay);
delay = Math.min(delay * 1.5, 5000);
}
throw new Error(`Generation timed out after ${timeout}ms`);
}
private async requestWithRetry(
method: string,
url: string,
body?: Record<string, unknown>,
maxRetries: number = 3
): Promise<Record<string, unknown>> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
return await this.handleResponse(response);
} catch (e) {
if (e instanceof RateLimitError) {
console.warn(`Rate limited, waiting ${e.retryAfter}s`);
await this.sleep(e.retryAfter * 1000 * (attempt + 1));
lastError = e;
} else if (e instanceof BFLError && e.statusCode && e.statusCode >= 500) {
const waitTime = Math.pow(2, attempt) * 1000;
console.warn(`Server error, retrying in ${waitTime}ms`);
await this.sleep(waitTime);
lastError = e;
} else {
throw e;
}
}
}
throw lastError ?? new Error("Max retries exceeded");
}
private async handleResponse(response: Response): Promise<Record<string, unknown>> {
if (response.ok) {
return response.json();
}
let errorData: Record<string, unknown>;
try {
errorData = await response.json();
} catch {
errorData = { message: await response.text() };
}
const errorCode = (errorData.error as string) ?? "unknown";
const message = (errorData.message as string) ?? "Unknown error";
switch (response.status) {
case 401:
throw new AuthenticationError(message);
case 402:
throw new InsufficientCreditsError(message);
case 429:
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "5", 10);
throw new RateLimitError(message, retryAfter);
case 400:
throw new ValidationError(message);
default:
throw new BFLError(message, response.status, errorCode);
}
}
private validateDimensions(width: number, height: number): void {
if (width % 16 !== 0) {
throw new ValidationError(`Width ${width} must be a multiple of 16`);
}
if (height % 16 !== 0) {
throw new ValidationError(`Height ${height} must be a multiple of 16`);
}
if (width * height > 4_000_000) {
throw new ValidationError(`Total pixels (${width}x${height}) exceeds 4MP limit`);
}
if (width < 64 || height < 64) {
throw new ValidationError("Minimum dimension is 64 pixels");
}
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// --- Webhook Verification ---
/**
* Verify a webhook signature from BFL.
*
* @param payload - Raw request body as string
* @param signature - X-BFL-Signature header value
* @param secret - Your webhook secret
* @returns True if signature is valid
*/
export function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
if (!signature || !signature.startsWith("sha256=")) {
return false;
}
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
const providedSignature = signature.slice(7); // Remove 'sha256=' prefix
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
// --- Example Usage ---
async function main() {
const apiKey = process.env.BFL_API_KEY;
if (!apiKey) {
console.error("Set BFL_API_KEY environment variable");
process.exit(1);
}
const client = new BFLClient(apiKey);
console.log("Generating image...");
const result = await client.generate("flux-2-pro", "A serene mountain landscape at golden hour", {
width: 1024,
height: 1024,
});
console.log(`Generated: ${result.url}`);
console.log(`Image ID: ${result.id}`);
}
// Run if executed directly
main().catch(console.error);
@@ -0,0 +1,286 @@
---
name: endpoints
description: Complete BFL API endpoint documentation
---
# BFL API Endpoints
Complete reference for all BFL FLUX API endpoints.
## Base URLs
| Region | Endpoint | Use Case |
| ------ | ----------------------- | ---------------------------------- |
| Global | `https://api.bfl.ai` | Default, automatic failover |
| EU | `https://api.eu.bfl.ai` | GDPR compliance, EU data residency |
| US | `https://api.us.bfl.ai` | US data residency |
**Recommendation:** Use the global endpoint (`api.bfl.ai`) unless you have specific regional requirements.
## Authentication
All requests require the `x-key` header with your API key:
```bash
x-key: YOUR_API_KEY
```
## FLUX.2 Text-to-Image and Image-to-Image Endpoints
### FLUX.2 [klein] 4B
```
POST /v1/flux-2-klein-4b
```
Fastest generation, 4B parameters.
### FLUX.2 [klein] 9B
```
POST /v1/flux-2-klein-9b
```
Fast generation with better quality, 9B parameters.
### FLUX.2 [max]
```
POST /v1/flux-2-max
```
Highest quality, supports grounding search.
### FLUX.2 [pro]
```
POST /v1/flux-2-pro
```
Production balanced quality and speed.
### FLUX.2 [flex]
```
POST /v1/flux-2-flex
```
Typography optimized, adjustable steps/guidance.
## FLUX.1 Endpoints
### FLUX1.1 [pro]
```
POST /v1/flux-pro-1.1
```
Text-to-image generation.
### FLUX.1 Kontext
```
POST /v1/flux-kontext
```
### FLUX.1 Kontext Max
```
POST /v1/flux-kontext-max
```
### FLUX.1 Fill
```
POST /v1/flux-fill
```
Inpainting and object removal - you can achieve inpainting and object removal with specific prompting style with FLUX.2 models for better performance.
## Common Request Parameters
### Text-to-Image (T2I)
| Parameter | Type | Required | Description |
| ------------------ | ------- | -------- | -------------------------------------------- |
| `prompt` | string | Yes | Text description (up to 32K tokens) |
| `width` | integer | No | Image width (multiple of 16, max 4MP total) |
| `height` | integer | No | Image height (multiple of 16, max 4MP total) |
| `seed` | integer | No | Random seed for reproducibility |
| `safety_tolerance` | integer | No | 0 (strict) to 5 (permissive), default 2 |
| `output_format` | string | No | "jpeg" or "png", default "jpeg" |
| `webhook_url` | string | No | URL for async notification |
| `webhook_secret` | string | No | Secret for webhook signature |
### Image-to-Image (I2I)
> **Important:** All FLUX.2 models (klein, pro, max, flex) support image-to-image editing via the `input_image` parameter. FLUX.2 is recommended over FLUX.1 Kontext for editing.
> **Preferred: Use URLs directly** - The API fetches URLs automatically, which is simpler and more convenient than downloading and encoding to base64. Both URL and base64 work, but URLs are recommended when available.
| Parameter | Type | Required | Description |
| --------------------------------- | ------- | -------- | -------------------------------------------------------------- |
| `prompt` | string | Yes | Edit instruction |
| `input_image` | string | Yes | **URL (preferred)** or base64 - API fetches URLs automatically |
| `input_image_2` - `input_image_8` | string | No | Additional reference URLs or base64 |
| `width` | integer | No | Output width |
| `height` | integer | No | Output height |
### FLUX.2 [flex] Specific
| Parameter | Type | Default | Description |
| ---------- | ------- | ------- | ----------------------- |
| `steps` | integer | 50 | Inference steps (1-50) |
| `guidance` | float | 4.5 | Guidance scale (1.5-10) |
## Resolution Constraints
- **Minimum:** 64x64 pixels
- **Maximum:** 4MP total (width x height)
- **Multiple of:** 16 (both dimensions)
### Common Resolutions
| Aspect Ratio | Resolution | Megapixels |
| --------------- | ---------- | ---------- |
| 1:1 (Square) | 1024x1024 | 1.05 MP |
| 16:9 (Wide) | 1920x1080 | 2.07 MP |
| 9:16 (Portrait) | 1080x1920 | 2.07 MP |
| 4:3 (Classic) | 1536x1152 | 1.77 MP |
| 2:1 (Panorama) | 2048x1024 | 2.10 MP |
## Example Requests
### Basic T2I Request
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A serene mountain landscape at golden hour",
"width": 1024,
"height": 1024
}'
```
### Response
```json
{
"id": "gen_abc123xyz",
"polling_url": "https://api.bfl.ai/v1/get_result?id=gen_abc123xyz"
}
```
### T2I with All Options
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-max" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Professional headshot of a business executive",
"width": 1024,
"height": 1280,
"seed": 42,
"safety_tolerance": 2,
"output_format": "png",
"webhook_url": "https://your-server.com/webhook",
"webhook_secret": "your-secret-key"
}'
```
### I2I Request (FLUX.2 - Recommended)
Edit images using any FLUX.2 model by passing the source image URL directly:
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-klein-9b" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Change the floor color to light blue",
"input_image": "https://example.com/room-photo.jpg"
}'
```
For higher quality edits, use FLUX.2 [pro] or [max]:
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Change the background to a beach sunset",
"input_image": "https://example.com/portrait.jpg"
}'
```
### Multi-Reference I2I (FLUX.2)
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-max" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Person from image 1 wearing outfit from image 2 in setting from image 3",
"input_image": "https://example.com/person.jpg",
"input_image_2": "https://example.com/outfit.jpg",
"input_image_3": "https://example.com/location.jpg"
}'
```
### FLUX.2 [flex] with Custom Steps
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-flex" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A poster with text \"SUMMER SALE\" in bold typography",
"steps": 50,
"guidance": 7.0
}'
```
## Polling Endpoint
### Get Result
```
GET /v1/get_result?id={generation_id}
```
### Response States
```json
// Pending
{ "status": "Pending" }
// Ready
{
"status": "Ready",
"result": {
"sample": "https://bfldeliveryprod.blob.core.windows.net/results/...",
"prompt": "...",
"seed": 1234567890
}
}
// Error
{
"status": "Error",
"error": "Error description"
}
```
## Error Responses
| Status Code | Meaning | Action |
| ----------- | ---------------- | ------------------ |
| 400 | Bad Request | Check parameters |
| 401 | Unauthorized | Verify API key |
| 402 | Payment Required | Add credits |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry with backoff |
@@ -0,0 +1,334 @@
---
name: error-handling
description: Error codes and recovery strategies for BFL API
---
# Error Handling
Comprehensive guide to handling errors from the BFL API.
## HTTP Status Codes
| Code | Meaning | Cause | Action |
|------|---------|-------|--------|
| 200 | OK | Request successful | Process response |
| 400 | Bad Request | Invalid parameters | Check request format |
| 401 | Unauthorized | Invalid/missing API key | Verify credentials |
| 402 | Payment Required | Insufficient credits | Add credits to account |
| 403 | Forbidden | Access denied | Check permissions |
| 404 | Not Found | Invalid endpoint | Verify URL |
| 429 | Too Many Requests | Rate limited | Implement backoff |
| 500 | Internal Server Error | Server issue | Retry with backoff |
| 502 | Bad Gateway | Network issue | Retry with backoff |
| 503 | Service Unavailable | Temporary outage | Retry with backoff |
## Error Response Format
```json
{
"error": "error_code",
"message": "Human-readable description",
"details": {
"field": "specific field info"
}
}
```
## Common Errors and Solutions
### Authentication Errors (401)
```json
{
"error": "invalid_api_key",
"message": "The provided API key is invalid or expired"
}
```
**Solution:**
```python
def verify_api_key(api_key):
if not api_key:
raise ValueError("API key is required")
if not api_key.startswith("bfl_"):
raise ValueError("Invalid API key format")
```
### Insufficient Credits (402)
```json
{
"error": "insufficient_credits",
"message": "Your account does not have enough credits"
}
```
**Solution:**
```python
def handle_payment_error(response):
if response.status_code == 402:
# Log and alert
logging.error("Insufficient credits - add funds")
# Optionally pause operations
raise InsufficientCreditsError("Add credits to continue")
```
### Rate Limiting (429)
```json
{
"error": "rate_limit_exceeded",
"message": "Too many concurrent requests",
"retry_after": 5
}
```
**Solution:**
```python
def handle_rate_limit(response):
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
time.sleep(retry_after)
return True # Signal to retry
return False
```
### Validation Errors (400)
```json
{
"error": "validation_error",
"message": "Invalid request parameters",
"details": {
"width": "Must be a multiple of 16",
"prompt": "Cannot be empty"
}
}
```
**Solution:**
```python
def validate_request(prompt, width, height):
errors = []
if not prompt or not prompt.strip():
errors.append("Prompt cannot be empty")
if width % 16 != 0:
errors.append(f"Width {width} must be multiple of 16")
if height % 16 != 0:
errors.append(f"Height {height} must be multiple of 16")
if width * height > 4_000_000:
errors.append("Total pixels cannot exceed 4MP")
if errors:
raise ValidationError(errors)
```
### Generation Failures
Failures during polling:
```json
{
"status": "Error",
"error": "content_policy_violation",
"message": "The prompt violated content policy"
}
```
**Common failure reasons:**
- `content_policy_violation` - Prompt/image flagged by safety
- `generation_timeout` - Took too long to generate
- `internal_error` - Server-side issue
- `invalid_image` - Input image couldn't be processed
## Retry Strategy
```python
import time
import random
class RetryableError(Exception):
"""Errors that can be retried."""
pass
class NonRetryableError(Exception):
"""Errors that should not be retried."""
pass
def classify_error(status_code, error_code):
"""Determine if error is retryable."""
# Retryable
if status_code in [429, 500, 502, 503]:
return RetryableError
# Non-retryable
if status_code in [400, 401, 402, 403]:
return NonRetryableError
# Generation failures
if error_code in ['generation_timeout', 'internal_error']:
return RetryableError
if error_code in ['content_policy_violation', 'invalid_image']:
return NonRetryableError
return RetryableError # Default to retryable
def make_request_with_retry(func, max_retries=3):
"""Execute function with retry logic."""
last_exception = None
for attempt in range(max_retries):
try:
return func()
except RetryableError as e:
last_exception = e
wait_time = (2 ** attempt) + random.uniform(0, 1)
print(f"Attempt {attempt + 1} failed, retrying in {wait_time:.1f}s")
time.sleep(wait_time)
except NonRetryableError:
raise # Don't retry
raise last_exception
```
## Comprehensive Error Handler
```python
import logging
class BFLError(Exception):
"""Base exception for BFL API errors."""
def __init__(self, message, status_code=None, error_code=None):
self.message = message
self.status_code = status_code
self.error_code = error_code
super().__init__(message)
class AuthenticationError(BFLError):
"""API key or authentication issue."""
pass
class InsufficientCreditsError(BFLError):
"""Account needs more credits."""
pass
class RateLimitError(BFLError):
"""Too many concurrent requests."""
def __init__(self, message, retry_after=5):
super().__init__(message, 429, "rate_limit_exceeded")
self.retry_after = retry_after
class ValidationError(BFLError):
"""Invalid request parameters."""
pass
class GenerationError(BFLError):
"""Generation failed."""
pass
def handle_response(response):
"""Process API response and raise appropriate errors."""
if response.status_code == 200:
return response.json()
try:
error_data = response.json()
except:
error_data = {"message": response.text}
error_code = error_data.get("error", "unknown")
message = error_data.get("message", "Unknown error")
if response.status_code == 401:
raise AuthenticationError(message, 401, error_code)
if response.status_code == 402:
raise InsufficientCreditsError(message, 402, error_code)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
raise RateLimitError(message, retry_after)
if response.status_code == 400:
raise ValidationError(message, 400, error_code)
if response.status_code >= 500:
raise BFLError(f"Server error: {message}", response.status_code, error_code)
raise BFLError(message, response.status_code, error_code)
```
## Logging Best Practices
```python
import logging
import json
def setup_logging():
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def log_request(endpoint, payload):
logging.info(f"Request: POST {endpoint}")
logging.debug(f"Payload: {json.dumps(payload, indent=2)}")
def log_error(error, context=None):
logging.error(f"Error: {error}")
if context:
logging.error(f"Context: {context}")
def log_generation_failure(status, error, prompt):
logging.warning(f"Generation failed: {error}")
logging.debug(f"Failed prompt: {prompt[:100]}...")
```
## Circuit Breaker Pattern
For production systems:
```python
import time
from threading import Lock
class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_timeout=60):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.failures = 0
self.last_failure_time = None
self.state = "closed" # closed, open, half-open
self.lock = Lock()
def record_success(self):
with self.lock:
self.failures = 0
self.state = "closed"
def record_failure(self):
with self.lock:
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = "open"
def can_proceed(self):
with self.lock:
if self.state == "closed":
return True
if self.state == "open":
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = "half-open"
return True
return False
# half-open: allow one request to test
return True
```
@@ -0,0 +1,240 @@
---
name: polling-patterns
description: Implementing async polling for BFL API responses
---
# Polling Patterns
BFL API uses asynchronous generation. All requests return a `polling_url` for status checking.
## Basic Flow
```
1. POST request to model endpoint
└─> Immediate response: { "polling_url": "..." }
2. GET polling_url (repeat until complete)
└─> { "status": "Pending" | "Ready" | "Error" }
3. When "Ready", download result sample URL
└─> URL expires in 10 minutes
```
## Response States
| Status | Description | Action |
|--------|-------------|--------|
| `Pending` | Request queued/processing | Continue polling |
| `Ready` | Generation finished | Download result |
| `Error` | Generation Error | Handle error |
## Polling Strategies
### Simple Fixed Interval
```python
import time
import requests
def poll_fixed_interval(polling_url, headers, interval=2, timeout=120):
"""Simple polling with fixed interval."""
start_time = time.time()
while time.time() - start_time < timeout:
response = requests.get(polling_url, headers=headers)
data = response.json()
if data["status"] == "Ready":
return data["result"]
elif data["status"] == "Error":
raise Exception(data.get("error", "Generation Error"))
time.sleep(interval)
raise TimeoutError("Polling timeout exceeded")
```
### Exponential Backoff (Recommended)
```python
import time
import random
import requests
def poll_with_backoff(polling_url, headers, max_attempts=30):
"""Polling with exponential backoff and jitter."""
base_delay = 0.5 # Start with 500ms
max_delay = 10 # Cap at 10 seconds
for attempt in range(max_attempts):
response = requests.get(polling_url, headers=headers)
data = response.json()
if data["status"] == "Ready":
return data["result"]
elif data["status"] == "Error":
raise Exception(data.get("error", "Generation Error"))
# Exponential backoff with jitter
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # 10% jitter
time.sleep(delay + jitter)
raise TimeoutError("Max polling attempts exceeded")
```
### Adaptive Polling
```python
import time
import requests
def poll_adaptive(polling_url, headers, timeout=120):
"""Adaptive polling that adjusts based on status."""
start_time = time.time()
delays = {
"Pending": 2.0, # Queue/processing
None: 1.5 # Unknown/default
}
while time.time() - start_time < timeout:
response = requests.get(polling_url, headers=headers)
data = response.json()
status = data.get("status")
if status == "Ready":
return data["result"]
elif status == "Error":
raise Exception(data.get("error", "Generation Error"))
delay = delays.get(status, delays[None])
time.sleep(delay)
raise TimeoutError("Polling timeout exceeded")
```
## Complete Example: Submit and Poll
```python
import time
import requests
class BFLClient:
def __init__(self, api_key, base_url="https://api.bfl.ai"):
self.base_url = base_url
self.headers = {
"x-key": api_key,
"Content-Type": "application/json"
}
def generate(self, model, prompt, **kwargs):
"""Submit generation request and poll for result."""
# Submit request
endpoint = f"{self.base_url}/v1/{model}"
payload = {"prompt": prompt, **kwargs}
response = requests.post(endpoint, headers=self.headers, json=payload)
response.raise_for_status()
polling_url = response.json()["polling_url"]
# Poll for result
return self._poll(polling_url)
def _poll(self, polling_url, timeout=120):
"""Poll until completion or timeout."""
start = time.time()
delay = 1.0
while time.time() - start < timeout:
response = requests.get(polling_url, headers=self.headers)
data = response.json()
if data["status"] == "Ready":
return data["result"]
elif data["status"] == "Error":
raise Exception(data.get("error"))
time.sleep(delay)
delay = min(delay * 1.5, 5.0) # Gradual backoff
raise TimeoutError("Generation timed out")
# Usage
client = BFLClient("your-api-key")
result = client.generate(
model="flux-2-pro",
prompt="A beautiful sunset over mountains"
)
print(f"Image URL: {result['sample']}")
```
## URL Expiration
**Critical:** Result URLs expire after 10 minutes. Always download immediately.
```python
def download_result(result_url, output_path):
"""Download result image before URL expires."""
response = requests.get(result_url)
response.raise_for_status()
with open(output_path, 'wb') as f:
f.write(response.content)
return output_path
```
## Batch Processing with Polling
```python
import asyncio
import aiohttp
async def generate_batch(client, prompts, model="flux-2-pro"):
"""Generate multiple images concurrently."""
async with aiohttp.ClientSession() as session:
# Submit all requests
tasks = []
for prompt in prompts:
task = submit_and_poll(session, client, model, prompt)
tasks.append(task)
# Wait for all to complete
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def submit_and_poll(session, client, model, prompt):
"""Async submit and poll for single image."""
# Submit
async with session.post(
f"{client.base_url}/v1/{model}",
headers=client.headers,
json={"prompt": prompt}
) as response:
data = await response.json()
polling_url = data["polling_url"]
# Poll
while True:
async with session.get(polling_url, headers=client.headers) as response:
data = await response.json()
if data["status"] == "Ready":
return data["result"]
elif data["status"] == "Error":
raise Exception(data.get("error"))
await asyncio.sleep(2)
```
## Best Practices
1. **Always implement timeouts** - Never poll indefinitely
2. **Use exponential backoff** - Reduces server load, handles congestion
3. **Add jitter** - Prevents thundering herd when polling multiple requests
4. **Handle all status values** - Including unexpected ones
5. **Download immediately** - URLs expire in 10 minutes
6. **Log polling attempts** - Useful for debugging and monitoring
7. **Respect rate limits** - Implement proper backoff on 429 responses
@@ -0,0 +1,246 @@
---
name: rate-limiting
description: Understanding and handling BFL API rate limits
---
# Rate Limiting
BFL API enforces rate limits to ensure fair usage and system stability.
## Current Limits
| Endpoint Category | Concurrent Requests |
| ---------------------- | ------------------- |
| Standard (most models) | 24 |
**Concurrent requests** means in-flight requests (submitted but not yet completed).
## Rate Limit Headers
Check response headers for rate limit status:
```
X-RateLimit-Limit: 24
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1640000000
```
## HTTP 429 Response
When rate limited, you receive HTTP 429:
```json
{
"error": "rate_limit_exceeded",
"message": "Too many concurrent requests",
"retry_after": 5
}
```
## Handling Strategies
### 1. Client-Side Tracking
Track active requests to stay under limits:
```python
from threading import Lock, Semaphore
class RateLimitedClient:
def __init__(self, api_key, max_concurrent=24):
self.api_key = api_key
self.semaphore = Semaphore(max_concurrent)
def generate(self, model, prompt, **kwargs):
with self.semaphore: # Blocks if at limit
return self._make_request(model, prompt, **kwargs)
def _make_request(self, model, prompt, **kwargs):
# Submit request
response = requests.post(...)
polling_url = response.json()["polling_url"]
# Poll until complete (request still "active")
return self._poll(polling_url)
```
### 2. Retry with Exponential Backoff
```python
import time
def request_with_retry(endpoint, payload, headers, max_retries=5):
"""Make request with automatic retry on rate limit."""
for attempt in range(max_retries):
response = requests.post(endpoint, json=payload, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
wait_time = retry_after * (2 ** attempt) # Exponential backoff
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
continue
response.raise_for_status()
return response
raise Exception("Max retries exceeded due to rate limiting")
```
### 3. Queue-Based Architecture
For high-volume applications:
```python
from queue import Queue
from threading import Thread
import time
class RequestQueue:
def __init__(self, api_key, max_concurrent=24):
self.api_key = api_key
self.queue = Queue()
self.active = 0
self.max_concurrent = max_concurrent
self.lock = Lock()
# Start worker threads
for _ in range(max_concurrent):
worker = Thread(target=self._worker, daemon=True)
worker.start()
def submit(self, model, prompt, callback):
"""Submit request to queue."""
self.queue.put({
'model': model,
'prompt': prompt,
'callback': callback
})
def _worker(self):
"""Process queue items."""
while True:
item = self.queue.get()
try:
result = self._process(item)
item['callback'](result, None)
except Exception as e:
item['callback'](None, e)
finally:
self.queue.task_done()
def _process(self, item):
# Make request and poll
...
# Usage
queue = RequestQueue("your-api-key")
def handle_result(result, error):
if error:
print(f"Error: {error}")
else:
print(f"Generated: {result['sample']}")
queue.submit("flux-2-pro", "A sunset", handle_result)
```
### 4. Async with Semaphore
```python
import asyncio
import aiohttp
class AsyncRateLimitedClient:
def __init__(self, api_key, max_concurrent=24):
self.api_key = api_key
self.semaphore = asyncio.Semaphore(max_concurrent)
self.headers = {"x-key": api_key}
async def generate(self, model, prompt):
async with self.semaphore:
async with aiohttp.ClientSession() as session:
# Submit
async with session.post(
f"https://api.bfl.ai/v1/{model}",
headers=self.headers,
json={"prompt": prompt}
) as response:
data = await response.json()
polling_url = data["polling_url"]
# Poll until complete
while True:
async with session.get(
polling_url,
headers=self.headers
) as response:
data = await response.json()
if data["status"] == "Ready":
return data["result"]
elif data["status"] == "Error":
raise Exception(data.get("error"))
await asyncio.sleep(2)
# Usage
async def main():
client = AsyncRateLimitedClient("your-api-key")
# Generate 50 images with rate limiting
prompts = [f"Image {i}" for i in range(50)]
tasks = [client.generate("flux-2-pro", p) for p in prompts]
results = await asyncio.gather(*tasks)
asyncio.run(main())
```
## Monitoring Rate Limits
```python
class RateLimitMonitor:
def __init__(self):
self.requests_made = 0
self.rate_limit_hits = 0
self.lock = Lock()
def record_request(self, response):
with self.lock:
self.requests_made += 1
if response.status_code == 429:
self.rate_limit_hits += 1
def get_stats(self):
return {
"total_requests": self.requests_made,
"rate_limit_hits": self.rate_limit_hits,
"hit_rate": self.rate_limit_hits / max(self.requests_made, 1)
}
```
## Best Practices
1. **Track active requests** - Know how many are in-flight
2. **Implement client-side limits** - Stay under limits proactively
3. **Use semaphores** - Clean way to limit concurrency
4. **Queue for high volume** - Buffer requests when traffic spikes
5. **Monitor headers** - React to remaining quota
6. **Graceful degradation** - Queue or delay when near limits
7. **Different limits per endpoint** - Remember Kontext Max is 6, not 24
## Regional Distribution
For very high volume, consider distributing across regions:
```python
ENDPOINTS = [
"https://api.bfl.ai",
"https://api.eu.bfl.ai",
"https://api.us.bfl.ai"
]
def get_endpoint():
"""Round-robin or least-loaded selection."""
return random.choice(ENDPOINTS)
```
Note: Verify regional rate limits are independent before relying on this strategy.
@@ -0,0 +1,339 @@
---
name: webhook-integration
description: Setting up webhooks for production BFL API integration
---
# Webhook Integration
For production workloads, use webhooks instead of polling to receive generation results.
## Benefits Over Polling
- **Reduced API calls** - No repeated polling requests
- **Immediate notification** - Know exactly when generation completes
- **Better resource efficiency** - No wasted compute on polling
- **Scalable architecture** - Event-driven design
## Setup
### Request with Webhook
Include `webhook_url` and optionally `webhook_secret` in your request:
```bash
curl -X POST "https://api.bfl.ai/v1/flux-2-pro" \
-H "x-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A beautiful sunset over mountains",
"webhook_url": "https://your-server.com/api/bfl-webhook",
"webhook_secret": "your-secret-key-here"
}'
```
### Webhook Payload
When generation completes, BFL sends a POST request to your webhook URL:
```json
{
"id": "gen_abc123xyz",
"status": "Ready",
"result": {
"sample": "https://bfldeliveryprod.blob.core.windows.net/results/...",
"prompt": "...",
"seed": 1234567890
},
"timestamp": "2025-01-15T10:30:00Z"
}
```
For failures:
```json
{
"id": "gen_abc123xyz",
"status": "Error",
"error": "content_policy_violation",
"message": "The prompt violated content policy",
"timestamp": "2025-01-15T10:30:00Z"
}
```
## Security
### Signature Verification
When `webhook_secret` is provided, BFL signs the payload with HMAC-SHA256:
```
X-BFL-Signature: sha256=<hex-encoded-signature>
```
### Verification Implementation
```python
import hmac
import hashlib
def verify_webhook_signature(payload, signature, secret):
"""Verify the webhook came from BFL."""
if not signature or not signature.startswith('sha256='):
return False
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
provided_signature = signature[7:] # Remove 'sha256=' prefix
return hmac.compare_digest(expected_signature, provided_signature)
```
### Flask Handler with Verification
```python
from flask import Flask, request, jsonify
import hmac
import hashlib
import requests
app = Flask(__name__)
WEBHOOK_SECRET = "your-secret-key-here"
@app.route('/api/bfl-webhook', methods=['POST'])
def handle_webhook():
# Verify signature
signature = request.headers.get('X-BFL-Signature')
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
data = request.json
if data['status'] == 'Ready':
handle_completion(data)
elif data['status'] == 'Error':
handle_failure(data)
return jsonify({'status': 'received'}), 200
def handle_completion(data):
generation_id = data['id']
result_url = data['result']['sample']
# Download image immediately (URL expires in 10 min)
image_data = requests.get(result_url).content
# Store to your storage
store_image(generation_id, image_data)
# Update your database
update_generation_status(generation_id, 'completed')
# Notify your application/users
notify_completion(generation_id)
def handle_failure(data):
generation_id = data['id']
error = data.get('error', 'unknown')
# Log the failure
log_generation_failure(generation_id, error)
# Update your database
update_generation_status(generation_id, 'failed', error)
# Maybe retry or notify
handle_generation_error(generation_id, error)
```
### Express.js Handler
```javascript
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = 'your-secret-key-here';
function verifySignature(payload, signature, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const providedSignature = signature.slice(7);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
app.post('/api/bfl-webhook', async (req, res) => {
const signature = req.headers['x-bfl-signature'];
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(req.body);
if (data.status === 'Ready') {
// Download image (URL expires in 10 min)
const imageResponse = await axios.get(data.result.sample, {
responseType: 'arraybuffer'
});
// Store the image
await storeImage(data.id, imageResponse.data);
}
res.json({ status: 'received' });
});
```
## Requirements
### HTTPS Required
Webhook URLs **must use HTTPS** in production. BFL will not send webhooks to HTTP endpoints.
### Response Requirements
- Respond with 2xx status code to acknowledge receipt
- Respond within 30 seconds
- Keep handler fast - offload heavy processing
### Retry Policy
BFL retries failed webhook deliveries:
| Attempt | Delay |
|---------|-------|
| 1st retry | 1 second |
| 2nd retry | 5 seconds |
| 3rd retry | 30 seconds |
After 3 failed attempts, the webhook is abandoned. Fall back to polling if critical.
## Idempotency
Handle duplicate webhook deliveries:
```python
from functools import lru_cache
import redis
redis_client = redis.Redis()
def is_duplicate_webhook(generation_id):
"""Check if we've already processed this webhook."""
key = f"webhook:processed:{generation_id}"
# Try to set with NX (only if not exists)
was_set = redis_client.set(key, "1", nx=True, ex=3600) # 1 hour TTL
return not was_set # If we couldn't set it, it's a duplicate
@app.route('/api/bfl-webhook', methods=['POST'])
def handle_webhook():
# ... signature verification ...
data = request.json
generation_id = data['id']
if is_duplicate_webhook(generation_id):
return jsonify({'status': 'already_processed'}), 200
# Process webhook...
```
## Hybrid Approach
Combine webhooks with polling fallback:
```python
class HybridClient:
def __init__(self, api_key, webhook_url, webhook_secret):
self.api_key = api_key
self.webhook_url = webhook_url
self.webhook_secret = webhook_secret
self.pending = {} # Track pending generations
def generate(self, prompt, timeout=300):
"""Generate with webhook, fall back to polling."""
response = self._submit(prompt)
generation_id = response['id']
polling_url = response['polling_url']
# Wait for webhook (with timeout)
result = self._wait_for_webhook(generation_id, timeout=timeout)
if result is None:
# Webhook didn't arrive, fall back to polling
result = self._poll(polling_url, timeout=60)
return result
def _submit(self, prompt):
return requests.post(
"https://api.bfl.ai/v1/flux-2-pro",
headers={"x-key": self.api_key},
json={
"prompt": prompt,
"webhook_url": self.webhook_url,
"webhook_secret": self.webhook_secret
}
).json()
def receive_webhook(self, data):
"""Called by webhook handler."""
generation_id = data['id']
if generation_id in self.pending:
self.pending[generation_id].set_result(data)
```
## Monitoring
Track webhook health:
```python
import time
class WebhookMetrics:
def __init__(self):
self.received = 0
self.processed = 0
self.failed = 0
self.avg_latency = 0
def record_webhook(self, generation_id, submit_time):
self.received += 1
latency = time.time() - submit_time
self.avg_latency = (self.avg_latency * (self.received - 1) + latency) / self.received
def record_success(self):
self.processed += 1
def record_failure(self):
self.failed += 1
def get_stats(self):
return {
"received": self.received,
"processed": self.processed,
"failed": self.failed,
"success_rate": self.processed / max(self.received, 1),
"avg_latency_seconds": self.avg_latency
}
```
+100
View File
@@ -0,0 +1,100 @@
---
name: create-video
description: |
Create videos from a text prompt using HeyGen's Video Agent. Use when: (1) Creating a video from a description or idea, (2) Generating explainer, demo, or marketing videos from a prompt, (3) Making a video without specifying exact avatars, voices, or scenes, (4) Quick video prototyping or drafts, (5) One-shot prompt-to-video generation, (6) User says "make me a video" or "create a video about X".
homepage: https://docs.heygen.com/reference/generate-video-agent
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# Create Video
Generate complete videos from a text prompt. Describe what you want and the AI handles script writing, avatar selection, visuals, voiceover, pacing, and captions automatically.
## Authentication
All requests require the `X-Api-Key` header. Set the `HEYGEN_API_KEY` environment variable.
```bash
curl -X POST "https://api.heygen.com/v1/video_agent/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Create a 60-second product demo video."}'
```
## Tool Selection
If HeyGen MCP tools are available (`mcp__heygen__*`), **prefer them** over direct HTTP API calls — they handle authentication and request formatting automatically.
| Task | MCP Tool | Fallback (Direct API) |
|------|----------|----------------------|
| Generate video from prompt | `mcp__heygen__generate_video_agent` | `POST /v1/video_agent/generate` |
| Check video status / get URL | `mcp__heygen__get_video` | `GET /v2/videos/{video_id}` |
| List account videos | `mcp__heygen__list_videos` | `GET /v2/videos` |
| Delete a video | `mcp__heygen__delete_video` | `DELETE /v2/videos/{video_id}` |
If no HeyGen MCP tools are available, use direct HTTP API calls as documented in the reference files.
## Default Workflow
Always use [prompt-optimizer.md](references/prompt-optimizer.md) guidelines to structure prompts with scenes, timing, and visual styles.
**With MCP tools:**
1. Write an optimized prompt using [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md)
2. Call `mcp__heygen__generate_video_agent` with prompt and config (duration_sec, orientation, avatar_id)
3. Call `mcp__heygen__get_video` with the returned video_id to poll status and get the download URL
**Without MCP tools (direct API):**
1. Write an optimized prompt using [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md)
2. `POST /v1/video_agent/generate` — see [video-agent.md](references/video-agent.md)
3. `GET /v2/videos/<id>` — see [video-status.md](references/video-status.md)
## Quick Reference
| Task | MCP Tool | Read |
|------|----------|------|
| Generate video from prompt | `mcp__heygen__generate_video_agent` | [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md) → [video-agent.md](references/video-agent.md) |
| Check video status / get download URL | `mcp__heygen__get_video` | [video-status.md](references/video-status.md) |
| Upload reference files for prompt | — | [assets.md](references/assets.md) |
## When to Use This Skill vs Avatar Video
This skill is for **prompt-based video creation** — describe what you want, and the AI handles the rest.
If the user needs **precise control** over specific avatars, exact scripts, per-scene voice/background configuration, or multi-scene composition, use the **avatar-video** skill instead.
| User Says | This Skill | Avatar Video Skill |
|-----------|:----------:|:------------------:|
| "Make me a video about X" | ✓ | |
| "Create a product demo" | ✓ | |
| "I want avatar Y to say exactly Z" | | ✓ |
| "Multi-scene video with different backgrounds" | | ✓ |
| "Transparent WebM for compositing" | | ✓ |
## Reference Files
### Core Workflow
- [references/prompt-optimizer.md](references/prompt-optimizer.md) - Writing effective prompts (core workflow + rules)
- [references/visual-styles.md](references/visual-styles.md) - 20 named visual styles with full specs
- [references/prompt-examples.md](references/prompt-examples.md) - Full production prompt example + ready-to-use templates
- [references/video-agent.md](references/video-agent.md) - Video Agent API endpoint details
### Foundation
- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs
- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events
- [references/assets.md](references/assets.md) - Uploading images, videos, audio as references
- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios
- [references/quota.md](references/quota.md) - Credit system and usage limits
## Best Practices
1. **Optimize your prompt** — The difference between mediocre and professional results depends entirely on prompt quality. Always use the prompt optimizer
2. **Specify duration** — Use `config.duration_sec` for predictable length
3. **Lock avatar if needed** — Use `config.avatar_id` for consistency across videos
4. **Upload reference files** — Help the agent understand your brand/product
5. **Iterate on prompts** — Refine based on results; Video Agent is great for quick iterations
@@ -0,0 +1,339 @@
---
name: assets
description: Uploading images, videos, and audio for use in HeyGen video generation
---
# Asset Upload and Management
HeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.
## Upload Flow
Asset uploads are a single-step process: POST the raw file binary directly to the upload endpoint. The Content-Type header must match the file's MIME type.
## Uploading an Asset
**Endpoint:** `POST https://upload.heygen.com/v1/asset`
### Request
| Header | Required | Description |
|--------|:--------:|-------------|
| `X-Api-Key` | ✓ | Your HeyGen API key |
| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |
The request body is the raw binary file data. No JSON or form fields are needed.
### Response
| Field | Type | Description |
|-------|------|-------------|
| `code` | number | Status code (`100` = success) |
| `data.id` | string | Unique asset ID for use in video generation |
| `data.name` | string | Asset name |
| `data.file_type` | string | `image`, `video`, or `audio` |
| `data.url` | string | Accessible URL for the uploaded file |
| `data.image_key` | string \| null | Key for creating uploaded photo avatars (images only) |
| `data.folder_id` | string | Folder ID (empty if not in a folder) |
| `data.meta` | string \| null | Asset metadata |
| `data.created_ts` | number | Unix timestamp of creation |
### curl
```bash
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./background.jpg'
```
### TypeScript
```typescript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
name: string;
file_type: string;
url: string;
image_key: string | null;
folder_id: string;
meta: string | null;
created_ts: number;
};
msg: string | null;
message: string | null;
}
async function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: fileBuffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
// Usage
const asset = await uploadAsset("./background.jpg", "image/jpeg");
console.log(`Uploaded asset: ${asset.id}`);
console.log(`Asset URL: ${asset.url}`);
```
### TypeScript (with streams for large files)
```typescript
import fs from "fs";
import path from "path";
import { stat } from "fs/promises";
async function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileStats = await stat(resolvedPath);
const fileStream = fs.createReadStream(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
"Content-Length": fileStats.size.toString(),
},
body: fileStream as any,
// @ts-ignore - duplex is needed for streaming
duplex: "half",
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
### Python
```python
import requests
import os
def upload_asset(file_path: str, content_type: str) -> dict:
with open(file_path, "rb") as f:
response = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": content_type
},
data=f
)
data = response.json()
if data.get("code") != 100:
raise Exception(data.get("message", "Upload failed"))
return data["data"]
# Usage
asset = upload_asset("./background.jpg", "image/jpeg")
print(f"Uploaded asset: {asset['id']}")
print(f"Asset URL: {asset['url']}")
```
## Supported Content Types
| Type | Content-Type | Use Case |
|------|--------------|----------|
| JPEG | `image/jpeg` | Backgrounds, talking photos |
| PNG | `image/png` | Backgrounds, overlays |
| MP4 | `video/mp4` | Video backgrounds |
| WebM | `video/webm` | Video backgrounds |
| MP3 | `audio/mpeg` | Custom audio input |
| WAV | `audio/wav` | Custom audio input |
## Uploading from URL
If your asset is already hosted online:
```typescript
async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse["data"]> {
// 1. Validate and download the file
const url = new URL(sourceUrl);
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs are supported");
}
const sourceResponse = await fetch(sourceUrl);
const buffer = Buffer.from(await sourceResponse.arrayBuffer());
// 2. Upload directly to HeyGen
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: buffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
## Using Uploaded Assets
### As Background Image
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello, this is a video with a custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: asset.url, // Use the URL from the upload response
},
},
],
};
```
### As Talking Photo Source
```typescript
const talkingPhotoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: asset.id, // Use the ID from the upload response
},
voice: {
type: "text",
input_text: "Hello from my talking photo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### As Audio Input
```typescript
const audioConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: asset.url, // Use the URL from the upload response
},
},
],
};
```
## Complete Upload Workflow
```typescript
async function createVideoWithCustomBackground(
backgroundPath: string,
script: string
): Promise<string> {
// 1. Upload background
console.log("Uploading background...");
const background = await uploadAsset(backgroundPath, "image/jpeg");
// 2. Create video config
const config = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: background.url,
},
},
],
dimension: { width: 1920, height: 1080 },
};
// 3. Generate video
console.log("Generating video...");
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const { data } = await response.json();
return data.video_id;
}
```
## Asset Limitations
- **File size**: 10MB maximum
- **Image dimensions**: Recommended to match video dimensions
- **Audio duration**: Should match expected video length
- **Retention**: Assets may be deleted after a period of inactivity
## Best Practices
1. **Optimize images** - Resize to match video dimensions before uploading
2. **Use appropriate formats** - JPEG for photos, PNG for graphics with transparency
3. **Validate before upload** - Check file type and size locally first
4. **Handle upload errors** - Implement retry logic for failed uploads
5. **Cache asset IDs** - Reuse assets across multiple video generations
@@ -0,0 +1,314 @@
---
name: dimensions
description: Resolution options (720p/1080p) and aspect ratios for HeyGen videos
---
# Video Dimensions and Resolution
HeyGen supports various video dimensions and aspect ratios to fit different platforms and use cases.
## Standard Resolutions
### Landscape (16:9)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 1280 | 720 | Standard quality, faster processing |
| 1080p | 1920 | 1080 | High quality, most common |
### Portrait (9:16)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 1280 | Mobile-first content |
| 1080p | 1080 | 1920 | High quality vertical |
### Square (1:1)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 720 | Social media posts |
| 1080p | 1080 | 1080 | High quality square |
## Setting Dimensions
### TypeScript
```typescript
// Landscape 1080p
const landscapeConfig = {
video_inputs: [...],
dimension: {
width: 1920,
height: 1080
}
};
// Portrait 1080p
const portraitConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1920
}
};
// Square 1080p
const squareConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1080
}
};
```
### curl
```bash
# Landscape 1080p
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [...],
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
## Dimension Helper Functions
```typescript
type AspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "4:5";
type Quality = "720p" | "1080p";
interface Dimensions {
width: number;
height: number;
}
function getDimensions(aspectRatio: AspectRatio, quality: Quality): Dimensions {
const configs: Record<AspectRatio, Record<Quality, Dimensions>> = {
"16:9": {
"720p": { width: 1280, height: 720 },
"1080p": { width: 1920, height: 1080 },
},
"9:16": {
"720p": { width: 720, height: 1280 },
"1080p": { width: 1080, height: 1920 },
},
"1:1": {
"720p": { width: 720, height: 720 },
"1080p": { width: 1080, height: 1080 },
},
"4:3": {
"720p": { width: 960, height: 720 },
"1080p": { width: 1440, height: 1080 },
},
"4:5": {
"720p": { width: 576, height: 720 },
"1080p": { width: 864, height: 1080 },
},
};
return configs[aspectRatio][quality];
}
// Usage
const youTubeDimensions = getDimensions("16:9", "1080p");
const tikTokDimensions = getDimensions("9:16", "1080p");
const instagramDimensions = getDimensions("1:1", "1080p");
```
## Platform-Specific Recommendations
### YouTube
```typescript
const youtubeConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape
};
```
### TikTok / Instagram Reels / YouTube Shorts
```typescript
const shortFormConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1920 }, // 9:16 portrait
};
```
### Instagram Feed Post
```typescript
const instagramFeedConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1080 }, // 1:1 square
};
```
### LinkedIn
```typescript
const linkedinConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred
};
```
### Twitter/X
```typescript
const twitterConfig = {
video_inputs: [...],
dimension: { width: 1280, height: 720 }, // 16:9, 720p is common
};
```
## Avatar IV Dimensions
For Avatar IV (photo-based avatars), dimensions are set via orientation:
```typescript
type VideoOrientation = "portrait" | "landscape" | "square";
function getAvatarIVDimensions(orientation: VideoOrientation): Dimensions {
switch (orientation) {
case "portrait":
return { width: 720, height: 1280 };
case "landscape":
return { width: 1280, height: 720 };
case "square":
return { width: 720, height: 720 };
}
}
```
## Custom Dimensions
HeyGen supports custom dimensions within limits:
```typescript
const customConfig = {
video_inputs: [...],
dimension: {
width: 1600,
height: 900 // Custom 16:9 at non-standard resolution
}
};
```
### Dimension Constraints
- **Minimum**: 128px on any side
- **Maximum**: 4096px on any side
- **Must be even numbers**: Both width and height must be divisible by 2
```typescript
function validateDimensions(width: number, height: number): boolean {
if (width < 128 || height < 128) {
throw new Error("Dimensions must be at least 128px");
}
if (width > 4096 || height > 4096) {
throw new Error("Dimensions cannot exceed 4096px");
}
if (width % 2 !== 0 || height % 2 !== 0) {
throw new Error("Dimensions must be even numbers");
}
return true;
}
```
## Resolution vs. Credit Cost
Higher resolutions may consume more credits:
| Resolution | Relative Cost |
|------------|---------------|
| 720p | Base rate |
| 1080p | ~1.5x base rate |
Consider using 720p for drafts and testing, then 1080p for final output.
## Background Considerations
Match background image/video dimensions to your video dimensions:
```typescript
// For 1080p landscape video
const config = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: "https://example.com/1920x1080-background.jpg" // Match video dimensions
}
}
],
dimension: { width: 1920, height: 1080 }
};
```
## Creating a Video Config Factory
```typescript
interface VideoConfigOptions {
script: string;
avatarId: string;
voiceId: string;
platform: "youtube" | "tiktok" | "instagram_feed" | "instagram_story" | "linkedin";
quality?: "720p" | "1080p";
}
function createVideoConfig(options: VideoConfigOptions) {
const platformDimensions: Record<string, Dimensions> = {
youtube: { width: 1920, height: 1080 },
tiktok: { width: 1080, height: 1920 },
instagram_feed: { width: 1080, height: 1080 },
instagram_story: { width: 1080, height: 1920 },
linkedin: { width: 1920, height: 1080 },
};
const dimension = platformDimensions[options.platform];
// Scale down for 720p if requested
if (options.quality === "720p") {
dimension.width = Math.round((dimension.width * 720) / 1080);
dimension.height = Math.round((dimension.height * 720) / 1080);
}
return {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: options.avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: options.script,
voice_id: options.voiceId,
},
},
],
dimension,
};
}
// Usage
const tiktokVideo = createVideoConfig({
script: "Hey everyone! Check this out!",
avatarId: "josh_lite3_20230714",
voiceId: "1bd001e7e50f421d891986aad5158bc8",
platform: "tiktok",
quality: "1080p",
});
```
@@ -0,0 +1,206 @@
---
name: prompt-examples
description: Full production prompt examples and ready-to-use templates for Video Agent
---
# Video Agent Prompt Examples
## Full Example: Brief to Production Prompt
### Input Brief
```
Topic: Monthly company report for a SaaS startup
Key data: $141M ARR (up from $54M), 1.85M signups (+28%), 3M paid videos/month
Customer story: Creator built AI character, 2.5M followers, 20 min/video
Challenge: Organic traffic volatile, -16% last week
Duration: ~90 seconds
Tone: Confident CEO, data-backed
```
### Output Prompt
```
FORMAT: Bloomberg-style company report. 90 seconds. Fast-paced, data-dense.
Record-breaking month. Proud but analytical.
TONE: Confident, direct, data-backed. Highlights hit hard with numbers.
Customer stories are the emotional core. Challenges are honest — no spin.
AVATAR: Man in simple black crew-neck tee, standing in a modern glass-walled
office at golden hour. Behind him, a wall-mounted display shows the company logo
in soft blue glow. Monitor to his right shows a dashboard with upward-trending
charts. Desk beside him: laptop, half-empty flat white, scattered sticky notes.
Warm afternoon light through floor-to-ceiling windows, long shadows on polished
concrete. Minimal, focused startup HQ.
STYLE — SWISS PULSE (Müller-Brockmann): Grid-locked compositions. Black (#1a1a1a),
white, electric blue (#0066FF), warm amber (#FF9500) for records. Helvetica Bold
headlines, Regular labels. Numbers LARGE. Animated counters count up from 0.
Diagonal compositions on accent moments. Grid wipe transitions. No dissolves.
CRITICAL ON-SCREEN TEXT (display literally):
- "1.85M SIGNUPS — +28% MoM"
- "$2.12M NEW SUBSCRIPTION REVENUE"
- "$54M → $141M ARR"
- "2.5M FOLLOWERS" and "20 MIN / VIDEO"
- Quote: "Use technology to serve the message, not distract from it."
- "ORGANIC: 65% OF SUBS — VOLATILE"
MUSIC: Upbeat electronic with a driving beat. Tycho meets Bloomberg opening theme.
Builds through highlights, warms for customer story, softens for challenges, peaks
on close.
---
SCENE 1 — A-ROLL (8s)
[Avatar center-frame, energetic, leaning slightly forward]
VOICEOVER: "January was a record month. New highs across acquisition, revenue,
and product velocity. Here's the full picture."
Lower-third SLIDES in: "COMPANY NAME | JANUARY 2026" white on blue bar.
Grid wipe.
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "One-point-eight-five million signups — twenty-eight percent month
over month. Two-point-one-two million in new subscription revenue. Both all-time
highs."
LAYER 1: Dark #1a1a1a background with thin grid lines pulsing at 8% opacity.
LAYER 2: "1.85M" SLAMS in from left, white Bold 140pt. "SIGNUPS" types on
in electric blue 32pt uppercase. "+28% MoM" appears in amber.
LAYER 3: Three stat cards CASCADE from top-right, staggered 0.3s:
"$2.12M New Revenue" — "$3.4M Business ARR" — "$3M Pro ARR."
Each number COUNTS UP from 0.
LAYER 4: Bottom ticker scrolls: "Non-brand search +36% • Brand impressions 9.2M
• Weekly subs +20.5%"
LAYER 5: Grid lines RIPPLE outward on "1.85M" slam. Diagonal amber bar behind
stat cards.
Hard cut.
SCENE 3 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "Zoom out. Twelve months ago — fifty-four million ARR. Today —
one hundred forty-one million. Nearly three X in a single year."
LAYER 1: Dark background, subtle grid scrolling upward.
LAYER 2: Animated line chart DRAWS ITSELF left to right. Y-axis: $50M to $150M.
Final point "$140.84M" glows amber and pulses.
LAYER 3: Milestone annotations float in at key data points.
LAYER 4: Second smaller chart below — "Paid Videos" 0.91M to 2.97M, same style.
LAYER 5: Thin grid lines converge toward final data point. Scan line sweeps.
Grid wipe.
SCENE 4 — A-ROLL (8s)
[Avatar center-frame, warm tone, genuine smile]
VOICEOVER: "But the numbers only tell half the story. The other half is the
people building on the platform."
Lower-third: "Customer Spotlight"
SCENE 5 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — warm palette]
VOICEOVER: "An AI character built entirely on the platform. Twenty minutes
per video. Two-point-five million Instagram followers. The creator's principle:
use technology to serve the message, not distract from it."
LAYER 1: Dark background with warm amber grid lines at low opacity.
LAYER 2: "CHARACTER NAME" in large white, center-top, 80pt.
LAYER 3: Stats cascade from right: "2.5M Followers" COUNTS UP in amber —
"20 min/video" — "7x Faster." Each a glowing node.
LAYER 4: Quote card SLIDES UP: "Use technology to serve the message, not
distract from it." Types on word by word.
LAYER 5: Warm light bloom. Grid lines soften into curved arcs.
Grid wipe.
SCENE 6 — A-ROLL (10s)
[Avatar center-frame, serious/candid]
VOICEOVER: "Now the honest part. Organic drives sixty-five percent of
subscriptions and it's volatile. Non-brand traffic dropped sixteen percent
last week. We've rebuilt attribution and we're investing in SEO."
Lower-third: "Challenges"
SCENE 7 — A-ROLL (7s)
[Avatar center-frame, energy lifts, direct eye contact]
VOICEOVER: "Fifty-four million to one-forty-one in twelve months. Three million
paid videos a month. January set the bar — now we raise it."
End card: Logo centered, blue glow fade-in. Grid lines converge. Music peaks.
---
NARRATION STYLE: CEO energy — conviction backed by data. Fast on highlights.
Warm on customer stories. Candid on challenges. Close with forward momentum.
```
## Ready-to-Use Templates
### Tech News Briefing
```
FORMAT: 75-second high-energy tech briefing. Think: Bloomberg meets Vice.
AVATAR: [Presenter in tech-casual at a multi-monitor station.
Describe clothing, monitor content, desk items, lighting.]
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures. Smash cut transitions.
CRITICAL ON-SCREEN TEXT:
- [List every stat, quote, handle that must appear]
SCENE 1 — A-ROLL (8s): Hook with energy. State what's happening.
SCENE 2 — B-ROLL (12s): First story with layered visuals (L1-L5).
SCENE 3 — A-ROLL + OVERLAY (10s): Second story, split frame.
SCENE 4 — B-ROLL (10s): Third story or dramatic data point.
SCENE 5 — A-ROLL (8s): Wrap-up and forward look.
```
### Product Comparison
```
FORMAT: 60-second comparison. [Product A] vs [Product B]. Data-driven.
AVATAR: [Presenter in review studio. Desk with both products visible.]
STYLE — DIGITAL GRID (Crouwel): Dark #0a0a0a, cyan #00D4FF and amber #FFB800.
Two-color coding: cyan = Product A, amber = Product B. Monospaced type.
CRITICAL ON-SCREEN TEXT:
- [Key stats for each product]
- [Pricing, features, differentiators]
Use SPLIT FRAME B-roll: Product A left, Product B right.
```
### Strategy Presentation
```
FORMAT: 90-second strategy briefing. Bloomberg meets board meeting.
AVATAR: [Executive in blazer over tee. Conference room with whiteboard frameworks.]
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + blue #0066FF.
Grid-locked. Helvetica. Animated counters. Grid wipe transitions.
CRITICAL ON-SCREEN TEXT:
- [Framework labels, quadrant labels, key quotes]
Build frameworks visually: draw axes, plot positions, animate labels.
```
### Social Ad (30 seconds)
```
FORMAT: 30-second social ad. Maximum energy. Portrait 9:16.
AVATAR: [Creator-style presenter. Ring light, colorful background.]
STYLE — CARNIVAL SURGE (Lins): Hot pink, yellow, teal. Collage layering.
Text MASSIVE at angles. Confetti. Smash cuts.
Three scenes: Hook (8s) → Value prop (12s) → CTA (10s).
Text fills 50-80% of every frame. Numbers SLAM.
```
### Premium Report
```
FORMAT: 120-second investor-grade report. Understated authority.
AVATAR: [Tailored merino sweater. Architectural room, diffused natural light.]
STYLE — VELVET STANDARD (Vignelli): Black, white, gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow cross-dissolves. Numbers fade in with weight.
```
@@ -0,0 +1,289 @@
---
name: prompt-optimizer
description: Write production-quality prompts for HeyGen Video Agent — from basic ideas to fully art-directed scene-by-scene scripts
---
# Video Agent Prompt Optimizer
Write effective prompts for the HeyGen Video Agent API. Based on patterns from 40+ produced videos.
**The core insight: Video Agent is an HTML interpreter.** It renders layouts, typography, and structured content natively. Describe B-roll as layered text motion graphics with action verbs ("slams in," "types on," "counts up") — not layout specs ("upper-left, 48pt").
## Reference Files
| File | Load when... |
|------|-------------|
| [visual-styles.md](visual-styles.md) | Choosing a visual style (20 styles with full specs) |
| [prompt-examples.md](prompt-examples.md) | Writing a prompt from scratch (full production example + templates) |
## Workflow: Brief to Prompt
1. **Pull data** — Research the topic: web search, APIs, internal docs. Gather real quotes, stats, handles
2. **Synthesize a thesis** — Not a list. A story. *"X is happening because Y — here's the proof."* Group into 3-5 themes with a narrative arc
3. **Choose a style** — Match mood first, content second. Ask: *"What should the viewer FEEL?"* See [visual-styles.md](visual-styles.md)
4. **Write the avatar** — Thematic wardrobe matching content's emotional context. Brand logos and content-specific props in the set (see Avatar Guide below)
5. **Extract critical text** — List every number, quote, handle, and label that must appear literally
6. **Break into scenes** — One concept per scene. Rotate scene types. Never 3+ of same type in a row. At least 2 pure B-roll scenes
7. **Write voiceover** — Spell out numbers in VO ("one-point-eight-five million"), use figures on screen ("1.85M"). Narration on EVERY scene including B-roll
8. **Layer each B-roll scene** — L1 background, L2 hero, L3 supporting, L4 info bar, L5 effects. Every element must MOVE
9. **Add music direction** — Reference artists, describe energy arc
10. **Add narration style** — How to deliver: fast/slow, where to pause, emotional register per section
## Prompt Anatomy
Every production-quality prompt follows this structure:
```
FORMAT: What kind of video, how long, what energy
TONE: Emotional register, references
AVATAR: Detailed physical + environment description (60-100 words)
STYLE: Named aesthetic with colors, typography, motion rules, transitions
CRITICAL ON-SCREEN TEXT: Exact strings that must appear
SCENE-BY-SCENE: Individual scene breakdowns with VO and layered visuals
MUSIC: Genre, reference artists, energy arc
NARRATION STYLE: How to deliver the voiceover
```
### FORMAT
```
FORMAT: 75-second high-energy tech daily briefing. Think: a creator who just got amazing news.
FORMAT: Bloomberg-style strategy briefing. 100-120 seconds. CEO-delivered.
```
### TONE
```
TONE: Confident, direct, data-backed. Highlights hit hard. Lowlights are honest — no spin.
TONE: Edgy, punk tech commentary. Vice News meets The Face magazine — raw, confrontational.
```
### CRITICAL ON-SCREEN TEXT
List every exact string that must appear on screen. Without this, the agent may summarize, round numbers, or rephrase quotes.
```
CRITICAL ON-SCREEN TEXT (display literally):
- "$141M ARR — All-Time High"
- "1.85M Signups — +28% MoM"
- Quote: "Use technology to serve the message, not distract from it." — Shalev Hani
- "@username" — exact social handle
```
### MUSIC & NARRATION
```
MUSIC: Driving electronic, heavy bass drops on key numbers. Run the Jewels meets
a tech keynote. Builds relentlessly, only softens for customer stories.
NARRATION STYLE: High energy throughout. Let numbers PUNCH — pause before big ones,
then deliver hard. Customer stories get warmth. The close should feel like a mic drop.
```
## Avatar Description Guide
**The avatar is NOT a fixed headshot** — design it for each video like a movie character. Think costume designer + set designer.
### Thematic Wardrobe Rule
The avatar's outfit and environment MUST match the content's emotional/cultural context:
| Content Type | Avatar Design | NOT This |
|---|---|---|
| Chinese New Year | Red qipao with gold embroidery, lantern-lit courtyard | "Reporter in a blazer" |
| Breaking tech news | Field reporter, windswept hair, earpiece, city skyline | "Anchor at a desk" |
| Sleep science | Oversized cream knit, cross-legged on bed, warm lamp | "Analyst in a lab" |
| Reddit community | Messy desk, Reddit alien on monitors, upvote arrows on wall | "Researcher in a studio" |
### What to Specify
| Element | Weak | Strong |
|---------|------|--------|
| Clothing | "Business casual" | "Black ribbed merino turtleneck, high collar framing jaw" |
| Environment | "An office" | "Glass-walled conference room. Whiteboard with hand-drawn tier pyramid" |
| Monitor content | "Computer screens" | "Monitor shows scrolling green terminal text and red security alerts" |
| Lighting | "Well lit" | "Cool blue monitor glow from left, warm amber desk lamp from right" |
### Template
```
AVATAR: [Clothing — fabric, color, fit, accessories, posture].
[Setting — specific props, brand logos, what's on the walls].
[Monitors/desk — content visible on screens, items on desk].
[Lighting — direction, color temperature]. [Mood of the space].
60-100 words. 3+ content-specific props. Brand elements visible.
```
## Scene Types
| Type | Format | When to Use |
|------|--------|-------------|
| **A-ROLL** | Avatar speaking to camera | Intros, key insights, CTAs, emotional beats |
| **FULL SCREEN B-ROLL** | No avatar — motion graphics only | Data visualization, information-dense content |
| **A-ROLL + OVERLAY** | Split frame: avatar + content | Presenting data while maintaining human connection |
**Rotation is mandatory.** Never 3+ of the same type in a row. Every prompt needs at least 2 pure B-roll scenes.
**Voiceover on EVERY scene.** Every B-roll scene MUST include a `VOICEOVER:` line. Silent B-roll = broken video.
### Scene Anatomy
**A-ROLL:**
```
SCENE 1 — A-ROLL (10s)
[Avatar center-frame, excited, hands gesturing]
VOICEOVER: "The exact script for this scene."
Lower-third: "TITLE TEXT" white on blue bar.
```
**B-ROLL with layers:**
```
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "The exact script for this scene."
LAYER 1: Dark #1a1a1a background with subtle grid lines pulsing.
LAYER 2: "HEADLINE" SLAMS in from left in white Bold 100pt at -5 degrees.
LAYER 3: Three data cards CASCADE from right, staggered 0.3s.
LAYER 4: Bottom ticker SLIDES in: "supporting text scrolling continuously."
LAYER 5: Grid lines RIPPLE outward from impact point.
Hard cut.
```
**A-ROLL + OVERLAY:**
```
SCENE 3 — A-ROLL + OVERLAY (10s)
[SPLIT — Avatar LEFT 35%. Content RIGHT 65%. NO overlap.]
Avatar gestures toward content side.
VOICEOVER: "The exact script for this scene."
RIGHT SIDE: "HEADLINE" in cyan 60pt. Three stats COUNT UP below.
```
Alternate which side the avatar appears on between overlay scenes.
## The Visual Layer System
Break B-roll into 5 stacked layers. This is the most powerful technique for motion graphics scenes.
| Layer | Purpose | Examples |
|-------|---------|---------|
| **L1** | Background | Textured surface, grid, gradient, color field |
| **L2** | Hero content | Main headline/number that dominates the frame |
| **L3** | Supporting data | Cards, stats, bullet points, secondary information |
| **L4** | Information bar | Tickers, labels, source attributions, quotes |
| **L5** | Effects | Particles, glitches, grid animations, ambient motion |
Every B-roll: 4+ layers. Every overlay content side: 3+ layers. **Every element must MOVE.**
## Motion Vocabulary
### High Energy
| Verb | Example |
|------|---------|
| **SLAMS** | `"$95M" SLAMS in from left at -5 degrees` |
| **CRASHES** | `Title CRASHES in from right, screen-shake on impact` |
| **PUNCHES** | `Quote card PUNCHES up from bottom` |
| **STAMPS** | `Data blocks STAMP in staggered 0.4s` |
| **SHATTERS** | `Text SHATTERS after 1.5s, revealing number underneath` |
### Medium Energy
| Verb | Example |
|------|---------|
| **CASCADE** | `Three cards CASCADE from top, staggered 0.3s` |
| **SLIDES** | `Ticker SLIDES in from right — continuous scroll` |
| **DROPS** | `"TIER 1" DROPS in with white flash` |
| **FILLS** | `Progress bar FILLS 0 to 90% in orange` |
| **DRAWS** | `Chart line DRAWS itself left to right` |
### Low Energy
| Verb | Example |
|------|---------|
| **types on** | `Quote types on word by word in italic white` |
| **fades in** | `Logo fades in at center, held for 3 seconds` |
| **FLOATS** | `Bokeh orbs FLOAT across frame at different speeds` |
| **morphs** | `Number morphs from 17 to 18.9` |
| **COUNTS UP** | `"1.85M" COUNTS UP from 0 in amber 96pt` |
## Transition Types
| Transition | Energy | Styles It Fits |
|------------|--------|---------------|
| Smash cut | Aggressive | Deconstructed, Maximalist, Carnival Surge |
| White flash frame | Punchy | Deconstructed, Maximalist |
| Grid wipe | Systematic | Swiss Pulse, Digital Grid |
| Hard cut | Clean | Swiss Pulse, Shadow Cut |
| Liquid dissolve | Elegant | Data Drift, Dream State |
| Slow cross-dissolve | Refined | Velvet Standard |
| Pop cut / bounce | Fun | Play Mode, Carnival Surge |
| Snap cut | Urgent | Red Wire, Contact Sheet |
| Soft dissolve | Warm | Soft Signal, Warm Grain, Quiet Drama |
| Iris wipe | Nostalgic | Heritage Reel |
## Timing Guidelines
| Content Type | Duration |
|--------------|----------|
| Hook/Intro (A-roll) | 6-10 seconds |
| Data-heavy B-roll | 10-15 seconds (NEVER ≤5s — causes black frames) |
| A-roll + Overlay | 8-12 seconds |
| CTA / Close (A-roll) | 6-8 seconds |
**Common video lengths:** Social clip: 30-45s (5-7 scenes) | Briefing: 60-75s (7-9 scenes) | Deep dive: 90-120s (10-13 scenes)
**Speaking pace:** ~150 words/minute. Calculate: `words / 150 * 60 = seconds`
## What Doesn't Work
Patterns that consistently produce poor results:
**Layout language** — Screen coordinates cause empty/black B-roll:
```
❌ "UPPER-LEFT: headline in 48pt Helvetica"
❌ "CENTER-SCREEN: display at coordinates (400, 300)"
✅ "135K" SLAMS in from left, white Impact 120pt, fills 40% of frame.
```
**Named artists without specs** — "Ikko Tanaka style" means nothing to Video Agent. Translate to concrete rules:
```
❌ "Use an Ikko Tanaka style"
✅ "Flat color blocks, maximum 3 colors per frame, 60% negative space, typography as primary element"
```
**Style examples injected into prompts** — Full example scenes from a style library confuse the agent. Use the style's **rules**, not example scenes.
**Forced short B-roll (≤5 seconds)** — Too short for rendering. Every tested video with 5s B-roll had empty/black screens. Use 10-15s.
**Content as a list, not a story** — "Here are 5 tweets" produces flat videos. Always synthesize: *"X is happening because Y — here's the proof."*
## Production Insights
### Style Performance (from 40+ videos)
| Rank | Style | Strength |
|------|-------|----------|
| 1 | Deconstructed (Brody) | Most reliable across all topics |
| 2 | Swiss Pulse (Müller-Brockmann) | Best for data-heavy content |
| 3 | Digital Grid (Crouwel) | Strong for tech topics |
| 4 | Geometric Bold (Tanaka) | Elegant and versatile |
| 5 | Maximalist Type (Scher) | High energy, use sparingly |
### Duration by Approach
| Approach | Avg Duration | Quality |
|----------|-------------|---------|
| Natural storyboard + custom avatar | ~106s | Best |
| Natural storyboard, no custom avatar | ~69s | Good |
| Forced short scenes + custom avatar | ~71s | Mixed |
| Layout language prompts | ~48s | Poor |
## Quality Checklist
- [ ] Thesis-driven — story, not bullet points
- [ ] Style named with colors, typography, motion, transitions (see [visual-styles.md](visual-styles.md))
- [ ] Avatar has thematic wardrobe + branded environment (60-100 words)
- [ ] Critical text listed — every stat, quote, label
- [ ] Scenes rotate types — never 3+ same type. At least 2 B-roll scenes
- [ ] Every scene has VOICEOVER — including B-roll
- [ ] B-roll scenes have 4+ layers, every element has motion verbs
- [ ] B-roll scenes are 10-15 seconds (never ≤5s)
- [ ] Brand logos appear when discussing companies
- [ ] Every element moves — no static frames
@@ -0,0 +1,195 @@
---
name: quota
description: Credit system, usage limits, and checking remaining quota for HeyGen
---
# HeyGen Quota and Credits
HeyGen uses a credit-based system for video generation. Understanding quota management helps prevent failed video generation requests.
## Checking Remaining Quota
### curl
```bash
curl -X GET "https://api.heygen.com/v2/user/remaining_quota" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface QuotaResponse {
error: null | string;
data: {
remaining_quota: number;
used_quota: number;
};
}
const response = await fetch("https://api.heygen.com/v2/user/remaining_quota", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data }: QuotaResponse = await response.json();
console.log(`Remaining credits: ${data.remaining_quota}`);
```
### Python
```python
import requests
import os
response = requests.get(
"https://api.heygen.com/v2/user/remaining_quota",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()["data"]
print(f"Remaining credits: {data['remaining_quota']}")
```
## Response Format
```json
{
"error": null,
"data": {
"remaining_quota": 450,
"used_quota": 50
}
}
```
## Credit Consumption
Different operations consume different amounts of credits:
| Operation | Credit Cost | Notes |
|-----------|-------------|-------|
| Standard video (1 min) | ~1 credit per minute | Varies by resolution |
| 720p video | Base rate | Standard quality |
| 1080p video | ~1.5x base rate | Higher quality |
| Video translation | Varies | Depends on video length |
| Streaming avatar | Per session | Real-time usage |
## Pre-Generation Quota Check
Always verify sufficient quota before generating videos:
```typescript
async function generateVideoWithQuotaCheck(videoConfig: VideoConfig) {
// Check quota first
const quotaResponse = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: quota } = await quotaResponse.json();
// Estimate required credits (rough estimate: 1 credit per minute)
const estimatedMinutes = videoConfig.estimatedDuration / 60;
const requiredCredits = Math.ceil(estimatedMinutes);
if (quota.remaining_quota < requiredCredits) {
throw new Error(
`Insufficient credits. Need ${requiredCredits}, have ${quota.remaining_quota}`
);
}
// Proceed with video generation
return generateVideo(videoConfig);
}
```
## Quota Management Best Practices
### 1. Monitor Usage Regularly
```typescript
async function logQuotaUsage() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
console.log({
remaining: data.remaining_quota,
used: data.used_quota,
percentUsed: (
(data.used_quota / (data.remaining_quota + data.used_quota)) *
100
).toFixed(1),
});
}
```
### 2. Set Up Alerts
```typescript
const QUOTA_WARNING_THRESHOLD = 50;
async function checkQuotaWithAlert() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.remaining_quota < QUOTA_WARNING_THRESHOLD) {
// Send alert (email, Slack, etc.)
await sendAlert(`Low HeyGen quota: ${data.remaining_quota} credits remaining`);
}
return data;
}
```
### 3. Use Test Mode for Development
When available, use test mode to avoid consuming credits during development:
```typescript
const videoConfig = {
test: true, // Use test mode during development
video_inputs: [...],
};
// Test videos may have watermarks but don't consume credits
```
## Subscription Tiers
Different subscription tiers have different quota allocations and features:
| Tier | Features |
|------|----------|
| Free | Limited credits, basic features |
| Creator | More credits, standard avatars |
| Team | Higher limits, team collaboration |
| Enterprise | Custom limits, API access, priority support |
API access typically requires Enterprise tier or higher.
## Error Handling for Quota Issues
```typescript
async function handleQuotaError(error: any) {
if (error.message.includes("quota") || error.message.includes("credit")) {
console.error("Quota exceeded. Consider:");
console.error("1. Upgrading your subscription");
console.error("2. Waiting for quota reset");
console.error("3. Purchasing additional credits");
// Check current quota
const quota = await getQuota();
console.error(`Current remaining: ${quota.remaining_quota}`);
}
throw error;
}
```
@@ -0,0 +1,347 @@
---
name: video-agent
description: One-shot prompt video generation with HeyGen Video Agent API
---
# Video Agent API
The Video Agent API generates complete videos from a single text prompt. Unlike the standard video generation API which requires detailed scene-by-scene configuration, Video Agent automatically handles script writing, avatar selection, visuals, voiceover, pacing, and captions.
## MCP Tool (Preferred)
If the HeyGen MCP server is connected, use `mcp__heygen__generate_video_agent` instead of direct API calls:
```
Tool: mcp__heygen__generate_video_agent
Parameters:
prompt: "<optimized prompt from prompt-optimizer.md>"
config:
duration_sec: 90 # optional, 5-300
avatar_id: "avatar_id" # optional, agent selects if omitted
orientation: "landscape" # optional, "landscape" or "portrait"
files: # optional
- asset_id: "uploaded_asset_id"
```
Then check status with `mcp__heygen__get_video` using the returned `video_id`.
The prompt quality is still the critical factor — always follow [prompt-optimizer.md](prompt-optimizer.md) regardless of whether you use MCP or direct API.
## When to Use Video Agent vs Standard API
| Use Case | Recommended API |
|----------|-----------------|
| Quick video from idea | Video Agent |
| Precise control over scenes, avatars, timing | Standard v2/video/generate |
| Automated content generation at scale | Video Agent |
| Specific avatar with exact script | Standard v2/video/generate |
| Prototype or draft video | Video Agent |
| Brand-consistent production video | Standard v2/video/generate |
## Before You Call This API
**Required step:** Optimize your prompt using [prompt-optimizer.md](prompt-optimizer.md) before generating a video. The difference between mediocre and professional results depends entirely on prompt quality.
Quick checklist:
1. Define visual style (colors, aesthetic) — see [visual-styles.md](visual-styles.md)
2. Structure scenes with specific scene types
3. Write VO script at ~150 words/minute
4. Specify media types for each scene (Motion Graphics, Stock, AI-generated)
## Direct API Endpoint
```
POST https://api.heygen.com/v1/video_agent/generate
```
## Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `prompt` | string | ✓ | Text prompt describing the video you want |
| `config` | object | | Configuration options (see below) |
| `files` | array | | Asset files to reference in generation |
| `callback_id` | string | | Custom ID for tracking. **Requires `callback_url` to also be set** — omit both if you don't need webhooks |
| `callback_url` | string | | Webhook URL for completion notification |
### Config Object
| Field | Type | Description |
|-------|------|-------------|
| `duration_sec` | integer | Approximate duration in seconds (5-300) |
| `avatar_id` | string | Specific avatar to use (optional - agent selects if not provided) |
| `orientation` | string | `"portrait"` or `"landscape"` |
### Files Array
| Field | Type | Description |
|-------|------|-------------|
| `asset_id` | string | Asset ID of uploaded file to reference |
## Response Format
```json
{
"error": null,
"data": {
"video_id": "abc123"
}
}
```
## curl Example
```bash
curl -X POST "https://api.heygen.com/v1/video_agent/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a 60-second product demo video for a new AI-powered calendar app. The tone should be professional but friendly, targeting busy professionals. Highlight the smart scheduling feature and time zone handling."
}'
```
## TypeScript
```typescript
interface VideoAgentConfig {
duration_sec?: number; // 5-300 seconds
avatar_id?: string; // Optional: specific avatar
orientation?: "portrait" | "landscape";
}
interface VideoAgentFile {
asset_id: string;
}
interface VideoAgentRequest {
prompt: string; // Required
config?: VideoAgentConfig;
files?: VideoAgentFile[];
callback_id?: string; // Requires callback_url if set
callback_url?: string;
}
interface VideoAgentResponse {
error: string | null;
data: {
video_id: string;
};
}
async function generateWithVideoAgent(
prompt: string,
config?: VideoAgentConfig
): Promise<string> {
const request: VideoAgentRequest = { prompt };
if (config) {
request.config = config;
}
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
const json: VideoAgentResponse = await response.json();
if (json.error) {
throw new Error(`Video Agent failed: ${json.error}`);
}
return json.data.video_id;
}
```
## Python
```python
import requests
import os
from typing import Optional
def generate_with_video_agent(
prompt: str,
duration_sec: Optional[int] = None,
avatar_id: Optional[str] = None,
orientation: Optional[str] = None
) -> str:
request_body = {"prompt": prompt}
config = {}
if duration_sec:
config["duration_sec"] = duration_sec
if avatar_id:
config["avatar_id"] = avatar_id
if orientation:
config["orientation"] = orientation
if config:
request_body["config"] = config
response = requests.post(
"https://api.heygen.com/v1/video_agent/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json=request_body
)
data = response.json()
if data.get("error"):
raise Exception(f"Video Agent failed: {data['error']}")
return data["data"]["video_id"]
```
## Examples
### Basic: Prompt Only
```typescript
const videoId = await generateWithVideoAgent(
"Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
);
```
### With Duration and Orientation
```typescript
const videoId = await generateWithVideoAgent(
"Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
{
duration_sec: 90,
orientation: "landscape"
}
);
```
### With Specific Avatar
```typescript
const videoId = await generateWithVideoAgent(
"Present quarterly sales results. Professional tone, data-focused.",
{
duration_sec: 120,
avatar_id: "josh_lite3_20230714",
orientation: "landscape"
}
);
```
### With Reference Files
Upload assets first, then reference them:
```typescript
// 1. Upload reference materials (see assets.md)
const logoAssetId = await uploadFile("./company-logo.png", "image/png");
const productImageId = await uploadFile("./product-screenshot.png", "image/png");
// 2. Generate video with references
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
config: {
duration_sec: 60,
orientation: "landscape"
},
files: [
{ asset_id: logoAssetId },
{ asset_id: productImageId }
]
}),
}
);
```
## Writing Effective Prompts
See **[prompt-optimizer.md](prompt-optimizer.md)** for comprehensive prompt writing guidance.
The prompt optimizer covers:
- Prompt complexity levels (basic → scene-by-scene)
- Visual style taxonomy and color specification
- Media type selection (Motion Graphics vs Stock vs AI-generated)
- Scene structure and timing calculations
- Ready-to-use templates for common video types
## Checking Video Status
Video Agent returns a `video_id` - use the standard status endpoint to check progress:
```typescript
// Same polling as standard video generation
const videoUrl = await waitForVideo(videoId);
```
See [video-status.md](video-status.md) for polling implementation.
## Comparison: Video Agent vs Standard API
### Video Agent Request
```typescript
// Simple: describe what you want
const videoId = await generateWithVideoAgent(
"Create a 60-second tutorial on setting up two-factor authentication. Professional tone, step-by-step."
);
```
### Equivalent Standard API Request
```typescript
// Complex: specify every detail
const videoId = await generateVideo({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to this tutorial on two-factor authentication...",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
// ... more scenes for each step
],
dimension: { width: 1920, height: 1080 },
});
```
## Limitations
- Less control over exact script wording
- Avatar selection may vary if not specified
- Scene composition is automated
- May not match precise brand guidelines
- Duration is approximate, not exact
## Best Practices
1. **Be specific in prompts** - More detail = better results
2. **Specify duration** - Use `config.duration_sec` for predictable length
3. **Lock avatar if needed** - Use `config.avatar_id` for consistency
4. **Upload reference files** - Help agent understand your brand/product
5. **Iterate on prompts** - Refine based on results
6. **Use for drafts** - Video Agent is great for quick iterations before final production
@@ -0,0 +1,502 @@
---
name: video-status
description: Polling patterns, status types, and retrieving download URLs for HeyGen videos
---
# Video Status and Polling
After generating a video, you need to poll for status until the video is complete. HeyGen processes videos asynchronously.
## MCP Tool (Preferred)
If the HeyGen MCP server is connected, use `mcp__heygen__get_video` with the `videoId` parameter. It returns status, video_url, thumbnail_url, duration, title, gif_url, captioned_video_url, and other metadata in a single call.
## Checking Video Status (Direct API)
### curl
```bash
curl -X GET "https://api.heygen.com/v2/videos/YOUR_VIDEO_ID" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface VideoStatusResponse {
error: null | string;
data: {
id: string;
status: "pending" | "processing" | "completed" | "failed";
video_url?: string;
thumbnail_url?: string;
duration?: number;
title?: string;
created_at?: string;
completed_at?: string;
gif_url?: string;
captioned_video_url?: string;
subtitle_url?: string;
folder_id?: string;
output_language?: string;
failure_code?: string;
failure_message?: string;
};
}
async function getVideoStatus(videoId: string): Promise<VideoStatusResponse["data"]> {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: VideoStatusResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
### Python
```python
import requests
import os
def get_video_status(video_id: str) -> dict:
response = requests.get(
f"https://api.heygen.com/v2/videos/{video_id}",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]
```
## Video Status Types
| Status | Description |
|--------|-------------|
| `pending` | Video is queued for processing |
| `processing` | Video is being generated |
| `completed` | Video is ready for download |
| `failed` | Video generation failed |
## Expected Generation Times
Video generation typically takes **5-15 minutes**, but can exceed 20 minutes during peak load or for longer scripts.
| Factor | Impact |
|--------|--------|
| Script length | Longer scripts = significantly longer processing |
| Resolution | 1080p takes longer than 720p |
| Avatar complexity | Some avatars render faster |
| Queue load | Peak hours may cause 15-20+ minute waits |
| Multiple scenes | Each scene adds processing time |
**Recommendations**:
- Set timeout to **15-20 minutes** (900,000-1,200,000 ms) for safety
- For scripts > 2 minutes of speech, expect 15+ minutes
- Consider async patterns (save video_id, check later) for long videos
## Response Format
### Completed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "completed",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"title": "My Video",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:38:00Z",
"gif_url": "https://files.heygen.ai/gif/abc123.gif",
"captioned_video_url": null,
"subtitle_url": null,
"folder_id": null,
"output_language": "en"
}
}
```
### Failed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "failed",
"failure_code": "script_too_long",
"failure_message": "Script too long for selected avatar"
}
}
```
## Polling Implementation
### Basic Polling
```typescript
async function waitForVideo(
videoId: string,
maxWaitMs = 600000, // 10 minutes
pollIntervalMs = 5000 // 5 seconds
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await getVideoStatus(videoId);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
case "pending":
case "processing":
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
break;
}
}
throw new Error("Video generation timed out");
}
```
### Polling with Progress Callback
```typescript
type ProgressCallback = (status: string, elapsed: number) => void;
async function waitForVideoWithProgress(
videoId: string,
onProgress?: ProgressCallback,
maxWaitMs = 600000,
pollIntervalMs = 5000
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const elapsed = Date.now() - startTime;
const status = await getVideoStatus(videoId);
onProgress?.(status.status, elapsed);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
default:
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
throw new Error("Video generation timed out");
}
// Usage
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`Status: ${status}, Elapsed: ${Math.round(elapsed / 1000)}s`);
}
);
```
### Python Polling
```python
import time
from typing import Optional, Callable
def wait_for_video(
video_id: str,
max_wait_seconds: int = 600,
poll_interval: int = 5,
on_progress: Optional[Callable[[str, int], None]] = None
) -> str:
start_time = time.time()
while time.time() - start_time < max_wait_seconds:
elapsed = int(time.time() - start_time)
status_data = get_video_status(video_id)
status = status_data["status"]
if on_progress:
on_progress(status, elapsed)
if status == "completed":
return status_data["video_url"]
elif status == "failed":
raise Exception(status_data.get("failure_message", "Video generation failed"))
time.sleep(poll_interval)
raise Exception("Video generation timed out")
# Usage
def progress_callback(status: str, elapsed: int):
print(f"Status: {status}, Elapsed: {elapsed}s")
video_url = wait_for_video(video_id, on_progress=progress_callback)
```
## Downloading the Video
Once the video is complete, download it. **Important**: The video URL may not be immediately available after status shows "completed". Use retry logic with backoff.
### TypeScript (with retry)
```typescript
import fs from "fs";
import path from "path";
async function downloadVideoWithRetry(
videoUrl: string,
outputPath = "./output/video.mp4",
maxRetries = 5,
initialDelayMs = 2000
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
console.log(`Video downloaded to ${outputPath}`);
return;
} catch (error) {
lastError = error as Error;
const delay = initialDelayMs * Math.pow(2, attempt); // Exponential backoff
console.log(`Download attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`Failed to download after ${maxRetries} attempts: ${lastError?.message}`);
}
```
### Python (with retry)
```python
import requests
import time
def download_video_with_retry(
video_url: str,
output_path: str,
max_retries: int = 5,
initial_delay: float = 2.0
) -> None:
last_error = None
for attempt in range(max_retries):
try:
response = requests.get(video_url, stream=True, timeout=60)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Video downloaded to {output_path}")
return
except Exception as e:
last_error = e
delay = initial_delay * (2 ** attempt) # Exponential backoff
print(f"Download attempt {attempt + 1} failed, retrying in {delay}s...")
time.sleep(delay)
raise Exception(f"Failed to download after {max_retries} attempts: {last_error}")
```
### Simple Download (no retry)
For quick scripts where you'll retry manually:
```typescript
async function downloadVideo(videoUrl: string, outputPath = "./output/video.mp4") {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
}
```
## Complete Workflow Example
```typescript
async function generateAndDownloadVideo(config: VideoConfig): Promise<string> {
// 1. Generate video
const generateResponse = await fetch(
"https://api.heygen.com/v2/video/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const { data: generateData } = await generateResponse.json();
const videoId = generateData.video_id;
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`[${Math.round(elapsed / 1000)}s] Status: ${status}`);
}
);
// 3. Download
const outputPath = `./output/${videoId}.mp4`;
await downloadVideo(videoUrl, outputPath);
return outputPath;
}
```
## Resumable Status Checking
For long-running generations, save the video_id and check status later rather than keeping a process waiting.
### Save State After Generation
```typescript
interface PendingVideo {
videoId: string;
createdAt: string;
script: string;
avatarId: string;
voiceId: string;
}
async function startVideoGeneration(config: VideoGenerateRequest): Promise<PendingVideo> {
const videoId = await generateVideo(config);
const pending: PendingVideo = {
videoId,
createdAt: new Date().toISOString(),
script: config.video_inputs[0].voice.input_text!,
avatarId: config.video_inputs[0].character.avatar_id!,
voiceId: config.video_inputs[0].voice.voice_id!,
};
// Save to file for later retrieval
fs.writeFileSync("pending-video.json", JSON.stringify(pending, null, 2));
console.log(`Video generation started. ID: ${videoId}`);
console.log("Check status later with: checkVideoStatus()");
return pending;
}
```
### Check Status Later
```typescript
async function checkVideoStatus(): Promise<void> {
if (!fs.existsSync("pending-video.json")) {
console.log("No pending video found");
return;
}
const pending: PendingVideo = JSON.parse(
fs.readFileSync("pending-video.json", "utf-8")
);
const elapsed = Date.now() - new Date(pending.createdAt).getTime();
console.log(`Checking video ${pending.videoId} (started ${Math.round(elapsed / 60000)} min ago)...`);
const status = await getVideoStatus(pending.videoId);
switch (status.status) {
case "completed":
console.log(`Video ready: ${status.video_url}`);
console.log(`Duration: ${status.duration}s`);
// Clean up pending file
fs.unlinkSync("pending-video.json");
// Save result
fs.writeFileSync("video-result.json", JSON.stringify({
...pending,
videoUrl: status.video_url,
thumbnailUrl: status.thumbnail_url,
duration: status.duration,
title: status.title,
createdAt: status.created_at,
completedAt: status.completed_at,
}, null, 2));
break;
case "failed":
console.error(`Video failed: ${status.failure_message}`);
fs.unlinkSync("pending-video.json");
break;
default:
console.log(`Status: ${status.status} - check again in a few minutes`);
}
}
```
### CLI-Friendly Pattern
```typescript
// generate-video.ts - Start generation and exit
async function main() {
const pending = await startVideoGeneration(config);
console.log(`\nVideo ID saved. Run 'npx tsx check-status.ts' to check progress.`);
process.exit(0); // Exit immediately, don't wait
}
// check-status.ts - Check and optionally wait
async function main() {
const args = process.argv.slice(2);
const shouldWait = args.includes("--wait");
if (shouldWait) {
// Poll until complete (with 20 min timeout)
const result = await waitForVideo(pending.videoId, apiKey, onProgress, 1200000);
console.log(`Done: ${result.video_url}`);
} else {
// Just check once and report
await checkVideoStatus();
}
}
```
## Alternative: Using Webhooks
Instead of polling, you can use webhooks to receive notifications when videos complete. See [webhooks.md](webhooks.md) for details. Webhooks are ideal for production systems where you don't want to maintain polling connections.
## Best Practices
1. **Use exponential backoff** - Increase poll intervals for long-running jobs
2. **Set reasonable timeouts** - Most videos complete within 10 minutes
3. **Handle failures gracefully** - Check error messages for actionable feedback
4. **Consider webhooks** - For production systems, webhooks are more efficient than polling
5. **Cache video URLs** - Downloaded video URLs are valid for a limited time
@@ -0,0 +1,367 @@
---
name: visual-styles
description: 20 named visual styles for Video Agent prompts — each with colors, typography, motion, and transitions
---
# Visual Style Library — 20 Styles
Named visual styles for Video Agent prompts. Each is inspired by a real graphic designer. Ordered by mood intensity.
**Picking a style:** Match mood first, content second. Ask: *"What should the viewer FEEL?"*
**Using a style:** Copy the style block into your prompt's STYLE section. Use the visual language rules — don't inject the example B-roll scenes (they confuse the agent).
**Custom styles:** These are examples. Create your own by combining elements, referencing other designers, art movements, or cultural aesthetics. The pattern: **named style + designer reference + color palette + typography + motion rules + transitions.**
## Quick Reference
| # | Style | Artist | Mood | Best For |
|---|---|---|---|---|
| 1 | Soft Signal | Sagmeister | Intimate, warm | Personal stories, wellness |
| 2 | Warm Grain | Eksell | Organic, friendly | Environmental, sustainability |
| 3 | Quiet Drama | Ray | Humanist, contemplative | Profiles, biographical |
| 4 | Heritage Reel | Cassandre | Nostalgic, vintage | History, retrospectives |
| 5 | Silk Route | Abedini | Flowing, mysterious | Global affairs, cross-cultural |
| 6 | Swiss Pulse | Müller-Brockmann | Clinical, precise | Data-heavy, analytical |
| 7 | Geometric Bold | Tanaka | Minimal, elegant | Lifestyle, visual essays |
| 8 | Velvet Standard | Vignelli | Premium, timeless | Luxury, investor updates |
| 9 | Digital Grid | Crouwel | Systematic, technical | Infrastructure, engineering |
| 10 | Contact Sheet | Brodovitch | Editorial, investigative | Journalism, deep dives |
| 11 | Folk Frequency | Terrazas | Cultural, vivid | Festivals, food, heritage |
| 12 | Earth Pulse | Ghariokwu | Grounded, communal | Community, grassroots |
| 13 | Dream State | Tomaszewski | Surreal, poetic | Op-eds, philosophy |
| 14 | Play Mode | Ahn Sang-soo | Playful, irreverent | Entertainment, pop culture |
| 15 | Carnival Surge | Lins | Euphoric, celebratory | Milestones, hype |
| 16 | Shadow Cut | Hillmann | Dark, cinematic | Exposés, investigations |
| 17 | Deconstructed | Brody | Industrial, raw | Tech news, punk energy |
| 18 | Maximalist Type | Scher | Loud, kinetic | Big announcements, launches |
| 19 | Data Drift | Anadol | Futuristic, immersive | AI/tech, innovation |
| 20 | Red Wire | Tartakover | Urgent, immediate | Breaking news, crisis |
## Mood-to-Style Guide
| Content feels... | Use... |
|---|---|
| Personal, intimate | Soft Signal, Quiet Drama |
| Natural, earthy | Warm Grain, Earth Pulse |
| Nostalgic, historical | Heritage Reel |
| Data-driven, analytical | Swiss Pulse, Digital Grid |
| Elegant, premium | Velvet Standard, Geometric Bold |
| Cultural, global | Silk Route, Folk Frequency |
| Investigative, serious | Contact Sheet, Shadow Cut |
| Fun, lighthearted | Play Mode, Carnival Surge |
| Philosophical, abstract | Dream State |
| Punk, grassroots, raw | Deconstructed |
| Hype, loud, high-energy | Maximalist Type |
| Tech-forward, futuristic | Data Drift |
| Breaking, urgent | Red Wire |
---
## 1. Soft Signal — Stefan Sagmeister
**Mood:** Intimate, warm | **Best for:** Personal stories, wellness, reflections
- Warm amber and cream with dusty rose, sage green, honey gold accents
- Handwritten-style text overlays — personal, lowercase, delicate
- Close-up framing: hands, faces, textures. Macro lens feel
- Slow drifts and floats, never snaps. Soft dissolves, warm light leaks
```
STYLE — SOFT SIGNAL (Sagmeister): Warm amber/cream, dusty rose, sage green.
Handwritten-style text. Close-up framing. Slow drifts and floats.
Soft dissolves with warm light leaks.
```
## 2. Warm Grain — Olle Eksell
**Mood:** Organic, friendly | **Best for:** Environmental, sustainability, community
- Earth tones: ochre, forest green, terracotta, cream, soft brown
- Rounded sans-serif type. Organic rounded compositions — nothing angular
- 16mm film grain, slightly desaturated. Natural textures: wood, linen, stone
- Gentle wipes, soft cuts, unhurried
```
STYLE — WARM GRAIN (Eksell): Earth tones — ochre, forest green, terracotta, cream.
Organic rounded compositions. 16mm film grain. Rounded sans-serif.
Gentle wipes and soft cuts.
```
## 3. Quiet Drama — Satyajit Ray
**Mood:** Humanist, contemplative | **Best for:** Profiles, biographical, cultural
- Muted warm: sepia, deep brown, soft gold, off-white, charcoal
- Clean serif type, positioned with care. Portrait framing
- Strong single-source contrast: window light, single lamp
- Deliberate pacing, longer holds. Slow fades to black
```
STYLE — QUIET DRAMA (Ray): Muted warm — sepia, deep brown, soft gold.
Portrait framing. Clean serif. Strong single-source contrast.
Slow fades to black.
```
## 4. Heritage Reel — Cassandre
**Mood:** Nostalgic, vintage | **Best for:** History, retrospectives, brand origins
- Faded gold, deep burgundy, navy, cream, sepia wash
- Elegant centered serif like classic film title cards
- Vignetting, softened edges. Film grain, light scratches, gentle jitter
- Iris wipes, film reel flicker
```
STYLE — HERITAGE REEL (Cassandre): Faded gold, burgundy, navy, sepia wash.
Elegant centered serif. Vignetting and aged film grain.
Iris wipe transitions.
```
## 5. Silk Route — Reza Abedini
**Mood:** Flowing, mysterious | **Best for:** Global affairs, cross-cultural, art/design
- Rich jewel tones: deep teal, burgundy, gold, lapis blue, black
- Elegant spaced type along natural visual lines
- Layered compositions — foreground, midground, background all active
- Flowing dissolves, smooth morphs
```
STYLE — SILK ROUTE (Abedini): Jewel tones — deep teal, burgundy, gold, lapis blue.
Layered compositions, all depths active. Elegant spaced type.
Flowing dissolves and smooth morphs.
```
## 6. Swiss Pulse — Josef Müller-Brockmann
**Mood:** Clinical, precise | **Best for:** Data-heavy, analytical, financial, metrics
- Black (#1a1a1a), white, ONE accent: electric blue (#0066FF)
- Helvetica Bold headlines, Regular labels. Numbers LARGE (80-120pt)
- Grid-locked compositions. Every element snaps to 12-column grid
- Animated counters COUNT UP from 0. Diagonal compositions on key moments
- Grid wipes, hard cuts. No dissolves
```
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + electric blue #0066FF.
Grid-locked. Helvetica Bold. Animated counters. Diagonal accents.
Grid wipe transitions.
```
## 7. Geometric Bold — Ikko Tanaka
**Mood:** Minimal, elegant | **Best for:** Clean lifestyle, culture, visual essays, brand profiles
- Maximum 3 flat colors per frame — no gradients
- Bold clean type as primary visual element
- Asymmetric composition, 60% negative space minimum. Single focal point
- Clean cuts on beat, no effects
```
STYLE — GEOMETRIC BOLD (Tanaka): Max 3 flat colors per frame.
60% negative space. Bold type as primary element.
Single focal point. Clean cuts on beat.
```
## 8. Velvet Standard — Massimo Vignelli
**Mood:** Premium, timeless | **Best for:** Luxury, investor updates, keynotes, product showcases
- Black, white, ONE rich accent: deep navy (#1a237e) or gold (#c9a84c)
- Thin sans-serif, ALL CAPS, letter-spaced wide
- Generous negative space. Symmetrical, centered, architectural precision
- Slow, deliberate. Sequential reveals. Elegant cross-dissolves
```
STYLE — VELVET STANDARD (Vignelli): Black, white, one accent: gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow elegant cross-dissolves.
```
## 9. Digital Grid — Wim Crouwel
**Mood:** Systematic, technical | **Best for:** Infrastructure, engineering, code, tech
- Dark (#0a0a0a) with cyan (#00E5FF), amber (#FFB300), green (#00FF88)
- Monospaced type throughout. Code-terminal aesthetic
- Pixel grid overlays visible. Everything snaps to system
- Grid nodes light up sequentially. Scan-line effects, cursor blinks
- Clean wipe transitions
```
STYLE — DIGITAL GRID (Crouwel): Monospaced type. Dark #0a0a0a with cyan #00E5FF, amber #FFB300.
Pixel grid overlays. Terminal aesthetic. Clean wipe transitions.
```
## 10. Contact Sheet — Alexey Brodovitch
**Mood:** Editorial, investigative | **Best for:** Journalism, deep dives, research breakdowns
- High contrast B&W with occasional desaturated color accents
- Bold sans-serif captions like editorial annotations
- Photo-editorial framing — multiple images, contact-sheet energy
- Raw grain, imperfect focus. Tight crops on faces and hands
- Hard cuts on beat, snap-zooms
```
STYLE — CONTACT SHEET (Brodovitch): High contrast B&W, desaturated accents.
Photo-editorial framing. Bold sans-serif annotations. Raw grain.
Hard cuts on beat. Snap-zooms.
```
## 11. Folk Frequency — Eduardo Terrazas
**Mood:** Cultural, vivid | **Best for:** Cultural events, food, tradition, heritage
- Vivid folk: hot pink, bright orange, cobalt blue, sun yellow, emerald
- Bold warm rounded type. Pattern and repetition — folk art rhythms
- Rich textures: woven fabrics, painted surfaces, ceramic, handmade
- Colorful wipes, quick cuts on festive rhythm
```
STYLE — FOLK FREQUENCY (Terrazas): Vivid folk — hot pink, cobalt blue, sun yellow, emerald.
Bold rounded type. Folk art rhythms. Rich handmade textures.
Colorful wipes on festive rhythm.
```
## 12. Earth Pulse — Lemi Ghariokwu
**Mood:** Grounded, communal | **Best for:** Community, music/culture, grassroots
- Warm saturated: burnt orange, deep green, rich yellow, terracotta
- Bold expressive type, center-frame. Wide community framing
- Rhythmic editing timed to musical beats
- Rhythmic cuts on beat, freeze-frames for emphasis
```
STYLE — EARTH PULSE (Ghariokwu): Warm saturated — burnt orange, deep green, rich yellow.
Bold expressive type. Wide community framing.
Rhythmic cuts on beat. Freeze-frames.
```
## 13. Dream State — Henryk Tomaszewski
**Mood:** Surreal, poetic | **Best for:** Op-eds, philosophy, think pieces, speculative
- Muted palette with one surreal accent: dusty blues, grey-greens, then shock of red or gold
- Sparse precise text — few words, maximum impact. Thin elegant floating type
- Unusual juxtapositions. Dreamlike quality: soft edges, atmospheric haze
- Slow morph dissolves. NEVER hard cuts
```
STYLE — DREAM STATE (Tomaszewski): Muted palette + one surreal accent.
Thin elegant floating type. Soft edges, atmospheric haze.
Slow morph dissolves — NEVER hard cuts.
```
## 14. Play Mode — Ahn Sang-soo
**Mood:** Playful, irreverent | **Best for:** Entertainment, pop culture, listicles, fun
- Bright candy: electric blue, hot pink, lime green, yellow, white
- Bouncy oversized tilted text. Asymmetric off-kilter compositions
- Quick cuts (1-3 seconds). Score cards, achievement popups, XP bars
- Bouncy spring physics — text overshoots and settles, screen shakes
- Pop cuts, whip pans, bounce effects
```
STYLE — PLAY MODE (Ahn Sang-soo): Electric blue, hot pink, lime green.
Bouncy spring physics. Oversized tilted text. Score cards, XP bars.
Pop cuts, bounce effects.
```
## 15. Carnival Surge — Rico Lins
**Mood:** Euphoric, celebratory | **Best for:** Big announcements, milestones, celebrations, hype
- Maximum color: hot pink (#FF1493), electric yellow (#FFE000), teal (#00CED1), orange, violet
- MASSIVE bold text at ANGLES over footage. Collage-style overlapping
- Rapid 1-2 second clips. Confetti, lights, constant energy
- Smash cuts, flash frames, rapid-fire montage
```
STYLE — CARNIVAL SURGE (Lins): Max color — hot pink #FF1493, yellow #FFE000, teal #00CED1.
Collage layering. Text MASSIVE at ANGLES. Confetti bursts.
Smash cuts, flash frames.
```
## 16. Shadow Cut — Hans Hillmann
**Mood:** Dark, cinematic | **Best for:** Exposés, investigations, controversy, dark deep dives
- Near-monochrome: deep blacks, cold greys, stark white + blood red or toxic green
- Sharp angular text like film noir title cards
- Heavy shadow — faces half-lit, objects emerging from darkness
- Slow creeping push-ins, slow reveals, tension
- Iris to black, slow fade from darkness, hard cuts to silence
```
STYLE — SHADOW CUT (Hillmann): Deep blacks, cold greys + blood red accent.
Sharp angular text. Heavy shadow. Slow creeping push-ins.
Hard cuts to black. Film noir tension.
```
## 17. Deconstructed — Neville Brody
**Mood:** Industrial, raw | **Best for:** Tech news, security, punk energy, counter-culture
- Dark grey (#1a1a1a), black, rust orange (#D4501E), raw white (#f0f0f0)
- Type at angles, overlapping edges, escaping frames. Bold industrial
- High contrast, gritty textures: scratched metal, peeling paint, scan-line glitch
- Text SLAMS, SHATTERS, PUNCHES. Letters scramble then snap
- Smash cuts, glitch transitions, white flash frames
```
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures, scan-line glitch.
Smash cuts with flash frames.
```
## 18. Maximalist Type — Paula Scher
**Mood:** Loud, kinetic | **Best for:** Big announcements, launches, high-energy recaps
- Bold saturated: red, yellow, black, white — maximum contrast
- Text IS the visual. Overlapping layers at different scales and angles, 50-80% of frame
- Kinetic energy: everything moving, slamming, sliding. 1-2 second rapid cuts
- Text layered OVER footage — never empty backgrounds
- Smash cuts, text slamming from edges, flash frames
```
STYLE — MAXIMALIST TYPE (Scher): Red, yellow, black, white — max contrast.
Text IS the visual. Overlapping at different scales, 50-80% of frame.
Kinetic everything. Smash cuts, flash frames.
```
## 19. Data Drift — Refik Anadol
**Mood:** Futuristic, immersive | **Best for:** AI/tech, speculative, cutting-edge science
- Iridescent: holographic silver, electric purple (#7c3aed), cyan (#06b6d4), deep black (#0a0a0a)
- Thin futuristic sans-serif — minimal, floating, weightless
- Fluid morphing compositions. Extreme scale shifts: microscopic to cosmic
- Particles coalesce into numbers, light traces data paths
- Liquid dissolves, particles dispersing and reforming
```
STYLE — DATA DRIFT (Anadol): Iridescent — purple #7c3aed, cyan #06b6d4, deep black.
Fluid morphing compositions. Thin futuristic type.
Liquid dissolves. Particles coalesce into numbers.
```
## 20. Red Wire — David Tartakover
**Mood:** Urgent, immediate | **Best for:** Breaking news, crisis updates, alerts
- High alert: red, black, white, emergency yellow — maximum contrast
- Bold condensed all caps — every word screams urgency
- Split screens, ticker-style text bars, timestamp overlays — max information density
- Multiple text elements simultaneously. Handheld energy
- Snap cuts, flash frames, zero breathing room
```
STYLE — RED WIRE (Tartakover): Red, black, white, emergency yellow.
Bold condensed all-caps. Split screens, tickers, timestamps.
Snap cuts, flash frames. Zero breathing room.
```
@@ -0,0 +1,393 @@
---
name: webhooks
description: Registering webhook endpoints and event types for HeyGen
---
# Webhooks
Webhooks allow HeyGen to notify your application when events occur, such as video completion. This is more efficient than polling for status updates.
## Overview
Instead of repeatedly checking video status, webhooks push notifications to your server when:
- Video generation completes
- Video generation fails
- Translation completes
- Avatar training completes
- Other async operations finish
## Setting Up a Webhook Endpoint
Your webhook endpoint should:
1. Accept POST requests
2. Return 200 status quickly
3. Handle events asynchronously
### Express.js Example
```typescript
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
// Webhook endpoint
app.post("/webhook/heygen", async (req, res) => {
// Acknowledge receipt immediately
res.status(200).send("OK");
// Process event asynchronously
processWebhookEvent(req.body).catch(console.error);
});
async function processWebhookEvent(event: HeyGenWebhookEvent) {
console.log(`Received event: ${event.event_type}`);
switch (event.event_type) {
case "avatar_video.success":
await handleVideoSuccess(event);
break;
case "avatar_video.fail":
await handleVideoFailure(event);
break;
case "video_translate.success":
await handleTranslationSuccess(event);
break;
default:
console.log(`Unknown event type: ${event.event_type}`);
}
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});
```
### Python Flask Example
```python
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
@app.route("/webhook/heygen", methods=["POST"])
def heygen_webhook():
event = request.json
# Acknowledge immediately
response = jsonify({"status": "received"})
# Process asynchronously
thread = threading.Thread(
target=process_webhook_event,
args=(event,)
)
thread.start()
return response, 200
def process_webhook_event(event):
event_type = event.get("event_type")
print(f"Received event: {event_type}")
if event_type == "avatar_video.success":
handle_video_success(event)
elif event_type == "avatar_video.fail":
handle_video_failure(event)
elif event_type == "video_translate.success":
handle_translation_success(event)
if __name__ == "__main__":
app.run(port=3000)
```
## Webhook Event Types
| Event Type | Description |
|------------|-------------|
| `avatar_video.success` | Video generation completed |
| `avatar_video.fail` | Video generation failed |
| `video_translate.success` | Translation completed |
| `video_translate.fail` | Translation failed |
| `instant_avatar.success` | Instant avatar created |
| `instant_avatar.fail` | Instant avatar creation failed |
## Event Payload Structure
### Video Success Event
```typescript
interface VideoSuccessEvent {
event_type: "avatar_video.success";
event_data: {
video_id: string;
video_url: string;
thumbnail_url: string;
duration: number;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.success",
"event_data": {
"video_id": "abc123",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"callback_id": "your_custom_id"
}
}
```
### Video Failure Event
```typescript
interface VideoFailureEvent {
event_type: "avatar_video.fail";
event_data: {
video_id: string;
error: string;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.fail",
"event_data": {
"video_id": "abc123",
"error": "Script too long for selected avatar",
"callback_id": "your_custom_id"
}
}
```
## Registering a Webhook URL
Configure your webhook URL through the HeyGen dashboard or API:
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `url` | string | ✓ | Your webhook endpoint URL |
| `events` | array | ✓ | Event types to subscribe to |
| `secret` | string | | Shared secret for signature verification |
### Via API
```bash
curl -X POST "https://api.heygen.com/v1/webhook/endpoint.add" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook/heygen",
"events": ["avatar_video.success", "avatar_video.fail"]
}'
```
### TypeScript
```typescript
interface WebhookConfig {
url: string; // Required
events: string[]; // Required
secret?: string;
}
async function registerWebhook(config: WebhookConfig): Promise<void> {
const response = await fetch("https://api.heygen.com/v1/webhook/endpoint.add", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}
```
## Using Callback IDs
Track which video triggered a webhook with callback IDs:
### Include Callback ID in Video Generation
```typescript
const videoConfig = {
video_inputs: [...],
callback_id: "order_12345", // Your custom identifier
};
```
### Handle in Webhook
```typescript
async function handleVideoSuccess(event: VideoSuccessEvent) {
const { video_id, video_url, callback_id } = event.event_data;
if (callback_id) {
// Look up your original request
const order = await getOrderByCallbackId(callback_id);
await updateOrderWithVideo(order.id, video_url);
}
}
```
## Webhook Security
### Verify Webhook Signatures
If HeyGen provides signature verification:
```typescript
import crypto from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post("/webhook/heygen", (req, res) => {
const signature = req.headers["x-heygen-signature"] as string;
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process event...
});
```
### Validate Event Origin
```typescript
function isValidHeygenEvent(event: any): boolean {
// Check required fields
if (!event.event_type || !event.event_data) {
return false;
}
// Check event type is known
const validEventTypes = [
"avatar_video.success",
"avatar_video.fail",
"video_translate.success",
"video_translate.fail",
];
return validEventTypes.includes(event.event_type);
}
```
## Handling Webhook Failures
Implement retry logic and error handling:
```typescript
async function processWebhookEvent(event: HeyGenWebhookEvent) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await handleEvent(event);
return;
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
// Store failed event for manual review
await storeFailedEvent(event);
}
```
## Webhook vs Polling Comparison
| Aspect | Webhook | Polling |
|--------|---------|---------|
| Latency | Immediate | Depends on interval |
| Efficiency | High (push) | Low (repeated requests) |
| Complexity | Requires endpoint | Simpler to implement |
| Reliability | Needs retry handling | Guaranteed delivery |
| Cost | Lower API usage | Higher API usage |
## Testing Webhooks
### Local Development with ngrok
```bash
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhook/heygen
```
### Webhook Testing Tool
```typescript
// Test webhook locally
async function simulateWebhook(event: HeyGenWebhookEvent) {
const response = await fetch("http://localhost:3000/webhook/heygen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
console.log(`Response: ${response.status}`);
}
// Simulate success event
await simulateWebhook({
event_type: "avatar_video.success",
event_data: {
video_id: "test_123",
video_url: "https://example.com/test.mp4",
thumbnail_url: "https://example.com/test.jpg",
duration: 30,
callback_id: "test_callback",
},
});
```
## Best Practices
1. **Respond quickly** - Return 200 within 5 seconds, process async
2. **Handle duplicates** - Same event may be sent multiple times
3. **Implement retries** - Handle temporary processing failures
4. **Log everything** - Store webhook payloads for debugging
5. **Use callback IDs** - Track requests through the system
6. **Secure endpoints** - Verify signatures, use HTTPS
7. **Monitor health** - Track webhook success rates
8. **Queue processing** - Use job queues for heavy processing
+820
View File
@@ -0,0 +1,820 @@
---
name: d3-viz
description: Creating interactive data visualisations using d3.js. This skill should be used when creating custom charts, graphs, network diagrams, geographic visualisations, or any complex SVG-based data visualisation that requires fine-grained control over visual elements, transitions, or interactions. Use this for bespoke visualisations beyond standard charting libraries, whether in React, Vue, Svelte, vanilla JavaScript, or any other environment.
---
# D3.js Visualisation
## Overview
This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks.
## When to use d3.js
**Use d3.js for:**
- Custom visualisations requiring unique visual encodings or layouts
- Interactive explorations with complex pan, zoom, or brush behaviours
- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams)
- Geographic visualisations with custom projections
- Visualisations requiring smooth, choreographed transitions
- Publication-quality graphics with fine-grained styling control
- Novel chart types not available in standard libraries
**Consider alternatives for:**
- 3D visualisations - use Three.js instead
## Core workflow
### 1. Set up d3.js
Import d3 at the top of your script:
```javascript
import * as d3 from 'd3';
```
Or use the CDN version (7.x):
```html
<script src="https://d3js.org/d3.v7.min.js"></script>
```
All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace.
### 2. Choose the integration pattern
**Pattern A: Direct DOM manipulation (recommended for most cases)**
Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment:
```javascript
function drawChart(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Select by ID, class, or DOM element
// Clear previous content
svg.selectAll("*").remove();
// Set up dimensions
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
// Create scales, axes, and draw visualisation
// ... d3 code here ...
}
// Call when data changes
drawChart(myData);
```
**Pattern B: Declarative rendering (for frameworks with templating)**
Use d3 for data calculations (scales, layouts) but render elements via your framework:
```javascript
function getChartElements(data) {
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, 400]);
return data.map((d, i) => ({
x: 50,
y: i * 30,
width: xScale(d.value),
height: 25
}));
}
// In React: {getChartElements(data).map((d, i) => <rect key={i} {...d} fill="steelblue" />)}
// In Vue: v-for directive over the returned array
// In vanilla JS: Create elements manually from the returned data
```
Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering.
### 3. Structure the visualisation code
Follow this standard structure in your drawing function:
```javascript
function drawVisualization(data) {
if (!data || data.length === 0) return;
const svg = d3.select('#chart'); // Or pass a selector/element
svg.selectAll("*").remove(); // Clear previous render
// 1. Define dimensions
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// 2. Create main group with margins
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 3. Create scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0]); // Note: inverted for SVG coordinates
// 4. Create and append axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
g.append("g")
.call(yAxis);
// 5. Bind data and create visual elements
g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 5)
.attr("fill", "steelblue");
}
// Call when data changes
drawVisualization(myData);
```
### 4. Implement responsive sizing
Make visualisations responsive to container size:
```javascript
function setupResponsiveChart(containerId, data) {
const container = document.getElementById(containerId);
const svg = d3.select(`#${containerId}`).append('svg');
function updateChart() {
const { width, height } = container.getBoundingClientRect();
svg.attr('width', width).attr('height', height);
// Redraw visualisation with new dimensions
drawChart(data, svg, width, height);
}
// Update on initial load
updateChart();
// Update on window resize
window.addEventListener('resize', updateChart);
// Return cleanup function
return () => window.removeEventListener('resize', updateChart);
}
// Usage:
// const cleanup = setupResponsiveChart('chart-container', myData);
// cleanup(); // Call when component unmounts or element removed
```
Or use ResizeObserver for more direct container monitoring:
```javascript
function setupResponsiveChartWithObserver(svgElement, data) {
const observer = new ResizeObserver(() => {
const { width, height } = svgElement.getBoundingClientRect();
d3.select(svgElement)
.attr('width', width)
.attr('height', height);
// Redraw visualisation
drawChart(data, d3.select(svgElement), width, height);
});
observer.observe(svgElement.parentElement);
return () => observer.disconnect();
}
```
## Common visualisation patterns
### Bar chart
```javascript
function drawBarChart(data, svgElement) {
if (!data || data.length === 0) return;
const svg = d3.select(svgElement);
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleBand()
.domain(data.map(d => d.category))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([innerHeight, 0]);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.category))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", "steelblue");
}
// Usage:
// drawBarChart(myData, document.getElementById('chart'));
```
### Line chart
```javascript
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX); // Smooth curve
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
```
### Scatter plot
```javascript
g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", d => sizeScale(d.size)) // Optional: size encoding
.attr("fill", d => colourScale(d.category)) // Optional: colour encoding
.attr("opacity", 0.7);
```
### Chord diagram
A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them:
```javascript
function drawChordDiagram(data) {
// data format: array of objects with source, target, and value
// Example: [{ source: 'A', target: 'B', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const innerRadius = Math.min(width, height) * 0.3;
const outerRadius = innerRadius + 30;
// Create matrix from data
const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));
data.forEach(d => {
const i = nodes.indexOf(d.source);
const j = nodes.indexOf(d.target);
matrix[i][j] += d.value;
matrix[j][i] += d.value;
});
// Create chord layout
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.source(d => d.source)
.target(d => d.target);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(nodes);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
const chords = chord(matrix);
// Draw ribbons
g.append("g")
.attr("fill-opacity", 0.67)
.selectAll("path")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => colourScale(nodes[d.source.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());
// Draw groups (arcs)
const group = g.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("d", arc)
.attr("fill", d => colourScale(nodes[d.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());
// Add labels
group.append("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", "0.31em")
.attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text((d, i) => nodes[i])
.style("font-size", "12px");
}
```
### Heatmap
A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories:
```javascript
function drawHeatmap(data) {
// data format: array of objects with row, column, and value
// Example: [{ row: 'A', column: 'X', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const margin = { top: 100, right: 30, bottom: 30, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Get unique rows and columns
const rows = Array.from(new Set(data.map(d => d.row)));
const columns = Array.from(new Set(data.map(d => d.column)));
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Create scales
const xScale = d3.scaleBand()
.domain(columns)
.range([0, innerWidth])
.padding(0.01);
const yScale = d3.scaleBand()
.domain(rows)
.range([0, innerHeight])
.padding(0.01);
// Colour scale for values
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([0, d3.max(data, d => d.value)]);
// Draw rectangles
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.column))
.attr("y", d => yScale(d.row))
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth())
.attr("fill", d => colourScale(d.value));
// Add x-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(columns)
.join("text")
.attr("x", d => xScale(d) + xScale.bandwidth() / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.text(d => d)
.style("font-size", "12px");
// Add y-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(rows)
.join("text")
.attr("x", -10)
.attr("y", d => yScale(d) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(d => d)
.style("font-size", "12px");
// Add colour legend
const legendWidth = 20;
const legendHeight = 200;
const legend = svg.append("g")
.attr("transform", `translate(${width - 60},${margin.top})`);
const legendScale = d3.scaleLinear()
.domain(colourScale.domain())
.range([legendHeight, 0]);
const legendAxis = d3.axisRight(legendScale)
.ticks(5);
// Draw colour gradient in legend
for (let i = 0; i < legendHeight; i++) {
legend.append("rect")
.attr("y", i)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", colourScale(legendScale.invert(i)));
}
legend.append("g")
.attr("transform", `translate(${legendWidth},0)`)
.call(legendAxis);
}
```
### Pie chart
```javascript
const pie = d3.pie()
.value(d => d.value)
.sort(null);
const arc = d3.arc()
.innerRadius(0)
.outerRadius(Math.min(width, height) / 2 - 20);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (d, i) => colourScale(i))
.attr("stroke", "white")
.attr("stroke-width", 2);
```
### Force-directed network
```javascript
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));
const link = g.selectAll("line")
.data(links)
.join("line")
.attr("stroke", "#999")
.attr("stroke-width", 1);
const node = g.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 8)
.attr("fill", "steelblue")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
```
## Adding interactivity
### Tooltips
```javascript
// Create tooltip div (outside SVG)
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "white")
.style("border", "1px solid #ddd")
.style("padding", "10px")
.style("border-radius", "4px")
.style("pointer-events", "none");
// Add to elements
circles
.on("mouseover", function(event, d) {
d3.select(this).attr("opacity", 1);
tooltip
.style("visibility", "visible")
.html(`<strong>${d.label}</strong><br/>Value: ${d.value}`);
})
.on("mousemove", function(event) {
tooltip
.style("top", (event.pageY - 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function() {
d3.select(this).attr("opacity", 0.7);
tooltip.style("visibility", "hidden");
});
```
### Zoom and pan
```javascript
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", event.transform);
});
svg.call(zoom);
```
### Click interactions
```javascript
circles
.on("click", function(event, d) {
// Handle click (dispatch event, update app state, etc.)
console.log("Clicked:", d);
// Visual feedback
d3.selectAll("circle").attr("fill", "steelblue");
d3.select(this).attr("fill", "orange");
// Optional: dispatch custom event for your framework/app to listen to
// window.dispatchEvent(new CustomEvent('chartClick', { detail: d }));
});
```
## Transitions and animations
Add smooth transitions to visual changes:
```javascript
// Basic transition
circles
.transition()
.duration(750)
.attr("r", 10);
// Chained transitions
circles
.transition()
.duration(500)
.attr("fill", "orange")
.transition()
.duration(500)
.attr("r", 15);
// Staggered transitions
circles
.transition()
.delay((d, i) => i * 50)
.duration(500)
.attr("cy", d => yScale(d.value));
// Custom easing
circles
.transition()
.duration(1000)
.ease(d3.easeBounceOut)
.attr("r", 10);
```
## Scales reference
### Quantitative scales
```javascript
// Linear scale
const xScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
// Log scale (for exponential data)
const logScale = d3.scaleLog()
.domain([1, 1000])
.range([0, 500]);
// Power scale
const powScale = d3.scalePow()
.exponent(2)
.domain([0, 100])
.range([0, 500]);
// Time scale
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 500]);
```
### Ordinal scales
```javascript
// Band scale (for bar charts)
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
// Point scale (for line/scatter categories)
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400]);
// Ordinal scale (for colours)
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
```
### Sequential scales
```javascript
// Sequential colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
// Diverging colour scale
const divScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
```
## Best practices
### Data preparation
Always validate and prepare data before visualisation:
```javascript
// Filter invalid values
const cleanData = data.filter(d => d.value != null && !isNaN(d.value));
// Sort data if order matters
const sortedData = [...data].sort((a, b) => b.value - a.value);
// Parse dates
const parsedData = data.map(d => ({
...d,
date: d3.timeParse("%Y-%m-%d")(d.date)
}));
```
### Performance optimisation
For large datasets (>1000 elements):
```javascript
// Use canvas instead of SVG for many elements
// Use quadtree for collision detection
// Simplify paths with d3.line().curve(d3.curveStep)
// Implement virtual scrolling for large lists
// Use requestAnimationFrame for custom animations
```
### Accessibility
Make visualisations accessible:
```javascript
// Add ARIA labels
svg.attr("role", "img")
.attr("aria-label", "Bar chart showing quarterly revenue");
// Add title and description
svg.append("title").text("Quarterly Revenue 2024");
svg.append("desc").text("Bar chart showing revenue growth across four quarters");
// Ensure sufficient colour contrast
// Provide keyboard navigation for interactive elements
// Include data table alternative
```
### Styling
Use consistent, professional styling:
```javascript
// Define colour palettes upfront
const colours = {
primary: '#4A90E2',
secondary: '#7B68EE',
background: '#F5F7FA',
text: '#333333',
gridLines: '#E0E0E0'
};
// Apply consistent typography
svg.selectAll("text")
.style("font-family", "Inter, sans-serif")
.style("font-size", "12px");
// Use subtle grid lines
g.selectAll(".tick line")
.attr("stroke", colours.gridLines)
.attr("stroke-dasharray", "2,2");
```
## Common issues and solutions
**Issue**: Axes not appearing
- Ensure scales have valid domains (check for NaN values)
- Verify axis is appended to correct group
- Check transform translations are correct
**Issue**: Transitions not working
- Call `.transition()` before attribute changes
- Ensure elements have unique keys for proper data binding
- Check that useEffect dependencies include all changing data
**Issue**: Responsive sizing not working
- Use ResizeObserver or window resize listener
- Update dimensions in state to trigger re-render
- Ensure SVG has width/height attributes or viewBox
**Issue**: Performance problems
- Limit number of DOM elements (consider canvas for >1000 items)
- Debounce resize handlers
- Use `.join()` instead of separate enter/update/exit selections
- Avoid unnecessary re-renders by checking dependencies
## Resources
### references/
Contains detailed reference materials:
- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples
- `scale-reference.md` - Complete guide to d3 scales with examples
- `colour-schemes.md` - D3 colour schemes and palette recommendations
### assets/
Contains boilerplate templates:
- `chart-template.js` - Starter template for basic chart
- `interactive-template.js` - Template with tooltips, zoom, and interactions
- `sample-data.json` - Example datasets for testing
These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework.
To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns.
@@ -0,0 +1,106 @@
import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
function BasicChart({ data }) {
const svgRef = useRef();
useEffect(() => {
if (!data || data.length === 0) return;
// Select SVG element
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous content
// Define dimensions and margins
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Create main group with margins
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Create scales
const xScale = d3.scaleBand()
.domain(data.map(d => d.label))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([innerHeight, 0])
.nice();
// Create and append axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// Bind data and create visual elements (bars in this example)
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.label))
.attr("y", d => yScale(d.value))
.attr("width", xScale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", "steelblue");
// Optional: Add axis labels
g.append("text")
.attr("class", "axis-label")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + margin.bottom - 5)
.attr("text-anchor", "middle")
.text("Category");
g.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -margin.left + 15)
.attr("text-anchor", "middle")
.text("Value");
}, [data]);
return (
<div className="chart-container">
<svg
ref={svgRef}
width="800"
height="400"
style={{ border: '1px solid #ddd' }}
/>
</div>
);
}
// Example usage
export default function App() {
const sampleData = [
{ label: 'A', value: 30 },
{ label: 'B', value: 80 },
{ label: 'C', value: 45 },
{ label: 'D', value: 60 },
{ label: 'E', value: 20 },
{ label: 'F', value: 90 }
];
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Basic D3.js Chart</h1>
<BasicChart data={sampleData} />
</div>
);
}
@@ -0,0 +1,227 @@
import { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
function InteractiveChart({ data }) {
const svgRef = useRef();
const tooltipRef = useRef();
const [selectedPoint, setSelectedPoint] = useState(null);
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
// Dimensions
const width = 800;
const height = 500;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Create main group
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, innerWidth])
.nice();
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0])
.nice();
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.size || 10)])
.range([3, 20]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
// Add zoom behaviour
const zoom = d3.zoom()
.scaleExtent([0.5, 10])
.on("zoom", (event) => {
g.attr("transform", `translate(${margin.left + event.transform.x},${margin.top + event.transform.y}) scale(${event.transform.k})`);
});
svg.call(zoom);
// Axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis);
const yAxisGroup = g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// Grid lines
g.append("g")
.attr("class", "grid")
.attr("opacity", 0.1)
.call(d3.axisLeft(yScale)
.tickSize(-innerWidth)
.tickFormat(""));
g.append("g")
.attr("class", "grid")
.attr("opacity", 0.1)
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale)
.tickSize(-innerHeight)
.tickFormat(""));
// Tooltip
const tooltip = d3.select(tooltipRef.current);
// Data points
const circles = g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", d => sizeScale(d.size || 10))
.attr("fill", d => colourScale(d.category || 'default'))
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.attr("opacity", 0.7)
.style("cursor", "pointer");
// Hover interactions
circles
.on("mouseover", function(event, d) {
// Enlarge circle
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 1)
.attr("stroke-width", 3);
// Show tooltip
tooltip
.style("display", "block")
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px")
.html(`
<strong>${d.label || 'Point'}</strong><br/>
X: ${d.x.toFixed(2)}<br/>
Y: ${d.y.toFixed(2)}<br/>
${d.category ? `Category: ${d.category}<br/>` : ''}
${d.size ? `Size: ${d.size.toFixed(2)}` : ''}
`);
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
// Restore circle
d3.select(this)
.transition()
.duration(200)
.attr("opacity", 0.7)
.attr("stroke-width", 2);
// Hide tooltip
tooltip.style("display", "none");
})
.on("click", function(event, d) {
// Highlight selected point
circles.attr("stroke", "#fff").attr("stroke-width", 2);
d3.select(this)
.attr("stroke", "#000")
.attr("stroke-width", 3);
setSelectedPoint(d);
});
// Add transition on initial render
circles
.attr("r", 0)
.transition()
.duration(800)
.delay((d, i) => i * 20)
.attr("r", d => sizeScale(d.size || 10));
// Axis labels
g.append("text")
.attr("class", "axis-label")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + margin.bottom - 5)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("X Axis");
g.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -margin.left + 15)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Y Axis");
}, [data]);
return (
<div className="relative">
<svg
ref={svgRef}
width="800"
height="500"
style={{ border: '1px solid #ddd', cursor: 'grab' }}
/>
<div
ref={tooltipRef}
style={{
position: 'absolute',
display: 'none',
padding: '10px',
background: 'white',
border: '1px solid #ddd',
borderRadius: '4px',
pointerEvents: 'none',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
fontSize: '13px',
zIndex: 1000
}}
/>
{selectedPoint && (
<div className="mt-4 p-4 bg-blue-50 rounded border border-blue-200">
<h3 className="font-bold mb-2">Selected Point</h3>
<pre className="text-sm">{JSON.stringify(selectedPoint, null, 2)}</pre>
</div>
)}
</div>
);
}
// Example usage
export default function App() {
const sampleData = Array.from({ length: 50 }, (_, i) => ({
id: i,
label: `Point ${i + 1}`,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 30 + 5,
category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)]
}));
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-2">Interactive D3.js Chart</h1>
<p className="text-gray-600 mb-4">
Hover over points for details. Click to select. Scroll to zoom. Drag to pan.
</p>
<InteractiveChart data={sampleData} />
</div>
);
}
@@ -0,0 +1,115 @@
{
"timeSeries": [
{ "date": "2024-01-01", "value": 120, "category": "A" },
{ "date": "2024-02-01", "value": 135, "category": "A" },
{ "date": "2024-03-01", "value": 128, "category": "A" },
{ "date": "2024-04-01", "value": 145, "category": "A" },
{ "date": "2024-05-01", "value": 152, "category": "A" },
{ "date": "2024-06-01", "value": 168, "category": "A" },
{ "date": "2024-07-01", "value": 175, "category": "A" },
{ "date": "2024-08-01", "value": 182, "category": "A" },
{ "date": "2024-09-01", "value": 190, "category": "A" },
{ "date": "2024-10-01", "value": 185, "category": "A" },
{ "date": "2024-11-01", "value": 195, "category": "A" },
{ "date": "2024-12-01", "value": 210, "category": "A" }
],
"categorical": [
{ "label": "Product A", "value": 450, "category": "Electronics" },
{ "label": "Product B", "value": 320, "category": "Electronics" },
{ "label": "Product C", "value": 580, "category": "Clothing" },
{ "label": "Product D", "value": 290, "category": "Clothing" },
{ "label": "Product E", "value": 410, "category": "Food" },
{ "label": "Product F", "value": 370, "category": "Food" }
],
"scatterData": [
{ "x": 12, "y": 45, "size": 25, "category": "Group A", "label": "Point 1" },
{ "x": 25, "y": 62, "size": 35, "category": "Group A", "label": "Point 2" },
{ "x": 38, "y": 55, "size": 20, "category": "Group B", "label": "Point 3" },
{ "x": 45, "y": 78, "size": 40, "category": "Group B", "label": "Point 4" },
{ "x": 52, "y": 68, "size": 30, "category": "Group C", "label": "Point 5" },
{ "x": 65, "y": 85, "size": 45, "category": "Group C", "label": "Point 6" },
{ "x": 72, "y": 72, "size": 28, "category": "Group A", "label": "Point 7" },
{ "x": 85, "y": 92, "size": 50, "category": "Group B", "label": "Point 8" }
],
"hierarchical": {
"name": "Root",
"children": [
{
"name": "Category 1",
"children": [
{ "name": "Item 1.1", "value": 100 },
{ "name": "Item 1.2", "value": 150 },
{ "name": "Item 1.3", "value": 80 }
]
},
{
"name": "Category 2",
"children": [
{ "name": "Item 2.1", "value": 200 },
{ "name": "Item 2.2", "value": 120 },
{ "name": "Item 2.3", "value": 90 }
]
},
{
"name": "Category 3",
"children": [
{ "name": "Item 3.1", "value": 180 },
{ "name": "Item 3.2", "value": 140 }
]
}
]
},
"network": {
"nodes": [
{ "id": "A", "group": 1 },
{ "id": "B", "group": 1 },
{ "id": "C", "group": 1 },
{ "id": "D", "group": 2 },
{ "id": "E", "group": 2 },
{ "id": "F", "group": 3 },
{ "id": "G", "group": 3 },
{ "id": "H", "group": 3 }
],
"links": [
{ "source": "A", "target": "B", "value": 1 },
{ "source": "A", "target": "C", "value": 2 },
{ "source": "B", "target": "C", "value": 1 },
{ "source": "C", "target": "D", "value": 3 },
{ "source": "D", "target": "E", "value": 2 },
{ "source": "E", "target": "F", "value": 1 },
{ "source": "F", "target": "G", "value": 2 },
{ "source": "F", "target": "H", "value": 1 },
{ "source": "G", "target": "H", "value": 1 }
]
},
"stackedData": [
{ "group": "Q1", "seriesA": 30, "seriesB": 40, "seriesC": 25 },
{ "group": "Q2", "seriesA": 45, "seriesB": 35, "seriesC": 30 },
{ "group": "Q3", "seriesA": 40, "seriesB": 50, "seriesC": 35 },
{ "group": "Q4", "seriesA": 55, "seriesB": 45, "seriesC": 40 }
],
"geographicPoints": [
{ "city": "London", "latitude": 51.5074, "longitude": -0.1278, "value": 8900000 },
{ "city": "Paris", "latitude": 48.8566, "longitude": 2.3522, "value": 2140000 },
{ "city": "Berlin", "latitude": 52.5200, "longitude": 13.4050, "value": 3645000 },
{ "city": "Madrid", "latitude": 40.4168, "longitude": -3.7038, "value": 3223000 },
{ "city": "Rome", "latitude": 41.9028, "longitude": 12.4964, "value": 2873000 }
],
"divergingData": [
{ "category": "Item A", "value": -15 },
{ "category": "Item B", "value": 8 },
{ "category": "Item C", "value": -22 },
{ "category": "Item D", "value": 18 },
{ "category": "Item E", "value": -5 },
{ "category": "Item F", "value": 25 },
{ "category": "Item G", "value": -12 },
{ "category": "Item H", "value": 14 }
]
}
@@ -0,0 +1,564 @@
# D3.js Colour Schemes and Palette Recommendations
Comprehensive guide to colour selection in data visualisation with d3.js.
## Built-in categorical colour schemes
### Category10 (default)
```javascript
d3.schemeCategory10
// ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
// '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
```
**Characteristics:**
- 10 distinct colours
- Good colour-blind accessibility
- Default choice for most categorical data
- Balanced saturation and brightness
**Use cases:** General purpose categorical encoding, legend items, multiple data series
### Tableau10
```javascript
d3.schemeTableau10
```
**Characteristics:**
- 10 colours optimised for data visualisation
- Professional appearance
- Excellent distinguishability
**Use cases:** Business dashboards, professional reports, presentations
### Accent
```javascript
d3.schemeAccent
// 8 colours with high saturation
```
**Characteristics:**
- Bright, vibrant colours
- High contrast
- Modern aesthetic
**Use cases:** Highlighting important categories, modern web applications
### Dark2
```javascript
d3.schemeDark2
// 8 darker, muted colours
```
**Characteristics:**
- Subdued palette
- Professional appearance
- Good for dark backgrounds
**Use cases:** Dark mode visualisations, professional contexts
### Paired
```javascript
d3.schemePaired
// 12 colours in pairs of similar hues
```
**Characteristics:**
- Pairs of light and dark variants
- Useful for nested categories
- 12 distinct colours
**Use cases:** Grouped bar charts, hierarchical categories, before/after comparisons
### Pastel1 & Pastel2
```javascript
d3.schemePastel1 // 9 colours
d3.schemePastel2 // 8 colours
```
**Characteristics:**
- Soft, low-saturation colours
- Gentle appearance
- Good for large areas
**Use cases:** Background colours, subtle categorisation, calming visualisations
### Set1, Set2, Set3
```javascript
d3.schemeSet1 // 9 colours - vivid
d3.schemeSet2 // 8 colours - muted
d3.schemeSet3 // 12 colours - pastel
```
**Characteristics:**
- Set1: High saturation, maximum distinction
- Set2: Professional, balanced
- Set3: Subtle, many categories
**Use cases:** Varied based on visual hierarchy needs
## Sequential colour schemes
Sequential schemes map continuous data from low to high values using a single hue or gradient.
### Single-hue sequential
**Blues:**
```javascript
d3.interpolateBlues
d3.schemeBlues[9] // 9-step discrete version
```
**Other single-hue options:**
- `d3.interpolateGreens` / `d3.schemeGreens`
- `d3.interpolateOranges` / `d3.schemeOranges`
- `d3.interpolatePurples` / `d3.schemePurples`
- `d3.interpolateReds` / `d3.schemeReds`
- `d3.interpolateGreys` / `d3.schemeGreys`
**Use cases:**
- Simple heat maps
- Choropleth maps
- Density plots
- Single-metric visualisations
### Multi-hue sequential
**Viridis (recommended):**
```javascript
d3.interpolateViridis
```
**Characteristics:**
- Perceptually uniform
- Colour-blind friendly
- Print-safe
- No visual dead zones
- Monotonically increasing perceived lightness
**Other perceptually-uniform options:**
- `d3.interpolatePlasma` - Purple to yellow
- `d3.interpolateInferno` - Black to white through red/orange
- `d3.interpolateMagma` - Black to white through purple
- `d3.interpolateCividis` - Colour-blind optimised
**Colour-blind accessible:**
```javascript
d3.interpolateTurbo // Rainbow-like but perceptually uniform
d3.interpolateCool // Cyan to magenta
d3.interpolateWarm // Orange to yellow
```
**Use cases:**
- Scientific visualisation
- Medical imaging
- Any high-precision data visualisation
- Accessible visualisations
### Traditional sequential
**Yellow-Orange-Red:**
```javascript
d3.interpolateYlOrRd
d3.schemeYlOrRd[9]
```
**Yellow-Green-Blue:**
```javascript
d3.interpolateYlGnBu
d3.schemeYlGnBu[9]
```
**Other multi-hue:**
- `d3.interpolateBuGn` - Blue to green
- `d3.interpolateBuPu` - Blue to purple
- `d3.interpolateGnBu` - Green to blue
- `d3.interpolateOrRd` - Orange to red
- `d3.interpolatePuBu` - Purple to blue
- `d3.interpolatePuBuGn` - Purple to blue-green
- `d3.interpolatePuRd` - Purple to red
- `d3.interpolateRdPu` - Red to purple
- `d3.interpolateYlGn` - Yellow to green
- `d3.interpolateYlOrBr` - Yellow to orange-brown
**Use cases:** Traditional data visualisation, familiar colour associations (temperature, vegetation, water)
## Diverging colour schemes
Diverging schemes highlight deviations from a central value using two distinct hues.
### Red-Blue (temperature)
```javascript
d3.interpolateRdBu
d3.schemeRdBu[11]
```
**Characteristics:**
- Intuitive temperature metaphor
- Strong contrast
- Clear positive/negative distinction
**Use cases:** Temperature, profit/loss, above/below average, correlation
### Red-Yellow-Blue
```javascript
d3.interpolateRdYlBu
d3.schemeRdYlBu[11]
```
**Characteristics:**
- Three-colour gradient
- Softer transition through yellow
- More visual steps
**Use cases:** When extreme values need emphasis and middle needs visibility
### Other diverging schemes
**Traffic light:**
```javascript
d3.interpolateRdYlGn // Red (bad) to green (good)
```
**Spectral (rainbow):**
```javascript
d3.interpolateSpectral // Full spectrum
```
**Other options:**
- `d3.interpolateBrBG` - Brown to blue-green
- `d3.interpolatePiYG` - Pink to yellow-green
- `d3.interpolatePRGn` - Purple to green
- `d3.interpolatePuOr` - Purple to orange
- `d3.interpolateRdGy` - Red to grey
**Use cases:** Choose based on semantic meaning and accessibility needs
## Colour-blind friendly palettes
### General guidelines
1. **Avoid red-green combinations** (most common colour blindness)
2. **Use blue-orange diverging** instead of red-green
3. **Add texture or patterns** as redundant encoding
4. **Test with simulation tools**
### Recommended colour-blind safe schemes
**Categorical:**
```javascript
// Okabe-Ito palette (colour-blind safe)
const okabePalette = [
'#E69F00', // Orange
'#56B4E9', // Sky blue
'#009E73', // Bluish green
'#F0E442', // Yellow
'#0072B2', // Blue
'#D55E00', // Vermillion
'#CC79A7', // Reddish purple
'#000000' // Black
];
const colourScale = d3.scaleOrdinal()
.domain(categories)
.range(okabePalette);
```
**Sequential:**
```javascript
// Use Viridis, Cividis, or Blues
d3.interpolateViridis // Best overall
d3.interpolateCividis // Optimised for CVD
d3.interpolateBlues // Simple, safe
```
**Diverging:**
```javascript
// Use blue-orange instead of red-green
d3.interpolateBrBG
d3.interpolatePuOr
```
## Custom colour palettes
### Creating custom sequential
```javascript
const customSequential = d3.scaleLinear()
.domain([0, 100])
.range(['#e8f4f8', '#006d9c']) // Light to dark blue
.interpolate(d3.interpolateLab); // Perceptually uniform
```
### Creating custom diverging
```javascript
const customDiverging = d3.scaleLinear()
.domain([0, 50, 100])
.range(['#ca0020', '#f7f7f7', '#0571b0']) // Red, grey, blue
.interpolate(d3.interpolateLab);
```
### Creating custom categorical
```javascript
// Brand colours
const brandPalette = [
'#FF6B6B', // Primary red
'#4ECDC4', // Secondary teal
'#45B7D1', // Tertiary blue
'#FFA07A', // Accent coral
'#98D8C8' // Accent mint
];
const colourScale = d3.scaleOrdinal()
.domain(categories)
.range(brandPalette);
```
## Semantic colour associations
### Universal colour meanings
**Red:**
- Danger, error, negative
- High temperature
- Debt, loss
**Green:**
- Success, positive
- Growth, vegetation
- Profit, gain
**Blue:**
- Trust, calm
- Water, cold
- Information, neutral
**Yellow/Orange:**
- Warning, caution
- Energy, warmth
- Attention
**Grey:**
- Neutral, inactive
- Missing data
- Background
### Context-specific palettes
**Financial:**
```javascript
const financialColours = {
profit: '#27ae60',
loss: '#e74c3c',
neutral: '#95a5a6',
highlight: '#3498db'
};
```
**Temperature:**
```javascript
const temperatureScale = d3.scaleSequential(d3.interpolateRdYlBu)
.domain([40, -10]); // Hot to cold (reversed)
```
**Traffic/Status:**
```javascript
const statusColours = {
success: '#27ae60',
warning: '#f39c12',
error: '#e74c3c',
info: '#3498db',
neutral: '#95a5a6'
};
```
## Accessibility best practices
### Contrast ratios
Ensure sufficient contrast between colours and backgrounds:
```javascript
// Good contrast example
const highContrast = {
background: '#ffffff',
text: '#2c3e50',
primary: '#3498db',
secondary: '#e74c3c'
};
```
**WCAG guidelines:**
- Normal text: 4.5:1 minimum
- Large text: 3:1 minimum
- UI components: 3:1 minimum
### Redundant encoding
Never rely solely on colour to convey information:
```javascript
// Add patterns or shapes
const symbols = ['circle', 'square', 'triangle', 'diamond'];
// Add text labels
// Use line styles (solid, dashed, dotted)
// Use size encoding
```
### Testing
Test visualisations for colour blindness:
- Chrome DevTools (Rendering > Emulate vision deficiencies)
- Colour Oracle (free desktop application)
- Coblis (online simulator)
## Professional colour recommendations
### Data journalism
```javascript
// Guardian style
const guardianPalette = [
'#005689', // Guardian blue
'#c70000', // Guardian red
'#7d0068', // Guardian pink
'#951c75', // Guardian purple
];
// FT style
const ftPalette = [
'#0f5499', // FT blue
'#990f3d', // FT red
'#593380', // FT purple
'#262a33', // FT black
];
```
### Academic/Scientific
```javascript
// Nature journal style
const naturePalette = [
'#0071b2', // Blue
'#d55e00', // Vermillion
'#009e73', // Green
'#f0e442', // Yellow
];
// Use Viridis for continuous data
const scientificScale = d3.scaleSequential(d3.interpolateViridis);
```
### Corporate/Business
```javascript
// Professional, conservative
const corporatePalette = [
'#003f5c', // Dark blue
'#58508d', // Purple
'#bc5090', // Magenta
'#ff6361', // Coral
'#ffa600' // Orange
];
```
## Dynamic colour selection
### Based on data range
```javascript
function selectColourScheme(data) {
const extent = d3.extent(data);
const hasNegative = extent[0] < 0;
const hasPositive = extent[1] > 0;
if (hasNegative && hasPositive) {
// Diverging: data crosses zero
return d3.scaleSequentialSymlog(d3.interpolateRdBu)
.domain([extent[0], 0, extent[1]]);
} else {
// Sequential: all positive or all negative
return d3.scaleSequential(d3.interpolateViridis)
.domain(extent);
}
}
```
### Based on category count
```javascript
function selectCategoricalScheme(categories) {
const n = categories.length;
if (n <= 10) {
return d3.scaleOrdinal(d3.schemeTableau10);
} else if (n <= 12) {
return d3.scaleOrdinal(d3.schemePaired);
} else {
// For many categories, use sequential with quantize
return d3.scaleQuantize()
.domain([0, n - 1])
.range(d3.quantize(d3.interpolateRainbow, n));
}
}
```
## Common colour mistakes to avoid
1. **Rainbow gradients for sequential data**
- Problem: Not perceptually uniform, hard to read
- Solution: Use Viridis, Blues, or other uniform schemes
2. **Red-green for diverging (colour blindness)**
- Problem: 8% of males can't distinguish
- Solution: Use blue-orange or purple-green
3. **Too many categorical colours**
- Problem: Hard to distinguish and remember
- Solution: Limit to 5-8 categories, use grouping
4. **Insufficient contrast**
- Problem: Poor readability
- Solution: Test contrast ratios, use darker colours on light backgrounds
5. **Culturally inconsistent colours**
- Problem: Confusing semantic meaning
- Solution: Research colour associations for target audience
6. **Inverted temperature scales**
- Problem: Counterintuitive (red = cold)
- Solution: Red/orange = hot, blue = cold
## Quick reference guide
**Need to show...**
- **Categories (≤10):** `d3.schemeCategory10` or `d3.schemeTableau10`
- **Categories (>10):** `d3.schemePaired` or group categories
- **Sequential (general):** `d3.interpolateViridis`
- **Sequential (scientific):** `d3.interpolateViridis` or `d3.interpolatePlasma`
- **Sequential (temperature):** `d3.interpolateRdYlBu` (inverted)
- **Diverging (zero):** `d3.interpolateRdBu` or `d3.interpolateBrBG`
- **Diverging (good/bad):** `d3.interpolateRdYlGn` (inverted)
- **Colour-blind safe (categorical):** Okabe-Ito palette (shown above)
- **Colour-blind safe (sequential):** `d3.interpolateCividis` or `d3.interpolateBlues`
- **Colour-blind safe (diverging):** `d3.interpolatePuOr` or `d3.interpolateBrBG`
**Always remember:**
1. Test for colour-blindness
2. Ensure sufficient contrast
3. Use semantic colours appropriately
4. Add redundant encoding (patterns, labels)
5. Keep it simple (fewer colours = clearer visualisation)
@@ -0,0 +1,869 @@
# D3.js Visualisation Patterns
This reference provides detailed code patterns for common d3.js visualisation types.
## Hierarchical visualisations
### Tree diagram
```javascript
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const tree = d3.tree().size([height - 100, width - 200]);
const root = d3.hierarchy(data);
tree(root);
const g = svg.append("g")
.attr("transform", "translate(100,50)");
// Links
g.selectAll("path")
.data(root.links())
.join("path")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x))
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-width", 2);
// Nodes
const node = g.selectAll("g")
.data(root.descendants())
.join("g")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("circle")
.attr("r", 6)
.attr("fill", d => d.children ? "#555" : "#999");
node.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.children ? -8 : 8)
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name)
.style("font-size", "12px");
}, [data]);
```
### Treemap
```javascript
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
d3.treemap()
.size([width, height])
.padding(2)
.round(true)(root);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const cell = svg.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);
cell.append("rect")
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => colourScale(d.parent.data.name))
.attr("stroke", "white")
.attr("stroke-width", 2);
cell.append("text")
.attr("x", 4)
.attr("y", 16)
.text(d => d.data.name)
.style("font-size", "12px")
.style("fill", "white");
}, [data]);
```
### Sunburst diagram
```javascript
useEffect(() => {
if (!data) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const radius = Math.min(width, height) / 2;
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
const partition = d3.partition()
.size([2 * Math.PI, radius]);
partition(root);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
g.selectAll("path")
.data(root.descendants())
.join("path")
.attr("d", arc)
.attr("fill", d => colourScale(d.depth))
.attr("stroke", "white")
.attr("stroke-width", 1);
}, [data]);
```
### Chord diagram
```javascript
function drawChordDiagram(data) {
// data format: array of objects with source, target, and value
// Example: [{ source: 'A', target: 'B', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 600;
const height = 600;
const innerRadius = Math.min(width, height) * 0.3;
const outerRadius = innerRadius + 30;
// Create matrix from data
const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])));
const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0));
data.forEach(d => {
const i = nodes.indexOf(d.source);
const j = nodes.indexOf(d.target);
matrix[i][j] += d.value;
matrix[j][i] += d.value;
});
// Create chord layout
const chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.source(d => d.source)
.target(d => d.target);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(nodes);
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);
const chords = chord(matrix);
// Draw ribbons
g.append("g")
.attr("fill-opacity", 0.67)
.selectAll("path")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => colourScale(nodes[d.source.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker());
// Draw groups (arcs)
const group = g.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("d", arc)
.attr("fill", d => colourScale(nodes[d.index]))
.attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker());
// Add labels
group.append("text")
.each(d => { d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("dy", "0.31em")
.attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text((d, i) => nodes[i])
.style("font-size", "12px");
}
// Data format example:
// const data = [
// { source: 'Category A', target: 'Category B', value: 100 },
// { source: 'Category A', target: 'Category C', value: 50 },
// { source: 'Category B', target: 'Category C', value: 75 }
// ];
// drawChordDiagram(data);
```
## Advanced chart types
### Heatmap
```javascript
function drawHeatmap(data) {
// data format: array of objects with row, column, and value
// Example: [{ row: 'A', column: 'X', value: 10 }, ...]
if (!data || data.length === 0) return;
const svg = d3.select('#chart');
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const margin = { top: 100, right: 30, bottom: 30, left: 100 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Get unique rows and columns
const rows = Array.from(new Set(data.map(d => d.row)));
const columns = Array.from(new Set(data.map(d => d.column)));
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Create scales
const xScale = d3.scaleBand()
.domain(columns)
.range([0, innerWidth])
.padding(0.01);
const yScale = d3.scaleBand()
.domain(rows)
.range([0, innerHeight])
.padding(0.01);
// Colour scale for values (sequential from light to dark red)
const colourScale = d3.scaleSequential(d3.interpolateYlOrRd)
.domain([0, d3.max(data, d => d.value)]);
// Draw rectangles
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => xScale(d.column))
.attr("y", d => yScale(d.row))
.attr("width", xScale.bandwidth())
.attr("height", yScale.bandwidth())
.attr("fill", d => colourScale(d.value));
// Add x-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(columns)
.join("text")
.attr("x", d => xScale(d) + xScale.bandwidth() / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.text(d => d)
.style("font-size", "12px");
// Add y-axis labels
svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.selectAll("text")
.data(rows)
.join("text")
.attr("x", -10)
.attr("y", d => yScale(d) + yScale.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(d => d)
.style("font-size", "12px");
// Add colour legend
const legendWidth = 20;
const legendHeight = 200;
const legend = svg.append("g")
.attr("transform", `translate(${width - 60},${margin.top})`);
const legendScale = d3.scaleLinear()
.domain(colourScale.domain())
.range([legendHeight, 0]);
const legendAxis = d3.axisRight(legendScale).ticks(5);
// Draw colour gradient in legend
for (let i = 0; i < legendHeight; i++) {
legend.append("rect")
.attr("y", i)
.attr("width", legendWidth)
.attr("height", 1)
.attr("fill", colourScale(legendScale.invert(i)));
}
legend.append("g")
.attr("transform", `translate(${legendWidth},0)`)
.call(legendAxis);
}
// Data format example:
// const data = [
// { row: 'Monday', column: 'Morning', value: 42 },
// { row: 'Monday', column: 'Afternoon', value: 78 },
// { row: 'Tuesday', column: 'Morning', value: 65 },
// { row: 'Tuesday', column: 'Afternoon', value: 55 }
// ];
// drawHeatmap(data);
```
### Area chart with gradient
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Define gradient
const defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "areaGradient")
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "0%")
.attr("y2", "100%");
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "steelblue")
.attr("stop-opacity", 0.8);
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "steelblue")
.attr("stop-opacity", 0.1);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([innerHeight, 0]);
const area = d3.area()
.x(d => xScale(d.date))
.y0(innerHeight)
.y1(d => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "url(#areaGradient)")
.attr("d", area);
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2)
.attr("d", line);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);
```
### Stacked bar chart
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const categories = Object.keys(data[0]).filter(k => k !== 'group');
const stackedData = d3.stack().keys(categories)(data);
const xScale = d3.scaleBand()
.domain(data.map(d => d.group))
.range([0, innerWidth])
.padding(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])])
.range([innerHeight, 0]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
g.selectAll("g")
.data(stackedData)
.join("g")
.attr("fill", (d, i) => colourScale(i))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", d => xScale(d.data.group))
.attr("y", d => yScale(d[1]))
.attr("height", d => yScale(d[0]) - yScale(d[1]))
.attr("width", xScale.bandwidth());
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);
```
### Grouped bar chart
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const categories = Object.keys(data[0]).filter(k => k !== 'group');
const x0Scale = d3.scaleBand()
.domain(data.map(d => d.group))
.range([0, innerWidth])
.padding(0.1);
const x1Scale = d3.scaleBand()
.domain(categories)
.range([0, x0Scale.bandwidth()])
.padding(0.05);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => Math.max(...categories.map(c => d[c])))])
.range([innerHeight, 0]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
const group = g.selectAll("g")
.data(data)
.join("g")
.attr("transform", d => `translate(${x0Scale(d.group)},0)`);
group.selectAll("rect")
.data(d => categories.map(key => ({ key, value: d[key] })))
.join("rect")
.attr("x", d => x1Scale(d.key))
.attr("y", d => yScale(d.value))
.attr("width", x1Scale.bandwidth())
.attr("height", d => innerHeight - yScale(d.value))
.attr("fill", d => colourScale(d.key));
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x0Scale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);
```
### Bubble chart
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0]);
const sizeScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.size)])
.range([0, 50]);
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", d => sizeScale(d.size))
.attr("fill", d => colourScale(d.category))
.attr("opacity", 0.6)
.attr("stroke", "white")
.attr("stroke-width", 2);
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale));
g.append("g")
.call(d3.axisLeft(yScale));
}, [data]);
```
## Geographic visualisations
### Basic map with points
```javascript
useEffect(() => {
if (!geoData || !pointData) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const projection = d3.geoMercator()
.fitSize([width, height], geoData);
const pathGenerator = d3.geoPath().projection(projection);
// Draw map
svg.selectAll("path")
.data(geoData.features)
.join("path")
.attr("d", pathGenerator)
.attr("fill", "#e0e0e0")
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
// Draw points
svg.selectAll("circle")
.data(pointData)
.join("circle")
.attr("cx", d => projection([d.longitude, d.latitude])[0])
.attr("cy", d => projection([d.longitude, d.latitude])[1])
.attr("r", 5)
.attr("fill", "steelblue")
.attr("opacity", 0.7);
}, [geoData, pointData]);
```
### Choropleth map
```javascript
useEffect(() => {
if (!geoData || !valueData) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 600;
const projection = d3.geoMercator()
.fitSize([width, height], geoData);
const pathGenerator = d3.geoPath().projection(projection);
// Create value lookup
const valueLookup = new Map(valueData.map(d => [d.id, d.value]));
// Colour scale
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, d3.max(valueData, d => d.value)]);
svg.selectAll("path")
.data(geoData.features)
.join("path")
.attr("d", pathGenerator)
.attr("fill", d => {
const value = valueLookup.get(d.id);
return value ? colourScale(value) : "#e0e0e0";
})
.attr("stroke", "#999")
.attr("stroke-width", 0.5);
}, [geoData, valueData]);
```
## Advanced interactions
### Brush and zoom
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const width = 800;
const height = 400;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([innerHeight, 0]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const circles = g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 5)
.attr("fill", "steelblue");
// Add brush
const brush = d3.brush()
.extent([[0, 0], [innerWidth, innerHeight]])
.on("start brush", (event) => {
if (!event.selection) return;
const [[x0, y0], [x1, y1]] = event.selection;
circles.attr("fill", d => {
const cx = xScale(d.x);
const cy = yScale(d.y);
return (cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1)
? "orange"
: "steelblue";
});
});
g.append("g")
.attr("class", "brush")
.call(brush);
}, [data]);
```
### Linked brushing between charts
```javascript
function LinkedCharts({ data }) {
const [selectedPoints, setSelectedPoints] = useState(new Set());
const svg1Ref = useRef();
const svg2Ref = useRef();
useEffect(() => {
// Chart 1: Scatter plot
const svg1 = d3.select(svg1Ref.current);
svg1.selectAll("*").remove();
// ... create first chart ...
const circles1 = svg1.selectAll("circle")
.data(data)
.join("circle")
.attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue");
// Chart 2: Bar chart
const svg2 = d3.select(svg2Ref.current);
svg2.selectAll("*").remove();
// ... create second chart ...
const bars = svg2.selectAll("rect")
.data(data)
.join("rect")
.attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue");
// Add brush to first chart
const brush = d3.brush()
.on("start brush end", (event) => {
if (!event.selection) {
setSelectedPoints(new Set());
return;
}
const [[x0, y0], [x1, y1]] = event.selection;
const selected = new Set();
data.forEach(d => {
const x = xScale(d.x);
const y = yScale(d.y);
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
selected.add(d.id);
}
});
setSelectedPoints(selected);
});
svg1.append("g").call(brush);
}, [data, selectedPoints]);
return (
<div>
<svg ref={svg1Ref} width="400" height="300" />
<svg ref={svg2Ref} width="400" height="300" />
</div>
);
}
```
## Animation patterns
### Enter, update, exit with transitions
```javascript
useEffect(() => {
if (!data || data.length === 0) return;
const svg = d3.select(svgRef.current);
const circles = svg.selectAll("circle")
.data(data, d => d.id); // Key function for object constancy
// EXIT: Remove old elements
circles.exit()
.transition()
.duration(500)
.attr("r", 0)
.remove();
// UPDATE: Modify existing elements
circles
.transition()
.duration(500)
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("fill", "steelblue");
// ENTER: Add new elements
circles.enter()
.append("circle")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 0)
.attr("fill", "steelblue")
.transition()
.duration(500)
.attr("r", 5);
}, [data]);
```
### Path morphing
```javascript
useEffect(() => {
if (!data1 || !data2) return;
const svg = d3.select(svgRef.current);
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX);
const path = svg.select("path");
// Morph from data1 to data2
path
.datum(data1)
.attr("d", line)
.transition()
.duration(1000)
.attrTween("d", function() {
const previous = d3.select(this).attr("d");
const current = line(data2);
return d3.interpolatePath(previous, current);
});
}, [data1, data2]);
```
@@ -0,0 +1,509 @@
# D3.js Scale Reference
Comprehensive guide to all d3 scale types with examples and use cases.
## Continuous scales
### Linear scale
Maps continuous input domain to continuous output range with linear interpolation.
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
scale(50); // Returns 250
scale(0); // Returns 0
scale(100); // Returns 500
// Invert scale (get input from output)
scale.invert(250); // Returns 50
```
**Use cases:**
- Most common scale for quantitative data
- Axes, bar lengths, position encoding
- Temperature, prices, counts, measurements
**Methods:**
- `.domain([min, max])` - Set input domain
- `.range([min, max])` - Set output range
- `.invert(value)` - Get domain value from range value
- `.clamp(true)` - Restrict output to range bounds
- `.nice()` - Extend domain to nice round values
### Power scale
Maps continuous input to continuous output with exponential transformation.
```javascript
const sqrtScale = d3.scalePow()
.exponent(0.5) // Square root
.domain([0, 100])
.range([0, 500]);
const squareScale = d3.scalePow()
.exponent(2) // Square
.domain([0, 100])
.range([0, 500]);
// Shorthand for square root
const sqrtScale2 = d3.scaleSqrt()
.domain([0, 100])
.range([0, 500]);
```
**Use cases:**
- Perceptual scaling (human perception is non-linear)
- Area encoding (use square root to map values to circle radii)
- Emphasising differences in small or large values
### Logarithmic scale
Maps continuous input to continuous output with logarithmic transformation.
```javascript
const logScale = d3.scaleLog()
.domain([1, 1000]) // Must be positive
.range([0, 500]);
logScale(1); // Returns 0
logScale(10); // Returns ~167
logScale(100); // Returns ~333
logScale(1000); // Returns 500
```
**Use cases:**
- Data spanning multiple orders of magnitude
- Population, GDP, wealth distributions
- Logarithmic axes
- Exponential growth visualisations
**Important:** Domain values must be strictly positive (>0).
### Time scale
Specialised linear scale for temporal data.
```javascript
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)])
.range([0, 800]);
timeScale(new Date(2022, 0, 1)); // Returns 400
// Invert to get date
timeScale.invert(400); // Returns Date object for mid-2022
```
**Use cases:**
- Time series visualisations
- Timeline axes
- Temporal animations
- Date-based interactions
**Methods:**
- `.nice()` - Extend domain to nice time intervals
- `.ticks(count)` - Generate nicely-spaced tick values
- All linear scale methods apply
### Quantize scale
Maps continuous input to discrete output buckets.
```javascript
const quantizeScale = d3.scaleQuantize()
.domain([0, 100])
.range(['low', 'medium', 'high']);
quantizeScale(25); // Returns 'low'
quantizeScale(50); // Returns 'medium'
quantizeScale(75); // Returns 'high'
// Get the threshold values
quantizeScale.thresholds(); // Returns [33.33, 66.67]
```
**Use cases:**
- Binning continuous data
- Heat map colours
- Risk categories (low/medium/high)
- Age groups, income brackets
### Quantile scale
Maps continuous input to discrete output based on quantiles.
```javascript
const quantileScale = d3.scaleQuantile()
.domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // Sample data
.range(['low', 'medium', 'high']);
quantileScale(8); // Returns based on quantile position
quantileScale.quantiles(); // Returns quantile thresholds
```
**Use cases:**
- Equal-size groups regardless of distribution
- Percentile-based categorisation
- Handling skewed distributions
### Threshold scale
Maps continuous input to discrete output with custom thresholds.
```javascript
const thresholdScale = d3.scaleThreshold()
.domain([0, 10, 20])
.range(['freezing', 'cold', 'warm', 'hot']);
thresholdScale(-5); // Returns 'freezing'
thresholdScale(5); // Returns 'cold'
thresholdScale(15); // Returns 'warm'
thresholdScale(25); // Returns 'hot'
```
**Use cases:**
- Custom breakpoints
- Grade boundaries (A, B, C, D, F)
- Temperature categories
- Air quality indices
## Sequential scales
### Sequential colour scale
Maps continuous input to continuous colour gradient.
```javascript
const colourScale = d3.scaleSequential(d3.interpolateBlues)
.domain([0, 100]);
colourScale(0); // Returns lightest blue
colourScale(50); // Returns mid blue
colourScale(100); // Returns darkest blue
```
**Available interpolators:**
**Single hue:**
- `d3.interpolateBlues`, `d3.interpolateGreens`, `d3.interpolateReds`
- `d3.interpolateOranges`, `d3.interpolatePurples`, `d3.interpolateGreys`
**Multi-hue:**
- `d3.interpolateViridis`, `d3.interpolateInferno`, `d3.interpolateMagma`
- `d3.interpolatePlasma`, `d3.interpolateWarm`, `d3.interpolateCool`
- `d3.interpolateCubehelixDefault`, `d3.interpolateTurbo`
**Use cases:**
- Heat maps, choropleth maps
- Continuous data visualisation
- Temperature, elevation, density
### Diverging colour scale
Maps continuous input to diverging colour gradient with a midpoint.
```javascript
const divergingScale = d3.scaleDiverging(d3.interpolateRdBu)
.domain([-10, 0, 10]);
divergingScale(-10); // Returns red
divergingScale(0); // Returns white/neutral
divergingScale(10); // Returns blue
```
**Available interpolators:**
- `d3.interpolateRdBu` - Red to blue
- `d3.interpolateRdYlBu` - Red, yellow, blue
- `d3.interpolateRdYlGn` - Red, yellow, green
- `d3.interpolatePiYG` - Pink, yellow, green
- `d3.interpolateBrBG` - Brown, blue-green
- `d3.interpolatePRGn` - Purple, green
- `d3.interpolatePuOr` - Purple, orange
- `d3.interpolateRdGy` - Red, grey
- `d3.interpolateSpectral` - Rainbow spectrum
**Use cases:**
- Data with meaningful midpoint (zero, average, neutral)
- Positive/negative values
- Above/below comparisons
- Correlation matrices
### Sequential quantile scale
Combines sequential colour with quantile mapping.
```javascript
const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues)
.domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]);
// Maps based on quantile position
```
**Use cases:**
- Perceptually uniform binning
- Handling outliers
- Skewed distributions
## Ordinal scales
### Band scale
Maps discrete input to continuous bands (rectangles) with optional padding.
```javascript
const bandScale = d3.scaleBand()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.1);
bandScale('A'); // Returns start position (e.g., 0)
bandScale('B'); // Returns start position (e.g., 110)
bandScale.bandwidth(); // Returns width of each band (e.g., 95)
bandScale.step(); // Returns total step including padding
bandScale.paddingInner(); // Returns inner padding (between bands)
bandScale.paddingOuter(); // Returns outer padding (at edges)
```
**Use cases:**
- Bar charts (most common use case)
- Grouped elements
- Categorical axes
- Heat map cells
**Padding options:**
- `.padding(value)` - Sets both inner and outer padding (0-1)
- `.paddingInner(value)` - Padding between bands (0-1)
- `.paddingOuter(value)` - Padding at edges (0-1)
- `.align(value)` - Alignment of bands (0-1, default 0.5)
### Point scale
Maps discrete input to continuous points (no width).
```javascript
const pointScale = d3.scalePoint()
.domain(['A', 'B', 'C', 'D'])
.range([0, 400])
.padding(0.5);
pointScale('A'); // Returns position (e.g., 50)
pointScale('B'); // Returns position (e.g., 150)
pointScale('C'); // Returns position (e.g., 250)
pointScale('D'); // Returns position (e.g., 350)
pointScale.step(); // Returns distance between points
```
**Use cases:**
- Line chart categorical x-axis
- Scatter plot with categorical axis
- Node positions in network graphs
- Any point positioning for categories
### Ordinal colour scale
Maps discrete input to discrete output (colours, shapes, etc.).
```javascript
const colourScale = d3.scaleOrdinal(d3.schemeCategory10);
colourScale('apples'); // Returns first colour
colourScale('oranges'); // Returns second colour
colourScale('apples'); // Returns same first colour (consistent)
// Custom range
const customScale = d3.scaleOrdinal()
.domain(['cat1', 'cat2', 'cat3'])
.range(['#FF6B6B', '#4ECDC4', '#45B7D1']);
```
**Built-in colour schemes:**
**Categorical:**
- `d3.schemeCategory10` - 10 colours
- `d3.schemeAccent` - 8 colours
- `d3.schemeDark2` - 8 colours
- `d3.schemePaired` - 12 colours
- `d3.schemePastel1` - 9 colours
- `d3.schemePastel2` - 8 colours
- `d3.schemeSet1` - 9 colours
- `d3.schemeSet2` - 8 colours
- `d3.schemeSet3` - 12 colours
- `d3.schemeTableau10` - 10 colours
**Use cases:**
- Category colours
- Legend items
- Multi-series charts
- Network node types
## Scale utilities
### Nice domain
Extend domain to nice round values.
```javascript
const scale = d3.scaleLinear()
.domain([0.201, 0.996])
.nice();
scale.domain(); // Returns [0.2, 1.0]
// With count (approximate tick count)
const scale2 = d3.scaleLinear()
.domain([0.201, 0.996])
.nice(5);
```
### Clamping
Restrict output to range bounds.
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500])
.clamp(true);
scale(-10); // Returns 0 (clamped)
scale(150); // Returns 500 (clamped)
```
### Copy scales
Create independent copies.
```javascript
const scale1 = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
const scale2 = scale1.copy();
// scale2 is independent of scale1
```
### Tick generation
Generate nice tick values for axes.
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
scale.ticks(10); // Generate ~10 ticks
scale.tickFormat(10); // Get format function for ticks
scale.tickFormat(10, ".2f"); // Custom format (2 decimal places)
// Time scale ticks
const timeScale = d3.scaleTime()
.domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]);
timeScale.ticks(d3.timeYear); // Yearly ticks
timeScale.ticks(d3.timeMonth, 3); // Every 3 months
timeScale.tickFormat(5, "%Y-%m"); // Format as year-month
```
## Colour spaces and interpolation
### RGB interpolation
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"]);
// Default: RGB interpolation
```
### HSL interpolation
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateHsl);
// Smoother colour transitions
```
### Lab interpolation
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateLab);
// Perceptually uniform
```
### HCL interpolation
```javascript
const scale = d3.scaleLinear()
.domain([0, 100])
.range(["blue", "red"])
.interpolate(d3.interpolateHcl);
// Perceptually uniform with hue
```
## Common patterns
### Diverging scale with custom midpoint
```javascript
const scale = d3.scaleLinear()
.domain([min, midpoint, max])
.range(["red", "white", "blue"])
.interpolate(d3.interpolateHcl);
```
### Multi-stop gradient scale
```javascript
const scale = d3.scaleLinear()
.domain([0, 25, 50, 75, 100])
.range(["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#66c2a5"]);
```
### Radius scale for circles (perceptual)
```javascript
const radiusScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.value)])
.range([0, 50]);
// Use with circles
circle.attr("r", d => radiusScale(d.value));
```
### Adaptive scale based on data range
```javascript
function createAdaptiveScale(data) {
const extent = d3.extent(data);
const range = extent[1] - extent[0];
// Use log scale if data spans >2 orders of magnitude
if (extent[1] / extent[0] > 100) {
return d3.scaleLog()
.domain(extent)
.range([0, width]);
}
// Otherwise use linear
return d3.scaleLinear()
.domain(extent)
.range([0, width]);
}
```
### Colour scale with explicit categories
```javascript
const colourScale = d3.scaleOrdinal()
.domain(['Low Risk', 'Medium Risk', 'High Risk'])
.range(['#2ecc71', '#f39c12', '#e74c3c'])
.unknown('#95a5a6'); // Fallback for unknown values
```
+376
View File
@@ -0,0 +1,376 @@
---
name: elevenlabs
description: Generate AI voiceovers, sound effects, and music using ElevenLabs APIs. Use when creating audio content for videos, podcasts, or games. Triggers include generating voiceovers, narration, dialogue, sound effects from descriptions, background music, soundtrack generation, voice cloning, or any audio synthesis task.
---
# ElevenLabs Audio Generation
Requires `ELEVENLABS_API_KEY` in `.env`.
## Text-to-Speech
```python
from elevenlabs.client import ElevenLabs
from elevenlabs import save, VoiceSettings
import os
client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
audio = client.text_to_speech.convert(
text="Welcome to my video!",
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_multilingual_v2",
voice_settings=VoiceSettings(
stability=0.5,
similarity_boost=0.75,
style=0.5,
speed=1.0
)
)
save(audio, "voiceover.mp3")
```
### Models
| Model | Quality | SSML Support | Notes |
|-------|---------|--------------|-------|
| `eleven_multilingual_v2` | Highest consistency | None | Stable, production-ready, 29 languages |
| `eleven_flash_v2_5` | Good | `<break>`, `<phoneme>` | Fast, supports pause/pronunciation tags |
| `eleven_turbo_v2_5` | Good | `<break>`, `<phoneme>` | Fastest latency |
| `eleven_v3` | Most expressive | None | Alpha — unreliable, needs prompt engineering |
**Choose:** multilingual_v2 for reliability, flash/turbo for SSML control, v3 for maximum expressiveness (expect retakes).
### Voice Settings by Style
| Style | stability | similarity | style | speed |
|-------|-----------|------------|-------|-------|
| Natural/professional | 0.75-0.85 | 0.9 | 0.0-0.1 | 1.0 |
| Conversational | 0.5-0.6 | 0.85 | 0.3-0.4 | 0.9-1.0 |
| Energetic/YouTuber | 0.3-0.5 | 0.75 | 0.5-0.7 | 1.0-1.1 |
### Pauses Between Sections
**With flash/turbo models:** Use SSML break tags inline:
```
...end of section. <break time="1.5s" /> Start of next...
```
Max 3 seconds per break. Excessive breaks can cause speed artifacts.
**With multilingual_v2 / v3:** No SSML support. Options:
- Paragraph breaks (blank lines) — creates ~0.3-0.5s natural pause
- Post-process with ffmpeg: split audio and insert silence
**WARNING:** `...` (ellipsis) is NOT a reliable pause — it can be vocalized as a word/sound. Do not use ellipsis as a pause mechanism.
### Pronunciation Control
**Phonetic spelling (any model):** Write words as you want them pronounced:
- `Janus``Jan-us`
- `nginx``engine-x`
- Use dashes, capitals, apostrophes to guide pronunciation
**SSML phoneme tags (flash/turbo only):**
```
<phoneme alphabet="ipa" ph="ˈdʒeɪnəs">Janus</phoneme>
```
### Iterative Workflow
1. Generate → listen → identify pronunciation/pacing issues
2. Adjust: phonetic spellings, break tags, voice settings
3. Regenerate. If pauses aren't precise enough, add silence in post with ffmpeg rather than fighting the TTS engine.
## Voice Cloning
### Instant Voice Clone
```python
with open("sample.mp3", "rb") as f:
voice = client.voices.ivc.create(
name="My Voice",
files=[f],
remove_background_noise=True
)
print(f"Voice ID: {voice.voice_id}")
```
- Use `client.voices.ivc.create()` (not `client.voices.clone()`)
- Pass file handles in binary mode (`"rb"`), not paths
- Convert m4a first: `ffmpeg -i input.m4a -codec:a libmp3lame -qscale:a 2 output.mp3`
- Multiple samples (2-3 clips) improve accuracy
- Save voice ID for reuse
**Professional Voice Clone:** Requires Creator plan+, 30+ min audio. See [reference.md](reference.md).
## Sound Effects
Max 22 seconds per generation.
```python
result = client.text_to_sound_effects.convert(
text="Thunder rumbling followed by heavy rain",
duration_seconds=10,
prompt_influence=0.3
)
with open("thunder.mp3", "wb") as f:
for chunk in result:
f.write(chunk)
```
**Prompt tips:** Be specific — "Heavy footsteps on wooden floorboards, slow and deliberate, with creaking"
## Music Generation
10 seconds to 5 minutes. Use `client.music.compose()` (not `.generate()`).
```python
result = client.music.compose(
prompt="Upbeat indie rock, catchy guitar riff, energetic drums, travel vlog",
music_length_ms=60000,
force_instrumental=True
)
with open("music.mp3", "wb") as f:
for chunk in result:
f.write(chunk)
```
**Prompt structure:** Genre, mood, instruments, tempo, use case. Add "no vocals" or use `force_instrumental=True` for background music.
## Remotion Integration
### Complete Workflow: Script to Synchronized Scene
```
VOICEOVER-SCRIPT.md → voiceover.py → public/audio/ → Remotion composition
↓ ↓ ↓ ↓
Scene narration Generate MP3 Audio files <Audio> component
with durations per scene with timing synced to scenes
```
### Step 1: Generate Per-Scene Audio
Use the toolkit's voiceover tool to generate audio for each scene:
```bash
# Generate voiceover files for each scene
python tools/voiceover.py --scene-dir public/audio/scenes --json
# Output:
# public/audio/scenes/
# ├── scene-01-title.mp3
# ├── scene-02-problem.mp3
# ├── scene-03-solution.mp3
# └── manifest.json (durations for each file)
```
The `manifest.json` contains timing info:
```json
{
"scenes": [
{ "file": "scene-01-title.mp3", "duration": 4.2 },
{ "file": "scene-02-problem.mp3", "duration": 12.8 },
{ "file": "scene-03-solution.mp3", "duration": 15.3 }
],
"totalDuration": 32.3
}
```
### Step 2: Use Audio in Remotion Composition
```tsx
// src/Composition.tsx
import { Audio, staticFile, Series, useVideoConfig } from 'remotion';
// Import scene components
import { TitleSlide } from './scenes/TitleSlide';
import { ProblemSlide } from './scenes/ProblemSlide';
import { SolutionSlide } from './scenes/SolutionSlide';
// Scene durations (from manifest.json, converted to frames at 30fps)
const SCENE_DURATIONS = {
title: Math.ceil(4.2 * 30), // 126 frames
problem: Math.ceil(12.8 * 30), // 384 frames
solution: Math.ceil(15.3 * 30), // 459 frames
};
export const MainComposition: React.FC = () => {
return (
<>
{/* Scene sequence */}
<Series>
<Series.Sequence durationInFrames={SCENE_DURATIONS.title}>
<TitleSlide />
</Series.Sequence>
<Series.Sequence durationInFrames={SCENE_DURATIONS.problem}>
<ProblemSlide />
</Series.Sequence>
<Series.Sequence durationInFrames={SCENE_DURATIONS.solution}>
<SolutionSlide />
</Series.Sequence>
</Series>
{/* Audio track - plays continuously across all scenes */}
<Audio src={staticFile('audio/voiceover.mp3')} volume={1} />
{/* Optional: Background music at lower volume */}
<Audio src={staticFile('audio/music.mp3')} volume={0.15} />
</>
);
};
```
### Step 3: Per-Scene Audio (Alternative)
For more control, add audio to each scene individually:
```tsx
// src/scenes/ProblemSlide.tsx
import { Audio, staticFile, useCurrentFrame } from 'remotion';
export const ProblemSlide: React.FC = () => {
const frame = useCurrentFrame();
return (
<div style={{ /* slide styles */ }}>
<h1>The Problem</h1>
{/* Scene content */}
{/* Audio starts when this scene starts (frame 0 of this sequence) */}
<Audio src={staticFile('audio/scenes/scene-02-problem.mp3')} />
</div>
);
};
```
### Syncing Visuals to Voiceover
Calculate scene duration from audio, not the other way around:
```tsx
// src/config/timing.ts
import manifest from '../../public/audio/scenes/manifest.json';
const FPS = 30;
// Convert audio durations to frame counts
export const sceneDurations = manifest.scenes.reduce((acc, scene) => {
const name = scene.file.replace(/^scene-\d+-/, '').replace('.mp3', '');
acc[name] = Math.ceil(scene.duration * FPS);
return acc;
}, {} as Record<string, number>);
// Usage in composition:
// <Series.Sequence durationInFrames={sceneDurations.title}>
```
### Audio Timing Patterns
```tsx
import { Audio, Sequence, interpolate, useCurrentFrame } from 'remotion';
// Fade in audio
export const FadeInAudio: React.FC<{ src: string; fadeFrames?: number }> = ({
src,
fadeFrames = 30
}) => {
const frame = useCurrentFrame();
const volume = interpolate(frame, [0, fadeFrames], [0, 1], {
extrapolateRight: 'clamp',
});
return <Audio src={src} volume={volume} />;
};
// Delayed audio start
export const DelayedAudio: React.FC<{ src: string; delayFrames: number }> = ({
src,
delayFrames
}) => (
<Sequence from={delayFrames}>
<Audio src={src} />
</Sequence>
);
// Usage:
// <FadeInAudio src={staticFile('audio/music.mp3')} fadeFrames={60} />
// <DelayedAudio src={staticFile('audio/sfx/whoosh.mp3')} delayFrames={45} />
```
### Voiceover + Demo Video Sync
When a scene has both voiceover and demo video:
```tsx
import { Audio, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';
export const DemoScene: React.FC = () => {
const { durationInFrames, fps } = useVideoConfig();
// Calculate playback rate to fit demo into voiceover duration
const demoDuration = 45; // seconds (original demo length)
const sceneDuration = durationInFrames / fps; // seconds (from voiceover)
const playbackRate = demoDuration / sceneDuration;
return (
<>
<OffthreadVideo
src={staticFile('demos/feature-demo.mp4')}
playbackRate={playbackRate}
/>
<Audio src={staticFile('audio/scenes/scene-04-demo.mp3')} />
</>
);
};
```
### Error Handling
```tsx
import { Audio, staticFile, delayRender, continueRender } from 'remotion';
import { useEffect, useState } from 'react';
export const SafeAudio: React.FC<{ src: string }> = ({ src }) => {
const [handle] = useState(() => delayRender());
const [audioReady, setAudioReady] = useState(false);
useEffect(() => {
const audio = new window.Audio(src);
audio.oncanplaythrough = () => {
setAudioReady(true);
continueRender(handle);
};
audio.onerror = () => {
console.error(`Failed to load audio: ${src}`);
continueRender(handle); // Continue without audio rather than hang
};
}, [src, handle]);
if (!audioReady) return null;
return <Audio src={src} />;
};
```
### Toolkit Command: /generate-voiceover
The `/generate-voiceover` command handles the full workflow:
```
/generate-voiceover
1. Reads VOICEOVER-SCRIPT.md
2. Extracts narration for each scene
3. Generates audio via ElevenLabs API
4. Saves to public/audio/scenes/
5. Creates manifest.json with durations
6. Updates project.json with timing info
```
## Popular Voices
- George: `JBFqnCBsd6RMkjVDRZzb` (warm narrator)
- Rachel: `21m00Tcm4TlvDq8ikWAM` (clear female)
- Adam: `pNInz6obpgDQGcFmaJgB` (professional male)
List all: `client.voices.get_all()`
For full API docs, see [reference.md](reference.md).
+167
View File
@@ -0,0 +1,167 @@
# ElevenLabs API Reference
Detailed API documentation for ElevenLabs audio generation services.
## Authentication
```python
from elevenlabs.client import ElevenLabs
client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
```
## Text-to-Speech Models
| Model ID | Description | Languages | Latency |
|----------|-------------|-----------|---------|
| `eleven_flash_v2_5` | Ultra-low latency streaming | 32 | ~75ms |
| `eleven_multilingual_v2` | Highest quality | 32 | Standard |
| `eleven_turbo_v2_5` | Fast, good quality | 32 | Low |
| `eleven_v3` | Best emotional range (alpha) | 32+ | Higher |
## Voice Settings
| Parameter | Range | Default | Effect |
|-----------|-------|---------|--------|
| `stability` | 0.0-1.0 | 0.5 | Lower = more expressive/variable |
| `similarity_boost` | 0.0-1.0 | 0.75 | Higher = closer to original voice |
| `style` | 0.0-1.0 | 0.0 | Style exaggeration (v2 models) |
| `speed` | 0.5-2.0 | 1.0 | Playback speed multiplier |
## Output Formats
| Format Code | Sample Rate | Bitrate | Tier Required |
|-------------|-------------|---------|---------------|
| `mp3_44100_128` | 44.1kHz | 128kbps | Free (default) |
| `mp3_44100_192` | 44.1kHz | 192kbps | Creator+ |
| `pcm_44100` | 44.1kHz | - | Pro+ |
| `ulaw_8000` | 8kHz | - | Free (telephony) |
## Long-form Audio (Stitching)
For continuity across multiple generations:
```python
result1 = client.text_to_speech.convert_with_timestamps(
text="First paragraph...",
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_multilingual_v2"
)
request_id_1 = result1.request_id
result2 = client.text_to_speech.convert(
text="Second paragraph...",
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_multilingual_v2",
previous_request_ids=[request_id_1]
)
```
## Professional Voice Cloning (PVC)
Requires Creator plan+. Creates a fine-tuned model (3-6 hours training).
**Requirements:**
- 30 min minimum, 2-3 hours optimal audio
- Professional XLR mic recommended
- Pop filter, ~20cm distance
- Peak levels: -6dB to -3dB
- Consistent performance style
**Workflow:**
```python
# 1. Create PVC with samples
pvc = client.voices.create_professional_voice_clone(
name="My Pro Voice",
files=["recording1.mp3", "recording2.mp3", ...],
)
# 2. Get verification captcha
captcha = client.voices.get_pvc_verification_captcha(voice_id=pvc.voice_id)
# Read the captcha text aloud and record
# 3. Submit verification
client.voices.verify_pvc(
voice_id=pvc.voice_id,
recording=open("captcha_reading.mp3", "rb")
)
# 4. Start training
client.voices.start_pvc_training(voice_id=pvc.voice_id)
```
## Sound Effects Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `text` | string | Yes | Description of sound effect |
| `duration_seconds` | float | No | 1-22 seconds (auto if omitted) |
| `prompt_influence` | float | No | 0.0-1.0 (default 0.3) |
**Billing:** 100 chars/generation (auto) or 25 chars/second (fixed duration)
**Example prompts:**
- Environmental: "Rain on a tin roof, steady and rhythmic"
- Action: "Sword being drawn from sheath, metallic ring"
- Mechanical: "Old car engine struggling to start then roaring to life"
## Music Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | Yes* | Natural language music description |
| `composition_plan` | object | Yes* | Detailed composition structure |
| `duration_ms` | int | No | 10000-300000 (10s-5min) |
| `instrumental` | bool | No | Force instrumental output |
*Either `prompt` or `composition_plan` required, not both.
**Effective prompts include:**
1. Genre/Style: "indie rock", "lo-fi hip hop", "orchestral"
2. Mood: "uplifting", "melancholic", "tense"
3. Instruments: "acoustic guitar", "synth pads", "strings"
4. Tempo/Energy: "slow", "upbeat", "driving"
5. Context: "for a travel vlog", "podcast intro"
## Rate Limits by Tier
| Tier | TTS Concurrent | SFX Concurrent | Music Concurrent |
|------|---------------|----------------|------------------|
| Free | 2 | 2 | 1 |
| Starter | 3 | 3 | 2 |
| Creator | 5 | 5 | 3 |
| Pro | 10 | 10 | 5 |
| Scale | 15 | 15 | 10 |
## Voice Management
```python
# List voices
voices = client.voices.get_all()
for voice in voices.voices:
print(f"{voice.name}: {voice.voice_id}")
# Delete voice
client.voices.delete(voice_id="your_voice_id")
```
## Error Handling
```python
from elevenlabs.core.api_error import ApiError
try:
audio = client.text_to_speech.convert(...)
except ApiError as e:
if e.status_code == 429:
print("Rate limited - wait and retry")
elif e.status_code == 401:
print("Invalid API key")
```
| Code | Meaning | Action |
|------|---------|--------|
| 401 | Invalid API key | Check API key |
| 403 | Feature not available | Upgrade tier |
| 422 | Invalid parameters | Check request body |
| 429 | Rate limited | Wait and retry |
+261
View File
@@ -0,0 +1,261 @@
---
name: faceswap
description: |
Swap faces in a video using AI via the HeyGen API. Use when: (1) Replacing a face in a video with another face, (2) Face swapping from a source image onto a target video, (3) Creating personalized videos by swapping in a person's face, (4) Working with HeyGen's /v1/workflows/executions endpoint for face swap processing.
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# Face Swap (HeyGen API)
Swap a face from a source image into a target video using GPU-accelerated AI processing. The source image provides the face to swap in, and the target video receives the new face.
## Authentication
All requests require the `X-Api-Key` header. Set the `HEYGEN_API_KEY` environment variable.
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workflow_type": "FaceswapNode", "input": {"source_image_url": "https://example.com/face.jpg", "target_video_url": "https://example.com/video.mp4"}}'
```
## Default Workflow
1. Call `POST /v1/workflows/executions` with `workflow_type: "FaceswapNode"`, a source face image, and a target video
2. Receive a `execution_id` in the response
3. Poll `GET /v1/workflows/executions/{id}` every 10 seconds until status is `completed`
4. Use the returned `video_url` from the output
## Execute Face Swap
### Endpoint
`POST https://api.heygen.com/v1/workflows/executions`
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `workflow_type` | string | Y | Must be `"FaceswapNode"` |
| `input.source_image_url` | string | Y | URL of the face image to swap in |
| `input.target_video_url` | string | Y | URL of the video to apply the face swap to |
### curl
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "FaceswapNode",
"input": {
"source_image_url": "https://example.com/face-photo.jpg",
"target_video_url": "https://example.com/original-video.mp4"
}
}'
```
### TypeScript
```typescript
interface FaceswapInput {
source_image_url: string;
target_video_url: string;
}
interface ExecuteResponse {
data: {
execution_id: string;
status: "submitted";
};
}
async function faceswap(input: FaceswapInput): Promise<string> {
const response = await fetch("https://api.heygen.com/v1/workflows/executions", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
workflow_type: "FaceswapNode",
input,
}),
});
const json: ExecuteResponse = await response.json();
return json.data.execution_id;
}
```
### Python
```python
import requests
import os
def faceswap(source_image_url: str, target_video_url: str) -> str:
payload = {
"workflow_type": "FaceswapNode",
"input": {
"source_image_url": source_image_url,
"target_video_url": target_video_url,
},
}
response = requests.post(
"https://api.heygen.com/v1/workflows/executions",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json",
},
json=payload,
)
data = response.json()
return data["data"]["execution_id"]
```
### Response Format
```json
{
"data": {
"execution_id": "node-gw-f1s2w3p4",
"status": "submitted"
}
}
```
## Check Status
### Endpoint
`GET https://api.heygen.com/v1/workflows/executions/{execution_id}`
### curl
```bash
curl -X GET "https://api.heygen.com/v1/workflows/executions/node-gw-f1s2w3p4" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### Response Format (Completed)
```json
{
"data": {
"execution_id": "node-gw-f1s2w3p4",
"status": "completed",
"output": {
"video_url": "https://resource.heygen.ai/faceswap/output.mp4"
}
}
}
```
## Polling for Completion
```typescript
async function faceswapAndWait(
input: FaceswapInput,
maxWaitMs = 600000,
pollIntervalMs = 10000
): Promise<string> {
const executionId = await faceswap(input);
console.log(`Submitted face swap: ${executionId}`);
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const response = await fetch(
`https://api.heygen.com/v1/workflows/executions/${executionId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
switch (data.status) {
case "completed":
return data.output.video_url;
case "failed":
throw new Error(data.error?.message || "Face swap failed");
case "not_found":
throw new Error("Workflow not found");
default:
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
}
throw new Error("Face swap timed out");
}
```
## Usage Examples
### Basic Face Swap
```bash
curl -X POST "https://api.heygen.com/v1/workflows/executions" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_type": "FaceswapNode",
"input": {
"source_image_url": "https://example.com/headshot.jpg",
"target_video_url": "https://example.com/presentation.mp4"
}
}'
```
### Chain with Avatar Video
Generate an avatar video first, then swap in a custom face:
```python
import time
# Step 1: Generate avatar video
avatar_execution_id = requests.post(
"https://api.heygen.com/v1/workflows/executions",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"], "Content-Type": "application/json"},
json={
"workflow_type": "AvatarInferenceNode",
"input": {
"avatar": {"avatar_id": "Angela-inblackskirt-20220820"},
"audio_list": [{"audio_url": "https://example.com/speech.mp3"}],
},
},
).json()["data"]["execution_id"]
# Step 2: Wait for avatar video to complete
while True:
status = requests.get(
f"https://api.heygen.com/v1/workflows/executions/{avatar_execution_id}",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]},
).json()["data"]
if status["status"] == "completed":
avatar_video_url = status["output"]["video"]["video_url"]
break
time.sleep(10)
# Step 3: Swap in a custom face
faceswap_execution_id = faceswap(
source_image_url="https://example.com/custom-face.jpg",
target_video_url=avatar_video_url,
)
```
## Best Practices
1. **Use a clear, front-facing face photo** — the source image should show a single face with good lighting
2. **Face swap is GPU-intensive** — expect 1-3 minutes processing time, poll every 10 seconds
3. **Source image quality matters** — higher resolution face photos produce better results
4. **One face per source image** — the source should contain exactly one face to swap in
5. **Works with any video** — the target video can be an avatar video, a recording, or any video with visible faces
6. **Chain with other workflows** — generate an avatar video first, then swap in a custom face for personalization
+432
View File
@@ -0,0 +1,432 @@
---
name: ffmpeg
description: Video and audio processing with FFmpeg. Use for format conversion, resizing, compression, audio extraction, and preparing assets for Remotion. Triggers include converting GIF to MP4, resizing video, extracting audio, compressing files, or any media transformation task.
---
# FFmpeg for Video Production
FFmpeg is the essential tool for video/audio processing. This skill covers common operations for Remotion video projects.
## Quick Reference
### GIF to MP4 (Remotion-compatible)
```bash
ffmpeg -i input.gif -movflags faststart -pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" output.mp4
```
**Why these flags:**
- `-movflags faststart` - Moves metadata to start for web streaming
- `-pix_fmt yuv420p` - Ensures compatibility with most players
- `scale=trunc(...)` - Forces even dimensions (required by most codecs)
### Resize Video
```bash
# To 1920x1080 (maintain aspect ratio, add black bars)
ffmpeg -i input.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" output.mp4
# To 1920x1080 (crop to fill)
ffmpeg -i input.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080" output.mp4
# Scale to width, auto height
ffmpeg -i input.mp4 -vf "scale=1280:-2" output.mp4
```
### Compress Video
```bash
# Good quality, smaller file (CRF 23 is default, lower = better quality)
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output.mp4
# Aggressive compression for web preview
ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset fast -c:a aac -b:a 96k output.mp4
# Target file size (e.g., ~10MB for 60s video = ~1.3Mbps)
ffmpeg -i input.mp4 -c:v libx264 -b:v 1300k -c:a aac -b:a 128k output.mp4
```
### Extract Audio
```bash
# Extract to MP3
ffmpeg -i input.mp4 -vn -acodec libmp3lame -q:a 2 output.mp3
# Extract to AAC
ffmpeg -i input.mp4 -vn -acodec aac -b:a 192k output.m4a
# Extract to WAV (uncompressed)
ffmpeg -i input.mp4 -vn output.wav
```
### Convert Audio Formats
```bash
# M4A to MP3 (for ElevenLabs voice samples)
ffmpeg -i input.m4a -codec:a libmp3lame -qscale:a 2 output.mp3
# WAV to MP3
ffmpeg -i input.wav -codec:a libmp3lame -b:a 192k output.mp3
# Adjust volume
ffmpeg -i input.mp3 -filter:a "volume=1.5" output.mp3
```
### Trim/Cut Video
```bash
# Cut from timestamp to duration (recommended - reliable)
ffmpeg -i input.mp4 -ss 00:00:30 -t 00:00:15 -c:v libx264 -c:a aac output.mp4
# Cut from timestamp to timestamp
ffmpeg -i input.mp4 -ss 00:00:30 -to 00:00:45 -c:v libx264 -c:a aac output.mp4
# Stream copy (faster but may lose frames at cut points)
# Only use when source has frequent keyframes
ffmpeg -i input.mp4 -ss 00:00:30 -t 00:00:15 -c copy output.mp4
```
**Note:** Re-encoding is recommended for trimming. Stream copy (`-c copy`) can silently drop video if the seek point doesn't align with a keyframe.
### Speed Up / Slow Down
```bash
# 2x speed (video and audio)
ffmpeg -i input.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" output.mp4
# 0.5x speed (slow motion)
ffmpeg -i input.mp4 -filter_complex "[0:v]setpts=2.0*PTS[v];[0:a]atempo=0.5[a]" -map "[v]" -map "[a]" output.mp4
# Video only (no audio)
ffmpeg -i input.mp4 -filter:v "setpts=0.5*PTS" -an output.mp4
```
### Concatenate Videos
```bash
# Create file list
echo "file 'clip1.mp4'" > list.txt
echo "file 'clip2.mp4'" >> list.txt
echo "file 'clip3.mp4'" >> list.txt
# Concatenate (same codec/resolution)
ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4
# Concatenate with re-encoding (different sources)
ffmpeg -f concat -safe 0 -i list.txt -c:v libx264 -c:a aac output.mp4
```
### Add Fade In/Out
```bash
# Fade in first 1 second, fade out last 1 second (30fps video)
ffmpeg -i input.mp4 -vf "fade=t=in:st=0:d=1,fade=t=out:st=9:d=1" -c:a copy output.mp4
# Audio fade
ffmpeg -i input.mp4 -af "afade=t=in:st=0:d=1,afade=t=out:st=9:d=1" -c:v copy output.mp4
```
### Get Video Info
```bash
# Duration, resolution, codec info
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4
# Full info
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
```
## Remotion-Specific Patterns
### Video Speed Adjustment for Remotion
**When to use FFmpeg vs Remotion `playbackRate`:**
| Scenario | Use FFmpeg | Use Remotion |
|----------|------------|--------------|
| Constant speed (1.5x, 2x) | Either works | ✅ Simpler |
| Extreme speeds (>4x or <0.25x) | ✅ More reliable | May have issues |
| Variable speed (accelerate over time) | ✅ Pre-process | Complex workaround needed |
| Need perfect audio sync | ✅ Guaranteed | Usually fine |
| Demo needs to fit voiceover timing | ✅ Pre-calculate | Runtime adjustment |
**Remotion limitation:** `playbackRate` must be constant. Dynamic interpolation like `playbackRate={interpolate(frame, [0, 100], [1, 5])}` won't work correctly because Remotion evaluates frames independently.
```bash
# Speed up demo to fit a scene (e.g., 60s demo into 20s = 3x speed)
ffmpeg -i demo-raw.mp4 \
-filter_complex "[0:v]setpts=0.333*PTS[v];[0:a]atempo=3.0[a]" \
-map "[v]" -map "[a]" \
public/demos/demo-fast.mp4
# Slow motion for emphasis (0.5x speed)
ffmpeg -i action.mp4 \
-filter_complex "[0:v]setpts=2.0*PTS[v];[0:a]atempo=0.5[a]" \
-map "[v]" -map "[a]" \
public/demos/action-slow.mp4
# Speed up without audio (common for screen recordings)
ffmpeg -i demo.mp4 -filter:v "setpts=0.5*PTS" -an public/demos/demo-2x.mp4
# Timelapse effect (10x speed, drop audio)
ffmpeg -i long-demo.mp4 -filter:v "setpts=0.1*PTS" -an public/demos/timelapse.mp4
```
**Calculate speed factor:**
- To fit X seconds of video into Y seconds of scene: `speed = X / Y`
- setpts multiplier = `1 / speed` (e.g., 3x speed = setpts=0.333*PTS)
- atempo value = `speed` (e.g., 3x speed = atempo=3.0)
**Extreme speed (>2x audio):** Chain atempo filters (each limited to 0.5-2.0 range):
```bash
# 4x speed audio
-filter_complex "[0:a]atempo=2.0,atempo=2.0[a]"
# 8x speed audio
-filter_complex "[0:a]atempo=2.0,atempo=2.0,atempo=2.0[a]"
```
### Prepare Demo Recording for Remotion
```bash
# Standard 1080p, 30fps, Remotion-ready
ffmpeg -i raw-recording.mp4 \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,fps=30" \
-c:v libx264 -crf 18 -preset slow \
-c:a aac -b:a 192k \
-movflags faststart \
public/demos/demo.mp4
```
### Screen Recording to Remotion Asset
```bash
# From iPhone/iPad recording (usually 60fps, variable resolution)
ffmpeg -i iphone-recording.mov \
-vf "scale=1920:-2,fps=30" \
-c:v libx264 -crf 20 \
-an \
public/demos/mobile-demo.mp4
```
### Batch Convert GIFs
```bash
for f in assets/*.gif; do
ffmpeg -i "$f" -movflags faststart -pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
"public/demos/$(basename "$f" .gif).mp4"
done
```
## Common Issues
### "Height not divisible by 2"
Add scale filter: `-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2"`
### Video won't play in browser
Use: `-movflags faststart -pix_fmt yuv420p -c:v libx264`
### Audio out of sync after speed change
Use filter_complex with atempo: `-filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]"`
### File too large
Increase CRF (23→28) or reduce resolution
## Quality Guidelines
| Use Case | CRF | Preset | Notes |
|----------|-----|--------|-------|
| Archive/Master | 18 | slow | Best quality, large files |
| Production | 20-22 | medium | Good balance |
| Web/Preview | 23-25 | fast | Smaller files |
| Draft/Quick | 28+ | veryfast | Fast encoding |
## Platform-Specific Output Optimization
After Remotion renders your video (typically to `out/video.mp4`), use FFmpeg to optimize for each distribution platform.
### Workflow Integration
```
Remotion render (master) FFmpeg optimization Platform upload
↓ ↓ ↓
out/video.mp4 ────────→ out/video-youtube.mp4 ───→ YouTube
────────→ out/video-twitter.mp4 ───→ Twitter/X
────────→ out/video-linkedin.mp4 ───→ LinkedIn
────────→ out/video-web.mp4 ───→ Website embed
```
### YouTube (Recommended Settings)
YouTube re-encodes everything, so upload high quality:
```bash
# YouTube optimized (1080p)
ffmpeg -i out/video.mp4 \
-c:v libx264 -preset slow -crf 18 \
-profile:v high -level 4.0 \
-bf 2 -g 30 \
-c:a aac -b:a 192k -ar 48000 \
-movflags +faststart \
out/video-youtube.mp4
# YouTube Shorts (vertical 1080x1920)
ffmpeg -i out/video.mp4 \
-vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2" \
-c:v libx264 -crf 18 -c:a aac -b:a 192k \
out/video-shorts.mp4
```
### Twitter/X
Twitter has strict limits: max 140s, 512MB, 1920x1200:
```bash
# Twitter optimized (under 15MB target for fast upload)
ffmpeg -i out/video.mp4 \
-c:v libx264 -preset medium -crf 24 \
-profile:v main -level 3.1 \
-vf "scale='min(1280,iw)':'min(720,ih)':force_original_aspect_ratio=decrease" \
-c:a aac -b:a 128k -ar 44100 \
-movflags +faststart \
-fs 15M \
out/video-twitter.mp4
# Check file size and duration
ffprobe -v error -show_entries format=duration,size -of csv=p=0 out/video-twitter.mp4
```
### LinkedIn
LinkedIn prefers MP4 with AAC audio, max 10 minutes:
```bash
# LinkedIn optimized
ffmpeg -i out/video.mp4 \
-c:v libx264 -preset medium -crf 22 \
-profile:v main \
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease" \
-c:a aac -b:a 192k -ar 48000 \
-movflags +faststart \
out/video-linkedin.mp4
```
### Website/Embed (Optimized for Fast Loading)
```bash
# Web-optimized MP4 (small file, progressive loading)
ffmpeg -i out/video.mp4 \
-c:v libx264 -preset medium -crf 26 \
-profile:v baseline -level 3.0 \
-vf "scale=1280:720" \
-c:a aac -b:a 128k \
-movflags +faststart \
out/video-web.mp4
# WebM alternative (better compression, wider browser support)
ffmpeg -i out/video.mp4 \
-c:v libvpx-vp9 -crf 30 -b:v 0 \
-vf "scale=1280:720" \
-c:a libopus -b:a 128k \
-deadline good \
out/video-web.webm
```
### GIF (for Previews/Thumbnails)
```bash
# High-quality GIF (first 5 seconds)
ffmpeg -i out/video.mp4 -t 5 \
-vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
out/preview.gif
# Smaller file GIF
ffmpeg -i out/video.mp4 -t 3 \
-vf "fps=10,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
out/preview-small.gif
```
### Platform Requirements Quick Reference
| Platform | Max Resolution | Max Size | Max Duration | Audio |
|----------|---------------|----------|--------------|-------|
| YouTube | 8K | 256GB | 12 hours | AAC 48kHz |
| Twitter/X | 1920x1200 | 512MB | 140s | AAC 44.1kHz |
| LinkedIn | 4096x2304 | 5GB | 10 min | AAC 48kHz |
| Instagram Feed | 1080x1350 | 4GB | 60s | AAC 48kHz |
| Instagram Reels | 1080x1920 | 4GB | 90s | AAC 48kHz |
| TikTok | 1080x1920 | 287MB | 10 min | AAC |
### Batch Export for All Platforms
```bash
#!/bin/bash
# save as: export-all-platforms.sh
INPUT="out/video.mp4"
# YouTube (high quality)
ffmpeg -i "$INPUT" -c:v libx264 -preset slow -crf 18 \
-c:a aac -b:a 192k -movflags +faststart \
out/video-youtube.mp4
# Twitter (compressed)
ffmpeg -i "$INPUT" -c:v libx264 -crf 24 \
-vf "scale='min(1280,iw)':'-2'" \
-c:a aac -b:a 128k -movflags +faststart \
out/video-twitter.mp4
# LinkedIn
ffmpeg -i "$INPUT" -c:v libx264 -crf 22 \
-c:a aac -b:a 192k -movflags +faststart \
out/video-linkedin.mp4
# Web embed (small)
ffmpeg -i "$INPUT" -c:v libx264 -crf 26 \
-vf "scale=1280:720" \
-c:a aac -b:a 128k -movflags +faststart \
out/video-web.mp4
echo "Exported:"
ls -lh out/video-*.mp4
```
## Error Handling
Common errors and fixes when processing video:
```bash
# Check if FFmpeg succeeded
ffmpeg -i input.mp4 -c:v libx264 output.mp4 && echo "Success" || echo "Failed: check input file"
# Validate output file is playable
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 output.mp4
# Get detailed error info
ffmpeg -v error -i input.mp4 -f null - 2>&1 | head -20
```
### Handling Common Failures
| Error | Cause | Fix |
|-------|-------|-----|
| "No such file" | Input path wrong | Check path, use quotes for spaces |
| "Invalid data" | Corrupted input | Re-download or re-record source |
| "height not divisible by 2" | Odd dimensions | Add scale filter with trunc |
| "encoder not found" | Missing codec | Install FFmpeg with full codecs |
| Output 0 bytes | Silent failure | Check full ffmpeg output for errors |
---
## Feedback & Contributions
If this skill is missing information or could be improved:
- **Missing a command?** Describe what you needed
- **Found an error?** Let me know what's wrong
- **Want to contribute?** I can help you:
1. Update this skill with improvements
2. Create a PR to github.com/digitalsamba/claude-code-video-toolkit
Just say "improve this skill" and I'll guide you through updating `.claude/skills/ffmpeg/SKILL.md`.
+173
View File
@@ -0,0 +1,173 @@
# FFmpeg Reference
## Filter Syntax
### Video Filters (-vf)
```bash
# Chain filters with comma
-vf "scale=1920:1080,fps=30,crop=1280:720"
# Complex filters with labels
-filter_complex "[0:v]scale=1920:1080[scaled];[scaled]fps=30[out]" -map "[out]"
```
### Common Video Filters
| Filter | Syntax | Example |
|--------|--------|---------|
| scale | `scale=w:h` | `scale=1920:1080` or `scale=1280:-1` (auto height) |
| crop | `crop=w:h:x:y` | `crop=1280:720:320:180` |
| fps | `fps=N` | `fps=30` |
| pad | `pad=w:h:x:y:color` | `pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black` |
| fade | `fade=t=in/out:st=N:d=N` | `fade=t=in:st=0:d=1` |
| setpts | `setpts=N*PTS` | `setpts=0.5*PTS` (2x speed) |
| drawtext | `drawtext=text='Hi':fontsize=24` | Add text overlay |
| overlay | `overlay=x:y` | Combine videos |
### Common Audio Filters (-af)
| Filter | Syntax | Example |
|--------|--------|---------|
| volume | `volume=N` | `volume=1.5` or `volume=0.5` |
| afade | `afade=t=in/out:st=N:d=N` | `afade=t=in:st=0:d=1` |
| atempo | `atempo=N` | `atempo=2.0` (2x speed, range 0.5-2.0) |
| loudnorm | `loudnorm` | Normalize audio levels |
## Codec Options
### Video Codecs (-c:v)
| Codec | Use Case | Notes |
|-------|----------|-------|
| libx264 | Universal H.264 | Best compatibility |
| libx265 | H.265/HEVC | Better compression, less compatible |
| libvpx-vp9 | WebM | Good for web |
| prores | ProRes | Professional editing |
| copy | Stream copy | No re-encoding, fastest |
### Audio Codecs (-c:a)
| Codec | Use Case | Notes |
|-------|----------|-------|
| aac | MP4 container | Most compatible |
| libmp3lame | MP3 | Universal |
| libvorbis | WebM/OGG | Open source |
| pcm_s16le | WAV | Uncompressed |
| copy | Stream copy | No re-encoding |
## Quality Settings
### CRF (Constant Rate Factor) for x264/x265
| CRF | Quality | Use Case |
|-----|---------|----------|
| 0 | Lossless | Archive |
| 17-18 | Visually lossless | Master |
| 19-22 | High quality | Production |
| 23 | Default | General use |
| 24-27 | Medium | Web delivery |
| 28+ | Low | Preview/draft |
### Presets (-preset)
Faster presets = larger files, quicker encoding
`ultrafast``superfast``veryfast``faster``fast``medium``slow``slower``veryslow`
## Container Formats
| Format | Extension | Best For |
|--------|-----------|----------|
| MP4 | .mp4 | Universal, web, mobile |
| MOV | .mov | Apple ecosystem, ProRes |
| WebM | .webm | Web (VP9) |
| MKV | .mkv | Archive, multiple streams |
| GIF | .gif | Short animations (no audio) |
## Input/Output Options
### Input Options (before -i)
| Option | Purpose | Example |
|--------|---------|---------|
| -ss | Seek to time | `-ss 00:01:30` |
| -t | Duration limit | `-t 00:00:30` |
| -r | Input framerate | `-r 30` |
| -f | Force format | `-f gif` |
### Output Options (after -i)
| Option | Purpose | Example |
|--------|---------|---------|
| -y | Overwrite output | `-y` |
| -n | Never overwrite | `-n` |
| -movflags faststart | Web streaming | `-movflags faststart` |
| -pix_fmt | Pixel format | `-pix_fmt yuv420p` |
| -an | No audio | `-an` |
| -vn | No video | `-vn` |
## Useful Patterns
### Get Duration in Seconds
```bash
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4
```
### Get Resolution
```bash
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 input.mp4
```
### Get Frame Count
```bash
ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 input.mp4
```
### Create Thumbnail
```bash
# At specific time
ffmpeg -i input.mp4 -ss 00:00:05 -vframes 1 thumbnail.jpg
# Best quality
ffmpeg -i input.mp4 -ss 00:00:05 -vframes 1 -q:v 2 thumbnail.jpg
```
### Create GIF from Video
```bash
# Simple (large file)
ffmpeg -i input.mp4 -vf "fps=10,scale=480:-1" output.gif
# With palette (better quality, smaller)
ffmpeg -i input.mp4 -vf "fps=10,scale=480:-1,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output.gif
```
### Picture-in-Picture
```bash
# Overlay small video in corner
ffmpeg -i main.mp4 -i overlay.mp4 \
-filter_complex "[1:v]scale=320:-1[pip];[0:v][pip]overlay=W-w-20:H-h-20" \
-c:a copy output.mp4
```
### Side-by-Side Videos
```bash
ffmpeg -i left.mp4 -i right.mp4 \
-filter_complex "[0:v][1:v]hstack=inputs=2[v]" \
-map "[v]" -c:v libx264 output.mp4
```
## Remotion Integration Notes
- Remotion uses `<OffthreadVideo>` which handles most formats
- Prefer H.264 (libx264) in MP4 container
- Always use `-movflags faststart` for web playback
- Match fps to composition (usually 30fps)
- Resolution should match composition (1920x1080 typical)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
---
name: flux-best-practices
description: Comprehensive guide for BFL FLUX image generation models. Covers prompting, T2I, I2I, structured JSON, hex colors, typography, multi-reference editing, and model-specific best practices for FLUX.2 and FLUX.1 families.
metadata:
author: Black Forest Labs
version: "1.0.0"
tags: flux, bfl, image-generation, prompting, t2i, i2i
---
# FLUX Best Practices
Use this skill when generating prompts for any BFL FLUX model to ensure optimal image quality and accurate prompt interpretation.
## When to Use
- Creating prompts for FLUX.2 or FLUX.1 models
- Text-to-image (T2I) generation
- Image-to-image (I2I) editing with FLUX.2 models
- Structured scene generation with JSON
- Typography and text rendering
- Multi-reference style transfer
- Color-accurate brand generations
## Quick Reference
### Prompt Structure Formula
```
[Subject] + [Action/Pose] + [Style/Medium] + [Context/Setting] + [Lighting] + [Camera/Technical]
```
### Model Selection
| Use Case | Recommended Model | Notes |
| ------------------- | ----------------- | -------------------------------------- |
| Fastest generation | FLUX.2 [klein] | 4B or 9B, sub-second |
| Highest quality | FLUX.2 [max] | Best detail, grounding search |
| Production balanced | FLUX.2 [pro] | Quality + speed |
| Typography/text | FLUX.2 [flex] | Best text rendering |
| Local/development | FLUX.2 [dev] | Open weights |
| Image editing | FLUX.2 [pro/max] | Pass image URL directly to input_image |
| Inpainting | FLUX.1 Fill | Object removal/completion |
| Context editing | FLUX.1 Kontext | Older model, prefer FLUX.2 |
### Critical Rules
1. **NO negative prompts** - FLUX does not support negative prompts; describe what you want
2. **Be specific** - Vague prompts produce mediocre results
3. **Use natural language** - Prose/narrative style works best
4. **Specify lighting** - Lighting has the biggest impact on quality
5. **Quote text** - Use "quoted text" for typography rendering
6. **Hex colors** - Use #RRGGBB format with color description
## Related
For API integration (endpoints, polling, webhooks), see the **bfl-api** skill.
## Rules Reference
Read individual rule files for detailed guidance:
- [rules/core-principles.md](rules/core-principles.md) - Universal FLUX prompting principles
- [rules/flux2-models.md](rules/flux2-models.md) - FLUX.2 family: klein, max, pro, flex, dev
- [rules/flux1-models.md](rules/flux1-models.md) - FLUX.1 family: older generation of FLUX.2 models - pro, Kontext, Fill
- [rules/t2i-prompting.md](rules/t2i-prompting.md) - Text-to-image prompting patterns
- [rules/i2i-prompting.md](rules/i2i-prompting.md) - Image-to-image editing with FLUX.2
- [rules/json-structured-prompting.md](rules/json-structured-prompting.md) - Complex scene composition
- [rules/hex-color-prompting.md](rules/hex-color-prompting.md) - Precise color specification
- [rules/typography-text.md](rules/typography-text.md) - Text rendering and typography
- [rules/multi-reference-editing.md](rules/multi-reference-editing.md) - Multi-image references
- [rules/negative-prompt-alternatives.md](rules/negative-prompt-alternatives.md) - Positive alternatives
- [rules/model-selection-guide.md](rules/model-selection-guide.md) - Choosing the right model
## Example Prompt
```
A weathered fisherman in his 70s with deep wrinkles and a salt-and-pepper beard,
wearing a navy cable-knit sweater, standing at the helm of his wooden boat.
Golden hour sunlight from the left creates dramatic rim lighting on his profile.
Shot on Hasselblad with 85mm lens at f/2.8, shallow depth of field with harbor
lights creating soft bokeh in the background. Kodak Portra 400 color science.
```
@@ -0,0 +1,51 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Core Principles (core)
**Impact:** CRITICAL
**Description:** Universal prompting principles that apply to all FLUX models. Master these before diving into specific techniques.
## 2. Model Selection (model, flux2, flux1)
**Impact:** HIGH
**Description:** Choosing the right FLUX model for your use case. Covers both FLUX.2 (latest) and FLUX.1 (legacy) model families.
## 3. Text-to-Image Prompting (t2i)
**Impact:** HIGH
**Description:** Crafting effective prompts for generating images from text descriptions.
## 4. Image-to-Image Editing (i2i)
**Impact:** HIGH
**Description:** Techniques for editing and transforming existing images using FLUX.2 models.
## 5. JSON Structured Prompting (json)
**Impact:** MEDIUM-HIGH
**Description:** Using structured JSON for complex multi-element scene composition.
## 6. Color Specification (hex)
**Impact:** MEDIUM
**Description:** Precise color control using hex codes for brand-accurate generations.
## 7. Typography and Text (typography)
**Impact:** MEDIUM
**Description:** Rendering text and typography within generated images.
## 8. Multi-Reference Editing (multi)
**Impact:** MEDIUM
**Description:** Combining multiple reference images for style transfer and composition.
## 9. Positive Prompt Alternatives (negative)
**Impact:** MEDIUM
**Description:** Strategies for achieving results without negative prompts, which FLUX does not support.
@@ -0,0 +1,138 @@
---
name: core-principles
description: Universal prompting principles that apply to all FLUX models
---
# Core FLUX Prompting Principles
These principles apply to all FLUX models and form the foundation of effective prompting.
## 1. Positive Descriptions Only
FLUX does NOT support negative prompts. Always describe what you WANT, not what you don't want.
### Wrong Approach
```
a portrait of a woman, no glasses, no hat, no makeup
```
### Correct Approach
```
a portrait of a woman with natural skin, clear face, bare head, visible eyes
```
See [negative-prompt-alternatives.md](negative-prompt-alternatives.md) for comprehensive replacement strategies.
## 2. Prompt Structure Formula
Build prompts using this structure for consistent results:
```
[Subject] + [Action/Pose] + [Style/Medium] + [Context/Setting] + [Lighting] + [Technical Details]
```
### Example
```
A young woman with flowing auburn hair (subject)
dancing gracefully in mid-leap (action)
in the style of classical oil painting (style)
in a moonlit garden with roses (context)
soft diffused moonlight with subtle rim lighting (lighting)
medium shot, shallow depth of field (technical)
```
## 3. Specificity Matters
More specific prompts yield dramatically better results.
### Vague (Poor Results)
```
a cat sitting
```
### Specific (Excellent Results)
```
A fluffy orange tabby cat with bright green eyes sitting regally on a vintage
velvet armchair, afternoon sunlight streaming through lace curtains, warm
golden hour lighting, shallow depth of field, shot on medium format film
```
## 4. Natural Language Works Best
Write prompts as descriptive prose rather than keyword lists.
### Keyword Style (Less Effective)
```
woman, portrait, beautiful, blonde, studio, professional, 8k, detailed
```
### Prose Style (More Effective)
```
A professional studio portrait of a beautiful blonde woman in her thirties,
captured with soft studio lighting that accentuates her features, rendered
in stunning detail with natural skin texture and subtle catchlights in her eyes
```
## 5. Lighting is Critical
Always specify lighting - it has the single greatest impact on image quality.
### Natural Lighting
- Golden hour - warm, soft, directional
- Overcast - soft, diffused, even
- Harsh midday - high contrast, strong shadows
- Dappled forest light - specular, organic patterns
### Studio Lighting
- Softbox - even, professional
- Rim light - edge definition, separation
- Butterfly lighting - beauty, glamour
- Rembrandt lighting - dramatic, classic portraits
### Atmospheric Lighting
- Volumetric fog - depth, mystery
- God rays - dramatic, spiritual
- Neon glow - urban, cyberpunk
- Candlelight - warm, intimate
### Mood-Based Lighting
- Dramatic shadows - tension, noir
- High key - bright, airy, clean
- Low key - moody, mysterious
- Chiaroscuro - strong contrast, painterly
## 6. Word Order Matters
FLUX prioritizes elements that appear earlier in the prompt. Front-load important elements.
### Less Effective
```
A forest background with soft lighting where a knight in shining armor stands
```
### More Effective
```
A knight in shining armor stands in a forest, soft dappled lighting filtering
through the canopy
```
## 7. Medium Prompt Length
Optimal prompt length is typically 30-80 words (FLUX can handle up to 512 tokens).
- Too short: Lacks direction, generic results
- Too long: Can become unfocused
- Sweet spot: Enough detail to guide, not so much it confuses
## 8. Iterative Refinement
Build prompts iteratively:
1. Start with core subject and action
2. Add style and medium
3. Specify lighting and atmosphere
4. Include technical details
5. Refine based on results
Change one element at a time to understand what affects your output.
@@ -0,0 +1,176 @@
---
name: flux1-models
description: Prompting guidelines for FLUX.1 model family
---
# FLUX.1 Model Family
> **Tip:** FLUX.2 models are the latest generation and recommended for most use cases. FLUX.1 models are still available for specific needs.
Guide to FLUX.1 models and their specialized capabilities.
## Model Overview
| Model | Purpose | Notes |
|-------|---------|-------|
| FLUX1.1 [pro] | Text-to-image | FLUX.2 [pro] offers improved results |
| FLUX.1 Kontext | Image-to-image | FLUX.2 with references recommended |
| FLUX.1 Kontext Max | Image-to-image | FLUX.2 [max] with references recommended |
| FLUX.1 Fill | Inpainting | Useful for specific inpainting tasks |
## FLUX1.1 [pro]
Fast and reliable text-to-image generation.
### Characteristics
- Strong prompt adherence
- Production-grade architecture
- Consistent, reliable results
- Scalable for high-volume
- Pricing: $0.04 per image
### Prompting Style
Standard descriptive prompts with clear subject and style specification.
### Example Prompt
```
A golden retriever puppy playing in autumn leaves, warm afternoon sunlight,
shallow depth of field with bokeh background, joyful expression, professional
pet photography style
```
## FLUX.1 Kontext - Image Editing
> **Recommendation:** FLUX.2 models with reference images provide improved editing results.
Context-aware image-to-image editing model for transformations and modifications.
### Characteristics
- Understands image context
- Preserves unedited regions
- Style transfer capabilities
- Object modification
- Basic to complex transformations
### Prompting Strategies
#### Basic Edits
Simple, direct instructions work best:
```
Change the color of the car to red
```
```
Make the sky sunset orange and pink
```
#### Controlled Edits
Be explicit about what to preserve:
```
Change the setting to nighttime while maintaining the exact same
composition and the painting's artistic style
```
#### Complex Transformations
For dramatic changes, be specific about preservation:
```
Transform the modern office into a Victorian library with the same
furniture arrangement. Keep the window positions and overall room
proportions identical.
```
#### Style Transfer
Reference specific artistic movements:
```
Transform this photograph into a Bauhaus art style with geometric
shapes and primary colors, maintaining the original composition
and subject positioning
```
#### Text Editing
Describe text placement and integration:
```
Add the text "OPEN" as a neon sign in the window, red glowing letters
with slight reflection on the glass, matching the nighttime atmosphere
```
### Tips for Kontext
- Be explicit about what should NOT change
- Start with simpler edits, build complexity
- Specify style preservation when needed
- Use for incremental refinement
## FLUX.1 Kontext Max
Advanced multi-reference editing for complex compositions.
### Characteristics
- Handles up to 10 reference images
- Best editing consistency across references
- Complex scene composition
- Character consistency maintenance
- Rate limit: 6 concurrent requests
### Multi-Reference Prompting
#### Natural Language References
Describe relationships between images naturally:
```
The person from image 1 is sitting in the cafe from image 2,
wearing the outfit from image 3, with the lighting style of image 4
```
#### Explicit Indexing
Reference images by number for precision:
```
Replace the top half of the person in image 1 with the clothing
from image 2, maintaining the pose and background
```
### Tips for Kontext Max
- Plan your reference images carefully
- Use natural language for relationships
- Specify which elements come from which image
- Leverage for character consistency across scenes
## FLUX.1 Fill - Inpainting
Specialized tool for object removal and area completion.
### Characteristics
- Clean object removal
- Intelligent background completion
- Texture-aware filling
- Seamless blending
### Use Cases
- Remove unwanted objects from photos
- Complete partial images
- Replace specific regions
- Clean up image artifacts
### Prompting for Fill
Describe what should fill the masked area:
```
Fill with continuation of the brick wall texture and ivy
```
```
Complete with matching ocean waves and sandy beach
```
### Tips for Fill
- Provide context about surrounding areas
- Specify texture and pattern continuation
- Describe lighting consistency
- Use for cleanup and removal tasks
@@ -0,0 +1,202 @@
---
name: flux2-models
description: Prompting guidelines specific to FLUX.2 model family
---
# FLUX.2 Model Family
Complete guide to FLUX.2 variants and their optimal prompting strategies.
> **Key Feature:** All FLUX.2 models natively support both text-to-image generation AND image-to-image editing via reference images. There's no need to use legacy FLUX.1 Kontext models for editing tasks.
## Model Overview
| Model | Parameters | Best For | Speed | Reference Images |
|-------|------------|----------|-------|------------------|
| [klein] | 4B/9B | Fast iterations, previews, quick edits | Fastest | Up to 4 |
| [max] | - | Highest quality generation & editing | Slowest | Up to 8-10 |
| [pro] | - | Production balanced | Medium | Up to 8 |
| [flex] | - | Typography, text rendering | Medium | Up to 8 |
| [dev] | - | Local development | Varies | Varies |
## FLUX.2 [klein] - Fast Generation
Best for rapid prototyping, previews, and high-volume generation.
### Characteristics
- 4B or 9B parameter versions available
- Sub-second generation times
- Optimized for speed over maximum detail
- **No prompt upsampling** - be descriptive yourself
- Supports up to 4 reference images
### Prompting Style: Narrative Prose
Klein responds best to descriptive, narrative-style prompts with emphasis on lighting and atmosphere.
### Example Prompt
```
A cozy coffee shop interior bathed in warm afternoon light, steam rising lazily
from ceramic cups, worn leather armchairs arranged around small wooden tables,
bookshelves lining exposed brick walls, the soft atmosphere of a quiet afternoon
with dust motes floating in sunbeams through tall windows
```
### Tips for [klein]
- Write like a novelist describing a scene
- Front-load your subject (word order critical)
- Emphasize lighting descriptions heavily
- Keep prompts moderately detailed (40-70 words)
## FLUX.2 [max] - Highest Quality
Premium model for final production assets and maximum detail.
### Characteristics
- Highest detail and coherence
- Best editing consistency
- Vast world knowledge
- Includes grounding search (real-time web data)
- Strongest prompt following
- Supports up to 8 reference images (API), 10 (playground)
### Prompting Style: Technical + Descriptive
[max] excels with detailed technical specifications combined with descriptive prose.
### Example Prompt
```
Portrait of a weathered fisherman, age 70, deep wrinkles telling stories of
decades at sea, salt-and-pepper beard with streaks of white, wearing a navy
cable-knit sweater with visible wool texture. Shot on Hasselblad X2D with
90mm f/2.8 lens at f/4, golden hour natural light from the left creating
strong rim lighting, shallow depth of field with soft bokeh from harbor
lights behind, Kodak Portra 400 color science with natural grain
```
### Tips for [max]
- Include camera and lens specifications for photorealism
- Specify film stock or digital sensor characteristics
- Use technical photography terms (aperture, focal length)
- Leverage grounding search for current events: "news photo of [recent event]"
## FLUX.2 [pro] - Production Balanced
Optimal balance of quality and speed for production workflows.
### Characteristics
- Good quality-to-speed ratio
- Reliable, consistent output
- Suitable for batch processing
- Supports prompt upsampling
- Supports up to 8 reference images
### Prompting Style: Balanced Detail
Standard detailed prompts work well without excessive technical specification.
### Example Prompt
```
A modern minimalist living room with floor-to-ceiling windows overlooking
a city skyline at dusk, clean white furniture with subtle textures, a single
statement plant in the corner, warm ambient lighting from hidden sources,
architectural photography style with clean lines and balanced composition
```
### Tips for [pro]
- Balance specificity with generation speed
- Good for template-based prompt systems
- Enable prompt upsampling for enhanced results
- Consistent quality for production pipelines
## FLUX.2 [flex] - Typography Specialist
Optimized for text rendering and typographic content.
### Characteristics
- Superior text rendering quality
- Handles multiple text elements
- Adjustable steps (1-50) and guidance (1.5-10)
- Best for signage, posters, UI mockups
- Supports up to 8 reference images
### Prompting Style: Typography-Focused
Always quote text and specify font characteristics explicitly.
### Example Prompt
```
A modern minimalist poster design with the headline "DESIGN SUMMIT 2025"
in bold condensed sans-serif typography centered in the upper third,
subtitle "Innovation Meets Creativity" in lighter weight below,
date "MARCH 15-17" in small caps at the bottom, all text in white
on a gradient background transitioning from deep purple #4A0080 to
coral #FF6B6B, clean geometric accent lines, professional print quality
```
### Tips for [flex]
- Always quote exact text: `"Your Text Here"`
- Specify font style: serif, sans-serif, script, display, monospace
- Describe text hierarchy: headline, subhead, body
- Include placement: centered, left-aligned, upper third
- Adjust steps (higher = better quality) and guidance (higher = stricter)
## FLUX.2 [dev] - Local Development
For local development, testing, and non-commercial use.
### Characteristics
- Open weights on Hugging Face
- Runs locally (~13GB VRAM recommended)
- Full customization available
- Free for non-commercial use
- Base variants available (undistilled) for fine-tuning
### Prompting Style: Standard
Same prompting patterns as [pro] work well.
### Tips for [dev]
- Use for development and testing before production
- Experiment with prompt variations
- Good for fine-tuning experiments
- Check license for commercial use restrictions
## Image-to-Image Editing with FLUX.2
All FLUX.2 models support image editing via reference images. This replaces the need for legacy FLUX.1 Kontext models.
### How It Works
1. Provide your source image(s) as reference images
2. Describe the desired changes in your prompt
3. The model preserves context while applying edits
### Example: Style Transfer
```
Reference: [your source image]
Prompt: Transform this image into a watercolor painting style,
maintaining the exact composition and subject positioning
```
### Example: Object Modification
```
Reference: [your source image]
Prompt: Change the car color to red while keeping everything else identical
```
### Example: Character Consistency
```
Reference: [character reference image]
Prompt: The same person from the reference image walking through
a busy Tokyo street at night, neon lights reflecting on wet pavement
```
### Model Selection for Editing
| Use Case | Recommended Model |
|----------|-------------------|
| Quick iterations/previews | FLUX.2 [klein] |
| Production quality edits | FLUX.2 [pro] |
| Maximum quality/complex edits | FLUX.2 [max] |
| Text/typography edits | FLUX.2 [flex] |
@@ -0,0 +1,210 @@
---
name: hex-color-prompting
description: Using hex color codes for precise color specification
---
# Hex Color Prompting
FLUX supports hex color codes (#RRGGBB) for precise color specification, essential for brand consistency and exact color matching.
## Syntax
Include hex codes directly in your prompt with descriptive names:
```
A modern living room with walls painted in #2C3E50 (dark blue-gray),
accent pillows in #E74C3C (vibrant red), and a #F39C12 (warm amber)
throw blanket on a #ECF0F1 (off-white) sofa
```
## Signal Keywords
Use these keywords to indicate color specification:
```
color #02eb3c
hex #edfa3c
in #FF5733
using color code #3498DB
```
## Best Practices
### 1. Always Pair with Description
Never use hex codes alone - include the color name:
```
Good: #FF6B6B (coral pink)
Bad: #FF6B6B
```
### 2. Associate with Specific Objects
Clearly connect colors to their targets:
```
A product shot featuring a smartphone with a #1DA1F2 (Twitter blue) case,
resting on a #14171A (near black) matte surface
```
### 3. Limit Color Palette
3-5 colors typically work best. Too many can confuse the model:
```
Color palette for the scene: #2ECC71 (emerald green), #3498DB (sky blue),
#F1C40F (sunflower yellow), #FFFFFF (pure white)
```
## Use Cases
### Brand Colors
```
Corporate office reception with brand colors prominently featured:
walls in #0066CC (company blue), accent furniture in #FF6600 (company orange),
logo displayed in #FFFFFF (white) against the blue backdrop
```
### Interior Design
```
Scandinavian minimalist bedroom with #F5F5F5 (warm white) walls,
#8B4513 (saddle brown) wooden headboard and nightstands,
#708090 (slate gray) linen bedding, and #DAA520 (goldenrod) accent lamp
```
### Fashion
```
Editorial fashion photo: model wearing #000000 (black) cashmere turtleneck,
#FF4500 (orange-red) wide-leg wool pants, #C0C0C0 (silver) geometric earrings,
against a #F0F0F0 (light gray) studio backdrop
```
### Product Design
```
Premium headphones product shot: #1C1C1E (space gray) aluminum body,
#F5F5F7 (silver) mesh ear cups, #FF9500 (iOS orange) accent ring around controls
```
### Digital Art
```
Synthwave cityscape: #FF00FF (magenta) and #00FFFF (cyan) neon signs,
#1A1A2E (deep navy) night sky, #E94560 (hot pink) setting sun on horizon,
#16213E (dark blue) building silhouettes, rain-slicked streets reflecting lights
```
### Data Visualization
```
Infographic showing market share: segments in #2ECC71 (green) for growth,
#E74C3C (red) for decline, #3498DB (blue) for stable, #95A5A6 (gray) for other,
clean #FFFFFF (white) background
```
## Gradient Colors
Specify gradients with start and end colors:
```
Abstract background starting with color #02eb3c (bright green) and
finishing with color #edfa3c (lime yellow), smooth horizontal gradient
```
```
Sunset sky gradient from #FF6B6B (coral) at horizon through
#FFA07A (light salmon) to #87CEEB (sky blue) at top
```
## Color Harmony Patterns
### Complementary (Opposite on color wheel)
```
Scene using complementary colors: #3498DB (blue) dominant with
#E67E22 (orange) accents for visual pop
```
### Analogous (Adjacent colors)
```
Harmonious palette using analogous colors: #9B59B6 (purple),
#8E44AD (deep purple), #3498DB (blue) - flowing naturally together
```
### Triadic (Evenly spaced)
```
Vibrant triadic scheme: #E74C3C (red), #F1C40F (yellow),
#3498DB (blue) - balanced and dynamic
```
### Monochromatic (Single hue variations)
```
Sophisticated monochromatic blue: #1A5276 (dark navy), #2980B9 (medium blue),
#85C1E9 (light blue), #D4E6F1 (pale blue) - elegant depth
```
## Combining with JSON Structured Prompts
```json
{
"scene": {
"setting": "modern tech startup office",
"mood": "innovative, energetic"
},
"colors": {
"primary": "#6C5CE7 (electric purple)",
"secondary": "#00CEC9 (teal)",
"accent": "#FD79A8 (pink)",
"neutral": "#DFE6E9 (light gray)",
"dark": "#2D3436 (charcoal)"
},
"application": {
"walls": "neutral #DFE6E9",
"furniture": "dark #2D3436",
"accent_pieces": "primary #6C5CE7",
"plants": "secondary #00CEC9 pots"
}
}
```
## Common Brand Color References
For reference only - always verify current brand guidelines:
```
# Social Media
Twitter/X Blue: #1DA1F2
Facebook Blue: #1877F2
Instagram Gradient: #833AB4 to #FD1D1D
LinkedIn Blue: #0A66C2
# Tech
Apple Gray: #1C1C1E
Google Blue: #4285F4
Microsoft Blue: #00A4EF
Amazon Orange: #FF9900
# Design
Figma Purple: #A259FF
Dribbble Pink: #EA4C89
Behance Blue: #1769FF
```
## Troubleshooting
### Color Not Accurate
- Add the color name alongside hex
- Specify the exact object the color applies to
- Use fewer total colors in the prompt
### Color Bleeding
- Clearly delineate which objects get which colors
- Use spatial descriptions: "the LEFT chair in #color"
### Muddy Colors
- Check hex code accuracy
- Specify lighting that won't shift colors
- Use "maintaining exact color #XXXXXX" for emphasis
@@ -0,0 +1,293 @@
---
name: i2i-prompting
description: Image-to-image editing prompts with FLUX models
---
# Image-to-Image (I2I) Prompting
Guide to effective image-to-image editing with FLUX models.
## Overview
All FLUX.2 models support image-to-image editing via reference images:
- **FLUX.2 [klein]**: Up to 4 reference images - fast editing
- **FLUX.2 [pro]**: Up to 8 reference images - balanced quality/speed
- **FLUX.2 [max]**: Up to 8-10 reference images - highest quality editing
- **FLUX.2 [flex]**: Up to 8 reference images - best for typography edits
Simply provide your source image as a reference and describe the desired changes. The model understands image context and can modify specific elements while preserving others.
> **Note:** FLUX.2 models are recommended for image editing. They provide better results than the older FLUX.1 Kontext models.
## Providing Images
**Preferred: Use URLs directly** - simpler and more convenient than base64.
When you have an image URL, pass it directly to `input_image`:
```json
{
"prompt": "Change the background to a beach sunset",
"input_image": "https://example.com/photo.jpg"
}
```
The API fetches URLs automatically. Both URL and base64 work, but URLs are recommended when available.
## Basic Edit Patterns
### Simple Modifications
Direct, single-change instructions:
```
Change the car color to red
```
```
Make the sky a dramatic sunset
```
```
Add snow to the ground
```
### Attribute Changes
Modifying specific characteristics:
```
Change her hair color to platinum blonde
```
```
Make the building taller
```
```
Age the person to appear 20 years older
```
## Controlled Editing
### Explicit Preservation
When you need to keep specific elements unchanged:
```
Change the background to a beach scene while keeping the subject's
pose, clothing, and expression exactly the same
```
```
Transform the daytime photo to nighttime, maintaining the exact
composition, colors of the subject's outfit, and lighting direction
```
### Style Preservation
Preventing unwanted style shifts:
```
Change the season to autumn with falling leaves, but maintain
the photograph's realistic style and color grading
```
```
Add rain effects to the scene while preserving the painting's
impressionist brushwork and color palette
```
## Transformation Types
### Environmental Changes
#### Time of Day
```
Convert to golden hour lighting with warm tones and long shadows,
keeping all other elements identical
```
```
Transform to blue hour with city lights beginning to glow,
maintaining the exact composition
```
#### Weather
```
Add heavy rain with wet reflections on surfaces, dark overcast sky
```
```
Create a foggy atmosphere with reduced visibility, mysterious mood
```
#### Season
```
Transform to winter with snow covering surfaces, bare trees,
cold blue color cast
```
```
Change to spring with cherry blossoms, fresh green leaves,
soft warm lighting
```
### Style Transfer
#### Artistic Movements
```
Transform into Art Nouveau style with flowing organic lines,
decorative patterns, and muted earth tones
```
```
Convert to Pop Art style with bold primary colors, halftone dots,
and high contrast graphic treatment
```
#### Artist References
```
Reimagine in the style of Monet with visible brushstrokes,
soft focus, and impressionist color harmony
```
```
Transform to match Edward Hopper's style with dramatic lighting,
urban isolation feeling, and muted palette
```
#### Medium Conversion
```
Convert this photograph to a detailed pencil sketch with
careful shading and visible line work
```
```
Transform into a watercolor painting with soft edges,
transparent washes, and paper texture visible
```
### Subject Modifications
#### Clothing Changes
```
Change the outfit to a formal black suit with white shirt and red tie
```
```
Replace the casual clothes with traditional Japanese kimono in blue floral pattern
```
#### Expression Changes
```
Change the expression to a warm genuine smile
```
```
Make the expression more serious and contemplative
```
#### Age Modifications
```
Age the subject to appear as a wise elderly person with grey hair and wrinkles
```
```
Make the subject appear younger, around 25 years old
```
### Object Editing
#### Addition
```
Add a vintage leather briefcase in the subject's left hand
```
```
Place a steaming cup of coffee on the table
```
#### Removal
```
Remove the background people, replace with empty street
```
```
Remove the text/logo from the shirt, replace with solid color
```
#### Replacement
```
Replace the modern car with a 1960s vintage Mustang in cherry red
```
```
Swap the coffee mug for an ornate teacup with floral pattern
```
## Text Editing
### Adding Text
```
Add a neon sign reading "OPEN 24 HOURS" in the window,
glowing red letters with blue outline
```
```
Include a wooden sign with hand-painted text "Welcome Home"
mounted above the door
```
### Modifying Text
```
Change the store sign to read "BAKER'S DOZEN" in the same style
```
```
Update the poster text to "SUMMER SALE 2025" maintaining the design
```
## Complex Multi-Step Edits
For dramatic transformations, consider breaking into steps:
### Step-by-Step Approach
Instead of:
```
Transform this modern office into a Victorian library with completely
different furniture, add a fireplace, change the lighting to candlelit,
and age the photograph
```
Try sequential edits:
1. `Change the furniture style to Victorian antique pieces`
2. `Add a stone fireplace on the right wall`
3. `Transform lighting to warm candlelit atmosphere`
4. `Apply vintage photograph aesthetic with sepia tones`
## Common Pitfalls
### Avoid Vague Instructions
```
Bad: Make it look better
Good: Increase contrast, add warm color grading, sharpen details
```
### Be Specific About Scope
```
Bad: Change the background
Good: Replace the office background with a tropical beach at sunset,
maintaining the subject's exact position and lighting direction
```
### Explicit Style Preservation
```
Bad: Make it nighttime
Good: Transform to nighttime while maintaining the photorealistic style,
add appropriate artificial lighting sources
```
## Best Practices
1. **Start Simple** - Begin with single-element changes
2. **Be Explicit** - State what should change AND what should stay
3. **Reference Context** - Mention existing elements when relevant
4. **Iterate** - Refine through multiple small edits rather than one large one
5. **Preserve Deliberately** - Always specify style/composition preservation needs
@@ -0,0 +1,241 @@
---
name: json-structured-prompting
description: Using JSON format for complex scene composition
---
# JSON Structured Prompting
For complex scenes with multiple elements, spatial relationships, or production automation, use JSON-structured prompts.
## When to Use
- Multiple characters with distinct attributes
- Precise spatial positioning
- Complex scene composition
- Reproducible, template-based prompts
- Programmatic prompt generation
- Production workflows with variable substitution
## Basic Structure
```json
{
"scene": {
"setting": "description of environment",
"time": "time of day/period",
"mood": "atmospheric quality"
},
"subjects": [
{
"type": "person/object/animal",
"description": "detailed description",
"position": "location in frame",
"action": "what they're doing"
}
],
"style": {
"medium": "photography/painting/illustration",
"technique": "specific style details",
"reference": "artist or style reference"
},
"technical": {
"camera": "camera and lens",
"lighting": "lighting setup",
"composition": "framing details"
},
"colors": ["#hex1", "#hex2"]
}
```
## Single Subject Example
```json
{
"scene": {
"setting": "cozy home office with bookshelves",
"time": "late afternoon",
"mood": "focused, peaceful"
},
"subjects": [
{
"type": "person",
"description": "woman in her 30s, dark curly hair in loose bun, wearing casual cream sweater",
"position": "seated at desk, center frame",
"action": "typing on laptop, slight smile of concentration"
}
],
"style": {
"medium": "photography",
"technique": "lifestyle editorial",
"reference": "kinfolk magazine aesthetic"
},
"technical": {
"camera": "Sony A7III with 50mm f/1.8",
"lighting": "soft natural window light from left",
"composition": "medium shot, rule of thirds"
}
}
```
## Multi-Character Scene
```json
{
"scene": {
"setting": "Victorian-era drawing room with ornate wallpaper and antique furniture",
"time": "evening, candlelit",
"mood": "tense, mysterious"
},
"subjects": [
{
"id": "detective",
"type": "person",
"description": "tall man in his 50s, sharp features, grey at temples, wearing brown tweed suit",
"position": "standing center-left, facing right",
"action": "examining a letter with magnifying glass, intense focus"
},
{
"id": "lady",
"type": "person",
"description": "elegant woman in her 40s, auburn hair in Victorian updo, emerald green evening dress",
"position": "seated on chaise lounge, right side",
"action": "watching the detective with concealed anxiety, hands clasped"
},
{
"id": "butler",
"type": "person",
"description": "elderly man in formal butler attire, stoic expression",
"position": "background, near doorway",
"action": "standing at attention, observing"
}
],
"style": {
"medium": "oil painting",
"technique": "classical realism with dramatic lighting",
"reference": "Victorian narrative painting, John Singer Sargent"
},
"technical": {
"lighting": "warm candlelight as key, cool moonlight through window as fill",
"composition": "triangular arrangement of figures, detective at apex"
}
}
```
## Product Scene with Colors
```json
{
"scene": {
"setting": "minimalist product photography studio",
"mood": "clean, premium, aspirational"
},
"subjects": [
{
"type": "product",
"description": "sleek wireless earbuds in charging case",
"position": "center, slightly angled",
"details": "matte finish, subtle branding"
}
],
"style": {
"medium": "commercial photography",
"technique": "high-end product shot",
"reference": "Apple product photography"
},
"technical": {
"camera": "Phase One with 120mm macro",
"lighting": "large softbox overhead, subtle fill from below",
"composition": "centered, hero product shot"
},
"colors": {
"product": "#1A1A2E",
"accent": "#E94560",
"background": "#FFFFFF"
}
}
```
## Converting JSON to Natural Language
Flatten your JSON into flowing prose for the actual prompt:
### From JSON
```json
{
"subjects": [
{
"type": "person",
"description": "elderly craftsman with weathered hands",
"position": "seated at workbench",
"action": "carefully carving wood"
}
],
"scene": { "setting": "traditional workshop", "time": "morning" },
"technical": { "lighting": "natural window light from right" }
}
```
### To Prompt
```
An elderly craftsman with weathered hands seated at his workbench in a
traditional workshop, carefully carving wood with focused precision.
Morning natural light streams through the window from the right,
illuminating the wood shavings and tools scattered across the worn surface.
```
## Template Variables
Use JSON structure for template-based generation:
```json
{
"template": "product_hero",
"variables": {
"product_name": "{{PRODUCT_NAME}}",
"product_color": "{{PRODUCT_COLOR}}",
"brand_primary": "{{BRAND_HEX_1}}",
"brand_secondary": "{{BRAND_HEX_2}}",
"background_style": "{{BG_STYLE}}"
},
"prompt_template": "Professional product photography of {{PRODUCT_NAME}} in {{PRODUCT_COLOR}}, brand colors {{BRAND_HEX_1}} and {{BRAND_HEX_2}} accents, {{BG_STYLE}} background, commercial quality"
}
```
## Spatial Relationships
Define explicit spatial relationships:
```json
{
"composition": {
"layout": "triangular",
"focal_point": "center-left intersection",
"depth_layers": [
{
"layer": "foreground",
"elements": ["flowers in vase"],
"focus": "soft blur"
},
{
"layer": "midground",
"elements": ["main subject"],
"focus": "sharp"
},
{
"layer": "background",
"elements": ["window", "garden view"],
"focus": "soft blur"
}
]
}
}
```
## Best Practices
1. **Use IDs for References** - Give subjects IDs when they interact
2. **Separate Concerns** - Keep scene, subjects, style, and technical distinct
3. **Be Consistent** - Use the same terminology throughout
4. **Include All Details** - Don't assume, specify everything
5. **Flatten for Execution** - Convert to natural language before sending to model
6. **Version Templates** - Track template versions for reproducibility
@@ -0,0 +1,246 @@
---
name: model-selection-guide
description: Choosing the right FLUX model for your use case
---
# FLUX Model Selection Guide
Decision guide for selecting the optimal FLUX model based on your requirements.
## Quick Decision Matrix
| Priority | Recommended Model |
| ------------- | ------------------------------- |
| Speed | FLUX.2 [klein] |
| Quality | FLUX.2 [max] |
| Balance | FLUX.2 [pro] |
| Typography | FLUX.2 [flex] |
| Image Editing | FLUX.2 [klein], [pro], or [max] |
| Local/Free | FLUX.2 [dev] |
| Inpainting | FLUX.1 Fill |
## Decision Tree
```
What's your primary need?
├─ Generate images (text-to-image OR image editing)
│ ├─ Need FASTEST possible generation?
│ │ └─ FLUX.2 [klein] (supports up to 4 reference images)
│ │
│ ├─ Need HIGHEST quality output?
│ │ └─ FLUX.2 [max] (supports up to 8-10 reference images)
│ │
│ ├─ Need TEXT/TYPOGRAPHY in image?
│ │ └─ FLUX.2 [flex] (supports up to 8 reference images)
│ │
│ ├─ Need BALANCED speed/quality?
│ │ └─ FLUX.2 [pro] (supports up to 8 reference images)
│ │
│ └─ Need LOCAL/FREE generation?
│ └─ FLUX.2 [dev]
├─ Need REAL-TIME web information?
│ └─ FLUX.2 [max] (grounding search)
└─ FLUX.1 family (only use when explicitly asked by user)
├─ FLUX.1 Kontext - context-aware editing
└─ FLUX.1 Fill - inpainting/object removal
```
**Note:** All FLUX.2 models natively support image-to-image editing via reference images. Simply provide your source image(s) as references and describe the desired changes.
## Detailed Model Comparisons
### By Speed
| Model | Relative Speed | Best For |
| ----------------- | -------------- | ----------------------- |
| FLUX.2 [klein] 4B | Fastest | Rapid prototyping |
| FLUX.2 [klein] 9B | Very Fast | Better quality previews |
| FLUX.2 [pro] | Medium | Production workflows |
| FLUX.2 [flex] | Medium | Typography tasks |
| FLUX.2 [max] | Slower | Final hero images |
### By Quality
| Model | Quality Level | Trade-off |
| ----------------- | ------------- | -------------------------- |
| FLUX.2 [max] | Highest | Slowest, most expensive |
| FLUX.2 [pro] | High | Good balance |
| FLUX.2 [flex] | High (text) | Specialized for typography |
| FLUX.2 [klein] 9B | Good | Fast, slightly less detail |
| FLUX.2 [klein] 4B | Moderate | Fastest, preview quality |
### By Cost
> **Credit pricing:** 1 credit = $0.01 USD. FLUX.2 uses megapixel-based pricing.
#### FLUX.2 Models
| Model | 1st MP | +MP | 1MP T2I | 1MP I2I | Volume Recommendation |
| ----------------- | ------ | ---- | ------- | ------- | --------------------------- |
| FLUX.2 [klein] 4B | 1.4c | 0.1c | $0.014 | $0.015 | High volume, previews |
| FLUX.2 [klein] 9B | 1.5c | 0.2c | $0.015 | $0.017 | High volume, better quality |
| FLUX.2 [pro] | 3c | 1.5c | $0.03 | $0.045 | Production workloads |
| FLUX.2 [max] | 7c | 3c | $0.07 | $0.10 | Hero images, premium |
| FLUX.2 [flex] | 5c | 5c | $0.05 | $0.10 | Typography |
| FLUX.2 [dev] | - | - | Free | Free | Local dev (non-commercial) |
> **Pricing formula:** `(firstMP + (outputMP-1) * mpPrice) + (inputMP * mpPrice)` in cents
#### FLUX.1 Models
| Model | Price/Image | Use Case |
| -------------------- | ----------- | ----------------------- |
| FLUX.1 Kontext [pro] | $0.04 | Context-aware editing |
| FLUX.1 Kontext [max] | $0.08 | Max quality editing |
| FLUX1.1 [pro] | $0.04 | Standard T2I |
| FLUX1.1 [pro] Ultra | $0.06 | Ultra high-resolution |
| FLUX1.1 [pro] Raw | $0.06 | Candid photography feel |
| FLUX.1 Fill [pro] | $0.05 | Inpainting |
| FLUX.1 [pro] | $0.05 | Original pro model |
> Use [bfl.ai/pricing](https://bfl.ai/pricing) calculator for exact costs at different resolutions.
## Use Case Recommendations
### Creative Exploration / Ideation
**Recommended: FLUX.2 [klein]**
- Fast iterations
- Quick concept testing
- Mood board generation
- Exploring prompt variations
### Production Marketing Assets
**Recommended: FLUX.2 [pro]**
- Consistent quality
- Reasonable speed
- Cost-effective at scale
- Reliable for automation
### Hero Images / Premium Content
**Recommended: FLUX.2 [max]**
- Maximum detail
- Best coherence
- Supports grounding search
- Worth the premium for key visuals
### Typography / Signage / Posters
**Recommended: FLUX.2 [flex]**
- Superior text rendering
- Adjustable quality settings
- Best for readable text
- UI mockups and infographics
### Character Consistency
**Recommended: FLUX.2 [max] or [pro]**
- Multi-reference support (up to 8-10 images)
- Best editing consistency
- Maintains identity across scenes
- Superior quality over FLUX.1 Kontext
### Photo Editing / Retouching
**Recommended: FLUX.2 [klein], [pro], or [max]**
- Native image-to-image support via references
- Style transfer
- Object modification
- Attribute changes
- Better results than FLUX.1 Kontext
### Real-Time Information
**Recommended: FLUX.2 [max]**
- Grounding search feature
- Current events
- Recent news visualization
- Weather/location data
### Local Development / Testing
**Recommended: FLUX.2 [dev]**
- No API costs
- Full control
- Fine-tuning experiments
- Offline capability
### Editorial with Typography
```
1. FLUX.2 [max] - Generate base image (highest quality)
2. FLUX.2 [flex] - Add text overlay pass
```
### Character-Consistent Series
```
1. FLUX.2 [max] - Create character reference
2. FLUX.2 [max]/[pro] - Generate consistent variations using reference images
3. FLUX.2 [klein] - Quick iteration on variations if needed
```
### E-commerce Product Pipeline
```
1. FLUX.2 [pro] - Bulk product generations
2. FLUX.2 [pro]/[klein] - Product variations (colors, angles) using references
3. FLUX.2 [flex] - Add promotional text/pricing
```
## Constraint-Based Selection
### Limited Budget
- **High volume**: FLUX.2 [klein] 4B
- **Quality needed**: FLUX.2 [pro] (best value)
### Tight Deadline
- **Any task**: FLUX.2 [klein]
- **Quality matters**: FLUX.2 [pro]
### Maximum Quality Required
- **Always**: FLUX.2 [max]
### Text Must Be Readable
- **Always**: FLUX.2 [flex]
### Editing Existing Images
- **Fast edits**: FLUX.2 [klein] with reference images
- **Quality edits**: FLUX.2 [max] or [pro] with reference images
- **Alternative**: FLUX.1 Kontext (FLUX.2 preferred)
### Rate Limit Sensitivity
- **Prefer**: FLUX.2 models (24 concurrent limit)
- **Avoid**: FLUX.1 Kontext Max (6 concurrent limit)
## Summary Cheat Sheet
```
Speed? → FLUX.2 [klein]
Quality? → FLUX.2 [max]
Balance? → FLUX.2 [pro]
Text? → FLUX.2 [flex]
Edit? → FLUX.2 [klein/pro/max] with reference images
Free? → FLUX.2 [dev]
```
**Key insight:** All FLUX.2 models support image editing natively via reference images. FLUX.2 is recommended over FLUX.1 Kontext for editing tasks.
@@ -0,0 +1,241 @@
---
name: multi-reference-editing
description: Using multiple reference images for complex compositions
---
# Multi-Reference Image Editing
Guide to using multiple reference images for character consistency, style transfer, and complex compositions.
## Overview
FLUX.2 models support multiple reference images for advanced editing:
- **FLUX.2 [klein]**: Up to 4 reference images - fast editing
- **FLUX.2 [pro]**: Up to 8 via API - balanced quality/speed
- **FLUX.2 [max]**: Up to 8 via API, 10 in playground - highest quality
- **FLUX.2 [flex]**: Up to 8 via API - best for typography
> **Note:** FLUX.2 models are recommended over FLUX.1 Kontext Max for better results.
## Providing Images
**Preferred: Use URLs directly** - simpler and more convenient than base64.
Pass image URLs directly to `input_image`, `input_image_2`, etc.:
```json
{
"prompt": "Person from image 1 wearing outfit from image 2",
"input_image": "https://example.com/person.jpg",
"input_image_2": "https://example.com/outfit.jpg"
}
```
The API fetches URLs automatically. Both URL and base64 work, but URLs are recommended when available.
## Reference Methods
### Natural Language Description
Describe relationships between images naturally:
```
The person from image 1 is sitting at the cafe table from image 2,
wearing the outfit from image 3, with the warm lighting style of image 4
```
### Explicit Indexing
Reference images by number for precision:
```
Replace the background of image 1 with the landscape from image 2,
maintaining the subject's exact position and lighting
```
```
Combine the face from image 1 with the hairstyle from image 2
on the body pose from image 3
```
## Use Cases
### Character Consistency
Maintain the same character across multiple scenes:
```
Input: Reference image of character
Prompt: The character from image 1 walking through a busy Tokyo street
at night, neon lights reflecting on wet pavement
```
For sequential consistency:
```
The same person from image 1, now seated at a desk in a modern office,
same clothing and hairstyle, different environment
```
### Style Transfer
Apply the style of one image to another:
```
Transform image 1 into the artistic style of image 2,
maintaining the original composition and subject
```
```
Apply the color grading and mood from image 2 to the scene in image 1
```
### Pose Guidance
Use a reference for body positioning:
```
The person from image 1 in the exact pose shown in image 2,
placed in the environment from image 3
```
### Object Composition
Combine elements from multiple images:
```
Place the product from image 1 on the table setting from image 2,
using the lighting style from image 3
```
### Background Replacement
```
Keep the subject from image 1 exactly as shown, replace the background
with the beach scene from image 2, match the lighting naturally
```
## Multi-Character Scenes
### Two Characters
```
Image 1 (person A) and image 2 (person B) having a conversation
at a coffee shop table, person A on the left gesturing, person B
on the right listening intently
```
### Group Composition
```
The three people from images 1, 2, and 3 standing together for a
group photo, arranged left to right in that order, friendly poses,
outdoor park setting
```
## Attribute Mixing
### Selective Attribute Transfer
```
The face and expression from image 1, the hairstyle from image 2,
wearing the outfit from image 3, in the pose from image 4
```
### Partial Transfer
```
Apply only the color palette from image 2 to image 1,
keeping all other aspects (style, composition, lighting) unchanged
```
## Collage Method
Use a collage input for layout guidance:
```
Arrange the scene using the layout shown in the collage input:
- Person from image 1 in the left position
- Object from image 2 in the center position
- Background element from image 3 filling the right side
```
## Best Practices
### 1. Clear Image Roles
Specify what each reference provides:
```
Image 1: face/identity reference
Image 2: pose/body reference
Image 3: style/aesthetic reference
Image 4: environment/background reference
```
### 2. Quality References
- Use high-quality, clear reference images
- Ensure good lighting in references
- Avoid heavily processed or filtered images
### 3. Consistent Lighting
When combining elements:
```
...ensure the lighting direction matches across all elements,
with main light source from the upper left
```
### 4. Resolution Awareness
For [pro] API with 9MP total limit:
- At 1MP output: up to 8 reference images comfortably
- Calculate: input images + output = total MP
### 5. Explicit Relationships
Don't assume - specify exactly how elements relate:
```
Vague: The person and the background together
Better: The person from image 1 standing in the foreground,
the beach from image 2 visible behind them at a distance
```
## Complex Composition Example
```
Create a scene combining:
- The woman from image 1 (keep exact face, expression, hair)
- Wearing the vintage dress from image 2 (exact pattern and cut)
- In the pose from image 3 (seated position, arm placement)
- Set in the library from image 4 (bookshelves, furniture)
- Using the warm lighting style from image 5 (golden hour quality)
Position her in the center of frame, medium shot, looking slightly
to the right with a thoughtful expression.
```
## Troubleshooting
### Elements Not Transferring
- Be more specific about which element from which image
- Use explicit indexing ("from image 1")
- Reduce the number of references and complexity
### Inconsistent Blending
- Specify lighting consistency
- Describe how elements should interact
- Use style references to unify the composition
### Identity Drift
- Emphasize key identifying features
- Use phrases like "maintaining exact likeness"
- Provide multiple angles of the same subject if available
@@ -0,0 +1,202 @@
---
name: negative-prompt-alternatives
description: Positive alternatives to negative prompts
---
# Negative Prompt Alternatives
FLUX does not support negative prompts. This guide provides positive alternatives for common negative prompt patterns.
## Why No Negative Prompts?
Negative prompts can actually make models focus MORE on unwanted elements. Instead, describe exactly what you DO want - this gives clearer direction and better results.
## Replacement Strategy
For any unwanted element:
1. Identify what you don't want
2. Ask: "What would be there instead?"
3. Describe the positive alternative
## Common Replacements
### People/Crowds
| Instead of | Use |
|-----------|-----|
| "no people" | "empty", "deserted", "solitary", "abandoned" |
| "no crowds" | "quiet", "peaceful", "secluded", "private" |
| "without background people" | "isolated subject", "clean background", "solo figure" |
**Example:**
```
Bad: A beach scene, no people
Good: A deserted beach at dawn, pristine untouched sand, solitary seagull
```
### Skin/Appearance
| Instead of | Use |
|-----------|-----|
| "no makeup" | "natural skin", "bare face", "fresh-faced" |
| "no blemishes" | "clear skin", "smooth complexion", "healthy glow" |
| "no wrinkles" | "youthful skin", "smooth features" |
**Example:**
```
Bad: Portrait of woman, no makeup, no blemishes
Good: Portrait of a woman with natural clear skin, fresh-faced with a healthy glow
```
### Accessories
| Instead of | Use |
|-----------|-----|
| "no glasses" | "visible eyes", "unobstructed gaze", "clear eye contact" |
| "no hat" | "bare head", "visible hair", "uncovered head" |
| "no jewelry" | "minimal accessories", "understated", "unadorned" |
**Example:**
```
Bad: Man portrait, no glasses, no hat
Good: Portrait of a man with clear direct gaze, wind-swept visible hair
```
### Colors
| Instead of | Use |
|-----------|-----|
| "no color" | "monochrome", "black and white", "grayscale" |
| "not colorful" | "muted tones", "subdued palette", "desaturated" |
| "no bright colors" | "neutral tones", "earth tones", "soft pastels" |
**Example:**
```
Bad: Landscape photo, no bright colors
Good: Landscape in muted earth tones, soft morning light, desaturated palette
```
### Text/Watermarks
| Instead of | Use |
|-----------|-----|
| "no text" | "clean surfaces", "unmarked", "text-free" |
| "no watermark" | "pristine image", "clean composition" |
| "no logos" | "unbranded", "plain", "logo-free surface" |
**Example:**
```
Bad: Product photo, no watermark, no text
Good: Clean product photography with pristine unmarked surfaces, minimal unbranded design
```
### Style/Era
| Instead of | Use |
|-----------|-----|
| "not modern" | "traditional", "classical", "vintage", "historical" |
| "no CGI look" | "photorealistic", "authentic", "natural", "organic" |
| "not cartoonish" | "realistic", "lifelike", "naturalistic" |
**Example:**
```
Bad: Building design, not modern, no futuristic elements
Good: Traditional Victorian architecture with classical ornate details and period-accurate features
```
### Quality/Artifacts
| Instead of | Use |
|-----------|-----|
| "no blur" | "sharp focus", "crisp details", "tack-sharp" |
| "no noise" | "clean image", "smooth gradients", "low ISO" |
| "no artifacts" | "pristine quality", "clean render", "flawless" |
**Example:**
```
Bad: Portrait, no blur, no noise
Good: Tack-sharp portrait with pristine image quality, smooth skin tones, crisp details
```
### Objects
| Instead of | Use |
|-----------|-----|
| "no cars" | "pedestrian area", "car-free zone", "walking street" |
| "no buildings" | "open landscape", "natural scenery", "wilderness" |
| "no furniture" | "empty room", "bare space", "minimalist interior" |
**Example:**
```
Bad: Street scene, no cars, no modern buildings
Good: Historic cobblestone walking street lined with traditional stone buildings from the 1800s
```
### Weather/Environment
| Instead of | Use |
|-----------|-----|
| "no rain" | "clear sky", "dry weather", "sunny day" |
| "no clouds" | "clear blue sky", "cloudless", "perfect visibility" |
| "not dark" | "well-lit", "bright", "daylight", "illuminated" |
**Example:**
```
Bad: Outdoor portrait, no rain, no clouds, not dark
Good: Outdoor portrait under clear blue sky on a bright sunny day, perfect natural lighting
```
### Composition
| Instead of | Use |
|-----------|-----|
| "no distractions" | "clean composition", "focused framing", "minimal elements" |
| "nothing in background" | "solid background", "isolated subject", "clean backdrop" |
| "no clutter" | "organized", "tidy", "minimal", "streamlined" |
**Example:**
```
Bad: Product shot, no distractions, nothing in background
Good: Product on clean white seamless backdrop, isolated subject, minimal focused composition
```
## Complex Replacement Examples
### Original Negative-Heavy Prompt
```
Portrait of a woman, no glasses, no makeup, no wrinkles, no blemishes,
no bright colors, no distracting background, no harsh lighting
```
### Positive Rewrite
```
Portrait of a youthful woman with clear natural skin and visible bright eyes,
fresh-faced with a healthy glow, wearing muted earth tones against a soft
blurred neutral background, gentle diffused lighting creating soft shadows
```
### Original Negative-Heavy Prompt
```
Landscape photo, no people, no buildings, no power lines, no modern elements,
no overcast sky, no dead trees
```
### Positive Rewrite
```
Pristine wilderness landscape with lush green living forest, clear blue sky,
untouched natural scenery stretching to the horizon, peaceful solitude with
only birdsong and wind, golden hour sunlight filtering through healthy foliage
```
## Quick Reference Card
| Unwanted | Positive Alternative |
|----------|---------------------|
| No people | Empty, solitary, deserted |
| No makeup | Natural, fresh-faced, bare |
| No text | Clean, unmarked, pristine |
| No blur | Sharp, crisp, tack-sharp |
| No modern | Traditional, vintage, classical |
| No dark | Bright, well-lit, luminous |
| No busy | Minimal, clean, focused |
| No artificial | Natural, organic, authentic |
@@ -0,0 +1,230 @@
---
name: t2i-prompting
description: Text-to-image prompting patterns and techniques
---
# Text-to-Image (T2I) Prompting
Comprehensive guide to crafting effective text-to-image prompts for FLUX models.
## Prompt Structure Framework
### Basic Formula
```
[Subject] + [Action] + [Style] + [Context] + [Lighting] + [Technical]
```
### Expanded Framework
```
[Main Subject] - who/what is the focus
[Attributes] - characteristics, details, clothing
[Action/Pose] - what they're doing
[Environment] - where, setting, background
[Style/Medium] - artistic approach
[Lighting] - light source, quality, mood
[Composition] - framing, camera angle
[Technical] - camera, lens, film stock
```
## Subject Types
### People/Portraits
```
A distinguished professor in his 60s with silver hair and round spectacles,
wearing a tweed jacket with leather elbow patches, deep-set thoughtful eyes,
slight smile suggesting hidden wisdom
```
### Animals
```
A majestic snow leopard with piercing blue-grey eyes, thick spotted fur
dusted with snowflakes, powerful muscular build, alert posture on a
rocky outcrop
```
### Objects/Products
```
A vintage Leica M3 camera with worn brass edges showing decades of use,
black leather covering with patina, sitting on weathered wooden table
```
### Landscapes
```
A dramatic fjord at dawn, steep granite cliffs rising from mirror-still
water, wisps of morning mist, distant snow-capped peaks catching first
golden light
```
### Architecture
```
A brutalist concrete apartment building in late afternoon light, geometric
shadows creating abstract patterns, warm sunlight contrasting with cool
grey concrete
```
## Style Categories
### Photorealistic
#### Modern Digital
```
shot on Sony A7IV, clean and sharp, high dynamic range, professional color grading
```
#### Film Photography
```
shot on Kodak Portra 400, natural film grain, organic colors, slight warmth
```
#### Vintage Digital (2000s)
```
early digital camera aesthetic, slight noise, flash photography, candid feel
```
#### 80s Film
```
80s film photography, film grain, warm color cast, soft focus, nostalgic
```
### Artistic Styles
#### Oil Painting
```
classical oil painting style, visible brushstrokes, rich colors, dramatic lighting
```
#### Watercolor
```
delicate watercolor painting, soft edges, transparent washes, paper texture visible
```
#### Digital Art
```
polished digital illustration, clean lines, vibrant colors, professional concept art
```
#### Anime/Manga
```
anime style, large expressive eyes, clean linework, cel shading, vibrant palette
```
## Lighting Patterns
### Portrait Lighting
```
Rembrandt lighting - 45 degree key light creating triangle shadow on cheek
Butterfly lighting - overhead key creating shadow under nose
Split lighting - 90 degree side light, half face in shadow
Loop lighting - slight angle creating small nose shadow
```
### Natural Lighting
```
Golden hour - warm, soft, directional light 1 hour before sunset
Blue hour - cool, ambient light just after sunset
Overcast - soft, even, diffused lighting
Harsh midday - strong contrast, defined shadows
```
### Atmospheric
```
Volumetric light - visible light rays through fog/dust
Rim lighting - backlight creating edge glow
Practical lighting - visible light sources in scene
Neon glow - colorful artificial urban lighting
```
## Camera and Lens Simulation
### Camera Bodies
```
Shot on Hasselblad X2D - medium format, exceptional detail
Shot on Canon 5D Mark IV - professional DSLR quality
Shot on Leica M10 - rangefinder character, smooth tonality
Shot on iPhone 15 Pro - computational photography look
```
### Lens Characteristics
```
85mm f/1.4 - classic portrait, creamy bokeh
24mm f/2.8 - wide angle, environmental
50mm f/1.2 - natural perspective, shallow DOF
135mm f/2 - compressed perspective, smooth background
Macro lens - extreme close-up detail
Tilt-shift lens - miniature effect or architectural correction
```
### Technical Settings
```
f/1.4 - extremely shallow depth of field
f/2.8 - moderate background blur
f/8 - sharp throughout, landscape
f/16 - maximum sharpness, long exposure
ISO 100 - clean, no noise
ISO 3200 - visible grain, low light
```
## Composition Techniques
### Framing
```
extreme close-up - filling frame with detail
close-up - head and shoulders
medium shot - waist up
full shot - entire body
wide shot - subject in environment
establishing shot - location focus
```
### Angles
```
eye level - natural, relatable
low angle - powerful, imposing
high angle - diminished, overview
Dutch angle - tension, unease
bird's eye - pattern, layout
worm's eye - dramatic upward view
```
### Composition Rules
```
rule of thirds - subject at intersection points
centered composition - symmetry, stability
leading lines - guiding eye to subject
frame within frame - natural framing elements
negative space - minimalist, breathing room
```
## Complete Example Prompts
### Editorial Portrait
```
A fashion editorial portrait of a young woman with striking features and
high cheekbones, wearing an avant-garde geometric collar in silver, dramatic
side lighting creating strong shadows, shot on Hasselblad with 100mm lens
at f/2.8, studio background with subtle gradient, high fashion magazine style
```
### Product Photography
```
A premium wireless headphone product shot, matte black finish with rose gold
accents, floating at slight angle against pure white background, soft even
lighting eliminating harsh shadows, reflection visible on glossy surface below,
commercial catalog style, ultra sharp focus throughout
```
### Landscape
```
A misty morning in ancient redwood forest, towering trees disappearing into
fog above, ferns covering forest floor in layers of green, single shaft of
golden sunlight breaking through canopy, shot on large format camera, rich
detail in bark textures, Ansel Adams inspired black and white with deep tones
```
### Architectural
```
Modern minimalist beach house at golden hour, floor-to-ceiling glass walls
reflecting sunset colors, clean white concrete and natural wood, infinity
pool merging with ocean horizon, architectural photography style, wide angle
showing full structure, warm evening light
```
@@ -0,0 +1,241 @@
---
name: typography-text
description: Prompting for text rendering and typography in FLUX
---
# Typography and Text Prompting
Guide to rendering text in FLUX images. Use FLUX.2 [flex] for best typography results.
## Basic Syntax
Always quote the exact text you want rendered:
```
A coffee shop chalkboard sign displaying "TODAY'S SPECIAL" in decorative script
```
## Core Rules
### 1. Use Quotation Marks
```
Correct: A poster with "HELLO WORLD" in bold letters
Wrong: A poster with HELLO WORLD in bold letters
```
### 2. Specify Font Style
```
"ADVENTURE" in bold sans-serif typography
"Welcome" in elegant cursive script
"CHAPTER ONE" in classic serif typeface
"CODE" in monospace terminal font
"SALE!" in decorative display lettering
```
### 3. Describe Size Hierarchy
```
Large headline "BREAKING NEWS" above smaller subtext "Details inside"
```
### 4. Indicate Placement
```
"OPEN" sign centered in storefront window
"EXIT" text positioned above doorway
"Page 1" in bottom right corner
```
### 5. Front-Load Text
Place text descriptions early in the prompt for better accuracy:
```
Good: A sign reading "FRESH BREAD" in a bakery window...
Less Good: A bakery window with a sign that says "FRESH BREAD"...
```
## Font Style Categories
### Sans-Serif (Modern/Clean)
```
"MINIMAL" in clean geometric sans-serif, Swiss modernist style
"TECH SUMMIT" in bold condensed grotesque typeface
"future" in thin uppercase sans-serif, contemporary design
```
### Serif (Classic/Elegant)
```
"The New Yorker" in traditional serif typeface, editorial masthead
"LUXURY" in high-contrast Didone serif with thin/thick strokes
"Wisdom" in old-style serif with subtle bracketed serifs
```
### Script/Cursive (Decorative)
```
"With Love" in flowing calligraphic script with flourishes
"Signature" in connected brush script, casual elegance
"Romance" in formal copperplate script, wedding invitation style
```
### Display/Decorative
```
"ROCK CONCERT" in distressed vintage concert poster lettering
"CIRCUS" in ornate Victorian display type with decorative elements
"RETRO" in 1970s rounded bubble letters
```
### Handwritten
```
"Note to self" in casual handwritten style, slightly imperfect
"Thanks!" in quick marker pen handwriting
"ideas" in sketchy pencil handwriting
```
### Monospace
```
"CODE_COMPLETE" in terminal monospace, developer aesthetic
"SYSTEM" in typewriter monospace, vintage tech
"DEBUG" in LCD-style digital monospace
```
## Text Effects
### Neon Signs
```
Glowing neon sign spelling "OPEN 24/7" in pink neon tubes with
blue outline, slight glow and reflection, night scene
```
### Metallic/3D
```
"GOLD" in three-dimensional metallic gold letters with realistic
reflections and subtle shadows, luxury aesthetic
```
### Embossed/Debossed
```
"PREMIUM" embossed into leather surface, subtle shadows showing
the raised letterforms
```
### Outlined
```
"MODERN" in outline-only letters, no fill, thin white stroke
on dark background
```
### Gradient Text
```
"SUMMER" with gradient fill from #FF6B6B (coral) at top to
#4ECDC4 (teal) at bottom
```
## Multi-Text Compositions
### Poster Design
```
Event poster with "SUMMER FEST 2025" as large headline in bold
condensed sans-serif at top, "JULY 15-17" as medium subheading
in regular weight, "Central Park, NYC" as small body text at
bottom, all in white text on #FF6B35 (sunset orange) background
```
### Book Cover
```
Book cover design: "THE GREAT GATSBY" in elegant art deco gold
lettering centered in upper third, author name "F. SCOTT FITZGERALD"
in smaller gold caps below, #1A1A2E (midnight blue) background
with geometric gold accents
```
### Magazine Cover
```
Fashion magazine cover with "VOGUE" in classic serif masthead at top,
cover line "SPRING COLLECTION" in bold sans-serif, "The New Rules of Style"
in lighter weight italic, all in white against dramatic portrait
```
### Signage
```
Vintage diner sign: "MEL'S DINER" in red neon script lettering,
"OPEN" below in separate green neon block letters, chrome border,
1950s Americana aesthetic
```
### Business Card
```
Minimalist business card with "JOHN SMITH" in medium weight sans-serif,
"Creative Director" in lighter weight below, contact details in small
type at bottom, #2C3E50 (dark blue) text on white background
```
## Text Placement Strategies
### Centered Composition
```
Centered text layout: "WELCOME" in large caps at center,
perfectly balanced with equal margins
```
### Left-Aligned
```
Left-aligned text block: "Company Name" as header,
"Tagline goes here" below, flush left alignment
```
### Text on Path
```
"GOING IN CIRCLES" text following a circular path around
the center of the design
```
### Text Overlay
```
"ADVENTURE AWAITS" in bold white text overlaid on landscape
photograph, positioned in lower third with slight shadow for readability
```
## Technical Considerations for [flex]
### Steps Parameter
- Higher steps (30-50) = better text quality
- Lower steps (10-20) = faster, lower quality
### Guidance Parameter
- Higher guidance (6-10) = stricter prompt following
- Lower guidance (1.5-4) = more creative interpretation
### Recommended Settings
```
For clean typography: steps=50, guidance=7
For artistic text: steps=30, guidance=4
```
## Troubleshooting
### Misspelled Words
- Keep text short (1-4 words work best)
- Use common words when possible
- Repeat the exact text in the prompt
### Illegible Text
- Specify larger text size
- Use simpler fonts (sans-serif)
- Ensure high contrast with background
- Use [flex] model
### Wrong Font Style
Be more specific:
```
Instead of: "text in a nice font"
Use: "text in bold geometric sans-serif similar to Futura"
```
### Text Not Appearing
- Front-load text description in prompt
- Put text in quotes
- Specify exact placement
- Reduce other prompt complexity
+179
View File
@@ -0,0 +1,179 @@
---
name: framer-motion
description: Use when implementing Disney's 12 animation principles with Framer Motion in React applications
---
# Framer Motion Animation Principles
Implement all 12 Disney animation principles using Framer Motion's declarative React API.
## 1. Squash and Stretch
```jsx
<motion.div
animate={{ scaleX: [1, 1.2, 1], scaleY: [1, 0.8, 1] }}
transition={{ duration: 0.3, times: [0, 0.5, 1] }}
/>
```
## 2. Anticipation
```jsx
<motion.div
variants={{
idle: { y: 0, scaleY: 1 },
anticipate: { y: 10, scaleY: 0.9 },
jump: { y: -200 }
}}
initial="idle"
animate={["anticipate", "jump"]}
transition={{ duration: 0.5, times: [0, 0.2, 1] }}
/>
```
## 3. Staging
```jsx
<motion.div animate={{ filter: "blur(3px)", opacity: 0.6 }} /> {/* bg */}
<motion.div animate={{ scale: 1.1, zIndex: 10 }} /> {/* hero */}
```
## 4. Straight Ahead / Pose to Pose
```jsx
<motion.div
animate={{
x: [0, 100, 200, 300],
y: [0, -50, 0, -30]
}}
transition={{ duration: 1, ease: "easeInOut" }}
/>
```
## 5. Follow Through and Overlapping Action
```jsx
<motion.div animate={{ x: 200 }} transition={{ duration: 0.5 }}>
<motion.span
animate={{ x: 200 }}
transition={{ duration: 0.5, delay: 0.05 }} // hair
/>
<motion.span
animate={{ x: 200 }}
transition={{ duration: 0.6, delay: 0.1 }} // cape
/>
</motion.div>
```
## 6. Slow In and Slow Out
```jsx
<motion.div
animate={{ x: 300 }}
transition={{
duration: 0.6,
ease: [0.42, 0, 0.58, 1] // easeInOut cubic-bezier
}}
/>
// Or use: "easeIn", "easeOut", "easeInOut"
```
## 7. Arc
```jsx
<motion.div
animate={{
x: [0, 100, 200],
y: [0, -100, 0]
}}
transition={{ duration: 1, ease: "easeInOut" }}
/>
```
## 8. Secondary Action
```jsx
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<motion.span
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 0.3 }}
>
Icon
</motion.span>
</motion.button>
```
## 9. Timing
```jsx
const timings = {
fast: { duration: 0.15 },
normal: { duration: 0.3 },
slow: { duration: 0.6 },
spring: { type: "spring", stiffness: 300, damping: 20 }
};
```
## 10. Exaggeration
```jsx
<motion.div
animate={{ scale: 1.5, rotate: 720 }}
transition={{
type: "spring",
stiffness: 200,
damping: 10 // low damping = overshoot
}}
/>
```
## 11. Solid Drawing
```jsx
<motion.div
style={{ perspective: 1000 }}
animate={{ rotateX: 45, rotateY: 30 }}
transition={{ duration: 0.5 }}
/>
```
## 12. Appeal
```jsx
<motion.div
whileHover={{
scale: 1.02,
boxShadow: "0 20px 40px rgba(0,0,0,0.2)"
}}
transition={{ duration: 0.3 }}
/>
```
## Stagger Children
```jsx
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 }
}
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map(item => <motion.li variants={itemVariant} />)}
</motion.ul>
```
## Key Framer Motion Features
- `animate` - Target state
- `variants` - Named animation states
- `whileHover` / `whileTap` - Gesture animations
- `transition` - Timing and easing
- `AnimatePresence` - Exit animations
- `useAnimation` - Programmatic control
- `layout` - Auto-animate layout changes
+103
View File
@@ -0,0 +1,103 @@
---
name: heygen
description: |
[DEPRECATED] Use `create-video` for prompt-based video generation or `avatar-video` for precise avatar/scene control. This legacy skill combines both workflows — the newer focused skills provide clearer guidance.
homepage: https://docs.heygen.com/reference/generate-video-agent
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# HeyGen API (Deprecated)
> **This skill is deprecated.** Use the focused skills instead:
> - **`create-video`** — Generate videos from a text prompt (Video Agent API)
> - **`avatar-video`** — Build videos with specific avatars, voices, scripts, and scenes (v2 API)
This skill remains for backward compatibility but will be removed in a future release.
---
AI avatar video creation API for generating talking-head videos, explainers, and presentations.
## Tool Selection
If HeyGen MCP tools are available (`mcp__heygen__*`), **prefer them** over direct HTTP API calls — they handle authentication and request formatting automatically.
| Task | MCP Tool | Fallback (Direct API) |
|------|----------|----------------------|
| Generate video from prompt | `mcp__heygen__generate_video_agent` | `POST /v1/video_agent/generate` |
| Check video status / get URL | `mcp__heygen__get_video` | `GET /v2/videos/{video_id}` |
| List account videos | `mcp__heygen__list_videos` | `GET /v2/videos` |
| Delete a video | `mcp__heygen__delete_video` | `DELETE /v2/videos/{video_id}` |
If no HeyGen MCP tools are available, use direct HTTP API calls with `X-Api-Key: $HEYGEN_API_KEY` header as documented in the reference files.
## Default Workflow
**Prefer Video Agent** for most video requests.
Always use [prompt-optimizer.md](references/prompt-optimizer.md) guidelines to structure prompts with scenes, timing, and visual styles.
**With MCP tools:**
1. Write an optimized prompt using [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md)
2. Call `mcp__heygen__generate_video_agent` with prompt and config (duration_sec, orientation, avatar_id)
3. Call `mcp__heygen__get_video` with the returned video_id to poll status and get the download URL
**Without MCP tools (direct API):**
1. Write an optimized prompt using [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md)
2. `POST /v1/video_agent/generate` — see [video-agent.md](references/video-agent.md)
3. `GET /v2/videos/<id>` — see [video-status.md](references/video-status.md)
Only use v2/video/generate when user explicitly needs:
- Exact script without AI modification
- Specific voice_id selection
- Different avatars/backgrounds per scene
- Precise per-scene timing control
- Programmatic/batch generation with exact specs
## Quick Reference
| Task | MCP Tool | Read |
|------|----------|------|
| Generate video from prompt (easy) | `mcp__heygen__generate_video_agent` | [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md) → [video-agent.md](references/video-agent.md) |
| Generate video with precise control | — | [video-generation.md](references/video-generation.md), [avatars.md](references/avatars.md), [voices.md](references/voices.md) |
| Check video status / get download URL | `mcp__heygen__get_video` | [video-status.md](references/video-status.md) |
| Add captions or text overlays | — | [captions.md](references/captions.md), [text-overlays.md](references/text-overlays.md) |
| Transparent video for compositing | — | [video-generation.md](references/video-generation.md) (WebM section) |
| Use with Remotion | — | [remotion-integration.md](references/remotion-integration.md) |
## Reference Files
### Foundation
- [references/authentication.md](references/authentication.md) - API key setup and X-Api-Key header
- [references/quota.md](references/quota.md) - Credit system and usage limits
- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs
- [references/assets.md](references/assets.md) - Uploading images, videos, audio
### Core Video Creation
- [references/avatars.md](references/avatars.md) - Listing avatars, styles, avatar_id selection
- [references/voices.md](references/voices.md) - Listing voices, locales, speed/pitch
- [references/scripts.md](references/scripts.md) - Writing scripts, pauses, pacing
- [references/video-generation.md](references/video-generation.md) - POST /v2/video/generate and multi-scene videos
- [references/video-agent.md](references/video-agent.md) - One-shot prompt video generation
- [references/prompt-optimizer.md](references/prompt-optimizer.md) - Writing effective Video Agent prompts (core workflow + rules)
- [references/visual-styles.md](references/visual-styles.md) - 20 named visual styles with full specs
- [references/prompt-examples.md](references/prompt-examples.md) - Full production prompt example + ready-to-use templates
- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios
### Video Customization
- [references/backgrounds.md](references/backgrounds.md) - Solid colors, images, video backgrounds
- [references/text-overlays.md](references/text-overlays.md) - Adding text with fonts and positioning
- [references/captions.md](references/captions.md) - Auto-generated captions and subtitles
### Advanced Features
- [references/templates.md](references/templates.md) - Template listing and variable replacement
- [references/photo-avatars.md](references/photo-avatars.md) - Creating avatars from photos
- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events
### Integration
- [references/remotion-integration.md](references/remotion-integration.md) - Using HeyGen in Remotion compositions
+339
View File
@@ -0,0 +1,339 @@
---
name: assets
description: Uploading images, videos, and audio for use in HeyGen video generation
---
# Asset Upload and Management
HeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.
## Upload Flow
Asset uploads are a single-step process: POST the raw file binary directly to the upload endpoint. The Content-Type header must match the file's MIME type.
## Uploading an Asset
**Endpoint:** `POST https://upload.heygen.com/v1/asset`
### Request
| Header | Required | Description |
|--------|:--------:|-------------|
| `X-Api-Key` | ✓ | Your HeyGen API key |
| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |
The request body is the raw binary file data. No JSON or form fields are needed.
### Response
| Field | Type | Description |
|-------|------|-------------|
| `code` | number | Status code (`100` = success) |
| `data.id` | string | Unique asset ID for use in video generation |
| `data.name` | string | Asset name |
| `data.file_type` | string | `image`, `video`, or `audio` |
| `data.url` | string | Accessible URL for the uploaded file |
| `data.image_key` | string \| null | Key for creating uploaded photo avatars (images only) |
| `data.folder_id` | string | Folder ID (empty if not in a folder) |
| `data.meta` | string \| null | Asset metadata |
| `data.created_ts` | number | Unix timestamp of creation |
### curl
```bash
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./background.jpg'
```
### TypeScript
```typescript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
name: string;
file_type: string;
url: string;
image_key: string | null;
folder_id: string;
meta: string | null;
created_ts: number;
};
msg: string | null;
message: string | null;
}
async function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: fileBuffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
// Usage
const asset = await uploadAsset("./background.jpg", "image/jpeg");
console.log(`Uploaded asset: ${asset.id}`);
console.log(`Asset URL: ${asset.url}`);
```
### TypeScript (with streams for large files)
```typescript
import fs from "fs";
import path from "path";
import { stat } from "fs/promises";
async function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileStats = await stat(resolvedPath);
const fileStream = fs.createReadStream(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
"Content-Length": fileStats.size.toString(),
},
body: fileStream as any,
// @ts-ignore - duplex is needed for streaming
duplex: "half",
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
### Python
```python
import requests
import os
def upload_asset(file_path: str, content_type: str) -> dict:
with open(file_path, "rb") as f:
response = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": content_type
},
data=f
)
data = response.json()
if data.get("code") != 100:
raise Exception(data.get("message", "Upload failed"))
return data["data"]
# Usage
asset = upload_asset("./background.jpg", "image/jpeg")
print(f"Uploaded asset: {asset['id']}")
print(f"Asset URL: {asset['url']}")
```
## Supported Content Types
| Type | Content-Type | Use Case |
|------|--------------|----------|
| JPEG | `image/jpeg` | Backgrounds, talking photos |
| PNG | `image/png` | Backgrounds, overlays |
| MP4 | `video/mp4` | Video backgrounds |
| WebM | `video/webm` | Video backgrounds |
| MP3 | `audio/mpeg` | Custom audio input |
| WAV | `audio/wav` | Custom audio input |
## Uploading from URL
If your asset is already hosted online:
```typescript
async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse["data"]> {
// 1. Validate and download the file
const url = new URL(sourceUrl);
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs are supported");
}
const sourceResponse = await fetch(sourceUrl);
const buffer = Buffer.from(await sourceResponse.arrayBuffer());
// 2. Upload directly to HeyGen
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: buffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
```
## Using Uploaded Assets
### As Background Image
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello, this is a video with a custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: asset.url, // Use the URL from the upload response
},
},
],
};
```
### As Talking Photo Source
```typescript
const talkingPhotoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: asset.id, // Use the ID from the upload response
},
voice: {
type: "text",
input_text: "Hello from my talking photo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### As Audio Input
```typescript
const audioConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: asset.url, // Use the URL from the upload response
},
},
],
};
```
## Complete Upload Workflow
```typescript
async function createVideoWithCustomBackground(
backgroundPath: string,
script: string
): Promise<string> {
// 1. Upload background
console.log("Uploading background...");
const background = await uploadAsset(backgroundPath, "image/jpeg");
// 2. Create video config
const config = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: background.url,
},
},
],
dimension: { width: 1920, height: 1080 },
};
// 3. Generate video
console.log("Generating video...");
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const { data } = await response.json();
return data.video_id;
}
```
## Asset Limitations
- **File size**: 10MB maximum
- **Image dimensions**: Recommended to match video dimensions
- **Audio duration**: Should match expected video length
- **Retention**: Assets may be deleted after a period of inactivity
## Best Practices
1. **Optimize images** - Resize to match video dimensions before uploading
2. **Use appropriate formats** - JPEG for photos, PNG for graphics with transparency
3. **Validate before upload** - Check file type and size locally first
4. **Handle upload errors** - Implement retry logic for failed uploads
5. **Cache asset IDs** - Reuse assets across multiple video generations
@@ -0,0 +1,232 @@
---
name: authentication
description: API key setup, X-Api-Key header, and authentication patterns for HeyGen
---
# HeyGen Authentication
All HeyGen API requests require authentication using an API key passed in the `X-Api-Key` header.
## Getting Your API Key
1. Go to https://app.heygen.com/settings?from=&nav=API
2. Log in if prompted
3. Copy your API key
## Environment Setup
Store your API key securely as an environment variable:
```bash
export HEYGEN_API_KEY="your-api-key-here"
```
For `.env` files:
```
HEYGEN_API_KEY=your-api-key-here
```
## Making Authenticated Requests
### curl
```bash
curl -X GET "https://api.heygen.com/v2/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript/JavaScript (fetch)
```typescript
const response = await fetch("https://api.heygen.com/v2/avatars", {
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
},
});
const { data } = await response.json();
```
### TypeScript/JavaScript (axios)
```typescript
import axios from "axios";
const client = axios.create({
baseURL: "https://api.heygen.com",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY,
},
});
const { data } = await client.get("/v2/avatars");
```
### Python (requests)
```python
import os
import requests
response = requests.get(
"https://api.heygen.com/v2/avatars",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
```
### Python (httpx)
```python
import os
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.heygen.com/v2/avatars",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
```
## Creating a Reusable API Client
### TypeScript
```typescript
class HeyGenClient {
private baseUrl = "https://api.heygen.com";
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
"X-Api-Key": this.apiKey,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint);
}
post<T>(endpoint: string, body: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: "POST",
body: JSON.stringify(body),
});
}
}
// Usage
const client = new HeyGenClient(process.env.HEYGEN_API_KEY!);
const avatars = await client.get("/v2/avatars");
```
## API Response Format
All HeyGen API responses follow this structure:
```typescript
interface ApiResponse<T> {
error: null | string;
data: T;
}
```
Successful response example:
```json
{
"error": null,
"data": {
"avatars": [...]
}
}
```
Error response example:
```json
{
"error": "Invalid API key",
"data": null
}
```
## Error Handling
Common authentication errors:
| Status Code | Error | Cause |
|-------------|-------|-------|
| 401 | Invalid API key | API key is missing or incorrect |
| 403 | Forbidden | API key doesn't have required permissions |
| 429 | Rate limit exceeded | Too many requests |
### Handling Errors
```typescript
async function makeRequest(endpoint: string) {
const response = await fetch(`https://api.heygen.com${endpoint}`, {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json = await response.json();
if (!response.ok || json.error) {
throw new Error(json.error || `HTTP ${response.status}`);
}
return json.data;
}
```
## Rate Limiting
HeyGen enforces rate limits on API requests:
- Standard rate limits apply per API key
- Some endpoints (like video generation) have stricter limits
- Use exponential backoff when receiving 429 errors
```typescript
async function requestWithRetry(
fn: () => Promise<Response>,
maxRetries = 3
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
const response = await fn();
if (response.status === 429) {
const waitTime = Math.pow(2, i) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}
```
## Security Best Practices
1. **Never expose API keys in client-side code** - Always make API calls from a backend server
2. **Use environment variables** - Don't hardcode API keys in source code
3. **Rotate keys periodically** - Generate new API keys regularly
4. **Monitor usage** - Check your HeyGen dashboard for unusual activity
+586
View File
@@ -0,0 +1,586 @@
---
name: avatars
description: Listing avatars, avatar styles, and avatar_id selection for HeyGen
---
# HeyGen Avatars
Avatars are the AI-generated presenters in HeyGen videos. You can use public avatars provided by HeyGen or create custom avatars.
## Previewing Avatars Before Generation
Always preview avatars before generating a video to ensure they match user preferences. Each avatar has preview URLs that can be opened directly in the browser - no downloading required.
### List Avatars and Show Previews
```typescript
async function listAndPreviewAvatars(openInBrowser = true): Promise<void> {
const response = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data } = await response.json();
for (const avatar of data.avatars.slice(0, 5)) {
console.log(`\n${avatar.avatar_name} (${avatar.gender})`);
console.log(` ID: ${avatar.avatar_id}`);
console.log(` Preview: ${avatar.preview_image_url}`);
}
// Preview URLs can be opened directly in any browser
for (const avatar of data.avatars.slice(0, 3)) {
console.log(`Open in browser: ${avatar.preview_image_url}`);
}
}
```
### Workflow: Preview Before Generate
1. **List available avatars** - get names, genders, and preview URLs
2. **Show preview URLs to user** - share `preview_image_url` for visual check
3. **User selects** preferred avatar by name or ID
4. **Get avatar details** for `default_voice_id`
5. **Generate video** with selected avatar
### Preview Fields in API Response
| Field | Description |
|-------|-------------|
| `preview_image_url` | Static image of the avatar (JPG) - publicly accessible URL |
| `preview_video_url` | Short video clip showing avatar animation |
Both URLs are publicly accessible - no authentication needed to view.
## Listing Available Avatars
### curl
```bash
curl -X GET "https://api.heygen.com/v2/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Avatar {
avatar_id: string;
avatar_name: string;
gender: "male" | "female";
preview_image_url: string;
preview_video_url: string;
}
interface AvatarsResponse {
error: null | string;
data: {
avatars: Avatar[];
talking_photos: TalkingPhoto[];
};
}
async function listAvatars(): Promise<Avatar[]> {
const response = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: AvatarsResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.avatars;
}
```
### Python
```python
import requests
import os
def list_avatars() -> list:
response = requests.get(
"https://api.heygen.com/v2/avatars",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["avatars"]
```
## Response Format
```json
{
"error": null,
"data": {
"avatars": [
{
"avatar_id": "josh_lite3_20230714",
"avatar_name": "Josh",
"gender": "male",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/..."
},
{
"avatar_id": "angela_expressive_20231010",
"avatar_name": "Angela",
"gender": "female",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/..."
}
],
"talking_photos": []
}
}
```
## Avatar Types
### Public Avatars
HeyGen provides a library of public avatars that anyone can use:
```typescript
// List only public avatars
const avatars = await listAvatars();
const publicAvatars = avatars.filter((a) => !a.avatar_id.startsWith("custom_"));
```
### Private/Custom Avatars
Custom avatars created from your own training footage:
```typescript
const customAvatars = avatars.filter((a) => a.avatar_id.startsWith("custom_"));
```
## Avatar Styles
Avatars support different rendering styles:
| Style | Description |
|-------|-------------|
| `normal` | Full body shot, standard framing |
| `closeUp` | Close-up on face, more expressive |
| `circle` | Avatar in circular frame (talking head) |
| `voice_only` | Audio only, no video rendering |
### When to Use Each Style
| Use Case | Recommended Style |
|----------|-------------------|
| Full-screen presenter video | `normal` |
| Personal/intimate content | `closeUp` |
| Picture-in-picture overlay | `circle` |
| Small corner widget | `circle` |
| Podcast/audio content | `voice_only` |
| Motion graphics with avatar overlay | `normal` or `closeUp` + transparent bg |
### Using Avatar Styles
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal", // "normal" | "closeUp" | "circle" | "voice_only"
},
voice: {
type: "text",
input_text: "Hello, world!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Circle Style for Talking Heads
Circle style is ideal for overlay compositions:
```typescript
// Circle avatar for picture-in-picture
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "circle",
},
voice: { ... },
background: {
type: "color",
value: "#00FF00", // Green for chroma key, or use webm endpoint
},
}
```
## Searching and Filtering Avatars
### By Gender
```typescript
function filterByGender(avatars: Avatar[], gender: "male" | "female"): Avatar[] {
return avatars.filter((a) => a.gender === gender);
}
const maleAvatars = filterByGender(avatars, "male");
const femaleAvatars = filterByGender(avatars, "female");
```
### By Name
```typescript
function searchByName(avatars: Avatar[], query: string): Avatar[] {
const lowerQuery = query.toLowerCase();
return avatars.filter((a) =>
a.avatar_name.toLowerCase().includes(lowerQuery)
);
}
const results = searchByName(avatars, "josh");
```
## Avatar Groups
Avatars are organized into groups for better management.
### List Avatar Groups
```bash
curl -X GET "https://api.heygen.com/v2/avatar_group.list?include_public=true" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_public` | bool | false | Include public avatars in results |
#### TypeScript
```typescript
interface AvatarGroupItem {
id: string;
name: string;
created_at: number;
num_looks: number;
preview_image: string;
group_type: string;
train_status: string;
default_voice_id: string | null;
}
interface AvatarGroupListResponse {
error: null | string;
data: {
avatar_group_list: AvatarGroupItem[];
};
}
async function listAvatarGroups(
includePublic = true
): Promise<AvatarGroupListResponse["data"]> {
const params = new URLSearchParams({
include_public: includePublic.toString(),
});
const response = await fetch(
`https://api.heygen.com/v2/avatar_group.list?${params}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: AvatarGroupListResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
### Get Avatars in a Group
```bash
curl -X GET "https://api.heygen.com/v2/avatar_group/{group_id}/avatars" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
## Using Avatars in Video Generation
### Basic Avatar Usage
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to our product demo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
### Multiple Scenes with Different Avatars
```typescript
const multiSceneConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hi, I'm Josh. Let me introduce my colleague.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
{
character: {
type: "avatar",
avatar_id: "angela_expressive_20231010",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! I'm Angela. Nice to meet you!",
voice_id: "2d5b0e6a8c3f47d9a1b2c3d4e5f60718",
},
},
],
};
```
## Using Avatar's Default Voice
Many avatars have a `default_voice_id` that's pre-matched for natural results. **This is the recommended approach** rather than manually selecting voices.
### Recommended Flow
```
1. GET /v2/avatars → Get list of avatar_ids
2. GET /v2/avatar/{id}/details → Get default_voice_id for chosen avatar
3. POST /v2/video/generate → Use avatar_id + default_voice_id
```
### Get Avatar Details (v2 API)
Given an `avatar_id`, fetch its details including the default voice:
```bash
curl -X GET "https://api.heygen.com/v2/avatar/{avatar_id}/details" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
#### Response Format
```json
{
"error": null,
"data": {
"type": "avatar",
"id": "josh_lite3_20230714",
"name": "Josh",
"gender": "male",
"preview_image_url": "https://files.heygen.ai/...",
"preview_video_url": "https://files.heygen.ai/...",
"premium": false,
"is_public": true,
"default_voice_id": "1bd001e7e50f421d891986aad5158bc8",
"tags": ["AVATAR_IV"]
}
}
```
#### TypeScript
```typescript
interface AvatarDetails {
type: "avatar";
id: string;
name: string;
gender: "male" | "female";
preview_image_url: string;
preview_video_url: string;
premium: boolean;
is_public: boolean;
default_voice_id: string | null;
tags: string[];
}
async function getAvatarDetails(avatarId: string): Promise<AvatarDetails> {
const response = await fetch(
`https://api.heygen.com/v2/avatar/${avatarId}/details`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
// Usage: Get default voice for a known avatar
const details = await getAvatarDetails("josh_lite3_20230714");
if (details.default_voice_id) {
console.log(`Using ${details.name} with default voice: ${details.default_voice_id}`);
} else {
console.log(`${details.name} has no default voice, select manually`);
}
```
#### Complete Example: Generate Video with Any Avatar's Default Voice
```typescript
async function generateWithAvatarDefaultVoice(
avatarId: string,
script: string
): Promise<string> {
// 1. Get avatar details to find default voice
const avatar = await getAvatarDetails(avatarId);
if (!avatar.default_voice_id) {
throw new Error(`Avatar ${avatar.name} has no default voice`);
}
// 2. Generate video with the avatar's default voice
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatar.id,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id,
},
}],
dimension: { width: 1920, height: 1080 },
});
return videoId;
}
```
### Why Use Default Voice?
1. **Guaranteed gender match** - Avatar and voice are pre-paired
2. **Natural lip sync** - Default voices are optimized for the avatar
3. **Simpler code** - No need to fetch and match voices separately
4. **Better quality** - HeyGen has tested this combination
## Selecting the Right Avatar
### Avatar Categories
HeyGen avatars fall into distinct categories. Match the category to your use case:
| Category | Examples | Best For |
|----------|----------|----------|
| **Business/Professional** | Josh, Angela, Wayne | Corporate videos, product demos, training |
| **Casual/Friendly** | Lily, various lifestyle avatars | Social media, informal content |
| **Themed/Seasonal** | Holiday-themed, costume avatars | Specific campaigns, seasonal content |
| **Expressive** | Avatars with "expressive" in name | Engaging storytelling, dynamic content |
### Selection Guidelines
**For business/professional content:**
- Choose avatars with neutral attire (business casual or formal)
- Avoid themed or seasonal avatars (holiday costumes, casual clothing)
- Preview the avatar to verify professional appearance
- Consider your audience demographics when selecting gender and appearance
**For casual/social content:**
- More flexibility in avatar choice
- Themed avatars can work for specific campaigns
- Match avatar energy to content tone
### Common Mistakes to Avoid
1. **Using themed avatars for business content** - A holiday-themed avatar looks unprofessional in a product demo
2. **Not previewing before generation** - Always check the preview URL to verify appearance
3. **Ignoring avatar style** - A `circle` style avatar may not work for full-screen presentations
4. **Mismatched voice gender** - Always use the avatar's `default_voice_id` or match genders manually
### Selection Checklist
Before generating a video:
- [ ] Previewed avatar image/video in browser
- [ ] Avatar appearance matches content tone (professional vs casual)
- [ ] Avatar style (`normal`, `closeUp`, `circle`) fits the video format
- [ ] Voice gender matches avatar gender
- [ ] Using `default_voice_id` when available
## Helper Functions
### Get Avatar by ID
```typescript
async function getAvatarById(avatarId: string): Promise<Avatar | null> {
const avatars = await listAvatars();
return avatars.find((a) => a.avatar_id === avatarId) || null;
}
```
### Validate Avatar ID
```typescript
async function isValidAvatarId(avatarId: string): Promise<boolean> {
const avatar = await getAvatarById(avatarId);
return avatar !== null;
}
```
### Get Random Avatar
```typescript
async function getRandomAvatar(gender?: "male" | "female"): Promise<Avatar> {
let avatars = await listAvatars();
if (gender) {
avatars = avatars.filter((a) => a.gender === gender);
}
const randomIndex = Math.floor(Math.random() * avatars.length);
return avatars[randomIndex];
}
```
## Common Avatar IDs
Some commonly used public avatar IDs (availability may vary):
| Avatar ID | Name | Gender |
|-----------|------|--------|
| `josh_lite3_20230714` | Josh | Male |
| `angela_expressive_20231010` | Angela | Female |
| `wayne_20240422` | Wayne | Male |
| `lily_20230614` | Lily | Female |
Always verify avatar availability by calling the list endpoint before using.
@@ -0,0 +1,295 @@
---
name: backgrounds
description: Solid colors, images, and video backgrounds for HeyGen videos
---
# Video Backgrounds
HeyGen supports various background types to customize the appearance of your avatar videos.
## Background Types
| Type | Description |
|------|-------------|
| `color` | Solid color background |
| `image` | Static image background |
| `video` | Looping video background |
## Color Backgrounds
The simplest option - use a solid color:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello with a colored background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#FFFFFF", // White background
},
},
],
};
```
### Common Color Values
| Color | Hex Value | Use Case |
|-------|-----------|----------|
| White | `#FFFFFF` | Clean, professional |
| Black | `#000000` | Dramatic, cinematic |
| Blue | `#0066CC` | Corporate, trustworthy |
| Green | `#00FF00` | Chroma key (for compositing) |
| Gray | `#808080` | Neutral, modern |
### Using Transparent/Green Screen
For compositing in post-production:
```typescript
background: {
type: "color",
value: "#00FF00", // Green screen
}
```
## Image Backgrounds
Use a static image as background:
### From URL
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Check out this custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/my-background.jpg",
},
},
],
};
```
### From Uploaded Asset
First upload your image, then use the asset URL:
```typescript
// 1. Upload the image
const assetId = await uploadFile("./background.jpg", "image/jpeg");
// 2. Use in video config
const videoConfig = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: `https://files.heygen.ai/asset/${assetId}`,
},
},
],
};
```
### Image Requirements
- **Formats**: JPEG, PNG
- **Recommended size**: Match video dimensions (e.g., 1920x1080 for 1080p)
- **Aspect ratio**: Should match video aspect ratio
- **File size**: Under 10MB recommended
## Video Backgrounds
Use a looping video as background:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Dynamic video background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "video",
url: "https://example.com/background-loop.mp4",
},
},
],
};
```
### Video Requirements
- **Format**: MP4 (H.264 codec recommended)
- **Looping**: Video will loop if shorter than avatar content
- **Audio**: Background video audio is typically muted
- **File size**: Under 100MB recommended
## Different Backgrounds Per Scene
Use different backgrounds for each scene:
```typescript
const multiBackgroundConfig = {
video_inputs: [
// Scene 1: Office background
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Let me start with an introduction.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/office-bg.jpg",
},
},
// Scene 2: Product showcase
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "closeUp",
},
voice: {
type: "text",
input_text: "Now let me show you our product.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/product-bg.jpg",
},
},
// Scene 3: Call to action
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Get started today!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
};
```
## Background Helper Functions
### TypeScript
```typescript
type BackgroundType = "color" | "image" | "video";
interface Background {
type: BackgroundType;
value?: string;
url?: string;
}
function createColorBackground(hexColor: string): Background {
return { type: "color", value: hexColor };
}
function createImageBackground(imageUrl: string): Background {
return { type: "image", url: imageUrl };
}
function createVideoBackground(videoUrl: string): Background {
return { type: "video", url: videoUrl };
}
// Preset backgrounds
const backgrounds = {
white: createColorBackground("#FFFFFF"),
black: createColorBackground("#000000"),
greenScreen: createColorBackground("#00FF00"),
corporate: createColorBackground("#0066CC"),
};
```
## Best Practices
1. **Match dimensions** - Background should match video dimensions
2. **Consider avatar position** - Leave space where avatar will appear
3. **Use contrasting colors** - Ensure avatar is visible against background
4. **Optimize file sizes** - Compress images/videos for faster processing
5. **Test with green screen** - For professional post-production workflows
6. **Keep backgrounds simple** - Avoid distracting elements behind the avatar
## Common Issues
### Background Not Showing
```typescript
// Wrong: missing url/value
background: {
type: "image"
}
// Correct
background: {
type: "image",
url: "https://example.com/bg.jpg"
}
```
### Aspect Ratio Mismatch
If your background doesn't match the video dimensions, it may be cropped or stretched. Always match your background aspect ratio to your video dimensions:
```typescript
// For 1920x1080 video
// Use 1920x1080 background image
// For 1080x1920 portrait video
// Use 1080x1920 background image
```
### Video Background Audio
Background video audio is typically muted to avoid conflicting with the avatar's voice. If you need background music, add it as a separate audio track in post-production.
@@ -0,0 +1,277 @@
---
name: captions
description: Auto-generated captions and subtitle options for HeyGen videos
---
# Video Captions
HeyGen can automatically generate captions (subtitles) for your videos, improving accessibility and engagement.
## Enabling Captions
Captions can be enabled when generating a video:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! This video will have automatic captions.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
// Caption settings (availability varies by plan)
caption: true,
};
```
## Caption Configuration Options
```typescript
interface CaptionConfig {
// Enable/disable captions
enabled: boolean;
// Caption style
style?: {
font_family?: string;
font_size?: number;
font_color?: string;
background_color?: string;
position?: "top" | "bottom";
};
// Language for caption generation
language?: string;
}
```
## Caption Styles
### Basic Captions
```typescript
const config = {
video_inputs: [...],
caption: true, // Enable with default styling
};
```
### Styled Captions
```typescript
const config = {
video_inputs: [...],
caption: {
enabled: true,
style: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
position: "bottom",
},
},
};
```
## Multi-Language Captions
For videos in different languages, captions are generated based on the voice language:
```typescript
// Spanish video with Spanish captions
const spanishConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "¡Hola! Este video tendrá subtítulos en español.",
voice_id: "spanish_voice_id",
},
},
],
caption: true,
};
```
## Working with SRT Files
### SRT File Format
Standard SRT format:
```srt
1
00:00:00,000 --> 00:00:03,000
Hello! This video will have
2
00:00:03,000 --> 00:00:06,000
automatic captions generated.
3
00:00:06,000 --> 00:00:09,000
They sync with the audio.
```
### Using Custom SRT
For video translation, you can provide your own SRT:
```typescript
const translationConfig = {
input_video_id: "original_video_id",
output_languages: ["es-ES", "fr-FR"],
srt_key: "path/to/custom.srt", // Custom SRT file
srt_role: "input", // "input" or "output"
};
```
## Caption Positioning
### Bottom (Default)
Standard position for most videos:
```typescript
caption: {
enabled: true,
style: {
position: "bottom"
}
}
```
### Top
For videos where bottom space is occupied:
```typescript
caption: {
enabled: true,
style: {
position: "top"
}
}
```
## Accessibility Best Practices
1. **Always enable captions** - Improves accessibility for deaf/hard-of-hearing viewers
2. **Use high contrast** - White text on dark background or vice versa
3. **Readable font size** - At least 24px for standard video, larger for mobile
4. **Don't cover important content** - Position captions away from key visual elements
5. **Sync timing** - Ensure captions match audio timing accurately
## Caption Helper Functions
```typescript
interface CaptionStyle {
font_family: string;
font_size: number;
font_color: string;
background_color: string;
position: "top" | "bottom";
}
const captionPresets: Record<string, CaptionStyle> = {
default: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
position: "bottom",
},
minimal: {
font_family: "Arial",
font_size: 28,
font_color: "#FFFFFF",
background_color: "transparent",
position: "bottom",
},
bold: {
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.9)",
position: "bottom",
},
branded: {
font_family: "Roboto",
font_size: 30,
font_color: "#00D1FF",
background_color: "rgba(26, 26, 46, 0.9)",
position: "bottom",
},
};
function createCaptionConfig(preset: keyof typeof captionPresets) {
return {
enabled: true,
style: captionPresets[preset],
};
}
```
## Social Media Caption Considerations
### TikTok / Instagram Reels
- Position captions in center or upper portion
- Avoid bottom 20% (covered by UI elements)
- Use larger font sizes for mobile viewing
```typescript
const socialCaptions = {
enabled: true,
style: {
font_size: 42,
position: "top", // Avoid bottom UI elements
},
};
```
### YouTube
- Standard bottom captions work well
- YouTube also supports closed captions upload
### LinkedIn
- Captions highly recommended (many watch without sound)
- Professional styling preferred
## Limitations
- Caption styles may be limited depending on your subscription tier
- Some advanced caption features may require the web interface
- Multi-speaker caption detection may have limited availability
- Caption accuracy depends on audio quality and speech clarity
## Integration with Video Translation
When using video translation, captions are automatically handled:
```typescript
// Video translation includes caption generation
const translationConfig = {
input_video_id: "original_video_id",
output_languages: ["es-ES"],
// Captions generated in target language
};
```
See [video-translation.md](video-translation.md) for more details.
@@ -0,0 +1,314 @@
---
name: dimensions
description: Resolution options (720p/1080p) and aspect ratios for HeyGen videos
---
# Video Dimensions and Resolution
HeyGen supports various video dimensions and aspect ratios to fit different platforms and use cases.
## Standard Resolutions
### Landscape (16:9)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 1280 | 720 | Standard quality, faster processing |
| 1080p | 1920 | 1080 | High quality, most common |
### Portrait (9:16)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 1280 | Mobile-first content |
| 1080p | 1080 | 1920 | High quality vertical |
### Square (1:1)
| Resolution | Width | Height | Use Case |
|------------|-------|--------|----------|
| 720p | 720 | 720 | Social media posts |
| 1080p | 1080 | 1080 | High quality square |
## Setting Dimensions
### TypeScript
```typescript
// Landscape 1080p
const landscapeConfig = {
video_inputs: [...],
dimension: {
width: 1920,
height: 1080
}
};
// Portrait 1080p
const portraitConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1920
}
};
// Square 1080p
const squareConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1080
}
};
```
### curl
```bash
# Landscape 1080p
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [...],
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
## Dimension Helper Functions
```typescript
type AspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "4:5";
type Quality = "720p" | "1080p";
interface Dimensions {
width: number;
height: number;
}
function getDimensions(aspectRatio: AspectRatio, quality: Quality): Dimensions {
const configs: Record<AspectRatio, Record<Quality, Dimensions>> = {
"16:9": {
"720p": { width: 1280, height: 720 },
"1080p": { width: 1920, height: 1080 },
},
"9:16": {
"720p": { width: 720, height: 1280 },
"1080p": { width: 1080, height: 1920 },
},
"1:1": {
"720p": { width: 720, height: 720 },
"1080p": { width: 1080, height: 1080 },
},
"4:3": {
"720p": { width: 960, height: 720 },
"1080p": { width: 1440, height: 1080 },
},
"4:5": {
"720p": { width: 576, height: 720 },
"1080p": { width: 864, height: 1080 },
},
};
return configs[aspectRatio][quality];
}
// Usage
const youTubeDimensions = getDimensions("16:9", "1080p");
const tikTokDimensions = getDimensions("9:16", "1080p");
const instagramDimensions = getDimensions("1:1", "1080p");
```
## Platform-Specific Recommendations
### YouTube
```typescript
const youtubeConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape
};
```
### TikTok / Instagram Reels / YouTube Shorts
```typescript
const shortFormConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1920 }, // 9:16 portrait
};
```
### Instagram Feed Post
```typescript
const instagramFeedConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1080 }, // 1:1 square
};
```
### LinkedIn
```typescript
const linkedinConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred
};
```
### Twitter/X
```typescript
const twitterConfig = {
video_inputs: [...],
dimension: { width: 1280, height: 720 }, // 16:9, 720p is common
};
```
## Avatar IV Dimensions
For Avatar IV (photo-based avatars), dimensions are set via orientation:
```typescript
type VideoOrientation = "portrait" | "landscape" | "square";
function getAvatarIVDimensions(orientation: VideoOrientation): Dimensions {
switch (orientation) {
case "portrait":
return { width: 720, height: 1280 };
case "landscape":
return { width: 1280, height: 720 };
case "square":
return { width: 720, height: 720 };
}
}
```
## Custom Dimensions
HeyGen supports custom dimensions within limits:
```typescript
const customConfig = {
video_inputs: [...],
dimension: {
width: 1600,
height: 900 // Custom 16:9 at non-standard resolution
}
};
```
### Dimension Constraints
- **Minimum**: 128px on any side
- **Maximum**: 4096px on any side
- **Must be even numbers**: Both width and height must be divisible by 2
```typescript
function validateDimensions(width: number, height: number): boolean {
if (width < 128 || height < 128) {
throw new Error("Dimensions must be at least 128px");
}
if (width > 4096 || height > 4096) {
throw new Error("Dimensions cannot exceed 4096px");
}
if (width % 2 !== 0 || height % 2 !== 0) {
throw new Error("Dimensions must be even numbers");
}
return true;
}
```
## Resolution vs. Credit Cost
Higher resolutions may consume more credits:
| Resolution | Relative Cost |
|------------|---------------|
| 720p | Base rate |
| 1080p | ~1.5x base rate |
Consider using 720p for drafts and testing, then 1080p for final output.
## Background Considerations
Match background image/video dimensions to your video dimensions:
```typescript
// For 1080p landscape video
const config = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: "https://example.com/1920x1080-background.jpg" // Match video dimensions
}
}
],
dimension: { width: 1920, height: 1080 }
};
```
## Creating a Video Config Factory
```typescript
interface VideoConfigOptions {
script: string;
avatarId: string;
voiceId: string;
platform: "youtube" | "tiktok" | "instagram_feed" | "instagram_story" | "linkedin";
quality?: "720p" | "1080p";
}
function createVideoConfig(options: VideoConfigOptions) {
const platformDimensions: Record<string, Dimensions> = {
youtube: { width: 1920, height: 1080 },
tiktok: { width: 1080, height: 1920 },
instagram_feed: { width: 1080, height: 1080 },
instagram_story: { width: 1080, height: 1920 },
linkedin: { width: 1920, height: 1080 },
};
const dimension = platformDimensions[options.platform];
// Scale down for 720p if requested
if (options.quality === "720p") {
dimension.width = Math.round((dimension.width * 720) / 1080);
dimension.height = Math.round((dimension.height * 720) / 1080);
}
return {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: options.avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: options.script,
voice_id: options.voiceId,
},
},
],
dimension,
};
}
// Usage
const tiktokVideo = createVideoConfig({
script: "Hey everyone! Check this out!",
avatarId: "josh_lite3_20230714",
voiceId: "1bd001e7e50f421d891986aad5158bc8",
platform: "tiktok",
quality: "1080p",
});
```
@@ -0,0 +1,853 @@
---
name: photo-avatars
description: Creating avatars from photos (talking photos) for HeyGen
---
# Photo Avatars (Talking Photos)
Photo avatars allow you to animate a static photo and make it speak. This is useful for creating personalized video content from portraits, headshots, or any suitable image.
## Creating a Photo Avatar from an Uploaded Image
The workflow is: **Upload Image → Create Avatar Group → Use in Video**
### Step 1: Upload the Image
Upload a portrait photo using the asset upload endpoint. The response includes an `image_key` which you'll use in the next step.
```bash
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./portrait.jpg'
```
Response:
```json
{
"code": 100,
"data": {
"id": "741299e941764988b432ed3a6757878f",
"name": "741299e941764988b432ed3a6757878f",
"file_type": "image",
"url": "https://resource2.heygen.ai/image/.../original.jpg",
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg"
}
}
```
> **Important:** Save the `image_key` field (not the `id`). The `image_key` is the S3 path used to create the photo avatar.
See [assets.md](assets.md) for full upload details.
### Step 2: Create Photo Avatar Group
Use the `image_key` from the upload response to create a photo avatar group. This processes the image and creates a usable photo avatar.
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/create`
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/avatar_group/create" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg",
"name": "My Photo Avatar"
}'
```
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `image_key` | string | ✓ | S3 image key from upload response |
| `name` | string | ✓ | Display name for the avatar |
| `generation_id` | string | | If using AI-generated photo (see below) |
Response:
```json
{
"error": null,
"data": {
"id": "045c260bc0364727b2cbe50442c3a5bf",
"image_url": "https://files2.heygen.ai/...",
"created_at": 1771798135.777256,
"name": "My Photo Avatar",
"status": "pending",
"group_id": "045c260bc0364727b2cbe50442c3a5bf",
"is_motion": false,
"business_type": "uploaded"
}
}
```
The `id` (same as `group_id`) is your `talking_photo_id` for video generation.
### Step 3: Wait for Processing
The photo avatar starts with `status: "pending"` and transitions to `"completed"` within seconds. Poll the status endpoint:
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`
```bash
curl "https://api.heygen.com/v2/photo_avatar/045c260bc0364727b2cbe50442c3a5bf" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
Wait until `status` is `"completed"` before using in video generation.
### Step 4: Use in Video Generation
Use the photo avatar `id` as `talking_photo_id`:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: "045c260bc0364727b2cbe50442c3a5bf",
},
voice: {
type: "text",
input_text: "Hello! This is my photo avatar speaking.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
## TypeScript: Complete Workflow
```typescript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
image_key: string;
url: string;
};
}
interface PhotoAvatarResponse {
error: string | null;
data: {
id: string;
group_id: string;
image_url: string;
name: string;
status: string;
is_motion: boolean;
business_type: string;
};
}
async function createPhotoAvatar(
imagePath: string,
name: string
): Promise<string> {
// 1. Upload image
const resolvedPath = path.resolve(imagePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const uploadResponse = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "image/jpeg",
},
body: fileBuffer,
});
const uploadJson: AssetUploadResponse = await uploadResponse.json();
if (uploadJson.code !== 100) {
throw new Error("Upload failed");
}
const imageKey = uploadJson.data.image_key;
// 2. Create avatar group
const createResponse = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ image_key: imageKey, name }),
}
);
const createJson: PhotoAvatarResponse = await createResponse.json();
if (createJson.error) {
throw new Error(createJson.error);
}
const photoAvatarId = createJson.data.id;
// 3. Wait for processing
await waitForPhotoAvatar(photoAvatarId);
return photoAvatarId;
}
async function waitForPhotoAvatar(id: string): Promise<void> {
for (let i = 0; i < 30; i++) {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: PhotoAvatarResponse = await response.json();
if (json.data.status === "completed") return;
if (json.data.status === "failed") {
throw new Error("Photo avatar processing failed");
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Photo avatar processing timed out");
}
async function createVideoFromPhoto(
photoPath: string,
script: string,
voiceId: string
): Promise<string> {
// 1. Create photo avatar
const talkingPhotoId = await createPhotoAvatar(photoPath, "Video Avatar");
// 2. Generate video
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: talkingPhotoId,
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
},
],
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
## Python: Complete Workflow
```python
import requests
import os
import time
def create_photo_avatar(image_path: str, name: str) -> str:
api_key = os.environ["HEYGEN_API_KEY"]
# 1. Upload image
with open(image_path, "rb") as f:
upload_resp = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": api_key,
"Content-Type": "image/jpeg",
},
data=f,
)
upload_data = upload_resp.json()
if upload_data.get("code") != 100:
raise Exception("Upload failed")
image_key = upload_data["data"]["image_key"]
# 2. Create avatar group
create_resp = requests.post(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
headers={
"X-Api-Key": api_key,
"Content-Type": "application/json",
},
json={"image_key": image_key, "name": name},
)
create_data = create_resp.json()
if create_data.get("error"):
raise Exception(create_data["error"])
photo_avatar_id = create_data["data"]["id"]
# 3. Wait for processing
for _ in range(30):
status_resp = requests.get(
f"https://api.heygen.com/v2/photo_avatar/{photo_avatar_id}",
headers={"X-Api-Key": api_key},
)
status = status_resp.json()["data"]["status"]
if status == "completed":
return photo_avatar_id
if status == "failed":
raise Exception("Photo avatar processing failed")
time.sleep(2)
raise Exception("Photo avatar processing timed out")
```
## Listing Existing Talking Photos
Retrieve all talking photos in your account:
**Endpoint:** `GET https://api.heygen.com/v1/talking_photo.list`
```bash
curl "https://api.heygen.com/v1/talking_photo.list" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
Response:
```json
{
"code": 100,
"data": [
{
"id": "ef0ed70f72c6497793e5e36e434d2aea",
"image_url": "https://files2.heygen.ai/talking_photo/.../image.WEBP",
"circle_image": ""
}
]
}
```
Each `id` can be used as `talking_photo_id` in video generation.
## Adding Photos to an Existing Group
Add additional photo looks to an existing avatar group:
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/add`
```typescript
async function addPhotosToGroup(
groupId: string,
imageKeys: string[],
name: string
): Promise<void> {
const response = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/add",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
group_id: groupId,
image_keys: imageKeys,
name,
}),
}
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}
```
## Training a Photo Avatar Group
Train the avatar group for improved animation quality:
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/train`
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/train" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"group_id": "045c260bc0364727b2cbe50442c3a5bf"}'
```
Check training status:
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/train/status/{group_id}`
## Avatar IV Video Generation
Avatar IV is HeyGen's latest photo avatar technology with improved quality and natural motion. It generates a video directly from an uploaded image, bypassing the avatar group creation step.
**Endpoint:** `POST https://api.heygen.com/v2/video/av4/generate`
```bash
curl -X POST "https://api.heygen.com/v2/video/av4/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_key": "image/741299e941764988b432ed3a6757878f/original.jpg",
"script": "Hello! This is Avatar IV with enhanced quality.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"video_orientation": "landscape",
"video_title": "My Avatar IV Video"
}'
```
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `image_key` | string | ✓ | S3 image key from asset upload |
| `script` | string | ✓ | Text for the avatar to speak |
| `voice_id` | string | ✓ | Voice to use |
| `video_orientation` | string | | `"portrait"`, `"landscape"`, or `"square"` |
| `video_title` | string | | Title for the video |
| `fit` | string | | `"cover"` or `"contain"` |
| `custom_motion_prompt` | string | | Motion/expression description |
| `enhance_custom_motion_prompt` | boolean | | Enhance the motion prompt with AI |
### TypeScript
```typescript
interface AvatarIVRequest {
image_key: string;
script: string;
voice_id: string;
video_orientation?: "portrait" | "landscape" | "square";
video_title?: string;
fit?: "cover" | "contain";
custom_motion_prompt?: string;
enhance_custom_motion_prompt?: boolean;
}
interface AvatarIVResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateAvatarIVVideo(
config: AvatarIVRequest
): Promise<string> {
const response = await fetch(
"https://api.heygen.com/v2/video/av4/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const json: AvatarIVResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Avatar IV Options
| Orientation | Dimensions | Use Case |
|-------------|------------|----------|
| `portrait` | 720x1280 | TikTok, Stories |
| `landscape` | 1280x720 | YouTube, Web |
| `square` | 720x720 | Instagram Feed |
| Fit | Description |
|-----|-------------|
| `cover` | Fill the frame, may crop edges |
| `contain` | Fit entire image, may show background |
### Custom Motion Prompts
```typescript
const videoId = await generateAvatarIVVideo({
image_key: "image/.../original.jpg",
script: "Let me tell you about our product.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
custom_motion_prompt: "nodding head and smiling",
enhance_custom_motion_prompt: true,
});
```
## Generating AI Photo Avatars
Generate synthetic photo avatars from text descriptions instead of uploading a photo.
**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/photo/generate`
> **IMPORTANT: All 8 fields are REQUIRED.** The API will reject requests missing any field.
> When a user asks to "generate an AI avatar of a professional man", you need to ask for or select values for ALL fields below.
### Required Fields (ALL must be provided)
| Field | Type | Allowed Values |
|-------|------|----------------|
| `name` | string | Name for the generated avatar |
| `age` | enum | `"Young Adult"`, `"Early Middle Age"`, `"Late Middle Age"`, `"Senior"`, `"Unspecified"` |
| `gender` | enum | `"Woman"`, `"Man"`, `"Unspecified"` |
| `ethnicity` | enum | `"White"`, `"Black"`, `"Asian American"`, `"East Asian"`, `"South East Asian"`, `"South Asian"`, `"Middle Eastern"`, `"Pacific"`, `"Hispanic"`, `"Unspecified"` |
| `orientation` | enum | `"square"`, `"horizontal"`, `"vertical"` |
| `pose` | enum | `"half_body"`, `"close_up"`, `"full_body"` |
| `style` | enum | `"Realistic"`, `"Pixar"`, `"Cinematic"`, `"Vintage"`, `"Noir"`, `"Cyberpunk"`, `"Unspecified"` |
| `appearance` | string | Text prompt describing appearance (clothing, mood, lighting, etc). Max 1000 chars |
### curl Example
```bash
curl -X POST "https://api.heygen.com/v2/photo_avatar/photo/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sarah Product Demo",
"age": "Young Adult",
"gender": "Woman",
"ethnicity": "White",
"orientation": "horizontal",
"pose": "half_body",
"style": "Realistic",
"appearance": "Professional woman with a friendly smile, wearing a navy blue blazer over a white blouse, soft studio lighting, clean neutral background"
}'
```
Response:
```json
{
"error": null,
"data": {
"generation_id": "6a7f7f2795de4599bec7cf1e06babe30"
}
}
```
### Check Generation Status
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/generation/{generation_id}`
The response includes multiple generated images to choose from:
```json
{
"error": null,
"data": {
"id": "6a7f7f2795de4599bec7cf1e06babe30",
"status": "success",
"image_url_list": [
"https://resource2.heygen.ai/photo_generation/.../image1.jpg",
"https://resource2.heygen.ai/photo_generation/.../image2.jpg",
"https://resource2.heygen.ai/photo_generation/.../image3.jpg",
"https://resource2.heygen.ai/photo_generation/.../image4.jpg"
],
"image_key_list": [
"photo_generation/.../image1.jpg",
"photo_generation/.../image2.jpg",
"photo_generation/.../image3.jpg",
"photo_generation/.../image4.jpg"
]
}
}
```
### TypeScript
```typescript
interface GeneratePhotoAvatarRequest {
name: string;
age: "Young Adult" | "Early Middle Age" | "Late Middle Age" | "Senior" | "Unspecified";
gender: "Woman" | "Man" | "Unspecified";
ethnicity: "White" | "Black" | "Asian American" | "East Asian" | "South East Asian" | "South Asian" | "Middle Eastern" | "Pacific" | "Hispanic" | "Unspecified";
orientation: "square" | "horizontal" | "vertical";
pose: "half_body" | "close_up" | "full_body";
style: "Realistic" | "Pixar" | "Cinematic" | "Vintage" | "Noir" | "Cyberpunk" | "Unspecified";
appearance: string;
}
interface GeneratePhotoAvatarResponse {
error: string | null;
data: {
generation_id: string;
};
}
interface PhotoGenerationStatus {
error: string | null;
data: {
id: string;
status: "pending" | "processing" | "success" | "failed";
msg: string | null;
image_url_list?: string[];
image_key_list?: string[];
};
}
async function generatePhotoAvatar(
config: GeneratePhotoAvatarRequest
): Promise<string> {
const response = await fetch(
"https://api.heygen.com/v2/photo_avatar/photo/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const json: GeneratePhotoAvatarResponse = await response.json();
if (json.error) {
throw new Error(`Photo avatar generation failed: ${json.error}`);
}
return json.data.generation_id;
}
async function waitForPhotoGeneration(
generationId: string
): Promise<string[]> {
for (let i = 0; i < 60; i++) {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/generation/${generationId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: PhotoGenerationStatus = await response.json();
if (json.error) throw new Error(json.error);
if (json.data.status === "success") {
return json.data.image_key_list!;
}
if (json.data.status === "failed") {
throw new Error(json.data.msg ?? "Photo generation failed");
}
await new Promise((r) => setTimeout(r, 5000));
}
throw new Error("Photo generation timed out");
}
```
### AI Photo → Avatar Group → Video
Use a generated AI photo to create an avatar group, then generate a video:
```typescript
// 1. Generate AI photo
const generationId = await generatePhotoAvatar({
name: "Product Demo Host",
age: "Young Adult",
gender: "Woman",
ethnicity: "Unspecified",
orientation: "horizontal",
pose: "half_body",
style: "Realistic",
appearance: "Professional woman, navy blazer, friendly smile, soft lighting",
});
// 2. Wait for generation and pick first result
const imageKeys = await waitForPhotoGeneration(generationId);
const selectedImageKey = imageKeys[0];
// 3. Create avatar group from the AI photo
const createResponse = await fetch(
"https://api.heygen.com/v2/photo_avatar/avatar_group/create",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
image_key: selectedImageKey,
name: "Product Demo Host",
generation_id: generationId,
}),
}
);
const { data } = await createResponse.json();
const talkingPhotoId = data.id;
// 4. Generate video (after status is "completed")
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "talking_photo",
talking_photo_id: talkingPhotoId,
},
voice: {
type: "text",
input_text: "Welcome to our product demo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
}],
dimension: { width: 1920, height: 1080 },
});
```
### Pre-Generation Checklist
Before calling the AI generation API, ensure you have values for ALL fields:
| # | Field | Question to Ask / Default |
|---|-------|---------------------------|
| 1 | `name` | What should we call this avatar? |
| 2 | `age` | Young Adult / Early Middle Age / Late Middle Age / Senior? |
| 3 | `gender` | Woman / Man? |
| 4 | `ethnicity` | Which ethnicity? (see enum values above) |
| 5 | `orientation` | horizontal (landscape) / vertical (portrait) / square? |
| 6 | `pose` | half_body (recommended) / close_up / full_body? |
| 7 | `style` | Realistic (recommended) / Cinematic / other? |
| 8 | `appearance` | Describe clothing, expression, lighting, background |
**If the user only provides a vague request** like "create a professional looking man", ask them to specify the missing fields OR make reasonable defaults (e.g., "Early Middle Age", "Realistic" style, "half_body" pose, "horizontal" orientation).
### Appearance Prompt Tips
The `appearance` field is a text prompt - be descriptive:
**Good prompts:**
- "Professional woman with shoulder-length brown hair, wearing a light blue button-down shirt, warm friendly smile, soft studio lighting, clean white background"
- "Young man with short black hair, casual tech startup style, wearing a dark hoodie, confident expression, modern office background with plants"
**Avoid:**
- Vague descriptions: "a nice person"
- Conflicting attributes
- Requesting specific real people
## Managing Photo Avatars
### Get Photo Avatar Details
**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`
```typescript
async function getPhotoAvatar(id: string): Promise<PhotoAvatarResponse> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
return response.json();
}
```
### Delete Photo Avatar
**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar/{id}`
```typescript
async function deletePhotoAvatar(id: string): Promise<void> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar/${id}`,
{
method: "DELETE",
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
}
);
if (!response.ok) {
throw new Error("Failed to delete photo avatar");
}
}
```
### Delete Photo Avatar Group
**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar_group/{group_id}`
```typescript
async function deletePhotoAvatarGroup(groupId: string): Promise<void> {
const response = await fetch(
`https://api.heygen.com/v2/photo_avatar_group/${groupId}`,
{
method: "DELETE",
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
}
);
if (!response.ok) {
throw new Error("Failed to delete photo avatar group");
}
}
```
## API Reference
| Endpoint | Method | Description |
|----------|--------|-------------|
| `upload.heygen.com/v1/asset` | POST | Upload image (returns `image_key`) |
| `/v2/photo_avatar/avatar_group/create` | POST | Create photo avatar from `image_key` |
| `/v2/photo_avatar/avatar_group/add` | POST | Add photos to existing group |
| `/v2/photo_avatar/train` | POST | Train avatar group |
| `/v2/photo_avatar/train/status/{group_id}` | GET | Check training status |
| `/v2/photo_avatar/{id}` | GET | Get photo avatar details/status |
| `/v2/photo_avatar/{id}` | DELETE | Delete photo avatar |
| `/v2/photo_avatar_group/{id}` | DELETE | Delete avatar group |
| `/v2/photo_avatar/photo/generate` | POST | Generate AI photo from text |
| `/v2/photo_avatar/generation/{id}` | GET | Check AI generation status |
| `/v2/video/av4/generate` | POST | Avatar IV video from `image_key` |
| `/v1/talking_photo.list` | GET | List all existing talking photos |
| `/v2/video/generate` | POST | Generate video with `talking_photo_id` |
## Photo Requirements
### Technical Requirements
| Aspect | Requirement |
|--------|-------------|
| Format | JPEG, PNG |
| Resolution | Minimum 512x512px |
| File size | Under 10MB |
| Face visibility | Clear, front-facing |
### Quality Guidelines
1. **Lighting** - Even, natural lighting on face
2. **Expression** - Neutral or slight smile
3. **Background** - Simple, uncluttered
4. **Face position** - Centered, not cut off
5. **Clarity** - Sharp, in focus
6. **Angle** - Straight-on or slight angle
## Best Practices
1. **Use high-quality photos** - Better input = better output
2. **Front-facing portraits** - Work best for animation
3. **Neutral expressions** - Allow for more natural animation
4. **Use Avatar IV for best quality** - Latest generation technology
5. **Train avatar groups** - Improves animation quality
6. **Reuse photo avatar IDs** - Once created, use the same `talking_photo_id` across multiple videos
## Limitations
- Photo quality significantly affects output
- Side-profile photos have limited support
- Full-body photos may not animate properly
- Some expressions may look unnatural
- Processing time varies by complexity
@@ -0,0 +1,206 @@
---
name: prompt-examples
description: Full production prompt examples and ready-to-use templates for Video Agent
---
# Video Agent Prompt Examples
## Full Example: Brief to Production Prompt
### Input Brief
```
Topic: Monthly company report for a SaaS startup
Key data: $141M ARR (up from $54M), 1.85M signups (+28%), 3M paid videos/month
Customer story: Creator built AI character, 2.5M followers, 20 min/video
Challenge: Organic traffic volatile, -16% last week
Duration: ~90 seconds
Tone: Confident CEO, data-backed
```
### Output Prompt
```
FORMAT: Bloomberg-style company report. 90 seconds. Fast-paced, data-dense.
Record-breaking month. Proud but analytical.
TONE: Confident, direct, data-backed. Highlights hit hard with numbers.
Customer stories are the emotional core. Challenges are honest — no spin.
AVATAR: Man in simple black crew-neck tee, standing in a modern glass-walled
office at golden hour. Behind him, a wall-mounted display shows the company logo
in soft blue glow. Monitor to his right shows a dashboard with upward-trending
charts. Desk beside him: laptop, half-empty flat white, scattered sticky notes.
Warm afternoon light through floor-to-ceiling windows, long shadows on polished
concrete. Minimal, focused startup HQ.
STYLE — SWISS PULSE (Müller-Brockmann): Grid-locked compositions. Black (#1a1a1a),
white, electric blue (#0066FF), warm amber (#FF9500) for records. Helvetica Bold
headlines, Regular labels. Numbers LARGE. Animated counters count up from 0.
Diagonal compositions on accent moments. Grid wipe transitions. No dissolves.
CRITICAL ON-SCREEN TEXT (display literally):
- "1.85M SIGNUPS — +28% MoM"
- "$2.12M NEW SUBSCRIPTION REVENUE"
- "$54M → $141M ARR"
- "2.5M FOLLOWERS" and "20 MIN / VIDEO"
- Quote: "Use technology to serve the message, not distract from it."
- "ORGANIC: 65% OF SUBS — VOLATILE"
MUSIC: Upbeat electronic with a driving beat. Tycho meets Bloomberg opening theme.
Builds through highlights, warms for customer story, softens for challenges, peaks
on close.
---
SCENE 1 — A-ROLL (8s)
[Avatar center-frame, energetic, leaning slightly forward]
VOICEOVER: "January was a record month. New highs across acquisition, revenue,
and product velocity. Here's the full picture."
Lower-third SLIDES in: "COMPANY NAME | JANUARY 2026" white on blue bar.
Grid wipe.
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "One-point-eight-five million signups — twenty-eight percent month
over month. Two-point-one-two million in new subscription revenue. Both all-time
highs."
LAYER 1: Dark #1a1a1a background with thin grid lines pulsing at 8% opacity.
LAYER 2: "1.85M" SLAMS in from left, white Bold 140pt. "SIGNUPS" types on
in electric blue 32pt uppercase. "+28% MoM" appears in amber.
LAYER 3: Three stat cards CASCADE from top-right, staggered 0.3s:
"$2.12M New Revenue" — "$3.4M Business ARR" — "$3M Pro ARR."
Each number COUNTS UP from 0.
LAYER 4: Bottom ticker scrolls: "Non-brand search +36% • Brand impressions 9.2M
• Weekly subs +20.5%"
LAYER 5: Grid lines RIPPLE outward on "1.85M" slam. Diagonal amber bar behind
stat cards.
Hard cut.
SCENE 3 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "Zoom out. Twelve months ago — fifty-four million ARR. Today —
one hundred forty-one million. Nearly three X in a single year."
LAYER 1: Dark background, subtle grid scrolling upward.
LAYER 2: Animated line chart DRAWS ITSELF left to right. Y-axis: $50M to $150M.
Final point "$140.84M" glows amber and pulses.
LAYER 3: Milestone annotations float in at key data points.
LAYER 4: Second smaller chart below — "Paid Videos" 0.91M to 2.97M, same style.
LAYER 5: Thin grid lines converge toward final data point. Scan line sweeps.
Grid wipe.
SCENE 4 — A-ROLL (8s)
[Avatar center-frame, warm tone, genuine smile]
VOICEOVER: "But the numbers only tell half the story. The other half is the
people building on the platform."
Lower-third: "Customer Spotlight"
SCENE 5 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — warm palette]
VOICEOVER: "An AI character built entirely on the platform. Twenty minutes
per video. Two-point-five million Instagram followers. The creator's principle:
use technology to serve the message, not distract from it."
LAYER 1: Dark background with warm amber grid lines at low opacity.
LAYER 2: "CHARACTER NAME" in large white, center-top, 80pt.
LAYER 3: Stats cascade from right: "2.5M Followers" COUNTS UP in amber —
"20 min/video" — "7x Faster." Each a glowing node.
LAYER 4: Quote card SLIDES UP: "Use technology to serve the message, not
distract from it." Types on word by word.
LAYER 5: Warm light bloom. Grid lines soften into curved arcs.
Grid wipe.
SCENE 6 — A-ROLL (10s)
[Avatar center-frame, serious/candid]
VOICEOVER: "Now the honest part. Organic drives sixty-five percent of
subscriptions and it's volatile. Non-brand traffic dropped sixteen percent
last week. We've rebuilt attribution and we're investing in SEO."
Lower-third: "Challenges"
SCENE 7 — A-ROLL (7s)
[Avatar center-frame, energy lifts, direct eye contact]
VOICEOVER: "Fifty-four million to one-forty-one in twelve months. Three million
paid videos a month. January set the bar — now we raise it."
End card: Logo centered, blue glow fade-in. Grid lines converge. Music peaks.
---
NARRATION STYLE: CEO energy — conviction backed by data. Fast on highlights.
Warm on customer stories. Candid on challenges. Close with forward momentum.
```
## Ready-to-Use Templates
### Tech News Briefing
```
FORMAT: 75-second high-energy tech briefing. Think: Bloomberg meets Vice.
AVATAR: [Presenter in tech-casual at a multi-monitor station.
Describe clothing, monitor content, desk items, lighting.]
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures. Smash cut transitions.
CRITICAL ON-SCREEN TEXT:
- [List every stat, quote, handle that must appear]
SCENE 1 — A-ROLL (8s): Hook with energy. State what's happening.
SCENE 2 — B-ROLL (12s): First story with layered visuals (L1-L5).
SCENE 3 — A-ROLL + OVERLAY (10s): Second story, split frame.
SCENE 4 — B-ROLL (10s): Third story or dramatic data point.
SCENE 5 — A-ROLL (8s): Wrap-up and forward look.
```
### Product Comparison
```
FORMAT: 60-second comparison. [Product A] vs [Product B]. Data-driven.
AVATAR: [Presenter in review studio. Desk with both products visible.]
STYLE — DIGITAL GRID (Crouwel): Dark #0a0a0a, cyan #00D4FF and amber #FFB800.
Two-color coding: cyan = Product A, amber = Product B. Monospaced type.
CRITICAL ON-SCREEN TEXT:
- [Key stats for each product]
- [Pricing, features, differentiators]
Use SPLIT FRAME B-roll: Product A left, Product B right.
```
### Strategy Presentation
```
FORMAT: 90-second strategy briefing. Bloomberg meets board meeting.
AVATAR: [Executive in blazer over tee. Conference room with whiteboard frameworks.]
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + blue #0066FF.
Grid-locked. Helvetica. Animated counters. Grid wipe transitions.
CRITICAL ON-SCREEN TEXT:
- [Framework labels, quadrant labels, key quotes]
Build frameworks visually: draw axes, plot positions, animate labels.
```
### Social Ad (30 seconds)
```
FORMAT: 30-second social ad. Maximum energy. Portrait 9:16.
AVATAR: [Creator-style presenter. Ring light, colorful background.]
STYLE — CARNIVAL SURGE (Lins): Hot pink, yellow, teal. Collage layering.
Text MASSIVE at angles. Confetti. Smash cuts.
Three scenes: Hook (8s) → Value prop (12s) → CTA (10s).
Text fills 50-80% of every frame. Numbers SLAM.
```
### Premium Report
```
FORMAT: 120-second investor-grade report. Understated authority.
AVATAR: [Tailored merino sweater. Architectural room, diffused natural light.]
STYLE — VELVET STANDARD (Vignelli): Black, white, gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow cross-dissolves. Numbers fade in with weight.
```
@@ -0,0 +1,289 @@
---
name: prompt-optimizer
description: Write production-quality prompts for HeyGen Video Agent — from basic ideas to fully art-directed scene-by-scene scripts
---
# Video Agent Prompt Optimizer
Write effective prompts for the HeyGen Video Agent API. Based on patterns from 40+ produced videos.
**The core insight: Video Agent is an HTML interpreter.** It renders layouts, typography, and structured content natively. Describe B-roll as layered text motion graphics with action verbs ("slams in," "types on," "counts up") — not layout specs ("upper-left, 48pt").
## Reference Files
| File | Load when... |
|------|-------------|
| [visual-styles.md](visual-styles.md) | Choosing a visual style (20 styles with full specs) |
| [prompt-examples.md](prompt-examples.md) | Writing a prompt from scratch (full production example + templates) |
## Workflow: Brief to Prompt
1. **Pull data** — Research the topic: web search, APIs, internal docs. Gather real quotes, stats, handles
2. **Synthesize a thesis** — Not a list. A story. *"X is happening because Y — here's the proof."* Group into 3-5 themes with a narrative arc
3. **Choose a style** — Match mood first, content second. Ask: *"What should the viewer FEEL?"* See [visual-styles.md](visual-styles.md)
4. **Write the avatar** — Thematic wardrobe matching content's emotional context. Brand logos and content-specific props in the set (see Avatar Guide below)
5. **Extract critical text** — List every number, quote, handle, and label that must appear literally
6. **Break into scenes** — One concept per scene. Rotate scene types. Never 3+ of same type in a row. At least 2 pure B-roll scenes
7. **Write voiceover** — Spell out numbers in VO ("one-point-eight-five million"), use figures on screen ("1.85M"). Narration on EVERY scene including B-roll
8. **Layer each B-roll scene** — L1 background, L2 hero, L3 supporting, L4 info bar, L5 effects. Every element must MOVE
9. **Add music direction** — Reference artists, describe energy arc
10. **Add narration style** — How to deliver: fast/slow, where to pause, emotional register per section
## Prompt Anatomy
Every production-quality prompt follows this structure:
```
FORMAT: What kind of video, how long, what energy
TONE: Emotional register, references
AVATAR: Detailed physical + environment description (60-100 words)
STYLE: Named aesthetic with colors, typography, motion rules, transitions
CRITICAL ON-SCREEN TEXT: Exact strings that must appear
SCENE-BY-SCENE: Individual scene breakdowns with VO and layered visuals
MUSIC: Genre, reference artists, energy arc
NARRATION STYLE: How to deliver the voiceover
```
### FORMAT
```
FORMAT: 75-second high-energy tech daily briefing. Think: a creator who just got amazing news.
FORMAT: Bloomberg-style strategy briefing. 100-120 seconds. CEO-delivered.
```
### TONE
```
TONE: Confident, direct, data-backed. Highlights hit hard. Lowlights are honest — no spin.
TONE: Edgy, punk tech commentary. Vice News meets The Face magazine — raw, confrontational.
```
### CRITICAL ON-SCREEN TEXT
List every exact string that must appear on screen. Without this, the agent may summarize, round numbers, or rephrase quotes.
```
CRITICAL ON-SCREEN TEXT (display literally):
- "$141M ARR — All-Time High"
- "1.85M Signups — +28% MoM"
- Quote: "Use technology to serve the message, not distract from it." — Shalev Hani
- "@username" — exact social handle
```
### MUSIC & NARRATION
```
MUSIC: Driving electronic, heavy bass drops on key numbers. Run the Jewels meets
a tech keynote. Builds relentlessly, only softens for customer stories.
NARRATION STYLE: High energy throughout. Let numbers PUNCH — pause before big ones,
then deliver hard. Customer stories get warmth. The close should feel like a mic drop.
```
## Avatar Description Guide
**The avatar is NOT a fixed headshot** — design it for each video like a movie character. Think costume designer + set designer.
### Thematic Wardrobe Rule
The avatar's outfit and environment MUST match the content's emotional/cultural context:
| Content Type | Avatar Design | NOT This |
|---|---|---|
| Chinese New Year | Red qipao with gold embroidery, lantern-lit courtyard | "Reporter in a blazer" |
| Breaking tech news | Field reporter, windswept hair, earpiece, city skyline | "Anchor at a desk" |
| Sleep science | Oversized cream knit, cross-legged on bed, warm lamp | "Analyst in a lab" |
| Reddit community | Messy desk, Reddit alien on monitors, upvote arrows on wall | "Researcher in a studio" |
### What to Specify
| Element | Weak | Strong |
|---------|------|--------|
| Clothing | "Business casual" | "Black ribbed merino turtleneck, high collar framing jaw" |
| Environment | "An office" | "Glass-walled conference room. Whiteboard with hand-drawn tier pyramid" |
| Monitor content | "Computer screens" | "Monitor shows scrolling green terminal text and red security alerts" |
| Lighting | "Well lit" | "Cool blue monitor glow from left, warm amber desk lamp from right" |
### Template
```
AVATAR: [Clothing — fabric, color, fit, accessories, posture].
[Setting — specific props, brand logos, what's on the walls].
[Monitors/desk — content visible on screens, items on desk].
[Lighting — direction, color temperature]. [Mood of the space].
60-100 words. 3+ content-specific props. Brand elements visible.
```
## Scene Types
| Type | Format | When to Use |
|------|--------|-------------|
| **A-ROLL** | Avatar speaking to camera | Intros, key insights, CTAs, emotional beats |
| **FULL SCREEN B-ROLL** | No avatar — motion graphics only | Data visualization, information-dense content |
| **A-ROLL + OVERLAY** | Split frame: avatar + content | Presenting data while maintaining human connection |
**Rotation is mandatory.** Never 3+ of the same type in a row. Every prompt needs at least 2 pure B-roll scenes.
**Voiceover on EVERY scene.** Every B-roll scene MUST include a `VOICEOVER:` line. Silent B-roll = broken video.
### Scene Anatomy
**A-ROLL:**
```
SCENE 1 — A-ROLL (10s)
[Avatar center-frame, excited, hands gesturing]
VOICEOVER: "The exact script for this scene."
Lower-third: "TITLE TEXT" white on blue bar.
```
**B-ROLL with layers:**
```
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "The exact script for this scene."
LAYER 1: Dark #1a1a1a background with subtle grid lines pulsing.
LAYER 2: "HEADLINE" SLAMS in from left in white Bold 100pt at -5 degrees.
LAYER 3: Three data cards CASCADE from right, staggered 0.3s.
LAYER 4: Bottom ticker SLIDES in: "supporting text scrolling continuously."
LAYER 5: Grid lines RIPPLE outward from impact point.
Hard cut.
```
**A-ROLL + OVERLAY:**
```
SCENE 3 — A-ROLL + OVERLAY (10s)
[SPLIT — Avatar LEFT 35%. Content RIGHT 65%. NO overlap.]
Avatar gestures toward content side.
VOICEOVER: "The exact script for this scene."
RIGHT SIDE: "HEADLINE" in cyan 60pt. Three stats COUNT UP below.
```
Alternate which side the avatar appears on between overlay scenes.
## The Visual Layer System
Break B-roll into 5 stacked layers. This is the most powerful technique for motion graphics scenes.
| Layer | Purpose | Examples |
|-------|---------|---------|
| **L1** | Background | Textured surface, grid, gradient, color field |
| **L2** | Hero content | Main headline/number that dominates the frame |
| **L3** | Supporting data | Cards, stats, bullet points, secondary information |
| **L4** | Information bar | Tickers, labels, source attributions, quotes |
| **L5** | Effects | Particles, glitches, grid animations, ambient motion |
Every B-roll: 4+ layers. Every overlay content side: 3+ layers. **Every element must MOVE.**
## Motion Vocabulary
### High Energy
| Verb | Example |
|------|---------|
| **SLAMS** | `"$95M" SLAMS in from left at -5 degrees` |
| **CRASHES** | `Title CRASHES in from right, screen-shake on impact` |
| **PUNCHES** | `Quote card PUNCHES up from bottom` |
| **STAMPS** | `Data blocks STAMP in staggered 0.4s` |
| **SHATTERS** | `Text SHATTERS after 1.5s, revealing number underneath` |
### Medium Energy
| Verb | Example |
|------|---------|
| **CASCADE** | `Three cards CASCADE from top, staggered 0.3s` |
| **SLIDES** | `Ticker SLIDES in from right — continuous scroll` |
| **DROPS** | `"TIER 1" DROPS in with white flash` |
| **FILLS** | `Progress bar FILLS 0 to 90% in orange` |
| **DRAWS** | `Chart line DRAWS itself left to right` |
### Low Energy
| Verb | Example |
|------|---------|
| **types on** | `Quote types on word by word in italic white` |
| **fades in** | `Logo fades in at center, held for 3 seconds` |
| **FLOATS** | `Bokeh orbs FLOAT across frame at different speeds` |
| **morphs** | `Number morphs from 17 to 18.9` |
| **COUNTS UP** | `"1.85M" COUNTS UP from 0 in amber 96pt` |
## Transition Types
| Transition | Energy | Styles It Fits |
|------------|--------|---------------|
| Smash cut | Aggressive | Deconstructed, Maximalist, Carnival Surge |
| White flash frame | Punchy | Deconstructed, Maximalist |
| Grid wipe | Systematic | Swiss Pulse, Digital Grid |
| Hard cut | Clean | Swiss Pulse, Shadow Cut |
| Liquid dissolve | Elegant | Data Drift, Dream State |
| Slow cross-dissolve | Refined | Velvet Standard |
| Pop cut / bounce | Fun | Play Mode, Carnival Surge |
| Snap cut | Urgent | Red Wire, Contact Sheet |
| Soft dissolve | Warm | Soft Signal, Warm Grain, Quiet Drama |
| Iris wipe | Nostalgic | Heritage Reel |
## Timing Guidelines
| Content Type | Duration |
|--------------|----------|
| Hook/Intro (A-roll) | 6-10 seconds |
| Data-heavy B-roll | 10-15 seconds (NEVER ≤5s — causes black frames) |
| A-roll + Overlay | 8-12 seconds |
| CTA / Close (A-roll) | 6-8 seconds |
**Common video lengths:** Social clip: 30-45s (5-7 scenes) | Briefing: 60-75s (7-9 scenes) | Deep dive: 90-120s (10-13 scenes)
**Speaking pace:** ~150 words/minute. Calculate: `words / 150 * 60 = seconds`
## What Doesn't Work
Patterns that consistently produce poor results:
**Layout language** — Screen coordinates cause empty/black B-roll:
```
❌ "UPPER-LEFT: headline in 48pt Helvetica"
❌ "CENTER-SCREEN: display at coordinates (400, 300)"
✅ "135K" SLAMS in from left, white Impact 120pt, fills 40% of frame.
```
**Named artists without specs** — "Ikko Tanaka style" means nothing to Video Agent. Translate to concrete rules:
```
❌ "Use an Ikko Tanaka style"
✅ "Flat color blocks, maximum 3 colors per frame, 60% negative space, typography as primary element"
```
**Style examples injected into prompts** — Full example scenes from a style library confuse the agent. Use the style's **rules**, not example scenes.
**Forced short B-roll (≤5 seconds)** — Too short for rendering. Every tested video with 5s B-roll had empty/black screens. Use 10-15s.
**Content as a list, not a story** — "Here are 5 tweets" produces flat videos. Always synthesize: *"X is happening because Y — here's the proof."*
## Production Insights
### Style Performance (from 40+ videos)
| Rank | Style | Strength |
|------|-------|----------|
| 1 | Deconstructed (Brody) | Most reliable across all topics |
| 2 | Swiss Pulse (Müller-Brockmann) | Best for data-heavy content |
| 3 | Digital Grid (Crouwel) | Strong for tech topics |
| 4 | Geometric Bold (Tanaka) | Elegant and versatile |
| 5 | Maximalist Type (Scher) | High energy, use sparingly |
### Duration by Approach
| Approach | Avg Duration | Quality |
|----------|-------------|---------|
| Natural storyboard + custom avatar | ~106s | Best |
| Natural storyboard, no custom avatar | ~69s | Good |
| Forced short scenes + custom avatar | ~71s | Mixed |
| Layout language prompts | ~48s | Poor |
## Quality Checklist
- [ ] Thesis-driven — story, not bullet points
- [ ] Style named with colors, typography, motion, transitions (see [visual-styles.md](visual-styles.md))
- [ ] Avatar has thematic wardrobe + branded environment (60-100 words)
- [ ] Critical text listed — every stat, quote, label
- [ ] Scenes rotate types — never 3+ same type. At least 2 B-roll scenes
- [ ] Every scene has VOICEOVER — including B-roll
- [ ] B-roll scenes have 4+ layers, every element has motion verbs
- [ ] B-roll scenes are 10-15 seconds (never ≤5s)
- [ ] Brand logos appear when discussing companies
- [ ] Every element moves — no static frames
+195
View File
@@ -0,0 +1,195 @@
---
name: quota
description: Credit system, usage limits, and checking remaining quota for HeyGen
---
# HeyGen Quota and Credits
HeyGen uses a credit-based system for video generation. Understanding quota management helps prevent failed video generation requests.
## Checking Remaining Quota
### curl
```bash
curl -X GET "https://api.heygen.com/v2/user/remaining_quota" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface QuotaResponse {
error: null | string;
data: {
remaining_quota: number;
used_quota: number;
};
}
const response = await fetch("https://api.heygen.com/v2/user/remaining_quota", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data }: QuotaResponse = await response.json();
console.log(`Remaining credits: ${data.remaining_quota}`);
```
### Python
```python
import requests
import os
response = requests.get(
"https://api.heygen.com/v2/user/remaining_quota",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()["data"]
print(f"Remaining credits: {data['remaining_quota']}")
```
## Response Format
```json
{
"error": null,
"data": {
"remaining_quota": 450,
"used_quota": 50
}
}
```
## Credit Consumption
Different operations consume different amounts of credits:
| Operation | Credit Cost | Notes |
|-----------|-------------|-------|
| Standard video (1 min) | ~1 credit per minute | Varies by resolution |
| 720p video | Base rate | Standard quality |
| 1080p video | ~1.5x base rate | Higher quality |
| Video translation | Varies | Depends on video length |
| Streaming avatar | Per session | Real-time usage |
## Pre-Generation Quota Check
Always verify sufficient quota before generating videos:
```typescript
async function generateVideoWithQuotaCheck(videoConfig: VideoConfig) {
// Check quota first
const quotaResponse = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: quota } = await quotaResponse.json();
// Estimate required credits (rough estimate: 1 credit per minute)
const estimatedMinutes = videoConfig.estimatedDuration / 60;
const requiredCredits = Math.ceil(estimatedMinutes);
if (quota.remaining_quota < requiredCredits) {
throw new Error(
`Insufficient credits. Need ${requiredCredits}, have ${quota.remaining_quota}`
);
}
// Proceed with video generation
return generateVideo(videoConfig);
}
```
## Quota Management Best Practices
### 1. Monitor Usage Regularly
```typescript
async function logQuotaUsage() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
console.log({
remaining: data.remaining_quota,
used: data.used_quota,
percentUsed: (
(data.used_quota / (data.remaining_quota + data.used_quota)) *
100
).toFixed(1),
});
}
```
### 2. Set Up Alerts
```typescript
const QUOTA_WARNING_THRESHOLD = 50;
async function checkQuotaWithAlert() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.remaining_quota < QUOTA_WARNING_THRESHOLD) {
// Send alert (email, Slack, etc.)
await sendAlert(`Low HeyGen quota: ${data.remaining_quota} credits remaining`);
}
return data;
}
```
### 3. Use Test Mode for Development
When available, use test mode to avoid consuming credits during development:
```typescript
const videoConfig = {
test: true, // Use test mode during development
video_inputs: [...],
};
// Test videos may have watermarks but don't consume credits
```
## Subscription Tiers
Different subscription tiers have different quota allocations and features:
| Tier | Features |
|------|----------|
| Free | Limited credits, basic features |
| Creator | More credits, standard avatars |
| Team | Higher limits, team collaboration |
| Enterprise | Custom limits, API access, priority support |
API access typically requires Enterprise tier or higher.
## Error Handling for Quota Issues
```typescript
async function handleQuotaError(error: any) {
if (error.message.includes("quota") || error.message.includes("credit")) {
console.error("Quota exceeded. Consider:");
console.error("1. Upgrading your subscription");
console.error("2. Waiting for quota reset");
console.error("3. Purchasing additional credits");
// Check current quota
const quota = await getQuota();
console.error(`Current remaining: ${quota.remaining_quota}`);
}
throw error;
}
```
@@ -0,0 +1,705 @@
---
name: remotion-integration
description: Using HeyGen avatar videos in Remotion compositions
---
# HeyGen + Remotion Integration
This guide covers workflows for generating HeyGen avatar videos and using them in Remotion compositions.
## Quick Start
```typescript
// 1. Get avatar with default voice
const avatar = await getAvatarDetails(avatarId);
// 2. Generate video (MP4 with background - most common)
const videoId = await generateVideo({
video_inputs: [{
character: { type: "avatar", avatar_id: avatar.id, avatar_style: "normal" },
voice: { type: "text", input_text: script, voice_id: avatar.default_voice_id },
background: { type: "color", value: "#1a1a2e" },
}],
dimension: { width: 1920, height: 1080 },
});
// 3. Poll for completion (10-15+ min)
// 4. Use in Remotion with motion graphics overlaid on top
```
## Overview
A typical workflow:
1. Generate avatar video with HeyGen
2. Wait for completion and get video URL
3. Download or use URL directly in Remotion
4. Compose with other elements (backgrounds, overlays, animations)
## Choosing the Right Output Format
| Your Composition | Recommended | Why |
|------------------|-------------|-----|
| Avatar as presenter with overlays | MP4 + background | Simpler, overlays go on top |
| Loom-style (avatar over screen recording) | WebM + `closeUp`, mask in Remotion | Need transparency, apply circle mask in CSS |
| Avatar overlaid ON other video/content | WebM (transparent) | Need to see through to content behind |
| Full-screen avatar | MP4 + background | Standard approach |
**Use MP4 with background for most cases.** Use WebM when you need to see content *behind* the avatar.
**Note:** WebM only supports `normal` and `closeUp` styles. For circular framing, use CSS `border-radius: 50%` in Remotion.
## Recommended: Parallel Development Workflow
HeyGen video generation takes **10-15+ minutes**. Don't wait - work in parallel:
1. **Start HeyGen generation** - save `video_id` to a file, exit immediately
2. **Build Remotion composition** - use a placeholder or the avatar's `preview_video_url` (a short loop)
3. **Check HeyGen status** periodically or when done building
4. **Swap placeholder** for real video URL once ready
**Estimate duration from script**: ~150 words/minute speech rate, so `wordCount / 150 * 60 * fps` gives approximate frames.
**Composition tip**: Design components to work with or without the avatar video, so motion graphics can be tested independently.
## Dimension Alignment
**Critical**: Match HeyGen output dimensions to your Remotion composition.
### Common Dimension Presets
```typescript
// Shared dimension constants for both HeyGen and Remotion
const DIMENSIONS = {
landscape_1080p: { width: 1920, height: 1080 },
landscape_720p: { width: 1280, height: 720 },
portrait_1080p: { width: 1080, height: 1920 },
portrait_720p: { width: 720, height: 1280 },
square_1080p: { width: 1080, height: 1080 },
square_720p: { width: 720, height: 720 },
} as const;
type DimensionPreset = keyof typeof DIMENSIONS;
```
### HeyGen Video Generation
```typescript
// Generate HeyGen video with specific dimensions
async function generateHeyGenVideo(
script: string,
avatarId: string,
voiceId: string,
preset: DimensionPreset
): Promise<string> {
const dimension = DIMENSIONS[preset];
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: "#00FF00", // Green screen for compositing
},
},
],
dimension,
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### Remotion Composition Setup
```tsx
// remotion/src/Root.tsx
import { Composition } from "remotion";
import { AvatarComposition } from "./AvatarComposition";
const DIMENSIONS = {
landscape_1080p: { width: 1920, height: 1080 },
// ... same as above
};
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="AvatarVideo"
component={AvatarComposition}
durationInFrames={300} // Will be set dynamically
fps={30}
width={DIMENSIONS.landscape_1080p.width}
height={DIMENSIONS.landscape_1080p.height}
defaultProps={{
avatarVideoUrl: "",
}}
/>
</>
);
};
```
## Generating Avatar Video for Remotion
### Standard: MP4 with Background
Most Remotion compositions work best with MP4 + background. Overlays and motion graphics go on top:
```typescript
async function generateAvatarForRemotion(
script: string,
avatarId: string,
voiceId: string,
options: {
style?: "normal" | "closeUp" | "circle";
backgroundColor?: string;
} = {}
): Promise<string> {
const { style = "normal", backgroundColor = "#1a1a2e" } = options;
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: style,
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: backgroundColor,
},
}],
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### Transparent Background (WebM)
Only use when you need to see content *behind* the avatar (e.g., avatar overlaid on screen recording):
```typescript
// Use /v1/video.webm endpoint for transparent background
// Note: Different structure than /v2/video/generate
const response = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required: avatar pose ID
avatar_style: "normal", // Required: "normal" or "closeUp" only
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
});
```
## Using HeyGen Video in Remotion
### Important: Use OffthreadVideo for Frame-Accurate Rendering
**Always use `OffthreadVideo` instead of `Video`** for HeyGen avatar videos. The basic `Video` component uses the browser's video decoder which isn't frame-accurate, causing jitter during rendering. `OffthreadVideo` extracts frames via FFmpeg for smooth, accurate playback.
`OffthreadVideo` is included in the core `remotion` package - no additional install needed.
### Basic Usage
```tsx
// remotion/src/AvatarComposition.tsx
import { OffthreadVideo, useVideoConfig } from "remotion";
interface AvatarCompositionProps {
avatarVideoUrl: string;
}
export const AvatarComposition: React.FC<AvatarCompositionProps> = ({
avatarVideoUrl,
}) => {
return (
<div style={{ flex: 1, backgroundColor: "#1a1a2e" }}>
<OffthreadVideo
src={avatarVideoUrl}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
);
};
```
### WebM with Transparent Background (Recommended)
Using WebM from `/v1/video.webm` - no chroma keying needed:
```tsx
import { OffthreadVideo, AbsoluteFill, Sequence } from "remotion";
export const AvatarWithMotionGraphics: React.FC<{
avatarWebmUrl: string
}> = ({ avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Layer 1: Your background/content */}
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
<YourMotionGraphics />
</AbsoluteFill>
{/* Layer 2: Avatar with transparent background - use OffthreadVideo for frame-accurate rendering */}
<OffthreadVideo
src={avatarWebmUrl}
transparent
style={{
position: "absolute",
bottom: 0,
right: 0,
width: "50%",
height: "auto",
}}
/>
{/* Layer 3: Overlays on top of avatar */}
<Sequence from={30}>
<AnimatedTitle text="Welcome!" />
</Sequence>
</AbsoluteFill>
);
};
```
### Loom-Style: Circle Avatar Over Screen Recording
Use `closeUp` style + WebM, then apply circular mask in Remotion:
```tsx
import { OffthreadVideo, AbsoluteFill } from "remotion";
export const LoomStyleComposition: React.FC<{
screenRecordingUrl: string;
avatarWebmUrl: string; // Generated with avatar_style: "closeUp" via /v1/video.webm
}> = ({ screenRecordingUrl, avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Screen recording fills the frame */}
<OffthreadVideo src={screenRecordingUrl} style={{ width: "100%", height: "100%" }} />
{/* Avatar with circular mask - transparent bg shows screen behind */}
<OffthreadVideo
src={avatarWebmUrl}
transparent
style={{
position: "absolute",
bottom: 40,
left: 40,
width: 180,
height: 180,
borderRadius: "50%", // Circular mask applied in CSS
overflow: "hidden",
objectFit: "cover",
}}
/>
</AbsoluteFill>
);
};
```
**Note:** WebM doesn't support `circle` style - use `normal` or `closeUp` and apply circular masking via CSS.
### Legacy: Green Screen with Chroma Key
If using MP4 with green background (not recommended - use WebM instead):
```tsx
// Note: True chroma key requires WebGL or post-processing
// WebM transparent background is much simpler
<OffthreadVideo
src={avatarVideoUrl}
style={{
mixBlendMode: "multiply", // Basic compositing only
}}
/>
```
### Layered Composition
```tsx
import { OffthreadVideo, Sequence, useVideoConfig, Img } from "remotion";
interface LayeredAvatarProps {
avatarVideoUrl: string;
backgroundUrl: string;
logoUrl: string;
title: string;
}
export const LayeredAvatarComposition: React.FC<LayeredAvatarProps> = ({
avatarVideoUrl,
backgroundUrl,
logoUrl,
title,
}) => {
const { fps } = useVideoConfig();
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
{/* Layer 1: Background */}
<Img
src={backgroundUrl}
style={{
position: "absolute",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
{/* Layer 2: Avatar video - use OffthreadVideo to prevent jitter */}
<OffthreadVideo
src={avatarVideoUrl}
style={{
position: "absolute",
bottom: 0,
right: 0,
width: "40%",
height: "auto",
}}
/>
{/* Layer 3: Title (appears after 1 second) */}
<Sequence from={fps}>
<div
style={{
position: "absolute",
top: 50,
left: 50,
color: "white",
fontSize: 48,
fontWeight: "bold",
}}
>
{title}
</div>
</Sequence>
{/* Layer 4: Logo */}
<Img
src={logoUrl}
style={{
position: "absolute",
top: 20,
right: 20,
width: 100,
height: "auto",
}}
/>
</div>
);
};
```
## Complete Workflow
### Generate and Compose
```typescript
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
async function generateAvatarVideoForRemotion(
script: string,
outputPath: string
) {
// 1. Generate HeyGen video
console.log("Generating HeyGen avatar video...");
const videoId = await generateHeyGenVideo(
script,
"josh_lite3_20230714",
"1bd001e7e50f421d891986aad5158bc8",
"landscape_1080p"
);
// 2. Wait for completion
console.log("Waiting for HeyGen video...");
const avatarVideoUrl = await waitForVideo(videoId);
console.log(`HeyGen video ready: ${avatarVideoUrl}`);
// 3. Get video duration for Remotion
const avatarDuration = await getVideoDuration(avatarVideoUrl);
const durationInFrames = Math.ceil(avatarDuration * 30); // 30 fps
// 4. Bundle Remotion project
console.log("Bundling Remotion project...");
const bundleLocation = await bundle({
entryPoint: "./remotion/src/index.ts",
});
// 5. Select composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id: "AvatarVideo",
inputProps: {
avatarVideoUrl,
},
});
// 6. Render final video
console.log("Rendering final composition...");
await renderMedia({
composition: {
...composition,
durationInFrames,
},
serveUrl: bundleLocation,
codec: "h264",
outputLocation: outputPath,
inputProps: {
avatarVideoUrl,
},
});
console.log(`Final video rendered: ${outputPath}`);
return outputPath;
}
```
### Dynamic Duration with calculateMetadata
```tsx
// remotion/src/AvatarComposition.tsx
import { CalculateMetadataFunction } from "remotion";
export const calculateAvatarMetadata: CalculateMetadataFunction<
AvatarCompositionProps
> = async ({ props }) => {
// Fetch video duration from HeyGen video
const duration = await getVideoDurationInSeconds(props.avatarVideoUrl);
return {
durationInFrames: Math.ceil(duration * 30),
fps: 30,
width: 1920,
height: 1080,
};
};
// In Root.tsx
<Composition
id="AvatarVideo"
component={AvatarComposition}
calculateMetadata={calculateAvatarMetadata}
defaultProps={{
avatarVideoUrl: "",
}}
/>
```
## Best Practices
### 1. Use Green Screen for Flexibility
Generate HeyGen videos with green screen background when you want to composite:
```typescript
background: {
type: "color",
value: "#00FF00", // Pure green for chroma key
}
```
### 2. Match Frame Rates
HeyGen default is 25 fps. Consider this when setting Remotion fps:
```typescript
// Option 1: Match HeyGen's 25 fps
fps: 25
// Option 2: Use 30 fps with playback rate adjustment
<OffthreadVideo
src={avatarVideoUrl}
playbackRate={25/30} // Slow down slightly to match
/>
```
### 3. URL vs Download: When to Use Each
**Use URL directly** when:
- Previewing in Remotion Studio (`npm run dev`)
- URL won't expire before render completes
- You want faster iteration during development
```tsx
// Direct URL usage - simpler, faster for dev
<OffthreadVideo src={avatarVideoUrl} />
```
**Download first** when:
- URL has expiration (HeyGen URLs expire after ~24 hours)
- Rendering will happen later or repeatedly
- Network reliability is a concern
- You need offline rendering
```typescript
// Download with retry for reliability
async function downloadVideoWithRetry(
url: string,
outputPath: string,
maxRetries = 5
): Promise<string> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const buffer = await response.arrayBuffer();
await fs.promises.writeFile(outputPath, Buffer.from(buffer));
return outputPath;
} catch (error) {
const delay = 2000 * Math.pow(2, attempt);
console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Download failed after retries");
}
// Use local file in Remotion
const localPath = await downloadVideoWithRetry(avatarVideoUrl, "./public/avatar.mp4");
```
**Hybrid approach** (recommended for production):
```typescript
// Save both URL and local path in metadata
const metadata = {
videoUrl: result.video_url, // For quick preview
localPath: "./public/avatar.mp4", // For reliable rendering
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // URL expiration
};
// In Remotion component, prefer local if available
const videoSrc = fs.existsSync(localPath) ? staticFile("avatar.mp4") : avatarVideoUrl;
```
### 4. Handle Avatar Positioning
Common avatar positions in compositions:
```typescript
const AVATAR_POSITIONS = {
fullscreen: { width: "100%", height: "100%", position: "center" },
bottomRight: { width: "40%", bottom: 0, right: 0 },
bottomLeft: { width: "40%", bottom: 0, left: 0 },
pictureInPicture: { width: "25%", bottom: 20, right: 20 },
leftThird: { width: "33%", left: 0, height: "100%" },
};
```
## Output Formats
### HeyGen Output
- Format: MP4 (H.264)
- Audio: AAC
- Resolution: As specified in request
### Remotion Output
- Codec: H.264 (default), VP8, VP9, ProRes
- Match or exceed HeyGen quality settings
```typescript
await renderMedia({
codec: "h264",
crf: 18, // High quality
// ...
});
```
## Troubleshooting
### Video Not Playing in Remotion
1. Check URL accessibility (CORS issues)
2. Verify video format compatibility
3. Try downloading locally first
### Dimension Mismatch
Ensure both HeyGen and Remotion use identical dimensions:
```typescript
// Shared config
const VIDEO_CONFIG = {
width: 1920,
height: 1080,
fps: 30,
};
// HeyGen
dimension: { width: VIDEO_CONFIG.width, height: VIDEO_CONFIG.height }
// Remotion
<Composition width={VIDEO_CONFIG.width} height={VIDEO_CONFIG.height} />
```
### Video Jitter During Rendering
If avatar video appears jittery or stuttery in rendered output:
1. **Use `OffthreadVideo` instead of `Video`** - The basic `Video` component uses the browser's video decoder which isn't frame-accurate
2. Update imports (no additional install needed - it's in core `remotion`):
```tsx
// Before (causes jitter)
import { Video } from "remotion";
// After (frame-accurate)
import { OffthreadVideo } from "remotion";
```
3. For WebM with transparency, add the `transparent` prop:
```tsx
<OffthreadVideo src={avatarWebmUrl} transparent />
```
### Audio Sync Issues
If avatar audio drifts:
- Verify source video frame rate
- Check for encoding issues
- Consider re-encoding with consistent settings
+322
View File
@@ -0,0 +1,322 @@
---
name: scripts
description: Writing effective scripts for HeyGen AI avatar videos
---
# Writing Scripts for HeyGen Videos
Scripts for AI avatar videos have different requirements than scripts for human presenters. This guide covers best practices for writing scripts that sound natural and render well.
## Script Basics
### Speech Rate and Duration
Typical speech is approximately **150 words per minute** at normal speed (1.0x). Use this as a rough estimate for planning script length.
| Script Length | Approximate Duration |
|---------------|---------------------|
| 75 words | 30 seconds |
| 150 words | 1 minute |
| 300 words | 2 minutes |
| 450 words | 3 minutes |
| 750 words | 5 minutes |
```typescript
// Estimate video duration from script
function estimateDuration(script: string, speed: number = 1.0): number {
const words = script.split(/\s+/).filter(w => w.length > 0).length;
const wordsPerMinute = 150 * speed;
return words / wordsPerMinute * 60; // seconds
}
// Estimate frames for Remotion
function estimateFrames(script: string, fps: number = 30, speed: number = 1.0): number {
const durationSeconds = estimateDuration(script, speed);
return Math.ceil(durationSeconds * fps);
}
```
### Sentence Structure
**Keep sentences short.** AI voices handle shorter sentences more naturally.
| Guideline | Example |
|-----------|---------|
| **Good**: 10-20 words per sentence | "Our platform helps teams collaborate. It syncs in real-time across all devices." |
| **Avoid**: 30+ word run-on sentences | "Our platform helps teams collaborate more effectively by providing real-time synchronization across all devices while also offering offline support and automatic conflict resolution." |
### Punctuation Affects Delivery
| Punctuation | Effect |
|-------------|--------|
| Period `.` | Full stop, natural pause |
| Comma `,` | Brief pause |
| Question mark `?` | Rising intonation |
| Exclamation `!` | Emphasis (use sparingly) |
| Ellipsis `...` | Trailing off, slight pause |
## Adding Pauses with Break Tags
Use SSML-style `<break>` tags for precise pause control:
```
<break time="Xs"/>
```
Where `X` is seconds (e.g., `0.5s`, `1s`, `1.5s`, `2s`).
### Formatting Rules
| Rule | Correct | Incorrect |
|------|---------|-----------|
| Space before tag | `word <break time="1s"/>` | `word<break time="1s"/>` |
| Space after tag | `<break time="1s"/> word` | `<break time="1s"/>word` |
| Use seconds with "s" | `<break time="1.5s"/>` | `<break time="1500ms"/>` |
| Self-closing tag | `<break time="1s"/>` | `<break time="1s"></break>` |
### When to Use Pauses
| Situation | Recommended Pause | Example |
|-----------|-------------------|---------|
| After greeting | 0.5-1s | `Hello! <break time="0.5s"/> Welcome to...` |
| Between sections | 1-1.5s | `...that's feature one. <break time="1.5s"/> Now let's look at...` |
| Before key point | 0.5s | `The most important thing is <break time="0.5s"/> consistency.` |
| For dramatic effect | 1.5-2s | `And the winner is... <break time="2s"/> you!` |
| After question | 1s | `Sound good? <break time="1s"/> Let's get started.` |
| List items | 0.5s | `First, speed. <break time="0.5s"/> Second, reliability.` |
### Pause Duration Guide
| Duration | Feel | Use For |
|----------|------|---------|
| 0.3-0.5s | Brief breath | Between clauses, light emphasis |
| 0.5-1s | Natural pause | Sentence breaks, transitions |
| 1-1.5s | Deliberate pause | Section changes, setup for key points |
| 1.5-2s | Dramatic | Reveals, important announcements |
| 2s+ | Long pause | Use sparingly, can feel unnatural |
### Examples
```typescript
// Section transitions
const script = `
Welcome to our product overview. <break time="1s"/>
Today I'll cover three key features. <break time="0.5s"/>
First, let's look at the dashboard. <break time="1.5s"/>
As you can see, it's designed for simplicity. <break time="0.5s"/>
Every action is just one click away.
`;
// Building suspense
const announcement = `
We've been working on something special. <break time="1s"/>
After months of development... <break time="1.5s"/>
I'm excited to announce <break time="0.5s"/> our new AI assistant.
`;
// List with rhythm
const features = `
Our platform offers three core benefits. <break time="0.5s"/>
Speed. <break time="0.5s"/>
Reliability. <break time="0.5s"/>
And simplicity. <break time="1s"/>
Let me show you each one.
`;
```
### Consecutive Breaks
Multiple consecutive breaks are combined:
```typescript
// These two breaks:
"Hello <break time=\"1s\"/> <break time=\"0.5s\"/> world"
// Are treated as a single 1.5s pause
```
## Script Structure Templates
### Product Demo (60 seconds, ~150 words)
```typescript
const productDemo = `
Hi, I'm [Name], and I'm excited to show you [Product]. <break time="1s"/>
[Product] helps you [main benefit] in just [timeframe]. <break time="0.5s"/>
Here's how it works. <break time="1s"/>
First, [step 1]. <break time="0.5s"/>
Then, [step 2]. <break time="0.5s"/>
And finally, [step 3]. <break time="1s"/>
What used to take [old time] now takes [new time]. <break time="0.5s"/>
Ready to get started? <break time="0.5s"/>
Visit [website] today.
`;
```
### Tutorial Introduction (90 seconds, ~225 words)
```typescript
const tutorial = `
Welcome to this tutorial on [topic]. <break time="0.5s"/>
I'm [Name], and I'll guide you through everything you need to know. <break time="1s"/>
By the end of this video, you'll be able to [outcome 1], [outcome 2], and [outcome 3]. <break time="1s"/>
Let's start with the basics. <break time="1.5s"/>
[Section 1 content - 2-3 sentences] <break time="1s"/>
Now that you understand [concept], let's move on to [next topic]. <break time="1.5s"/>
[Section 2 content - 2-3 sentences] <break time="1s"/>
And finally, let's cover [last topic]. <break time="1.5s"/>
[Section 3 content - 2-3 sentences] <break time="1s"/>
That's everything you need to get started. <break time="0.5s"/>
If you have questions, leave a comment below. <break time="0.5s"/>
Thanks for watching!
`;
```
### Announcement (30 seconds, ~75 words)
```typescript
const announcement = `
Big news! <break time="0.5s"/>
We're thrilled to announce [announcement]. <break time="1s"/>
This means [benefit 1] and [benefit 2] for all our users. <break time="0.5s"/>
Starting [date], you'll be able to [new capability]. <break time="1s"/>
Head to [location] to learn more. <break time="0.5s"/>
We can't wait to hear what you think!
`;
```
## Writing Tips for AI Voices
### Do
- **Write conversationally** - Read it aloud to check flow
- **Use contractions** - "We're" not "We are", "It's" not "It is"
- **Break up long sentences** - Split at natural pause points
- **Spell out abbreviations** - "API" may sound like "a pee eye"
- **Add pauses for emphasis** - Guide the listener's attention
- **End sections clearly** - Don't trail off mid-thought
### Avoid
- **Jargon without context** - Explain technical terms
- **Long parentheticals** - Move to separate sentences
- **Ambiguous pronunciations** - "read" (present) vs "read" (past)
- **Excessive exclamation marks** - One per script is usually enough
- **Run-on sentences** - Break into digestible chunks
- **Dense information** - Space out facts with pauses
### Pronunciation Hints
For words that might be mispronounced, spell phonetically or add hints:
```typescript
// Technical terms
const script1 = "Our API (A-P-I) handles authentication...";
// Ambiguous words
const script2 = "I read (red) the documentation yesterday...";
// Brand names
const script3 = "Welcome to HeyGen (hey-jen)...";
```
## Multi-Scene Scripts
When splitting scripts across scenes (for different backgrounds or avatars):
```typescript
const multiSceneVideo = {
video_inputs: [
{
// Scene 1: Introduction
character: { type: "avatar", avatar_id: "josh_lite3_20230714", avatar_style: "normal" },
voice: {
type: "text",
input_text: "Welcome to our quarterly update. <break time=\"1s\"/> I'm Josh, and I'll walk you through the highlights.",
voice_id: "voice_id_here",
},
background: { type: "color", value: "#1a1a2e" },
},
{
// Scene 2: Main content (different background)
character: { type: "avatar", avatar_id: "josh_lite3_20230714", avatar_style: "normal" },
voice: {
type: "text",
input_text: "Let's start with revenue. <break time=\"0.5s\"/> We grew 25 percent quarter over quarter. <break time=\"1s\"/> Here's what drove that growth.",
voice_id: "voice_id_here",
},
background: { type: "image", url: "https://..." },
},
// ... more scenes
],
};
```
### Scene Transition Tips
- End each scene with a complete thought
- Start new scenes with brief context
- Maintain consistent tone across scenes
- Use pauses at scene starts to let visuals register
## Testing Your Script
Before generating the full video:
1. **Read aloud** - Time yourself, check for awkward phrasing
2. **Count words** - Verify expected duration
3. **Check break tags** - Ensure proper spacing and syntax
4. **Preview with short clip** - Generate a 10-second test if unsure about pronunciation
```typescript
// Test a small portion first
const testScript = script.split('.').slice(0, 2).join('.') + '.';
const testVideoId = await generateVideo({
video_inputs: [{
character: { type: "avatar", avatar_id: avatarId, avatar_style: "normal" },
voice: { type: "text", input_text: testScript, voice_id: voiceId },
}],
dimension: { width: 1280, height: 720 }, // Lower res for test
});
```
## Voice Speed Adjustment
Adjust delivery speed in the voice configuration:
```typescript
voice: {
type: "text",
input_text: script,
voice_id: "voice_id",
speed: 1.1, // Slightly faster (range: 0.5 - 2.0)
}
```
| Speed | Effect | Use Case |
|-------|--------|----------|
| 0.8-0.9 | Slower, deliberate | Complex topics, older audiences |
| 1.0 | Normal | General use |
| 1.1-1.2 | Slightly faster | Energetic content, younger audiences |
| 1.3+ | Fast | Use sparingly, may reduce clarity |
See [voices.md](voices.md) for full voice configuration options.
@@ -0,0 +1,433 @@
---
name: templates
description: Template listing and variable replacement for HeyGen videos
---
# Video Templates
HeyGen templates allow you to create reusable video structures with variable placeholders, enabling personalized video generation at scale.
## Listing Templates
### curl
```bash
curl -X GET "https://api.heygen.com/v2/templates" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Template {
template_id: string;
name: string;
thumbnail_url: string;
variables: TemplateVariable[];
}
interface TemplateVariable {
name: string;
type: "text" | "image" | "audio";
properties?: {
max_length?: number;
default_value?: string;
};
}
interface TemplatesResponse {
error: null | string;
data: {
templates: Template[];
};
}
async function listTemplates(): Promise<Template[]> {
const response = await fetch("https://api.heygen.com/v2/templates", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: TemplatesResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.templates;
}
```
### Python
```python
import requests
import os
def list_templates() -> list:
response = requests.get(
"https://api.heygen.com/v2/templates",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["templates"]
```
## Response Format
```json
{
"error": null,
"data": {
"templates": [
{
"template_id": "template_abc123",
"name": "Product Announcement",
"thumbnail_url": "https://files.heygen.ai/...",
"variables": [
{
"name": "product_name",
"type": "text",
"properties": {
"max_length": 50
}
},
{
"name": "presenter_script",
"type": "text",
"properties": {
"max_length": 500
}
},
{
"name": "product_image",
"type": "image"
}
]
}
]
}
}
```
## Getting Template Details
### curl
```bash
curl -X GET "https://api.heygen.com/v2/template/{template_id}" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
async function getTemplate(templateId: string): Promise<Template> {
const response = await fetch(
`https://api.heygen.com/v2/template/${templateId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
## Generating Video from Template
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `variables` | object | ✓ | Key-value pairs matching template variables |
| `test` | boolean | | Test mode (watermarked, no credits) |
| `title` | string | | Video name for organization |
| `callback_id` | string | | Custom ID for webhook tracking |
| `callback_url` | string | | URL for completion notification |
**Note:** The `variables` object keys must match the template's defined variable names. Check template details to see which variables are defined.
### curl
```bash
curl -X POST "https://api.heygen.com/v2/template/{template_id}/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test": false,
"variables": {
"product_name": "SuperWidget Pro",
"presenter_script": "Introducing our latest innovation!",
"product_image": "https://example.com/product.jpg"
}
}'
```
### TypeScript
```typescript
interface TemplateGenerateRequest {
variables: Record<string, string>; // Required
test?: boolean;
title?: string;
callback_id?: string;
callback_url?: string;
}
interface TemplateGenerateResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateFromTemplate(
templateId: string,
variables: Record<string, string>,
test: boolean = false
): Promise<string> {
const response = await fetch(
`https://api.heygen.com/v2/template/${templateId}/generate`,
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ test, variables }),
}
);
const json: TemplateGenerateResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Python
```python
def generate_from_template(template_id: str, variables: dict, test: bool = False) -> str:
response = requests.post(
f"https://api.heygen.com/v2/template/{template_id}/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json={
"test": test,
"variables": variables
}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["video_id"]
```
## Variable Types
### Text Variables
For dynamic text content:
```typescript
const variables = {
customer_name: "John Smith",
product_name: "SuperWidget Pro",
price: "$99.99",
cta_text: "Order Now!",
};
```
### Image Variables
For dynamic images (backgrounds, product shots):
```typescript
const variables = {
product_image: "https://example.com/product.jpg",
logo: "https://example.com/logo.png",
background: "https://example.com/bg.jpg",
};
```
### Audio Variables
For custom audio content:
```typescript
const variables = {
background_music: "https://example.com/music.mp3",
custom_voiceover: "https://example.com/voiceover.mp3",
};
```
## Batch Video Generation
Generate multiple personalized videos from a template:
```typescript
interface PersonalizationData {
name: string;
email: string;
company: string;
customMessage: string;
}
async function batchGenerateVideos(
templateId: string,
recipients: PersonalizationData[]
): Promise<string[]> {
const videoIds: string[] = [];
for (const recipient of recipients) {
const variables = {
recipient_name: recipient.name,
company_name: recipient.company,
personalized_message: recipient.customMessage,
};
const videoId = await generateFromTemplate(templateId, variables);
videoIds.push(videoId);
// Rate limiting: add delay between requests
await new Promise((r) => setTimeout(r, 1000));
}
return videoIds;
}
// Usage
const recipients = [
{
name: "John Smith",
email: "john@example.com",
company: "Acme Inc",
customMessage: "Thanks for your interest in our product!",
},
{
name: "Jane Doe",
email: "jane@example.com",
company: "Tech Corp",
customMessage: "We'd love to show you a demo!",
},
];
const videoIds = await batchGenerateVideos("template_abc123", recipients);
```
## Template Validation
Validate variables before generating:
```typescript
function validateTemplateVariables(
template: Template,
variables: Record<string, string>
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
for (const templateVar of template.variables) {
const value = variables[templateVar.name];
// Check if required variable is provided
if (!value) {
errors.push(`Missing required variable: ${templateVar.name}`);
continue;
}
// Check text length limits
if (templateVar.type === "text" && templateVar.properties?.max_length) {
if (value.length > templateVar.properties.max_length) {
errors.push(
`Variable "${templateVar.name}" exceeds max length of ${templateVar.properties.max_length}`
);
}
}
// Validate image URLs
if (templateVar.type === "image") {
try {
new URL(value);
} catch {
errors.push(`Variable "${templateVar.name}" is not a valid URL`);
}
}
}
return {
valid: errors.length === 0,
errors,
};
}
```
## Complete Template Workflow
```typescript
async function createPersonalizedVideo(
templateId: string,
personalization: Record<string, string>
): Promise<string> {
// 1. Get template details
const template = await getTemplate(templateId);
console.log(`Using template: ${template.name}`);
// 2. Validate variables
const validation = validateTemplateVariables(template, personalization);
if (!validation.valid) {
throw new Error(`Validation errors: ${validation.errors.join(", ")}`);
}
// 3. Generate video
console.log("Generating video...");
const videoId = await generateFromTemplate(templateId, personalization);
console.log(`Video ID: ${videoId}`);
// 4. Wait for completion
const videoUrl = await waitForVideo(videoId);
console.log(`Video ready: ${videoUrl}`);
return videoUrl;
}
// Usage
const videoUrl = await createPersonalizedVideo("template_abc123", {
customer_name: "John Smith",
product_name: "SuperWidget Pro",
offer_details: "Get 20% off your first order!",
});
```
## Best Practices
1. **Design for flexibility** - Create templates with generic placeholders
2. **Set reasonable limits** - Define max lengths for text variables
3. **Validate inputs** - Check variable values before generating
4. **Use test mode** - Test with `test: true` to verify before production
5. **Implement rate limiting** - Add delays for batch generation
6. **Cache template data** - Reduce API calls by caching template details
7. **Error handling** - Gracefully handle generation failures
## Use Cases
- **Sales outreach** - Personalized prospect videos
- **Customer onboarding** - Welcome videos with customer name
- **Product updates** - Announcements with dynamic content
- **Training** - Customized training modules
- **Marketing campaigns** - Targeted promotional videos
@@ -0,0 +1,316 @@
---
name: text-overlays
description: Adding text overlays with fonts and positioning to HeyGen videos
---
# Text Overlays
Add text overlays to your HeyGen videos for titles, captions, lower thirds, and other on-screen text elements.
## Basic Text Overlay
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to our presentation!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
// Text overlay configuration (if supported in your API tier)
// Note: Availability varies by plan
};
```
## Text Overlay Configuration
Text overlays typically support these properties:
```typescript
interface TextOverlay {
text: string;
x: number; // X position (pixels or percentage)
y: number; // Y position (pixels or percentage)
width?: number; // Text box width
height?: number; // Text box height
font_family?: string;
font_size?: number;
font_color?: string;
background_color?: string;
text_align?: "left" | "center" | "right";
duration?: {
start: number; // Start time in seconds
end: number; // End time in seconds
};
}
```
## Positioning Text
### Coordinate System
- **Origin**: Top-left corner (0, 0)
- **X-axis**: Increases to the right
- **Y-axis**: Increases downward
- **Units**: Typically pixels or percentage of video dimensions
### Common Positions
For a 1920x1080 video:
| Position | X | Y | Description |
|----------|---|---|-------------|
| Top-left | 50 | 50 | Upper left corner |
| Top-center | 960 | 50 | Top center |
| Top-right | 1870 | 50 | Upper right corner |
| Center | 960 | 540 | Dead center |
| Bottom-left | 50 | 1030 | Lower third left |
| Bottom-center | 960 | 1030 | Lower third center |
### Position Helper Function
```typescript
interface Position {
x: number;
y: number;
}
function getTextPosition(
location: "top-left" | "top-center" | "top-right" | "center" | "bottom-left" | "bottom-center" | "bottom-right",
videoWidth: number,
videoHeight: number,
padding: number = 50
): Position {
const positions: Record<string, Position> = {
"top-left": { x: padding, y: padding },
"top-center": { x: videoWidth / 2, y: padding },
"top-right": { x: videoWidth - padding, y: padding },
"center": { x: videoWidth / 2, y: videoHeight / 2 },
"bottom-left": { x: padding, y: videoHeight - padding },
"bottom-center": { x: videoWidth / 2, y: videoHeight - padding },
"bottom-right": { x: videoWidth - padding, y: videoHeight - padding },
};
return positions[location];
}
```
## Font Styling
### Available Font Properties
```typescript
const textStyle = {
font_family: "Arial",
font_size: 48,
font_color: "#FFFFFF",
font_weight: "bold",
background_color: "rgba(0, 0, 0, 0.5)",
text_align: "center",
};
```
### Common Font Families
| Font | Style | Use Case |
|------|-------|----------|
| Arial | Sans-serif | Clean, universal |
| Helvetica | Sans-serif | Modern, professional |
| Times New Roman | Serif | Traditional, formal |
| Georgia | Serif | Elegant, readable |
| Roboto | Sans-serif | Modern, digital |
| Open Sans | Sans-serif | Friendly, accessible |
## Common Text Overlay Patterns
### Title Card
```typescript
const titleOverlay = {
text: "Product Demo",
x: 960,
y: 540,
font_family: "Arial",
font_size: 72,
font_color: "#FFFFFF",
text_align: "center",
duration: {
start: 0,
end: 3,
},
};
```
### Lower Third (Name/Title)
```typescript
const lowerThirdOverlay = {
text: "John Smith\nCEO, Company Inc.",
x: 100,
y: 900,
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 102, 204, 0.9)",
text_align: "left",
duration: {
start: 2,
end: 8,
},
};
```
### Call to Action
```typescript
const ctaOverlay = {
text: "Visit example.com",
x: 960,
y: 1000,
font_family: "Arial",
font_size: 42,
font_color: "#FFD700",
text_align: "center",
duration: {
start: 25,
end: 30,
},
};
```
## Creating Text Overlay Templates
```typescript
interface TextOverlayTemplate {
name: string;
style: Partial<TextOverlay>;
}
const templates: TextOverlayTemplate[] = [
{
name: "title",
style: {
font_family: "Arial",
font_size: 72,
font_color: "#FFFFFF",
text_align: "center",
},
},
{
name: "subtitle",
style: {
font_family: "Arial",
font_size: 42,
font_color: "#CCCCCC",
text_align: "center",
},
},
{
name: "lower-third",
style: {
font_family: "Arial",
font_size: 36,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.7)",
text_align: "left",
},
},
{
name: "caption",
style: {
font_family: "Arial",
font_size: 32,
font_color: "#FFFFFF",
background_color: "rgba(0, 0, 0, 0.5)",
text_align: "center",
},
},
];
function createTextOverlay(
text: string,
templateName: string,
position: Position,
duration?: { start: number; end: number }
): TextOverlay {
const template = templates.find((t) => t.name === templateName);
if (!template) {
throw new Error(`Template "${templateName}" not found`);
}
return {
text,
x: position.x,
y: position.y,
...template.style,
duration,
};
}
```
## Timing Text Overlays
Coordinate text appearance with your script:
```typescript
// Script with timing markers
const script = `
Hello and welcome. [0:00 - 0:03]
Let me show you our features. [0:03 - 0:08]
First, we have analytics. [0:08 - 0:15]
Get started today! [0:15 - 0:20]
`;
// Matching text overlays
const overlays = [
{
text: "Welcome",
duration: { start: 0, end: 3 },
...titleStyle,
},
{
text: "Feature Overview",
duration: { start: 3, end: 8 },
...subtitleStyle,
},
{
text: "Analytics Dashboard",
duration: { start: 8, end: 15 },
...lowerThirdStyle,
},
{
text: "www.example.com",
duration: { start: 15, end: 20 },
...ctaStyle,
},
];
```
## Best Practices
1. **Readability** - Use sufficient contrast between text and background
2. **Size** - Ensure text is large enough to read on mobile devices
3. **Duration** - Give viewers enough time to read (rule of thumb: 3 seconds minimum)
4. **Positioning** - Don't overlap with the avatar's face
5. **Consistency** - Use consistent fonts and styles throughout
6. **Accessibility** - Consider color-blind friendly palettes
## Limitations
- Text overlay support varies by subscription tier
- Some advanced styling options may not be available via API
- Complex animations may require post-production tools
- For auto-generated captions, see [captions.md](captions.md)
@@ -0,0 +1,347 @@
---
name: video-agent
description: One-shot prompt video generation with HeyGen Video Agent API
---
# Video Agent API
The Video Agent API generates complete videos from a single text prompt. Unlike the standard video generation API which requires detailed scene-by-scene configuration, Video Agent automatically handles script writing, avatar selection, visuals, voiceover, pacing, and captions.
## MCP Tool (Preferred)
If the HeyGen MCP server is connected, use `mcp__heygen__generate_video_agent` instead of direct API calls:
```
Tool: mcp__heygen__generate_video_agent
Parameters:
prompt: "<optimized prompt from prompt-optimizer.md>"
config:
duration_sec: 90 # optional, 5-300
avatar_id: "avatar_id" # optional, agent selects if omitted
orientation: "landscape" # optional, "landscape" or "portrait"
files: # optional
- asset_id: "uploaded_asset_id"
```
Then check status with `mcp__heygen__get_video` using the returned `video_id`.
The prompt quality is still the critical factor — always follow [prompt-optimizer.md](prompt-optimizer.md) regardless of whether you use MCP or direct API.
## When to Use Video Agent vs Standard API
| Use Case | Recommended API |
|----------|-----------------|
| Quick video from idea | Video Agent |
| Precise control over scenes, avatars, timing | Standard v2/video/generate |
| Automated content generation at scale | Video Agent |
| Specific avatar with exact script | Standard v2/video/generate |
| Prototype or draft video | Video Agent |
| Brand-consistent production video | Standard v2/video/generate |
## Before You Call This API
**Required step:** Optimize your prompt using [prompt-optimizer.md](prompt-optimizer.md) before generating a video. The difference between mediocre and professional results depends entirely on prompt quality.
Quick checklist:
1. Define visual style (colors, aesthetic) — see [visual-styles.md](visual-styles.md)
2. Structure scenes with specific scene types
3. Write VO script at ~150 words/minute
4. Specify media types for each scene (Motion Graphics, Stock, AI-generated)
## Direct API Endpoint
```
POST https://api.heygen.com/v1/video_agent/generate
```
## Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `prompt` | string | ✓ | Text prompt describing the video you want |
| `config` | object | | Configuration options (see below) |
| `files` | array | | Asset files to reference in generation |
| `callback_id` | string | | Custom ID for tracking. **Requires `callback_url` to also be set** — omit both if you don't need webhooks |
| `callback_url` | string | | Webhook URL for completion notification |
### Config Object
| Field | Type | Description |
|-------|------|-------------|
| `duration_sec` | integer | Approximate duration in seconds (5-300) |
| `avatar_id` | string | Specific avatar to use (optional - agent selects if not provided) |
| `orientation` | string | `"portrait"` or `"landscape"` |
### Files Array
| Field | Type | Description |
|-------|------|-------------|
| `asset_id` | string | Asset ID of uploaded file to reference |
## Response Format
```json
{
"error": null,
"data": {
"video_id": "abc123"
}
}
```
## curl Example
```bash
curl -X POST "https://api.heygen.com/v1/video_agent/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a 60-second product demo video for a new AI-powered calendar app. The tone should be professional but friendly, targeting busy professionals. Highlight the smart scheduling feature and time zone handling."
}'
```
## TypeScript
```typescript
interface VideoAgentConfig {
duration_sec?: number; // 5-300 seconds
avatar_id?: string; // Optional: specific avatar
orientation?: "portrait" | "landscape";
}
interface VideoAgentFile {
asset_id: string;
}
interface VideoAgentRequest {
prompt: string; // Required
config?: VideoAgentConfig;
files?: VideoAgentFile[];
callback_id?: string; // Requires callback_url if set
callback_url?: string;
}
interface VideoAgentResponse {
error: string | null;
data: {
video_id: string;
};
}
async function generateWithVideoAgent(
prompt: string,
config?: VideoAgentConfig
): Promise<string> {
const request: VideoAgentRequest = { prompt };
if (config) {
request.config = config;
}
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
const json: VideoAgentResponse = await response.json();
if (json.error) {
throw new Error(`Video Agent failed: ${json.error}`);
}
return json.data.video_id;
}
```
## Python
```python
import requests
import os
from typing import Optional
def generate_with_video_agent(
prompt: str,
duration_sec: Optional[int] = None,
avatar_id: Optional[str] = None,
orientation: Optional[str] = None
) -> str:
request_body = {"prompt": prompt}
config = {}
if duration_sec:
config["duration_sec"] = duration_sec
if avatar_id:
config["avatar_id"] = avatar_id
if orientation:
config["orientation"] = orientation
if config:
request_body["config"] = config
response = requests.post(
"https://api.heygen.com/v1/video_agent/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json=request_body
)
data = response.json()
if data.get("error"):
raise Exception(f"Video Agent failed: {data['error']}")
return data["data"]["video_id"]
```
## Examples
### Basic: Prompt Only
```typescript
const videoId = await generateWithVideoAgent(
"Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
);
```
### With Duration and Orientation
```typescript
const videoId = await generateWithVideoAgent(
"Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
{
duration_sec: 90,
orientation: "landscape"
}
);
```
### With Specific Avatar
```typescript
const videoId = await generateWithVideoAgent(
"Present quarterly sales results. Professional tone, data-focused.",
{
duration_sec: 120,
avatar_id: "josh_lite3_20230714",
orientation: "landscape"
}
);
```
### With Reference Files
Upload assets first, then reference them:
```typescript
// 1. Upload reference materials (see assets.md)
const logoAssetId = await uploadFile("./company-logo.png", "image/png");
const productImageId = await uploadFile("./product-screenshot.png", "image/png");
// 2. Generate video with references
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
config: {
duration_sec: 60,
orientation: "landscape"
},
files: [
{ asset_id: logoAssetId },
{ asset_id: productImageId }
]
}),
}
);
```
## Writing Effective Prompts
See **[prompt-optimizer.md](prompt-optimizer.md)** for comprehensive prompt writing guidance.
The prompt optimizer covers:
- Prompt complexity levels (basic → scene-by-scene)
- Visual style taxonomy and color specification
- Media type selection (Motion Graphics vs Stock vs AI-generated)
- Scene structure and timing calculations
- Ready-to-use templates for common video types
## Checking Video Status
Video Agent returns a `video_id` - use the standard status endpoint to check progress:
```typescript
// Same polling as standard video generation
const videoUrl = await waitForVideo(videoId);
```
See [video-status.md](video-status.md) for polling implementation.
## Comparison: Video Agent vs Standard API
### Video Agent Request
```typescript
// Simple: describe what you want
const videoId = await generateWithVideoAgent(
"Create a 60-second tutorial on setting up two-factor authentication. Professional tone, step-by-step."
);
```
### Equivalent Standard API Request
```typescript
// Complex: specify every detail
const videoId = await generateVideo({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to this tutorial on two-factor authentication...",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
// ... more scenes for each step
],
dimension: { width: 1920, height: 1080 },
});
```
## Limitations
- Less control over exact script wording
- Avatar selection may vary if not specified
- Scene composition is automated
- May not match precise brand guidelines
- Duration is approximate, not exact
## Best Practices
1. **Be specific in prompts** - More detail = better results
2. **Specify duration** - Use `config.duration_sec` for predictable length
3. **Lock avatar if needed** - Use `config.avatar_id` for consistency
4. **Upload reference files** - Help agent understand your brand/product
5. **Iterate on prompts** - Refine based on results
6. **Use for drafts** - Video Agent is great for quick iterations before final production
@@ -0,0 +1,770 @@
---
name: video-generation
description: POST /v2/video/generate workflow and multi-scene videos for HeyGen
---
# Video Generation
## Table of Contents
- [Video Output Formats](#video-output-formats)
- [Basic Video Generation](#basic-video-generation)
- [Request Fields](#request-fields)
- [Video Configuration Options](#video-configuration-options)
- [Multi-Scene Videos](#multi-scene-videos)
- [Using Different Character Types](#using-different-character-types)
- [Voice Input Types](#voice-input-types)
- [Complete Workflow Example](#complete-workflow-example)
- [Error Handling](#error-handling)
- [Script Length Limits](#script-length-limits)
- [Adding Pauses to Scripts](#adding-pauses-to-scripts)
- [Test Mode](#test-mode)
- [Production-Ready Workflow](#production-ready-workflow)
- [Transparent Background Videos (WebM)](#transparent-background-videos-webm)
- [Best Practices](#best-practices)
---
The `/v2/video/generate` endpoint is the primary way to create AI avatar videos with HeyGen.
## Video Output Formats
| Endpoint | Format | Use Case |
|----------|--------|----------|
| `/v2/video/generate` | MP4 | **Standard** - videos with background (most common) |
| `/v1/video.webm` | WebM | Transparent background - only when needed |
Use MP4 with background for most cases. WebM is only needed when you want to see content *behind* the avatar (e.g., overlaying avatar on a screen recording).
## Basic Video Generation
### curl
```bash
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [
{
"character": {
"type": "avatar",
"avatar_id": "josh_lite3_20230714",
"avatar_style": "normal"
},
"voice": {
"type": "text",
"input_text": "Hello! Welcome to HeyGen.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8"
}
}
],
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
## Request Fields
### Top-Level Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `video_inputs` | array | ✓ | Array of 1-50 video input objects |
| `dimension` | object | | Video dimensions `{width, height}` |
| `title` | string | | Video name for organization |
| `test` | boolean | | Test mode (watermarked, no credits) |
| `caption` | boolean | | Enable auto-captions |
| `callback_id` | string | | Custom ID for webhook tracking |
| `callback_url` | string | | URL for completion notification |
| `folder_id` | string | | Storage folder ID |
### video_inputs[].character Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | ✓ | `"avatar"` or `"talking_photo"` |
| `avatar_id` | string | ✓* | Avatar ID (*required when type is "avatar") |
| `talking_photo_id` | string | ✓* | Photo ID (*required when type is "talking_photo") |
| `avatar_style` | string | | `"normal"`, `"closeUp"`, or `"circle"` |
| `scale` | number | | Avatar scale factor |
| `offset` | object | | Position offset `{x, y}` |
### video_inputs[].voice Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | ✓ | `"text"`, `"audio"`, or `"silence"` |
| `voice_id` | string | ✓* | Voice ID (*required when type is "text") |
| `input_text` | string | ✓* | Script text (*required when type is "text") |
| `audio_url` | string | ✓* | Audio URL (*required when type is "audio") |
| `duration` | number | ✓* | Duration in seconds (*required when type is "silence") |
| `speed` | number | | Speech speed 0.5-2.0 (default 1.0) |
| `pitch` | number | | Voice pitch -20 to 20 (default 0) |
### video_inputs[].background Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `type` | string | | `"color"`, `"image"`, or `"video"` |
| `value` | string | | Hex color (when type is "color") |
| `url` | string | | Image/video URL (when type is "image"/"video") |
| `fit` | string | | `"cover"` or `"contain"` |
### TypeScript
```typescript
// Required fields have no '?' - optional fields have '?'
interface VideoInput {
character: {
type: "avatar" | "talking_photo"; // Required
avatar_id?: string; // Required when type="avatar"
talking_photo_id?: string; // Required when type="talking_photo"
avatar_style?: "normal" | "closeUp" | "circle";
scale?: number;
offset?: { x: number; y: number };
};
voice: {
type: "text" | "audio" | "silence"; // Required
input_text?: string; // Required when type="text"
voice_id?: string; // Required when type="text"
audio_url?: string; // Required when type="audio"
duration?: number; // Required when type="silence"
speed?: number;
pitch?: number;
};
background?: {
type?: "color" | "image" | "video";
value?: string;
url?: string;
fit?: "cover" | "contain";
};
}
interface VideoGenerateRequest {
video_inputs: VideoInput[]; // Required
dimension?: { width: number; height: number };
test?: boolean;
title?: string;
caption?: boolean;
callback_id?: string;
callback_url?: string;
folder_id?: string;
}
interface VideoGenerateResponse {
error: null | string;
data: {
video_id: string;
};
}
async function generateVideo(config: VideoGenerateRequest): Promise<string> {
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json: VideoGenerateResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.video_id;
}
```
### Python
```python
import requests
import os
def generate_video(config: dict) -> str:
response = requests.post(
"https://api.heygen.com/v2/video/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json=config
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["video_id"]
```
## Video Configuration Options
### Full Configuration Example
```typescript
const fullConfig: VideoGenerateRequest = {
// Test mode (no credits consumed, watermarked output)
test: false,
// Video title (for organization)
title: "Product Demo Video",
// Video dimensions
dimension: {
width: 1920,
height: 1080,
},
// Video scenes/inputs
video_inputs: [
{
// Avatar configuration
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
// Voice configuration
voice: {
type: "text",
input_text: "Welcome to our product demonstration!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.0,
pitch: 0,
},
// Background configuration
background: {
type: "color",
value: "#FFFFFF",
},
},
],
};
```
## Multi-Scene Videos
Create videos with multiple scenes:
```typescript
const multiSceneConfig = {
video_inputs: [
// Scene 1: Introduction
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Today I'll show you three key features.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
// Scene 2: Feature 1
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "closeUp",
},
voice: {
type: "text",
input_text: "First, let's look at our dashboard.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: "https://example.com/dashboard-bg.jpg",
},
},
// Scene 3: Conclusion
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Thanks for watching! Try it today.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
],
dimension: { width: 1920, height: 1080 },
};
```
## Using Different Character Types
### Avatar
```typescript
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal"
}
}
```
### Talking Photo
```typescript
{
character: {
type: "talking_photo",
talking_photo_id: "your_talking_photo_id"
}
}
```
## Voice Input Types
### Text-to-Speech
```typescript
{
voice: {
type: "text",
input_text: "Your script here",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.0, // 0.5 - 2.0
pitch: 0 // -20 to 20
}
}
```
### Custom Audio
```typescript
{
voice: {
type: "audio",
audio_url: "https://example.com/your-audio.mp3"
}
}
```
## Complete Workflow Example
```typescript
async function createVideo(script: string, avatarId: string, voiceId: string) {
// 1. Generate video
console.log("Starting video generation...");
const videoId = await generateVideo({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: voiceId,
},
background: {
type: "color",
value: "#FFFFFF",
},
},
],
dimension: { width: 1920, height: 1080 },
});
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
console.log("Waiting for video completion...");
const videoUrl = await waitForVideo(videoId);
console.log(`Video ready: ${videoUrl}`);
return videoUrl;
}
// Helper function for polling
async function waitForVideo(videoId: string): Promise<string> {
const maxAttempts = 60;
const pollInterval = 10000; // 10 seconds
for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.status === "completed") {
return data.video_url;
} else if (data.status === "failed") {
throw new Error(data.failure_message || "Video generation failed");
}
await new Promise((r) => setTimeout(r, pollInterval));
}
throw new Error("Video generation timed out");
}
```
## Error Handling
```typescript
async function generateVideoSafe(config: VideoGenerateRequest) {
try {
const videoId = await generateVideo(config);
return { success: true, videoId };
} catch (error) {
// Common errors
if (error.message.includes("quota")) {
console.error("Insufficient credits");
} else if (error.message.includes("avatar")) {
console.error("Invalid avatar ID");
} else if (error.message.includes("voice")) {
console.error("Invalid voice ID");
} else if (error.message.includes("script")) {
console.error("Script too long or invalid");
}
return { success: false, error: error.message };
}
}
```
## Script Length Limits
| Tier | Max Characters |
|------|----------------|
| Free | ~500 |
| Creator | ~1,500 |
| Team | ~3,000 |
| Enterprise | ~5,000+ |
## Adding Pauses to Scripts
Use `<break>` tags to add pauses in your script:
```typescript
const script = "Welcome to our demo. <break time=\"1s\"/> Let me show you the features.";
```
**Format:** `<break time="Xs"/>` where X is seconds (e.g., `1s`, `1.5s`, `0.5s`)
**Important:** Break tags must have spaces before and after them.
See [voices.md](voices.md) for detailed break tag documentation.
## Test Mode
Use test mode during development:
```typescript
const config = {
test: true, // Watermarked output, no credits consumed
video_inputs: [...],
};
```
## Production-Ready Workflow
Complete example using avatar's default voice (recommended), proper timeouts, and retry logic:
```typescript
interface VideoGenerationResult {
videoId: string;
videoUrl: string;
duration: number;
avatarId: string;
voiceId: string;
avatarName: string;
}
async function generateAvatarVideo(
script: string,
options: {
avatarId?: string; // Specific avatar, or will pick first available
width?: number;
height?: number;
} = {}
): Promise<VideoGenerationResult> {
const { width = 1920, height = 1080 } = options;
let { avatarId } = options;
// 1. List avatars if no specific one provided
if (!avatarId) {
console.log("Listing available avatars...");
const listResponse = await fetch("https://api.heygen.com/v2/avatars", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const listData = await listResponse.json();
if (!listData.data?.avatars?.length) {
throw new Error("No avatars available");
}
avatarId = listData.data.avatars[0].avatar_id;
}
// 2. Get avatar details including default_voice_id
console.log(`Getting details for avatar: ${avatarId}`);
const detailsResponse = await fetch(
`https://api.heygen.com/v2/avatar/${avatarId}/details`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: avatar } = await detailsResponse.json();
if (!avatar.default_voice_id) {
throw new Error(`Avatar ${avatar.name} has no default voice - select voice manually`);
}
console.log(`Using avatar: ${avatar.name} with default voice: ${avatar.default_voice_id}`);
// 3. Generate video using avatar's default voice
const videoId = await generateVideo({
video_inputs: [{
character: {
type: "avatar",
avatar_id: avatar.id, // from details response
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id, // pre-matched default voice
speed: 1.0,
},
background: {
type: "color",
value: "#1a1a2e",
},
}],
dimension: { width, height },
});
console.log(`Video ID: ${videoId}`);
// 3. Wait for completion (20 minute timeout - generation can take 15+ min)
console.log("Waiting for video generation (typically 5-15 minutes, can be longer)...");
const result = await waitForVideo(
videoId,
process.env.HEYGEN_API_KEY!,
(status, elapsed) => {
console.log(` [${Math.round(elapsed / 1000)}s] ${status}`);
},
1200000 // 20 minute timeout for safety
);
return {
videoId,
videoUrl: result.video_url!,
duration: result.duration!,
avatarId: avatar.id,
voiceId: avatar.default_voice_id,
avatarName: avatar.name,
};
}
// Usage - let it pick an avatar automatically
const result = await generateAvatarVideo(
"Hello! Welcome to our product demonstration."
);
console.log(`Video ready: ${result.videoUrl}`);
// Or specify a known avatar_id
const result2 = await generateAvatarVideo(
"Hello! Welcome to our product demonstration.",
{ avatarId: "josh_lite3_20230714" }
);
```
## Transparent Background Videos (WebM)
Use WebM **only when you need transparency** - i.e., when the avatar should be overlaid on other video content and you need to see through to what's behind.
**Don't need WebM for:**
- Avatar with motion graphics/text overlaid ON TOP of avatar
- Picture-in-picture with solid background
- Standard presenter videos
**Do need WebM for:**
- Avatar overlaid on screen recording
- Avatar floating over video background
- True alpha-channel compositing
### WebM Request Fields
**Note:** The WebM endpoint (`/v1/video.webm`) uses a different structure than `/v2/video/generate`.
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `avatar_pose_id` | string | ✓ | Avatar pose ID (from avatar details) |
| `avatar_style` | string | ✓ | `"normal"` or `"closeUp"` only (no circle) |
| `input_text` | string | ✓* | Script text (*required if not using input_audio) |
| `voice_id` | string | ✓* | Voice ID (*required with input_text) |
| `input_audio` | string | ✓* | Audio URL (*required if not using input_text) |
| `dimension` | object | | `{width, height}` (default: 1280x720) |
**Either** (`input_text` + `voice_id`) **OR** `input_audio` must be provided, but not both.
### curl
```bash
curl -X POST "https://api.heygen.com/v1/video.webm" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"avatar_pose_id": "josh_lite3_20230714",
"avatar_style": "normal",
"input_text": "Hello! This video has a transparent background.",
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"dimension": {
"width": 1920,
"height": 1080
}
}'
```
### TypeScript
```typescript
interface WebMVideoRequest {
avatar_pose_id: string; // Required
avatar_style: "normal" | "closeUp"; // Required (no circle support)
input_text?: string; // Required if not using input_audio
voice_id?: string; // Required with input_text
input_audio?: string; // Required if not using input_text
dimension?: { width: number; height: number };
}
async function generateTransparentVideo(
script: string,
avatarPoseId: string,
voiceId: string
): Promise<string> {
const response = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required
avatar_style: "normal", // Required: "normal" or "closeUp"
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
});
const { data } = await response.json();
return data.video_id;
}
```
### When to Use WebM vs MP4
| Scenario | Format | Why |
|----------|--------|-----|
| Avatar with overlays on top | **MP4** | Overlays go on top, don't need transparency |
| Standard presenter | **MP4** | Simpler, more compatible |
| Loom-style (avatar over screen recording) | **WebM** + `normal`/`closeUp` | Need transparency, crop to circle in post |
| Avatar floating over video content | **WebM** | Need to see content behind avatar |
**Note:** WebM only supports `normal` and `closeUp` styles. Circle style is not supported for WebM - apply circular masking in your video editor/Remotion instead.
### WebM Example: Loom-Style (Avatar Over Screen Recording)
Generate with `normal` or `closeUp` style (circle not supported for WebM):
```typescript
// Generate avatar with transparent background
const videoId = await fetch("https://api.heygen.com/v1/video.webm", {
method: "POST",
headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" },
body: JSON.stringify({
avatar_pose_id: avatarPoseId, // Required
avatar_style: "closeUp", // Required: "normal" or "closeUp" only
input_text: script, // Required (with voice_id)
voice_id: voiceId, // Required (with input_text)
dimension: { width: 1920, height: 1080 },
}),
}).then(r => r.json()).then(d => d.data.video_id);
```
Apply circular masking in Remotion:
```tsx
import { Video, AbsoluteFill } from "remotion";
export const LoomStyleVideo: React.FC<{
screenRecordingUrl: string;
avatarWebmUrl: string;
}> = ({ screenRecordingUrl, avatarWebmUrl }) => {
return (
<AbsoluteFill>
{/* Screen recording as base layer */}
<Video src={screenRecordingUrl} style={{ width: "100%", height: "100%" }} />
{/* Avatar with circular mask applied in CSS */}
<Video
src={avatarWebmUrl}
style={{
position: "absolute",
bottom: 20,
left: 20,
width: 150,
height: 150,
borderRadius: "50%", // Circular mask
overflow: "hidden",
objectFit: "cover",
}}
/>
</AbsoluteFill>
);
};
```
### Note on Status Polling
WebM videos use the same status endpoint as MP4:
```typescript
// Same polling as regular videos
const status = await getVideoStatus(videoId);
// status.video_url will be a .webm file
```
## Best Practices
1. **Preview avatars before generating** - Download `preview_image_url` so user can see what the avatar looks like before committing to a video (see [avatars.md](avatars.md))
2. **Use avatar's default voice** - Most avatars have a `default_voice_id` that's pre-matched for natural results (see [avatars.md](avatars.md))
2. **Fallback: match gender manually** - If no default voice, ensure avatar and voice genders match (see [voices.md](voices.md))
3. **Validate inputs** - Check avatar and voice IDs before generating
4. **Use test mode** - Test configurations without consuming credits
5. **Set generous timeouts** - Use 15-20 minutes; generation often takes 10-15 min, sometimes longer
6. **Consider async patterns** - For long videos, save video_id and check status later (see [video-status.md](video-status.md))
7. **Handle errors gracefully** - Implement proper error handling
8. **Monitor progress** - Implement polling with progress feedback
9. **Optimize scripts** - Keep scripts concise and natural
10. **Consider dimensions** - Match dimensions to your use case (see [dimensions.md](dimensions.md))
@@ -0,0 +1,502 @@
---
name: video-status
description: Polling patterns, status types, and retrieving download URLs for HeyGen videos
---
# Video Status and Polling
After generating a video, you need to poll for status until the video is complete. HeyGen processes videos asynchronously.
## MCP Tool (Preferred)
If the HeyGen MCP server is connected, use `mcp__heygen__get_video` with the `videoId` parameter. It returns status, video_url, thumbnail_url, duration, title, gif_url, captioned_video_url, and other metadata in a single call.
## Checking Video Status (Direct API)
### curl
```bash
curl -X GET "https://api.heygen.com/v2/videos/YOUR_VIDEO_ID" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface VideoStatusResponse {
error: null | string;
data: {
id: string;
status: "pending" | "processing" | "completed" | "failed";
video_url?: string;
thumbnail_url?: string;
duration?: number;
title?: string;
created_at?: string;
completed_at?: string;
gif_url?: string;
captioned_video_url?: string;
subtitle_url?: string;
folder_id?: string;
output_language?: string;
failure_code?: string;
failure_message?: string;
};
}
async function getVideoStatus(videoId: string): Promise<VideoStatusResponse["data"]> {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: VideoStatusResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}
```
### Python
```python
import requests
import os
def get_video_status(video_id: str) -> dict:
response = requests.get(
f"https://api.heygen.com/v2/videos/{video_id}",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]
```
## Video Status Types
| Status | Description |
|--------|-------------|
| `pending` | Video is queued for processing |
| `processing` | Video is being generated |
| `completed` | Video is ready for download |
| `failed` | Video generation failed |
## Expected Generation Times
Video generation typically takes **5-15 minutes**, but can exceed 20 minutes during peak load or for longer scripts.
| Factor | Impact |
|--------|--------|
| Script length | Longer scripts = significantly longer processing |
| Resolution | 1080p takes longer than 720p |
| Avatar complexity | Some avatars render faster |
| Queue load | Peak hours may cause 15-20+ minute waits |
| Multiple scenes | Each scene adds processing time |
**Recommendations**:
- Set timeout to **15-20 minutes** (900,000-1,200,000 ms) for safety
- For scripts > 2 minutes of speech, expect 15+ minutes
- Consider async patterns (save video_id, check later) for long videos
## Response Format
### Completed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "completed",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"title": "My Video",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:38:00Z",
"gif_url": "https://files.heygen.ai/gif/abc123.gif",
"captioned_video_url": null,
"subtitle_url": null,
"folder_id": null,
"output_language": "en"
}
}
```
### Failed Video
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "failed",
"failure_code": "script_too_long",
"failure_message": "Script too long for selected avatar"
}
}
```
## Polling Implementation
### Basic Polling
```typescript
async function waitForVideo(
videoId: string,
maxWaitMs = 600000, // 10 minutes
pollIntervalMs = 5000 // 5 seconds
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await getVideoStatus(videoId);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
case "pending":
case "processing":
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
break;
}
}
throw new Error("Video generation timed out");
}
```
### Polling with Progress Callback
```typescript
type ProgressCallback = (status: string, elapsed: number) => void;
async function waitForVideoWithProgress(
videoId: string,
onProgress?: ProgressCallback,
maxWaitMs = 600000,
pollIntervalMs = 5000
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const elapsed = Date.now() - startTime;
const status = await getVideoStatus(videoId);
onProgress?.(status.status, elapsed);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
default:
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
throw new Error("Video generation timed out");
}
// Usage
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`Status: ${status}, Elapsed: ${Math.round(elapsed / 1000)}s`);
}
);
```
### Python Polling
```python
import time
from typing import Optional, Callable
def wait_for_video(
video_id: str,
max_wait_seconds: int = 600,
poll_interval: int = 5,
on_progress: Optional[Callable[[str, int], None]] = None
) -> str:
start_time = time.time()
while time.time() - start_time < max_wait_seconds:
elapsed = int(time.time() - start_time)
status_data = get_video_status(video_id)
status = status_data["status"]
if on_progress:
on_progress(status, elapsed)
if status == "completed":
return status_data["video_url"]
elif status == "failed":
raise Exception(status_data.get("failure_message", "Video generation failed"))
time.sleep(poll_interval)
raise Exception("Video generation timed out")
# Usage
def progress_callback(status: str, elapsed: int):
print(f"Status: {status}, Elapsed: {elapsed}s")
video_url = wait_for_video(video_id, on_progress=progress_callback)
```
## Downloading the Video
Once the video is complete, download it. **Important**: The video URL may not be immediately available after status shows "completed". Use retry logic with backoff.
### TypeScript (with retry)
```typescript
import fs from "fs";
import path from "path";
async function downloadVideoWithRetry(
videoUrl: string,
outputPath = "./output/video.mp4",
maxRetries = 5,
initialDelayMs = 2000
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
console.log(`Video downloaded to ${outputPath}`);
return;
} catch (error) {
lastError = error as Error;
const delay = initialDelayMs * Math.pow(2, attempt); // Exponential backoff
console.log(`Download attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`Failed to download after ${maxRetries} attempts: ${lastError?.message}`);
}
```
### Python (with retry)
```python
import requests
import time
def download_video_with_retry(
video_url: str,
output_path: str,
max_retries: int = 5,
initial_delay: float = 2.0
) -> None:
last_error = None
for attempt in range(max_retries):
try:
response = requests.get(video_url, stream=True, timeout=60)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Video downloaded to {output_path}")
return
except Exception as e:
last_error = e
delay = initial_delay * (2 ** attempt) # Exponential backoff
print(f"Download attempt {attempt + 1} failed, retrying in {delay}s...")
time.sleep(delay)
raise Exception(f"Failed to download after {max_retries} attempts: {last_error}")
```
### Simple Download (no retry)
For quick scripts where you'll retry manually:
```typescript
async function downloadVideo(videoUrl: string, outputPath = "./output/video.mp4") {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
}
```
## Complete Workflow Example
```typescript
async function generateAndDownloadVideo(config: VideoConfig): Promise<string> {
// 1. Generate video
const generateResponse = await fetch(
"https://api.heygen.com/v2/video/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const { data: generateData } = await generateResponse.json();
const videoId = generateData.video_id;
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`[${Math.round(elapsed / 1000)}s] Status: ${status}`);
}
);
// 3. Download
const outputPath = `./output/${videoId}.mp4`;
await downloadVideo(videoUrl, outputPath);
return outputPath;
}
```
## Resumable Status Checking
For long-running generations, save the video_id and check status later rather than keeping a process waiting.
### Save State After Generation
```typescript
interface PendingVideo {
videoId: string;
createdAt: string;
script: string;
avatarId: string;
voiceId: string;
}
async function startVideoGeneration(config: VideoGenerateRequest): Promise<PendingVideo> {
const videoId = await generateVideo(config);
const pending: PendingVideo = {
videoId,
createdAt: new Date().toISOString(),
script: config.video_inputs[0].voice.input_text!,
avatarId: config.video_inputs[0].character.avatar_id!,
voiceId: config.video_inputs[0].voice.voice_id!,
};
// Save to file for later retrieval
fs.writeFileSync("pending-video.json", JSON.stringify(pending, null, 2));
console.log(`Video generation started. ID: ${videoId}`);
console.log("Check status later with: checkVideoStatus()");
return pending;
}
```
### Check Status Later
```typescript
async function checkVideoStatus(): Promise<void> {
if (!fs.existsSync("pending-video.json")) {
console.log("No pending video found");
return;
}
const pending: PendingVideo = JSON.parse(
fs.readFileSync("pending-video.json", "utf-8")
);
const elapsed = Date.now() - new Date(pending.createdAt).getTime();
console.log(`Checking video ${pending.videoId} (started ${Math.round(elapsed / 60000)} min ago)...`);
const status = await getVideoStatus(pending.videoId);
switch (status.status) {
case "completed":
console.log(`Video ready: ${status.video_url}`);
console.log(`Duration: ${status.duration}s`);
// Clean up pending file
fs.unlinkSync("pending-video.json");
// Save result
fs.writeFileSync("video-result.json", JSON.stringify({
...pending,
videoUrl: status.video_url,
thumbnailUrl: status.thumbnail_url,
duration: status.duration,
title: status.title,
createdAt: status.created_at,
completedAt: status.completed_at,
}, null, 2));
break;
case "failed":
console.error(`Video failed: ${status.failure_message}`);
fs.unlinkSync("pending-video.json");
break;
default:
console.log(`Status: ${status.status} - check again in a few minutes`);
}
}
```
### CLI-Friendly Pattern
```typescript
// generate-video.ts - Start generation and exit
async function main() {
const pending = await startVideoGeneration(config);
console.log(`\nVideo ID saved. Run 'npx tsx check-status.ts' to check progress.`);
process.exit(0); // Exit immediately, don't wait
}
// check-status.ts - Check and optionally wait
async function main() {
const args = process.argv.slice(2);
const shouldWait = args.includes("--wait");
if (shouldWait) {
// Poll until complete (with 20 min timeout)
const result = await waitForVideo(pending.videoId, apiKey, onProgress, 1200000);
console.log(`Done: ${result.video_url}`);
} else {
// Just check once and report
await checkVideoStatus();
}
}
```
## Alternative: Using Webhooks
Instead of polling, you can use webhooks to receive notifications when videos complete. See [webhooks.md](webhooks.md) for details. Webhooks are ideal for production systems where you don't want to maintain polling connections.
## Best Practices
1. **Use exponential backoff** - Increase poll intervals for long-running jobs
2. **Set reasonable timeouts** - Most videos complete within 10 minutes
3. **Handle failures gracefully** - Check error messages for actionable feedback
4. **Consider webhooks** - For production systems, webhooks are more efficient than polling
5. **Cache video URLs** - Downloaded video URLs are valid for a limited time
@@ -0,0 +1,367 @@
---
name: visual-styles
description: 20 named visual styles for Video Agent prompts — each with colors, typography, motion, and transitions
---
# Visual Style Library — 20 Styles
Named visual styles for Video Agent prompts. Each is inspired by a real graphic designer. Ordered by mood intensity.
**Picking a style:** Match mood first, content second. Ask: *"What should the viewer FEEL?"*
**Using a style:** Copy the style block into your prompt's STYLE section. Use the visual language rules — don't inject the example B-roll scenes (they confuse the agent).
**Custom styles:** These are examples. Create your own by combining elements, referencing other designers, art movements, or cultural aesthetics. The pattern: **named style + designer reference + color palette + typography + motion rules + transitions.**
## Quick Reference
| # | Style | Artist | Mood | Best For |
|---|---|---|---|---|
| 1 | Soft Signal | Sagmeister | Intimate, warm | Personal stories, wellness |
| 2 | Warm Grain | Eksell | Organic, friendly | Environmental, sustainability |
| 3 | Quiet Drama | Ray | Humanist, contemplative | Profiles, biographical |
| 4 | Heritage Reel | Cassandre | Nostalgic, vintage | History, retrospectives |
| 5 | Silk Route | Abedini | Flowing, mysterious | Global affairs, cross-cultural |
| 6 | Swiss Pulse | Müller-Brockmann | Clinical, precise | Data-heavy, analytical |
| 7 | Geometric Bold | Tanaka | Minimal, elegant | Lifestyle, visual essays |
| 8 | Velvet Standard | Vignelli | Premium, timeless | Luxury, investor updates |
| 9 | Digital Grid | Crouwel | Systematic, technical | Infrastructure, engineering |
| 10 | Contact Sheet | Brodovitch | Editorial, investigative | Journalism, deep dives |
| 11 | Folk Frequency | Terrazas | Cultural, vivid | Festivals, food, heritage |
| 12 | Earth Pulse | Ghariokwu | Grounded, communal | Community, grassroots |
| 13 | Dream State | Tomaszewski | Surreal, poetic | Op-eds, philosophy |
| 14 | Play Mode | Ahn Sang-soo | Playful, irreverent | Entertainment, pop culture |
| 15 | Carnival Surge | Lins | Euphoric, celebratory | Milestones, hype |
| 16 | Shadow Cut | Hillmann | Dark, cinematic | Exposés, investigations |
| 17 | Deconstructed | Brody | Industrial, raw | Tech news, punk energy |
| 18 | Maximalist Type | Scher | Loud, kinetic | Big announcements, launches |
| 19 | Data Drift | Anadol | Futuristic, immersive | AI/tech, innovation |
| 20 | Red Wire | Tartakover | Urgent, immediate | Breaking news, crisis |
## Mood-to-Style Guide
| Content feels... | Use... |
|---|---|
| Personal, intimate | Soft Signal, Quiet Drama |
| Natural, earthy | Warm Grain, Earth Pulse |
| Nostalgic, historical | Heritage Reel |
| Data-driven, analytical | Swiss Pulse, Digital Grid |
| Elegant, premium | Velvet Standard, Geometric Bold |
| Cultural, global | Silk Route, Folk Frequency |
| Investigative, serious | Contact Sheet, Shadow Cut |
| Fun, lighthearted | Play Mode, Carnival Surge |
| Philosophical, abstract | Dream State |
| Punk, grassroots, raw | Deconstructed |
| Hype, loud, high-energy | Maximalist Type |
| Tech-forward, futuristic | Data Drift |
| Breaking, urgent | Red Wire |
---
## 1. Soft Signal — Stefan Sagmeister
**Mood:** Intimate, warm | **Best for:** Personal stories, wellness, reflections
- Warm amber and cream with dusty rose, sage green, honey gold accents
- Handwritten-style text overlays — personal, lowercase, delicate
- Close-up framing: hands, faces, textures. Macro lens feel
- Slow drifts and floats, never snaps. Soft dissolves, warm light leaks
```
STYLE — SOFT SIGNAL (Sagmeister): Warm amber/cream, dusty rose, sage green.
Handwritten-style text. Close-up framing. Slow drifts and floats.
Soft dissolves with warm light leaks.
```
## 2. Warm Grain — Olle Eksell
**Mood:** Organic, friendly | **Best for:** Environmental, sustainability, community
- Earth tones: ochre, forest green, terracotta, cream, soft brown
- Rounded sans-serif type. Organic rounded compositions — nothing angular
- 16mm film grain, slightly desaturated. Natural textures: wood, linen, stone
- Gentle wipes, soft cuts, unhurried
```
STYLE — WARM GRAIN (Eksell): Earth tones — ochre, forest green, terracotta, cream.
Organic rounded compositions. 16mm film grain. Rounded sans-serif.
Gentle wipes and soft cuts.
```
## 3. Quiet Drama — Satyajit Ray
**Mood:** Humanist, contemplative | **Best for:** Profiles, biographical, cultural
- Muted warm: sepia, deep brown, soft gold, off-white, charcoal
- Clean serif type, positioned with care. Portrait framing
- Strong single-source contrast: window light, single lamp
- Deliberate pacing, longer holds. Slow fades to black
```
STYLE — QUIET DRAMA (Ray): Muted warm — sepia, deep brown, soft gold.
Portrait framing. Clean serif. Strong single-source contrast.
Slow fades to black.
```
## 4. Heritage Reel — Cassandre
**Mood:** Nostalgic, vintage | **Best for:** History, retrospectives, brand origins
- Faded gold, deep burgundy, navy, cream, sepia wash
- Elegant centered serif like classic film title cards
- Vignetting, softened edges. Film grain, light scratches, gentle jitter
- Iris wipes, film reel flicker
```
STYLE — HERITAGE REEL (Cassandre): Faded gold, burgundy, navy, sepia wash.
Elegant centered serif. Vignetting and aged film grain.
Iris wipe transitions.
```
## 5. Silk Route — Reza Abedini
**Mood:** Flowing, mysterious | **Best for:** Global affairs, cross-cultural, art/design
- Rich jewel tones: deep teal, burgundy, gold, lapis blue, black
- Elegant spaced type along natural visual lines
- Layered compositions — foreground, midground, background all active
- Flowing dissolves, smooth morphs
```
STYLE — SILK ROUTE (Abedini): Jewel tones — deep teal, burgundy, gold, lapis blue.
Layered compositions, all depths active. Elegant spaced type.
Flowing dissolves and smooth morphs.
```
## 6. Swiss Pulse — Josef Müller-Brockmann
**Mood:** Clinical, precise | **Best for:** Data-heavy, analytical, financial, metrics
- Black (#1a1a1a), white, ONE accent: electric blue (#0066FF)
- Helvetica Bold headlines, Regular labels. Numbers LARGE (80-120pt)
- Grid-locked compositions. Every element snaps to 12-column grid
- Animated counters COUNT UP from 0. Diagonal compositions on key moments
- Grid wipes, hard cuts. No dissolves
```
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + electric blue #0066FF.
Grid-locked. Helvetica Bold. Animated counters. Diagonal accents.
Grid wipe transitions.
```
## 7. Geometric Bold — Ikko Tanaka
**Mood:** Minimal, elegant | **Best for:** Clean lifestyle, culture, visual essays, brand profiles
- Maximum 3 flat colors per frame — no gradients
- Bold clean type as primary visual element
- Asymmetric composition, 60% negative space minimum. Single focal point
- Clean cuts on beat, no effects
```
STYLE — GEOMETRIC BOLD (Tanaka): Max 3 flat colors per frame.
60% negative space. Bold type as primary element.
Single focal point. Clean cuts on beat.
```
## 8. Velvet Standard — Massimo Vignelli
**Mood:** Premium, timeless | **Best for:** Luxury, investor updates, keynotes, product showcases
- Black, white, ONE rich accent: deep navy (#1a237e) or gold (#c9a84c)
- Thin sans-serif, ALL CAPS, letter-spaced wide
- Generous negative space. Symmetrical, centered, architectural precision
- Slow, deliberate. Sequential reveals. Elegant cross-dissolves
```
STYLE — VELVET STANDARD (Vignelli): Black, white, one accent: gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow elegant cross-dissolves.
```
## 9. Digital Grid — Wim Crouwel
**Mood:** Systematic, technical | **Best for:** Infrastructure, engineering, code, tech
- Dark (#0a0a0a) with cyan (#00E5FF), amber (#FFB300), green (#00FF88)
- Monospaced type throughout. Code-terminal aesthetic
- Pixel grid overlays visible. Everything snaps to system
- Grid nodes light up sequentially. Scan-line effects, cursor blinks
- Clean wipe transitions
```
STYLE — DIGITAL GRID (Crouwel): Monospaced type. Dark #0a0a0a with cyan #00E5FF, amber #FFB300.
Pixel grid overlays. Terminal aesthetic. Clean wipe transitions.
```
## 10. Contact Sheet — Alexey Brodovitch
**Mood:** Editorial, investigative | **Best for:** Journalism, deep dives, research breakdowns
- High contrast B&W with occasional desaturated color accents
- Bold sans-serif captions like editorial annotations
- Photo-editorial framing — multiple images, contact-sheet energy
- Raw grain, imperfect focus. Tight crops on faces and hands
- Hard cuts on beat, snap-zooms
```
STYLE — CONTACT SHEET (Brodovitch): High contrast B&W, desaturated accents.
Photo-editorial framing. Bold sans-serif annotations. Raw grain.
Hard cuts on beat. Snap-zooms.
```
## 11. Folk Frequency — Eduardo Terrazas
**Mood:** Cultural, vivid | **Best for:** Cultural events, food, tradition, heritage
- Vivid folk: hot pink, bright orange, cobalt blue, sun yellow, emerald
- Bold warm rounded type. Pattern and repetition — folk art rhythms
- Rich textures: woven fabrics, painted surfaces, ceramic, handmade
- Colorful wipes, quick cuts on festive rhythm
```
STYLE — FOLK FREQUENCY (Terrazas): Vivid folk — hot pink, cobalt blue, sun yellow, emerald.
Bold rounded type. Folk art rhythms. Rich handmade textures.
Colorful wipes on festive rhythm.
```
## 12. Earth Pulse — Lemi Ghariokwu
**Mood:** Grounded, communal | **Best for:** Community, music/culture, grassroots
- Warm saturated: burnt orange, deep green, rich yellow, terracotta
- Bold expressive type, center-frame. Wide community framing
- Rhythmic editing timed to musical beats
- Rhythmic cuts on beat, freeze-frames for emphasis
```
STYLE — EARTH PULSE (Ghariokwu): Warm saturated — burnt orange, deep green, rich yellow.
Bold expressive type. Wide community framing.
Rhythmic cuts on beat. Freeze-frames.
```
## 13. Dream State — Henryk Tomaszewski
**Mood:** Surreal, poetic | **Best for:** Op-eds, philosophy, think pieces, speculative
- Muted palette with one surreal accent: dusty blues, grey-greens, then shock of red or gold
- Sparse precise text — few words, maximum impact. Thin elegant floating type
- Unusual juxtapositions. Dreamlike quality: soft edges, atmospheric haze
- Slow morph dissolves. NEVER hard cuts
```
STYLE — DREAM STATE (Tomaszewski): Muted palette + one surreal accent.
Thin elegant floating type. Soft edges, atmospheric haze.
Slow morph dissolves — NEVER hard cuts.
```
## 14. Play Mode — Ahn Sang-soo
**Mood:** Playful, irreverent | **Best for:** Entertainment, pop culture, listicles, fun
- Bright candy: electric blue, hot pink, lime green, yellow, white
- Bouncy oversized tilted text. Asymmetric off-kilter compositions
- Quick cuts (1-3 seconds). Score cards, achievement popups, XP bars
- Bouncy spring physics — text overshoots and settles, screen shakes
- Pop cuts, whip pans, bounce effects
```
STYLE — PLAY MODE (Ahn Sang-soo): Electric blue, hot pink, lime green.
Bouncy spring physics. Oversized tilted text. Score cards, XP bars.
Pop cuts, bounce effects.
```
## 15. Carnival Surge — Rico Lins
**Mood:** Euphoric, celebratory | **Best for:** Big announcements, milestones, celebrations, hype
- Maximum color: hot pink (#FF1493), electric yellow (#FFE000), teal (#00CED1), orange, violet
- MASSIVE bold text at ANGLES over footage. Collage-style overlapping
- Rapid 1-2 second clips. Confetti, lights, constant energy
- Smash cuts, flash frames, rapid-fire montage
```
STYLE — CARNIVAL SURGE (Lins): Max color — hot pink #FF1493, yellow #FFE000, teal #00CED1.
Collage layering. Text MASSIVE at ANGLES. Confetti bursts.
Smash cuts, flash frames.
```
## 16. Shadow Cut — Hans Hillmann
**Mood:** Dark, cinematic | **Best for:** Exposés, investigations, controversy, dark deep dives
- Near-monochrome: deep blacks, cold greys, stark white + blood red or toxic green
- Sharp angular text like film noir title cards
- Heavy shadow — faces half-lit, objects emerging from darkness
- Slow creeping push-ins, slow reveals, tension
- Iris to black, slow fade from darkness, hard cuts to silence
```
STYLE — SHADOW CUT (Hillmann): Deep blacks, cold greys + blood red accent.
Sharp angular text. Heavy shadow. Slow creeping push-ins.
Hard cuts to black. Film noir tension.
```
## 17. Deconstructed — Neville Brody
**Mood:** Industrial, raw | **Best for:** Tech news, security, punk energy, counter-culture
- Dark grey (#1a1a1a), black, rust orange (#D4501E), raw white (#f0f0f0)
- Type at angles, overlapping edges, escaping frames. Bold industrial
- High contrast, gritty textures: scratched metal, peeling paint, scan-line glitch
- Text SLAMS, SHATTERS, PUNCHES. Letters scramble then snap
- Smash cuts, glitch transitions, white flash frames
```
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures, scan-line glitch.
Smash cuts with flash frames.
```
## 18. Maximalist Type — Paula Scher
**Mood:** Loud, kinetic | **Best for:** Big announcements, launches, high-energy recaps
- Bold saturated: red, yellow, black, white — maximum contrast
- Text IS the visual. Overlapping layers at different scales and angles, 50-80% of frame
- Kinetic energy: everything moving, slamming, sliding. 1-2 second rapid cuts
- Text layered OVER footage — never empty backgrounds
- Smash cuts, text slamming from edges, flash frames
```
STYLE — MAXIMALIST TYPE (Scher): Red, yellow, black, white — max contrast.
Text IS the visual. Overlapping at different scales, 50-80% of frame.
Kinetic everything. Smash cuts, flash frames.
```
## 19. Data Drift — Refik Anadol
**Mood:** Futuristic, immersive | **Best for:** AI/tech, speculative, cutting-edge science
- Iridescent: holographic silver, electric purple (#7c3aed), cyan (#06b6d4), deep black (#0a0a0a)
- Thin futuristic sans-serif — minimal, floating, weightless
- Fluid morphing compositions. Extreme scale shifts: microscopic to cosmic
- Particles coalesce into numbers, light traces data paths
- Liquid dissolves, particles dispersing and reforming
```
STYLE — DATA DRIFT (Anadol): Iridescent — purple #7c3aed, cyan #06b6d4, deep black.
Fluid morphing compositions. Thin futuristic type.
Liquid dissolves. Particles coalesce into numbers.
```
## 20. Red Wire — David Tartakover
**Mood:** Urgent, immediate | **Best for:** Breaking news, crisis updates, alerts
- High alert: red, black, white, emergency yellow — maximum contrast
- Bold condensed all caps — every word screams urgency
- Split screens, ticker-style text bars, timestamp overlays — max information density
- Multiple text elements simultaneously. Handheld energy
- Snap cuts, flash frames, zero breathing room
```
STYLE — RED WIRE (Tartakover): Red, black, white, emergency yellow.
Bold condensed all-caps. Split screens, tickers, timestamps.
Snap cuts, flash frames. Zero breathing room.
```
+505
View File
@@ -0,0 +1,505 @@
---
name: voices
description: Listing voices, locales, speed/pitch configuration for HeyGen
---
# HeyGen Voices
HeyGen provides a wide variety of AI voices for different languages, accents, and styles. Voices convert your text script into natural-sounding speech.
## Listing Available Voices
### curl
```bash
curl -X GET "https://api.heygen.com/v2/voices" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```typescript
interface Voice {
voice_id: string;
name: string;
language: string;
gender: "male" | "female";
preview_audio: string;
support_pause: boolean;
emotion_support: boolean;
}
interface VoicesResponse {
error: null | string;
data: {
voices: Voice[];
};
}
async function listVoices(): Promise<Voice[]> {
const response = await fetch("https://api.heygen.com/v2/voices", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const json: VoicesResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data.voices;
}
```
### Python
```python
import requests
import os
def list_voices() -> list:
response = requests.get(
"https://api.heygen.com/v2/voices",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]["voices"]
```
## Response Format
```json
{
"error": null,
"data": {
"voices": [
{
"voice_id": "1bd001e7e50f421d891986aad5158bc8",
"name": "Sara",
"language": "English",
"gender": "female",
"preview_audio": "https://files.heygen.ai/...",
"support_pause": true,
"emotion_support": true
},
{
"voice_id": "de8b5d78f2e0485f88d1e9f5c8e7f9a6",
"name": "Paul",
"language": "English",
"gender": "male",
"preview_audio": "https://files.heygen.ai/...",
"support_pause": true,
"emotion_support": false
}
]
}
}
```
## Supported Languages
HeyGen supports many languages including:
| Language | Code | Notes |
|----------|------|-------|
| English (US) | en-US | Multiple voice options |
| English (UK) | en-GB | British accent |
| Spanish | es-ES | Spain Spanish |
| Spanish (Latin) | es-MX | Mexican Spanish |
| French | fr-FR | France French |
| German | de-DE | Standard German |
| Portuguese | pt-BR | Brazilian Portuguese |
| Chinese (Mandarin) | zh-CN | Simplified Chinese |
| Japanese | ja-JP | Standard Japanese |
| Korean | ko-KR | Standard Korean |
| Italian | it-IT | Standard Italian |
| Dutch | nl-NL | Standard Dutch |
| Polish | pl-PL | Standard Polish |
| Arabic | ar-SA | Saudi Arabic |
## Using Voices in Video Generation
### Basic Voice Usage
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Welcome to our presentation.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Voice with Speed Adjustment
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "This is spoken at a faster pace.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
speed: 1.2, // 1.0 is normal, range: 0.5 - 2.0
},
},
],
};
```
### Voice with Pitch Adjustment
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "This has a higher pitch.",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
pitch: 10, // Range: -20 to 20
},
},
],
};
```
## Adding Pauses with Break Tags
HeyGen supports SSML-style `<break>` tags to add pauses in scripts.
### Break Tag Format
```
<break time="Xs"/>
```
Where `X` is the duration in seconds (e.g., `1s`, `1.5s`, `0.5s`).
### Requirements
| Rule | Example |
|------|---------|
| Use seconds with "s" suffix | `<break time="1.5s"/>` ✓ |
| Must have space before tag | `word <break time="1s"/>` ✓ |
| Must have space after tag | `<break time="1s"/> word` ✓ |
| Self-closing tag | `<break time="1s"/>` ✓ |
**Incorrect:** `word<break time="1s"/>word` (no spaces)
**Correct:** `word <break time="1s"/> word`
### Examples
```typescript
// Single pause
const script1 = "Hello and welcome. <break time=\"1s\"/> Let me introduce our product.";
// Multiple pauses
const script2 = "First point. <break time=\"1.5s\"/> Second point. <break time=\"1s\"/> Third point.";
// Pause at start (dramatic opening)
const script3 = "<break time=\"0.5s\"/> Welcome to our presentation.";
// Longer pause for emphasis
const script4 = "And the winner is... <break time=\"2s\"/> You!";
```
### Full Example
```typescript
const scriptWithPauses = `
Welcome to our product demo. <break time="1s"/>
Today I'll show you three key features. <break time="0.5s"/>
First, let's look at the dashboard. <break time="1.5s"/>
As you can see, it's incredibly intuitive.
`;
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: scriptWithPauses,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};
```
### Consecutive Breaks
Multiple consecutive break tags are automatically combined:
```typescript
// These two breaks:
"Hello <break time=\"1s\"/> <break time=\"0.5s\"/> world"
// Are treated as a single 1.5s pause
```
### Best Practices
1. **Use for emphasis** - Add pauses before important points
2. **Keep pauses reasonable** - 0.5s to 2s is typical; longer feels unnatural
3. **Match natural speech** - Add pauses where a human would breathe or pause
4. **Test the output** - Listen to generated audio to verify timing feels right
## Using Custom Audio Instead of TTS
Instead of text-to-speech, you can provide your own audio:
```typescript
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: "https://example.com/my-audio.mp3",
},
},
],
};
```
## Filtering Voices
### By Language
```typescript
function filterByLanguage(voices: Voice[], language: string): Voice[] {
return voices.filter((v) =>
v.language.toLowerCase().includes(language.toLowerCase())
);
}
const englishVoices = filterByLanguage(voices, "english");
const spanishVoices = filterByLanguage(voices, "spanish");
```
### By Gender
```typescript
function filterByGender(voices: Voice[], gender: "male" | "female"): Voice[] {
return voices.filter((v) => v.gender === gender);
}
const femaleVoices = filterByGender(voices, "female");
```
### By Features
```typescript
function filterByFeatures(
voices: Voice[],
options: { supportPause?: boolean; emotionSupport?: boolean }
): Voice[] {
return voices.filter((v) => {
if (options.supportPause !== undefined && v.support_pause !== options.supportPause) {
return false;
}
if (options.emotionSupport !== undefined && v.emotion_support !== options.emotionSupport) {
return false;
}
return true;
});
}
const expressiveVoices = filterByFeatures(voices, { emotionSupport: true });
```
## Voice Selection Helper
```typescript
interface VoiceSelectionCriteria {
language?: string;
gender?: "male" | "female";
supportPause?: boolean;
emotionSupport?: boolean;
}
async function findVoice(criteria: VoiceSelectionCriteria): Promise<Voice | null> {
const voices = await listVoices();
const filtered = voices.filter((v) => {
if (criteria.language && !v.language.toLowerCase().includes(criteria.language.toLowerCase())) {
return false;
}
if (criteria.gender && v.gender !== criteria.gender) {
return false;
}
if (criteria.supportPause !== undefined && v.support_pause !== criteria.supportPause) {
return false;
}
if (criteria.emotionSupport !== undefined && v.emotion_support !== criteria.emotionSupport) {
return false;
}
return true;
});
return filtered[0] || null;
}
// Usage
const voice = await findVoice({
language: "english",
gender: "female",
emotionSupport: true,
});
```
## Multi-Language Videos
Create videos with different languages per scene:
```typescript
const multiLanguageConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello! Welcome to our global product launch.",
voice_id: "english_voice_id",
},
},
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hola! Bienvenidos al lanzamiento global de nuestro producto.",
voice_id: "spanish_voice_id",
},
},
],
};
```
## Matching Voice to Avatar
### Recommended: Use Avatar's Default Voice
Many avatars have a `default_voice_id` that's pre-matched. **This is the best approach.**
```typescript
// Using v2 API to get avatar with default voice
const response = await fetch(
"https://api.heygen.com/v2/avatar_group.list?include_public=true",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
// Find avatar with a default voice
const avatar = data.avatar_group_list.find((a: any) => a.default_voice_id);
if (avatar) {
const videoConfig = {
video_inputs: [{
character: { type: "avatar", avatar_id: avatar.id },
voice: {
type: "text",
input_text: script,
voice_id: avatar.default_voice_id, // Pre-matched voice
},
}],
};
}
```
See [avatars.md](avatars.md) for complete examples.
### Fallback: Match Gender Manually
If avatar has no default voice, match genders manually:
```typescript
interface AvatarVoicePair {
avatarId: string;
voiceId: string;
gender: "male" | "female";
}
async function findMatchingAvatarAndVoice(
preferredGender?: "male" | "female"
): Promise<AvatarVoicePair> {
const [avatars, voices] = await Promise.all([
listAvatars(),
listVoices(),
]);
// Default to male if no preference
const gender = preferredGender || "male";
// Find avatar with matching gender
const avatar = avatars.find((a) => a.gender === gender);
if (!avatar) {
throw new Error(`No ${gender} avatar available`);
}
// Find voice with matching gender AND language
const voice = voices.find(
(v) => v.gender === gender && v.language.toLowerCase().includes("english")
);
if (!voice) {
throw new Error(`No ${gender} English voice available`);
}
return {
avatarId: avatar.avatar_id,
voiceId: voice.voice_id,
gender,
};
}
```
## Best Practices
1. **Match voice gender to avatar** - Always pair male voices with male avatars, female with female
2. **Match voice to content** - Use professional voices for business content
3. **Test voice previews** - Listen to preview audio before selecting
4. **Consider locale** - Match voice accent to target audience
5. **Use natural pacing** - Adjust speed for clarity, typically 0.9-1.1x
6. **Add pauses** - Use SSML breaks for more natural speech flow
7. **Validate availability** - Always verify voice_id exists before using
@@ -0,0 +1,393 @@
---
name: webhooks
description: Registering webhook endpoints and event types for HeyGen
---
# Webhooks
Webhooks allow HeyGen to notify your application when events occur, such as video completion. This is more efficient than polling for status updates.
## Overview
Instead of repeatedly checking video status, webhooks push notifications to your server when:
- Video generation completes
- Video generation fails
- Translation completes
- Avatar training completes
- Other async operations finish
## Setting Up a Webhook Endpoint
Your webhook endpoint should:
1. Accept POST requests
2. Return 200 status quickly
3. Handle events asynchronously
### Express.js Example
```typescript
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
// Webhook endpoint
app.post("/webhook/heygen", async (req, res) => {
// Acknowledge receipt immediately
res.status(200).send("OK");
// Process event asynchronously
processWebhookEvent(req.body).catch(console.error);
});
async function processWebhookEvent(event: HeyGenWebhookEvent) {
console.log(`Received event: ${event.event_type}`);
switch (event.event_type) {
case "avatar_video.success":
await handleVideoSuccess(event);
break;
case "avatar_video.fail":
await handleVideoFailure(event);
break;
case "video_translate.success":
await handleTranslationSuccess(event);
break;
default:
console.log(`Unknown event type: ${event.event_type}`);
}
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});
```
### Python Flask Example
```python
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
@app.route("/webhook/heygen", methods=["POST"])
def heygen_webhook():
event = request.json
# Acknowledge immediately
response = jsonify({"status": "received"})
# Process asynchronously
thread = threading.Thread(
target=process_webhook_event,
args=(event,)
)
thread.start()
return response, 200
def process_webhook_event(event):
event_type = event.get("event_type")
print(f"Received event: {event_type}")
if event_type == "avatar_video.success":
handle_video_success(event)
elif event_type == "avatar_video.fail":
handle_video_failure(event)
elif event_type == "video_translate.success":
handle_translation_success(event)
if __name__ == "__main__":
app.run(port=3000)
```
## Webhook Event Types
| Event Type | Description |
|------------|-------------|
| `avatar_video.success` | Video generation completed |
| `avatar_video.fail` | Video generation failed |
| `video_translate.success` | Translation completed |
| `video_translate.fail` | Translation failed |
| `instant_avatar.success` | Instant avatar created |
| `instant_avatar.fail` | Instant avatar creation failed |
## Event Payload Structure
### Video Success Event
```typescript
interface VideoSuccessEvent {
event_type: "avatar_video.success";
event_data: {
video_id: string;
video_url: string;
thumbnail_url: string;
duration: number;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.success",
"event_data": {
"video_id": "abc123",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"callback_id": "your_custom_id"
}
}
```
### Video Failure Event
```typescript
interface VideoFailureEvent {
event_type: "avatar_video.fail";
event_data: {
video_id: string;
error: string;
callback_id?: string;
};
}
```
```json
{
"event_type": "avatar_video.fail",
"event_data": {
"video_id": "abc123",
"error": "Script too long for selected avatar",
"callback_id": "your_custom_id"
}
}
```
## Registering a Webhook URL
Configure your webhook URL through the HeyGen dashboard or API:
### Request Fields
| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `url` | string | ✓ | Your webhook endpoint URL |
| `events` | array | ✓ | Event types to subscribe to |
| `secret` | string | | Shared secret for signature verification |
### Via API
```bash
curl -X POST "https://api.heygen.com/v1/webhook/endpoint.add" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook/heygen",
"events": ["avatar_video.success", "avatar_video.fail"]
}'
```
### TypeScript
```typescript
interface WebhookConfig {
url: string; // Required
events: string[]; // Required
secret?: string;
}
async function registerWebhook(config: WebhookConfig): Promise<void> {
const response = await fetch("https://api.heygen.com/v1/webhook/endpoint.add", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}
```
## Using Callback IDs
Track which video triggered a webhook with callback IDs:
### Include Callback ID in Video Generation
```typescript
const videoConfig = {
video_inputs: [...],
callback_id: "order_12345", // Your custom identifier
};
```
### Handle in Webhook
```typescript
async function handleVideoSuccess(event: VideoSuccessEvent) {
const { video_id, video_url, callback_id } = event.event_data;
if (callback_id) {
// Look up your original request
const order = await getOrderByCallbackId(callback_id);
await updateOrderWithVideo(order.id, video_url);
}
}
```
## Webhook Security
### Verify Webhook Signatures
If HeyGen provides signature verification:
```typescript
import crypto from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post("/webhook/heygen", (req, res) => {
const signature = req.headers["x-heygen-signature"] as string;
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process event...
});
```
### Validate Event Origin
```typescript
function isValidHeygenEvent(event: any): boolean {
// Check required fields
if (!event.event_type || !event.event_data) {
return false;
}
// Check event type is known
const validEventTypes = [
"avatar_video.success",
"avatar_video.fail",
"video_translate.success",
"video_translate.fail",
];
return validEventTypes.includes(event.event_type);
}
```
## Handling Webhook Failures
Implement retry logic and error handling:
```typescript
async function processWebhookEvent(event: HeyGenWebhookEvent) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await handleEvent(event);
return;
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
// Store failed event for manual review
await storeFailedEvent(event);
}
```
## Webhook vs Polling Comparison
| Aspect | Webhook | Polling |
|--------|---------|---------|
| Latency | Immediate | Depends on interval |
| Efficiency | High (push) | Low (repeated requests) |
| Complexity | Requires endpoint | Simpler to implement |
| Reliability | Needs retry handling | Guaranteed delivery |
| Cost | Lower API usage | Higher API usage |
## Testing Webhooks
### Local Development with ngrok
```bash
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhook/heygen
```
### Webhook Testing Tool
```typescript
// Test webhook locally
async function simulateWebhook(event: HeyGenWebhookEvent) {
const response = await fetch("http://localhost:3000/webhook/heygen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
console.log(`Response: ${response.status}`);
}
// Simulate success event
await simulateWebhook({
event_type: "avatar_video.success",
event_data: {
video_id: "test_123",
video_url: "https://example.com/test.mp4",
thumbnail_url: "https://example.com/test.jpg",
duration: 30,
callback_id: "test_callback",
},
});
```
## Best Practices
1. **Respond quickly** - Return 200 within 5 seconds, process async
2. **Handle duplicates** - Same event may be sent multiple times
3. **Implement retries** - Handle temporary processing failures
4. **Log everything** - Store webhook payloads for debugging
5. **Use callback IDs** - Track requests through the system
6. **Secure endpoints** - Verify signatures, use HTTPS
7. **Monitor health** - Track webhook success rates
8. **Queue processing** - Use job queues for heavy processing
+163
View File
@@ -0,0 +1,163 @@
---
name: lottie-bodymovin
description: Use when implementing Disney's 12 animation principles with Lottie animations exported from After Effects
---
# Lottie Animation Principles
Implement all 12 Disney animation principles using Lottie (Bodymovin) for vector animations.
## 1. Squash and Stretch
In After Effects before export:
- Animate Scale X and Y inversely
- Use expression: `s = transform.scale[1]; [100 + (100-s), s]`
```javascript
// Control at runtime
lottie.setSpeed(1.5); // affect squash timing
```
## 2. Anticipation
Structure your AE composition:
1. **Frames 0-10**: Wind-up pose
2. **Frames 10-40**: Main action
3. **Frames 40-50**: Settle
```javascript
// Play anticipation segment
anim.playSegments([0, 10], true);
setTimeout(() => anim.playSegments([10, 50], true), 200);
```
## 3. Staging
```javascript
// Layer multiple Lotties
<div className="scene">
<Lottie animationData={background} style={{ opacity: 0.6 }} />
<Lottie animationData={hero} style={{ zIndex: 10 }} />
</div>
```
## 4. Straight Ahead / Pose to Pose
Pose to pose in AE:
- Set keyframes at key poses
- Let AE interpolate between
- Use Easy Ease for smoothing
```javascript
// Jump to specific poses
anim.goToAndStop(25, true); // frame 25
```
## 5. Follow Through and Overlapping Action
In After Effects:
- Offset child layer keyframes by 2-4 frames
- Use parenting with delayed expressions
- `thisComp.layer("Parent").transform.position.valueAtTime(time - 0.05)`
## 6. Slow In and Slow Out
AE Keyframe settings:
- Select keyframes > Easy Ease (F9)
- Use Graph Editor to adjust curves
- Bezier handles control acceleration
```javascript
// Adjust playback speed dynamically
anim.setSpeed(0.5); // slower
anim.setSpeed(2); // faster
```
## 7. Arc
In After Effects:
- Use motion paths (position property)
- Convert keyframes to Bezier
- Pull handles to create arcs
- Or use "Auto-Orient to Path"
## 8. Secondary Action
```javascript
// Trigger secondary animation
mainAnim.addEventListener('complete', () => {
secondaryAnim.play();
});
// Or sync with frame
mainAnim.addEventListener('enterFrame', (e) => {
if (e.currentTime > 15) particleAnim.play();
});
```
## 9. Timing
```javascript
anim.setSpeed(0.5); // half speed - dramatic
anim.setSpeed(1); // normal
anim.setSpeed(2); // double speed - snappy
// Or control frame rate in AE export
// 24fps = cinematic, 30fps = smooth, 60fps = fluid
```
## 10. Exaggeration
In After Effects:
- Push scale beyond 100% (120-150%)
- Overshoot rotation
- Use Overshoot expression
- `amp = 15; freq = 3; decay = 5; n = 0; time_start = key(1).time; if (time > time_start) { n = (time - time_start) / thisComp.frameDuration; amp * Math.sin(freq*n) / Math.exp(decay*n/100); } else { 0; }`
## 11. Solid Drawing
In After Effects:
- Use 3D layers
- Apply perspective camera
- Animate Z position and rotation
- Use depth of field
## 12. Appeal
Design principles in AE:
- Smooth curves over sharp angles
- Consistent timing patterns
- Pleasing color palette
- Clean vector shapes
```javascript
// React Lottie with hover
<Lottie
animationData={data}
onMouseEnter={() => anim.setDirection(1)}
onMouseLeave={() => anim.setDirection(-1)}
/>
```
## Lottie Implementation
```javascript
import Lottie from 'lottie-react';
import animationData from './animation.json';
<Lottie
animationData={animationData}
loop={true}
autoplay={true}
style={{ width: 200, height: 200 }}
/>
```
## Key Lottie Features
- `playSegments([start, end])` - Play frame range
- `setSpeed(n)` - Control timing
- `setDirection(1/-1)` - Forward/reverse
- `goToAndStop(frame)` - Pose control
- `addEventListener` - Frame events
- Interactivity via `lottie-interactivity`
+176
View File
@@ -0,0 +1,176 @@
---
name: ltx2
description: AI video generation with LTX-2.3 22B — text-to-video, image-to-video clips for video production. Use when generating video clips, animating images, creating b-roll, animated backgrounds, or motion content. Triggers include video generation, animate image, b-roll, motion, video clip, text-to-video, image-to-video.
---
# LTX-2.3 Video Generation
Generate ~5 second video clips from text prompts or images using the LTX-2.3 22B DiT model.
Runs on Modal (A100-80GB). Requires `MODAL_LTX2_ENDPOINT_URL` in `.env`.
## Quick Reference
```bash
# Text-to-video
python3 tools/ltx2.py --prompt "A sunset over the ocean, golden light on waves, cinematic" --output sunset.mp4
# Image-to-video (animate a still image)
python3 tools/ltx2.py --prompt "Gentle camera drift, soft ambient motion" --input photo.jpg --output animated.mp4
# Custom resolution and duration
python3 tools/ltx2.py --prompt "..." --width 1024 --height 576 --num-frames 161 --output wide.mp4
# Fast mode (fewer steps, quicker)
python3 tools/ltx2.py --prompt "..." --quality fast --output quick.mp4
# Reproducible output
python3 tools/ltx2.py --prompt "..." --seed 42 --output reproducible.mp4
```
## Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `--prompt` | (required) | Text description of the video |
| `--input` | - | Input image for image-to-video |
| `--width` | 768 | Video width (divisible by 64) |
| `--height` | 512 | Video height (divisible by 64) |
| `--num-frames` | 121 | Frame count, must satisfy `(n-1) % 8 == 0` |
| `--fps` | 24 | Frames per second |
| `--quality` | standard | `standard` (30 steps) or `fast` (15 steps) |
| `--steps` | 30 | Override inference steps directly |
| `--seed` | random | Seed for reproducibility |
| `--output` | auto | Output file path |
| `--negative-prompt` | sensible default | What to avoid |
## Valid Frame Counts
`(n - 1) % 8 == 0`: 25 (~1s), 49 (~2s), 73 (~3s), 97 (~4s), **121 (~5s default)**, 161 (~6.7s), 193 (~8s max practical).
## Common Resolutions
| Resolution | Ratio | Notes |
|------------|-------|-------|
| 768x512 | 3:2 | Default, good balance |
| 512x512 | 1:1 | Square, fastest |
| 1024x576 | 16:9 | Widescreen |
| 576x1024 | 9:16 | Portrait/vertical |
## Prompting Guide
LTX-2 responds well to cinematographic descriptions. Layer these dimensions:
- **Camera:** "Slow dolly forward", "Aerial drone shot", "Tracking shot", "Static wide angle"
- **Lighting:** "Golden hour", "Cinematic lighting", "Neon-lit", "Soft diffused light"
- **Motion:** "Timelapse of...", "Slow motion", "Gentle camera drift", "Gradually transitions"
- **Style:** "Shot on 35mm film", "Documentary style", "Clean minimal aesthetic"
- **Negative:** Always implicitly avoids "worst quality, blurry, jittery, watermark, text, logo"
Keep prompts under 200 words. Be specific about the scene.
### Good Prompts
```
# Atmospheric b-roll
"Aerial drone shot slowly flying over turquoise ocean waves breaking on white sand, golden hour sunlight, cinematic"
# Product/tech scene
"Close-up of hands typing on a mechanical keyboard, shallow depth of field, soft desk lamp lighting, cozy atmosphere"
# Abstract background
"Dark moody abstract background with flowing blue light streaks, subtle geometric grid, bokeh particles floating, cinematic tech atmosphere"
# Animate a portrait
"Professional headshot, subtle natural head movement, confident warm expression, studio lighting, shallow depth of field"
# Animate a slide/screenshot
"Gentle subtle particle effects floating across a presentation slide, soft ambient light shifts, very slight camera drift"
```
### Bad Prompts
```
# Too vague
"A cool video"
# Too many competing ideas
"A cat riding a skateboard while juggling fire on the moon during a thunderstorm"
# Describing text/UI (model can't render text reliably)
"A website showing the text 'Welcome to our platform'"
```
## Video Production Use Cases
### B-Roll Clips
Generate atmospheric 5s shots for cutaways between narrated scenes:
```bash
python3 tools/ltx2.py --prompt "Futuristic holographic interface, glowing data visualizations, clean workspace, cinematic" --output broll_tech.mp4
python3 tools/ltx2.py --prompt "Aerial view of European city at golden hour, modern architecture" --output broll_europe.mp4
```
### Animated Slide Backgrounds
Feed a slide screenshot and add subtle motion:
```bash
python3 tools/ltx2.py --prompt "Gentle particle effects, soft ambient light shifts, very slight camera drift" --input slide.png --output animated_slide.mp4
```
### Animated Portraits
Bring still headshots to life:
```bash
python3 tools/ltx2.py --prompt "Subtle natural head movement, warm expression, professional lighting" --input headshot.png --output animated_portrait.mp4
```
### Branded Intro/Outro
Generate abstract motion backgrounds for title cards:
```bash
python3 tools/ltx2.py --prompt "Dark moody background with flowing blue and coral light streaks, bokeh particles, cinematic tech atmosphere, no text" --output intro_bg.mp4
```
### Combining with Other Tools
LTX-2 generates raw clips. Combine with the rest of the toolkit:
| Workflow | Tools |
|----------|-------|
| Generate clip → upscale | `ltx2.py``upscale.py` |
| Generate clip → add to Remotion | `ltx2.py` → use as `<OffthreadVideo>` in composition |
| Generate image → animate | `flux2.py``ltx2.py --input` |
| Generate clip → extract audio | `ltx2.py``ffmpeg -i clip.mp4 -vn audio.wav` |
| Generate clip → add voiceover | `ltx2.py` → mix with `qwen3_tts.py` output |
## Technical Details
- **Model:** LTX-2.3 22B DiT (Lightricks), bf16
- **GPU:** A100-80GB on Modal (~$4.68/hr)
- **Inference:** ~2.5 min per clip (768x512, 121 frames, 30 steps)
- **Cost:** ~$0.20-0.25 per 5s clip
- **Cold start:** ~60-90s (loading ~55GB weights)
- **Output:** H.264 MP4 with synchronized ambient audio (24fps)
- **Max duration:** ~8s (193 frames) per clip
### Known Limitations
- **Training data artifacts:** ~30% of generations may have unwanted logos/text from training data. Re-run with different `--seed`.
- **Text rendering:** Cannot reliably generate readable text in video. Use Remotion overlays instead.
- **Max duration:** ~8s per clip. Longer content needs stitching.
- **Audio:** Generated audio is ambient/environmental only. Use voiceover/music tools for speech and music.
- **License:** Community License — free under $10M revenue, commercial license needed above that.
## Setup
```bash
# 1. Create Modal secret for HuggingFace (one-time)
modal secret create huggingface-token HF_TOKEN=hf_your_token
# 2. Deploy (downloads ~55GB of weights, takes ~10 min)
modal deploy docker/modal-ltx2/app.py
# 3. Save endpoint URL to .env
echo "MODAL_LTX2_ENDPOINT_URL=https://yourname--video-toolkit-ltx2-ltx2-generate.modal.run" >> .env
# 4. Test
python3 tools/ltx2.py --prompt "A candle flickering on a dark table, cinematic" --output test.mp4
```
**Important:** HuggingFace token needs read-access scope. Accept the [Gemma 3 license](https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized) before deploying. Unauthenticated downloads are severely rate-limited.
+138
View File
@@ -0,0 +1,138 @@
---
name: manim-composer
description: |
Trigger when: (1) User wants to create an educational/explainer video, (2) User has a vague concept they want visualized, (3) User mentions "3b1b style" or "explain like 3Blue1Brown", (4) User wants to plan a Manim video or animation sequence, (5) User asks to "compose" or "plan" a math/science visualization.
Transforms vague video ideas into detailed scene-by-scene plans (scenes.md). Conducts research, asks clarifying questions about audience/scope/focus, and outputs comprehensive scene specifications ready for implementation with ManimCE or ManimGL.
Use this BEFORE writing any Manim code. This skill plans the video; use manimce-best-practices or manimgl-best-practices for implementation.
---
## Workflow
### Phase 1: Understand the Concept
1. **Research the topic** deeply before asking questions
- Use web search to understand the core concepts
- Identify the key insights that make this topic interesting
- Find the "aha moment" - what makes this click for learners
- Note common misconceptions to address
2. **Identify the narrative hook**
- What question does this video answer?
- Why should the viewer care?
- What's the surprising or counterintuitive element?
### Phase 2: Clarify with User
Ask targeted questions (not all at once - adapt based on responses):
**Audience & Scope**
- What math/science background should I assume? (e.g., "knows calculus" or "high school algebra")
- Target video length? (short: 5-10min, medium: 15-20min, long: 30min+)
- Should this be self-contained or part of a series?
**Focus & Depth**
- Any specific aspects to emphasize or skip?
- Proof-heavy or intuition-focused?
- Real-world applications to include?
**Style Preferences**
- Color scheme preferences?
- Narration style? (casual, formal, playful)
- Any specific visual metaphors you have in mind?
### Phase 3: Create scenes.md
Output a comprehensive `scenes.md` file with this structure:
```markdown
# [Video Title]
## Overview
- **Topic**: [Core concept]
- **Hook**: [Opening question/mystery]
- **Target Audience**: [Prerequisites]
- **Estimated Length**: [X minutes]
- **Key Insight**: [The "aha moment"]
## Narrative Arc
[2-3 sentences describing the journey from confusion to understanding]
---
## Scene 1: [Scene Name]
**Duration**: ~X seconds
**Purpose**: [What this scene accomplishes]
### Visual Elements
- [List of mobjects needed]
- [Animations to use]
- [Camera movements]
### Content
[Detailed description of what happens, what's shown, what's explained]
### Narration Notes
[Key points to convey, tone, pacing notes]
### Technical Notes
- [Specific Manim classes/methods to use]
- [Any tricky implementations to note]
---
## Scene 2: [Scene Name]
...
---
## Transitions & Flow
[Notes on how scenes connect, recurring visual motifs]
## Color Palette
- Primary: [color] - used for [purpose]
- Secondary: [color] - used for [purpose]
- Accent: [color] - used for [purpose]
- Background: [color]
## Mathematical Content
[List of equations, formulas, or mathematical objects that need to be rendered]
## Implementation Order
[Suggested order for implementing scenes, noting dependencies]
```
## 3b1b Style Principles
Apply these principles when composing scenes:
### Visual Storytelling
- **Show, don't just tell** - Every concept needs a visual representation
- **Progressive revelation** - Build complexity gradually, don't show everything at once
- **Visual continuity** - Transform objects rather than replacing them when possible
### Pacing & Rhythm
- **Pause for insight** - Give viewers time to absorb key moments
- **Vary the pace** - Mix quick sequences with slower explanations
- **End scenes with resolution** - Each scene should feel complete
### Mathematical Beauty
- **Emphasize elegance** - Highlight when math is surprisingly simple or beautiful
- **Connect representations** - Show the same concept multiple ways (algebraic, geometric, intuitive)
- **Embrace abstraction gradually** - Start concrete, then generalize
### Engagement Techniques
- **Pose questions** - Make viewers curious before revealing answers
- **Acknowledge difficulty** - "This might seem confusing at first..."
- **Celebrate insight** - Make the "aha moment" feel earned
## References
- [references/narrative-patterns.md](references/narrative-patterns.md) - Common 3b1b narrative structures
- [references/visual-techniques.md](references/visual-techniques.md) - Effective visualization patterns
- [references/scene-examples.md](references/scene-examples.md) - Example scenes.md excerpts
## Templates
- [templates/scenes-template.md](templates/scenes-template.md) - Blank scenes.md template
@@ -0,0 +1,125 @@
# Narrative Patterns for Math Explainers
Common structures used in effective 3Blue1Brown-style videos.
## Pattern 1: Mystery → Investigation → Resolution
**Structure:**
1. Present a puzzling result or paradox
2. Investigate why it's true through visual exploration
3. Reveal the underlying principle
4. Show how the principle generalizes
**Example topics:** Euler's identity, Bayes theorem, infinite series paradoxes
**Opening hooks:**
- "What does it even mean to raise a number to an imaginary power?"
- "This equation looks wrong, but it's actually true..."
- "Most people get this probability question wrong..."
---
## Pattern 2: Build Up → Payoff
**Structure:**
1. Introduce simple building blocks
2. Combine them to create something complex
3. Show the beautiful/surprising result
4. Reflect on why it works
**Example topics:** Fourier series, neural networks, linear algebra
**Opening hooks:**
- "Let's start with something simple..."
- "Each piece here is easy, but together they do something remarkable..."
---
## Pattern 3: Two Perspectives → Unity
**Structure:**
1. Show concept from perspective A (e.g., algebraic)
2. Show same concept from perspective B (e.g., geometric)
3. Reveal they're the same thing
4. Explore implications of this connection
**Example topics:** Dot product, determinants, complex multiplication
**Opening hooks:**
- "There are two ways to think about this..."
- "These seem like completely different ideas, but..."
---
## Pattern 4: Wrong → Less Wrong → Right
**Structure:**
1. Present common misconception or naive approach
2. Show why it fails
3. Refine the approach
4. Arrive at correct understanding
**Example topics:** Limits, probability distributions, definitions
**Opening hooks:**
- "Your first instinct here is probably wrong..."
- "The obvious approach doesn't quite work..."
---
## Pattern 5: Specific → General
**Structure:**
1. Solve a specific concrete example
2. Notice patterns in the solution
3. Abstract to general principle
4. Apply to new situations
**Example topics:** Derivatives, group theory, algorithm analysis
**Opening hooks:**
- "Let's work through a specific example..."
- "Once you see the pattern here, it shows up everywhere..."
---
## Pattern 6: History as Narrative
**Structure:**
1. Present the problem as historically encountered
2. Follow the journey of discovery
3. Show key insights that led to breakthroughs
4. Connect to modern understanding
**Example topics:** Calculus, cryptography, quantum mechanics
**Opening hooks:**
- "Imagine you're a mathematician in the 1600s..."
- "This problem stumped the greatest minds for centuries..."
---
## Combining Patterns
Most effective videos combine multiple patterns:
- Mystery hook + Build Up explanation
- Two Perspectives + Specific → General examples
- Wrong → Right + History narrative
## Pacing Guidelines
| Video Length | Intro Hook | Main Content | Recap/Implications |
|--------------|------------|--------------|-------------------|
| 5-10 min | 30-60s | 4-8 min | 30-60s |
| 15-20 min | 1-2 min | 12-16 min | 1-2 min |
| 30+ min | 2-3 min | 24-26 min | 2-4 min |
## Emotional Arc
Every video should have emotional beats:
1. **Curiosity** (opening) - Why should I care?
2. **Confusion** (early) - This is harder than it looks
3. **Partial clarity** (middle) - I'm starting to see...
4. **Aha moment** (climax) - Oh! That's beautiful!
5. **Satisfaction** (end) - Now I truly understand

Some files were not shown because too many files have changed in this diff Show More