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:
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Character animation tools."""
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user