# Cinema Mode Framework — Post-Build Recon

**Built:** 2026-05-17
**Spec:** `consultations/recoil/cinema-mode-framework-2026-05-17/BUILD_SPEC.md`
**Synthesis inputs:** `gemini_round_1.md` + `opus_round_2.md` (Opus authoritative on conflicts)

## Files

| Path | Action | Purpose |
|------|--------|---------|
| `recoil/config/CINEMA_MODES.yaml` | NEW | SSOT — 6 catalogs + 6 starter modes |
| `recoil/pipeline/_lib/cinema_loader.py` | NEW | Loader + `resolve_mode` + `render_cinema_tokens` |
| `recoil/config/model_profiles.json` | MOD | `cinema_token_map` per model (seeddance, kling-v3, wan-r2v, veo) |
| `recoil/pipeline/_lib/plan_loader.py` | MOD | Add typed `CanonicalShot.cinematography` field + `_validate_cinematography_block` (load-time validation) |
| `recoil/pipeline/_lib/prompt_engine.py` | MOD | Inject `render_cinema_tokens()` into 6 t2v/r2v builders, reading the typed cinematography block |
| `recoil/pipeline/tests/lib/test_cinema_loader.py` | NEW | Unit tests (~14 functions) |
| `recoil/pipeline/tests/lib/test_cinema_integration.py` | NEW | Integration tests (~18 functions, incl. typed-field + load-time validation tests) |

## Files INTENTIONALLY untouched
- `recoil/config/PROMPT_BIBLE.yaml` — Opus §Q2 confirmed no camera vocab to migrate.
- `recoil/pipeline/_lib/dispatch_payload.py` — `{**shot.raw}` unpack at line 183 already preserves arbitrary keys; the typed `cinematography` block rides through verbatim.
- All i2v builders (`build_kling_i2v_prompt`, `build_seeddance_i2v_prompt`, `build_wan_i2v_prompt`) — Opus §Q3 i2v skip rule.
- Keyframe / previz builders — Phase 2 future scope.

## Data flow (live)

```
plan.json
  └─ shots[i].cinematography = {"mode": ..., "overrides": {...}}
        ↓ load_plan() → _canonicalize_shot() validates the block at LOAD time
        ↓                (unknown mode / invalid override id → CinemaConfigError)
        ↓ CanonicalShot.cinematography (typed field) + raw["cinematography"]
        ↓ {**shot.raw}                          (dispatch_payload.py:183)
        ↓ builder(shot, bible, project_config) — t2v / r2v / r2v_multi only
        ↓ block = shot.get("cinematography") or {}
        ↓ render_cinema_tokens(block["mode"], model_id, block["overrides"])
        ↓ cinema_loader: load_cinema_modes() → resolve_mode() → per-model render
        ↓ string emitted INTO Zone C STYLE slot (where Shot on {film_stock} lived)
        ↓ _enforce_prompt_length()              (prompt_engine.py:71)
final prompt
```

## Override precedence
1. `shot.cinematography["overrides"]` — partial catalog overrides (applied last)
2. `shot.cinematography["mode"]` — per-shot mode (beats project default)
3. `project_config["cinema_mode"]` — project default
4. None → existing PROMPT_BIBLE `film_stock_default` baseline runs (zero regression)

## Wiring trace (Phase 6 call sites)

Six exact insertion points in `prompt_engine.py` — each replaces the existing
`Shot on {film_stock}` line with a conditional cinema-tokens-or-baseline branch:

| Builder | Line | Model id passed |
|---------|------|------------------|
| `build_seeddance_r2v_prompt` | ~4108 | `seeddance-2.0` |
| `build_seeddance_r2v_prompt_multi` | ~4473 | `seeddance-2.0` |
| `build_seeddance_t2v_prompt` | ~4591 | `seeddance-2.0` |
| `build_kling_t2v_prompt` | ~3362 | `kling-v3` |
| `build_wan_r2v_prompt` | ~3815 | `wan-2.7-r2v` |
| `build_veo_prompt` | ~2928 | `veo-3.1` |

**Veo special case:** `build_veo_prompt` is the only builder dispatched for both
`("veo-3.1", "t2v")` and `("veo-3.1", "i2v")` via the BUILDERS table. A
`_has_start_frame` guard enforces the i2v skip rule — cinema tokens are never
injected when `routing_data.start_frame_path` is present.

## Per-model cinema_token_map strategy

| Model | body | lens | filtration | stock | grain | grade | aperture | shutter |
|-------|------|------|-----------|-------|-------|-------|----------|---------|
| `seeddance-2.0` | full | full | full | full | full | full | full | full |
| `kling-v3` | null | compressed | compressed | compressed | compressed | compressed | null | null |
| `wan-2.7-r2v` | full | full | full | full | full | full | full | full |
| `veo-3.1` | null | null | compressed | compressed | compressed | compressed | null | null |

`"full"` = emit `prompt_tokens` verbatim (period-joined with other full fields).
`"compressed"` = head noun phrase only (first comma-delimited clause; comma-joined).
`null` = field omitted entirely for this model.

## Validation gates run

- Phase 1 YAML parse + structural assertions
- Phase 2 loader import + reference integrity validator
- Phase 3 JSON schema check + token_map enumeration
- Phase 4 functional render tests (full + compressed)
- Phase 5 plan_loader typed-field populate + load-time crash + roundtrip
- Phase 6 syntax + import count + typed-block read count + i2v skip regression
- Phase 7 unit + integration + i2v regression + typed-field + full _lib suite

## Rollback

Single tag (taken by /dispatch wrapper before build): `pre-cinema-mode-framework`.

To roll back:
```
git reset --hard pre-cinema-mode-framework
```

All changes are additive except the 6 STYLE-block edits in `prompt_engine.py`
and the `CanonicalShot.cinematography` field addition in `plan_loader.py` —
the STYLE edits are conditional fallbacks (`if cinema_tokens: ... elif
film_stock: ...`) and the typed field defaults to None so legacy plans
without a `cinematography` block load unchanged. Even mid-rollback, removing
`CINEMA_MODES.yaml` would not break anything — `load_cinema_modes()` raises
`FileNotFoundError`, which only fires when `_validate_cinematography_block`
is invoked (and that only fires for plans with a `cinematography` block, of
which there are zero pre-build).

## Future expansion (NOT in this build — Opus §Q14)
- Phase 2: coverage planner auto-stamps `cinematography` blocks per shot (the dropped Gemini §6)
- Phase 2: wire keyframe/previz builders (currently `build_previs_prompt`, `build_seedream_prompt`)
- Phase 3: add eval-panel scoring for cinema-mode-attributed quality lift
