feat(character-animation): add local rigged character pipeline

New beta pipeline for reusable cartoon characters with SVG rigs, pose
libraries, action timelines, and Canvas/Remotion/HyperFrames rendering.

- pipeline_defs/character-animation.yaml: 11-stage manifest
- skills/pipelines/character-animation/: 11 stage director skills
- tools/character/: BaseTool implementations for char design, rigging,
  pose libraries, action timelines, previews, and QA
- schemas/artifacts/{character_design,rig_plan,pose_library,
  action_timeline,character_qa_report}.schema.json: canonical artifacts
- schemas/artifacts/scene_plan.schema.json: extended for character-led
  scenes
- .agents/skills/{canvas-procedural-animation,character-animation-qa,
  character-rigging,pose-library-design,svg-character-animation}/:
  Layer 3 vendor knowledge
- AGENT_GUIDE / PROJECT_CONTEXT / README / ARCHITECTURE / PROVIDERS:
  surface the new pipeline and its capability family
- tools/video/hyperframes_compose.py: SVG character rig support
- tests/contracts/test_character_animation_pipeline.py: contract tests
This commit is contained in:
calesthio
2026-04-28 08:11:02 -07:00
parent 386338c92b
commit 2b0801030c
33 changed files with 2528 additions and 15 deletions
@@ -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/
@@ -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
+53
View File
@@ -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
@@ -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
@@ -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
+4 -1
View File
@@ -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` |
+2 -1
View File
@@ -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
+7 -6
View File
@@ -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/<project-name>/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`.
+13 -6
View File
@@ -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/<project-name>/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/<name>/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)
+33
View File
@@ -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/<project-name>/hyperframes/`. Final videos still follow the normal OpenMontage convention: `projects/<project-name>/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.
+302
View File
@@ -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/<name>/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
+5
View File
@@ -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",
]
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
+59
View File
@@ -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
}
+22 -1
View File
@@ -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" },
@@ -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/<project-name>/assets/characters/<character-id>/
```
Use subfolders:
```text
parts/
poses/
previews/
```
Generated backgrounds go under:
```text
projects/<project-name>/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.
@@ -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.
@@ -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/<project-name>/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`.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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`.
@@ -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()
+2
View File
@@ -0,0 +1,2 @@
"""Character animation tools."""
+896
View File
@@ -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"""
<g class=\"character\" id=\"character_{cid}\" data-character=\"{cid}\" transform=\"translate({x - 320:.1f} 0) scale({scale})\">
<ellipse class=\"shadow\" cx=\"320\" cy=\"560\" rx=\"120\" ry=\"22\" fill=\"rgba(0,0,0,.18)\" />
<ellipse class=\"body outline\" cx=\"320\" cy=\"400\" rx=\"80\" ry=\"120\" fill=\"{body_fill}\" />
<circle class=\"head outline\" cx=\"320\" cy=\"230\" r=\"90\" fill=\"{head_fill}\" />
<ellipse class=\"eye eye-left outline\" cx=\"285\" cy=\"215\" rx=\"18\" ry=\"26\" fill=\"white\" />
<ellipse class=\"eye eye-right outline\" cx=\"355\" cy=\"215\" rx=\"18\" ry=\"26\" fill=\"white\" />
<circle class=\"pupil pupil-left\" cx=\"289\" cy=\"218\" r=\"8\" fill=\"#202632\" />
<circle class=\"pupil pupil-right\" cx=\"359\" cy=\"218\" r=\"8\" fill=\"#202632\" />
<path class=\"mouth outline\" d=\"M285 275 Q320 305 355 275\" fill=\"none\" />
<path class=\"arm arm-left outline\" d=\"M255 360 C210 380 190 420 180 455\" fill=\"none\" />
<path class=\"arm arm-right outline\" d=\"M385 360 C440 330 465 290 475 240\" fill=\"none\" />
</g>"""
)
html = f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>Character Animation Preview</title>
<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js\"></script>
<style>
body {{ margin: 0; overflow: hidden; background: #9bd7ff; font-family: system-ui, sans-serif; }}
#stage {{ width: 100vw; height: 100vh; display: grid; place-items: center; background: linear-gradient(#9bd7ff 0 65%, #75c878 65%); }}
svg {{ width: min(82vw, 720px); overflow: visible; }}
.outline {{ stroke: #202632; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }}
#note {{ position: fixed; left: 16px; bottom: 16px; background: white; border: 2px solid #202632; padding: 10px 12px; border-radius: 8px; }}
</style>
</head>
<body>
<div id=\"stage\">
<svg viewBox=\"0 0 640 640\" role=\"img\" aria-label=\"Character preview\">
{''.join(character_svgs)}
</svg>
</div>
<div id=\"note\">Local character preview. Characters: <span id=\"characters\"></span> · Scenes: <span id=\"count\"></span></div>
<script>
window.__ACTION_TIMELINE__ = {timeline_json};
document.querySelector('#count').textContent = window.__ACTION_TIMELINE__.scenes.length;
const characters = gsap.utils.toArray('.character');
document.querySelector('#characters').textContent = characters.map((node) => node.dataset.character).join(', ');
characters.forEach((node, index) => {{
const q = gsap.utils.selector(node);
gsap.set(q('.head'), {{ svgOrigin: '320 320' }});
gsap.set(q('.arm-right'), {{ svgOrigin: '385 360' }});
gsap.set(q('.arm-left'), {{ svgOrigin: '255 360' }});
gsap.timeline({{ repeat: -1, defaults: {{ ease: 'power2.inOut' }}, delay: index * 0.12 }})
.to(node, {{ y: -16, duration: 0.45 }})
.to(node, {{ y: 0, duration: 0.45 }});
gsap.timeline({{ repeat: -1, repeatDelay: 0.5, delay: index * 0.18 }})
.to(q('.head'), {{ rotation: index % 2 ? 8 : -8, duration: 0.35 }})
.to(q('.pupil'), {{ x: index % 2 ? -8 : 8, y: -3, duration: 0.2 }}, '<')
.to(q('.arm-right'), {{ rotation: index % 2 ? -22 : 28, duration: 0.35 }}, '<')
.to(q('.eye'), {{ scaleY: 0.08, transformOrigin: 'center', duration: 0.08 }})
.to(q('.eye'), {{ scaleY: 1, duration: 0.1 }})
.to(q('.head'), {{ rotation: index % 2 ? -6 : 6, duration: 0.35 }})
.to(q('.pupil'), {{ x: index % 2 ? 6 : -6, y: 3, duration: 0.2 }}, '<')
.to(q('.arm-right'), {{ rotation: index % 2 ? 8 : -8, duration: 0.35 }}, '<');
}});
</script>
</body>
</html>
"""
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"""<template id=\"character-scene-template\">
<div data-composition-id=\"character-scene\" data-start=\"0\" data-duration=\"{total_duration:.3f}\" data-width=\"1280\" data-height=\"720\">
<style>
[data-composition-id=\"character-scene\"] {{ position: relative; width: 1280px; height: 720px; overflow: hidden; background: linear-gradient(#9bd7ff 0 65%, #75c878 65%); }}
[data-composition-id=\"character-scene\"] svg {{ width: 920px; position: absolute; left: 180px; top: 42px; overflow: visible; }}
[data-composition-id=\"character-scene\"] .outline {{ stroke: #202632; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }}
</style>
<svg viewBox=\"0 0 640 640\" role=\"img\" aria-label=\"Character animation scene\">
{''.join(character_svgs)}
</svg>
<script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>
<script>
window.__timelines = window.__timelines || {{}};
const tl = gsap.timeline({{ paused: true }});
const characters = gsap.utils.toArray('[data-composition-id=\"character-scene\"] .character');
characters.forEach((node, index) => {{
const q = gsap.utils.selector(node);
tl.set(q('.head'), {{ svgOrigin: '320 320' }}, 0);
tl.set(q('.arm-right'), {{ svgOrigin: '385 360' }}, 0);
tl.set(q('.arm-left'), {{ svgOrigin: '255 360' }}, 0);
tl.from(node, {{ y: 26, scale: 0.94, opacity: 0, duration: 0.45, ease: 'back.out(1.8)' }}, 0.15 + index * 0.12);
tl.to(node, {{ y: -16, duration: 0.45, repeat: {finite_bounce_repeats}, yoyo: true, ease: 'power2.inOut' }}, 0.7 + index * 0.08);
tl.to(q('.head'), {{ rotation: index % 2 ? 8 : -8, duration: 0.35, repeat: {finite_acting_repeats}, yoyo: true, ease: 'sine.inOut' }}, 0.55 + index * 0.16);
tl.to(q('.pupil'), {{ x: index % 2 ? -8 : 8, y: -3, duration: 0.2, repeat: {finite_acting_repeats}, yoyo: true, ease: 'power2.inOut' }}, 0.6 + index * 0.16);
tl.to(q('.arm-right'), {{ rotation: index % 2 ? -22 : 28, duration: 0.35, repeat: {finite_acting_repeats}, yoyo: true, ease: 'back.inOut(1.4)' }}, 0.65 + index * 0.16);
tl.to(q('.eye'), {{ scaleY: 0.08, transformOrigin: 'center', duration: 0.08, repeat: {finite_acting_repeats}, repeatDelay: 1.4, yoyo: true, ease: 'power1.inOut' }}, 1.1 + index * 0.12);
}});
window.__timelines['character-scene'] = tl;
</script>
</div>
</template>
"""
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),
)
+18
View File
@@ -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'<div id="{cut_id}" class="clip composition-clip" '
f'data-composition-id="{self._escape_attr(composition_id)}" '
f'data-composition-src="{self._escape_attr(rel)}" '
f'data-start="{self._f(in_s)}" data-duration="{self._f(duration)}" '
f'data-width="{width}" data-height="{height}" '
f'data-track-index="1"></div>'
)
return html, None
placeholder = self._escape_text(text or cut.get("reason") or f"Scene {index + 1}")
html = (
f'<div id="{cut_id}" class="clip text-card" '
@@ -1182,5 +1195,10 @@ class HyperFramesCompose(BaseTool):
# If it's already a relative path starting with assets/, keep as-is.
if not p.is_absolute():
return str(p).replace("\\", "/")
parts = p.parts
for anchor in ("assets", "compositions"):
if anchor in parts:
index = len(parts) - 1 - list(reversed(parts)).index(anchor)
return "/".join(parts[index:])
# Otherwise emit just the basename under assets/.
return f"assets/{p.name}"