Film maker
Skill dasein108/slope-studio/.agent-instructions/skills/film-maker
"Automated AI short-video studio: idea → published YouTube Short, $0.06/video"
npx -y skills add dasein108/slope-studio --skill film-makerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Operator playbook for the Slope Studio short-video pipeline. Use when the user wants to produce, generate, render, run, debug, observe, or publish a short vertical video (TikTok / YouTube Shorts) from an idea — or to run/inspect any single stage (script, visuals, clips, stitch, voice, save, publish). Covers the `studio` CLI end-to-end: provider choice, cost control, artifact inspection, and troubleshooting.
SKILL.md
25.8 KB, as published. Nobody here has run it
film-maker — Slope Studio operator guide
You are driving studio, a 7-stage CLI that turns a text idea into a finished
vertical Short. This skill tells you exactly how to run and observe each stage.
0. Environment check (do this first)
cd /Users/dasein/dev/slope-studio
source .venv/bin/activate 2>/dev/null || { uv venv && source .venv/bin/activate && uv pip install -e ".[fal]"; }
command -v ffmpeg ffprobe >/dev/null || echo "MISSING ffmpeg — install it"
studio --help # confirm the CLI is importable
Keys live in .env (gitignored). studio auto-detects them and picks providers;
FAL_KEY unlocks Nano Banana images + AI video. No keys → it still runs on the
free/offline path (stub script, stub images, kenburns video, edge voice).
1. The pipeline at a glance
script → visuals → narrate → clips → stitch → audio → voice → save → [publish]
① ② ②.5 ③ ④ ④.5 ⑤ ⑥ ⑦
Each stage reads/writes runs/<id>/. The manifest project.json records provider,
cost, latency, done-flag per stage. Stages are idempotent (skip if output exists;
--force to redo visuals/clips). audio ④.5 generates sfx + a music bed between
stitch and voice; the voice stage mixes them in (music ducked under narration). Sound
is a cheap, big quality lever — see guides §11.
narrate (stage ②.5, runs when voice is on) TTS-synthesizes each scene first,
so every clip lasts exactly as long as its narration → the final length follows the
speech (~target ±30s), perfectly synced, no truncation. It writes 05_voice/scenes/*.mp3,
05_voice/timing.json, and the aligned 05_voice/captions.srt.
Silent held beats — a scene with empty
narrationis NOT dropped:narrateemits silence for the scene's plannedduration_s(start→end) and holds the clip that long. Use this for wordless cinematic interludes (atmosphere skies, a held glow) that stretch a short text to a longer runtime — keep them moving (parallax/drift) and let themusicbed carry them so they read as breathing room, not dead air.
1.5 Orientation / aspect ratio — vertical OR classic landscape
The pipeline renders whatever aspect the scenario asks for. Set it once and every
stage (visuals, animators, transitions, captions, master) adapts — driven by
studio/canvas.py (set_from_aspect → W×H); helpers default their w,h to that canvas.
aspect | pixels | use |
|---|---|---|
9:16 (default) | 1080×1920 | vertical Shorts / TikTok |
16:9 | 1920×1080 | classic / landscape YouTube |
1:1 · 4:5 · 4:3 · 21:9 | square / portrait / TV / ultrawide | other placements |
studio run "topic" --aspect 16:9 --duration 600 # classic landscape YouTube video
studio init "topic" --aspect 16:9 # then drive stages; aspect lives in 01_script.json
- A hand-authored
01_script.jsonjust sets"aspect": "16:9"(top level) — stages read it. - Captions stay on-frame in any aspect (
burn_subsmargin +caption_stripwidth/height scale with the canvas; landscape wraps wider so text fills the frame, never clipped). - Compose prompts for the orientation: for
16:9, write WIDE horizontal framings (vistas, lateral staging, subject offset left/right) and put16:9 widescreenin thecharacterstyle string; for9:16, vertical framings. - ⚠️ Length note: a 9:16 ≤180s upload is a Short; anything longer (or any 16:9) is a regular YouTube video, not a Short. Long landscape readings are fine — just not "Shorts".
1.6 Preview thumbnail — REQUIRED for long-form (the Kafka style)
Shorts don't need a custom thumbnail (the vertical frame IS the preview). But for every long-form / landscape video (the literary readings), generate a preview BEFORE publishing so the channel keeps one consistent, recognizable look:
studio thumbnail <id> --author "Franz Kafka" --title "Before the Law" --at 8
# omit --title/--author to auto-derive them from 06_final.json metadata / topic
studio publish <id> --target youtube --channel <name> # auto-uploads 06_thumb.png if present
This writes 06_thumb.png (1280×720) in the canonical style — modeled on the Kafka set
(Before the Law · An Imperial Message · First Sorrow): a darkened hero frame, a short HOOK
caption top-left, the AUTHOR in gold, a gold accent bar, then the big white TITLE
bottom-left (cardgen.thumbnail). Keep every long-form preview in this one style so the
series reads as a matched set:
- Same layout every time: AUTHOR (gold) + accent bar + TITLE, with a one-line hook on top.
- Pick a strong, legible hero frame with
--at <seconds>(a clear subject, not a transition). - Title ≤4 words; use the real author's name; let the hook tease, not spoil.
studio publish(andstudio run --publish-to) auto-attach06_thumb.png— so just runstudio thumbnailfirst; if you skip it, YouTube auto-picks a random frame (don't).
2. Produce a whole video (one command) — pick a TIER
--tier free|cheap|balanced|premium sets all providers + video strategy. Override any
stage with a flag. --max-cost (default $3) caps spend; clips trims/aborts to fit.
# cheap: real Nano Banana stills + free Ken-Burns motion (~$0.59 / 150s)
studio run "the chemistry of humor" --duration 150 --tier cheap
# balanced: stills + SMART AI video filling $3 on the best scenes
studio run "the chemistry of humor" --duration 150 --tier balanced --video-model ltx --max-cost 3
# free: fully offline draft, no spend, no AI
studio run "topic" --duration 30 --tier free
# premium: AI on every scene (expensive — uncap with --max-cost 0)
studio run "topic" --duration 60 --tier premium --max-cost 0
# explicit per-scene control overrides the tier:
studio run "topic" --tier cheap --ai-scenes 1,8,15 --video-model kling --max-cost 3
--max-cost N(default 3) — clips estimates per-second cost and aborts/trims before spending.--publish-to youtube --privacy publicto upload at the end.--from-stage/--to-stageto run a slice (e.g.--from-stage visuals --to-stage stitch).--run-id NAMEreuses/resumes a run; without it a timestamped id is created.
Always report the run id + studio status <id> after producing.
3. Run & observe ONE stage at a time
Create a run, then drive stages individually — the normal mode when iterating or debugging:
RID=$(studio init "octopuses are aliens" --duration 60 | awk '{print $2}')
# (or: studio init ... --run-id myrun → then RID=myrun)
① script
studio script $RID --provider stub # or groq|openrouter|ollama|openai|gemini
cat runs/$RID/01_script.json | jq '.scenes | length, .scenes[0]' # inspect scenes/timing
Check: scenes tile [0, duration] with no gaps; each ≤8s; narration present if voice. Timing warnings print to stderr.
①.5 critic — gate the scenario BEFORE spending on visuals/clips
⚠️ RULE — never render visuals/clips on a weak scenario. A wired-but-empty video (vague narration, no real content, no feeling) is the #1 failure. After
script, read01_script.jsonend-to-end and score it against the four criteria below. If it fails any, rework the scenario and re-script — do not proceed tovisuals.
The critic checklist (all four must pass):
- Topic revealed? — Does the piece actually deliver on its title/promise? A viewer who only saw this must come away knowing the thing the title teased, not just hearing it gestured at. (If the title asks a question, the body must ANSWER it.)
- A concrete fact / thought / event, explained? — Is there ≥1 specific, nameable fact, idea, or moment (a number, a name, a date, a mechanism, a turning point) that is stated AND explained — not just asserted? Vague "it changed everything" with no what/why fails.
- Informative AND interesting? — Does it teach something a smart viewer didn't already know, framed with a real curiosity gap? If it's hollow (filler, generic, obvious) → decline with written feedback naming exactly what's missing, then rewrite.
- Emotional payoff? — What does the viewer FEEL (awe, dread, injustice, wonder, the click of a paradox)? Name the intended emotion per act. If the arc is flat / emotionless → decline with feedback and add the missing beat (a stake, a twist, a human cost, a reveal).
Write the verdict explicitly (PASS, or DECLINE + per-criterion feedback). On decline, edit
01_script.json (or re-run studio script with a sharper idea/outline) and re-critique until
it passes. Only then run visuals. Authoring patterns that pass this bar: guides §0.5.
This gate is also wired into the CLI (so headless studio run / the cron autopilot get it too):
studio critic $RID # score the current scenario → 01_critic.json (exit 1 if it fails)
studio run "idea" --critic on # DEFAULT: gate + auto-rework the script up to --critic-retries (2),
# then proceed with the best attempt
studio run "idea" --critic strict # abort the run if it still fails after retries
studio run "idea" --critic off # skip the gate
# --critic-provider <llm> picks the judge LLM (defaults to the script provider)
The CLI judge applies the SAME four criteria; --script-provider stub skips it (wiring-only).
When you're driving by hand, still do the agent critique above — your judgment is sharper than
the in-code judge; the CLI gate is the safety net for unattended runs.
② visuals
studio visuals $RID --provider fal-nanobanana # or stub (offline) | pollinations
ls runs/$RID/02_visuals/ # one PNG per scene
# pass a character reference for consistency:
studio visuals $RID --provider fal-nanobanana --char-ref path/to/face.png --force
Observe: open the PNGs; verify the character looks consistent across scenes.
③ clips ⚠️ COST CENTER — always studio estimate first
AI video is billed per second (kling $0.07/s → 150s = $10.50). Pick a strategy:
studio estimate $RID --budget 3 # preview cost per model + what fits
studio clips $RID --strategy kenburns # FREE pan/zoom on stills ($0)
studio clips $RID --strategy auto --model ltx --max-cost 3 # SMART: fill AI within $3
studio clips $RID --strategy hybrid --ai-scenes 1,8,15 # only these scenes get AI
studio clips $RID --strategy all --model kling --max-cost 0 # AI every scene (0 = no cap)
for f in runs/$RID/03_clips/*.mp4; do ffprobe -v error -show_entries format=duration -of csv=p=0 "$f"; done
--model(all PER-SECOND, verified 2026-06-04):ltx(cheapest$0.04/s @1080p → 5s ≈ $0.20) ·$0.045/s) ·hailuo(kling(default $0.07/s) ·wan($0.16/s → 6s ≈ $0.80) ·$0.30/s, premium).seedance(- The stage aborts/trims before spending if the estimate exceeds
--max-cost. - Each clip normalized to 1080x1920/30fps; fal i2v clamps to 5/10s.
- To bias
auto, set"priority": Non important scenes in01_script.json.
④ stitch
studio stitch $RID --transition fade --transition-s 0.4 # fade|cut|wipeleft|dissolve|...
ffprobe -v error -show_entries format=duration -of csv=p=0 runs/$RID/04_stitched.mp4
⑤ voice
studio voice $RID --provider edge # captions OFF by default (YouTube auto-generates)
# add a music bed: --music beds/lofi.mp3
# only bake in text for muted-autoplay feeds: --captions burn
mpv runs/$RID/05_voice/final.mp4 # or open in QuickTime; check VO sync (+ captions if burned)
Captions are off by default — narrate still writes captions.srt, so upload that as a
YouTube sidecar instead of burning a text wall over the visuals. See guides §6.
⑥ save
studio save $RID
ffprobe -v error -show_entries format=duration:stream=codec_name,width,height \
-of default=noprint_wrappers=1 runs/$RID/06_final.mp4 # expect h264 1080x1920 + aac
⑥.5 metadata + ⑦ publish (optional)
studio metadata $RID # SEO title/desc/tags → 06_final.json (auto-runs before publish)
# YouTube: one-time OAuth setup (client_secret.json) — see docs/40-publishing/youtube.md
uv pip install -e ".[youtube]"
studio publish $RID --target youtube --privacy public # receipt → 07_publish.json
# or in one go: studio run "..." --publish-to youtube --privacy public
# TikTok is audit-gated → tiktok target raises (private-only until audited)
Full setup + quota/constraints: docs/40-publishing/youtube.md.
⑥.7 thumbnail / preview (REQUIRED for landscape uploads)
⚠️ RULE — every normal (non-vertical, i.e.
16:9/landscape) video MUST get a generated preview/thumbnail before YouTube upload. It's a balanced composition with a hook (a curiosity-gap line or the most striking frame), and if the piece has an Author/Name (e.g. Franz Kafka — Before the Law) that author + title MUST appear on the thumbnail.publishauto-attaches06_thumb.pngif present, so make it first. (Vertical Shorts don't need one — YouTube auto-picks a Shorts cover.)
studio thumbnail $RID --at 6 # hero frame @6s + auto title/author → 06_thumb.png
studio thumbnail $RID --title "BEFORE THE LAW" --author "Franz Kafka" \
--hook "a door made only for you" # override any field
# then publish — the thumbnail is set on the upload automatically:
studio publish $RID --target youtube --privacy unlisted --channel <name>
- Title/author auto-derive from the scenario
topic/title(e.g. "… — Franz Kafka", "… by Kafka"); override with--title/--author.--hookis the curiosity line. - Free (Pillow):
cardgen.thumbnailcover-crops the frame, adds a scrim, yellow accent, author-in-yellow + big white title, optional hook. 1280×720. - Custom thumbnails need a verified YouTube channel; if the account isn't verified the upload still succeeds, the thumbnail is just skipped.
⑦.5 retire / reupload an already-published video
Flip an existing upload's privacy, or replace a 16:9 upload with a fresh 9:16 Short.
Needs the youtube.force-ssl OAuth scope on the channel token.
# Just retire (hide) an existing video — no reupload. USE THIS, not the raw helper:
studio unlist <VIDEO_ID> --channel <name> # default privacy=unlisted; --privacy private|public
# ↑ flips YouTube privacy AND sets journal entry.unlisted → recomputes percentiles/bandit/
# outcome over the PUBLIC portfolio (SLO-39). A retired video stops skewing stats.
# Retire old + reupload the master as a 9:16 Short (converts, uploads, patches journal):
python3 scripts/reupload_as_shorts.py \
--run-id <run-id> --old-video-id <VIDEO_ID> --channel <name>
# --old-privacy unlisted|private old video state after reupload (default: unlisted)
# --skip-convert reuse existing 06_final_shorts.mp4
# --skip-upload dry run (retire only, no new upload)
- Journal reflection:
studio unlistsetsentry.unlistedso the video drops out of all stats. The rawpublish.set_privacy(...)helper does not — it only calls the YouTube API. Preferstudio unlistwhenever the video has a journal entry. - Reupload = a new
video_id→ zero views/analytics, cold-start; the journal entry is swapped to the new id and its metrics/snapshots cleared. Preferunlistedoverprivatefor the old one so its comments/analytics stay reachable by direct link. - New Short only counts as a Short if ≤180s (see the ⚠️ length note above).
3.5 Animation & transitions (per-scene, free)
Authoring for QUALITY? Read
film-maker-guides.md— the marvelous-effects playbook (parallax depth, theslicereveal, literal manim moments, caption safety, operator preferences). Use it whenever a video should look great, not just be wired.
Each scene in 01_script.json controls its own look — set by the script author/LLM:
animator:kenburns(default) ·motion-driftright|driftleft|driftup|driftdown|zoomin|zoomout|pulse·kinetic·parallax·blurred-parallax·slice·static·puppet·talkinghead·manimtransition(into the scene):cut(default) ·fade·wipeleft/right/up/down·slide*·circleopen/close·dissolve·radial·zoomin…transition_dur: seconds (default 0.4) ·manim_code: vector animation body foranimator:"manim"
Transitions are overlap-compensated → video stays synced to narration (no drift).
animator applies to FREE scenes; AI scenes use fal-i2v (paid). Mix freely.
⚠️ RULE —
parallaxis for PERSPECTIVE / DEPTH scenery, NOT a big foreground subject. Author it on frames built from receding depth planes — sky & clouds (far), mountains / hills / a city skyline (mid), houses / trees / a road / terrain (near) — so the planes drift at different speeds and read as real 2.5D depth.
- 🚫 Do NOT use
parallaxon a frame that a human, animal, face, or one single object DOMINATES (takes most of the space). A big close subject has no depth to reveal and just floats as a flat cutout — it looks worse, not better. For a big/close subject usestatic,slice, ormotion-drift*instead. A small figure inside a deep landscape is fine — it simply becomes the nearest plane.- It NEVER cuts a subject out of a single still (the old auto rembg-cut tore frames — gone). Default (no plate): a clean full-image lateral pan over the whole vista — safe anywhere. Layered 2.5D (balanced+):
studio run --tier balanced|premium(orstudio visuals --parallax-plates) renders a separatescene_NN_bg.pngplate that drifts behind the held near elements — two DIFFERENT images, real depth, no hole. For the strongest perspective, author 2–3 distinct planes (e.g. cloud layer / mountain layer / foreground) as separate PNGs, or useblurred-parallaxfor a free 2-plane sky-vs-ground depth. Compose the visual prompt as a landscape with clear distance (foreground, midground, horizon), not a portrait of one thing.
⚠️ RULE —
pulseandkineticare OFF by default; use only on explicit need.
motion-pulse(breathing zoom) reads as twitchy/epileptic — never reach for it unless the user explicitly asks (same family as the bannedzoomin/zoomout).kineticbakes a big on-screen HEADLINE — use it only when a scene genuinely needs on-screen words (the hook, a shouted line, a title/outro card), and only over a text-free illustration (never acard, which doubles the text). For ordinary narrated/story beats useparallax/slice/motion-drift*/staticinstead — the spoken line (and captions, if on) already carries the words. Don't sprinklekineticas generic motion.
⚠️ RULE — any zoom effect (
kinetic,pulse, ken-burns,motion-zoom*) must keep the image FILLING the frame at all times — never reveal white / black bars / empty canvas. Bound the zoom so it only ever goes from full image (widest) → zoomed-in (tightest), never wider than the full image:
- widest extreme = the whole image covers the frame (scale-to-cover, zoom = 1.0);
- tightest extreme = a zoomed-in crop; never zoom OUT past 1.0 (that exposes the background = white/empty).
- A
kineticheadline always sits over the cover-filled still, never on a blank card. If a still doesn't match the aspect, it's cover-cropped (increase+crop), not letterboxed (decrease+pad) — padding shows empty bars. (Enforced inffmpeg.kinetic.)
⚠️ RULE — keep RECURRING characters & settings visually CONSISTENT across scenes. Same-style is not enough: the doorkeeper must look like the same person every scene, the gate the same gate, an interior the same room — as in real continuity, not a new face/door each cut. The
characterstyle string only fixes the art style. For subject identity:
- Decide each recurring entity's canonical look once (the doorkeeper's exact face, beard, fur coat; the gate's exact shape/material; the hall's columns) and paste that same detailed description verbatim into every scene that shows it.
- And/or pass a
--char-ref <image.png>tostudio visuals(Nano-Bananaeditmodel holds the reference identity); reuse one scene's good render as the ref.- Author the scenario so each subject has a fixed description block reused across scenes, not a fresh ad-hoc description per scene.
Extra effects catalog (rain, snow, fire, fog, sunrise/sunset, water, film grain,
glitch, god-rays, shape morphs, kinetic typography…): the bullet above lists what's
wired. The full research-backed library is the effects index
docs/30-animation/effects/, status-tagged:
- ✅ live (set
animator:"…"); 🧩 author today viaanimator:"manim"+manim_code(morphs, kinetic typography, lightning, leaves — paste-ready snippets ineffects/manim-effects.md); - 🧪 recipe-backlog (drop-in ffmpeg filtergraphs in
effects/ffmpeg-recipes.md); 🔬 research-only (shaders.md/particles.md). - ⚠️ Never set a 🧪/🔬 name in
animator— unknown names fall back tokenburns. Reach those via Manim or an ffmpeg post-pass; to wire one, follow effects → adding an effect.
Voice & tone: Script voice_name (man|woman|cartoon|narrator) + tone
(neutral|serious|mystical|friendly|sad|excited|poetic); per-scene tone overrides. CLI:
--voice man --tone mystical. edge approximates tone (rate/pitch, free); openai-tts
gives real tone. See voices.md.
Poetry / spoken-verse is a special case where voice + tone IS the art — accents must land on the right words. Use
narrate/voice --provider openai-tts --tone poetic(NOT edge: it only pitch-shifts, can't place accents; openrouter is script-only, no TTS). One scene per line/couplet + per-scenetonefor line-level breath. Full preset:docs/recipes/poetry.mdand guides §13.
Full references (read before authoring scenes or adding presets):
- Schema:
docs/30-animation/scenario-schema.md - Index + decision matrix:
docs/30-animation/README.md - Effects index (full catalog):
docs/30-animation/effects/README.md transitions.md·motion.md·kinetic.md·parallax.md·slice.md·manim.md
Deps: parallax → uv pip install -e ".[parallax]"; manim → ".[manim]". Both
fall back to kenburns (recorded in the manifest note) if a dep/render fails.
4. Observe the whole run
studio status $RID # table: stage / done / provider / cost / latency / note
cat runs/$RID/project.json | jq . # raw manifest incl total_cost_usd
find runs/$RID -type f | sort # every artifact produced
5. Cost control
- Stage 3 (video) dominates cost. Iterate cheaply on ① ② (cents) before rendering ③.
- Nano Banana image = $0.039 (verified). fal i2v per-clip is an estimate in the manifest — confirm against fal.ai's current pricing; refine after a pilot.
- Use
--max-costonstudio run. Usekenburns+stubto dry-run wiring for $0. - A 150s video ≈ 19–30 scenes by default; lower
--durationwhile iterating.
6. Troubleshooting
| Symptom | Cause / fix |
|---|---|
402 Payment Required on visuals | Pollinations paywalls anonymous. Use --image-provider stub or fal-nanobanana. |
No such filter: 'drawtext'/subtitles | This ffmpeg lacks libfreetype AND libass. Caption burn overlays Pillow PNG strips (burn_subs→caption_strip); never switch to subtitles=/drawtext. Stub/card images avoid drawtext too. |
missing FAL_KEY | Add it to .env. Or use free providers (stub/kenburns). |
| script JSON parse error | Free LLMs sometimes break JSON. Retry, or --script-provider stub. |
| clips too short / trimmed | i2v caps at 5/10s; long scenes need splitting in stage 1. |
| output shorter than expected | Fixed via apad in mux; if recurring, check narration vs video length. |
| TikTok publish raises | By design — audit-gated. Default to --privacy self_only or use YouTube. |
| need to unlist/replace a live video | See ⑦.5 — studio unlist <id> --channel <name> (also drops it from journal stats), or scripts/reupload_as_shorts.py to retire + reupload as a Short. Needs youtube.force-ssl scope. |
7. After producing
Always: print the run id, the studio status table (providers + total cost),
the master path (runs/<id>/06_final.mp4), and offer to publish or iterate a stage.
For deeper rationale (model comparisons, pricing, tiers) point to docs/.