diff --git a/.gitignore b/.gitignore index b6a5e0d..ddff2fe 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,9 @@ node_modules/ # Remotion build output and copied project assets (regenerable at render time) remotion-composer/out/ +# Project-local bespoke ("atelier") compositions — hand-authored per video, +# throwaway and regenerable. Keeps the shared src/ free of one-off scenes. +remotion-composer/projects/ remotion-composer/public/* # But keep demo props (shipped with the project for zero-key demos) !remotion-composer/public/demo-props/ diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index 7cdf3a8..a94016b 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -130,6 +130,15 @@ Exception: if only one runtime is available on the machine, the agent proceeds w This rule applies to every pipeline that invokes `video_compose` — not just Wave 1. A pipeline's director skill may recommend a runtime, but that recommendation is input to the conversation with the user, not a decision. +### Composition Authoring Mode — Templated vs Atelier + +Orthogonal to *runtime* is *authoring mode*: **how** the composition is built. Present it as its own proposal decision and log it in `decision_log` (`category: "composition_mode"`). + +- **Templated** — assemble the stock `cut.type` scene-types (`text_card`, `stat_card`, `bar_chart`, …) into the `Explainer`/`CinematicRenderer` compositions. Fast, cheap, reliable — and the reason most videos look alike. Right for batch output, localization variants, quick drafts, and low-stakes internal clips. +- **Atelier** — **hand-author the composition from scratch**: bespoke scenes, a one-off theme, and motion written for this piece, rendered via `composition_mode: "atelier"` (see `video_compose` → `_render_via_atelier`). No reusable creative components; a fresh visual language every time. + +**Default to atelier for hero work** — marketing, launches, brand pieces, any single-deliverable explainer that must impress. The deciding rule: *reuse engine knowledge, never creative components.* In atelier mode the stock scene-type catalog, `hyperframes-registry` blocks, fixtures, and finished components are **off-limits** — they are frozen looks that reintroduce sameness. Before building, route through **`skills/meta/bespoke-composition.md`**, which sequences: art direction (`visual-style`) → motion principles (Disney 12 via `framer-motion`/`lottie-bodymovin`) → engine mechanics (`remotion-best-practices` + the stock components read *only as a mechanics codex*) → render via the atelier path. Close with a **distinctness review**: *could this be any other product's video? does it reuse a look I've made before?* — the inverse of "does it match the reference." Atelier costs more tokens and iteration than templated; say so at proposal so the user opts in knowingly. + ### Escalate Blockers Explicitly When a blocker occurs, the agent must surface it immediately using this structure: @@ -396,6 +405,8 @@ For these requests: See `remotion-composer/SCENE_TYPES.md` for the authoritative list and their cut schemas. Current scene types usable via `cut.type`: `text_card`, `stat_card`, `callout`, `comparison`, `hero_title`, `terminal_scene`, `anime_scene`, `bar_chart`, `line_chart`, `pie_chart`, `kpi_grid`, `progress_bar`. Overlay types include `section_title`, `stat_reveal`, `hero_title`, `provider_chip`. +These stock scene-types are the **templated** path — fast and reliable, but they are why videos look alike. For **hero work, prefer atelier mode** (hand-authored composition) over this catalog; read those types as a *mechanics codex*, not a menu to assemble. See "Composition Authoring Mode" above and `skills/meta/bespoke-composition.md`. + **When Remotion is NOT available** and `render_runtime="remotion"` was NOT locked, `video_compose` may use FFmpeg Ken Burns motion on still images. This still works but produces less engaging visuals. Mention this tradeoff in the proposal. When `render_runtime="remotion"` IS locked and Remotion is unavailable, that's a blocker — escalate, don't silently swap. When `render_runtime="hyperframes"` is locked and HyperFrames is unavailable (Node < 22, missing `ffmpeg`/`npx`, or `hyperframes doctor` reports issues), that's also a blocker. Do not substitute Remotion or FFmpeg without user approval + a logged `render_runtime_selection` decision. diff --git a/schemas/artifacts/decision_log.schema.json b/schemas/artifacts/decision_log.schema.json index 7ae2dc0..c4076db 100644 --- a/schemas/artifacts/decision_log.schema.json +++ b/schemas/artifacts/decision_log.schema.json @@ -29,6 +29,7 @@ "provider_selection", "renderer_family_selection", "render_runtime_selection", + "composition_mode", "playbook_selection", "fallback_decision", "budget_tradeoff", diff --git a/schemas/artifacts/edit_decisions.schema.json b/schemas/artifacts/edit_decisions.schema.json index 8f20506..63eeca4 100644 --- a/schemas/artifacts/edit_decisions.schema.json +++ b/schemas/artifacts/edit_decisions.schema.json @@ -199,6 +199,26 @@ "enum": ["remotion", "hyperframes", "ffmpeg"], "description": "Locked at proposal stage — technical runtime that realizes renderer_family. Edit MUST carry this forward unchanged unless a logged render_runtime_selection decision overrides it." }, + "composition_mode": { + "type": "string", + "enum": ["templated", "atelier"], + "description": "Locked at proposal — HOW the composition is built. Edit MUST carry forward unchanged. 'atelier' routes video_compose to _render_via_atelier (no cut-schema, no stock registry); requires `bespoke` block below." + }, + "bespoke": { + "type": "object", + "description": "Required when composition_mode='atelier'. Contract for the hand-authored, project-local Remotion render. See tools/video/video_compose.py → _render_via_atelier docstring.", + "required": ["entry", "composition_id", "art_direction"], + "properties": { + "entry": { "type": "string", "description": "Path to the project-local Remotion entry .tsx (typically projects//index.tsx). If outside remotion-composer/, auto-staged via directory junction/symlink at render time." }, + "composition_id": { "type": "string", "description": "id registered in that entry's Root" }, + "art_direction": { "type": "string", "description": "Short commitment to a fresh visual language (or a path to art-direction.md). REQUIRED — enforced by _run_atelier_checks." }, + "props_path": { "type": "string", "description": "Absolute path to a props JSON (Remotion --props)" }, + "public_dir": { "type": "string", "description": "Per-project public dir (avoids copying the bloated shared remotion-composer/public/)" }, + "scale": { "type": "number", "minimum": 0.1, "maximum": 1.0 }, + "crf": { "type": "integer", "minimum": 0, "maximum": 51 }, + "concurrency": { "type": "integer", "minimum": 1 } + } + }, "slideshow_risk_score": { "type": "object", "description": "Slideshow risk assessment from lib/slideshow_risk.py", diff --git a/schemas/artifacts/final_review.schema.json b/schemas/artifacts/final_review.schema.json index 709e4ce..4514d55 100644 --- a/schemas/artifacts/final_review.schema.json +++ b/schemas/artifacts/final_review.schema.json @@ -122,13 +122,34 @@ "type": "object", "description": "Optional: transcribe the output and compare to source script", "properties": { - "transcript_matches_script": { "type": "boolean" }, - "word_accuracy": { "type": "number", "minimum": 0, "maximum": 1 }, + "transcript_matches_script": { "type": ["boolean", "null"] }, + "word_accuracy": { "type": ["number", "null"], "minimum": 0, "maximum": 1 }, "issues": { "type": "array", "items": { "type": "string" } } } + }, + "atelier": { + "type": "object", + "description": "Atelier-only doctrine checks (populated when composition_mode='atelier'). Generated by tools/video/video_compose.py → _run_atelier_checks.", + "properties": { + "stock_reuse_detected": { "type": "boolean", "description": "TRUE if the bespoke project imports from the stock creative registry — fails the render." }, + "offending_imports": { + "type": "array", + "description": "Files + import paths that violated the doctrine.", + "items": { + "type": "object", + "properties": { + "file": { "type": "string" }, + "import": { "type": "string" } + } + } + }, + "art_direction_declared": { "type": "boolean" }, + "art_direction": { "type": ["string", "null"] }, + "issues": { "type": "array", "items": { "type": "string" } } + } } }, "additionalProperties": false @@ -140,7 +161,7 @@ }, "recommended_action": { "type": "string", - "enum": ["present_to_user", "re_render", "revise_edit", "revise_assets", "block"], + "enum": ["present_to_user", "re_render", "revise_edit", "revise_assets", "block", "re_author"], "description": "What the agent should do next based on the review findings" }, "metadata": { "type": "object" } diff --git a/schemas/artifacts/proposal_packet.schema.json b/schemas/artifacts/proposal_packet.schema.json index fb97c0f..55a1169 100644 --- a/schemas/artifacts/proposal_packet.schema.json +++ b/schemas/artifacts/proposal_packet.schema.json @@ -155,6 +155,15 @@ "enum": ["remotion", "hyperframes", "ffmpeg"], "description": "Locked at proposal stage — the technical runtime that realizes renderer_family. remotion=React scene components, hyperframes=HTML/CSS/GSAP, ffmpeg=simple concat/trim. Must be explicit and auditable; silent swaps are forbidden." }, + "composition_mode": { + "type": "string", + "enum": ["templated", "atelier"], + "description": "Locked at proposal — HOW the composition is built (orthogonal to render_runtime). templated=assemble stock cut.type scenes (Explainer/CinematicRenderer); atelier=hand-author a project-local composition from scratch with no creative-component reuse. Default atelier for hero work. See AGENT_GUIDE.md → 'Composition Authoring Mode' and skills/meta/bespoke-composition.md. The decision MUST be logged in decision_log with category='composition_mode' and BOTH options presented." + }, + "art_direction": { + "type": "string", + "description": "Required when composition_mode='atelier'. Short note (or path to art-direction.md) committing to a fresh visual language for THIS piece — palette, type, motion, signature device. Per skills/meta/bespoke-composition.md step 1, written down BEFORE authoring scenes." + }, "music_source": { "type": "object", "description": "Resolved music plan from the proposal stage", diff --git a/scripts/scaffold_atelier_project.py b/scripts/scaffold_atelier_project.py new file mode 100644 index 0000000..5cd6812 --- /dev/null +++ b/scripts/scaffold_atelier_project.py @@ -0,0 +1,237 @@ +"""Scaffold a project-local bespoke (atelier) Remotion composition. + +This is the ONLY thing reused across atelier videos: the engine plumbing +(registerRoot/Composition/calculateMetadata boilerplate and the project layout). +**No creative content is ever emitted.** The placeholder scene is deliberately +blank — the agent must hand-author it from scratch per +`skills/meta/bespoke-composition.md`. + +Why this exists: friction is what nudges agents back to the templated path. +Re-deriving the entry/Root/index boilerplate from memory every time is the kind +of friction this removes; emitting a finished scene would reintroduce the +template trap. + +Usage: + python scripts/scaffold_atelier_project.py [--composition-id CamelName] + +Creates: + projects// + index.tsx # registerRoot(Root) + Root.tsx # one Composition + calculateMetadata + Composition.tsx # EMPTY scene with TODO; no imports from src/ + art-direction.md # checklist to fill BEFORE authoring + artifacts/props.template.json + assets/{audio,music,footage}/ + public/ # narration/music get copied here for staticFile() + renders/ + README.md # render command + doctrine pointer +""" +from __future__ import annotations +import argparse +import re +import sys +from pathlib import Path + + +def to_camel(slug: str) -> str: + parts = re.split(r"[\s_\-]+", slug.strip()) + return "".join(p.capitalize() for p in parts if p) or "Bespoke" + + +def write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + print(f" skip (exists) {path.relative_to(Path.cwd())}") + return + path.write_text(content, encoding="utf-8") + print(f" wrote {path.relative_to(Path.cwd())}") + + +def scaffold(slug: str, comp_id: str, root: Path) -> Path: + proj = root / "projects" / slug + (proj / "artifacts").mkdir(parents=True, exist_ok=True) + for sub in ("assets/audio", "assets/music", "assets/footage", "public", "renders"): + (proj / sub).mkdir(parents=True, exist_ok=True) + + # --- index.tsx ------------------------------------------------------- + write(proj / "index.tsx", """\ +import { registerRoot } from "remotion"; +import { Root } from "./Root"; + +registerRoot(Root); +""") + + # --- Root.tsx -------------------------------------------------------- + write(proj / "Root.tsx", f"""\ +import {{ Composition }} from "remotion"; +import {{ Scene, calculateMetadata, SceneProps }} from "./Composition"; + +export const Root: React.FC = () => ( + +); +""") + + # --- Composition.tsx (EMPTY scene; no creative content) -------------- + write(proj / "Composition.tsx", """\ +import React from "react"; +import { AbsoluteFill, CalculateMetadataFunction } from "remotion"; + +// ---------------------------------------------------------------------------- +// ATELIER (BESPOKE) — hand-authored from scratch. +// +// HARD RULES (enforced by tools/video/video_compose.py → _run_atelier_checks +// and skills/meta/reviewer.md → Composition Authoring Mode Review): +// 1. Do NOT import from remotion-composer/src/components, src/Explainer, +// src/CinematicRenderer, src/{TitledVideo,TalkingHead,CollageBurst,...}. +// The stock registry is a mechanics codex, not a parts bin. +// 2. Read skills/meta/bespoke-composition.md FIRST. +// 3. Fill in art-direction.md BEFORE writing the scene. +// +// Engine knowledge you MAY reuse freely (from `remotion`, `@remotion/*`): +// useCurrentFrame, useVideoConfig, spring, interpolate, Sequence, +// AbsoluteFill, Audio, OffthreadVideo, Img, staticFile, random, Easing. +// ---------------------------------------------------------------------------- + +export interface SceneProps { + // TODO: define the props your composition consumes (timing, narration path, + // captions, etc.). Keep this minimal — props are data, not configuration. +} + +export const Scene: React.FC = () => { + // TODO: hand-stitch your scene here. The placeholder below renders solid + // black so the render pipeline can be validated end-to-end before authoring. + // Remove it before committing. + return ; +}; + +export const calculateMetadata: CalculateMetadataFunction = async ({ props }) => ({ + durationInFrames: 30 * 30, // TODO: derive from your props (e.g. total seconds * fps) + fps: 30, + width: 1920, + height: 1080, +}); +""") + + # --- art-direction.md (the divergence engine — fill BEFORE authoring) --- + write(proj / "art-direction.md", f"""\ +# Art Direction — {slug} + +> Fill this in BEFORE writing any scene code. It is the divergence engine: the +> reason this video looks like nothing you've made before. Per +> `skills/meta/bespoke-composition.md` step 1 and reviewer enforcement. + +## Subject +What this video is about, in one sentence. What makes its *visual* problem +unlike any other you've solved. + +## Palette +Three to five concrete hex colors. Why these specifically — what feeling do they +carry, what does the subject demand? + +## Type personality +Two or three concrete fonts (heading, body, accent). Why this voice, not +another? + +## Motion character +How things move and feel — adjectives + concrete physics (spring damping, +durations, easings). "Settling, ink-on-paper" feels different from "snap, neon" +even with the same Remotion primitives. + +## Layout & rhythm +Where the eye goes. Hierarchy. Negative space. Time signature of cuts. + +## Signature device +ONE bespoke visual element that belongs to *this* video and no other. +A hand-drawn diagram. An ink-settle reveal. A coin stamp. A custom transition. +If this slot is blank, the video will look like every other. + +## Anti-references +What this should NOT look like — including any prior video you've made. +Naming them keeps you from drifting into them. +""") + + # --- props template ------------------------------------------------------ + write(proj / "artifacts" / "props.template.json", """\ +{ + "// note": "Fill from your build pipeline. Put narration.mp3 / music.mp3 etc.", + "// note2": "in ./public/ so Remotion staticFile() can resolve them." +} +""") + + # --- README ------------------------------------------------------------- + rel_proj = f"projects/{slug}" + write(proj / "README.md", f"""\ +# {slug} — atelier (bespoke) composition + +Hand-authored Remotion composition. Source of truth lives here under +`{rel_proj}/`; at render time the atelier path auto-stages a junction at +`remotion-composer/projects/{slug}/` so the bundler can resolve `node_modules`. + +## Doctrine +- Read `skills/meta/bespoke-composition.md` first. +- Fill in `art-direction.md` BEFORE authoring scenes. +- No imports from `remotion-composer/src/*` (the tool will fail the render). +- Reuse engine knowledge only; hand-stitch every creative component. + +## Render + +```python +from tools.video.video_compose import VideoCompose +P = r"{rel_proj.replace('/', chr(92)*2)}" # absolute path on your machine +VideoCompose().execute({{ + "operation": "render", + "output_path": P + r"\\renders\\final.mp4", + "edit_decisions": {{ + "render_runtime": "remotion", + "composition_mode": "atelier", + "bespoke": {{ + "entry": P + r"\\index.tsx", + "composition_id": "{comp_id}", + "props_path": P + r"\\artifacts\\props.json", + "public_dir": P + r"\\public", + "art_direction": "", + "scale": 1.0, "crf": 18, "concurrency": 8 + }} + }} +}}) +``` +""") + + return proj + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("slug", help="kebab-case project name, e.g. 'compound-snowball'") + ap.add_argument("--composition-id", help="React composition id (default: CamelCase of slug)") + ap.add_argument("--root", default=".", help="Repo root (default: cwd)") + args = ap.parse_args(argv) + + slug = args.slug.strip().lower() + if not re.match(r"^[a-z][a-z0-9\-]*$", slug): + print(f"error: slug must be kebab-case, got {slug!r}", file=sys.stderr) + return 2 + comp_id = args.composition_id or to_camel(slug) + root = Path(args.root).resolve() + + print(f"Scaffolding atelier project '{slug}' (composition_id={comp_id}) under {root}\n") + proj = scaffold(slug, comp_id, root) + print(f"\nDone. Now:") + print(f" 1. Open {proj.relative_to(root)}/art-direction.md and fill it in.") + print(f" 2. Read skills/meta/bespoke-composition.md.") + print(f" 3. Hand-author Composition.tsx (replace the black placeholder).") + print(f" 4. Render — see {proj.relative_to(root)}/README.md.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/INDEX.md b/skills/INDEX.md index 59ab925..d6dd1b4 100644 --- a/skills/INDEX.md +++ b/skills/INDEX.md @@ -280,6 +280,8 @@ Cross-cutting skills that apply to all pipelines: | Reviewer | `meta/reviewer.md` | Self-review protocol after every stage | | Checkpoint Protocol | `meta/checkpoint-protocol.md` | When/how to checkpoint and request human approval | | Skill Creator | `meta/skill-creator.md` | Dynamically create new skills during pipeline runs | +| Animation Runtime Selector | `meta/animation-runtime-selector.md` | Choose render runtime + animation library per scene | +| Bespoke Composition (Atelier) | `meta/bespoke-composition.md` | Hand-author a composition from scratch (hero work) — no stock scene-types; routes art-direction → motion principles → engine mechanics → atelier render | ## Style Playbooks diff --git a/skills/meta/animation-runtime-selector.md b/skills/meta/animation-runtime-selector.md index 030db9c..5e65d55 100644 --- a/skills/meta/animation-runtime-selector.md +++ b/skills/meta/animation-runtime-selector.md @@ -7,6 +7,13 @@ Meta-skill that answers two questions: Read this before authoring any animated component or composition, and whenever you're choosing `render_runtime` at proposal time. It routes you to the right Layer 3 skill so you don't waste time hand-rolling what a plugin already solves. +> **Authoring mode comes first.** Before runtime or library, decide *how* the composition is +> built: **templated** (assemble stock `cut.type` scenes) vs **atelier** (hand-author from +> scratch). Default to atelier for hero work and follow `skills/meta/bespoke-composition.md`. +> The routing below applies within either mode — but in atelier mode the stock scene-types and +> registry blocks are off-limits; you write your own. "Does a stock cut-type fit?" is **not** a +> valid shortcut for a hero piece. See `AGENT_GUIDE.md` → "Composition Authoring Mode". + ## When to use this skill Apply when: diff --git a/skills/meta/bespoke-composition.md b/skills/meta/bespoke-composition.md new file mode 100644 index 0000000..04b692b --- /dev/null +++ b/skills/meta/bespoke-composition.md @@ -0,0 +1,171 @@ +# Bespoke Composition (Atelier Mode) + +Meta-skill for **hand-authoring a composition from scratch** instead of assembling stock +scene-types. This is the "hand-stitched every time" path: for hero pieces, every pixel of the +look is written fresh so no two videos share a visual language. + +Read this whenever you've chosen **atelier mode** for a piece (see "When to use"). It does not +hand you components — it routes you to the *principles, engine mechanics, and tool wiring* you +need so that what you build is correct, and distinct. + +> The single rule that governs everything below: **reuse engine knowledge, never creative +> components.** How Remotion resolves an asset is engine knowledge — reuse it freely. How a +> previous video looked is a creative decision — never reuse it. + +## When to use this skill (authoring mode is a proposal decision) + +OpenMontage now separates three orthogonal axes, all locked at proposal: + +- `renderer_family` — creative grammar +- `render_runtime` — technical engine (remotion / hyperframes / ffmpeg) +- **`composition_mode`** — **templated** (assemble stock `cut.type` scenes) **vs. atelier** (hand-author) + +Pick **atelier** by default for: marketing, launches, explainers that must impress, brand +pieces, anything single-deliverable where quality is the point. Pick **templated** for: batch +output, localization variants, quick drafts, low-stakes internal clips — places where reliable +sameness is fine and bespoke cost is unjustified. Present the choice to the user at proposal and +log it in `decision_log` (`category: "composition_mode"`), the same way you present runtime. + +If atelier is chosen, the stock scene-type catalog, the `hyperframes-registry` blocks, fixtures, +and any finished component are **off-limits** — they are frozen looks and reintroduce sameness. + +## The construction route + +Author in this order. Each step routes you to existing knowledge — do not skip the first one. + +### 1. Commit to an art direction *for this subject* — the divergence engine +Before writing any component, decide a visual language that fits **this** topic and no other. +Use the **`visual-style`** Layer 3 skill (CREATE mode) to lock: palette, type personality, +motion character, layout system, and **one signature device** unique to this piece. Difference +between videos is guaranteed here — not by withholding components, but by forcing a fresh +direction each time. Write it down (a short `art-direction.md` in the project) and build to it. + +Ask yourself: *what visual metaphor belongs to this subject that I have not used before?* If the +answer resembles a past piece, you haven't found the direction yet. + +### 1.5 Plan each scene as its own composition — no hero-component spine +The most insidious form of templating sneaks back in at the *scene* level: pick one striking +visual (a candle, a browser frame, a score ring), then re-use it every scene with different +text underneath. The piece feels custom because the hero is custom — but every scene is +mechanically the same composition. That's branded slides, not a film. **Don't do that.** + +The signature device named in your art-direction is meant to appear in **one or at most two +beats** — typically the climactic moment — not as the visual scaffolding of every scene. It +earns its weight by being scarce. + +For each scene in the plan, answer concretely *before* writing code: + +- **What is this scene's primary visual subject?** It must be *different* from the previous + scene's. A character. A diagram. A piece of evidence. A landscape. A typographic moment. The + signature device. A void. Each scene's primary subject is its job. +- **Why does this beat exist?** What does it do for the story that no other beat does? If you + can collapse two scenes into one without losing meaning, you should. +- **How does it differ visually from the scene before and after?** Different composition (rule + of thirds vs centered vs split). Different scale (intimate close vs wide field). Different + motion register (still vs busy). Different palette emphasis. Different type treatment. +- **If you removed the signature device from this scene, would the scene still work?** If yes, + the signature device probably doesn't belong in this scene — it's there as filler. Cut it. + +The reviewer enforces this as a "scene_distinctness" check (see +`skills/meta/reviewer.md` → Composition Authoring Mode Review): a recorded inventory of +each scene's primary subject + first frame, and an explicit answer to "do any two scenes +share their primary visual subject?" Yes ⇒ CRITICAL ⇒ re-plan. + +The corollary: the per-scene plan is a *first-class artifact*, not implied. Write it down +(in `art-direction.md` or a sibling `scenes.md`) before authoring `Composition.tsx`. + +### 2. Decide the motion language — principles, not presets +Reach for **principle** skills, never finished animations: +- **`framer-motion`** and **`lottie-bodymovin`** — Disney's 12 principles (anticipation, staging, + follow-through, slow-in/out, arc, timing, exaggeration, appeal). Runtime-agnostic; apply the + *principles* in your own Remotion `spring()`/`interpolate()` code. +- The HyperFrames `references/motion-principles.md` — easing as emotion, timing as weight. + +### 3. Reach for a richer vocabulary only when the concept demands it +Most scenes are Remotion primitives. Escalate when the *idea* needs it, not by default: +`gsap-*` (kinetic typography via SplitText, shape morph via MorphSVG, curved motion via +MotionPath, line-draw via DrawSVG, custom easing), `threejs-*` (3D), `d3-viz` (data-driven +custom charts — build the chart by hand; do **not** drop in the stock `bar_chart`/`line_chart`), +`manim-*` (math), `canvas-procedural-animation` (particles/weather). + +### 4. Get the engine mechanics right — the gotcha codex +This is the only place you "reuse": the engine's solved problems. These are facts about how +Remotion works, not looks. Study `.agents/skills/remotion-best-practices` (19 rule files: +timing, transitions, text-animations, transparent video, fonts, audio, sequencing, measuring +text). You may also read the stock components in `remotion-composer/src/components/` **as a +mechanics codex — to learn idioms, never to import or imitate a look.** + +Recurring mechanics that bite if you don't know them: +- **Determinism**: no `Math.random()` / `Date.now()` per frame — use Remotion `random(seed)` or a + seeded helper, or particles/easing flicker across the render. +- **Per-scene duration**: `useVideoConfig().durationInFrames` returns the *composition* length, not + your scene's. Drive scene-local timing from a passed `durationInFrames`/`Sequence`, not the global. +- **Asset paths**: URLs and `staticFile()` (public/) work everywhere; **`