@amg555/frameforge-mcp 2.4.10 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +22 -4
- package/CHANGELOG.md +174 -0
- package/CLAUDE.md +192 -9
- package/README.md +47 -6
- package/output/harness_test_1794418727139/frames/f000000.jpg +1 -0
- package/package.json +4 -3
- package/plugins/video-pipeline/.claude-plugin/plugin.json +8 -2
- package/plugins/video-pipeline/skills/.upstream-skills.json +143 -0
- package/plugins/video-pipeline/skills/html-renderer/SKILL.md +59 -15
- package/plugins/video-pipeline/skills/html-renderer/references/animation-patterns.md +609 -0
- package/plugins/video-pipeline/skills/html-renderer/references/character-animation.md +388 -0
- package/plugins/video-pipeline/skills/html-renderer/references/collage-broll.md +41 -0
- package/plugins/video-pipeline/skills/html-renderer/references/design.md +217 -0
- package/plugins/video-pipeline/skills/html-renderer/references/directing.md +165 -0
- package/plugins/video-pipeline/skills/html-renderer/references/earshot-audio.md +201 -0
- package/plugins/video-pipeline/skills/html-renderer/references/engine-api.md +480 -0
- package/plugins/video-pipeline/skills/html-renderer/references/film-styles.md +95 -0
- package/plugins/video-pipeline/skills/html-renderer/references/hyperframes-motion.md +208 -0
- package/plugins/video-pipeline/skills/html-renderer/references/onetake-camera.md +59 -0
- package/plugins/video-pipeline/skills/html-renderer/references/procedural-sfx.md +135 -0
- package/plugins/video-pipeline/skills/html-renderer/references/remotion-patterns.md +159 -0
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +19 -2
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +15 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +21 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +1293 -20
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +36 -6
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +126 -2
- package/scripts/deep-validate.js +52 -2
- package/src/audio-mixer/mixer.js +594 -63
- package/src/cli/doctor.js +36 -2
- package/src/cli/make.js +113 -9
- package/src/harness/bin/harness.mjs +127 -5
- package/src/harness/lib/capture.mjs +215 -192
- package/src/harness/lib/verify-quality.mjs +47 -7
- package/src/mcp-server/server.js +1015 -59
- package/src/skills/sync-upstream.js +290 -0
- package/src/utils/media-downloader.js +162 -0
- package/src/utils/transcribe.js +341 -0
- package/src/video-editor/editor.js +96 -11
- package/src/workflows/ingest.js +10 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# HyperFrames Motion Craft — Measured from Official Films
|
|
2
|
+
|
|
3
|
+
Adapted from `heygen-com/hyperframes` `skills/hyperframes/references/motion.md`.
|
|
4
|
+
All values measured frame-by-frame from the official HyperFrames launch films.
|
|
5
|
+
|
|
6
|
+
These are defaults. A reference the user provides, or a scene that deliberately does otherwise, wins for its own beat.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Core Motion Rules
|
|
11
|
+
|
|
12
|
+
### Speed and Duration
|
|
13
|
+
- **A fast move takes 0.3–0.6 s** with motion blur on its own ease, peaking at its fastest frame
|
|
14
|
+
- **Motion blur**: 6–12 px on text-sized elements, 15–20 px full-frame, horizontal-only on horizontal moves
|
|
15
|
+
- **Render-time blur**: `data-motion-blur="on"` on the composition root smears moving elements along their real path (opt-in)
|
|
16
|
+
|
|
17
|
+
### Arrival vs Departure
|
|
18
|
+
- **Arrive slow, leave clean**: long-tailed ease-outs on arrivals; landing takes longer than leaving
|
|
19
|
+
- An exit is one smooth motion — never slide, stop, then leave
|
|
20
|
+
- Entrances that are too snappy read as glitches; slowing by ~1 second often fixes it
|
|
21
|
+
|
|
22
|
+
### Ease by Role
|
|
23
|
+
|
|
24
|
+
| Role | Ease |
|
|
25
|
+
|------|------|
|
|
26
|
+
| Arrival / landing | `expo.out`, `power4.out` (long tail) |
|
|
27
|
+
| Camera move / colour flood | `power2.inOut` (both ways) |
|
|
28
|
+
| Zoom through / exit | `power3.in` (accelerates out) |
|
|
29
|
+
| Rotation / progress bar | `linear` |
|
|
30
|
+
|
|
31
|
+
The same `power2.out` at 0.5 s on **everything** is the flat default that makes a film feel generated.
|
|
32
|
+
|
|
33
|
+
### Springs
|
|
34
|
+
|
|
35
|
+
Springs are named by damping ratio:
|
|
36
|
+
|
|
37
|
+
| Damping | Feel |
|
|
38
|
+
|---------|------|
|
|
39
|
+
| 1.0 | Never bounces — dead landing |
|
|
40
|
+
| 0.7–0.85 | Reads premium |
|
|
41
|
+
| 0.4–0.6 | Playful |
|
|
42
|
+
| < 0.3 | Cartoon |
|
|
43
|
+
|
|
44
|
+
- Position and size **may** bounce; opacity and colour **never** bounce
|
|
45
|
+
- GSAP equivalents: `elastic.out(1, 0.5)` and `back.out(1.4)`
|
|
46
|
+
- A closed-form spring of `t` keeps any frame renderable on its own (deterministic)
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// FrameForge engine.js — spring with damping ratio control
|
|
50
|
+
spring(t, { tension: 200, friction: 20 }) // damping ≈ 0.7 (premium)
|
|
51
|
+
spring(t, { tension: 300, friction: 10 }) // damping ≈ 0.3 (cartoon)
|
|
52
|
+
spring(t, { tension: 120, friction: 28 }) // damping ≈ 1.0 (dead)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Timing Relationships
|
|
56
|
+
- **Exits are 40–50% shorter** than their entrances
|
|
57
|
+
- **Overlap**: the next element starts when the previous is 30–50% done — fully sequential looks robotic, all-at-once looks cheap
|
|
58
|
+
- **Stagger**: letters a frame apart, words 60–120 ms, list items 40–80 ms
|
|
59
|
+
|
|
60
|
+
### Beat Grid
|
|
61
|
+
- Frames per beat = `fps × 60 / BPM`
|
|
62
|
+
- Compute each beat from its **index** and round it — never add rounded beats repeatedly (at 128 BPM and 30 fps that drifts 2 frames in 32 beats)
|
|
63
|
+
- Cut on the beat; change the layout on the bar
|
|
64
|
+
|
|
65
|
+
### Cuts
|
|
66
|
+
- Cut at the **fastest frame** of a move, with matching direction and speed on both sides
|
|
67
|
+
- A click into a new screen is a match cut on the click's rebound — the new screen comes in slightly large and settles
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Title Animation Patterns
|
|
72
|
+
|
|
73
|
+
A landed title is **dead still** — the motion moves to the next element, the camera, or the world behind. Vary the entrance within a film.
|
|
74
|
+
|
|
75
|
+
| Pattern | How |
|
|
76
|
+
|---------|-----|
|
|
77
|
+
| Per word blur-in | `blur(6px)` → 0 and opacity 0 → 1 in 0.13 s, 0.067 s apart, no translate |
|
|
78
|
+
| Per word colour | Light grey → ink, 0.07–0.1 s apart, no position change |
|
|
79
|
+
| Whole line blur | 8 px → 0 in 0.13–0.2 s |
|
|
80
|
+
| Per letter name | 0.1 s apart, opacity 0.3 → 1, slight blur |
|
|
81
|
+
| Slot machine | One word rolls through options, ~0.1 s a roll |
|
|
82
|
+
| Behind subject | Title between the plate and a matte of the person |
|
|
83
|
+
|
|
84
|
+
### Hold Times
|
|
85
|
+
- A line the viewer must read holds **0.5 s + 0.3 s per word**
|
|
86
|
+
- The film's last line: **at least 2 s**
|
|
87
|
+
- A one- or two-word chapter line: **0.4 s**
|
|
88
|
+
- Exit: blur-out and fade 0.13–0.25 s word by word, or with the scene's move
|
|
89
|
+
|
|
90
|
+
### Title Sizes
|
|
91
|
+
- Body: 4–9% of frame height, one or two lines
|
|
92
|
+
- Quiet lines: 2.5–4% against hero words that fill 60–90% of width
|
|
93
|
+
- One highlighted word in the accent colour (carries meaning)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Rhythm
|
|
98
|
+
|
|
99
|
+
| Pattern | Value |
|
|
100
|
+
|---------|-------|
|
|
101
|
+
| Montage cuts | Every 0.13–0.8 s |
|
|
102
|
+
| Demo hold | 7–10 s while UI acts |
|
|
103
|
+
| Average shot | 1.8–3 s with motion inside |
|
|
104
|
+
| Pre-payoff hold | 0.3–0.75 s before the payoff |
|
|
105
|
+
| Payoff speed | Normal speed (not slow) |
|
|
106
|
+
|
|
107
|
+
**Continuous motion that works**: UI working, footage playing, character acting, 3D world turning, slow orbit of background stage (3–5° per second).
|
|
108
|
+
|
|
109
|
+
**Defaults that look generated**:
|
|
110
|
+
- Breathing/floating/pulsing loops on titles and UI
|
|
111
|
+
- Titles that keep sliding or scaling after they land
|
|
112
|
+
- A slow creeping scale or push late in a scene
|
|
113
|
+
- Bounce on UI elements
|
|
114
|
+
- Click rings
|
|
115
|
+
- Every scene entering the same way
|
|
116
|
+
- Every element fading and sliding up
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Transitions with a Cause
|
|
121
|
+
|
|
122
|
+
Use 3–5 kinds in a film, each **caused by something on screen**. Hard cuts belong inside a scene (punch-ins); at most a third of seams between beats are hard cuts. Worlds never crossfade.
|
|
123
|
+
|
|
124
|
+
| Transition | How to Build |
|
|
125
|
+
|------------|-------------|
|
|
126
|
+
| Zoom through a button | Camera tracks to button with blur (0.6 s), button scales from ~8% to ~250% of frame in 0.35 s, strong ease-in; its fill becomes the next frame |
|
|
127
|
+
| Object becomes the world | Pill or card grows past the frame in 0.65 s ease-in-out, radius shrinks; what was around it blurs ~15 px and fades in 0.3 s; new content fades in while still growing |
|
|
128
|
+
| Morph | One container widens into the next (~110 → ~880 px in 0.2 s `power3.out`) with horizontal blur 12 → 0 |
|
|
129
|
+
| Carry across | Result shrinks 1 → 0.6 with blur peaking at 6 px mid-move, lands inside the next app as it builds around it |
|
|
130
|
+
| Colour flood | Hero object stays while the whole ground changes colour in ~0.4 s `power2.inOut` |
|
|
131
|
+
| Whip | Horizontal or vertical smear of 0.13–0.3 s; arriving element lands blurred and sharpens in ~0.13 s |
|
|
132
|
+
| Iris from click | `clip-path: circle()` opens from the clicked point onto the next world in 0.15 s |
|
|
133
|
+
| Dip cut | Outgoing frame darkens ~40% for 3 frames before a hard cut, so the cut reads as intentional |
|
|
134
|
+
| Blur swap | Out: opacity → 0 with ~8 px blur in 0.13–0.2 s; in: blur → 0 in 0.13 s; ground never cuts |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## The Right Mechanism for Camera Moves
|
|
139
|
+
|
|
140
|
+
| Request | Mechanism |
|
|
141
|
+
|---------|-----------|
|
|
142
|
+
| Punch-in, zoom, pan, Ken Burns, reframe | Tween `scale` and `x`/`y` on a **wrapper inside** the clip — never on the timed media element |
|
|
143
|
+
| Crop or mask reveal | Tween `clip-path` or mask on an inner wrapper; source time untouched |
|
|
144
|
+
| Constant speed change | `data-playback-rate` on the media (0.1–10) |
|
|
145
|
+
| Speed ramp | `rate` lane in `data-automation` on the media |
|
|
146
|
+
| Hold or freeze | Hold a final frame; a freeze mid-source needs a still exported first |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Sound (HyperFrames/Earshot Standard)
|
|
151
|
+
|
|
152
|
+
Sound is half of how a film feels.
|
|
153
|
+
|
|
154
|
+
### The Default
|
|
155
|
+
- **A film over ~15 s** that tells something gets a music bed and sounds on its visible actions — unless the person asked for silence
|
|
156
|
+
- **Each storyline says its sound** in one line: the bed's feel and tempo, the one moment the music changes, which actions get effects
|
|
157
|
+
|
|
158
|
+
### The Clock
|
|
159
|
+
- **Narrated**: the voice is the clock — time each beat's start to its anchor word
|
|
160
|
+
- **Music only**: the music is the clock — land the payoff on strong beats; cuts follow motion, not the drums
|
|
161
|
+
|
|
162
|
+
### Effects: Few and Exact
|
|
163
|
+
|
|
164
|
+
Every effect has a cause on screen. Too many read as a template.
|
|
165
|
+
|
|
166
|
+
| Sound | Only on |
|
|
167
|
+
|-------|---------|
|
|
168
|
+
| click | A real cursor click on screen |
|
|
169
|
+
| whoosh | A scene cut, or a large card moving fast |
|
|
170
|
+
| soft key tick | Text arriving letter by letter or line by line |
|
|
171
|
+
| pop | A badge, chip, or number landing |
|
|
172
|
+
| riser into impact | The payoff or the mark — **once in the film** |
|
|
173
|
+
|
|
174
|
+
- Sound peak lands **one frame after** its picture (light arrives before sound)
|
|
175
|
+
- Each hit sits **3–6 dB over the bed**
|
|
176
|
+
- Rotate 2–3 takes of a repeated sound — the same whoosh on every cut is the most common tell
|
|
177
|
+
- No synthesized tones, chimes, or dings
|
|
178
|
+
|
|
179
|
+
### Levels
|
|
180
|
+
- Music under a voice: ~0.14 volume (≈ −17 dB relative)
|
|
181
|
+
- Target loudness: **−16 to −14 LUFS** (social/web)
|
|
182
|
+
- True peak: **−1 dBTP** or lower
|
|
183
|
+
- Check with `npx hyperframes audio <video> --json` equivalent: use earshot `measure` or ffmpeg `-filter loudnorm`
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Common Faults and Fixes
|
|
188
|
+
|
|
189
|
+
| Symptom | Cause |
|
|
190
|
+
|---------|-------|
|
|
191
|
+
| Blank or frozen scene | Timeline registered before async build finished, or `visibility` tweened on a `.clip` |
|
|
192
|
+
| Stutter or drift between renders | Layout measured in `onUpdate`, `requestAnimationFrame`, or clock time |
|
|
193
|
+
| Jump at a cut | Outgoing and incoming states differ at the seam |
|
|
194
|
+
| Ease looks off at a handoff | Speeds don't match across the seam — mirror the ease (`power3.in` out, `power3.out` in) |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Quick Reference — FrameForge Mapping
|
|
199
|
+
|
|
200
|
+
| HyperFrames concept | FrameForge equivalent |
|
|
201
|
+
|--------------------|-----------------------|
|
|
202
|
+
| `data-start` / `data-duration` on `.clip` | `layer('id').on((t, lt, dur) => ...)` — `lt` is local time, `dur` is duration |
|
|
203
|
+
| `window.__timelines["id"] = tl` | Not needed — FrameForge engine handles timeline registration |
|
|
204
|
+
| GSAP `interpolate(t, frames, values, easing)` | `spring(lt, opts)`, `at(t, start, end)`, `kf(t, keyframes)` |
|
|
205
|
+
| `Easing.spring({damping: 200})` | `spring(lt, { tension: 200, friction: 20 })` |
|
|
206
|
+
| `data-motion-blur="on"` | `render_animation({ motionBlur: true })` (Playwright shutter) |
|
|
207
|
+
| `npx hyperframes check` | `validate_storyboard({ checkDeterminism: true })` |
|
|
208
|
+
| `npx hyperframes snapshot --review` | `render_animation({ mode: "review", contactSheet: true })` |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# OneTake Continuous Camera Choreography
|
|
2
|
+
|
|
3
|
+
Derived from `feitangyuan/onetake`. Eliminates abrupt black cuts, slideshow jumps, and disconnected slide transitions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The OneTake Principle
|
|
8
|
+
|
|
9
|
+
In standard presentation videos, scene transitions cut to black or snap immediately between unrelated slide geometries. In **OneTake**, the entire video occurs within a single, continuous, unified spatial universe:
|
|
10
|
+
|
|
11
|
+
$$\text{Camera}(t) = \text{Spline}(\mathbf{P}_0, \mathbf{P}_1, \dots, \mathbf{P}_N, t)$$
|
|
12
|
+
|
|
13
|
+
Every scene's starting camera transform $(\mathbf{x}, \mathbf{y}, \text{zoom})$ is identically anchored to the previous scene's terminating transform.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Unified Spatial Canvas
|
|
18
|
+
|
|
19
|
+
Instead of unmounting and mounting DOM elements per scene, the entire video timeline exists on a 2.5D coordinate plane:
|
|
20
|
+
|
|
21
|
+
- **Scene 1 (Hook)**: Positioned at $(0, 0)$, Zoom $= 1.0$
|
|
22
|
+
- **Scene 2 (Mechanism)**: Positioned at $(1920, 0)$, Zoom $= 1.15$
|
|
23
|
+
- **Scene 3 (Zoom Detail)**: Positioned at $(1920, 400)$, Zoom $= 1.80$
|
|
24
|
+
- **Scene 4 (Wide Resolution)**: Positioned at $(960, 200)$, Zoom $= 0.85$
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Spring-Interpolated Camera Navigation
|
|
29
|
+
|
|
30
|
+
When moving from Scene $k$ to Scene $k+1$:
|
|
31
|
+
1. **Never use linear interpolation**.
|
|
32
|
+
2. **Use underdamped spring physics** with slight overshoot:
|
|
33
|
+
- Tension: `140`
|
|
34
|
+
- Friction: `14`
|
|
35
|
+
3. **Camera Lead / Look-Ahead**:
|
|
36
|
+
$$\text{CameraTarget}(t) = \text{SubjectPos}(t) + \mathbf{v}_{\text{subject}} \times 0.25$$
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Depth-Parallax Layering
|
|
41
|
+
|
|
42
|
+
During continuous camera travel, layers shift at varying rates proportional to depth $z$:
|
|
43
|
+
|
|
44
|
+
| Layer | Depth $z$ | Parallax Factor | Visual Content |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| Background Grid | $z = 0.2$ | $0.2 \times \Delta x$ | Blueprint grid, starfield, faint topographical lines |
|
|
47
|
+
| Middle Ground | $z = 0.6$ | $0.6 \times \Delta x$ | Secondary cards, connecting vector pipes, data nodes |
|
|
48
|
+
| Primary Stage | $z = 1.0$ | $1.0 \times \Delta x$ | Main subject, active text card, character avatar |
|
|
49
|
+
| Foreground Overlay | $z = 1.4$ | $1.4 \times \Delta x$ | Lens vignette, framing pillars, floating particles |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 4. Zero-Cut Scene Handshake Contract
|
|
54
|
+
|
|
55
|
+
When transitioning between narrative beats:
|
|
56
|
+
- The outgoing scene subject smoothly settles into a secondary supporting node (e.g., shifts left to $x = 300\text{px}$ and scales down to $0.65$).
|
|
57
|
+
- The incoming scene subject enters from depth ($z = 0.5 \to 1.0$) rather than snapping onto the screen.
|
|
58
|
+
- A whoosh sound effect from `Leonxlnx/earshot` fires at the exact inflection point of peak camera velocity.
|
|
59
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Procedural Audio & SFX — Professional Broadcast Engineering
|
|
2
|
+
|
|
3
|
+
Derived from `Leonxlnx/earshot`. Provides deterministic CPU sound-effect synthesis,
|
|
4
|
+
millisecond-accurate audio-motion alignment, and a complete broadcast-grade voice chain
|
|
5
|
+
for coding agents producing professional video.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Core Procedural SFX Primitives — 13-Type Library
|
|
10
|
+
|
|
11
|
+
Agents synthesize or cue sound effects by semantic cue type:
|
|
12
|
+
|
|
13
|
+
| Cue Type | Physical Character | Best Narrative Trigger | Duration | FFmpeg Freq |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| **whoosh** | Filtered pitch sweep down | Camera translation, card wipe, split transition | 400ms | 200Hz |
|
|
16
|
+
| **impact** | Damped 60Hz sine + transient punch | Title punch-in, major claim reveal, beat drop | 500ms | 60Hz |
|
|
17
|
+
| **pop** | Short sinusoidal chirp (600Hz) | Badge reveal, icon appearance, bullet item | 120ms | 600Hz |
|
|
18
|
+
| **click** | High-frequency impulse (2.5kHz) | Kinetic letter reveal, counter tick increment | 50ms | 2500Hz |
|
|
19
|
+
| **riser** | Exponential pitch sweep upward (100Hz) | Build-up to scene climax or problem resolution | 2.0s | 100Hz |
|
|
20
|
+
| **chime** | UI notification bell (1200Hz) | Completion state, success, milestone reached | 350ms | 1200Hz |
|
|
21
|
+
| **buzz** | Error buzzer (80Hz damped) | Rejection state, failure feedback, wrong answer | 180ms | 80Hz |
|
|
22
|
+
| **tick** | Clock tick (3200Hz impulse) | Timer countdown, per-frame counter, typing | 40ms | 3200Hz |
|
|
23
|
+
| **swish** | Soft page turn (350Hz sweep) | Slide reveal, soft transition, card flip | 300ms | 350Hz |
|
|
24
|
+
| **punch** | Heavy sub-bass punch (45Hz) | Major beat emphasis, heroic reveal | 250ms | 45Hz |
|
|
25
|
+
| **sparkle** | High sparkle (4000Hz burst) | Magic moment, aha reveal, idea appears | 200ms | 4000Hz |
|
|
26
|
+
| **deep_boom** | Sub-bass cinematic boom (30Hz) | Epic reveal, cinematic opening, climax moment | 1.2s | 30Hz |
|
|
27
|
+
| **transition** | Musical bridge tone (440Hz) | Scene seam smoother, narrative pivot | 600ms | 440Hz |
|
|
28
|
+
|
|
29
|
+
Use via `mix_audio_pro` tool:
|
|
30
|
+
```
|
|
31
|
+
mix_audio_pro({
|
|
32
|
+
voicePath: ".frameforge-files/voice_0.m4a",
|
|
33
|
+
outputPath: ".frameforge-files/audio_pro.m4a",
|
|
34
|
+
sfxCues: [
|
|
35
|
+
{ name: "whoosh", time: 2.5, volume: 0.8 }, // camera translation at t=2.5s
|
|
36
|
+
{ name: "impact", time: 8.0, volume: 1.0 }, // title punch at t=8.0s
|
|
37
|
+
{ name: "pop", time: 12.3, volume: 0.6 }, // badge reveal at t=12.3s
|
|
38
|
+
{ name: "riser", time: 18.0, volume: 0.7 }, // build-up before climax
|
|
39
|
+
{ name: "chime", time: 25.5, volume: 0.5 }, // success state
|
|
40
|
+
{ name: "deep_boom",time: 30.0, volume: 0.9 }, // epic reveal
|
|
41
|
+
{ name: "sparkle", time: 35.2, volume: 0.4 }, // aha moment
|
|
42
|
+
{ name: "transition",time: 40.0, volume: 0.3 }, // scene seam
|
|
43
|
+
],
|
|
44
|
+
// New parameters:
|
|
45
|
+
roomProfile: "cinematic", // studio_dry | broadcast | cinematic | concert_hall | intimate
|
|
46
|
+
noiseGate: true, // remove breath noise between words
|
|
47
|
+
platform: "youtube", // sets -14 LUFS automatically
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or in `edit.js` for render-time visual sync (engine.js `cue()` function):
|
|
52
|
+
```javascript
|
|
53
|
+
import { cue } from './reel/engine.js';
|
|
54
|
+
cue('whoosh', 2.5, 0.8); // registered in window.__cues for render.mjs
|
|
55
|
+
cue('impact', 8.0, 1.0);
|
|
56
|
+
cue('riser', 18.0, 0.7);
|
|
57
|
+
cue('deep_boom',30.0, 0.9);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Millisecond Audio-Motion Alignment Contract
|
|
63
|
+
|
|
64
|
+
Never place sound effects at arbitrary timestamps. Audio cues synchronize strictly
|
|
65
|
+
with physical acceleration peaks:
|
|
66
|
+
|
|
67
|
+
$$\Delta t_{\text{cue}} = t_{\text{peak velocity}} - 0.04\text{s}$$
|
|
68
|
+
|
|
69
|
+
- **Perceptual Lead**: The human ear perceives sound ~40ms before visual processing finishes.
|
|
70
|
+
Placing audio 40ms ahead of the visual contact makes the impact feel physical and immediate.
|
|
71
|
+
- **Sidechain Ducking**: During voiceover narration, background music ducks by -18dB and
|
|
72
|
+
SFX ducks by -6dB to maintain speech intelligibility (LUFS -14 ±1.5LU).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Professional Voice Signal Chain (10+ Year Sound Engineer Standard)
|
|
77
|
+
|
|
78
|
+
The `mix_audio_pro` MCP tool and `mix_audio_pro.py` script apply the full chain:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Input Voice ──► 80Hz HPF ──► Presence +2dB@5kHz ──► Air Shelf +1.5dB@12kHz
|
|
82
|
+
──► 3:1 Compressor (-20dBFS, 5ms attack, 50ms release)
|
|
83
|
+
──► De-esser (6.5kHz, -3dB dynamic)
|
|
84
|
+
──► [Optional: Room Acoustics 0.3s RT60]
|
|
85
|
+
──► Sidechain Ducking (-18dB on music)
|
|
86
|
+
──► [Optional: Stereo Widener 1.4x M/S on music]
|
|
87
|
+
──► SFX Timeline Mix (40ms perceptual lead)
|
|
88
|
+
──► Two-Pass EBU R128 Loudnorm (-14 LUFS, ≤-1 dBTP)
|
|
89
|
+
──► Output: 48kHz / 192k AAC Stereo
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Why each stage matters (15+ year professional rationale)
|
|
93
|
+
|
|
94
|
+
| Stage | Purpose | When to engage |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| **80Hz HPF** | Remove proximity effect, desk rumble, HVAC drone | Always on — never skip |
|
|
97
|
+
| **Presence +2dB@5kHz** | Add intelligibility, cut through music bed | Always on for narration |
|
|
98
|
+
| **Air Shelf +1.5dB@12kHz** | Open "air" and breathiness on voice | Always on |
|
|
99
|
+
| **3:1 Compressor** | Control dynamics, keep voice at consistent level | Always on — broadcast standard |
|
|
100
|
+
| **De-esser @6.5kHz** | Remove harsh sibilants ("s", "sh", "ch") | Always on for TTS/VO |
|
|
101
|
+
| **Room Acoustics** | 30ms pre-delay + 0.3s RT60 aecho | Enable for cinematic narrator; disable for tutorial/VO |
|
|
102
|
+
| **Sidechain Ducking** | Music auto-ducks -18dB when voice active | Any time music is present |
|
|
103
|
+
| **Stereo Widener** | M/S extrastereo 1.4x on music layer only | Enable for music beds; voice stays mono-center |
|
|
104
|
+
| **Two-Pass Loudnorm** | Linear EBU R128 normalization | Always — final step, never skip |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 4. Music Bed Selection & Mood Matching
|
|
109
|
+
|
|
110
|
+
For `music_mood` field in scenes and `withMusic: true` in `create_video_from_script`:
|
|
111
|
+
|
|
112
|
+
| Mood | Tempo | Frequency Character | Use Case |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `energetic` | 120–140 BPM | High energy, punchy low end | Product launches, viral reels, calls-to-action |
|
|
115
|
+
| `tense` | 80–100 BPM | Staccato, minor key | Problem reveal, stakes building |
|
|
116
|
+
| `calm` | 60–80 BPM | Warm pads, sparse percussion | Explanation, resolution, educational |
|
|
117
|
+
| `mysterious` | 70–90 BPM | Sparse, reverb-heavy, minor | Open-question hooks, discovery reveals |
|
|
118
|
+
| `dramatic` | 90–110 BPM | Orchestral swells, crescendo | Climactic moments, emotional payoff |
|
|
119
|
+
| `neutral` | 100–120 BPM | Background, minimal | Default filler — prefer a named mood |
|
|
120
|
+
| `humorous` | 120–160 BPM | Staccato, comedic | Comedic beats, visual gags |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 5. Broadcast Loudness Standards Reference
|
|
125
|
+
|
|
126
|
+
| Platform | Target LUFS | True Peak |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| YouTube | -14 LUFS | -1 dBTP |
|
|
129
|
+
| Spotify Podcast | -16 LUFS | -1 dBTP |
|
|
130
|
+
| Apple Podcasts | -16 LUFS | -1 dBTP |
|
|
131
|
+
| Netflix / Streaming | -27 LUFS (dialogue) | -3 dBTP |
|
|
132
|
+
| Broadcast (EBU R128) | -23 LUFS | -1 dBTP |
|
|
133
|
+
| **FrameForge Default** | **-14 LUFS** | **-1 dBTP** |
|
|
134
|
+
|
|
135
|
+
Set `targetLufs` in `mix_audio_pro` to match your delivery platform.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Remotion Design Patterns in FrameForge
|
|
2
|
+
|
|
3
|
+
FrameForge ports Remotion's industry-standard motion primitives directly into its pure HTML/Playwright compositor. This document provides a side-by-side mapping and explains how to port Remotion code into FrameForge with zero React overhead, instant execution, and zero commercial licensing restrictions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Architectural Comparison
|
|
8
|
+
|
|
9
|
+
| Dimension | Remotion (`remotion-dev/remotion`) | FrameForge (`html-renderer`) |
|
|
10
|
+
| :--- | :--- | :--- |
|
|
11
|
+
| **Runtime** | React 18/19, Babel/Vite bundler | Pure ES Modules, zero build step |
|
|
12
|
+
| **License** | Commercial license ($25+/dev/mo for >3 devs) | Pure MIT License |
|
|
13
|
+
| **Frame Execution** | React re-renders virtual DOM per frame | Direct `seek(t)` property dispatch |
|
|
14
|
+
| **Deterministic Clock** | Custom webpack injection & timing hooks | Playwright Virtual Clock injection |
|
|
15
|
+
| **Cold Startup** | 3–10 seconds (bundler startup + bundle load) | Instant (<100ms native browser load) |
|
|
16
|
+
| **Visual Math** | Analytical springs, noise, interpolation | 100% mathematically identical algorithms |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. Concept Mapping Table
|
|
21
|
+
|
|
22
|
+
| Remotion Concept | FrameForge Primitive | Signature / Usage |
|
|
23
|
+
| :--- | :--- | :--- |
|
|
24
|
+
| `<Composition>` | `reel(opts)` | `reel({ width: 1920, height: 1080, fps: 30, duration: 60 })` |
|
|
25
|
+
| `<Sequence from={f} durationInFrames={d}>` | `sequence(from, dur, fn)` / `layer(name, { start, end })` | Scoped relative timeline window `[start, end]` |
|
|
26
|
+
| `useCurrentFrame()` / `useVideoConfig()` | `(t, lt, dur)` in layer handler | `layer('x').on((t, lt, dur) => ({ ... }))` |
|
|
27
|
+
| `<AbsoluteFill>` | `absoluteFill(extraStyles)` | Full-bleed absolute overlay preset |
|
|
28
|
+
| `interpolate()` | `interpolate(val, inRange, outRange, opts)` | Multi-segment range mapping with clamp/identity |
|
|
29
|
+
| `interpolateColors()` | `interpolateColors(val, inRange, outColors, opts)` | Gamma-corrected (2.2) perceptual color interpolation |
|
|
30
|
+
| `spring()` | `spring(t, opts)` | Analytical damped harmonic oscillator ($0 \to 1$) |
|
|
31
|
+
| `measureSpring()` | `measureSpring(opts)` | Closed-form settling time ($t_{\text{settle}} = -\ln(\epsilon) / (\zeta \omega)$) |
|
|
32
|
+
| `delayRender()` / `continueRender()` | `window.delayRender()` / `window.continueRender()` | Async screenshot barrier awaiting textures/shaders/fonts |
|
|
33
|
+
| `@remotion/noise` | `noise2D(seed, x, y)` / `noise3D(seed, x, y, z)` | Deterministic continuous gradient noise ($[-1, 1]$) |
|
|
34
|
+
| `@remotion/paths` (`evolvePath`) | `evolvePath(progress, length)` | SVG `strokeDasharray` & `strokeDashoffset` calculator |
|
|
35
|
+
| `@remotion/transitions` | `transition.slide()`, `.wipe()`, `.fade()`, `.flip()` | CSS transform and clip-path generators |
|
|
36
|
+
| `<Loop durationInFrames={d}>` | `loopTime(t, duration)` | `t % duration` modulo loop time |
|
|
37
|
+
| `@remotion/google-fonts` | `loadGoogleFont(family, opts)` | Dynamic web font loader with automatic render barrier |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Side-by-Side Implementations
|
|
42
|
+
|
|
43
|
+
### A. Spring Motion with Analytical Settling Time
|
|
44
|
+
|
|
45
|
+
#### Remotion:
|
|
46
|
+
```tsx
|
|
47
|
+
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
|
|
48
|
+
|
|
49
|
+
export const MyComponent = () => {
|
|
50
|
+
const frame = useCurrentFrame();
|
|
51
|
+
const { fps } = useVideoConfig();
|
|
52
|
+
const progress = spring({ frame, fps, config: { damping: 200, mass: 1 } });
|
|
53
|
+
return <div style={{ transform: `scale(${progress})` }}>Hello</div>;
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### FrameForge:
|
|
58
|
+
```js
|
|
59
|
+
import { layer, spring, measureSpring } from './reel/engine.js';
|
|
60
|
+
|
|
61
|
+
// Precompute exact duration of entrance animation
|
|
62
|
+
const settleTime = measureSpring({ tension: 200, friction: 22 }); // 0.72s
|
|
63
|
+
|
|
64
|
+
layer('hero', { start: 0, end: 10 }).on((t, lt) => {
|
|
65
|
+
const s = spring(lt, { tension: 200, friction: 22 });
|
|
66
|
+
return {
|
|
67
|
+
transform: `scale(${0.85 + 0.15 * s})`,
|
|
68
|
+
opacity: s,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### B. Gamma-Corrected Color Blending
|
|
76
|
+
|
|
77
|
+
Linear interpolation between saturated colors results in dull brown/gray midpoints. FrameForge uses Remotion's gamma-corrected linear-light transfer ($r^{2.2}$).
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { layer, interpolateColors } from './reel/engine.js';
|
|
81
|
+
|
|
82
|
+
layer('background').on((t) => ({
|
|
83
|
+
backgroundColor: interpolateColors(
|
|
84
|
+
t,
|
|
85
|
+
[0, 2.5, 5],
|
|
86
|
+
['#ff0055', '#00e5ff', '#ffe600']
|
|
87
|
+
),
|
|
88
|
+
}));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### C. Handheld Organic Camera Shake (`noise2D`)
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { layer, noise2D } from './reel/engine.js';
|
|
97
|
+
|
|
98
|
+
layer('camera').on((t) => {
|
|
99
|
+
const shakeX = noise2D('handheld', t * 2.5, 0) * 8; // +/- 8px
|
|
100
|
+
const shakeY = noise2D('handheld', 0, t * 2.5) * 6; // +/- 6px
|
|
101
|
+
const rot = noise2D('handheld', t * 1.5, 10) * 0.75; // +/- 0.75 deg
|
|
102
|
+
return {
|
|
103
|
+
transform: `translate(${shakeX}px, ${shakeY}px) rotate(${rot}deg)`,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### D. Path Drawing & Handwriting Reveal (`evolvePath`)
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import { evolvePath, spring } from './reel/engine.js';
|
|
114
|
+
|
|
115
|
+
const pathEl = document.getElementById('chart-line');
|
|
116
|
+
const totalLen = pathEl.getTotalLength();
|
|
117
|
+
|
|
118
|
+
layer('chart', { start: 1, end: 5 }).on((t, lt) => {
|
|
119
|
+
const progress = spring(lt, { tension: 150, friction: 20 });
|
|
120
|
+
const { strokeDasharray, strokeDashoffset } = evolvePath(progress, totalLen);
|
|
121
|
+
pathEl.style.strokeDasharray = strokeDasharray;
|
|
122
|
+
pathEl.style.strokeDashoffset = strokeDashoffset;
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### E. Transitions between Shots
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { layer, transition, seg } from './reel/engine.js';
|
|
132
|
+
|
|
133
|
+
// Wipe entrance over first 0.5s of shot
|
|
134
|
+
layer('b-roll', { start: 5, end: 10 }).on((t, lt) => {
|
|
135
|
+
const inProgress = seg(lt, 0, 0.5);
|
|
136
|
+
return {
|
|
137
|
+
...transition.wipe(inProgress, { direction: 'from-left' }),
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### F. Async Texture / Shader / Font Preloading (`delayRender`)
|
|
145
|
+
|
|
146
|
+
When loading remote media or compiling Three.js WebGL shaders, prevent Playwright from screenshotting prematurely:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { loadGoogleFont } from './reel/engine.js';
|
|
150
|
+
|
|
151
|
+
// Automatically acquires delayRender, loads font weights, and continues render
|
|
152
|
+
await loadGoogleFont('Space Grotesk', { weights: [400, 600, 700] });
|
|
153
|
+
|
|
154
|
+
// Manual async assets:
|
|
155
|
+
const handle = window.delayRender('loading-hdr-texture');
|
|
156
|
+
const texture = await new THREE.TextureLoader().loadAsync('assets/hdr.jpg');
|
|
157
|
+
window.continueRender(handle);
|
|
158
|
+
```
|
|
159
|
+
|
|
@@ -26,13 +26,15 @@ IDE Agent (IBM Bob / Codex / opencode / Cursor / Windsurf / Claude Code)
|
|
|
26
26
|
│
|
|
27
27
|
▼ Cloudflare Remote MCP (recommended) ─────────────────────────────
|
|
28
28
|
│ https://video-mcp.YOUR_SUBDOMAIN.workers.dev/mcp
|
|
29
|
-
│
|
|
30
|
-
│ generate_scene_code · preview_scene
|
|
29
|
+
│ 28 tools: create_video_from_script · edit_video · render_from_scenes
|
|
30
|
+
│ generate_scene_code · preview_scene · verify_scene_quality
|
|
31
31
|
│ ingest_media · edit_scenes · render_tts · multi_voice_tts
|
|
32
32
|
│ generate_scene_image · assemble_video · publish_video
|
|
33
33
|
│ research_topic · write_script · external_platform_call
|
|
34
34
|
│ queue_full_pipeline · get_job_status · export_to_editor
|
|
35
35
|
│ scaffold_animation_project · validate_storyboard · render_animation
|
|
36
|
+
│ plan_video_edit · sync_upstream_skills · check_upstream_updates
|
|
37
|
+
│ transcribe_media · ingest_url · mix_audio_pro
|
|
36
38
|
│
|
|
37
39
|
▼ OR — Local stdio / HTTP MCP ────────────────────────────────────
|
|
38
40
|
│ node src/mcp-server/stdio.js (stdio)
|
|
@@ -181,6 +183,11 @@ publish_video({ productionId, masterPath, channels: ["file"] })
|
|
|
181
183
|
|
|
182
184
|
Use when: the user wants Remotion-style motion graphics, image gen is unavailable, or they ask for "animated video" or "make it look good". Every pixel is a pure function of time `t`.
|
|
183
185
|
|
|
186
|
+
> **Model effort (Opus 5.5 / Claude Code):**
|
|
187
|
+
> - Beat sheet + first scene authorship → `effort: xhigh`
|
|
188
|
+
> - Storyboard review, strip-range patches, encode-only runs → `effort: medium`
|
|
189
|
+
> - `tool_choice` must be `auto` (forced tool use not supported in Opus 5.5)
|
|
190
|
+
|
|
184
191
|
> **Beat sheet gate:** Reply with a beat sheet and 4 keyframe descriptions first. Stop. Wait for approval before writing any scene code.
|
|
185
192
|
|
|
186
193
|
```
|
|
@@ -214,6 +221,16 @@ render_animation({ projectDir: ".frameforge-files/my-anim/", mode: "crop",
|
|
|
214
221
|
// 7. Full render (only after stills pass)
|
|
215
222
|
render_animation({ projectDir: ".frameforge-files/my-anim/", mode: "full",
|
|
216
223
|
audioPath: ".frameforge-files/voice_0.m4a" })
|
|
224
|
+
|
|
225
|
+
// 8. Closed-loop QA fix protocol (mandatory — Opus 5.5 / Claude Code pattern)
|
|
226
|
+
render_animation({ projectDir: ".frameforge-files/my-anim/", mode: "quality" })
|
|
227
|
+
// Returns: { passed, overallScore, worstFrames: [{time, score, issues}] }
|
|
228
|
+
// If passed: false → fix loop (max 3 rounds):
|
|
229
|
+
// 1. Read worstFrames[]. 2. Patch edit.js for those timestamps only.
|
|
230
|
+
// 3. render_animation({ mode: "strip", timestamps: [failingT - 0.5, failingT + 0.5] })
|
|
231
|
+
// 4. render_animation({ mode: "quality" })
|
|
232
|
+
// 5. Repeat until passed or 3 rounds — then escalate to user.
|
|
233
|
+
// Never re-render full film to fix one timestamp. Use strip only.
|
|
217
234
|
```
|
|
218
235
|
|
|
219
236
|
Requires one-time: `cd <outDir> && npm install playwright && npx playwright install chromium`
|
|
@@ -7,6 +7,21 @@ You are the **orchestrator**. You own the full production timeline and the final
|
|
|
7
7
|
You spawn scene-author and diagram-author subagents, integrate their output, and run
|
|
8
8
|
the verification loop.
|
|
9
9
|
|
|
10
|
+
## Model effort settings (Opus 5.5 / Claude Code)
|
|
11
|
+
|
|
12
|
+
Use the right effort level at each stage — wrong effort wastes cost or quality:
|
|
13
|
+
|
|
14
|
+
| Stage | effort | Reason |
|
|
15
|
+
|-------|--------|--------|
|
|
16
|
+
| Beat sheet + storyboard authorship | `xhigh` | New creative work — maximum reasoning |
|
|
17
|
+
| First-time scene code authorship | `xhigh` | New composition, determinism matters |
|
|
18
|
+
| Storyboard gate review / patch | `medium` | Analytical, not creative |
|
|
19
|
+
| Strip-range patch (fix failing frames) | `medium` | Targeted surgical fix |
|
|
20
|
+
| Encode-only / mux-only runs | `medium` | No creative decisions |
|
|
21
|
+
| Research / scripting (Path 3) | `xhigh` for first draft, `medium` for revisions |
|
|
22
|
+
|
|
23
|
+
`tool_choice` must always be `auto` — forced tool use is not supported in Opus 5.5.
|
|
24
|
+
|
|
10
25
|
## Order of operations
|
|
11
26
|
|
|
12
27
|
```
|
|
@@ -22,6 +22,27 @@ assigned to you by the orchestrator. You do not edit shared helpers.
|
|
|
22
22
|
5. If `cuePhrase()` throws a fatal error, your scene has a missing voiceover phrase.
|
|
23
23
|
Fix the phrase match before returning.
|
|
24
24
|
|
|
25
|
+
## Revision attempt cap — 3 maximum
|
|
26
|
+
|
|
27
|
+
You have a maximum of **3 revision attempts** per chapter assignment.
|
|
28
|
+
|
|
29
|
+
- Attempt 1: Initial write → storyboard still → return.
|
|
30
|
+
- Attempt 2: Patch based on orchestrator review → storyboard → return.
|
|
31
|
+
- Attempt 3: Final fix → storyboard → return.
|
|
32
|
+
|
|
33
|
+
**If the chapter does not pass storyboard-validator + purity gate after 3 attempts:**
|
|
34
|
+
Stop. Do not attempt a 4th revision. Return to the orchestrator with:
|
|
35
|
+
1. The exact error message from `storyboard-validator.mjs` or `verify-purity.mjs`.
|
|
36
|
+
2. The specific line(s) in your scene file you believe are responsible.
|
|
37
|
+
3. What you have already tried.
|
|
38
|
+
|
|
39
|
+
Unbounded revision loops are the primary driver of token waste. 3 attempts is the cap.
|
|
40
|
+
|
|
41
|
+
## Model effort
|
|
42
|
+
|
|
43
|
+
Use `effort: xhigh` for the initial scene authorship. Use `effort: medium` for patch revisions.
|
|
44
|
+
This matches the Opus 5.5 recommended settings: `xhigh` for new creative work, `medium` for targeted fixes.
|
|
45
|
+
|
|
25
46
|
## File naming
|
|
26
47
|
|
|
27
48
|
Your chapter file is: `edit_chapter_N.js` where N is your chapter number.
|