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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add dasein108/slope-studio --skill film-maker --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Film Maker?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/dasein108-film-maker)More formats (shields.io, HTML) on the badges page.
---
name: film-maker
description: >
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.
---
# 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)
```bash
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 `narration`** is NOT dropped: `narrate`
> emits silence for the scene's planned `duration_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 the
> `music` bed 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 |
```bash
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.json` just sets `"aspect": "16:9"` (top level) — stages read it.
- **Captions stay on-frame in any aspect** (`burn_subs` margin + `caption_strip` width/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 put `16:9 widescreen` in the `character`
style string; for `9: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:
```bash
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` (and `studio run --publish-to`) auto-attach `06_thumb.png` — so just run
`studio thumbnail` first; 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.
```bash
# 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 public` to upload at the end.
- `--from-stage` / `--to-stage` to run a slice (e.g. `--from-stage visuals --to-stage stitch`).
- `--run-id NAME` reuses/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:
```bash
RID=$(studio init "octopuses are aliens" --duration 60 | awk '{print $2}')
# (or: studio init ... --run-id myrun → then RID=myrun)
```
### ① script
```bash
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`, **read
> `01_script.json` end-to-end and score it against the four criteria below.** If it fails any,
> **rework the scenario and re-script — do not proceed to `visuals`.**
The critic checklist (all four must pass):
1. **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.)
2. **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.
3. **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.
4. **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):
```bash
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
```bash
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:
```bash
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) · `hailuo` (~$0.045/s) · `kling` (default $0.07/s) · `wan` (~$0.16/s → 6s ≈ $0.80) · `seedance` (~$0.30/s, premium).
- 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": N` on important scenes in `01_script.json`.
### ④ stitch
```bash
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
```bash
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
```bash
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)
```bash
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`](../../../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.** `publish` auto-attaches `06_thumb.png` if present, so make
> it first. (Vertical Shorts don't need one — YouTube auto-picks a Shorts cover.)
```bash
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`. `--hook` is the curiosity line.
- Free (Pillow): `cardgen.thumbnail` cover-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.
```bash
# 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 unlist` sets `entry.unlisted` so the video drops out of all
stats. The raw `publish.set_privacy(...)` helper does **not** — it only calls the YouTube API.
Prefer `studio unlist` whenever 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. Prefer `unlisted` over `private`
for 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`](film-maker-guides.md) —
> the marvelous-effects playbook (parallax depth, the `slice` reveal, 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` · `manim`
- `transition` (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 for `animator:"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 — `parallax` is 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 `parallax` on 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 use `static`,
> `slice`, or `motion-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` (or `studio visuals
> --parallax-plates`) renders a separate `scene_NN_bg.png` plate 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 use `blurred-parallax` for 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 — `pulse` and `kinetic` are 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 banned `zoomin`/`zoomout`).
> - `kinetic` bakes 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 a `card`, which doubles the text). For ordinary
> narrated/story beats use `parallax` / `slice` / `motion-drift*` / `static` instead —
> the spoken line (and captions, if on) already carries the words. Don't sprinkle
> `kinetic` as 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 `kinetic` headline 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 in `ffmpeg.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 `character` style 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>`** to `studio visuals` (Nano-Banana `edit`
> model 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/`](../../../docs/30-animation/effects/README.md), status-tagged:
- ✅ live (set `animator:"…"`); 🧩 author today via `animator:"manim"` + `manim_code`
(morphs, kinetic typography, lightning, leaves — paste-ready snippets in
[`effects/manim-effects.md`](../../../docs/30-animation/effects/manim-effects.md));
- 🧪 recipe-backlog (drop-in ffmpeg filtergraphs in
[`effects/ffmpeg-recipes.md`](../../../docs/30-animation/effects/ffmpeg-recipes.md));
🔬 research-only ([`shaders.md`](../../../docs/30-animation/effects/shaders.md) /
[`particles.md`](../../../docs/30-animation/effects/particles.md)).
- ⚠️ **Never set a 🧪/🔬 name in `animator`** — unknown names fall back to `kenburns`. Reach
those via Manim or an ffmpeg post-pass; to wire one, follow
[effects → adding an effect](../../../docs/30-animation/effects/README.md#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`](../../../docs/30-animation/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-scene `tone` for line-level breath. Full preset:
> [`docs/recipes/poetry.md`](../../../docs/recipes/poetry.md) and guides §13.
Full references (read before authoring scenes or adding presets):
- Schema: [`docs/30-animation/scenario-schema.md`](../../../docs/30-animation/scenario-schema.md)
- Index + decision matrix: [`docs/30-animation/README.md`](../../../docs/30-animation/README.md)
- **Effects index (full catalog):** [`docs/30-animation/effects/README.md`](../../../docs/30-animation/effects/README.md)
- [`transitions.md`](../../../docs/30-animation/transitions.md) · [`motion.md`](../../../docs/30-animation/motion.md) · [`kinetic.md`](../../../docs/30-animation/kinetic.md) · [`parallax.md`](../../../docs/30-animation/parallax.md) · [`slice.md`](../../../docs/30-animation/slice.md) · [`manim.md`](../../../docs/30-animation/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
```bash
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-cost` on `studio run`. Use `kenburns` + `stub` to dry-run wiring for $0.
- A 150s video ≈ 19–30 scenes by default; lower `--duration` while 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/`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!