From lazyreel
Generate multi-shot UGC video ads for DTC brands and creative agencies using Seedance 2.0 via the fal.ai API. Auto-writes UGC dialogue and shot lists from a product image plus an ad angle (unboxing, testimonial, lifestyle demo, problem-solution, before-after, or freeform), fires prompts to Seedance, downloads every clip, and stitches them into one finished vertical ad with ffmpeg. Use whenever the user wants to create a UGC ad, generate UGC video, make a TikTok/Reels/Meta video ad, turn a product photo into video, clone a UGC ad, or make creator-style video content for a DTC brand. Also trigger on phrases like "make me a UGC video ad," "generate a Seedance ad," "UGC from this product," or when the user drops a product image and asks for a video ad.
How this skill is triggered — by the user, by Claude, or both
Slash command
/lazyreel:lazyreel-ugc-ad-generatorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generates multi-shot UGC video ads for DTC brands using Seedance 2.0 on fal.ai. One product image plus one ad angle produces a finished vertical ad with hook, demo, and CTA, ready to upload to Meta, TikTok, or Reels.
Generates multi-shot UGC video ads for DTC brands using Seedance 2.0 on fal.ai. One product image plus one ad angle produces a finished vertical ad with hook, demo, and CTA, ready to upload to Meta, TikTok, or Reels.
Recommended: Claude Code. Runs on the user's machine with full network access. No sandbox, no allowlist, no workarounds needed. Every pattern in this skill works out of the box.
Also works: Claude Cowork, with specific setup (see "Cowork setup" section below). The sandbox blocks several fal.ai hosts by default and requires an allowlist configuration + session restart.
Does not work: Regular Claude chat. No code execution, no file system, no API calls to fal.ai.
brand/brand-dna.md, brand/brand-voice.md, brand/icp-cards.md if they exist (skips silently if not)The user needs ONE of these ready:
.env file in the working directory with FAL_KEY=<their-fal-api-key>, preferredFAL_KEY exported in shellIf neither is present, open .env and guide the user to paste their key there. Never ask them to paste the key into the chat.
Get the key: fal.ai → Settings → API Keys → Create key. Load credits (Seedance 2.0 at 720p is ~$0.30/second, a 15-second ad runs ~$4.50; reference-to-video with video refs is 40% cheaper).
pip install fal-client python-dotenv requests
Check ffmpeg is installed:
ffmpeg -version
If missing:
brew install ffmpegsudo apt install ffmpegThe Cowork sandbox blocks fal.ai's CDN and queue hosts by default. Without this setup, uploads fail, downloads fail, and fal_client.subscribe hangs.
1. Add these domains to your project's allowlist:
Project Settings → Network egress → Additional allowed domains. Add as wildcards:
*.fal.media (for output MP4 downloads)*.fal.ai (for the upload handshake host)*.fal.run (covers fal.run AND queue.fal.run)storage.googleapis.com (some outputs route through GCS)2. Restart the Cowork session.
Allowlist changes DO NOT hot-reload. End the session entirely (don't just close the tab, end it from the menu) and start a new one in the same project. The sandbox proxy loads its ruleset at session start.
3. In Cowork, use the sync endpoint, not fal_client.subscribe.
fal_client.subscribe polls queue.fal.run under the hood. Even with the allowlist configured correctly, the queue endpoint behaves unreliably in Cowork's sandbox. Use a direct POST against the sync endpoint instead:
import requests
import os
response = requests.post(
"https://fal.run/bytedance/seedance-2.0/reference-to-video",
headers={
"Authorization": f"Key {os.getenv('FAL_KEY')}",
"Content-Type": "application/json",
},
json=arguments,
timeout=180, # sync endpoint holds the connection until done
)
result = response.json()
The sync endpoint blocks until the video is ready (~60-120 seconds for a 5-sec clip) and returns the same response shape as the queue endpoint. In Claude Code, fal_client.subscribe works fine, the issue is Cowork-specific.
4. For image upload in Cowork, use the direct HTTP endpoint:
fal_client.upload_file routes through hosts that sometimes aren't covered by the allowlist cleanly. Use the direct storage upload instead:
import requests
with open(image_path, "rb") as f:
r = requests.post(
"https://fal.run/storage/upload",
headers={"Authorization": f"Key {os.getenv('FAL_KEY')}"},
files={"file": f},
timeout=60,
)
image_url = r.json()["url"]
Before you write the shot script, pull what wins so the ad is not a guess. If the LazyReel MCP is connected, call breakout_laws for the first-3-seconds laws and study_videos for the niche's winning format. Then write the shots as a cut sequence, never one held shot:
The fuller payload (the five laws, the negative-prompt library, the niche-to-opening map) is the one the director skills carry: ../lazyreel-ugc-ad-director/references/breakout-prompting.md. When the skills are installed together (the plugin), read it; the essentials are inline above either way.
Collect from the user:
unboxing, 3 shots (reveal → close-up detail → wear/use shot)testimonial, 4 shots (hook/pain → product intro → benefit → CTA)lifestyle-demo, 5 shots (environment → product intro → 2 use-case shots → wrap)problem-solution, 4 shots (problem → reveal product → demo → result/CTA)before-after, 3 shots (before state → product reveal → after state)hold-and-show, 3 shots (hook/pickup → product detail rotation → smile-and-CTA). Safest angle for products Seedance struggles with (see category guidance below)."5" seconds per shot (string, not int). Seedance supports "4"–"15" or "auto".Do NOT ask about every option at once. Ask in a natural conversational way, typically just product image + angle in the first turn, then details once you know the angle.
Some product categories are harder for Seedance 2.0 to render convincingly. If the user asks for a risky angle in one of these categories, suggest a safer alternative instead of blindly generating it:
Makeup and skincare application, Seedance regularly hallucinates:
For cream blush, lipstick, foundation, serums, or anything the creator would physically apply to themselves, steer toward:
hold-and-show, she holds the product, rotates to show the shade/packaging, talks about it. No application.If the user INSISTS on an application angle, write very restrictive prompts: describe the exact starting state ("clean face, no makeup, even skin tone"), specify the exact application location ("applies to the apple of her left cheek only, twice, then sets the product down"), and explicitly negate everything you don't want ("no color on hands, no color on lips, no pre-existing blush, no smudges on clothing"). Expect to regenerate once or twice.
Other risky categories that need similar care:
Look for these files in the working directory:
brand/brand-dna.mdbrand/brand-voice.mdbrand/icp-cards.mdIf any exist, read them silently and use them to inform dialogue voice and ICP targeting. Do NOT mention to the user that you found or read them. If none exist, proceed without, this skill works with just a product image.
Seedance 2.0 has three endpoints. Pick per-shot:
| Shot type | Best mode | Why |
|---|---|---|
| First shot (establishing/product reveal) | image_to_video | Product image IS the first frame, maximum fidelity to the actual product |
| Shots 2+ needing the SAME creator from shot 1 | reference_to_video with video_urls=[shot_1.mp4] + image_urls=[product.jpg] | Passes shot 1's output as @Video1 and the product as @Image1, locking character + product across the ad. ALSO 40% cheaper. |
| Before-after transition in a single shot | image_to_video with end_image_url | Seedance transitions from first frame to last frame, perfect for before/after ads |
| Pure lifestyle shots with no product in frame | text_to_video | No visual reference needed, but use sparingly, less fidelity |
Default pattern for a multi-shot UGC ad:
image_to_video with product image as first framereference_to_video with image_urls=[product] and video_urls=[shot_1_output]This gives you character consistency AND cuts cost by 40% on shots 2+.
For the chosen angle, write a shot list in this structure:
Shot 1/N, [Shot name, e.g., "Hook close-up"]
Mode: image_to_video | reference_to_video | text_to_video
Visual: [what's happening on screen, subject, framing, action, lighting]
Dialogue: [what the creator says, kept tight, ~3 words per second of screen time]
Prompt: [full prompt to send to the model, see references/prompting.md]
References: [which images/videos/audio are passed, and how they're referenced in the prompt using @Image1, @Video1, @Audio1]
Critical dialogue rules:
brand/brand-voice.md was loaded, mirror its sentence patterns and word listCritical reference syntax (reference-to-video only):
@Image1, @Image2, @Video1, @Audio1, etc."The same woman from @Video1 now holds @Image1 up to the camera and says 'Seriously, try this.'"Show the user the shot list + dialogue and ask for approval before firing to the API. This is the step where they either green-light or redirect, it's much cheaper to revise dialogue than regenerate videos.
Create a manifest.json at outputs/<project-name>/manifest.json:
{
"project_name": "mr-paid-social-sneakers",
"output_dir": "outputs/mr-paid-social-sneakers",
"shots": [
{
"shot_number": 1,
"mode": "image_to_video",
"prompt": "...",
"image_url": "https://v3b.fal.media/...",
"duration": "5",
"resolution": "720p",
"aspect_ratio": "9:16",
"generate_audio": true
},
{
"shot_number": 2,
"mode": "reference_to_video",
"prompt": "The same creator from @Video1 now holds @Image1 up... ",
"image_urls": ["https://v3b.fal.media/..."],
"video_urls": ["<will be filled after shot 1 completes>"],
"duration": "5",
"resolution": "720p",
"aspect_ratio": "9:16",
"generate_audio": true
}
]
}
Key details:
duration is a string ("5" not 5), fal.ai rejects integersvideo_urls with shot 1's output URL before running shot 2+. Either do this in two passes, or use a simpler structure where every shot references only the product image (no character continuity, but simpler).Before building the manifest, upload the user's local product image:
import fal_client
url = fal_client.upload_file("/path/to/product.jpg")
print(url) # use this URL in the manifest
Use scripts/generate.py:
python scripts/generate.py --manifest outputs/<project>/manifest.json
The script uses the official fal-client Python package, submits shots in parallel (up to 5 concurrent), and downloads each finished MP4 to outputs/<project>/clips/shot-N.mp4.
Handle failures:
duration as integer instead of string → fix and retryfal_client.upload_file() and retryTrack costs as the script reports them. After all shots complete, tell the user the running total.
Use the script at scripts/stitch.sh:
./scripts/stitch.sh outputs/<project>/clips outputs/<project>/final-ad.mp4
The script handles:
Present three things to the user:
shot-list.md file (so they can iterate next time)outputs/<project-name>/
├── shot-list.md # approved script for this run
├── manifest.json # metadata + prompts + results + costs
├── clips/
│ ├── shot-1.mp4
│ ├── shot-2.mp4
│ └── shot-N.mp4
└── final-ad.mp4 # stitched output
references/fal-api.md, exact fal.ai API schemas for all three Seedance 2.0 endpoints, auth, pricing, file upload, response shape, parallel generation patternreferences/prompting.md, how to write Seedance prompts that actually work (structure, UGC modifiers, @Image1/@Video1 reference syntax, common failure modes)references/angles.md, detailed shot-by-shot breakdowns for each preset anglescripts/stitch.sh, ffmpeg concatenation scriptscripts/generate.py, Python helper using the official fal-client packagegenerate_audio: false and dub in ElevenLabs afterward (audio generation is free anyway, cost is the same either way, so keep it on unless you specifically want a clean silent clip).@Video1 AND the product image as @Image1. Just passing the product image is NOT enough for character consistency.npx claudepluginhub dylanpakd-cyber/lazyreel --plugin lazyreelGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.