| name | customize-composition |
| description | Validate the generated content against the composition's real variable schema and stage it for rendering. Use after generate-script and before render-and-return. |
Customize the composition
Turn the content you wrote into a validated, render-ready bundle. This step
catches mistakes (wrong color format, a value that doesn't match a declared
variable) locally, so the render API never sees a bad payload.
You don't edit the composition's HTML. HyperFrames injects your variable values
at render time — the composition reads them with
window.__hyperframes.getVariables(). So "customizing" means producing a clean
variables.json and staging the composition directory.
Steps
-
Make sure your generated content is at /.agents/workspace/output/proposed.json
(from generate-script).
-
Validate it against the chosen composition and write the clean variables:
python3 /.agents/workspace/scripts/customize.py \
/.agents/workspace/compositions/<chosen-id> \
/.agents/workspace/output/proposed.json \
/.agents/workspace/output/variables.json
- On success it writes
/.agents/workspace/output/variables.json (your values plus
declared defaults for anything you left out) and exits 0.
- On failure it exits non-zero and prints exactly what's wrong (unknown
variable, wrong type, a color that isn't a hex value, a number out of
range). Read the message, fix
proposed.json, and run it again. Do not
skip ahead to rendering with an invalid file.
-
Copy the chosen composition into the output area so the render step has a
self-contained directory:
rm -rf /.agents/workspace/output/composition
cp -r /.agents/workspace/compositions/<chosen-id> /.agents/workspace/output/composition
-
Bake in the voiceover. The renderer reads the <audio> src from static
HTML, so substitute the synthesized URL into the staged composition (or strip
the audio element if there's no narration):
python3 /.agents/workspace/scripts/apply_voiceover.py \
/.agents/workspace/output/composition \
"$(cat /.agents/workspace/output/voiceover_url.txt 2>/dev/null)"
Free-composition case: the shipped starters already have the voiceover
<audio> slot. But if you free-authored the composition's HTML (custom
video, not a starter), you MUST include the same slot or the video renders
silent — apply_voiceover.py only substitutes a slot that exists. Put this
inside the composition root, near the other timed elements, with the literal
placeholder src (apply_voiceover fills it; it's stripped if there's no
narration). Use a real, literal data-duration in seconds — your
composition's own length — e.g. for a 10-second video:
<audio id="voiceover" data-start="0" data-duration="10"
data-track-index="0" data-volume="1" src="__VOICEOVER_URL__"></audio>
- The ONLY placeholder is
src="__VOICEOVER_URL__" (apply_voiceover
substitutes it). Every other attribute must be a concrete value — do not
leave a literal like <comp-duration> in the HTML; the renderer parses
data-duration as a number, and a non-number is silently dropped → silent
video. (You may also omit data-duration entirely; it then defaults to the
audio's own length.)
- Do NOT use
data-has-audio or data-composition-variable — the renderer
reads a static src; the placeholder + apply_voiceover is the working path.
Multi-turn edits
On a follow-up ("make the headline punchier", "switch to a dark background"),
you don't need to start over:
- Edit only the changed keys in
/.agents/workspace/output/proposed.json.
- Re-run
customize.py to re-validate and rewrite variables.json.
- Keep the same staged composition unless the user wants a different starter.
Output of this step
/.agents/workspace/output/variables.json — validated values.
/.agents/workspace/output/composition/ — the staged composition directory.
Then move to render-and-return.