diff --git a/.agents/skills/canvas-procedural-animation/SKILL.md b/.agents/skills/canvas-procedural-animation/SKILL.md new file mode 100644 index 00000000..1bf1e6f3 --- /dev/null +++ b/.agents/skills/canvas-procedural-animation/SKILL.md @@ -0,0 +1,47 @@ +--- +name: canvas-procedural-animation +description: Use p5.js/canvas for local procedural character effects: particles, weather, squash/stretch, walk cycles, and environmental motion. +license: MIT +--- + +# Canvas Procedural Animation + +Use this skill when p5.js or Canvas is used for character-supporting motion: +rain, snow, leaves, feathers, ambient particles, squash/stretch, or procedural +walk cycles. + +## Proven Pattern + +p5.js runs setup once and redraws continuously through `draw()`. Keep animation +state deterministic from time/frame values when rendering previews. + +```js +function setup() { + createCanvas(1920, 1080); +} + +function draw() { + const t = millis() / 1000; + clear(); + drawCharacter(width / 2, height / 2 + sin(t * 8) * 8); +} +``` + +## Use For + +- Particle/weather overlays. +- Environmental motion. +- Simple procedural bodies. +- Effects that do not need individually authored SVG parts. + +## Avoid For + +- Complex facial acting where SVG/layered rig parts are easier to inspect. +- Final renders that need exact frame determinism unless the runtime exposes + frame-index control. + +## Sources + +- p5.js `setup()` reference: https://p5js.org/reference/p5/setup/ +- p5.js `draw()` reference: https://p5js.org/reference/p5/draw/ +- p5.js animation examples: https://p5js.org/examples/ diff --git a/.agents/skills/character-animation-qa/SKILL.md b/.agents/skills/character-animation-qa/SKILL.md new file mode 100644 index 00000000..c0daa823 --- /dev/null +++ b/.agents/skills/character-animation-qa/SKILL.md @@ -0,0 +1,43 @@ +--- +name: character-animation-qa +description: Review local character animation with schema checks, Playwright browser previews, frame sampling, and FFmpeg/ffprobe final output checks. +license: MIT +--- + +# Character Animation QA + +Use this skill before presenting a character-animation preview or final render. + +## Review Layers + +1. Schema validation: character design, rig plan, pose library, action timeline. +2. Static asset checks: referenced parts and backgrounds exist. +3. Browser preview: load the preview, capture screenshots, collect console errors. +4. Motion check: compare sampled frames for non-trivial differences. +5. Final MP4 check: ffprobe metadata, duration, resolution, audio, frame samples. +6. Agent visual review: inspect sampled frames for detached limbs, bad layers, + off-frame characters, unreadable expressions, broken text. + +## Playwright Pattern + +```ts +const browser = await chromium.launch(); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); +await page.goto(previewUrl, { waitUntil: "networkidle" }); +await page.screenshot({ path: "preview.png" }); +``` + +## Pass/Revise/Fail + +- `pass`: technical checks pass, acting is readable. +- `revise`: fixable rig/timeline issue. +- `fail`: missing assets, blank render, runtime failure, or wrong runtime. + +## Sources + +- Playwright screenshots: + https://playwright.dev/docs/screenshots +- Playwright page navigation: + https://playwright.dev/docs/api/class-page#page-goto +- FFmpeg/ffprobe should be used for final media probing: + https://ffmpeg.org/ffprobe.html diff --git a/.agents/skills/character-rigging/SKILL.md b/.agents/skills/character-rigging/SKILL.md new file mode 100644 index 00000000..8379639d --- /dev/null +++ b/.agents/skills/character-rigging/SKILL.md @@ -0,0 +1,53 @@ +--- +name: character-rigging +description: Build data-driven 2D character rigs for local animation: parts, pivots, layers, constraints, views, and reusable rig packages. +license: MIT +--- + +# Character Rigging + +Use this skill when building OpenMontage `rig_plan` artifacts or renderer input +for local 2D character animation. + +## Proven Patterns + +- Keep runtime code generic; make each character a data package. +- Split characters into independently transformable parts. +- Define pivots in the same coordinate space as the artwork. +- Store constraints on moving parts to prevent impossible rotations. +- Keep layer order explicit; do not rely on SVG source order after generation. +- Start with one view and add views only when the shot list requires them. + +## Rig Package + +```json +{ + "character_id": "mouse", + "rig_type": "svg_rig", + "parts": [ + { "id": "body", "kind": "torso", "layer": 10 }, + { "id": "head", "kind": "head", "layer": 30, "parent": "body" }, + { "id": "arm_right", "kind": "limb", "layer": 40, "parent": "body" } + ], + "joints": { + "head": { "pivot": [320, 180], "rotation": [-20, 20] }, + "arm_right": { "pivot": [390, 310], "rotation": [-70, 95] } + } +} +``` + +## Quality Checklist + +- Every moving part has a pivot. +- Every child part has a parent where hierarchy matters. +- Mouth shapes are separate assets or separate path groups. +- Eyes and pupils are separate when gaze needs to change. +- Props are separate if the character touches or carries them. + +## Sources + +- SVG transform-origin behavior is browser-defined and can be sensitive to + coordinate space; prefer explicit SVG-coordinate pivots when using GSAP + `svgOrigin`: https://gsap.com/docs/v3/GSAP/CorePlugins/CSS/ +- Remotion animations must be frame-driven and deterministic via current frame: + https://www.remotion.dev/docs/use-current-frame diff --git a/.agents/skills/pose-library-design/SKILL.md b/.agents/skills/pose-library-design/SKILL.md new file mode 100644 index 00000000..c5293126 --- /dev/null +++ b/.agents/skills/pose-library-design/SKILL.md @@ -0,0 +1,58 @@ +--- +name: pose-library-design +description: Design reusable 2D character pose libraries, action cycles, and expression states for data-driven animation. +license: MIT +--- + +# Pose Library Design + +Use this skill when producing `pose_library` artifacts. + +## Pose Categories + +- Neutral: idle, breathe, listening. +- Attention: look_up, look_down, look_left, look_right. +- Emotion: happy, sad, surprised, worried, determined. +- Action: reach, point, hold, jump, flap, walk_contact, walk_passing. +- Mouth: closed, small_o, wide_open, smile, frown, phoneme-ish shapes. + +## Acting Pattern + +Use timed pose sequences: + +```text +anticipation -> action -> hold -> settle +``` + +Do not continuously animate every part. Holds make the acting readable. + +## Pose Data Pattern + +```json +{ + "pose": "surprised", + "parts": { + "head": { "rotation": -6, "y": -4 }, + "pupil_left": { "x": 4, "y": -6 }, + "mouth": "small_o" + }, + "hold_frames": 18, + "transition": "back.out" +} +``` + +## Quality Checklist + +- Required emotions have poses. +- Required actions have poses or cycles. +- Reused cycles have contact and passing poses. +- Poses name only changed parts; defaults come from the rig. + +## Sources + +- GSAP timeline sequencing for readable multi-step poses: + https://gsap.com/docs/v3/GSAP/Timeline/ +- Remotion interpolation for frame-based transitions: + https://www.remotion.dev/docs/interpolate +- Remotion spring for natural motion: + https://www.remotion.dev/docs/spring diff --git a/.agents/skills/svg-character-animation/SKILL.md b/.agents/skills/svg-character-animation/SKILL.md new file mode 100644 index 00000000..3a0ddaa2 --- /dev/null +++ b/.agents/skills/svg-character-animation/SKILL.md @@ -0,0 +1,56 @@ +--- +name: svg-character-animation +description: Animate SVG character rigs with GSAP, CSS transforms, Remotion frame control, and HyperFrames-compatible browser previews. +license: MIT +--- + +# SVG Character Animation + +Use this skill when animating character rigs made from SVG parts. + +## Runtime Rules + +- Animate transforms (`x`, `y`, `scale`, `rotation`) rather than layout. +- Use timelines for multi-part acting beats. +- For SVG elements, use stable pivots (`svgOrigin` or correctly scoped + transform origins). +- In Remotion, do not let GSAP advance with `requestAnimationFrame`; drive a + paused timeline from the current frame. + +## Browser Pattern + +```js +gsap.set("#arm_right", { svgOrigin: "390 310" }); +const tl = gsap.timeline({ defaults: { ease: "power2.inOut" } }); +tl.to("#head", { rotation: -8, duration: 0.2 }) + .to("#arm_right", { rotation: 35, duration: 0.4 }, "<"); +``` + +## Remotion Pattern + +```tsx +const frame = useCurrentFrame(); +const progress = frame / durationInFrames; +timeline.progress(progress); +``` + +## HyperFrames Pattern + +Use HTML/SVG/GSAP components with deterministic timelines and validate via the +HyperFrames CLI before final render. + +## Quality Checklist + +- Parts stay connected at pivots during motion. +- Blinks, gaze, and mouth shapes are separate enough to read. +- Pose holds are long enough to communicate emotion. +- Frame sampling shows meaningful deltas, not frozen animation. + +## Sources + +- GSAP core transform properties and SVG handling: + https://gsap.com/docs/v3/GSAP/CorePlugins/CSS/ +- GSAP timelines and sequencing: + https://gsap.com/docs/v3/GSAP/Timeline/ +- Remotion `useCurrentFrame`: + https://www.remotion.dev/docs/use-current-frame diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index add7be44..cf743976 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -230,6 +230,7 @@ If the folder has tracks, the proposal and asset stages should present them as o | `podcast-repurpose` | Podcast highlights and derivatives | beta | | `cinematic` | Trailer, teaser, and mood-led edits | production | | `animation` | Motion-graphics and animation-first videos | production | +| `character-animation` | Local rigged cartoon characters and reusable character acting | beta | | `hybrid` | Source footage plus support visuals | production | | `avatar-spokesperson` | Presenter-led avatar or lip-sync videos | production | | `localization-dub` | Subtitle, dub, and translated variants | beta | @@ -361,7 +362,7 @@ print('HyperFrames note:', info.get('hyperframes_note')) |--------|----------|----------| | **FFmpeg** | Video-only cuts, concat, trim, subtitle burn | `ffmpeg` binary (always available) | | **Remotion** | React-based composition: still images → animated video, text cards, stat cards, charts, callouts, comparisons, transitions with spring physics, word-level caption burn, TalkingHead avatar | Node.js (`npx`) + `remotion-composer/` + `node_modules` | -| **HyperFrames** | HTML/CSS/GSAP composition: kinetic typography, product promos, launch reels, website-to-video, registry-block-driven scenes | Node.js ≥ 22 + FFmpeg + `npx` (consumed via `npx @hyperframes/cli`) | +| **HyperFrames** | HTML/CSS/GSAP composition: kinetic typography, product promos, launch reels, website-to-video, registry-block-driven scenes, SVG character rigs | Node.js ≥ 22 + FFmpeg + `npx` (consumed via `npx hyperframes`) | `render_runtime` is **locked at proposal** (`proposal_packet.production_plan.render_runtime`) and **carried through edit_decisions unchanged**. `video_compose` routes based on this field; silent runtime swaps are forbidden. If the chosen runtime becomes unavailable at compose time, surface a structured blocker per "Escalate Blockers Explicitly" above. See `skills/core/hyperframes.md` for the Remotion-vs-HyperFrames decision matrix. @@ -448,6 +449,7 @@ Key capability families to look for in the output: - **audio_processing** — Mixing, enhancement (FFmpeg-based, always local). - **analysis** — Transcription, scene detection, frame sampling. - **avatar** — Talking head and lip sync generation. +- **character_animation** — Local character specs, SVG rigs, pose libraries, action timelines, previews, and QA. - **enhancement** — Upscale, background removal, face enhance, color grading. Each tool in the registry declares `best_for`, `install_instructions`, `runtime` (LOCAL, API, LOCAL_GPU, HYBRID), and `status`. Read these fields — do not assume tool strengths from memory. @@ -637,6 +639,7 @@ The `.agents/skills/` directory is large. When you're not coming in through a to |---|---| | **Composition runtime** | `remotion`, `remotion-best-practices`, `synthetic-screen-recording` (fake terminal/UI demos via Remotion TerminalScene) | | **Animation knowledge (generic)** | `gsap-core`, `gsap-timeline`, `gsap-plugins` (SplitText / MorphSVG / DrawSVG / MotionPath / Flip / CustomEase), `gsap-utils`, `gsap-react`, `gsap-performance`, `gsap-scrolltrigger`, `gsap-frameworks`, `framer-motion` (Disney 12 principles), `lottie-bodymovin` (Lottie export) | +| **Character animation** | `character-rigging`, `svg-character-animation`, `pose-library-design`, `canvas-procedural-animation`, `character-animation-qa` | | **Image generation** | `bfl-api`, `flux-best-practices` | | **Video generation** | `seedance-2-0` (preferred premium default — cinematic, trailer, multi-shot, synced audio, lip-sync), `ai-video-gen`, `ltx2` | | **Audio** | `elevenlabs`, `music`, `sound-effects`, `acestep`, `text-to-speech`, `setup-api-key` | diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index f7136413..136856bd 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -72,6 +72,7 @@ Each tool's `agent_skills[]` field bridges Layer 1 → Layer 3. See `skills/INDE | `tools/video/video_stitch.py` | Multi-clip assembly (stitch, spatial, validate, preview) | | `tools/video/video_compose.py` | Runtime-aware composition orchestrator — routes to Remotion / HyperFrames / FFmpeg based on `edit_decisions.render_runtime` | | `tools/video/hyperframes_compose.py` | HyperFrames runtime — workspace materialization, `hyperframes lint`/`validate`/`render`, FFmpeg floor check | +| `tools/character/character_animation.py` | Local character-animation tools — character specs, SVG rig plans, pose libraries, action timelines, HyperFrames packages, and QA reports | | `lib/hyperframes_style_bridge.py` | Playbook → CSS custom properties + `DESIGN.md` bridge for HyperFrames workspaces | | `remotion-composer/src/components/` | 8 Remotion components (TextCard, StatCard, ProgressBar, CalloutBox, ComparisonCard + charts/) | | `.agents/skills/hyperframes*/` | Vendored HyperFrames Layer 3 skills (authoring contract, CLI, registry, website-to-video) | @@ -90,6 +91,7 @@ Each tool's `agent_skills[]` field bridges Layer 1 → Layer 3. See `skills/INDE | `podcast-repurpose` | `pipeline_defs/podcast-repurpose.yaml` | Podcast repurposing | | `cinematic` | `pipeline_defs/cinematic.yaml` | Cinematic edit | | `animation` | `pipeline_defs/animation.yaml` | Animation-first | +| `character-animation` | `pipeline_defs/character-animation.yaml` | Local rigged character animation | | `hybrid` | `pipeline_defs/hybrid.yaml` | Source-plus-support hybrid | | `avatar-spokesperson` | `pipeline_defs/avatar-spokesperson.yaml` | Avatar presenter | | `localization-dub` | `pipeline_defs/localization-dub.yaml` | Localization and dubbing | @@ -115,4 +117,3 @@ Each tool's `agent_skills[]` field bridges Layer 1 → Layer 3. See `skills/INDE 6. Let discovery happen through `tools/tool_registry.py`; do not depend on ad hoc imports 7. Add a JSON schema in `schemas/tools/` if the tool has complex I/O 8. Add tests only after the runtime path is correct - diff --git a/README.md b/README.md index ba33ff3e..0d24608a 100644 --- a/README.md +++ b/README.md @@ -205,15 +205,16 @@ You don't need paid API keys to make real videos. Out of the box, `make setup` g | **Open footage** | Archive.org + NASA + Wikimedia Commons | Free/open archival footage, educational media, and documentary texture | | **Extra stock** | Pexels + Unsplash + Pixabay | Free stock footage/images (developer keys are free to get) | | **Composition (React)** | Remotion | React-based rendering — spring-animated image scenes, text cards, stat cards, charts, TikTok-style word-level captions, TalkingHead | -| **Composition (HTML/GSAP)** | HyperFrames | HTML/CSS/GSAP rendering — kinetic typography, product promos, launch reels, registry blocks, website-to-video | +| **Composition (HTML/GSAP)** | HyperFrames | HTML/CSS/GSAP rendering — kinetic typography, product promos, launch reels, registry blocks, website-to-video, rigged SVG character animation | | **Post-production** | FFmpeg | Encoding, subtitle burn-in, audio mixing, color grading | | **Subtitles** | Built-in | Auto-generated captions with word-level timing | -OpenMontage picks between Remotion and HyperFrames at proposal time (locked as `render_runtime`). Remotion is the default for data-driven explainers and anything using the existing React scene stack; HyperFrames is the default for motion-graphics-heavy briefs that express naturally as HTML + GSAP. See `skills/core/hyperframes.md` for the full decision matrix. +OpenMontage picks between Remotion and HyperFrames at proposal time (locked as `render_runtime`). Remotion is the default for data-driven explainers and anything using the existing React scene stack; HyperFrames is the default for motion-graphics-heavy briefs that express naturally as HTML + GSAP, including the `character-animation` pipeline's SVG/GSAP rig output. See `skills/core/hyperframes.md` for the full decision matrix. **Two free-ish paths:** - **Image-based video:** Piper narrates your script, images provide the visuals, and Remotion animates them into a polished edit. +- **Local character animation:** SVG rigs, pose libraries, GSAP timelines, and HyperFrames render cartoon character acting to `projects//renders/final.mp4`. - **Real-footage video:** the documentary montage pipeline builds a CLIP-searchable corpus from Archive.org, NASA, Wikimedia Commons, and optional free-key sources like Pexels and Unsplash, then cuts together actual motion footage into a finished video. If you want the second one, prompt for a **documentary montage**, **tone poem**, or **stock-footage collage**, and explicitly say **use real footage only**. @@ -374,8 +375,8 @@ OpenMontage/ │ ├── avatar/ # Talking head, lip sync │ └── subtitle/ # SRT/VTT generation │ -├── pipeline_defs/ # 11 YAML pipeline manifests (the agent's playbook) -├── skills/ # 124 Markdown skill files (the agent's knowledge) +├── pipeline_defs/ # YAML pipeline manifests (the agent's playbook) +├── skills/ # Markdown skill files (the agent's knowledge) │ ├── pipelines/ # Per-pipeline stage director skills │ ├── creative/ # Creative technique skills │ ├── core/ # Core tool skills @@ -393,7 +394,7 @@ OpenMontage/ ``` Layer 1: tools/ + pipeline_defs/ "What exists" — executable capabilities + orchestration Layer 2: skills/ "How to use it" — OpenMontage conventions and quality bars -Layer 3: .agents/skills/ "How it works" — 47 external technology knowledge packs +Layer 3: .agents/skills/ "How it works" — external technology knowledge packs ``` Each tool declares which Layer 3 skills it relies on. The agent reads Layer 1 to know what's available, Layer 2 to know how OpenMontage wants it used, and Layer 3 for deep technical knowledge when needed. @@ -509,7 +510,7 @@ Each tool declares which Layer 3 skills it relies on. The agent reads Layer 1 to | Engine | Type | What It Does | |--------|------|-------------| | **Remotion** | Local (Node.js) | React-based programmatic video — spring-animated image scenes, stat reveals, section titles, hero cards, TikTok-style word-by-word captions, scene transitions (fade/slide/wipe/flip), Google Fonts, audio with fade curves, and the TalkingHead avatar composition. **When no video generation providers are configured, the agent generates still images and Remotion turns them into fully animated video.** | -| **HyperFrames** | Local (Node.js ≥ 22) | HTML/CSS/GSAP programmatic video — kinetic typography, product promos, launch reels, custom motion graphics, registry blocks (data charts, grain overlays, shader transitions), website-to-video workflows. Consumed via `npx @hyperframes/cli`; no monorepo checkout needed. | +| **HyperFrames** | Local (Node.js ≥ 22) | HTML/CSS/GSAP programmatic video — kinetic typography, product promos, launch reels, custom motion graphics, registry blocks (data charts, grain overlays, shader transitions), website-to-video workflows, and rigged SVG character animation. Consumed via `npx hyperframes`; no monorepo checkout needed. | | **FFmpeg** | Local | Core video assembly, encoding, subtitle burn, audio muxing, color grading | Runtime is chosen at proposal (`render_runtime`) and locked through `edit_decisions`. Silent swaps between runtimes are a governance violation — see `skills/core/hyperframes.md`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b961062c..e7b26d94 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -53,7 +53,7 @@ OpenMontage/ │ ├── subtitle/ # SRT/VTT generation from timestamps │ └── video/ # 13 video gen providers, composition, stitching, trimming │ -├── pipeline_defs/ # 11 YAML pipeline manifests +├── pipeline_defs/ # YAML pipeline manifests ├── schemas/ # JSON Schema definitions for validation │ ├── artifacts/ # 11 artifact schemas (brief → publish_log) │ ├── checkpoints/ # Checkpoint state schema @@ -65,9 +65,9 @@ OpenMontage/ │ ├── core/ # FFmpeg, Remotion, WhisperX, color grading skills │ ├── creative/ # Video editing, enhancement, data viz, prompt engineering │ ├── meta/ # reviewer, checkpoint-protocol, skill-creator -│ └── pipelines/ # Per-pipeline stage-director skills (10 pipelines) +│ └── pipelines/ # Per-pipeline stage-director skills │ -├── .agents/skills/ # Layer 3: 47 external technology skills (FFmpeg, ElevenLabs, FLUX, etc.) +├── .agents/skills/ # Layer 3: external technology skills (FFmpeg, HyperFrames, GSAP, etc.) ├── styles/ # Visual style playbooks (YAML) + loader ├── remotion-composer/ # Node.js/React — Remotion video composition renderer ├── tests/ # Contract tests, QA integration tests, eval harness @@ -200,13 +200,14 @@ stages: # ... through publish ``` -### Available Pipelines (11) +### Available Pipelines | Pipeline | Category | Description | |----------|----------|-------------| | `animated-explainer` | generated | AI-produced explainer with research, narration, visuals, music | | `animation` | animation | Motion graphics, kinetic typography | | `avatar-spokesperson` | talking_head | Avatar-driven presenter videos | +| `character-animation` | animation | Local rigged cartoon characters with SVG rigs, pose libraries, GSAP timelines, and HyperFrames rendering | | `cinematic` | cinematic | Trailer, teaser, mood-driven edits | | `clip-factory` | custom | Batch short-form clips from long source | | `hybrid` | hybrid | Source footage + AI-generated support visuals | @@ -218,7 +219,7 @@ stages: ### Standard Stage Progression -All production pipelines follow a canonical 8-stage flow: +Most production pipelines follow a canonical 8-stage flow: ``` research → proposal → script → scene_plan → assets → edit → compose → publish @@ -231,6 +232,11 @@ Each stage: 4. Has **review_focus** criteria and **success_criteria** 5. Can require **human approval** before proceeding +Specialized pipelines may insert domain-specific stages. For example, +`character-animation` adds `character_design` and `rig_plan` before +`scene_plan`, then emits a HyperFrames workspace and final deliverable at +`projects//renders/final.mp4`. + --- ## Checkpoint System @@ -439,9 +445,10 @@ A standalone Node.js/React subproject in `remotion-composer/` using [Remotion](h Consumed via `npx hyperframes` (no monorepo checkout needed). Runtime floor: Node.js ≥ 22, FFmpeg, `npx`. -- Handles kinetic typography, product promos, launch reels, website-to-video, registry blocks +- Handles kinetic typography, product promos, launch reels, website-to-video, registry blocks, and SVG/GSAP character rigs - Driver: `tools/video/hyperframes_compose.py` materializes a workspace under `projects//hyperframes/`, then runs `lint → validate → render` - Layer 3 skills vendored at `.agents/skills/hyperframes*/`; Layer 2 guide at `skills/core/hyperframes.md` +- The `character-animation` pipeline uses HyperFrames as the production render package. Browser previews are QA/debug artifacts only, not the render path. ### FFmpeg (fallback / simple cuts) diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index a5cea351..5d84ed47 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -503,6 +503,39 @@ If Remotion is not installed, compositions fall back to FFmpeg Ken Burns pan-and --- +### HyperFrames - HTML/CSS/GSAP Video Composition + +> **GSAP-native local rendering.** HyperFrames is the preferred runtime for motion-graphics-heavy HTML compositions and the `character-animation` pipeline's rigged SVG character acting. + +**Tool:** `hyperframes_compose` directly, or `video_compose` with `edit_decisions.render_runtime="hyperframes"` +**Runtime:** CPU (Node.js >= 22, FFmpeg, and `npx` required) +**Env var:** None + +#### Setup + +```bash +node --version +ffmpeg -version +npx --yes hyperframes doctor +``` + +The CLI is consumed as `npx hyperframes`. Do not use `npx @hyperframes/cli`; that package name is not the OpenMontage runtime path. + +#### What HyperFrames Renders + +| Use case | What it produces | +|----------|------------------| +| **Kinetic typography** | HTML/CSS text animation driven by GSAP timelines | +| **Product / launch videos** | Structured HTML scenes, registry blocks, and transitions | +| **Website-to-video** | Browser-captured site compositions with HyperFrames validation | +| **Character animation** | SVG character rigs, pose/action timelines, and GSAP acting beats rendered to `renders/final.mp4` | + +HyperFrames workspaces live under `projects//hyperframes/`. Final videos still follow the normal OpenMontage convention: `projects//renders/final.mp4`. + +**Cost:** Free. Always local. + +--- + ### Piper TTS — Offline Text-to-Speech > **Completely free, fully offline TTS.** No network required. Good quality for drafts and budget-constrained projects. diff --git a/pipeline_defs/character-animation.yaml b/pipeline_defs/character-animation.yaml new file mode 100644 index 00000000..22213467 --- /dev/null +++ b/pipeline_defs/character-animation.yaml @@ -0,0 +1,302 @@ +name: character-animation +version: "0.1" +description: > + Character animation pipeline for local, reusable cartoon characters. It turns + a script and scene plan into character specs, rig plans, pose libraries, action + timelines, and browser-rendered SVG/Canvas/Remotion/HyperFrames animation. + The pipeline is designed for deterministic local motion, not remote video-gen + replacement. +category: animation +stability: beta +default_checkpoint_policy: guided + +reference_input: + supported: true + analysis_depth: standard + analysis_tools: + - video_analyzer + - transcript_fetcher + - video_downloader + - scene_detect + - frame_sampler + +extensions: + custom_scripts: true + custom_playbooks: true + custom_skills: true + custom_tools: false + +required_skills: + - pipelines/character-animation/executive-producer + - pipelines/character-animation/research-director + - pipelines/character-animation/proposal-director + - pipelines/character-animation/script-director + - pipelines/character-animation/character-design-director + - pipelines/character-animation/rig-plan-director + - pipelines/character-animation/scene-director + - pipelines/character-animation/asset-director + - pipelines/character-animation/edit-director + - pipelines/character-animation/compose-director + - pipelines/character-animation/publish-director + - meta/reviewer + - meta/checkpoint-protocol + - meta/animation-runtime-selector + +orchestration: + mode: executive-producer + skill: pipelines/character-animation/executive-producer + budget_default_usd: 2.00 + max_revisions_per_stage: 3 + max_send_backs: 3 + max_wall_time_minutes: 20 + +compatible_playbooks: + recommended: + - flat-motion-graphics + also_works: + - clean-professional + - minimalist-diagram + custom_allowed: true + +stages: + - name: research + skill: pipelines/character-animation/research-director + produces: + - research_brief + tools_available: [] + checkpoint_required: false + human_approval_default: false + review_focus: + - Reference style and character-animation technique are researched when a reference exists + - At least 3 comparable character-animation examples or techniques are summarized + - Feasibility separates rigged local animation from frame-by-frame traditional animation + success_criteria: + - Schema-valid research_brief + - Technique notes identify reusable rigs, pose libraries, and effects needs + + - name: proposal + skill: pipelines/character-animation/proposal-director + required_artifacts_in: + - research_brief + produces: + - proposal_packet + - decision_log + tools_available: [] + checkpoint_required: true + human_approval_default: true + review_focus: + - Concepts are differentiated and not a carbon copy of any reference + - Character count, action complexity, and reuse strategy are explicit + - Render runtime selection presents Remotion and HyperFrames when both are available + - Motion requirement is preserved; FFmpeg-only fallback is not proposed for character acting + - Sample-first plan is included before full production + success_criteria: + - Schema-valid proposal_packet + - Selected concept includes character_count, rig_complexity, reuse_strategy, and render_runtime + - User approval is recorded before character assets are generated + sub_stages: + - name: sample + description: "10-15 second character-animation proof before full production" + condition: "approved_concept_exists" + human_approval_default: true + tools_available: + - character_spec_generator + - svg_rig_builder + - pose_library_builder + - action_timeline_compiler + - character_rig_renderer + - character_animation_reviewer + - video_compose + review_focus: + - Sample proves character style, rig integrity, pose readability, and motion timing + + - name: script + skill: pipelines/character-animation/script-director + required_artifacts_in: + - proposal_packet + optional_artifacts_in: + - research_brief + - video_analysis_brief + produces: + - script + tools_available: + - transcriber + checkpoint_required: true + human_approval_default: true + review_focus: + - Script is written as action beats, not only narration + - Dialogue/narration architecture is locked + - Every emotional turn can be expressed with poses and actions + success_criteria: + - Schema-valid script + - Character beats and audio architecture are explicit + + - name: character_design + skill: pipelines/character-animation/character-design-director + required_artifacts_in: + - script + - proposal_packet + produces: + - character_design + tools_available: + - character_spec_generator + - image_selector + checkpoint_required: true + human_approval_default: true + review_focus: + - Each character has a distinct role, silhouette, emotional range, and action list + - Character count is realistic for local rigging + - Style anchors are reusable across scenes + success_criteria: + - Schema-valid character_design + - Every main character has required emotions, actions, and views + + - name: rig_plan + skill: pipelines/character-animation/rig-plan-director + required_artifacts_in: + - character_design + produces: + - rig_plan + - pose_library + tools_available: + - svg_rig_builder + - pose_library_builder + checkpoint_required: true + human_approval_default: false + review_focus: + - Parts, pivots, layers, constraints, views, and required poses are complete + - Rig plan avoids per-character code paths; character differences are data + - Known risky motions are surfaced before asset generation + success_criteria: + - Schema-valid rig_plan + - Schema-valid pose_library + - Every required action has at least one pose or action strategy + + - name: scene_plan + skill: pipelines/character-animation/scene-director + required_artifacts_in: + - script + - character_design + - rig_plan + - pose_library + optional_artifacts_in: + - proposal_packet + produces: + - scene_plan + tools_available: [] + checkpoint_required: true + human_approval_default: true + review_focus: + - Scenes use character_scene or animation types with timed actions + - Each scene has characters, actions, camera/framing, background, and effects + - Scene complexity fits the approved sample and budget + success_criteria: + - Schema-valid scene_plan + - Every scene maps to rigged characters and required assets + + - name: assets + skill: pipelines/character-animation/asset-director + required_artifacts_in: + - character_design + - rig_plan + - pose_library + - scene_plan + optional_artifacts_in: + - script + - proposal_packet + produces: + - asset_manifest + tools_available: + - image_selector + - tts_selector + - music_gen + - character_rig_renderer + checkpoint_required: true + human_approval_default: false + review_focus: + - Character parts, backgrounds, props, audio, and effects are linked to scenes + - Layer 3 skills are read for every generation or animation-runtime tool + - Asset provenance and prompts are recorded + - Missing rig assets are blocked, not hidden + success_criteria: + - Schema-valid asset_manifest + - All referenced asset files exist + - Character assets are organized under projects//assets/characters + + - name: edit + skill: pipelines/character-animation/edit-director + required_artifacts_in: + - scene_plan + - asset_manifest + - pose_library + optional_artifacts_in: + - script + - rig_plan + produces: + - edit_decisions + - action_timeline + tools_available: + - action_timeline_compiler + checkpoint_required: true + human_approval_default: false + review_focus: + - Action timeline preserves acting beats and emotional readability + - Pose transitions are timed with holds, anticipation, and follow-through + - render_runtime is carried from proposal unchanged + success_criteria: + - Schema-valid edit_decisions + - Schema-valid action_timeline + - Every scene has timed actions + + - name: compose + skill: pipelines/character-animation/compose-director + required_artifacts_in: + - edit_decisions + - action_timeline + - asset_manifest + optional_artifacts_in: + - rig_plan + - pose_library + - proposal_packet + produces: + - render_report + - final_review + - character_qa_report + tools_available: + - character_rig_renderer + - character_animation_reviewer + - video_compose + - audio_mixer + checkpoint_required: true + human_approval_default: false + review_focus: + - Runtime chosen in proposal is the runtime actually used + - Browser preview passes Playwright/screenshot checks where available + - Final MP4 passes ffprobe, frame sampling, character QA, and visual self-review + - No silent downgrade to still-image motion + success_criteria: + - Schema-valid render_report + - Schema-valid final_review + - Schema-valid character_qa_report + - Output exists and passes technical validation + + - name: publish + skill: pipelines/character-animation/publish-director + required_artifacts_in: + - render_report + - final_review + - character_qa_report + optional_artifacts_in: + - proposal_packet + - script + produces: + - publish_log + tools_available: [] + checkpoint_required: true + human_approval_default: true + review_focus: + - Metadata describes the character-animation style honestly + - Thumbnail/poster frame features the main character and emotional hook + - Any limitations are surfaced in delivery notes + success_criteria: + - Schema-valid publish_log diff --git a/schemas/artifacts/__init__.py b/schemas/artifacts/__init__.py index 319cd6c8..acf15edf 100644 --- a/schemas/artifacts/__init__.py +++ b/schemas/artifacts/__init__.py @@ -15,7 +15,11 @@ ARTIFACT_NAMES = [ "proposal_packet", "brief", "script", + "character_design", + "rig_plan", + "pose_library", "scene_plan", + "action_timeline", "asset_manifest", "edit_decisions", "render_report", @@ -25,6 +29,7 @@ ARTIFACT_NAMES = [ "decision_log", "source_media_review", "final_review", + "character_qa_report", "video_analysis_brief", ] diff --git a/schemas/artifacts/action_timeline.schema.json b/schemas/artifacts/action_timeline.schema.json new file mode 100644 index 00000000..dd5ba7f0 --- /dev/null +++ b/schemas/artifacts/action_timeline.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "openmontage/artifacts/action_timeline", + "title": "Action Timeline", + "description": "Timed character actions and poses compiled for rendering.", + "type": "object", + "required": ["version", "scenes"], + "properties": { + "version": { "type": "string", "const": "1.0" }, + "fps": { "type": "number", "minimum": 1, "default": 30 }, + "scenes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["scene_id", "start_seconds", "end_seconds", "actions"], + "properties": { + "scene_id": { "type": "string" }, + "start_seconds": { "type": "number", "minimum": 0 }, + "end_seconds": { "type": "number", "minimum": 0 }, + "camera": { "type": "object" }, + "background": { "type": "string" }, + "effects": { "type": "array", "items": { "type": "string" } }, + "actions": { + "type": "array", + "items": { + "type": "object", + "required": ["at_seconds", "character_id", "action"], + "properties": { + "at_seconds": { "type": "number", "minimum": 0 }, + "duration_seconds": { "type": "number", "minimum": 0 }, + "character_id": { "type": "string" }, + "action": { "type": "string" }, + "pose": { "type": "string" }, + "emotion": { "type": "string" }, + "target": { "type": "string" }, + "easing": { "type": "string" }, + "notes": { "type": "string" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "metadata": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/schemas/artifacts/character_design.schema.json b/schemas/artifacts/character_design.schema.json new file mode 100644 index 00000000..79a5f3ca --- /dev/null +++ b/schemas/artifacts/character_design.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "openmontage/artifacts/character_design", + "title": "Character Design", + "description": "Character definitions for local rigged animation.", + "type": "object", + "required": ["version", "characters"], + "properties": { + "version": { "type": "string", "const": "1.0" }, + "style": { + "type": "object", + "properties": { + "visual_style": { "type": "string" }, + "palette": { "type": "array", "items": { "type": "string" } }, + "line_style": { "type": "string" }, + "texture": { "type": "string" } + }, + "additionalProperties": false + }, + "characters": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "role", "body_type", "style", "required_emotions", "required_actions"], + "properties": { + "id": { "type": "string" }, + "display_name": { "type": "string" }, + "role": { "type": "string" }, + "body_type": { "type": "string" }, + "style": { "type": "string" }, + "silhouette_notes": { "type": "string" }, + "required_emotions": { "type": "array", "items": { "type": "string" } }, + "required_actions": { "type": "array", "items": { "type": "string" } }, + "required_views": { "type": "array", "items": { "type": "string" } }, + "props": { "type": "array", "items": { "type": "string" } }, + "constraints": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + }, + "metadata": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/schemas/artifacts/character_qa_report.schema.json b/schemas/artifacts/character_qa_report.schema.json new file mode 100644 index 00000000..8daf448b --- /dev/null +++ b/schemas/artifacts/character_qa_report.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "openmontage/artifacts/character_qa_report", + "title": "Character QA Report", + "description": "Structured review of character rig integrity, motion, and render readiness.", + "type": "object", + "required": ["version", "status", "checks"], + "properties": { + "version": { "type": "string", "const": "1.0" }, + "status": { "type": "string", "enum": ["pass", "revise", "fail"] }, + "preview_path": { "type": "string" }, + "checks": { + "type": "object", + "properties": { + "schema_valid": { "type": "boolean" }, + "assets_exist": { "type": "boolean" }, + "pivots_defined": { "type": "boolean" }, + "poses_defined": { "type": "boolean" }, + "actions_timed": { "type": "boolean" }, + "motion_detected": { "type": "boolean" }, + "browser_preview_checked": { "type": "boolean" }, + "frame_samples_checked": { "type": "boolean" } + }, + "additionalProperties": false + }, + "issues": { "type": "array", "items": { "type": "string" } }, + "recommended_action": { + "type": "string", + "enum": ["present_to_user", "fix_rig", "fix_assets", "fix_timeline", "block"] + }, + "metadata": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/schemas/artifacts/pose_library.schema.json b/schemas/artifacts/pose_library.schema.json new file mode 100644 index 00000000..7acef5f2 --- /dev/null +++ b/schemas/artifacts/pose_library.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "openmontage/artifacts/pose_library", + "title": "Pose Library", + "description": "Named reusable poses, expressions, and transition hints for character rigs.", + "type": "object", + "required": ["version", "characters"], + "properties": { + "version": { "type": "string", "const": "1.0" }, + "characters": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["character_id", "poses"], + "properties": { + "character_id": { "type": "string" }, + "poses": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "description": { "type": "string" }, + "parts": { "type": "object" }, + "expression": { "type": "string" }, + "hold_frames": { "type": "integer", "minimum": 0 }, + "transition": { "type": "string" } + }, + "additionalProperties": true + } + }, + "mouth_shapes": { "type": "object" }, + "action_cycles": { "type": "object" } + }, + "additionalProperties": false + } + }, + "metadata": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/schemas/artifacts/rig_plan.schema.json b/schemas/artifacts/rig_plan.schema.json new file mode 100644 index 00000000..605c9a39 --- /dev/null +++ b/schemas/artifacts/rig_plan.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "openmontage/artifacts/rig_plan", + "title": "Rig Plan", + "description": "Rig parts, pivots, layers, constraints, and views for character animation.", + "type": "object", + "required": ["version", "characters"], + "properties": { + "version": { "type": "string", "const": "1.0" }, + "characters": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["character_id", "parts", "joints", "layers", "required_poses"], + "properties": { + "character_id": { "type": "string" }, + "rig_type": { "type": "string", "enum": ["svg_rig", "canvas_procedural", "lottie", "hybrid"], "default": "svg_rig" }, + "parts": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "kind", "layer"], + "properties": { + "id": { "type": "string" }, + "kind": { "type": "string" }, + "layer": { "type": "integer" }, + "asset_path": { "type": "string" }, + "parent": { "type": "string" } + }, + "additionalProperties": false + } + }, + "joints": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["pivot"], + "properties": { + "pivot": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, + "rotation": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, + "scale": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 } + }, + "additionalProperties": false + } + }, + "layers": { "type": "array", "items": { "type": "string" } }, + "views": { "type": "array", "items": { "type": "string" } }, + "required_poses": { "type": "array", "items": { "type": "string" } }, + "required_actions": { "type": "array", "items": { "type": "string" } }, + "risks": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + }, + "metadata": { "type": "object" } + }, + "additionalProperties": false +} diff --git a/schemas/artifacts/scene_plan.schema.json b/schemas/artifacts/scene_plan.schema.json index 2086e569..751665bf 100644 --- a/schemas/artifacts/scene_plan.schema.json +++ b/schemas/artifacts/scene_plan.schema.json @@ -17,7 +17,7 @@ "id": { "type": "string" }, "type": { "type": "string", - "enum": ["talking_head", "broll", "animation", "diagram", "text_card", "transition", "generated", "screen_recording"] + "enum": ["talking_head", "broll", "animation", "character_scene", "diagram", "text_card", "transition", "generated", "screen_recording"] }, "description": { "type": "string" }, "start_seconds": { "type": "number", "minimum": 0 }, @@ -68,6 +68,27 @@ "default": false, "description": "True if this is the visual peak of the video — deserves extra attention" }, + "character_actions": { + "type": "array", + "description": "Character-specific acting beats for rigged character animation scenes", + "items": { + "type": "object", + "required": ["character_id", "action_sequence"], + "properties": { + "character_id": { "type": "string" }, + "emotion": { "type": "string" }, + "action_sequence": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + }, + "dialogue": { "type": "string" }, + "target": { "type": "string" }, + "notes": { "type": "string" } + }, + "additionalProperties": false + } + }, "texture_keywords": { "type": "array", "items": { "type": "string" }, diff --git a/skills/pipelines/character-animation/asset-director.md b/skills/pipelines/character-animation/asset-director.md new file mode 100644 index 00000000..c6161a0b --- /dev/null +++ b/skills/pipelines/character-animation/asset-director.md @@ -0,0 +1,57 @@ +# Asset Director - Character Animation Pipeline + +## Goal + +Produce `asset_manifest` with character parts, backgrounds, props, audio, music, +and preview artifacts. + +## Layer 3 Gate + +Before authoring or generating animation assets, read the relevant Layer 3 skills: + +- `character-rigging` +- `svg-character-animation` +- `pose-library-design` +- `canvas-procedural-animation` when p5/canvas effects are used +- `character-animation-qa` before review +- `gsap-core`, `gsap-timeline`, and `gsap-react` for GSAP/Remotion work +- `remotion` and `remotion-best-practices` for Remotion render work +- `hyperframes` and `hyperframes-cli` for HyperFrames work + +Before image/TTS/music generation, read the tool's `agent_skills` from the +registry. + +## Asset Organization + +Write character assets under: + +```text +projects//assets/characters// +``` + +Use subfolders: + +```text +parts/ +poses/ +previews/ +``` + +Generated backgrounds go under: + +```text +projects//assets/backgrounds/ +``` + +## Process + +1. Produce or source only the parts required by `rig_plan`. +2. Keep each moving part separate. +3. Preserve transparent backgrounds for parts. +4. Record prompts, seeds, providers, and model names. +5. Build a small preview before full asset expansion. + +## Quality Bar + +All parts referenced by `rig_plan` must exist before compose. Missing parts are a +blocker unless the action timeline removes the action requiring them. diff --git a/skills/pipelines/character-animation/character-design-director.md b/skills/pipelines/character-animation/character-design-director.md new file mode 100644 index 00000000..c8f955db --- /dev/null +++ b/skills/pipelines/character-animation/character-design-director.md @@ -0,0 +1,32 @@ +# Character Design Director - Character Animation Pipeline + +## Goal + +Produce `character_design`: a small cast with clear silhouettes, roles, +emotions, actions, and style anchors. + +## Process + +1. List every character with `id`, role, body type, and style. +2. Identify the minimum emotional range needed by the story. +3. Identify the minimum action list needed by the story. +4. Decide required views: front, 3/4, side, back. Keep MVPs to one or two views. +5. Note props attached to characters, such as scarf, feather, bag, glasses. + +## Constraints + +- One or two characters is the MVP sweet spot. +- Animal characters need species-specific parts and action cycles. +- More views multiply asset and pose requirements. +- Do not invent more poses than the approved duration can use. + +## Tool Use + +Use `character_spec_generator` for structured drafts. Use `image_selector` only +after the visual style and character sheet requirements are explicit. Before +using image generation, read the tool's Layer 3 skills from the registry. + +## Quality Bar + +A character design is ready only when an animator or tool can infer what parts, +expressions, and actions must exist. diff --git a/skills/pipelines/character-animation/compose-director.md b/skills/pipelines/character-animation/compose-director.md new file mode 100644 index 00000000..dc8d8821 --- /dev/null +++ b/skills/pipelines/character-animation/compose-director.md @@ -0,0 +1,49 @@ +# Compose Director - Character Animation Pipeline + +## Goal + +Render the approved character animation and prove it was reviewed. + +## Runtime Routing + +First read `edit_decisions.render_runtime`. It must match the runtime locked in +proposal unless a `render_runtime_selection` decision explicitly changed it. + +- `remotion`: stage assets into `remotion-composer/public`, build composition + JSON, render via `video_compose`. +- `hyperframes`: materialize a HyperFrames workspace and let `video_compose` + delegate to `hyperframes_compose`. `hyperframes lint` and `validate` must pass. +- `ffmpeg`: only for post-processing or simple video assembly; not enough for + character acting by itself. + +## Review Workflow + +1. Run `character_rig_renderer` to produce or refresh the HyperFrames package. + The browser preview is a QA/debug artifact only, not the render path. +2. Verify the renderer emitted a HyperFrames `workspace_path`, composition HTML, + `asset_manifest`, and `edit_decisions.render_runtime: "hyperframes"` handoff. +3. Run `character_animation_reviewer` against rig, poses, timeline, and preview. +4. Render final video through `video_compose` using the renderer handoff or the + approved Remotion/HyperFrames package. The deliverable path is + `projects//renders/final.mp4`, matching the standard + OpenMontage project convention. +5. Run standard `final_review`: ffprobe, frame sampling, visual spotcheck, audio + spotcheck, promise preservation. + +## Browser QA + +When Playwright is available: + +- open the preview, +- capture opening/middle/end frames, +- check for console errors, +- verify characters are visible, +- compare frame deltas to ensure motion exists. + +When Playwright is unavailable, use static artifact checks and FFmpeg frame +sampling, and report the reduced confidence. + +## Quality Bar + +Do not present the output as complete when `character_qa_report.status` is +`revise` or `fail`. diff --git a/skills/pipelines/character-animation/edit-director.md b/skills/pipelines/character-animation/edit-director.md new file mode 100644 index 00000000..796b9338 --- /dev/null +++ b/skills/pipelines/character-animation/edit-director.md @@ -0,0 +1,33 @@ +# Edit Director - Character Animation Pipeline + +## Goal + +Produce `edit_decisions` and `action_timeline`. + +## Process + +1. Carry `render_runtime` forward from the approved proposal. +2. Convert scene beats into timed character actions. +3. Add anticipation, hold, action, and follow-through where appropriate. +4. Align mouth/gesture beats to dialogue or music. +5. Keep action density readable. + +## Timing Pattern + +Most acting beats need: + +```text +anticipation -> action -> hold/reaction -> settle +``` + +Do not animate everything continuously. Holds are part of acting. + +## Tool Use + +Use `action_timeline_compiler` for a first pass, then revise the timeline if the +acting or rhythm is weak. + +## Quality Bar + +Every scene has timed actions. Every action maps to a pose, action cycle, or +procedural effect that the renderer can understand. diff --git a/skills/pipelines/character-animation/executive-producer.md b/skills/pipelines/character-animation/executive-producer.md new file mode 100644 index 00000000..5eeaa076 --- /dev/null +++ b/skills/pipelines/character-animation/executive-producer.md @@ -0,0 +1,48 @@ +# Executive Producer - Character Animation Pipeline + +## When To Use + +Use this pipeline when the requested deliverable depends on reusable animated +characters: cartoon shorts, mascot explainers, music-led character scenes, +dialogue between simple characters, or reference-inspired local animation. + +Do not use this pipeline for one-off motion graphics with no acting. Route those +to `animation`. Do not use it for avatar presenter lip-sync. Route that to +`avatar-spokesperson`. + +## Contract + +The pipeline produces local, deterministic character animation. It does not +silently substitute still-image motion for acting. If the character motion cannot +be built with the available rigs, assets, or runtime, surface a blocker. + +## Stage Order + +1. `research` - understand reference, technique, and feasibility. +2. `proposal` - present concepts, runtime options, cost, music plan, sample plan. +3. `script` - write action-friendly beats and dialogue/narration. +4. `character_design` - define characters, silhouettes, emotions, actions. +5. `rig_plan` - define parts, pivots, layers, constraints, poses. +6. `scene_plan` - map story beats to character scenes. +7. `assets` - produce or source character parts, backgrounds, props, audio. +8. `edit` - compile timed action timeline. +9. `compose` - render through the approved runtime and run QA. +10. `publish` - package the final output. + +## Governance Rules + +- Run registry preflight before proposal. +- If both Remotion and HyperFrames are available, present both before locking + `render_runtime`. +- Produce a 10-15 second sample before full asset generation. +- Character differences belong in rig data, not one-off code paths. +- Every generated or runtime-authored asset must list Layer 3 skills read. +- Use `character_animation_reviewer` plus final `final_review` before delivery. + +## Send-Back Triggers + +- `character_design` lacks required actions or emotional range. +- `rig_plan` lacks pivots for moving parts. +- `pose_library` has no readable acting poses. +- `action_timeline` has actions that cannot be rendered by the rig. +- Compose used a runtime not approved in proposal. diff --git a/skills/pipelines/character-animation/proposal-director.md b/skills/pipelines/character-animation/proposal-director.md new file mode 100644 index 00000000..cec0c4cf --- /dev/null +++ b/skills/pipelines/character-animation/proposal-director.md @@ -0,0 +1,58 @@ +# Proposal Director - Character Animation Pipeline + +## Goal + +Present character-animation concepts that are honest about local rigged motion, +reuse, cost, and runtime choice. + +## Required Proposal Elements + +Each option must include: + +- characters and roles, +- visual style, +- action complexity, +- rig reuse strategy, +- sample plan, +- audio architecture, +- music plan, +- render runtime options, +- cost estimate, +- honest limitation note. + +## Runtime Selection + +Read `skills/meta/animation-runtime-selector.md` before recommending a runtime. + +When both Remotion and HyperFrames are available: + +- Remotion: best when the final composition needs deterministic React-rendered + video, captions, audio, scene JSON, and final MP4 governance. +- HyperFrames: best when the character scene is HTML/SVG/GSAP-heavy and benefits + from web-native authoring, lint, validate, and registry blocks. +- FFmpeg: post-processing only. Do not pick FFmpeg as the primary runtime for + character acting. + +Wait for user approval before locking `render_runtime`. + +## Sample-First Rule + +Before full production, propose a 10-15 second sample containing: + +- one main character, +- one expression change, +- one body action, +- one camera/background treatment, +- one audio/music cue if relevant. + +Do not batch-generate all assets until this sample is approved. + +## Cost Honesty + +Local rigging is cheap at render time but expensive in authoring complexity. +Report the difference: + +- asset generation cost, +- TTS/music cost, +- local render cost, +- manual complexity risk. diff --git a/skills/pipelines/character-animation/publish-director.md b/skills/pipelines/character-animation/publish-director.md new file mode 100644 index 00000000..6ddf2346 --- /dev/null +++ b/skills/pipelines/character-animation/publish-director.md @@ -0,0 +1,26 @@ +# Publish Director - Character Animation Pipeline + +## Goal + +Package the final character-animation deliverable with honest metadata and a +strong character-forward thumbnail concept. + +## Requirements + +- Mention the actual visual treatment: local rigged character animation, + procedural effects, Remotion/HyperFrames render, or mixed. +- Pick a poster frame where the main character's emotion is readable. +- If the output is a sample, label it as a sample. +- If the final is inspired by a reference, describe the inspiration without + claiming duplication. + +## Output + +Produce `publish_log` with: + +- final video path, +- thumbnail/poster-frame notes, +- title ideas, +- description, +- platform-specific export notes, +- limitations or follow-up recommendations. diff --git a/skills/pipelines/character-animation/research-director.md b/skills/pipelines/character-animation/research-director.md new file mode 100644 index 00000000..dcf9954c --- /dev/null +++ b/skills/pipelines/character-animation/research-director.md @@ -0,0 +1,45 @@ +# Research Director - Character Animation Pipeline + +## Goal + +Ground the character-animation plan in real references and current technique. +For reference videos, start from `video_analysis_brief`: content, pacing, motion +classification, keyframes, color, and production complexity. + +## Process + +1. Identify what the reference actually uses: + - rigged local animation, + - frame-by-frame traditional animation, + - video generation, + - still-image motion, + - mixed techniques. +2. Research 3-5 relevant examples or techniques. +3. Separate what the pipeline can reproduce locally from what requires manual + illustration, video generation, or a larger asset library. +4. Record reusable animation primitives: + - walk cycle, + - blink, + - head turn, + - reach, + - wing flap, + - squash/stretch, + - camera pan/parallax, + - particles/weather. + +## Output Guidance + +The `research_brief` should include: + +- `character_animation_fit`: high/medium/low, +- `reference_motion_type`, +- `required_character_actions`, +- `rig_complexity`, +- `manual_asset_risks`, +- `local_runtime_candidates`. + +## Quality Bar + +Be explicit when a reference is hand-drawn or frame-by-frame. The user can still +choose an inspired local rigged style, but the proposal must not imply exact +traditional-animation quality from an automatic rig. diff --git a/skills/pipelines/character-animation/rig-plan-director.md b/skills/pipelines/character-animation/rig-plan-director.md new file mode 100644 index 00000000..a7fe4e89 --- /dev/null +++ b/skills/pipelines/character-animation/rig-plan-director.md @@ -0,0 +1,40 @@ +# Rig Plan Director - Character Animation Pipeline + +## Goal + +Produce `rig_plan` and `pose_library` from `character_design`. + +## Process + +1. Convert each character into rig parts: + - body, + - head, + - eyes/pupils, + - brows, + - mouth shapes, + - limbs/wings, + - tail/accessories, + - props. +2. Define pivots for every moving part. +3. Define layer order. +4. Define constraints so limbs do not rotate into impossible positions. +5. Define named poses for the approved scenes. +6. Define action cycles only when reused at least twice or central to the story. + +## Runtime Pattern + +Character differences are data. The renderer should not need one-off code for a +mouse versus a bird. A bird may have `wing_left`; a mouse may have `tail`, but +both feed the same pose interpolation and timeline compiler. + +## Quality Checks + +- Every moving part has a pivot. +- Every required action has poses or a procedural strategy. +- Every pose names the changed parts. +- Risky actions are called out, not hidden. + +## Tool Use + +Use `svg_rig_builder` to draft rig data and `pose_library_builder` to draft the +initial pose library. The agent may revise their output before checkpointing. diff --git a/skills/pipelines/character-animation/scene-director.md b/skills/pipelines/character-animation/scene-director.md new file mode 100644 index 00000000..1bd57339 --- /dev/null +++ b/skills/pipelines/character-animation/scene-director.md @@ -0,0 +1,37 @@ +# Scene Director - Character Animation Pipeline + +## Goal + +Produce a `scene_plan` where each scene is feasible for rigged character +animation. + +## Scene Planning Fields + +For each scene, include: + +- character IDs, +- emotional beat, +- action sequence, +- camera/framing, +- background, +- props, +- effects, +- required assets, +- transition notes. + +Use `type: "character_scene"` for rigged character acting scenes. Store +character-specific detail in `character_actions`; do not put per-scene acting +data in arbitrary metadata because the shared `scene_plan` schema rejects +unknown per-scene fields. + +## Complexity Budget + +Prefer fewer, stronger shots: + +- one establish, +- one action beat, +- one reaction beat, +- one resolution beat. + +Avoid scenes that require many unique views or complex physical contact unless +the user approved that complexity. diff --git a/skills/pipelines/character-animation/script-director.md b/skills/pipelines/character-animation/script-director.md new file mode 100644 index 00000000..c1c310ed --- /dev/null +++ b/skills/pipelines/character-animation/script-director.md @@ -0,0 +1,37 @@ +# Script Director - Character Animation Pipeline + +## Goal + +Write scripts as performable animation beats, not just narration. + +## Process + +1. Lock audio architecture: + - music-only, + - narrator, + - character dialogue, + - narrator plus character sounds/dialogue. +2. Break the story into beats that can be acted with poses. +3. For each beat, state what changes visually: + - emotion, + - gaze, + - body pose, + - prop interaction, + - camera, + - environment. + +## Writing Rules + +- Prefer short visual beats with readable holds. +- Avoid action that needs many unique hand-drawn poses unless approved. +- Dialogue should be short enough for mouth-shape approximation. +- Silent/music-led scenes need stronger physical acting notes. + +## Output Notes + +In the `script` artifact metadata, include: + +- `audio_architecture`, +- `character_beats`, +- `required_emotions`, +- `required_actions`. diff --git a/tests/contracts/test_character_animation_pipeline.py b/tests/contracts/test_character_animation_pipeline.py new file mode 100644 index 00000000..542df066 --- /dev/null +++ b/tests/contracts/test_character_animation_pipeline.py @@ -0,0 +1,276 @@ +"""Contract tests for the local character-animation pipeline.""" + +import sys +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +from lib.pipeline_loader import get_required_tools, get_stage_order, load_pipeline +from schemas.artifacts import ARTIFACT_NAMES, validate_artifact +from tools.character.character_animation import ( + ActionTimelineCompiler, + CharacterAnimationReviewer, + CharacterRigRenderer, + CharacterSpecGenerator, + PoseLibraryBuilder, + SvgRigBuilder, +) +from tools.tool_registry import registry +from tools.video.hyperframes_compose import HyperFramesCompose +from tools.video.video_compose import VideoCompose + + +def test_character_animation_manifest_contract(): + manifest = load_pipeline("character-animation") + + assert manifest["name"] == "character-animation" + assert get_stage_order(manifest) == [ + "research", + "proposal", + "script", + "character_design", + "rig_plan", + "scene_plan", + "assets", + "edit", + "compose", + "publish", + ] + assert { + "character_spec_generator", + "svg_rig_builder", + "pose_library_builder", + "action_timeline_compiler", + "character_rig_renderer", + "character_animation_reviewer", + }.issubset(set(get_required_tools(manifest))) + + +def test_character_artifacts_are_registered(): + assert { + "character_design", + "rig_plan", + "pose_library", + "action_timeline", + "character_qa_report", + }.issubset(set(ARTIFACT_NAMES)) + + +def test_character_tools_discover_in_registry(): + registry.discover() + + names = {tool.name for tool in registry.get_by_capability("character_animation")} + assert { + "character_spec_generator", + "svg_rig_builder", + "pose_library_builder", + "action_timeline_compiler", + "character_rig_renderer", + "character_animation_reviewer", + }.issubset(names) + + +def test_character_animation_smoke_flow(tmp_path): + character_result = CharacterSpecGenerator().execute( + { + "characters": [ + { + "id": "mouse_lead", + "role": "curious lead", + "body_type": "mouse with tail", + "required_actions": ["idle", "gesture", "tail_swish"], + }, + { + "id": "bird_friend", + "role": "expressive sidekick", + "body_type": "round bird", + "required_actions": ["idle", "wing_flap", "react"], + }, + ], + "output_path": str(tmp_path / "character_design.json"), + } + ) + assert character_result.success + character_design = character_result.data["character_design"] + validate_artifact("character_design", character_design) + + rig_result = SvgRigBuilder().execute( + { + "character_design": character_design, + "output_path": str(tmp_path / "rig_plan.json"), + } + ) + assert rig_result.success + rig_plan = rig_result.data["rig_plan"] + validate_artifact("rig_plan", rig_plan) + + pose_result = PoseLibraryBuilder().execute( + {"rig_plan": rig_plan, "output_path": str(tmp_path / "pose_library.json")} + ) + assert pose_result.success + pose_library = pose_result.data["pose_library"] + validate_artifact("pose_library", pose_library) + + scene_plan = { + "version": "1.0", + "scenes": [ + { + "id": "scene-1", + "type": "character_scene", + "start_seconds": 0, + "end_seconds": 4, + "description": "The mouse discovers a glowing seed while the bird reacts.", + "hero_moment": True, + "character_actions": [ + { + "character_id": "mouse_lead", + "emotion": "surprised", + "action_sequence": ["anticipate", "perform", "settle"], + }, + { + "character_id": "bird_friend", + "emotion": "surprised", + "action_sequence": ["react", "follow", "settle"], + }, + ], + } + ], + } + validate_artifact("scene_plan", scene_plan) + timeline_result = ActionTimelineCompiler().execute( + { + "scene_plan": scene_plan, + "character_ids": ["mouse_lead", "bird_friend"], + "output_path": str(tmp_path / "action_timeline.json"), + } + ) + assert timeline_result.success + action_timeline = timeline_result.data["action_timeline"] + validate_artifact("action_timeline", action_timeline) + assert {action["character_id"] for action in action_timeline["scenes"][0]["actions"]} == { + "mouse_lead", + "bird_friend", + } + + preview_path = tmp_path / "preview.html" + render_result = CharacterRigRenderer().execute( + { + "rig_plan": rig_plan, + "pose_library": pose_library, + "action_timeline": action_timeline, + "output_path": str(preview_path), + } + ) + assert render_result.success + assert preview_path.exists() + preview_html = preview_path.read_text(encoding="utf-8") + assert "character_mouse-lead" in preview_html + assert "character_bird-friend" in preview_html + + qa_result = CharacterAnimationReviewer().execute( + { + "rig_plan": rig_plan, + "pose_library": pose_library, + "action_timeline": action_timeline, + "preview_path": str(preview_path), + "output_path": str(tmp_path / "character_qa_report.json"), + } + ) + assert qa_result.success + qa_report = qa_result.data["character_qa_report"] + validate_artifact("character_qa_report", qa_report) + assert qa_report["status"] == "pass" + assert qa_report["checks"]["schema_valid"] is True + + +def test_character_style_is_normalized_for_schema(tmp_path): + result = CharacterSpecGenerator().execute( + { + "characters": [{"id": "style_test", "role": "lead", "body_type": "round"}], + "style": { + "name": "flat-motion-graphics", + "palette": ["#ff8f68", "#75b8ff"], + "unexpected": "should not leak into artifact", + }, + "output_path": str(tmp_path / "character_design.json"), + } + ) + + assert result.success + character_design = result.data["character_design"] + validate_artifact("character_design", character_design) + assert character_design["style"] == { + "visual_style": "flat-motion-graphics", + "palette": ["#ff8f68", "#75b8ff"], + } + + +def test_character_renderer_can_handoff_to_video_compose(tmp_path): + hyperframes = HyperFramesCompose() + runtime = hyperframes._runtime_check() + if not runtime["runtime_available"]: + pytest.skip("HyperFrames runtime is required for character render handoff") + + character_design = CharacterSpecGenerator().execute( + {"characters": [{"id": "mouse_lead", "role": "lead", "body_type": "mouse with tail"}]} + ).data["character_design"] + rig_plan = SvgRigBuilder().execute({"character_design": character_design}).data["rig_plan"] + pose_library = PoseLibraryBuilder().execute({"rig_plan": rig_plan}).data["pose_library"] + scene_plan = { + "version": "1.0", + "scenes": [ + { + "id": "scene-1", + "type": "character_scene", + "description": "Mouse reacts to a tiny surprise.", + "start_seconds": 0, + "end_seconds": 1, + "character_actions": [ + { + "character_id": "mouse_lead", + "emotion": "surprised", + "action_sequence": ["anticipate", "perform", "settle"], + } + ], + } + ], + } + validate_artifact("scene_plan", scene_plan) + action_timeline = ActionTimelineCompiler().execute( + {"scene_plan": scene_plan, "character_ids": ["mouse_lead"]} + ).data["action_timeline"] + + render_result = CharacterRigRenderer().execute( + { + "rig_plan": rig_plan, + "pose_library": pose_library, + "action_timeline": action_timeline, + "output_path": str(tmp_path / "preview.html"), + "workspace_path": str(tmp_path / "hyperframes"), + } + ) + assert render_result.success + validate_artifact("asset_manifest", render_result.data["asset_manifest"]) + validate_artifact("edit_decisions", render_result.data["edit_decisions"]) + assert render_result.data["edit_decisions"]["render_runtime"] == "hyperframes" + assert Path(render_result.data["composition_path"]).exists() + + output_path = tmp_path / "renders" / "final.mp4" + compose_result = VideoCompose().execute( + { + "operation": "render", + "asset_manifest": render_result.data["asset_manifest"], + "edit_decisions": render_result.data["edit_decisions"], + "workspace_path": render_result.data["hyperframes_workspace"], + "output_path": str(output_path), + "skip_contrast": True, + "quality": "draft", + "fps": 24, + } + ) + + assert compose_result.success, compose_result.error + assert output_path.exists() diff --git a/tools/character/__init__.py b/tools/character/__init__.py new file mode 100644 index 00000000..edf80e61 --- /dev/null +++ b/tools/character/__init__.py @@ -0,0 +1,2 @@ +"""Character animation tools.""" + diff --git a/tools/character/character_animation.py b/tools/character/character_animation.py new file mode 100644 index 00000000..92bd1c7f --- /dev/null +++ b/tools/character/character_animation.py @@ -0,0 +1,896 @@ +"""Local character-animation contract tools. + +These tools provide deterministic artifact generation and validation for the +character-animation pipeline. They intentionally keep creative orchestration in +skills and manifests; Python only creates structured artifacts and lightweight +preview/review outputs. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +import time +from pathlib import Path +from typing import Any + +from schemas.artifacts import validate_artifact +from tools.base_tool import ( + BaseTool, + Determinism, + ExecutionMode, + ResourceProfile, + ToolResult, + ToolStability, + ToolTier, +) + + +def _write_json(path: str | None, data: dict[str, Any]) -> list[str]: + if not path: + return [] + out = Path(path) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(data, indent=2), encoding="utf-8") + return [str(out)] + + +def _slug(value: str) -> str: + chars = [c.lower() if c.isalnum() else "-" for c in value.strip()] + return "-".join("".join(chars).split("-")).strip("-") or "character" + + +def _character_color(index: int) -> tuple[str, str]: + palettes = [ + ("#ff8f68", "#ffd39f"), + ("#75b8ff", "#ffe7a3"), + ("#8fd17f", "#f7c8ff"), + ("#f2c94c", "#fce6c9"), + ] + return palettes[index % len(palettes)] + + +def _normalize_style(style: Any) -> dict[str, Any]: + if not isinstance(style, dict): + return {} + normalized: dict[str, Any] = {} + visual_style = style.get("visual_style") or style.get("name") or style.get("style") + if visual_style: + normalized["visual_style"] = str(visual_style) + palette = style.get("palette") + if isinstance(palette, list): + normalized["palette"] = [str(color) for color in palette] + for key in ["line_style", "texture"]: + if style.get(key): + normalized[key] = str(style[key]) + return normalized + + +def _render_preview_mp4(preview_path: Path, video_path: Path, duration_seconds: float, fps: int) -> None: + if shutil.which("ffmpeg") is None: + raise RuntimeError("ffmpeg is required to render preview MP4") + try: + from playwright.sync_api import sync_playwright + except Exception as exc: # pragma: no cover - dependency-specific branch + raise RuntimeError("Playwright is required to render preview MP4") from exc + + frame_dir = video_path.parent / f"{video_path.stem}_frames" + frame_dir.mkdir(parents=True, exist_ok=True) + frame_count = max(2, int(duration_seconds * fps)) + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page(viewport={"width": 1280, "height": 720}) + page.goto(preview_path.resolve().as_uri(), wait_until="networkidle") + for frame in range(frame_count): + if frame: + page.wait_for_timeout(int(1000 / fps)) + page.screenshot(path=str(frame_dir / f"frame_{frame:04d}.png")) + browser.close() + + cmd = [ + "ffmpeg", + "-y", + "-framerate", + str(fps), + "-i", + str(frame_dir / "frame_%04d.png"), + "-r", + str(fps), + "-pix_fmt", + "yuv420p", + str(video_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "ffmpeg failed to render preview MP4") + + +class CharacterSpecGenerator(BaseTool): + name = "character_spec_generator" + version = "0.1.0" + tier = ToolTier.CORE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=10) + agent_skills = ["character-rigging", "pose-library-design"] + capabilities = ["draft_character_design", "normalize_character_specs"] + best_for = ["Converting approved concepts into structured character_design artifacts"] + not_good_for = ["Generating artwork pixels or finished animation"] + input_schema = { + "type": "object", + "properties": { + "characters": {"type": "array"}, + "brief": {"type": "string"}, + "style": {"type": "object"}, + "output_path": {"type": "string"}, + }, + } + output_schema = {"type": "object", "properties": {"character_design": {"type": "object"}}} + artifact_schema = {"artifact": "character_design"} + side_effects = ["optionally writes character_design JSON to output_path"] + user_visible_verification = ["Review character count, action list, and emotional range"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + raw_characters = inputs.get("characters") or [ + { + "id": "main_character", + "role": "lead character", + "body_type": "simple rounded cartoon character", + "style": "local rigged cartoon", + "required_emotions": ["neutral", "curious", "happy", "surprised"], + "required_actions": ["idle", "blink", "look", "gesture"], + } + ] + characters: list[dict[str, Any]] = [] + for raw in raw_characters: + name = raw.get("id") or raw.get("name") or raw.get("display_name") or "character" + characters.append( + { + "id": _slug(str(name)), + "display_name": raw.get("display_name", str(name).replace("_", " ").title()), + "role": raw.get("role", "supporting character"), + "body_type": raw.get("body_type", "simple cartoon body"), + "style": raw.get("style", inputs.get("style", {}).get("visual_style", "cartoon")), + "silhouette_notes": raw.get("silhouette_notes", ""), + "required_emotions": raw.get("required_emotions", ["neutral", "happy", "surprised"]), + "required_actions": raw.get("required_actions", ["idle", "blink", "look"]), + "required_views": raw.get("required_views", ["front", "side"]), + "props": raw.get("props", []), + "constraints": raw.get("constraints", []), + } + ) + artifact = { + "version": "1.0", + "style": _normalize_style(inputs.get("style", {})), + "characters": characters, + "metadata": { + "source": "character_spec_generator", + "brief": inputs.get("brief", ""), + }, + } + artifacts = _write_json(inputs.get("output_path"), artifact) + return ToolResult( + success=True, + data={"character_design": artifact}, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) + + +class SvgRigBuilder(BaseTool): + name = "svg_rig_builder" + version = "0.1.0" + tier = ToolTier.CORE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=10) + agent_skills = ["character-rigging", "svg-character-animation", "gsap-core", "gsap-timeline"] + capabilities = ["draft_svg_rig_plan", "define_parts_pivots_layers"] + input_schema = { + "type": "object", + "required": ["character_design"], + "properties": { + "character_design": {"type": "object"}, + "output_path": {"type": "string"}, + }, + } + artifact_schema = {"artifact": "rig_plan"} + side_effects = ["optionally writes rig_plan JSON to output_path"] + user_visible_verification = ["Check pivots and layers before asset generation"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + design = inputs["character_design"] + rig_characters: list[dict[str, Any]] = [] + for character in design.get("characters", []): + cid = character["id"] + actions = character.get("required_actions", []) + base_parts = [ + ("body", "torso", 20, None, [320, 380]), + ("head", "head", 40, "body", [320, 220]), + ("eye_left", "eye", 50, "head", [288, 210]), + ("eye_right", "eye", 50, "head", [352, 210]), + ("pupil_left", "pupil", 51, "eye_left", [288, 210]), + ("pupil_right", "pupil", 51, "eye_right", [352, 210]), + ("mouth", "mouth", 52, "head", [320, 260]), + ("arm_left", "limb", 35, "body", [260, 330]), + ("arm_right", "limb", 35, "body", [380, 330]), + ("leg_left", "limb", 10, "body", [285, 470]), + ("leg_right", "limb", 10, "body", [355, 470]), + ] + if "tail" in character.get("body_type", "").lower() or "mouse" in cid: + base_parts.append(("tail", "tail", 5, "body", [245, 425])) + if any("wing" in a for a in actions) or "bird" in cid: + base_parts.extend( + [ + ("wing_left", "wing", 30, "body", [275, 330]), + ("wing_right", "wing", 30, "body", [365, 330]), + ] + ) + parts = [ + { + "id": part_id, + "kind": kind, + "layer": layer, + **({"parent": parent} if parent else {}), + } + for part_id, kind, layer, parent, _ in base_parts + ] + joints = { + part_id: { + "pivot": pivot, + "rotation": [-35, 35] if kind in {"head", "tail"} else [-75, 95], + "scale": [0.8, 1.2], + } + for part_id, kind, _, _, pivot in base_parts + } + required_poses = sorted( + set(["idle", "blink", "look_left", "look_right", "surprised"] + actions) + ) + rig_characters.append( + { + "character_id": cid, + "rig_type": "svg_rig", + "parts": parts, + "joints": joints, + "layers": [p["id"] for p in sorted(parts, key=lambda p: p["layer"])], + "views": character.get("required_views", ["front", "side"]), + "required_poses": required_poses, + "required_actions": actions, + "risks": [ + "Generated pivots are first-pass estimates; review with preview frames.", + ], + } + ) + artifact = {"version": "1.0", "characters": rig_characters, "metadata": {"source": self.name}} + artifacts = _write_json(inputs.get("output_path"), artifact) + return ToolResult( + success=True, + data={"rig_plan": artifact}, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) + + +class PoseLibraryBuilder(BaseTool): + name = "pose_library_builder" + version = "0.1.0" + tier = ToolTier.CORE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=10) + agent_skills = ["pose-library-design", "character-rigging", "svg-character-animation"] + capabilities = ["draft_pose_library", "draft_action_cycles"] + input_schema = { + "type": "object", + "required": ["rig_plan"], + "properties": {"rig_plan": {"type": "object"}, "output_path": {"type": "string"}}, + } + artifact_schema = {"artifact": "pose_library"} + side_effects = ["optionally writes pose_library JSON to output_path"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + characters = [] + for rig in inputs["rig_plan"].get("characters", []): + cid = rig["character_id"] + poses = { + "idle": {"description": "Neutral readable hold", "parts": {}, "hold_frames": 24}, + "blink": { + "description": "Quick eye close/open", + "parts": {"eye_left": {"scaleY": 0.08}, "eye_right": {"scaleY": 0.08}}, + "hold_frames": 3, + "transition": "power1.inOut", + }, + "look_left": { + "description": "Gaze shifts left", + "parts": {"pupil_left": {"x": -6}, "pupil_right": {"x": -6}}, + "hold_frames": 18, + }, + "look_right": { + "description": "Gaze shifts right", + "parts": {"pupil_left": {"x": 6}, "pupil_right": {"x": 6}}, + "hold_frames": 18, + }, + "surprised": { + "description": "Head lifts, eyes widen, mouth opens", + "parts": {"head": {"y": -4, "rotation": -4}, "mouth": {"shape": "small_o"}}, + "expression": "surprised", + "hold_frames": 24, + "transition": "back.out", + }, + } + for action in rig.get("required_actions", []): + poses.setdefault( + action, + { + "description": f"First-pass pose for {action}", + "parts": {}, + "hold_frames": 18, + "transition": "power2.inOut", + }, + ) + characters.append( + { + "character_id": cid, + "poses": poses, + "mouth_shapes": { + "closed": {"description": "Neutral closed mouth"}, + "small_o": {"description": "Small open mouth for surprise or vowel"}, + "wide": {"description": "Wide open mouth"}, + "smile": {"description": "Smile shape"}, + }, + "action_cycles": { + "walk": ["walk_contact", "walk_passing"], + "breathe": ["idle"], + }, + } + ) + artifact = {"version": "1.0", "characters": characters, "metadata": {"source": self.name}} + artifacts = _write_json(inputs.get("output_path"), artifact) + return ToolResult( + success=True, + data={"pose_library": artifact}, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) + + +class ActionTimelineCompiler(BaseTool): + name = "action_timeline_compiler" + version = "0.1.0" + tier = ToolTier.CORE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=10) + agent_skills = ["pose-library-design", "svg-character-animation", "gsap-timeline"] + capabilities = ["compile_scene_actions", "draft_action_timeline"] + input_schema = { + "type": "object", + "required": ["scene_plan"], + "properties": { + "scene_plan": {"type": "object"}, + "character_ids": {"type": "array", "items": {"type": "string"}}, + "fps": {"type": "number"}, + "output_path": {"type": "string"}, + }, + } + artifact_schema = {"artifact": "action_timeline"} + side_effects = ["optionally writes action_timeline JSON to output_path"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + character_ids = inputs.get("character_ids") or ["main_character"] + scenes = [] + for scene in inputs["scene_plan"].get("scenes", []): + start_s = scene.get("start_seconds", 0) + end_s = scene.get("end_seconds", start_s + 3) + duration = max(0.1, end_s - start_s) + actions = [] + for index, character_id in enumerate(character_ids): + offset = min(duration * 0.08 * index, duration * 0.2) + is_primary = index == 0 + actions.extend( + [ + { + "at_seconds": start_s + offset, + "duration_seconds": min(0.5, duration / 4), + "character_id": character_id, + "action": "anticipate" if is_primary else "react", + "pose": "idle", + "easing": "power2.out", + }, + { + "at_seconds": start_s + duration * 0.25 + offset, + "duration_seconds": duration * 0.35, + "character_id": character_id, + "action": "perform" if is_primary else "follow", + "pose": ( + "surprised" + if scene.get("hero_moment") or not is_primary + else "look_right" + ), + "easing": "back.out", + "notes": scene.get("description", ""), + }, + { + "at_seconds": start_s + duration * 0.7 + offset, + "duration_seconds": duration * 0.25, + "character_id": character_id, + "action": "settle", + "pose": "idle", + "easing": "power2.inOut", + }, + ] + ) + scenes.append( + { + "scene_id": scene["id"], + "start_seconds": start_s, + "end_seconds": end_s, + "camera": {"framing": scene.get("framing", "medium")}, + "background": scene.get("description", ""), + "effects": [], + "actions": actions, + } + ) + artifact = { + "version": "1.0", + "fps": inputs.get("fps", 30), + "scenes": scenes, + "metadata": {"source": self.name}, + } + artifacts = _write_json(inputs.get("output_path"), artifact) + return ToolResult( + success=True, + data={"action_timeline": artifact}, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) + + +class CharacterRigRenderer(BaseTool): + name = "character_rig_renderer" + version = "0.1.0" + tier = ToolTier.CORE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=50) + agent_skills = [ + "character-rigging", + "svg-character-animation", + "canvas-procedural-animation", + "gsap-core", + "gsap-timeline", + "remotion-best-practices", + "hyperframes", + ] + capabilities = ["write_browser_preview", "prepare_character_render_package"] + input_schema = { + "type": "object", + "required": ["action_timeline"], + "properties": { + "action_timeline": {"type": "object"}, + "rig_plan": {"type": "object"}, + "pose_library": {"type": "object"}, + "output_path": {"type": "string"}, + "workspace_path": {"type": "string"}, + "video_output_path": {"type": "string"}, + "render_video": {"type": "boolean", "default": False}, + "duration_seconds": {"type": "number", "minimum": 0.1, "default": 3}, + "fps": {"type": "integer", "minimum": 1, "default": 12}, + }, + } + output_schema = { + "type": "object", + "properties": { + "preview_path": {"type": "string"}, + "hyperframes_workspace": {"type": "string"}, + "composition_path": {"type": "string"}, + "video_path": {"type": "string"}, + "asset_manifest": {"type": "object"}, + "edit_decisions": {"type": "object"}, + }, + } + side_effects = [ + "writes a lightweight HTML preview to output_path", + "writes a HyperFrames workspace/package", + "optionally writes preview MP4", + ] + user_visible_verification = ["Open preview and check character visibility and motion"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + output_path = Path(inputs.get("output_path", "projects/character-preview/preview.html")) + output_path.parent.mkdir(parents=True, exist_ok=True) + timeline_json = json.dumps(inputs["action_timeline"]) + rig_characters = (inputs.get("rig_plan") or {}).get("characters", []) + if not rig_characters: + seen_ids = { + action.get("character_id") + for scene in inputs["action_timeline"].get("scenes", []) + for action in scene.get("actions", []) + if action.get("character_id") + } + rig_characters = [{"character_id": cid} for cid in sorted(seen_ids)] or [ + {"character_id": "main_character"} + ] + count = len(rig_characters) + spacing = 620 / max(count, 1) + character_svgs = [] + for index, character in enumerate(rig_characters): + cid = _slug(character.get("character_id", f"character-{index + 1}")) + x = 110 + spacing * index if count > 1 else 320 + scale = 0.82 if count > 1 else 1 + body_fill, head_fill = _character_color(index) + character_svgs.append( + f""" + + + + + + + + + + + + """ + ) + html = f""" + + + + + Character Animation Preview + + + + +
+ +{''.join(character_svgs)} + +
+
Local character preview. Characters: · Scenes:
+ + + +""" + output_path.write_text(html, encoding="utf-8") + total_duration = max( + [ + float(scene.get("end_seconds", 0) or 0) + for scene in inputs["action_timeline"].get("scenes", []) + ] + or [float(inputs.get("duration_seconds", 3))] + ) + workspace_path = Path( + inputs.get("workspace_path") + or output_path.parent / "hyperframes" + ) + composition_dir = workspace_path / "compositions" + composition_dir.mkdir(parents=True, exist_ok=True) + (workspace_path / "assets").mkdir(parents=True, exist_ok=True) + (workspace_path / "hyperframes.json").write_text( + json.dumps( + { + "registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry", + "paths": { + "blocks": "compositions", + "components": "compositions/components", + "assets": "assets", + }, + }, + indent=2, + ), + encoding="utf-8", + ) + (workspace_path / "DESIGN.md").write_text( + "# DESIGN\n\n" + "Generated for OpenMontage character animation.\n\n" + "- Background: `#9bd7ff` sky and `#75c878` ground\n" + "- Foreground: `#202632` ink outlines\n" + "- Accent: saturated cartoon body colors\n" + "- Motion: GSAP pose holds, squash/bounce, gaze, blink, and arm arcs\n", + encoding="utf-8", + ) + finite_bounce_repeats = max(0, int(total_duration / 0.9) - 1) + finite_acting_repeats = max(0, int(total_duration / 2.1) - 1) + composition_html = f""" +""" + composition_path = composition_dir / "character-scene.html" + composition_path.write_text(composition_html, encoding="utf-8") + asset_id = "character_scene_hyperframes" + asset_manifest = { + "version": "1.0", + "assets": [ + { + "id": asset_id, + "type": "animation", + "path": str(composition_path), + "source_tool": self.name, + "scene_id": "character_preview", + "duration_seconds": total_duration, + "format": "html", + "generation_summary": "HyperFrames SVG/GSAP character composition package.", + } + ], + "total_cost_usd": 0, + "metadata": {"source": self.name, "workspace_path": str(workspace_path)}, + } + edit_decisions = { + "version": "1.0", + "render_runtime": "hyperframes", + "renderer_family": "animation-first", + "cuts": [ + { + "id": "character-scene", + "source": asset_id, + "in_seconds": 0, + "out_seconds": total_duration, + "reason": "HyperFrames SVG/GSAP character scene generated by character_rig_renderer.", + } + ], + "metadata": { + "proposal_render_runtime": "hyperframes", + "title": "Character Animation", + }, + } + data: dict[str, Any] = { + "preview_path": str(output_path), + "render_package": "hyperframes_workspace", + "hyperframes_workspace": str(workspace_path), + "composition_path": str(composition_path), + "asset_manifest": asset_manifest, + "edit_decisions": edit_decisions, + } + artifacts = [str(output_path), str(workspace_path / "hyperframes.json"), str(composition_path)] + render_video = bool(inputs.get("render_video") or inputs.get("video_output_path")) + if render_video: + video_path = Path( + inputs.get("video_output_path") + or output_path.with_suffix(".mp4") + ) + video_path.parent.mkdir(parents=True, exist_ok=True) + duration_seconds = float(inputs.get("duration_seconds", 3)) + fps = int(inputs.get("fps", 12)) + _render_preview_mp4(output_path, video_path, duration_seconds, fps) + video_asset_id = f"{output_path.stem}_preview_video" + video_asset_manifest = { + "version": "1.0", + "assets": [ + { + "id": video_asset_id, + "type": "video", + "path": str(video_path), + "source_tool": self.name, + "scene_id": "character_preview", + "duration_seconds": duration_seconds, + "format": "mp4", + "generation_summary": "Rendered from local SVG/GSAP character preview via Playwright frame capture and ffmpeg.", + } + ], + "total_cost_usd": 0, + "metadata": {"source": self.name, "preview_path": str(output_path)}, + } + video_edit_decisions = { + "version": "1.0", + "render_runtime": "ffmpeg", + "renderer_family": "animation-first", + "cuts": [ + { + "id": "character-preview-cut", + "source": video_asset_id, + "in_seconds": 0, + "out_seconds": duration_seconds, + "reason": "Local rendered character preview for video_compose handoff.", + } + ], + "metadata": { + "proposal_render_runtime": "ffmpeg", + "character_preview_path": str(output_path), + }, + } + data.update( + { + "video_path": str(video_path), + "video_asset_manifest": video_asset_manifest, + "video_edit_decisions": video_edit_decisions, + } + ) + artifacts.append(str(video_path)) + return ToolResult( + success=True, + data=data, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) + + +class CharacterAnimationReviewer(BaseTool): + name = "character_animation_reviewer" + version = "0.1.0" + tier = ToolTier.ANALYZE + capability = "character_animation" + provider = "openmontage" + stability = ToolStability.BETA + execution_mode = ExecutionMode.SYNC + determinism = Determinism.DETERMINISTIC + resource_profile = ResourceProfile(cpu_cores=1, ram_mb=128, vram_mb=0, disk_mb=10) + agent_skills = ["character-animation-qa"] + capabilities = ["review_character_artifacts", "draft_character_qa_report"] + input_schema = { + "type": "object", + "properties": { + "rig_plan": {"type": "object"}, + "pose_library": {"type": "object"}, + "action_timeline": {"type": "object"}, + "preview_path": {"type": "string"}, + "review_level": {"type": "string", "enum": ["static", "browser", "final"], "default": "static"}, + "browser_preview_checked": {"type": "boolean", "default": False}, + "frame_samples_checked": {"type": "boolean", "default": False}, + "output_path": {"type": "string"}, + }, + } + artifact_schema = {"artifact": "character_qa_report"} + side_effects = ["optionally writes character_qa_report JSON to output_path"] + + def execute(self, inputs: dict[str, Any]) -> ToolResult: + start = time.time() + issues: list[str] = [] + rig = inputs.get("rig_plan") or {} + poses = inputs.get("pose_library") or {} + timeline = inputs.get("action_timeline") or {} + preview_path = inputs.get("preview_path") + review_level = inputs.get("review_level", "static") + browser_preview_checked = bool(inputs.get("browser_preview_checked", False)) + frame_samples_checked = bool(inputs.get("frame_samples_checked", False)) + assets_exist = True + if not preview_path: + assets_exist = False + issues.append("Preview path is required for character animation QA.") + elif not Path(preview_path).exists(): + assets_exist = False + issues.append(f"Preview path does not exist: {preview_path}") + pivots_defined = all( + bool(character.get("joints")) + for character in rig.get("characters", []) + ) if rig else False + poses_defined = all( + bool(character.get("poses")) + for character in poses.get("characters", []) + ) if poses else False + actions_timed = all( + bool(scene.get("actions")) + for scene in timeline.get("scenes", []) + ) if timeline else False + if not pivots_defined: + issues.append("Rig plan is missing joints/pivots for one or more characters.") + if not poses_defined: + issues.append("Pose library is missing poses for one or more characters.") + if not actions_timed: + issues.append("Action timeline has scenes without timed actions.") + schema_valid = True + for artifact_name, artifact in [ + ("rig_plan", rig), + ("pose_library", poses), + ("action_timeline", timeline), + ]: + if not artifact: + continue + try: + validate_artifact(artifact_name, artifact) + except Exception as exc: + schema_valid = False + issues.append(f"{artifact_name} schema validation failed: {exc}") + if review_level in {"browser", "final"} and not browser_preview_checked: + issues.append("Browser preview check is required for browser/final QA.") + if review_level == "final" and not frame_samples_checked: + issues.append("Frame sample check is required for final QA.") + status = "pass" if not issues else "revise" + report = { + "version": "1.0", + "status": status, + "preview_path": preview_path or "", + "checks": { + "schema_valid": schema_valid, + "assets_exist": assets_exist, + "pivots_defined": pivots_defined, + "poses_defined": poses_defined, + "actions_timed": actions_timed, + "motion_detected": actions_timed, + "browser_preview_checked": browser_preview_checked, + "frame_samples_checked": frame_samples_checked, + }, + "issues": issues, + "recommended_action": "present_to_user" if status == "pass" else "fix_rig", + "metadata": { + "source": self.name, + "confidence": "static artifact review; run Playwright/FFmpeg checks in compose for final output", + }, + } + artifacts = _write_json(inputs.get("output_path"), report) + return ToolResult( + success=True, + data={"character_qa_report": report}, + artifacts=artifacts, + duration_seconds=round(time.time() - start, 2), + ) diff --git a/tools/video/hyperframes_compose.py b/tools/video/hyperframes_compose.py index 33d338a2..330d9c78 100644 --- a/tools/video/hyperframes_compose.py +++ b/tools/video/hyperframes_compose.py @@ -1089,6 +1089,19 @@ class HyperFramesCompose(BaseTool): # Unknown cut shape — render a placeholder text card so the render # still succeeds; lint/validate will surface the issue. + if ext in {".html", ".htm"} and src_path: + rel = self._rel_from_workspace(str(src_path)) + composition_id = Path(rel).stem + html = ( + f'
' + ) + return html, None + placeholder = self._escape_text(text or cut.get("reason") or f"Scene {index + 1}") html = ( f'