Documentary Montage hardening plus governance fixes

This commit is contained in:
calesthio
2026-04-10 16:42:39 -07:00
parent 44baede67f
commit de94d4dba3
27 changed files with 1107 additions and 98 deletions
@@ -6,7 +6,7 @@ The shot list exists. You now have to actually go out and find the
clips that fill each slot. This is a two-step operation:
1. **Build the corpus** — fan the scene director's queries out across
Pexels / Archive.org / NASA and download/embed the candidates.
Pexels / Archive.org / NASA / Wikimedia / Unsplash and download/embed the candidates.
2. **Pick per slot** — run CLIP retrieval against the corpus with each
slot description and choose one winner per slot.
@@ -71,7 +71,7 @@ corpus_builder.execute({
{"query": "taxi heavy rain yellow", "kind": "video", "per_source": 6},
# ... one entry per unique slot query
],
"sources": ["pexels", "archive_org"], # from preferred_sources union
"sources": ["pexels", "archive_org", "wikimedia"], # from preferred_sources union
"filters": {
"min_duration": 3,
"max_duration": 40,
@@ -85,6 +85,9 @@ corpus_builder.execute({
**Rules for the fan-out:**
- If the brief pins a source and `corpus_builder.source_provider_menu`
says that source is unavailable, STOP and surface it. Do not silently
drop to the remaining sources.
- Budget the corpus for 8-12x the slot count. A 15-slot montage wants
~150 candidates so retrieval has real choices.
- `per_source` of 4-8 per query is usually enough. Pushing to 20+
@@ -94,6 +97,8 @@ corpus_builder.execute({
is slow — don't interleave it with the modern Pexels batch.
- If any slot has `nasa` in `preferred_sources`, run ONE small
`nasa`-only batch. NASA is slow and its results are niche.
- `unsplash` is image-only. Use it as a support source, not the
backbone of a motion-led documentary cut.
### 3. Sanity-Check The Corpus Before Retrieval
@@ -16,7 +16,7 @@ The output is a single mp4 plus a `render_report` artifact.
| Schema | `schemas/artifacts/render_report.schema.json` | Artifact validation |
| Prior artifact | `state.artifacts["edit"]["edit_decisions"]` | Cuts, transitions, music, metadata hints |
| Prior artifact | `state.artifacts["assets"]["asset_manifest"]` | File paths, durations, providers |
| Tool | `video_compose` (FFmpeg + Remotion) | Primary render engine |
| Tool | `video_compose` (Remotion-first + FFmpeg fallback) | Primary render engine |
| Tool | `audio_mixer` | Music fade, silence window, L-cuts |
| Tool (optional) | `color_grade` | Uniform LUT across mixed-era clips |
| Tool (optional) | `video_trimmer`, `video_stitch` | Lower-level helpers if needed |
@@ -47,11 +47,16 @@ If the brief said "no narration" and a narration track somehow
appeared in the edit, STOP and ask. Do not render over a contract
violation.
Also confirm the render engine you intend to use is actually
available — `video_compose` in FFmpeg-only mode is fine for this
pipeline (the whole piece is footage-led, no Remotion scenes needed
unless the user asked for title cards). FFmpeg alone can render this
pipeline end to end.
Also confirm that `edit_decisions.renderer_family` is locked to
`documentary-montage` and that the chosen render engine preserves that
decision. For this repo's governance model, `video_compose` is
Remotion-first on `operation="render"`, even for footage-led pieces.
- If Remotion is available, use the normal `render` path and keep the
approved renderer family.
- If Remotion is unavailable, do NOT quietly drop to FFmpeg. Surface
the engine change and get approval before using a lower-level
FFmpeg-only path.
### 1. Resolve The Canvas
@@ -83,32 +88,8 @@ For a pipeline this simple, the cleanest path is:
video_compose.execute({
"operation": "render",
"output_path": "projects/<name>/renders/final.mp4",
"canvas": { "width": 1920, "height": 1080, "fps": 24 },
"cuts": [
{
"source": "<resolved file path>",
"in": 1.2,
"out": 5.2,
"scale": "fit_canvas_center_crop",
"transition_in": "fade",
"transition_in_duration": 0.8,
},
# ... more cuts
],
"audio": {
"music_path": "<resolved music path>",
"music_volume": 0.7,
"music_fade_in": 1.0,
"music_fade_out": 4.0,
"silence_windows": [{"start": 54.0, "end": 56.0}],
"sfx_layers": [
{"source": "<rain carry clip>", "start": 20.8, "duration": 1.2, "volume": 0.6}
],
},
"frame_treatment": {
"lut_path": "styles/luts/warm_film_100.cube",
"letterbox": "2.35:1"
}
"edit_decisions": edit_decisions_with_renderer_family,
"asset_manifest": asset_manifest,
})
```
@@ -116,6 +97,9 @@ The exact field names come from the live `video_compose` schema at
render time — consult the tool's `agent_skills` if available before
writing the call. Do not invent parameters.
`edit_decisions_with_renderer_family` means the normal edit artifact
with `renderer_family = "documentary-montage"` preserved intact.
### 3. Apply Grade Via LUT, Not Per Clip
Read `edit_decisions.metadata.grade_profile`. Map it to a LUT file:
@@ -163,8 +147,8 @@ Recommended encoder settings for doc montage:
| Audio codec | `aac` | Universal |
| Audio bitrate | `192k` | Music-bed friendly |
If the source clips are 30fps and the canvas is 24fps, let FFmpeg
drop frames evenly — don't blend. Motion interpolation on
If the source clips are 30fps and the canvas is 24fps, let the render
pipeline drop frames evenly — don't blend. Motion interpolation on
mixed-source footage looks awful.
### 6. Post-Render Verification
@@ -210,7 +194,7 @@ Record verifications in `render_report.verification_notes`.
"Silence window 54-56s confirmed (music -60dB)",
"Last frame fades to black at 89.0s"
],
"render_grammar": "cinematic-trailer",
"render_grammar": "documentary-montage",
"metadata": {
"pipeline": "documentary-montage",
"canvas": { "width": 1920, "height": 1080 },
@@ -247,10 +231,9 @@ Record verifications in `render_report.verification_notes`.
- **Per-clip color grading.** One LUT across the whole piece. Do
not try to balance each clip individually — it takes 10x the time
and makes the register LESS consistent, not more.
- **Quiet render engine swap.** If `video_compose` routes through
Remotion for some reason and the aesthetic changes, stop and
surface. This pipeline is FFmpeg-friendly and shouldn't need
Remotion unless the user asked for title cards.
- **Quiet FFmpeg downgrade.** If Remotion is blocked and you route to
FFmpeg without surfacing it, you've changed the approved render path.
Stop and surface that downgrade before rendering.
- **Overriding edit decisions at render time.** If you find yourself
adjusting volumes, fades, or trims in the render call, you're
editing during compose. Go back to the edit stage, fix the
@@ -34,6 +34,10 @@ The enemy is "slideshow" — a sequence of clips played back-to-back
with the same hold length and no sound design. If it feels like a
slideshow, the edit has failed, regardless of how good the clips are.
This stage also locks the render grammar. For documentary montage,
set `renderer_family` to `documentary-montage` so compose stays on the
approved Remotion-first path.
## Process
### 0. Guardrails — No Silent Major Changes
@@ -228,6 +232,7 @@ Canonical shape for this pipeline:
```json
{
"version": "1.0",
"renderer_family": "documentary-montage",
"cuts": [
{
"id": "cut_01",
@@ -284,6 +289,7 @@ Canonical shape for this pipeline:
- `sum(out - in for cut in cuts)` is within ±10% of
`brief.duration_seconds`.
- `renderer_family = "documentary-montage"` is present and unchanged.
- Hero slots have the longest holds.
- No two adjacent cuts share subject AND scale.
- The transition vocabulary is at most 4 distinct values.
@@ -55,7 +55,7 @@ before starting the stage.
| Tool | Role |
|------|------|
| `corpus_builder` | Fans out across Pexels/Archive.org/NASA, downloads + embeds + indexes |
| `corpus_builder` | Fans out across Pexels/Archive.org/NASA/Wikimedia/Unsplash, downloads + embeds + indexes |
| `clip_search` | Ranks clips for a slot, finds similar sets, diversifies selections |
| `video_compose` / Remotion | Renders the final timeline |
@@ -113,8 +113,9 @@ open for the scene director to decide per slot.
- Tone is ONE register from the fixed list.
- Duration and shape are concrete numbers / enum values.
- Music source is named OR the brief explicitly says "no music".
- Sources list is non-empty and at least one is `available` per the
tool registry.
- Sources list is non-empty and at least one requested source is
`available` per `corpus_builder.source_provider_menu` surfaced in
preflight.
## Common Pitfalls
@@ -5,7 +5,7 @@
The brief exists. You now have to turn a thematic question into a
concrete list of SLOTS the retrieval layer can fill. Each slot is an
intention ("a silhouette at a doorway at dusk") plus the queries that
will find it in the real world (Pexels/Archive.org/NASA).
will find it in the real world (Pexels/Archive.org/NASA/Wikimedia/Unsplash).
This is the most creative stage in the pipeline. Retrieval is only as
good as the slot descriptions you write.
@@ -147,6 +147,8 @@ based on what footage lives where:
| `pexels` | Modern HD footage, clean shots, people, cities, nature | Default for modern/any era |
| `archive_org` | Prelinger home movies, mid-century educational film, 1940s-1980s texture | Vintage, wry, dreamlike, anything nostalgic |
| `nasa` | Earth-from-orbit, astronomy, flight, scale imagery | Reverent, anything about scale, space, planet, flight |
| `wikimedia` | Commons photos and CC video, civic/documentary/public-event coverage | Public spaces, landmarks, protests, city texture, educational footage |
| `unsplash` | Polished editorial stills, lifestyle, product-adjacent photography | Modern still-image support shots when motion footage is thin |
If `era_mix = "vintage"`, bias slots toward `archive_org` and write
queries in period-appropriate vocabulary ("commuter", "housewife",