---
name: pipeline
description: Recoil visual production pipeline. Compiles prompt packages, generates frames via native 9:16 pipeline with 4K grid planning, manages reference libraries. Project-agnostic — specify --project on every command.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
argument-hint: "[command] [--episode N] [--shots N-M] [--dry-run]"
---

# /pipeline — Recoil Visual Production Pipeline

Generate cinematic frames for any microdrama project via frontier models (Gemini 3 Pro Image Preview / NBP). Specify project via `--project` flag on every command. Architecture validated through 4-round Gemini 3.1 Pro consultation (Feb 26, 2026).

## Usage

```bash
/pipeline preview --episode 1 --shot 2        # Preview compiled PromptPackage (zero API cost)
/pipeline generate --episode 1 --shots 1-3    # Generate frames for shots 1-3
/pipeline generate --episode 1 --shots 1-3 --grid   # Use 4K grid planning pass
/pipeline location-refs --episode 1 --dry-run # Show location ref generation plan
/pipeline location-refs --episode 1           # Generate missing location refs via 3x3 grids
/pipeline expressions --generate              # Generate grayscale expression library
/pipeline bundle --episode 1 --shots 1-5 --model kling-2.5  # Build upload bundle
/pipeline review                              # Launch review server (port 8430)
/pipeline backfill --episode 2 --dry-run      # Preview storyboard backfill changes
```

## Architecture Overview

**Location:** `~/CLAUDE_PROJECTS/recoil/pipeline/`
**Reads from:** `~/CLAUDE_PROJECTS/recoil/` (never copies, never modifies unless backfill)
**Writes to:** `~/Dropbox/CLAUDE_DATA/recoil/projects/{project}/output/` (shared refs stay in `starsend/output/refs/`)

### File Structure

```
starsend/
├── lib/
│   ├── recoil_bridge.py     — Load storyboard, breakdown, config, refs from Recoil
│   ├── asset_manager.py     — Ref image loading, caching, expression library, mirroring
│   ├── prompt_engine.py     — Cinematic prompt building, kinetic descriptors, wide-shot branching
│   ├── assembler.py         — ShotAssembler: weight-sorted Parts payload, mirroring
│   └── model_profiles.py    — Per-model capabilities (NBP, Flash, Kling, Veo)
├── orchestrator/
│   ├── scene_planner.py     — Scene-aware ordering, ENV shots first
│   ├── grid_engine.py       — 4K grid generation (2x2, 3x3), splitting, panel extraction
│   ├── pipeline.py          — Main 3-pass orchestrator (grid → extract → pro render)
│   └── cost_tracker.py      — API cost logging, budget enforcement
├── tools/
│   ├── generate_location_refs.py  — Generate 72 missing location refs via 3x3 grids
│   ├── generate_expressions.py    — Pre-generate grayscale expression library
│   ├── build_upload_bundle.py     — Package prompts + refs for manual frontier upload
│   ├── backfill_storyboard.py     — Write spatial data + prompts back to Recoil
│   └── compare_frames.py          — CLI frame comparison utility
├── editors/
│   ├── production-console.html — Production Console (port 8430 /console) — Previz, Board, Dailies
│   ├── review.html          — Legacy frame review (deprecated, use Dailies tab)
│   └── review_server.py     — Serves console (--project required), / redirects to /console
├── config/
│   ├── starsend_config.json — Settings (recoil_root, defaults, complexity tiers)
│   └── model_profiles.json  — NBP, Flash, Kling, Veo capability definitions
├── assets/
│   └── expressions/         — Universal grayscale expression matrix (27 refs: 9 emotions × 3 intensities)
├── output/
│   ├── frames/ep_{NNN}/     — Generated grids + extracted panels
│   ├── bundles/             — Upload bundles for manual frontier model use
│   └── refs/locations/      — Generated location reference images
└── gemini_consultation/     — 4-round architecture consultation transcripts
```

---

## Locked Architectural Pillars (Gemini-Validated)

These decisions are FINAL. Do not deviate without re-consulting.

### 1. Native 9:16 for Production Frames
All final production frames generate at 9:16 aspect ratio via `aspect_ratio="9:16"` API parameter. Never generate at 1:1 and crop — ruins compositional framing.

### 2. 4K Grid Method for Planning + References
Grids (2x2, 3x3) at 4K (4096x4096) for scene planning, pose exploration, and reference generation. At 4K, a 3x3 grid yields 1365x1365 sub-panels — sufficient for hero selection and reference use after upscale.

**Why grids > independent candidates:** All panels in a single generation share the same latent seed, forcing consistent color palettes, lighting vectors, and character identities. Independent API calls use different seeds and produce inconsistent micro-details.

**Text-only grid prompting:** Prompt "Generate a 3x3 cinematic contact sheet" — do NOT upload blank gridline template images. Template images cause Gemini to interpret gridlines as physical scene elements. Text-only prompting activates latent understanding of "photographic contact sheets." **AVOID "character design sheet"** — this triggers illustration/concept art mode in NBP (ADR-C01). Use diegetic framing: "casting call contact sheet" for humans, "VFX studio contact sheet" for synthetics.

#### Structured Grid Types (Round 5 — Community-Validated)

Four grid strategies, used based on objective:

**1. Scene Coverage Grid (Structured 3x3):**
Map actual storyboard shots to grid positions. Each panel gets a specific shot from the JSON with its own framing/action. Generates foundational continuity anchors for an entire scene in one call.

**2. Director's Take Grid (Structured 2x2):**
For complex action shots: 4 specific cinematic variations of the same shot (standard take, dutch angle, tight framing, imposing angle). Extract the best as Hero Pose Ref.

**3. Action Burst Grid (Generic 2x2):**
When framing is locked but need subtle pose variations: "A 2x2 grid of the exact same Medium Shot, showing 4 slightly different poses."

**4. Skip Grids (Direct to Pro):**
Simple inserts, static talking heads. Straight to native 9:16 Pro render.

#### Visual Anchors (Critical Prompt Pattern)

Every grid prompt MUST include a `VISUAL ANCHORS` block listing 3-6 traits constant across all panels. Triggers Gemini's instruction-following weights to aggressively police continuity:

```
VISUAL ANCHORS (MUST REMAIN CONSTANT IN ALL PANELS):
- Environment: [location description from storyboard]
- Lighting: [lighting vector — direction + quality]
- Character: [name + key visual traits from breakdown]
- Prop: [signature props]
```

`prompt_engine.py` deterministically compiles Visual Anchors from storyboard JSON + breakdown.json (replaces LLM-driven "Scene Breakdown" step from community prompts — we already have the data).

#### Structured Grid Prompt Format

```
CRITICAL DIRECTIVE: Generate a single [2x2|3x3] cinematic contact sheet image.
Maintain strict continuity across all panels.

VISUAL ANCHORS (MUST REMAIN CONSTANT IN ALL PANELS):
- Environment: ...
- Lighting: ...
- Character: ...

PANEL ASSIGNMENTS:

PANEL 1 (TOP LEFT) - [label]:
Shot Type: [from storyboard]. Camera Angle: [from storyboard].
Action: [from storyboard]. Emotion: [from storyboard].

PANEL 2 (TOP RIGHT) - [label]:
...
```

Location refs: 3x3 grid = 9 angles of same location from 1 call.
Expression library: 3x3 with semantic gradient (Row 1: subtle, Row 2: moderate, Row 3: extreme).

### 3. Reference Ordering (Recency Bias)
The image Part closest to the text prompt has the highest attention weight. Order strictly:

```
Scene ENV ref           (weight 1-3, lowest attention)
Pose/Flash hero ref     (weight 4-5)
Expression ref          (weight 6-7, grayscale)
Character identity refs (weight 8-10, highest attention, closest to prompt)
[TEXT PROMPT]           (always last)
```

Character identity MUST be the last reference before the prompt text. Identity is the one thing that cannot fail.

### 4. Pristine Identity References
White-background character refs stay **unaltered**. Never tint, overlay, or color-correct them. Color contamination from white-bg refs is real — the Flash/grid planning pass (which generates in correct scene lighting) serves as the lighting anchor for the final Pro render.

### 5. Hardware Mirroring for Screen Direction
`ImageOps.mirror()` on reference images to enforce screen direction. Text prompts ("facing left") are weakly bound. Flip the ref image — Gemini anchors to visual orientation.

### 6. Expression Transfer (Universal Expression Matrix — ADR-C05)
**Problem:** "Blank Stare Bug" — neutral character reference photos override text prompts for emotion. If you feed 3 photos of Jade looking neutral and prompt "screaming in terror," you get calm Jade.

**Solution:** Universal grayscale expression matrix. 27 refs from a generic, bald, androgynous actor (9 emotions × 3 intensity levels). Stored in `assets/expressions/` as `{emotion}_{intensity}.png`.

**Why universal (not character-specific):** Feeding a hero identity image into expression generation causes "over-baking" — the model receives character identity twice (expression ref AND identity ref), producing rigid, mask-like faces. Universal refs transfer only muscle geometry.

**Why grayscale:** Eliminates skin-tone and lighting contamination via color channels. Gemini's cross-attention pulls only facial geometry/muscle tension from grayscale refs.

**Usage:** Pass expression ref at medium weight (6-7), labeled `[FACIAL EXPRESSION TO MATCH]`, before the character identity refs. Default intensity is "active" (middle row). Use "subtle" for internalized moments, "extreme" for peak emotion.

**Matrix structure:** 3 grids, each 3 emotions × 3 intensities:
- Grid 1: Joy, Anger, Sorrow
- Grid 2: Fear, Determination, Exhaustion
- Grid 3: Surprise, Disgust, Neutral
- Rows: Subtle/Internalized → Active/Conversational → Peak/Extreme

**Generation:** `python -m tools.prep_expressions` — run ONCE for entire show. 3 grids × $0.039 = $0.117 via Flash 3.1, temp 0.2.

### 7. Wide-Shot Prompt Branching
For WIDE/LS/EWS shots, **strip facial detail demands** from the prompt. At 9:16, a full-body shot renders the face at ~40x40 latent pixels — demanding detail causes "mushy monster" hallucination. Instead:

```python
if shot_type in ['WIDE', 'LS', 'EWS']:
    prompt += "Focus on full body silhouette, posture, and environmental scale. "
    prompt += "Facial features are indistinct at this distance."
else:
    prompt += "Anatomically flawless hands, perfect skeletal symmetry. "
    prompt += "Highly detailed facial features."
```

If client demands perfect faces on wide shots, use external FaceDetailer (SDXL Adetailer) as post-process. Do NOT fight the foundational model.

### 8. Positive Constraints Over Negative Language
Say "anatomically flawless hands, exactly five fingers, perfect skeletal symmetry" — NOT "no deformed hands, no extra fingers." Gemini's diffusion process responds better to positive mechanical constraints.

### 9. Kinetic Descriptors Over Semantic Emphasis
Replace ALL CAPS shouting with camera-artifact-based language:
- BAD: `ACTION & EMOTION: She is PUSHING HARD against the panel`
- GOOD: `Motion blur, kinetic energy, unbalanced dynamic pose, muscles taut, off-axis framing, dust kicked up into the lens`

### 10. Lighting Vector Locking
Every prompt within a scene must include explicit directional lighting coordinates: "amber light casting hard shadows from the TOP LEFT." Not just "amber light." Consistent lighting vectors lock environments across shots.

### 11. No Output Chaining Between Shots
Do NOT feed Shot N's generated output as a reference for Shot N+1. This causes "Generation Drift" — each generation degrades like a photocopy of a photocopy. By Shot 5, proportions warp and contrast deep-fries. Always anchor back to pristine ENV reference and pristine identity refs.

### 12. Two-Character Shot Reference Budget
Cap at **7 references total** for dual-character shots:
Scene (1) + Flash Pose (1) + Jade Expression (1) + Jade Identity (2) + Wren Identity (2) = 7

Drop Wren's expression ref (he's mechanical — emotion via posture/prompt, not micro-expressions). Drop 3rd identity ref for both characters — 2 pristine refs are sufficient when placed last.

**Prompt bleeding prevention:** Use spatial syntax — "LEFT SIDE OF FRAME: [Jade]. RIGHT SIDE OF FRAME: [Wren]." Separate character descriptions with periods, not commas.

### 13. Casting Grid Configuration (ADR-C01–C07)

Casting grids use **diegetic framing** to force photorealism from NBP. Key rules:

| Rule | Detail |
|------|--------|
| **Diegetic frame (human)** | "A live-action casting call contact sheet. 9 DIFFERENT actors auditioning for the exact same role." |
| **Diegetic frame (synthetic)** | "A practical VFX studio contact sheet. 9 different high-budget animatronic prop tests." |
| **Camera anchor** | "35mm motion picture film still, shot on Arri Alexa 65, 85mm f/2.8 lens" |
| **Background** | 18% neutral gray seamless backdrop (NOT white — white causes light wrap/edge blowout) |
| **Anti-airbrush** | "Unretouched photorealism. Visible skin pores, peach fuzz, micro-imperfections. DO NOT AIRBRUSH." |
| **Anti-illustration** | "DO NOT RENDER. NO ILLUSTRATION. NO 3D MODELS. NO CONCEPT ART." |
| **Codenames** | Never inject IP-loaded character names (JADE → Subject_Alpha). Codename map in `prep_character_angles.py`. |
| **Temperature** | 0.4 (higher → stylistic drift into illustration) |
| **Aspect ratio** | 1:1 at 4K for concept grids |
| **Grid stability** | "Strictly isolated panels, no overlapping elements" + "keep the age range identical" |
| **Description density** | Keep descriptions >40 words (short descriptions cause NBP to over-index on nouns) |
| **Gender anchoring** | Always include explicit gender (Female/Male) at the start of PHYSICALITY line. Pronouns in descriptive prose do not override visual defaults. Use `--gender` flag or `_GENDER_MAP` in `prep_character_angles.py`. |
| **Final identity refs** | rembg extracts from gray → composite onto white #FFFFFF (not transparent alpha) |

**Validation:** Flash Sandbox protocol — test prompts on Flash 3.1 ($0.039) before NBP ($0.134). Same semantic brain, lower cost.

---

## 3-Pass Production Pipeline

### Complexity Tiers

| Tier | Pipeline | Cost/Shot | Use For |
|------|----------|-----------|---------|
| `simple` | Straight to Pro | $0.134 | Inserts, static wides, prop CUs |
| `standard` | Grid exploration → Pro | ~$0.17-0.30 | Action, character shots |
| `complex` | Grid + Expression + multi-Pro | ~$0.56 | Two-shots, heavy emotion |

### Standard Pipeline (per shot)

```
1. Grid Planning (3x3 at 4K, 1:1)
   Model: gemini-3.1-flash-image-preview ($0.039) or gemini-3-pro-image-preview ($0.134)
   Prompt: "Generate a 3x3 grid collage of 9 cinematic stills showing [scene]..."
   Refs: Scene ENV + Character Identity (weight-ordered)
   Output: 4096x4096 grid image

2. Hero Extraction
   Select best sub-panel (human via review UI or VLM auto-pick)
   Extract 1365x1365 panel → crop to 9:16 (768x1365)
   Optional: upscale via SeedVR2

3. Pro Final Render (native 9:16)
   Model: gemini-3-pro-image-preview ($0.134)
   Refs (weight-ordered):
     - Scene ENV ref (weight 1)
     - Grid hero as pose/composition ref (weight 5)
     - Grayscale expression ref if needed (weight 7)
     - Character identity refs x2 (weight 9-10)
   Prompt: Full cinematic prompt with kinetic descriptors + lighting vector
   Output: Native 9:16 production frame
```

### Scene-Level Workflow

```
Per Scene in Episode:
  1. Generate ENV anchor — 9:16 via Pro, sanitized prompt (no people)
     First ENV shot becomes SCENE REFERENCE for all subsequent shots
  2. For each character shot:
     a. Determine complexity tier (simple/standard/complex)
     b. Run appropriate pipeline
     c. Save to output/frames/ep_{NNN}/
  3. Log costs to cost_tracker
```

---

## ENV Sanitization

Strip human-presence language from environment-only shots. Gemini's safety weights are strong — even saying "no people" activates human concepts. Strip the language entirely:

```python
_HUMAN_PATTERNS = [
    r",?\s*a\s+figure'?s?\s+\w+\s+visible\s+[^,\.]*[,\.]?",
    r",?\s*\b(?:a|the)\s+(?:figure|person|silhouette|someone|somebody)\b[^,\.]*[,\.]?",
    r"\b(?:her|his|their)\s+(?:arms?|hands?|fingers?|face|body|boots?|feet|foot|legs?|torso|shoulders?)\b",
    r"(?:^|\.\s*)\b(?:She|He)\s+[^\.]+\.",
    r"\b(?:both|two)\s+(?:arms?|hands?|fingers?|legs?|feet)\b",
    r"\bthe\s+figure\b",
    r"\bfigure'?s?\b",
]
```

Plus ENV block: "CRITICAL: This is an ENVIRONMENT-ONLY shot. ABSOLUTELY NO PEOPLE in ANY panel."

---

## Models

| Model ID | Cost | Use For |
|----------|------|---------|
| `gemini-3-pro-image-preview` | $0.134/img | Final production frames, ENV anchors |
| `gemini-3.1-flash-image-preview` | ~$0.039/img | Grid exploration, planning passes |
| `gemini-2.5-flash-image` | $0.039/img | Fallback exploration if 3.1 unavailable |

**Rate limiting:** 3.1 Flash has strict QPS limits. Use `tenacity` for exponential backoff. Cap concurrent API calls to 2.

---

## Recoil Data Sources (Read-Only)

| File | What We Use |
|------|-------------|
| `projects/{project}/_pipeline/shot_plans/storyboard_ep_{NNN}.json` | Shot data (40+ fields per shot) |
| `projects/{project}/visual/breakdown.json` | Characters, locations, wardrobe phases |
| `projects/{project}/visual/project_config.json` | Camera body, film stock, HEX palette, budget, prop patterns |
| `projects/{project}/visual/lora_candidates/{CHAR}/picks/` | Curated white-bg character refs |
| `projects/{project}/visual/lora_candidates/{CHAR}/keystones/` | Identity anchor (e.g. Jade_Hero.jpeg) |
| `recoil/lib/config_loader.py` | Project config, identity lock templates |

---

## Cost Estimates

| Scope | Simple Tier | Standard Tier | Complex Tier | Blended |
|-------|-------------|---------------|--------------|---------|
| EP001 (31 shots) | $1.34 (10) | $4.50 (15) | $3.36 (6) | ~$9.20 |
| Full season (1,800 shots) | — | — | — | ~$534 |

---

## Implementation Risks & Mitigations

1. **Prompt bleeding in two-character shots** → Spatial syntax ("LEFT SIDE: ... RIGHT SIDE: ..."), periods not commas
2. **Mushy wide-shot faces** → Wide-shot branching strips facial demands; FaceDetailer post-process if needed
3. **API rate limiting (HTTP 429)** → Exponential backoff via tenacity, max 2 concurrent workers
4. **Anatomy hallucination on complex poses** → Downgrade to simple tier, use sketch/blockout as pose ref
5. **Aspect ratio rejections** → Always use native `aspect_ratio="9:16"` API parameter

---

## Gemini Consultation Reference

4-round architecture review (Feb 26, 2026) with Gemini 3.1 Pro Preview.
Full transcripts: `starsend/gemini_consultation/`

| Round | Topic | Outcome |
|-------|-------|---------|
| 1 | Initial analysis | Grid templates rejected, aspect ratio flaw identified, recency bias discovered |
| 2 | Pushback + merge | Expression Transfer pattern, wide-shot branching, reference ordering finalized |
| 3 | Convergence | Final architecture locked, cost model, EP001 test protocol |
| 4 | Grid method pushback | Grids reinstated for planning at 4K, text-only prompting validated |
| 5 | Structured grids | Visual Anchors pattern, 4 grid types, exact Shot 2 test prompt |

---

## Gemini Re-Consultation Protocol

Architecture decisions are tagged by the model behavior they depend on. When models change, re-consult:

```bash
python3 gemini_consult.py --round 1   # Send updated context, get fresh analysis
```

**Triggers for re-consultation:**
- New Gemini model version (architecture changes may invalidate attention/ordering assumptions)
- API capability changes (new aspect ratios, higher ref limits, native multi-image)
- Pricing changes affecting tier economics
- Production findings contradicting consultation assumptions

**Decisions most likely to change with new models:**
- Recency bias ordering (depends on attention mechanism)
- Expression Transfer via grayscale (depends on cross-attention modularity)
- Color contamination from white-bg refs (depends on illumination sampling)
- Wide-shot face degradation threshold (depends on latent resolution)

Full decision-to-model-behavior mapping: `gemini_consultation/README.md`

---

## Phase A: Video Pipeline Router Architecture (Feb 27, 2026)

Evolved from linear 3-pass still-image pipeline into a **Router-Pipeline architecture** with 4 sub-pipelines. Validated via 3-round Gemini consultation (`consultations/full_review_feb27/SYNTHESIS.md`).

### 4 Sub-Pipelines

| Pipeline | Engine | API Pattern | Use Case | Cost |
|----------|--------|-------------|----------|------|
| **Still** | NBP (Gemini 3 Pro) | genai_inline | Keyframes, inserts, ENV shots | $0.134/img |
| **I2V** | Kling V3 | kling_rest | Start+end frame precision | $0.10/sec |
| **T2V** | SeedDance 2.0 / Kling / Veo | fal_ai / kling_rest / genai_inline | Character motion, dialogue | varies |
| **Multi-Shot** | SeedDance 2.0 | fal_ai | Scene batching (3-8 shots) | $0.0133/sec |

### Routing Priority (`scene_planner.route_shot()`)

1. Scene eligible for batching → **multi_shot** (SeedDance)
2. Requires start+end frame precision → **i2v** (Kling)
3. Static/INSERT/ECU of object → **still** (NBP)
4. Complex camera movement or >15s → **t2v** (Veo)
5. Dialogue or 2+ characters → **t2v** (SeedDance)
6. Default → **t2v** (Kling V3)

### Multi-Shot Batching Criteria

Scenes qualify for `seeddance-2.0` multi-shot batching when:
1. 3-8 shots in the scene
2. All shots share the same location
3. No shots require I2V precision (start/end frame)
4. Max 4 unique characters across batch

### Video Models

| Model | Cost | Max Duration | Key Feature |
|-------|------|-------------|-------------|
| `seeddance-2.0` | $0.0133/sec | 20s | Multi-shot, lip sync, 9 refs |
| `kling-v3` | $0.10/sec | 15s | Start+end frame I2V |
| `veo-3.1` | $0.05/sec | 60s | Long shots, scene extension |

### Unified API Client (`lib/api_client.py`)

Factory pattern routes to model-specific clients:
- `GoogleGenaiClient` — NBP, Flash, Veo 3.1 (implemented)
- `KlingClient` — Kling V3 (JWT auth, REST API, polling — implemented)
- `SeedDanceClient` — SeedDance 2.0 via fal.ai (stub, Phase B)

`generate_with_fallback()` tries primary model, then `fallback_model` if defined.

### Video Prompt Builders (`lib/prompt_engine.py`)

- `build_video_prompt()` — Single-shot video with duration, camera movement, audio
- `build_multi_shot_prompt()` — Scene batch with shared visual anchors + per-shot sections
- `build_previs_prompt()` — Simplified mechanical format for Flash previs gate
- `build_kling_i2v_prompt()` — I2V prompt for Kling V3
- `build_kling_t2v_prompt()` — T2V prompt for Kling V3 (75-100 words)

### EDL/FCPXML Export (`tools/export_edl.py`)

Skeleton for Phase C. Will generate CMX 3600 EDL or FCPXML 1.11 for NLE import (Premiere Pro / DaVinci Resolve).

### Full Season Video Cost Estimate

~$1,732 total (~$28.86/episode) with full pipeline:
- Extraction: $0.01/ep
- Previs: $1.52/ep
- Keyframes: $5.23/ep
- Video: $21.80/ep
- QC Gates: $0.30/ep

---

## Phase B: Holistic Visual Pipeline (Feb 27, 2026)

Implements 9 ADRs from holistic Gemini consultation (`consultations/holistic_visual_pipeline_review/SYNTHESIS.md`).

### Sub-Agent Extraction Skills (ADR H08)

4 dedicated sub-agent skills for extraction pipeline stages:

| Skill | Purpose | Output |
|-------|---------|--------|
| `/camera-test` | Break screenplay into shot boundaries (28-41/ep) | `state/visual/camera_tested/ep_NNN.json` |
| `/global-bible` | Synthesize characters, locations, props, lighting | `state/visual/global_bible.json` |
| `/storyboard-pass` | Produce 5-consumer-group shot records | `state/visual/plans/ep_NNN_plan.json` |
| `/prompt-gen` | Compile model-specific prompts per shot | Enriched plan with compiled prompts |

Extraction uses Opus 4.6 by default (ADR H07). Gemini configurable as fallback.

### Character Ref Pipeline (`tools/prep_character_angles.py`)

Two paths (ADR H01):
- **Path A:** Existing hero → Qwen Multi-Angle → rembg → white background → Gate 0
- **Path B:** Text description → NBP 3x3 grid (concepting/casting) → pick → angles

Output: `output/refs/characters/{CHAR_ID}/`

### 4-Gate Validation Framework (`lib/validation.py`, ADR H03)

| Gate | What | Cost | Action on Fail |
|------|------|------|----------------|
| Gate 0 | Ref set consistency | $0.039 | Block character pipeline |
| Gate 1 | Mechanical artifacts | $0.039 | Retry 2x, then failed |
| Gate 2 | Semantic identity check | $0.039 | keyframe_rejected → human review |
| Gate 3 | Video identity drift | $0.039/frame | Flag for review, do NOT auto-reject |

All gates use Flash 3.1. Binary JSON format (pass/fail), not 1-10 scoring.

### Mandatory Previs Gate (`tools/generate_previs.py`, ADR H02)

Flash 3.1 previs frame per shot ($0.039/frame, ~$1.52/episode) before keyframe/video generation. Gate 1 mechanical QC on each frame.

### Two-Layer Plan + Log (ADR H04)

- **Layer 1 — Plan** (`state/visual/plans/ep_NNN_plan.json`): Shot specs, routing, 5 consumer groups
- **Layer 2 — Log** (`orchestrator/manifest.py` → `EpisodeLog`): Generation tracking, takes, costs, approvals

Shot status state machine:
```
previs_pending → previs_generating → previs_generated → previs_approved →
keyframe_pending → keyframe_generating → keyframe_generated → keyframe_approved →
video_pending → video_submitted → video_processing → video_ready →
video_downloading → video_complete
```

### Session-Based Crash Recovery (ADR H05)

- UUID `session_id` per pipeline run
- Orphan detection: shots with status=processing + session_id mismatch
- Recovery: poll job_id 10min max, then re-submit
- Atomic writes: temp file + `os.replace()`

### Review Server Previs Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/previs/{episode}` | GET | List previs frames with statuses |
| `/api/approve-previs` | POST | Approve previs → Layer 1 human_approvals |
| `/api/execution/{episode}` | GET | Return execution plan state |

---

## Related

- **Gemini consultation (stills):** `starsend/gemini_consultation/` (5-round transcripts + README with decision index)
- **Gemini consultation (video):** `starsend/consultations/full_review_feb27/SYNTHESIS.md` (3-round video pipeline review)
- **Holistic pipeline consultation:** `starsend/consultations/holistic_visual_pipeline_review/SYNTHESIS.md` (9 ADRs)
- **Consultation script:** `starsend/gemini_consult.py` (reusable, supports N rounds with full context carry)
- **Recoil test script:** `recoil/tools/test_nbp_direct.py` (V4 triptych, port patterns)
- **Recoil prompt compiler:** `recoil/lib/prompt_compiler.py` (data resolution logic)
- **Recoil config loader:** `recoil/lib/config_loader.py` (identity locks, project config)
- **Obsidian doc:** `$VAULT/02_Projects/Recoil-Studios/Starsend-Architecture.md`
- **Memory context:** `~/.claude/projects/.../memory/starsend-context.md`
