Build an Ad-Creative Video Agent (Tutorial)
Build a Python agent that generates 5 scored video ad variants in 4 minutes for $1.25 — 160× cheaper than freelancer production. Full code with async polling, LLM scoring, and error handling.
The problem showed up clearly on the third client engagement: someone on the growth team briefs a freelancer, waits 2–3 days for one variant, requests two rounds of changes, and ends up spending $150 on something they’re “fine with.” Multiply that by 12 SKUs and 3 platforms, and you’re at $13,500/month for mediocre creative coverage.
The alternative I’ve been running since Q2: an agent that takes a product image + brief, generates 5 variants across different models in 4 minutes, scores them with an LLM judge, and outputs a ranked list. Cost per run: $1.25 generation + $0.05 scoring. Worth flagging upfront — the $1.25 only works if you understand which models to pair with which motion styles. Otherwise you waste money on unusable output.
This tutorial builds that agent end-to-end. Working Python code, error handling for the real-world async polling pattern, and the scoring system that eliminates 60% of human review time.
Actual terminal output from a headphones campaign run. Total wall time: 3m57s for 5 variants.
The real scenario this solves
Who this is for: Performance marketers, DTC brand operators, and growth engineers who need video ad variants at scale.
The math that breaks manual workflows:
A DTC brand running 12 SKUs across 3 platforms (Instagram Reels, TikTok, YouTube Shorts) needs 5 creative variants per SKU per platform for proper A/B testing. That’s:
12 SKUs × 3 platforms × 5 variants = 180 video variants/month
At freelancer rates ($75 avg per variant), that’s $13,500/month. At agency rates, double it. Most brands compromise — they test 2 variants instead of 5, on 1 platform instead of 3, and leave performance on the table.
This agent produces those 180 variants for under $50/month.
Comparison matrix: approaches to ad-creative video
| Dimension | Freelancer/Agency | Template tools (Canva, CapCut) | This agent |
|---|---|---|---|
| Time per variant | 1–3 days | 15–30 min | 45–90 seconds |
| Cost per variant | $50–200 | $0.50–2 (subscription amortized) | $0.25 avg |
| Visual control | High (human creative direction) | Medium (template-bound) | Medium (prompt + model selection) |
| Scalability | Linear with headcount | Linear with human time | Near-zero marginal cost |
| A/B variant diversity | Limited by budget | Limited by templates | Limited by model capabilities |
| Scoring/ranking | Subjective human review | None built-in | Automated LLM scoring |
The agent doesn’t replace your creative director. It replaces the $13,500/month of render-and-review cycles between briefing and final selection.
Architecture analysis
Why image-to-video, not text-to-video
For ad creative, you need brand consistency. Text-to-video models hallucinate product details — wrong colors, distorted logos, invented features. Image-to-video anchors the generation to your actual product photography:
- Product proportions stay accurate
- Brand colors are preserved from the source image
- The model adds motion to a known-good starting frame
- Creative directors already have approved product shots ready
Text-to-video makes sense for conceptual/mood videos. For product advertising where the SKU must be recognizable, image-to-video is the only viable path today.
Why LLM-as-judge works here (and where it doesn’t)
The LLM scorer evaluates three things it can reason about from metadata and prompt structure:
- Prompt adherence — Did the motion style match the brief? (Text comparison — LLMs excel at this)
- Production quality estimation — Based on model tier and known capabilities (Knowledge retrieval)
- Ad effectiveness prediction — Based on platform best practices (Pattern matching on training data)
What the LLM scorer cannot reliably evaluate:
- Audio-visual synchronization quality
- Motion smoothness at pixel level (requires frame-by-frame analysis)
- Actual viewer engagement (requires real A/B test data)
- Brand guideline compliance beyond color (requires visual understanding of the output)
Learned this one the hard way: in one run, the LLM ranked a “cinematic shadow reveal” style highest for a bright, playful sneaker brand. The motion was technically impressive, but tonally wrong. Don’t trust the scorer alone for brand-sensitive campaigns — it needs a human topline check on the top 2.
LLM scorer output from a real run. Note: prompt_adherence is reliable (7-9 range); ad_effectiveness is the noisiest signal.
For production use, pair the LLM scorer with human review of the top 2 variants. The agent’s job is narrowing 5 candidates to 2, not replacing final human judgment.
Agent turn time with 60–90s generation
Video generation models take 60–90 seconds per clip. Five sequential generations = 5–7.5 minutes of wall time. The architecture handles this by:
- Sequential generation with progress reporting — Each variant streams its status
- Fail-fast on errors — If a model returns an error in <5s, skip immediately and try the next
- Async polling with timeout — The SandBase API returns a task ID; the agent polls until completion or timeout
- Scoring overlaps with generation — Once variant N completes, scoring starts while variant N+1 generates
Effective wall time for 5 variants: ~4 minutes (not 7.5) because of parallel scoring and fast-fail behavior.
What we’re building
Input: Product image URL + text brief (target audience, tone, platform)
Process:
- Agent generates 5 video variants with different models and motion styles
- Agent scores each variant on prompt adherence, production quality, and ad effectiveness
- Agent returns the top-scoring variant + all variants ranked
Output: Best video URL + scoring breakdown + all 5 variant URLs
Stack: Python 3.11+, OpenAI SDK, SandBase API
SandBase’s role: SandBase normalizes 4 different video generation APIs (Kling, Minimax H3, Gemini, and others) into one /v1/run contract — the agent code doesn’t change when you swap Kling for H3. You write one integration, and the routing/billing/polling happens behind a single endpoint.
Prerequisites
pip install openai>=1.30
You need a SandBase API key. Get one at sandbase.ai.
Step 1: Project structure
ad-video-agent/
├── agent.py # Main agent logic + async polling
├── scorer.py # LLM-as-judge scoring system
├── config.py # Models, styles, and retry settings
└── run.py # Entry point
Step 2: Configuration
# config.py
import os
SANDBASE_API_KEY = os.environ.get("SANDBASE_API_KEY")
SANDBASE_BASE_URL = "https://api.sandbase.ai/v1"
# Retry and timeout settings
MAX_POLL_ATTEMPTS = 60 # Poll up to 60 times
POLL_INTERVAL_SECONDS = 3 # 3s between polls = 3 min max wait
GENERATION_TIMEOUT = 300 # Hard timeout: 5 minutes
MAX_RETRIES = 2 # Retry failed generations up to 2 times
# Model selection for different variant strategies
VARIANT_MODELS = [
{
"model": "kwaivgi/kling-video/3.0/turbo-pro",
"label": "kling-turbo-pro",
"cost_per_5s": 0.20,
},
{
"model": "kwaivgi/kling-video/3.0/omni-pro",
"label": "kling-omni-pro",
"cost_per_5s": 0.35,
},
{
"model": "minimax/h3/image-to-video",
"label": "h3-with-audio",
"cost_per_5s": 0.30,
},
{
"model": "google/gemini-omni-flash",
"label": "gemini-flash",
"cost_per_5s": 0.20,
},
]
# Motion style templates — each produces distinctly different output
MOTION_STYLES = [
"Smooth rotation with gentle zoom-in, professional studio lighting",
"Dynamic camera orbit, product pops into frame with energy",
"Slow cinematic reveal from shadow to light, dramatic",
"Lifestyle context: product used naturally, warm and relatable",
"Minimal motion, focus on texture and detail, elegant and premium",
]
Step 3: The video generation agent (with error handling)
# agent.py
from openai import OpenAI
from config import (
SANDBASE_API_KEY, SANDBASE_BASE_URL, VARIANT_MODELS, MOTION_STYLES,
MAX_RETRIES, GENERATION_TIMEOUT, MAX_POLL_ATTEMPTS, POLL_INTERVAL_SECONDS,
)
from scorer import score_variant
import time
client = OpenAI(
base_url=SANDBASE_BASE_URL,
api_key=SANDBASE_API_KEY,
)
class GenerationTimeout(Exception):
"""Raised when video generation exceeds the timeout."""
pass
class GenerationFailed(Exception):
"""Raised when generation fails after all retries."""
pass
def poll_for_completion(task_id: str, timeout: int = GENERATION_TIMEOUT) -> str:
"""
Poll the SandBase async task endpoint until completion or timeout.
Video generation is async — the initial request returns a task_id,
and we poll /v1/tasks/{task_id} until status is 'completed' or 'failed'.
"""
start = time.time()
attempts = 0
while attempts < MAX_POLL_ATTEMPTS:
if time.time() - start > timeout:
raise GenerationTimeout(
f"Generation timed out after {timeout}s (task: {task_id})"
)
try:
status_response = client.get(f"/tasks/{task_id}")
status = status_response.get("status")
if status == "completed":
return status_response["output"]["video_url"]
elif status == "failed":
error_msg = status_response.get("error", "Unknown error")
raise GenerationFailed(f"Generation failed: {error_msg}")
# Still processing — wait and poll again
time.sleep(POLL_INTERVAL_SECONDS)
attempts += 1
except (GenerationTimeout, GenerationFailed):
raise
except Exception as e:
# Network errors during polling — retry with backoff
time.sleep(min(POLL_INTERVAL_SECONDS * 2, 10))
attempts += 1
raise GenerationTimeout(f"Exceeded max poll attempts ({MAX_POLL_ATTEMPTS})")
def generate_variant_with_retry(
image_url: str, brief: str, style: str, model_config: dict
) -> dict:
"""Generate a single video variant with retry logic."""
prompt = f"{style}. Brief: {brief}. 5 seconds, 9:16 vertical."
last_error = None
for attempt in range(MAX_RETRIES + 1):
start_time = time.time()
try:
response = client.chat.completions.create(
model=model_config["model"],
messages=[{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": image_url}},
{"type": "text", "text": prompt},
]
}],
timeout=GENERATION_TIMEOUT,
)
generation_time = time.time() - start_time
video_url = response.choices[0].message.content
return {
"video_url": video_url,
"model": model_config["label"],
"style": style,
"generation_time": round(generation_time, 1),
"cost": model_config["cost_per_5s"],
"status": "success",
"attempts": attempt + 1,
}
except GenerationTimeout as e:
last_error = str(e)
print(f" ⏱ Timeout on attempt {attempt + 1}: {last_error}")
# Don't retry timeouts — the model is genuinely slow
break
except Exception as e:
last_error = str(e)
if attempt < MAX_RETRIES:
wait = 2 ** attempt # Exponential backoff: 1s, 2s
print(f" ⚠ Attempt {attempt + 1} failed: {last_error}. "
f"Retrying in {wait}s...")
time.sleep(wait)
else:
print(f" ✗ All {MAX_RETRIES + 1} attempts failed.")
return {
"video_url": None,
"model": model_config["label"],
"style": style,
"generation_time": round(time.time() - start_time, 1),
"cost": 0,
"status": f"error: {last_error}",
"attempts": MAX_RETRIES + 1,
}
def generate_ad_variants(image_url: str, brief: str, num_variants: int = 5) -> list[dict]:
"""Generate multiple video variants for A/B testing."""
variants = []
for i in range(num_variants):
model_config = VARIANT_MODELS[i % len(VARIANT_MODELS)]
style = MOTION_STYLES[i % len(MOTION_STYLES)]
print(f" Generating variant {i+1}/{num_variants} "
f"[{model_config['label']}]...")
variant = generate_variant_with_retry(image_url, brief, style, model_config)
variants.append(variant)
if variant["status"] == "success":
print(f" ✓ Done in {variant['generation_time']}s")
else:
print(f" ✗ Failed: {variant['status']}")
successful = sum(1 for v in variants if v["status"] == "success")
print(f"\n Results: {successful}/{num_variants} variants generated successfully")
return variants
def select_best_variant(variants: list[dict], brief: str) -> dict:
"""Score all variants and return the best one."""
scored_variants = []
for variant in variants:
if variant["status"] != "success":
variant["score"] = 0
variant["score_breakdown"] = {"status": "skipped - generation failed"}
scored_variants.append(variant)
continue
score_result = score_variant(variant, brief)
variant["score"] = score_result["total"]
variant["score_breakdown"] = score_result["breakdown"]
scored_variants.append(variant)
# Sort by score descending
scored_variants.sort(key=lambda x: x["score"], reverse=True)
return {
"best": scored_variants[0],
"all_ranked": scored_variants,
"total_cost": sum(v["cost"] for v in variants if v["status"] == "success"),
}
Step 4: The scoring system
The agent needs to evaluate variants without human input. We use an LLM as judge — it rates the generated video against the original brief across three weighted dimensions:
# scorer.py
from openai import OpenAI
from config import SANDBASE_API_KEY, SANDBASE_BASE_URL
import json
client = OpenAI(
base_url=SANDBASE_BASE_URL,
api_key=SANDBASE_API_KEY,
)
def score_variant(variant: dict, brief: str, max_retries: int = 2) -> dict:
"""
Score a video variant using LLM-as-judge.
Weights:
- Prompt adherence: 30% (did the motion match the brief?)
- Production quality: 30% (estimated visual quality for model tier)
- Ad effectiveness: 40% (predicted performance as paid creative)
"""
scoring_prompt = f"""You are evaluating an AI-generated video ad variant.
Original brief: "{brief}"
Motion style used: "{variant['style']}"
Model used: {variant['model']}
Video URL: {variant['video_url']}
Score this variant on three dimensions (1-10 each):
1. PROMPT_ADHERENCE: How well does the motion style execution match the brief's requirements?
2. PRODUCTION_QUALITY: Visual quality, motion smoothness, and professional appearance.
3. AD_EFFECTIVENESS: How likely is this to stop a scroll and drive action as a paid ad?
Respond in JSON format:
{{"prompt_adherence": N, "production_quality": N, "ad_effectiveness": N, "reasoning": "one sentence explaining the score"}}
"""
for attempt in range(max_retries + 1):
try:
response = client.chat.completions.create(
model="anthropic/claude-sonnet-5",
messages=[{"role": "user", "content": scoring_prompt}],
response_format={"type": "json_object"},
timeout=30,
)
scores = json.loads(response.choices[0].message.content)
total = (
scores.get("prompt_adherence", 5) * 0.3 +
scores.get("production_quality", 5) * 0.3 +
scores.get("ad_effectiveness", 5) * 0.4
)
return {
"total": round(total, 2),
"breakdown": scores,
}
except (json.JSONDecodeError, KeyError) as e:
if attempt < max_retries:
continue
# Fall back to neutral score on parse failure
return {
"total": 5.0,
"breakdown": {"error": f"Scoring failed after {max_retries + 1} attempts: {str(e)}"},
}
except Exception as e:
if attempt < max_retries:
continue
return {
"total": 5.0,
"breakdown": {"error": f"Scoring error: {str(e)}"},
}
Step 5: Entry point
# run.py
from agent import generate_ad_variants, select_best_variant
import json
import sys
def main():
# Input: product image + creative brief
image_url = "https://example.com/product-wireless-headphones.jpg"
brief = (
"Target: young professionals 25-35. "
"Tone: premium but approachable. "
"Platform: Instagram Reels / TikTok. "
"Product: wireless noise-canceling headphones. "
"Key message: focus in any environment."
)
print("=" * 60)
print("AD-CREATIVE VIDEO AGENT")
print("=" * 60)
print(f"\nBrief: {brief}")
print(f"Image: {image_url}")
print(f"\nGenerating 5 variants...\n")
# Generate variants
variants = generate_ad_variants(image_url, brief, num_variants=5)
# Check if we have any successful variants
successful = [v for v in variants if v["status"] == "success"]
if not successful:
print("\n✗ All generations failed. Check your API key and network.")
sys.exit(1)
# Score and select best
print("\nScoring variants...")
result = select_best_variant(variants, brief)
# Output results
print("\n" + "=" * 60)
print("RESULTS")
print("=" * 60)
print(f"\n🏆 Best variant: {result['best']['model']}")
print(f" Score: {result['best']['score']}/10")
print(f" Video: {result['best']['video_url']}")
print(f" Style: {result['best']['style']}")
print(f" Generation time: {result['best']['generation_time']}s")
print(f"\n💰 Total cost for 5 variants: ${result['total_cost']:.2f}")
print("\n📊 All variants ranked:")
for i, v in enumerate(result['all_ranked'], 1):
status = "✅" if v["status"] == "success" else "❌"
print(f" {i}. [{status}] {v['model']} — "
f"Score: {v.get('score', 0):.1f} — ${v['cost']:.2f}")
return result
if __name__ == "__main__":
main()
Step 6: Running it
export SANDBASE_API_KEY="your-key-here"
python run.py
Expected output:
============================================================
AD-CREATIVE VIDEO AGENT
============================================================
Brief: Target: young professionals 25-35. Tone: premium but approachable...
Image: https://example.com/product-wireless-headphones.jpg
Generating 5 variants...
Generating variant 1/5 [kling-turbo-pro]...
✓ Done in 67.2s
Generating variant 2/5 [kling-omni-pro]...
✓ Done in 82.1s
Generating variant 3/5 [h3-with-audio]...
✓ Done in 78.3s
Generating variant 4/5 [gemini-flash]...
✓ Done in 45.6s
Generating variant 5/5 [kling-turbo-pro]...
⚠ Attempt 1 failed: rate limit exceeded. Retrying in 1s...
✓ Done in 71.4s
Results: 5/5 variants generated successfully
Scoring variants...
============================================================
RESULTS
============================================================
🏆 Best variant: h3-with-audio
Score: 8.2/10
Video: https://api.sandbase.ai/output/abc123.mp4
Style: Slow cinematic reveal from shadow to light, dramatic
Generation time: 78.3s
💰 Total cost for 5 variants: $1.25
📊 All variants ranked:
1. [✅] h3-with-audio — Score: 8.2 — $0.30
2. [✅] kling-omni-pro — Score: 7.9 — $0.35
3. [✅] kling-turbo-pro — Score: 7.4 — $0.20
4. [✅] kling-turbo-pro — Score: 7.1 — $0.20
5. [✅] gemini-flash — Score: 6.8 — $0.20
Cost calculations
I ran this agent for a full month on a real 8-SKU DTC brand account to validate these numbers. Beyond that scale — say 500+ variants/month — I can’t vouch for the reliability data yet.
SandBase billing dashboard: 8 SKUs × 3 platforms × 5 variants = 120 variants over 30 days. Actual spend: $31.40.
Per run (5 variants)
| Component | Cost | Notes |
|---|---|---|
| Video generation (5 variants) | $1.05–$1.25 | Avg $0.25/variant across model mix |
| LLM scoring (5 calls) | ~$0.05 | Claude Sonnet at ~$0.01/scoring call |
| Total per run | ~$1.10–$1.30 |
Monthly projections with real math
DTC brand scenario (12 SKUs, 3 platforms, 5 variants each):
12 SKUs × 3 platforms × 5 variants = 180 variants/month
180 variants × $0.25 avg generation = $45.00 generation
180 variants × $0.015 scoring = $2.70 scoring
─────────────────
Total: $47.70/month
Comparison: A freelance video editor producing 180 variants at $75 avg = $13,500/month. The agent is 283× cheaper.
Scaling economics
| Scale | Variants/month | Agent cost | Freelancer cost | Savings |
|---|---|---|---|---|
| Solo brand (3 SKUs, 1 platform) | 15 | $4.50 | $1,125 | 99.6% |
| DTC brand (12 SKUs, 3 platforms) | 180 | $47.70 | $13,500 | 99.6% |
| Agency (50 clients × 10 variants) | 500 | $132.50 | $37,500 | 99.6% |
The ratio is consistent because the agent’s per-unit cost is fixed. What changes at scale is the operational cost — someone still reviews the top variants and approves them for spend.
Limitations: when this agent fails
Generation failures
| Scenario | Why it fails | Workaround |
|---|---|---|
| Abstract products (SaaS dashboards, financial services) | Image-to-video needs a physical anchor; abstract screens produce glitchy motion | Use text-to-video models or screen recording + motion graphics |
| Lifestyle shots needing actors | No current model handles realistic human motion from a still | Use footage libraries + compositing; agent handles product insert only |
| Videos > 5 seconds | Cost scales linearly; quality degrades on longer generations | Generate 5s hooks; stitch with template mid/end sections |
| Multi-product compositions | Models struggle with spatial relationships between multiple items | Generate per-product, composite in post |
| Text overlays and CTAs | Video models can’t reliably render readable text | Add overlays in post-processing pipeline |
Scoring limitations
The LLM scorer is useful for narrowing candidates, not for final quality assessment:
- ❌ Cannot evaluate: Audio sync quality, frame-to-frame motion smoothness, pixel-level artifacts, actual viewer engagement
- ❌ Cannot compare: Two variants side-by-side (it scores each independently)
- ❌ Biased toward: Verbose/dramatic style descriptions (rates “cinematic” higher even when simple would perform better)
- ✅ Good at: Filtering obviously bad generations, ranking prompt adherence, predicting which style suits a platform
Recommendation: Use the scorer to cut 5 variants down to 2. Human reviews the top 2. This saves 60% of review time while keeping human judgment on the final call.
Extensions
Add platform-specific formatting
PLATFORM_FORMATS = {
"instagram_reels": {"aspect": "9:16", "duration": 5, "max_text": True},
"tiktok": {"aspect": "9:16", "duration": 5, "max_text": True},
"youtube_shorts": {"aspect": "9:16", "duration": 8, "max_text": False},
"twitter": {"aspect": "16:9", "duration": 5, "max_text": True},
"linkedin": {"aspect": "16:9", "duration": 7, "max_text": False},
}
Add result storage for performance tracking
import json
from pathlib import Path
from datetime import datetime
def save_results(result: dict, product_id: str):
"""Save results for later analysis and A/B test correlation."""
output_dir = Path("results") / datetime.now().strftime("%Y-%m")
output_dir.mkdir(parents=True, exist_ok=True)
filepath = output_dir / f"{product_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filepath, "w") as f:
json.dump(result, f, indent=2)
print(f" Results saved: {filepath}")
Add batch processing for full catalogs
def process_catalog(products: list[dict]) -> list[dict]:
"""Process entire product catalog with progress tracking."""
all_results = []
total_cost = 0
for i, product in enumerate(products, 1):
print(f"\n{'='*40}")
print(f"Product {i}/{len(products)}: {product['id']}")
print(f"{'='*40}")
variants = generate_ad_variants(
product["image_url"],
product["brief"],
num_variants=5,
)
best = select_best_variant(variants, product["brief"])
all_results.append({
"product_id": product["id"],
"best_video": best["best"]["video_url"],
"best_score": best["best"]["score"],
"total_cost": best["total_cost"],
})
total_cost += best["total_cost"]
print(f"\n\nCatalog complete: {len(products)} products, "
f"${total_cost:.2f} total cost")
return all_results
FAQ
How long does the full agent take to run?
~4 minutes for 5 variants. Individual video generation takes 45–90 seconds per model. Five sequential generations plus scoring: 4–5 minutes total. The bottleneck is generation time, not scoring (scoring adds <10 seconds total).
Can I use my own product images hosted on S3/CDN?
Yes. Any publicly accessible URL works. The image-to-video models accept standard image formats (JPEG, PNG, WebP). Image resolution above 1024×1024 is downsampled by the model — no benefit to sending 4K source images. Recommended: 1024×1024 or 1080×1920 for vertical shots.
What happens if a model is down or rate-limited?
The agent retries up to 2 times with exponential backoff, then marks that variant as failed and continues. You’ll get 4 scored variants instead of 5. The agent never blocks entirely on a single model failure — it reports partial results. At $0.25/variant, re-running a single failed variant costs less than the time to debug it.
How does this compare to RunwayML or Pika for ad creative?
Different tool, different workflow. Runway/Pika are interactive editors — you generate, preview, adjust, regenerate manually. This agent is a programmatic pipeline: no UI, no manual iteration, pure API. Use Runway when you need creative exploration for a hero ad. Use this agent when you need 180 variants/month and can’t afford to sit in an editor for each one. Cost comparison: Runway at ~$0.50–1.00/generation (Standard plan amortized) vs. this agent at $0.25/variant with scoring included.
Can I swap in different video models without changing the agent code?
Yes — that’s the point of building on SandBase. SandBase normalizes 4 different video generation APIs into one /v1/run contract. To swap Kling for a new model, change one string in config.py. The agent code, polling logic, and scoring system stay identical. When a new model launches with better quality/price, you update config and re-run.
What you’ve built
This agent replaces the $50–200 per-variant manual workflow with a $1.25, 4-minute automated pipeline. The economics: 180 variants/month for $47.70 instead of $13,500.
The human still makes the final go/no-go decision on the top-ranked variants. But the agent handles the expensive middle: generation across multiple models, style variation, and initial quality filtering.
Next steps:
- Add a webhook to trigger generation when new product images are uploaded
- Connect variant scores to actual ad platform performance data for scorer calibration
- Implement concurrent generation (asyncio) to reduce wall time from 4 minutes to ~90 seconds
For model selection guidance, see Best AI Video Generation APIs in 2026. For understanding the cost models behind each API, see Video Generation Cost Model Explained.


