# Generation & the Model Router

CoreReflex's AI is orchestrated **first-party on Google Vertex AI** and dispatched through a small, dependency-free model router. This doc maps the generation subsystem as it exists in `src/`: the central model registry, the provider/model router, the camera-motion channel, the direct Veo-via-Vertex path (including last-frame and reference-image conditioning), the **agentic film director** and its **PRODUCE → CRITIQUE → ASSEMBLE** producer loop with real per-shot QA, the full set of supporting modalities (music, images, logos, mask editing, voice, captions, video-native highlight detection, styled captions, story narration, semantic search, and more), the portable JSON prompt-spec engine, and the inference trace that rides with every artifact.

Everything here is plain Node `fetch` against Google APIs — no SDK, no aggregator client. Google's models (Gemini, Veo, Imagen, Lyria, Chirp) are named as the integrations they are; the orchestration, routing, QA, and prompting layers are ours. Optional externals: **fal.ai** for Kling (queue REST) and **xAI Grok Imagine** for photo/video with native audio (`XAI_API_KEY` → `https://api.x.ai/v1`). A deterministic **mock** keeps the whole pipeline runnable and testable with zero keys.

## On this page

- [At a glance](#at-a-glance)
- [The central model registry (`src/models.js`)](#the-central-model-registry-srcmodelsjs)
- [The model router (`src/providers.js`)](#the-model-router-srcprovidersjs)
- [The multi-model capability registry and self-hosted lanes (`src/models-registry.js`)](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs)
- [Camera motion (`src/camera.js`)](#camera-motion-srccamerajs)
- [Video generation orchestration (`src/generation.js`)](#video-generation-orchestration-srcgenerationjs)
- [Direct Veo via Vertex AI (`src/vertex.js`)](#direct-veo-via-vertex-ai-srcvertexjs)
- [The shared Gemini seam (text / stream / tool-use / vision)](#the-shared-gemini-seam-text--stream--tool-use--vision)
- [The agentic cinema director (`src/director.js`)](#the-agentic-cinema-director-srcdirectorjs)
- [The agentic producer + per-shot QA](#the-agentic-producer--produce--critique--assemble-srcproducerjs)
- [LLM-judge plan evaluation (`src/eval.js`)](#llm-judge-plan-evaluation-srcevaljs)
- [Supporting modalities](#supporting-modalities)
- [The portable prompt-spec engine (`src/prompts/`)](#the-portable-prompt-spec-engine-srcprompts)
- [Inference tracing (`src/inference.js`)](#inference-tracing-srcinferencejs)
- [Upscaling and the 8K master path](#upscaling-and-the-8k-master-path)
- [Roadmap / not-yet-shipped](#roadmap--not-yet-shipped)

For the strategy-level deep dives, see the parent-folder references:

- [../MODEL-ROUTER.md](../MODEL-ROUTER.md) — the proposed capability-driven `MODELS` registry, routing policy, and resolution tiers (and the native-4K correctness bug flagged below — still open).
- [../PROMPTING.md](../PROMPTING.md) — the four-layer prompting roadmap (structured schema → multi-pass director → LLM-judge gate → execution). Layers 1–3 have shipped; see the re-baselined [roadmap](#roadmap--not-yet-shipped).
- [../UPSCALING-8K.md](../UPSCALING-8K.md) — the governed 8K "Master" stage (the v1 own-tech upscaler has shipped; the SR-model lane has not).

Sibling dev docs: [api-reference.md](api-reference.md) for the raw routes, [editor.md](editor.md) for how the editor consumes voices/moods/captions, [render-worker.md](render-worker.md) for the MP4 render/master path and the ffmpeg-backed engine endpoints, [auto-clips-pipeline.md](auto-clips-pipeline.md) for the full long-form → shorts pipeline these modalities feed, [own-the-stack.md](own-the-stack.md) for the self-hosted voice/avatar/upscale seams, [data-model.md](data-model.md) for the `assets` / `assets.embedding` schema, [configuration.md](configuration.md) for every env var.

---

## At a glance

| Modality | Module | Model (default) | Verb | Sync? | Route |
|----------|--------|-----------------|------|-------|-------|
| Video | `src/generation.js` + `src/providers.js` + `src/vertex.js` + `src/xai.js` | `veo-3.0-generate-001` (Vertex) / Kling on fal / Grok Imagine on xAI | `:predictLongRunning` → poll; xAI `/v1/videos/generations` → poll | async (202 + poll) | `POST /api/generate`, `GET /api/generate/:id` |
| Film plan | `src/director.js` | `gemini-2.5-flash` | `:generateContent` (schema'd) | sync | `POST /api/director/plan` |
| Full film (agentic) | `src/producer.js` | (drives `generation.js`) | orchestrates Veo shots | sync, long-running | `POST /api/director/produce` |
| Plan evaluation (LLM judge) | `src/eval.js` | `gemini-2.5-flash` | `:generateContent` (schema'd) | sync | attached as `plan.eval` when `DIRECTOR_EVAL=true` |
| Per-shot QA | `src/qa-analyzer.js` | `gemini-2.5-flash` (multimodal) | `:generateContent` (schema'd) | sync | internal (producer CRITIQUE) |
| Music | `src/music.js` | `lyria-002` | `:predict` | sync | `POST /api/music`, `GET /api/music/moods` |
| Image | `src/imagen.js` | `imagen-3.0-generate-002` / Grok Imagine / fal / self-host | `:predict` or xAI `/v1/images/generations` | sync | `POST /api/imagen` |
| Logo (batch concepts) | `src/imagen.js` (`generateLogoOptions`) | `imagen-4.0-generate-001` (`IMAGEN_LOGO_MODEL`) | `:predict` ×N in parallel | sync | `POST /api/logo/options` |
| Image mask editing | `src/image-edit.js` | `imagen-3.0-capability-001` (`IMAGEN_EDIT_MODEL`) | `:predict` | sync | `POST /api/image/edit` |
| Conversational canvas edits | `src/image-assist.js` | `gemini-2.5-flash` (text) | `:generateContent` | sync | `POST /api/image/assist` |
| Auto-reframe subject box | `src/reframe.js` | `gemini-2.5-flash` (vision) | `:generateContent` | sync | `POST /api/image/subject-box` |
| Background removal | `src/remove-bg-engine.js` | render-worker (Vertex segment + ffmpeg alpha) | worker HTTP | sync | `POST /api/image/remove-bg` |
| Vectorize (raster → SVG) | `src/vectorize-engine.js` | render-worker (VTracer) | worker HTTP | sync | `POST /api/image/vectorize` |
| Upscale (own-tech) | `src/upscale.js` + render-worker | ffmpeg lanczos v1 (`UPSCALER=sr` seam) | worker HTTP | sync | `POST /api/upscale` |
| Voice (TTS) | `src/voice.js` | `en-US-Chirp3-HD-*` | `texttospeech.googleapis.com` | sync | `POST /api/voice`, `GET /api/voice/voices` |
| Captions (STT) | `src/captions.js` | `latest_long` (Speech-to-Text v2) | `…:recognize` | sync | `POST /api/captions` |
| Caption translation | `src/captions.js` | `gemini-2.5-flash` (text) | `:generateContent` | sync | `POST /api/captions/translate` |
| Transcript highlights | `src/highlights.js` (`rankHighlights`) | `gemini-2.5-flash` (text) | `:generateContent` | sync | `POST /api/captions/highlights` |
| Video-native highlights | `src/highlights.js` (`detectHighlightsFromVideo`) | `gemini-2.5-flash` (video) | `:generateContent` (schema'd) | sync | `POST /api/highlights/detect` |
| Styled caption generation | `src/caption-generate.js` + `src/caption-styles.js` | `gemini-2.5-flash` (video) | `:generateContent` (schema'd) | sync | `POST /api/captions/generate` |
| Story narrator | `src/story-narrator.js` | `gemini-2.5-flash` (text) | `:generateContent` | sync | `POST /api/auto-clips/story` |
| Prompt enhancement | `src/enhance.js` | `gemini-2.5-flash` | `:generateContent` | sync | `POST /api/enhance` |
| Brand voice synthesis | `src/enhance.js` | `gemini-2.5-flash` | `:generateContent` | sync | `POST /api/brand-voice` |
| Soundscapes | `src/soundscape.js` | **none — procedural CPU synth** | — | sync | `POST /api/soundscape`, `GET /api/soundscapes` |
| Embeddings | `src/embeddings.js` | `gemini-embedding-001` | `:predict` | sync, best-effort | internal |
| Semantic media search | `src/gallery.js` (`searchWorkspaceAssets`) | embedding + pgvector | SQL `<=>` | sync | `GET /api/media/search?q=` |
| Prompt specs | `src/prompts/registry.js` + `specs/*.json` | — | — | — | `GET /api/prompts`, `GET /api/prompts/:id` |

All Vertex modalities authenticate with **one** service account (`GCP_SERVICE_ACCOUNT_JSON`) via the signed-JWT grant in `src/vertex.js` (`getAccessToken`). The access token is cached process-wide until ~60s before expiry.

> **Self-hosted alternatives.** Beyond the vendor rows above, video generation has **two first-party self-hosted lanes** in the capability registry — **CoreReflex 1.0** (`corereflex-1`) and **CoreReflex Long-Form** (`corereflex-lf`) — reached through `SELFHOST_VIDEO_URL` and the inference gateway; see [the capability-registry section](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs). Voice (`VOICE_ENGINE_URL` / per-engine URLs like `VOICE_KOKORO_URL`), lip-sync/avatar (`PERSONA_LIPSYNC_URL` / `AVATAR_ENGINE_URL`), and upscale (`UPSCALE_URL`) run behind the same engine-agnostic HTTP seams — swap an env var, not the code. See [own-the-stack.md](own-the-stack.md). All self-hosted lanes are **white-labeled** as CoreReflex custom models ([`src/model-disclosure.js`](../../src/model-disclosure.js)); the open-model name is an internal NOTICE only. User-supplied media URLs the server forwards to these seams are screened by the SSRF guard `safePublicUrl` in [`src/net-guard.js`](../../src/net-guard.js) (https-only; rejects loopback / private / link-local / cloud-metadata hosts).

---

## The central model registry (`src/models.js`)

`src/models.js` is the **one source of truth for every Vertex model ID the app uses** — changing a model is one env var, one place. `models(env)` returns:

| Key | Default | Env override | Used for |
|-----|---------|--------------|----------|
| `gemini` | `gemini-2.5-flash` | `GEMINI_VERTEX_MODEL` | Director reasoning, agent loop, eval, QA analyzer, highlights, styled captions, enhancement, chat |
| `geminiPro` | `gemini-2.5-pro` | `GEMINI_PRO_VERTEX_MODEL` | Deeper reasoning (pro tier, via `routeModel`) |
| `veo` | `veo-3.0-generate-001` | `VEO_VERTEX_MODEL` | Video generation |
| `imagen` | `imagen-3.0-generate-002` | `IMAGEN_VERTEX_MODEL` | Image generation |
| `lyria` | `lyria-002` | `LYRIA_VERTEX_MODEL` | Music |
| `embedding` | `gemini-embedding-001` | `EMBED_VERTEX_MODEL` | Embeddings / semantic recall |
| `live` | `gemini-live-2.5-flash` | `LIVE_VERTEX_MODEL` | Real-time voice agent (Gemini Live) |
| `stt` | `chirp-2` | `CAPTIONS_STT_MODEL` | Speech-to-text (captions) |

`NEWEST_GENERATION` (also exported) is a frozen map of suggested newest-generation IDs to flip to via env (e.g. `GEMINI_VERTEX_MODEL=gemini-3-flash`, `IMAGEN_VERTEX_MODEL=imagen-4.0-generate-001`) — verify the exact slug in the project's Model Garden first, since preview IDs carry version/date suffixes.

### Intent-based quality routing

`qualityTier(text, env)` reads a brief for tier cues and returns `'high' | 'draft' | 'standard'`:

- **High triggers** (`QUALITY_HIGH_TRIGGERS`, comma-list override): `masterpiece, award-winning, cinematic, photorealistic, hyper-realistic, ultra-detailed, 4k, 8k, premium, flagship, best quality`, …
- **Draft triggers** (`QUALITY_DRAFT_TRIGGERS`): `draft, sketch, rough, quick, wireframe, mockup, lo-fi, placeholder, thumbnail`, …

`routeModel(text, env)` resolves that to `{ tier, model, modifier }`: a `high` ask picks `geminiPro` and attaches a fidelity prompt modifier ("Render at the highest fidelity: crisp detail, cinematic lighting…"); everything else stays on the fast/cheap `gemini`. Today the **modifier** is consumed by image generation (`src/imagen.js:83` folds it into the composed prompt), while the pro-model pick is available to any caller — no reasoning path routes to `geminiPro` through it yet (see [Gaps in the roadmap](#roadmap--not-yet-shipped)).

### Registry vs. capability registry — don't confuse the two

`src/models.js` answers *"which model ID do we call?"* for the **Vertex** modalities. It is not the same thing as the capability-driven registry in [`src/models-registry.js`](../../src/models-registry.js) — the per-model `nativeMaxResolution` / `upscaleTo` / duration / mode / continuity spectrum that drives video routing. **That capability registry has now shipped** (it was the MODEL-ROUTER.md proposal; see [the dedicated section below](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs)).

> **The native-4K mislabel is fixed.** The old bug — `PROVIDERS` hard-coding `kling`, `veo`, *and* `mock` all `native4k: true` while Veo is really native 1080p — is gone. `PROVIDERS` is now **derived** from the capability registry (`providersFromRegistry` sets `native4k` from the honest `nativeMaxResolution`), so Veo correctly reports `native4k: false` with an `upscaleTo: '4k'` plan. `qaResolution` now *allows* a 4K request when the model has either native 4K **or** a valid upscale plan, and stamps the result `upscaled-4k` vs `native-4k` accordingly (`planForModel.resolutionStamp`). The own-tech upscaler (`src/upscale.js` + `POST /api/upscale`) is the sibling-derivative that realizes the plan. The gate no longer lies about nativeness.

Two smaller registry drifts worth knowing:

- **STT default divergence.** `models().stt` defaults to `chirp-2`, but the live transcription path (`transcribeFromStorage` in `src/captions.js:18`) has its own fallback of `latest_long` and never reads the registry. Both honor `CAPTIONS_STT_MODEL`, so set the env var explicitly if you care which model runs; unset, `/api/captions` uses `latest_long` (it reliably emits word offsets in every region).
- `src/catalog.js:65` advertises the `/api/voice` (TTS) endpoint with `m.stt` — the wrong model in the public capability catalog.

---

## The model router (`src/providers.js`)

The router is a tiny set of pure functions. Its `PROVIDERS` map is **no longer a hard-coded literal** — it is `providersFromRegistry()`, a back-compat view **derived from the capability registry** ([`src/models-registry.js`](../../src/models-registry.js)), so every row's `native4k` comes from the honest `nativeMaxResolution`. New code should call `selectRoute(spec)` (→ `routePolicy`) for the full route decision (model + generation resolution + upscale plan); `selectProvider` remains as a thin back-compat wrapper over it. The registry carries far more than the three legacy providers — Kling, Veo (Vertex), Veo (fal), Wan, Hailuo, Seedance, the two self-hosted **CoreReflex** lanes, and Mock — each env-gated by whether its credentials are configured. See [the capability-registry section](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs) for the full spectrum.

### `normalizeSpec(input)` — the untrusted-input firewall

Every request body becomes a normalized internal `GenerationSpec` before anything touches a model. It clamps and allow-lists every field:

- `prompt` sliced to 2000 chars, `negativePrompt` to 1000.
- `aspectRatio` ∈ `{16:9, 9:16, 1:1}` (default `16:9`); `resolution` ∈ `{720p, 1080p, 4k}` (default `4k`).
- `durationInSeconds` clamped to **[3, 15]** (default 5). **Gotcha:** Veo only accepts `{4, 6, 8}`s — `src/vertex.js` snaps to the nearest via `snapVeoDuration` (configurable with `VEO_DURATIONS`). Lyria is fixed ~30s.
- `mode` = `frames` when a `startImageUrl` is present, else `text` (T2V vs I2V).
- `cfgScale` ∈ [0, 1] (default 0.5), `seed` truncated to an int or `null`, `audio.enabled` (default true).
- `ingredients` = up to 3 `https?://` URLs (Kling "elements"; **Veo 3.1 reference images** on the Vertex path — see below); `startImageUrl` / `endImageUrl` URL-validated.
- `camera` normalized via `normalizeCamera` (`src/camera.js`) — a bare move string or the director's `{ move, intensity }` object.

### `selectProvider(spec, env)` / `selectRoute(spec, env)` — the routing ladder

`selectProvider` now delegates to `selectRoute → routePolicy` (the capability-registry router) and returns the chosen model as a back-compat provider view. The ladder `routePolicy` runs:

```
explicit spec.provider / spec.model pin wins (honored even if unconfigured)
  → GENERATION_FORCE_MOCK=true              → mock
  → else: filter to CONFIGURED models that support the mode (t2v/i2v/continue),
          the continuity need, and can reach the wanted resolution (native OR upscale)
  → prefer Vertex Veo over Veo-on-fal when both are configured
  → no survivor + nothing configured        → mock in dev/tests;
                                              503 in production (unless GENERATION_ALLOW_MOCK=true)
  → score survivors by tier:
        hero / fidelity / premium           → highest qualityRank  (Veo)
        draft / economy / cost-ceiling       → cheapest per-second  (Wan / CoreReflex / Hailuo)
        standard                            → prefer native-4K, then quality
```

So the default is **cost-aware, not Veo-first**: with several providers configured, everyday `standard`/`economy` traffic prefers the cheaper configured models and **Veo is reserved for hero/fidelity or an explicit pin** (`GENERATION_DEFAULT_TIER=economy` makes every untiered request cost-first). The production honesty gate still holds: with **no** provider configured and `NODE_ENV=production`, `routePolicy` **throws a 503** ("Video generation is unavailable — no provider is configured…") rather than silently emitting a `mock://` clip. `vertexConfigured(env)` is true when both a project (`VERTEX_PROJECT` / `GOOGLE_CLOUD_PROJECT`) and `GCP_SERVICE_ACCOUNT_JSON` are present. See [the capability-registry section](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs) for the full model spectrum and the honest resolution stamping.

### `resolveVendor(provider, env)` — provider → runtime vendor

A *provider* is the catalog entry; the *vendor* is where the call actually goes. `veo` resolves to `vertex` when Vertex is configured, otherwise its static `vendor` (`fal`). `mock` stays `mock`. This is why `PROVIDERS.veo.vendor` is `'fal'` but production calls land on Vertex.

### `qaResolution(spec, provider, routePlan)` — the honest 4K gate

A `4k` request now passes when the provider is **either** native-4K **or** has a valid upscale plan (`upscaleTo` reaching 4K/8K). Native passes silently; an upscale path emits an `info` finding carrying the `upscalePlan` and stamps `upscaled-4k`; only a provider with **neither** native 4K nor an upscale route is `block`ed. This is the correct behavior the old `native4k`-boolean gate couldn't express — see [the capability-registry callout](#registry-vs-capability-registry--dont-confuse-the-two) above.

### Provider request shaping

`buildFalRequest(providerId, spec)` maps the normalized spec into each vendor's body — and the field names differ:

- **Kling**: `prompt` composed via `composeCameraPrompt` (camera clause leads), `duration` as a **string** (`"5"`), `cfg_scale`, `generate_audio`, `start_image_url` / `end_image_url`, `elements: [{ image_url }]`, and — when `KLING_CAMERA_CONTROL=true` — a structured `camera_control` object. Model slug `KLING_MODEL_I2V` / `KLING_MODEL_T2V` (defaults target the native-4K `fal-ai/kling-video/v3/4k/*` endpoints).
- **Veo on fal**: `prompt` (same camera composition), `aspect_ratio`, `duration` as `"5s"`, `generate_audio`, `image_url`. Slug `VEO_MODEL_I2V` / `VEO_MODEL_T2V` (default `fal-ai/veo3*`).

`runFalGeneration` is a submit → poll `status_url` → read `response_url` loop against `https://queue.fal.run` (2.5s poll, 600s timeout). All model slugs are env-overridable because vendors rev them often — **verify slugs against fal before prod**.

---

## The multi-model capability registry and self-hosted lanes (`src/models-registry.js`)

This is the "spectrum brain" the MODEL-ROUTER.md proposal became. Every video model is a row of **honest capability data** — `nativeMaxResolution`, `upscaleTo`, `maxDurationSec`, `modes`, `continuity`, `costPerSecCents`, `creditReason`, `qualityRank`, and a `configured` gate — and `routePolicy` scores the configured survivors by tier/cost. `planForModel` derives the generation resolution, the `upscalePlan` for non-native 4K, and the honest `resolutionStamp` (`native-4k` vs `upscaled-4k`).

The seeded catalog:

| `id` | Brand (`brandLabel`) | Tier | Vendor / route | Native max | Upscale | Max dur | Modes | Continuity | Configured when |
|------|----------------------|------|----------------|-----------|---------|---------|-------|-----------|-----------------|
| `kling` | Kling 3.0 | standard | fal | 4k | — | 15s | t2v, i2v | multi-shot | `FAL_KEY` |
| `veo` | Veo 3.1 | premium | vertex | 1080p | 4k | 8s | t2v, i2v | last-frame | Vertex creds (allowlist-gated) |
| `veo-fal` | Veo 3 (fal) | premium | fal | 1080p | 4k | 8s | t2v, i2v | last-frame | `FAL_KEY` (folds into `veo`) |
| `wan` | Wan 2.7 | economy | fal | 1080p | 4k | 10s | t2v, i2v | last-frame | `FAL_KEY` + `WAN_ENABLED` |
| `hailuo` | Hailuo 02 | economy | fal | 1080p | 4k | 10s | t2v, i2v | none | `FAL_KEY` + `HAILUO_ENABLED` |
| `seedance` | Seedance 2.0 | standard | fal | 1080p | 4k | 10s | t2v, i2v | last-frame | `FAL_KEY` + `SEEDANCE_ENABLED` |
| **`corereflex-1`** | **CoreReflex 1.0** | own | selfhost | 1080p | 4k | 10s | t2v, i2v | last-frame | `SELFHOST_VIDEO_URL` |
| **`corereflex-lf`** | **CoreReflex Long-Form** | own | selfhost | 720p | 4k | **60s** | t2v, i2v, **continue** | **native-continuation** | `SELFHOST_VIDEO_URL` + `LONGCAT_VIDEO_ENABLED=true` |
| `mock` | Mock (dev) | dev | mock | 4k | — | 15s | t2v, i2v | multi-shot | always |

### The two self-hosted CoreReflex video lanes

There are now **two** first-party, self-hosted video models — both run on our own GPU fleet behind the inference gateway, both are presented to users as **CoreReflex** custom models (the open-model name is an internal NOTICE only, per the [white-label rule](#white-label-enforcement-srcmodel-disclosurejs) below):

- **CoreReflex 1.0** (`corereflex-1`) — the general lane. Underlying engine: **Wan 2.2** (Apache-2.0). Gateway engine slug `wan` (the registry stamps `wan-open` as the provenance label; `safeEngineSlug` falls back to the full production repo if a distilled/turbo slug is ever configured — those are policy-banned). T2V/I2V, ≤10s, native 1080p → own-upscaler 4K. Bills `usage:corereflex` (15 credits). Live when `SELFHOST_VIDEO_URL` is set.
- **CoreReflex Long-Form** (`corereflex-lf`) — the long-form lane. Underlying engine: **LongCat-Video** by Meituan (MIT, 13.6B). Its differentiator is **native video continuation** — minutes-long output without color drift — so it carries a **60-second-per-call ceiling** and an extra **`continue`** mode (source clip → extended clip). T2V/I2V/continue, native 720p → 4K upscale. Bills `usage:corereflex-lf` (20 credits). Gated on `SELFHOST_VIDEO_URL` **and** `LONGCAT_VIDEO_ENABLED=true`. `normalizeSpec` unlocks the 60s duration ceiling when a `sourceVideoUrl` is present or the model is pinned `corereflex-lf` (otherwise the standard 15s clip bound holds).

### The self-hosted seam → the gateway

`runSelfHostGeneration(spec, { engine })` ([`src/providers.js`](../../src/providers.js)) POSTs to `${SELFHOST_VIDEO_URL}/generate` with `{ prompt, negative_prompt, aspect_ratio, duration, start_image_url, source_video_url, engine, seed, brand }`. Two fields carry the long-form value: `source_video_url` (continuation — longcat-only) and `engine` (forwarded **only** when it is one of `wan` / `ltx-2` / `longcat-video`; a legacy provenance label like `wan-open` is omitted so the gateway falls back to its instance default). Output is mirrored into MinIO exactly like the fal path.

At the gateway ([`inference-worker/gateway.js`](../../inference-worker/gateway.js)), `engine: 'longcat-video'` routes to an **HTTP PyTorch FastAPI backend** at `LONGCAT_VIDEO_BACKEND_URL` (a `wrappers/longcat-video/` service — **not** a ComfyUI graph like `wan`/`ltx-2`). Beyond `source_video_url`, that backend also accepts `segment_prompts` (1–12 non-empty strings — interactive per-segment story steering, longcat-only). The **avatar** seam was upgraded in the same pass: `AVATAR_ENGINE=longcat-avatar` (vs the default `echomimic-v3`) unlocks AT2V (a text `prompt` invents the character instead of a reference image), 1–20 chained long-form `segments`, 1–4 `characters[]` multi-character scenes, `audioMode` merge/concat, `distill` + `quantization:int8` tiers, and 480p/720p `resolution` — all documented (and cross-referenced, not duplicated) in [`docs/INFERENCE-GATEWAY.md`](../INFERENCE-GATEWAY.md). The persona seam forwards the avatar long-form knobs (`resolution`/`segments`/`distill`/`quantization`) via `renderPersonaClip` — see [own-the-stack.md](own-the-stack.md).

### White-label enforcement (`src/model-disclosure.js`)

Disclosure is **derived from where inference runs**, never a hand-picked list (`runtimeOf`): self-hosted open weights present as CoreReflex custom; paid vendor APIs stay disclosed. On every public surface (catalog, `/api/generate` response, editor picker, `/api/voice`, `/api/imagen`), the self-hosted lane is redacted:

- `publicModelView` — strips the underlying engine/vendor/route/cost and returns a `CoreReflex` brand label.
- `publicImageResult` — nulls the `engine` slug and `trace.model`, rebrands `vendor: 'corereflex'`.
- `publicVoiceResult` / `publicVoiceModels` — drop the open model's engine slug + raw attribution NOTICE and rebrand `voiceName` (this session fixed a `/api/voice` leak where `voiceName` came back as "Kokoro" — now "CoreReflex Voice").

The underlying model names (Wan 2.2, LongCat-Video, Kokoro, …) and their licenses live **only** as an internal compliance NOTICE — `MODEL_DISCLOSURE_META` here and `ENGINES_META` in [`inference-worker/engines-meta.js`](../../inference-worker/engines-meta.js). Authoritative policy: [`planning/MODEL_DISCLOSURE_PROVENANCE.md`](../../planning/MODEL_DISCLOSURE_PROVENANCE.md).

### Live vs. GPU-gated — the honest status

> **The GPU box is the single unlock.** Every self-hosted AI lane in the registry — CoreReflex Image (Qwen-Image), CoreReflex 1.0 (Wan), CoreReflex Long-Form (LongCat), avatar (LongCat-Avatar), the self-hosted LLM (Qwen), and CoreReflex Music (ACE-Step) — reports `configured: false` today because it needs a 24–48 GB GPU box that isn't rented yet (tracked as **`OPEN-009`** in [`planning/roadmap/OPEN_MODELS.md`](../../planning/roadmap/OPEN_MODELS.md)). The **only** self-hosted lane live in prod right now is **CoreReflex Voice** (Kokoro narration + NeuTTS zero-shot cloning) on the CPU box. Everything else generating today is paid **Imagen** + **Veo** on Vertex (plus the JSON engine + render pipeline). To go live: deploy the inference-worker GPU compose profile + weights, then set the `SELFHOST_VIDEO_URL` / `*_ENABLED` env vars — no app code changes.

---

## Camera motion (`src/camera.js`)

The Director decides a per-shot camera move; this module makes the model **obey** it. This closes the old competitive-gap item G5 — camera moves used to be generated by the planner and then dropped before the model. They now flow through two channels:

1. **Prompt fragment (universal, always on).** `composeCameraPrompt(prompt, camera)` weaves the move into the prompt as a **leading** `Camera: <speed> <phrase>.` clause — leading, not trailing, because video models weight early tokens hardest. This channel works for every provider: it's applied in `buildFalRequest` (Kling *and* Veo-on-fal) and in `buildVertexRequest` (`src/vertex.js:125`).
2. **Kling structured `camera_control` (opt-in).** `klingCameraControl(camera)` emits a precise `{ type: 'simple', config: { pan/tilt/zoom/… } }` object with unit deltas scaled by intensity into roughly −10..10 — sent **only when `KLING_CAMERA_CONTROL=true`**, because the exact fal schema varies by model version and an unknown param must never 422 a generation.

Supporting pieces:

- `CAMERA_MOVES` — 15 supported moves (`static, pan, tilt, dolly, truck, pedestal, zoom, orbit, fpv, crane, handheld, push-in, pull-out, whip-pan, aerial`), a superset of the Director's enum, plus aliases (`tracking → truck`, `boom → crane`, …).
- `normalizeCamera(input)` — accepts a bare string or `{ move, intensity }`; unknown moves → `null`; `static` → intensity 0.
- Intensity maps to a speed word: `< 0.34` → *slow*, `< 0.67` → *steady*, else *fast*.

The full move list is also published to API consumers via `src/catalog.js` (the `video` capability advertises `camera: CAMERA_MOVES`).

---

## Video generation orchestration (`src/generation.js`)

`POST /api/generate` → `meterUsage(user, 'usage:veo')` → `startGeneration(user, input)` → **202 Accepted** with a `jobId`. The client polls `GET /api/generate/:id`. There is **no WebSocket** — it is long-poll only.

Flow inside `startGeneration` → `runJob`:

1. `normalizeSpec(input)`; require a `prompt` or a `startImageUrl`.
2. `selectProvider` → `qaResolution`; a `block` finding throws `400 Native-4K QA gate: …`.
3. Build a job (`{ id, workspaceId, userId, private, crossTenantLearning, provider, spec, status, qa, … }`), insert it into `generation_jobs`, cache it in-process while active, and `runJob` fire-and-forget.
4. `resolveVendor` → `runVendor` dispatches to `runMockGeneration` | `runVertexGeneration` | `runFalGeneration`. Every vendor adapter returns the **same envelope** — `{ url, modelId, mirrored?, raw? }`.
5. **Footage residency:** Vertex already lands output on the fleet (MinIO) and mock is synthetic; only the **fal** path needs `mirrorToMinio` — the provider CDN body is *streamed* (`duplex: 'half'`, never buffered) into a presigned MinIO PUT at `generated/{workspaceId}/{jobId}.{ext}`. A mirror failure never fails the job (recorded as `job.mirrorError`).
6. `job.result` is stamped `{ assetUrl, mirrored, provider, modelId, durationInSeconds, resolution, signals }`, the `generation_jobs` row moves to `status='done'`, and `recordGeneration(job, assetUrl)` ([`src/gallery.js`](../../src/gallery.js)) indexes the clip into the gallery (best-effort) and stamps `assets.provenance` + `visibility`.

### Job store and visibility

Jobs persist to **`generation_jobs`** ([migration `015`](../../migrations/015_generation_jobs.sql)) and are also cached in an **in-memory `Map`, bounded to 500** (`MAX_JOBS`, oldest evicted) while the current app process runs the active generation. `GET /api/generate/:id` first reads the active cache, then falls back to the persisted row scoped by `workspace_id`, so terminal status survives app restarts and can be polled by any replica. A job is `private` when `input.private === true` **or** the workspace plan is `enterprise`; private clips never reach the public showcase — `recordGeneration` writes `visibility='private'`, and the public feed in `src/gallery.js` filters on `provenance.kind='generation' AND visibility='public'`. Music/avatar/soundscape assets carry their own `kind`, so they never leak into the video showcase.

`buildGenerationProvenance` stamps each asset with the full routing story: `{ kind, prompt (500-char cap), crossTenantLearning, provider, aspectRatio, resolution, route: { provider, vendor, modelId, priority }, cost: { estimateCents, metered, basis }, trace? }` — so any gallery row can answer "which model, which vendor, what did it cost, and exactly what was asked."

### Cross-provider fallback ("Veo to the max")

If a real provider call throws (e.g. fal returns 401 for a bad key) and the vendor was not already `vertex`/`mock` and `vertexConfigured()` is true, `runJob` retries on `runVertexGeneration`, sets `job.provider='veo'`, and records `job.fallbackFrom`. One bad credential does not kill a production. (There is still no *model-level* fallback for the veo-3.1 allowlist case — see [Roadmap](#roadmap--not-yet-shipped).)

---

## Direct Veo via Vertex AI (`src/vertex.js`)

The "go direct" hero path. Generated bytes land on the fleet, so footage never lives at Google — the proprietary win over the aggregator.

**Auth.** `serviceAccount(env)` parses `GCP_SERVICE_ACCOUNT_JSON` and **normalizes `\n` → real newlines** in the PEM (env-stored keys are routinely double-escaped, which otherwise makes RS256 signing fail and every call 500). `signJwtRs256` mints a signed-JWT assertion; `getAccessToken` exchanges it at the token URI for a cloud-platform access token, cached until ~1 min before expiry.

**Request.** `buildVertexRequest(spec, config, image, extras)` builds:

- `submitUrl = …/models/{model}:predictLongRunning`, `fetchOpUrl = …:fetchPredictOperation`.
- `instances[0] = { prompt, image?, lastFrame?, referenceImages? }` — the prompt is `composeCameraPrompt(spec.prompt, spec.camera)`, so the director's camera decision reaches Veo; I2V inlines the start image as `bytesBase64Encoded`.
- `parameters = { sampleCount: 1, aspectRatio, durationSeconds: snapVeoDuration(...), generateAudio, resolution?, negativePrompt?, seed? }`.
- **Resolution:** defaults to `1080p` for 16:9 (portrait is 720p-only on veo-3.0). `VEO_VERTEX_RESOLUTION` overrides.

### Conditioning — last frame + reference images (wired)

`instance.lastFrame` and `instance.referenceImages` **are wired** (`src/vertex.js:114-145, 192-203`), gated by model-family capability checks so an unsupported field never 400s a generation up front:

- **`lastFrame`** (first → last frame interpolation): sent when `spec.endImageUrl` is set **and** `modelSupportsLastFrame(config.model)` — the regex `veo-(2.* | 3.1+)` (veo-3.0 does not support it).
- **`referenceImages`** (asset-consistency conditioning — keep a character/product consistent across shots): built from `spec.ingredients` (up to 3, `referenceType: 'asset'`) when `modelSupportsReferenceImages(config.model)` — **veo-3.1+ only**.

`runVertexGeneration` fetches the start image, last frame, and references **in parallel and best-effort** (`fetchImageBase64` returns `null` on any failure — a dead URL degrades that one condition, never the generation). As a second line of defense, if the submit still 400s (a project/model rejecting an explicit `resolution` or a conditioning field the gate mispredicted), it **retries once with `resolution`, `lastFrame`, and `referenceImages` all stripped** — so conditioning and resolution can only ever improve a generation, never break one.

Practical upshot: flipping `VEO_VERTEX_MODEL` to a veo-3.1 slug (once the project is allowlisted) automatically activates both conditioning channels with no code change.

**Run.** `runVertexGeneration` submits, polls `:fetchPredictOperation` until `op.done` (5s poll, 600s timeout), then reads `op.response.videos[0]`. Inline `bytesBase64Encoded` are uploaded to MinIO (`uploadBytesToMinio`, key `generated/vertex/{uuid}.mp4`) and returned as a browser-reachable **signed-proxy** URL; a returned `gcsUri` is used only as a fallback. Every Vertex HTTP call goes through `vxFetch`: per-request timeout (`VERTEX_REQUEST_TIMEOUT_MS`, default 60s) plus exponential backoff on 429/5xx/network/timeout (`VERTEX_RETRIES`, default 3) — without these a single hung Vertex call would block a paid multi-minute orchestration indefinitely.

**Config defaults:** `VEO_VERTEX_MODEL=veo-3.0-generate-001` (GA), `VERTEX_LOCATION=us-central1`. `veo-3.1-*` are preview and **404 on `:predictLongRunning` until the project is allowlisted**. `buildVertexRequest` hard-codes `sampleCount: 1` (no batching/multi-sample).

---

## The shared Gemini seam (text / stream / tool-use / vision)

The video path above is `predictLongRunning`; everything Gemini-shaped uses `generateContent`, sharing the same SA-token + `vertexConfig()` plumbing. Four entry points at the bottom of `src/vertex.js`:

| Function | Shape | Consumers |
|----------|-------|-----------|
| `generateGeminiText` | `{ system, messages, model?, generationConfig }` → `{ text, usage, finishReason }` | caption translation, transcript highlights, image assist, story narrator, chat |
| `streamGeminiText` | same, plus `onDelta()` — SSE (`?alt=sse`) token streaming | chat widget |
| `generateGeminiToolUse` | raw `contents` + `tools` (functionDeclarations) → `{ text, functionCalls, content, usage }` | the agent kernel's multi-turn tool-use loop |
| `generateGeminiVision` | **one inline base64 image** + a text prompt → `{ text, usage }` | `detectSubjectBox` (auto-reframe) — the platform's first still-image vision call; any future frame-aware analysis |

The default model for all four is `models(env).gemini`; callers can pass a `model` explicitly. Timeouts: `CHAT_REQUEST_TIMEOUT_MS` (45s) / `CHAT_STREAM_TIMEOUT_MS` (60s). Video-understanding calls (highlights, styled captions, QA analyzer) do **not** use these helpers — they build `generateContent` bodies directly so they can attach `fileData`/`inlineData` media parts and `responseSchema`, but they share `geminiEndpoint()` from `src/director.js`, which handles the Gemini 3 quirk: Gemini 3.x serves from the **global** endpoint (set `GEMINI_VERTEX_LOCATION=global`; regional hosts 404 the new slugs) while Veo/Imagen stay on their regional `VERTEX_LOCATION`.

---

## The agentic cinema director (`src/director.js`)

The IP layer on top of the router: it turns a concept brief into a continuity-locked multi-shot **CinemaPlan** where every shot is a router-ready `GenerationSpec`. This is **phase 1 (PLAN)** of the agentic pipeline; the [producer loop](#the-agentic-producer--produce--critique--assemble-srcproducerjs) builds on this plan shape.

`POST /api/director/plan` → `planFilm(brief)`. The reasoner is pluggable:

- **`geminiReason`** runs the planning on `models().gemini` via Vertex `:generateContent` (25s `GEMINI_TIMEOUT_MS` hard ceiling so a hung egress degrades before any reverse-proxy 5xx).
- **`mockReason`** is a deterministic, key-free beat structure — the whole pipeline plans (and tests) offline.

**Structured planning has shipped.** `directorGenConfig()` attaches Google agent-platform options, all env-toggleable:

- **`responseSchema` (on by default):** the plan is generated against `PLAN_SCHEMA` (controlled generation — shots, lock sheets, camera, durations as a validated JSON contract). Disable with `DIRECTOR_STRUCTURED_OUTPUT=false`.
- **Google Search grounding (opt-in):** `DIRECTOR_GROUNDING=true` adds the `googleSearch` tool. Grounding and `responseSchema` are **mutually exclusive** (tools can't co-constrain a schema'd output), so grounding falls back to plain JSON mode.
- **Thinking budget:** `GEMINI_THINKING_BUDGET=<tokens>` sets `thinkingConfig` (Gemini 3 / 2.5).
- Temperature: `DIRECTOR_TEMPERATURE` (default 0.7).

**Never hard-fails the front door:** if the real reasoner throws, `planFilm` falls back to `mockReason` and attaches a `degraded` notice carrying the real cause; a *mock* failure is a genuine bug and propagates.

`normalizePlan` shapes the raw plan into the final `CinemaPlan`: shot 1 is `text` mode; every later shot is `frames` mode with `continuity: 'from-prev'`. Each shot carries `referenceSentence`, `emotionalObjective`, and `camera` from the **Lock-Sheet method**, and each `spec` passes through `normalizeSpec` (including the camera object, so the move survives all the way to the model).

### Pre-spend gates on the plan

Three advisory layers are attached to every plan before any Veo credit is spent — all non-blocking (they annotate; caller-side UIs and the workflow runner decide):

1. **`plan.structure`** — deterministic structural lint: `slideshowRisk` + `variationLint` flag slideshow-like / low-variation plans.
2. **`plan.validation`** — `validatePlan` (COPILOT-003): flags missing lock sheets, empty/dropped shots, and duration drift so a broken plan fails LOUD before the producer runs.
3. **`plan.eval`** — the [LLM judge](#llm-judge-plan-evaluation-srcevaljs), attached when `DIRECTOR_EVAL=true`.

### Vote-learning and semantic recall (the learning loop)

`POST /api/director/plan` enriches the brief before calling `planFilm` (see `src/api.js`):

1. `getPreferenceBrief(workspaceId)` derives `liked` / `disliked` from upvotes/downvotes.
2. `findSimilarPrompts(workspaceId, concept, { upvotedOnly: true, limit: 4 })` (pgvector, `src/embeddings.js`) recalls the **nearest upvoted prior prompts** to *this* concept.
3. `findCommunityRecallPrompts(concept, { excludeWorkspaceId })` adds positively voted cross-tenant winners only when the asset is public **and** explicitly opted into shared learning, reranked by similarity, Wilson vote quality, and engagement, with contact/secret material redacted before it crosses the boundary.
4. The `liked` sources merge (deduped, capped at 8) and are injected into the director's "LEARNED PREFERENCES" prompt block.

This is empty/no-op until votes exist. See [data-model.md](data-model.md) for `assets.embedding` (`vector(1536)`, HNSW cosine) and the votes ledger.

---

## The agentic producer — PRODUCE → CRITIQUE → ASSEMBLE (`src/producer.js`)

The flagship loop: it takes a `CinemaPlan` and turns it into a graded, continuity-chained, ready-to-edit composition — not a one-shot prompt.

`POST /api/director/produce` → `produceFilm(plan, { user })`. The body is either a `plan` already returned from `/api/director/plan`, **or** a raw concept brief (in which case the route calls `planFilm` first). The route is rate-limited to **6/min per IP** and meters `usage:veo` **per shot** (`multiplier: plan.shots.length`), because each call drives several Veo generations and is long-running. **No browser surface should call this route directly** — it blocks for minutes and 502s behind proxies; the editor and dashboard both run the client-side plan → generate → poll → assemble loop instead.

The three phases, all behind injectable seams so the loop is unit-testable offline:

1. **PRODUCE** (`produceShot`) executes each planned shot via the `generate` seam. Shot 1 is text-to-video; every later shot with `continuity: 'from-prev'` is given the **previous shot's last frame** as `startImageUrl` (image-to-video). The continuity anchor (`prevFrameUrl`) threads from shot to shot.
2. **CRITIQUE** scores each produced shot, normalizes the result against the **QA bar** (`DEFAULT_QA_BAR = 0.6`), and **selectively regenerates only the shots that miss the bar**, up to `maxRetries` (default 1) — always keeping the **best** attempt seen, not the last.
3. **ASSEMBLE** (`assembleComposition`) sequences the produced clips into the editor/Remotion composition IR — a single track of back-to-back `video` items at 30 FPS, sized from the plan's aspect ratio (`16:9→1920×1080`, `9:16→1080×1920`, `1:1→1080×1080`, `4:5→1080×1350`). This IR is exactly the manifest the [render worker](render-worker.md) consumes.

### Real per-shot QA (shipped — `src/qa-analyzer.js`)

The critique is no longer a fabricated "did we get a URL?" score. The producer's prod bindings:

| Seam | Default | What it does |
|------|---------|--------------|
| `generate(spec, shot)` | `defaultGenerate(user)` — `startGeneration` then polls `getGeneration` up to 900×1s | Veo via Vertex / Kling via fal |
| `analyze(input)` | `defaultAnalyze` → `analyzeClip` (`src/qa-analyzer.js`) when Vertex is configured and `QA_ANALYSIS_DISABLED` ≠ `'true'` | **Gemini multimodal watches the actual clip** and returns the 8 shot-applicable QA15 audio+visual checks as schema-validated booleans (`ANALYZER_SCHEMA`). Media rides as `fileData.fileUri` for `gs://` or is byte-capped inline for https (`QA_ANALYZER_MAX_BYTES`, default 24 MB). |
| `score(gen, shot)` | `defaultScore` | No video → hard fail (score 0). Real signals → `scoreShotReadiness` band. **No signals → `band: 'unscored'`, `verified: false`** — accepted but never claimed as a QA pass. The analyzer never fabricates: unconfigured/unfetchable/errored → `null` → unscored. |
| `extractLastFrame(videoUrl)` | `defaultExtractLastFrame` → **`null`** | Still a stub — `FRAME_EXTRACT_URL` is named in comments but not yet read. |

**Continuity is graceful-degrade today:** because `extractLastFrame` returns `null`, the producer still runs end-to-end — it can't chain a real last frame, so later shots fall back to text-to-video. Wiring a fleet frame service turns on true i2v continuity with no code change. (Note the overlap with the now-wired Veo `lastFrame` conditioning: that field interpolates *toward a supplied end frame within one clip*; the producer's chain needs a *frame extractor* to hand shot N's final frame to shot N+1.)

The response carries `{ title, aspectRatio, shots, assembly, summary }` where `summary = { total, passed, regenerated, allPassed }`.

---

## LLM-judge plan evaluation (`src/eval.js`)

Shipped — this was the top roadmap item and is now live. An automated quality gate that scores a film plan against a fixed cinema rubric *before* the expensive producer runs:

- **`RUBRIC`** — five dimensions scored 0–5: `continuity` (lock sheets carry across shots), `specificity` (anti-slop), `narrative` (hook/escalation/payoff), `format` (shot count/length/aspect honored), `prompt_quality` (render-ready for Veo/Kling).
- **`geminiJudge`** — controlled generation against `EVAL_SCHEMA` (`responseMimeType: application/json` + `responseSchema`), temperature 0.1, `EVAL_TIMEOUT_MS` (default 20s), fully traced via `runTraced`.
- **`mockJudge`** — deterministic key-free heuristics with the same output shape, so eval is always available offline.
- **`normalizeEval`** — clamps scores, recomputes `overall` (0–1) from dimensions when missing, derives `verdict`: `pass` ≥ 0.7, `revise` ≥ 0.5, else `fail`.
- **`evaluatePlan`** — public entry; degrades to the mock judge on any LLM error (with a `degraded` notice), so a bad eval can never block a generation.

Wiring: `DIRECTOR_EVAL=true` attaches the verdict as `plan.eval` in `planFilm` (off by default; never blocks — it annotates so the UI/admin/workflow runner can gate). The editor surfaces `plan.eval` and per-shot QA in `director-overlay.tsx`.

---

## Supporting modalities

All Vertex modalities below reuse the service account and land their bytes on MinIO, returning a signed-proxy URL. Most are **synchronous** (seconds), so the API returns **200**, not 202+poll.

### Music — Lyria (`src/music.js`)

`POST /api/music` → `meterUsage('usage:lyria')` → `generateMusic`. Model `lyria-002` (`LYRIA_VERTEX_MODEL`), `us-central1` (`LYRIA_VERTEX_LOCATION`), the `:predict` verb. Six curated `MOODS` (`cinematic`, `upbeat`, `ambient`, `tense`, `uplifting`, `lofi`) each map to a prompt suffix; `GET /api/music/moods` lists them. Output is a fixed **~30s WAV** landed under `generated/music/{ws}/{uuid}.wav`, indexed with `kind='music'` so it stays out of the public video feed.

> **Originality / recitation guard.** Lyria refuses prompts whose output could "recite" copyrighted music. `generateMusic` leads every prompt with an explicit originality instruction. If Lyria still returns a `400` matching `/recitation/i`, it **retries once** with a stronger originality fallback prompt **and a fresh deterministic seed**. A second failure surfaces a `502` advising a more specific, original prompt.

### Image generation — Imagen (`src/imagen.js`)

`POST /api/imagen` → `meterUsage('usage:imagen')` → `generateImage`. Model `imagen-3.0-generate-002` (`IMAGEN_VERTEX_MODEL`). The prompt is composed via the `visual.master` spec's **master-prompt anatomy**: a `likenessSuffix` for persona reference stills, else a general `qualitySuffix`, plus an optional curated style preset (`resolvePreset`) and the **intent-aware fidelity modifier** from `routeModel(prompt).modifier` (a "4k / masterpiece" ask gets fidelity cues folded into the prompt). Aspect ∈ `{1:1, 9:16, 16:9, 3:4, 4:3}`.

Two Imagen-specific gotchas are handled in code:

- **`negativePrompt` is rejected (400 INVALID_ARGUMENT) by `imagen-3.0-generate-002` and every Imagen 4 model.** `modelSupportsNegativePrompt` gates the parameter; on unsupported models the caller's negative prompt is folded into the prompt text as `Avoid: …` so intent survives instead of hard-failing.
- **A pinned `seed` requires `addWatermark: false`** (Imagen 4 requires it for reproducibility) — sent only when a seed is pinned.

Every generated image is signed with **CoreReflex Content Credentials** (`tryMintCredential` — best-effort, never fails the generation) so downstream `assets.provenance.credential` carries a verifiable AI-generated record.

### Logo generation (`src/imagen.js` — `generateLogoOptions`)

`POST /api/logo/options` — the staged logo workflow, step 1: N **distinct concepts** (not N variations of one idea), one `:predict` per artistic direction fanned out in parallel (`Promise.allSettled` — one bad direction doesn't sink the batch).

- **The single biggest quality lever is the model:** logos default to **`IMAGEN_LOGO_MODEL=imagen-4.0-generate-001`** while general image gen stays on `IMAGEN_VERTEX_MODEL` — Imagen 3 is weak at clean iconography and garbles lettering.
- Nine `LOGO_ARCHETYPES` spread the batch across directions: six symbol-forward/text-free (`geometric`, `abstract`, `emblem`, `pictorial`, `negative-space`, `monoline`) lead the default rotation; three `lettering: true` directions (`wordmark`, `lettermark`, `combination`) render the brand name and are selectable per concept.
- The photography `qualitySuffix` is **replaced** by a flat-vector `LOGO_SUFFIX`, and all "avoid" steering lives **in the prompt** (`LOGO_AVOID` / `LOGO_AVOID_LETTERING`) because `negativePrompt` would 400 on these models.
- The brief is enriched once up front via `enhancePrompt` (opt out with `enrich: false`); lettering concepts keep the **raw** brief so the exact name is preserved.
- **Metering is delivered-count, not requested-count:** the route pre-gates one `usage:imagen`, then reconciles to `result.variants` after generation — `meterUsage` is decrement-only with no refund path, so billing the request would over-charge a partial failure.

The 4-up treatment viewpane (Color / Grayscale / B&W stamp / Reverse) is derived **client-side** from each color master — same mark, four ways, no extra model calls.

### Image mask editing — Imagen capability model (`src/image-edit.js`)

`POST /api/image/edit` (metered `usage:imagen-edit`, rate-limited 10/min) → `editImage`. Mask-based edits on the **capability model** `imagen-3.0-capability-001` (`IMAGEN_EDIT_MODEL`) — this is a *separate* model from the generate models, which reject `referenceImages`. Three modes, each tuned per Google's guidance:

| Mode | Vertex `editMode` | `baseSteps` | Notes |
|------|-------------------|-------------|-------|
| `fill` | `EDIT_MODE_INPAINT_INSERTION` | 35 | needs a prompt (what to generate in the brushed region) |
| `erase` | `EDIT_MODE_INPAINT_REMOVAL` | 12 | **few** steps — more steps hallucinate replacement objects into the hole |
| `expand` | `EDIT_MODE_OUTPAINT` | 35 | larger mask dilation (0.03) so the seam blends |

The client paints a mask (white = change), sends working image + mask as base64 (raw or data-URL; the **whole** string is base64-validated because `Buffer.from` silently truncates at the first invalid char), capped at ~12 MB decoded per payload. This one route reads with a larger body cap than the global 1 MiB JSON limit. The result lands at `generated/image-edit/{ws}/{uuid}.{ext}` as a **sibling asset** — sources are never overwritten. Fully traced via `runTraced`.

### Conversational canvas edits (`src/image-assist.js`)

`POST /api/image/assist` (metered `usage:content`) → `assistImageEdit`. One natural-language instruction + the design/node context in, **one structured action** out, applied client-side by the Image Studio: `adjust` (absolute filter-stack targets, clamped per `ADJUSTMENT_RANGES` — brightness/contrast/saturate/sepia/hueRotate/temperature/shadows/highlights), `fill` / `erase` (arm the mask brush — a region is never guessed), `expand` (outpaint at an aspect), or `none`. Text-only Gemini via the injectable `generate` seam; an `adjust` with no usable fields degrades to `none` so the client never commits an empty undo entry.

### Auto-reframe — the first vision call (`src/reframe.js`)

`POST /api/image/subject-box` (metered `usage:content`, rate-limited 30/min) → `detectSubjectBox`, powered by **`generateGeminiVision`** — the platform's first still-image vision call. One frame in → the main subject's bounding box out, normalized 0..1, temperature 0. Output is hard-clamped: a malformed or degenerate/hallucinated box degrades to a **centered fallback** (`{x:0.25, y:0.25, w:0.5, h:0.5}, fallback: true`) — never a crash or an off-canvas crop. The editor keeps that box centered when converting a clip between aspect ratios (16:9 → 9:16 → 1:1).

### Own-tech engines — remove background, vectorize, upscale

Three sibling-derivative engines follow the same adapter pattern: the app has no ffmpeg/compositing, so the real work runs on the [render worker](render-worker.md)'s HTTP endpoints, authenticated with `RENDER_SHARED_SECRET` (`x-render-token`), with an SSRF-guarded public-https source URL, a **mock passthrough** for e2e testing without the worker, and an honesty 503 ("being provisioned") **before metering** when the worker URL is unset. The source asset is never overwritten — every result is a sibling.

| Engine | Adapter | Worker endpoint | Env (URL / mock) | Route + meter |
|--------|---------|-----------------|------------------|---------------|
| Background removal | `src/remove-bg-engine.js` | `/remove-bg` — segments the subject (Vertex) + composites the mask as alpha (ffmpeg) → transparent PNG | `REMOVE_BG_URL` / `BG_REMOVE_MOCK` | `POST /api/image/remove-bg`, `usage:remove-bg` |
| Vectorize | `src/vectorize-engine.js` | `/vectorize` — traces raster → clean SVG (VTracer) | `VECTORIZE_URL` / `VECTORIZE_MOCK` | `POST /api/image/vectorize`, `usage:vectorize` |
| Upscale | `src/upscale.js` (policy) + worker ffmpeg | `/upscale` — lanczos resample + detail-preserving unsharp, faststart | `UPSCALE_URL` / `UPSCALE_MOCK` | `POST /api/upscale`, `usage:upscale` |

`src/upscale.js` is the pure **policy** half: `resolveUpscalePlan` (auto mode lifts sub-1080 to 1080; explicit tiers `720p/1080p/4k/8k`; never downscales; magnification capped at 4× so interpolation doesn't manufacture mush), `clampTier` (gates 8K behind a per-deploy ceiling, `UPSCALE_MAX_TIER`), and `ffmpegUpscaleArgs` (`UPSCALE_CRF` default 18, `UPSCALE_X264_PRESET` default medium). `upscalerBackend()` selects `ffmpeg` (v1, default) or `sr` — the self-hosted super-resolution seam, reserved for when the GPU-fleet service lands.

### Voice — Chirp 3 HD TTS and the CoreReflex Voice registry (`src/voice.js`)

`POST /api/voice` → `synthesizeVoice`. Calls Google Cloud Text-to-Speech (`texttospeech.googleapis.com/v1/text:synthesize`), **MP3** output, `speakingRate` clamped [0.5, 1.5]. Five curated **Chirp 3 HD** voices (`charon`, `aoede`, `kore`, `puck`, `fenrir`); default `VOICE_DEFAULT`. `GET /api/voice/voices` lists them plus `engine` info (whether true voice design is available or a description will be mapped onto a named voice). `POST /api/voice/design` maps a plain-language voice description onto a concrete voice via `src/voice-design.js` — the seam the auto-clips voice presets ride (see below). Lands `voice/{ws}/{uuid}.mp3`. Voice cloning is consent-gated behind the persona routes; see [own-the-stack.md](own-the-stack.md).

**Self-hosted voice is a first-class, per-request pick.** A voice-model registry ([`src/voice-engines.js`](../../src/voice-engines.js)) exposes one row per selectable TTS engine, each activating when its per-engine URL env is set: `corereflex-voice` (`VOICE_ENGINE_URL` — the clone/design flagship), `kokoro` (`VOICE_KOKORO_URL`), `piper`, `neutts` (clone-only), `chatterbox`, `qwen3-tts`, plus vendor `chirp-hd`. A request pins one via `model` (e.g. `{ text, model: 'kokoro' }`); self-hosted engines bill **`usage:voice-own`** instead of `usage:voice`. Resolution order in `synthesizeVoice`: explicit `model` → `VOICE_DEFAULT_MODEL` → the env-selected engine (`VOICE_ENGINE=selfhosted`). The result is white-labeled by `publicVoiceResult` ([`src/model-disclosure.js`](../../src/model-disclosure.js)) — the open engine's slug + raw attribution are dropped and a narrated `voiceName` is rebranded to **"CoreReflex Voice"**.

> **Live vs. GPU-gated (be precise).** `VOICE_ENGINE_URL` and `VOICE_KOKORO_URL` are set in prod, so **CoreReflex Voice — Kokoro narration + NeuTTS zero-shot cloning — is live on the CPU box today**. But `VOICE_ENGINE` is **unset**, so *default* `/api/voice` narration (no `model`, no clone) still routes to **paid Chirp** — you must pin `model: 'kokoro'` to get the self-hosted engine. The GPU-only voice engines (`chatterbox`, `qwen3-tts`) wait on the [GPU box](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs). This is the one self-hosted lane already in production; video/image/avatar/LLM/music are not.

### Captions — Speech-to-Text v2 (`src/captions.js`)

`POST /api/captions` → `transcribeFromStorage({ fileKey })`. The editor extracts a clip's audio to WAV, uploads it to MinIO, and posts the `fileKey` (IDOR-guarded to the caller's workspace); this fetches the audio via a presigned GET, transcribes with **Speech-to-Text v2** (`…/recognizers/_:recognize`) requesting **word-level time offsets**, and maps the result to the **`@remotion/captions` `Caption[]`** shape (`{ text, startMs, endMs, timestampMs, confidence }`).

**Model:** `CAPTIONS_STT_MODEL` everywhere, but note the default divergence — the central registry (`models().stt`) says `chirp-2` while this module's local fallback is `latest_long` (it reliably emits word offsets in every region). The live route uses the module fallback. `CAPTIONS_LANGUAGE` defaults `en-US`.

The same module also hosts three pure/text helpers:

- **`translateCaptions`** (`POST /api/captions/translate`, `usage:content`) — segment-level Gemini translation (word-by-word is nonsense across languages) with translated words redistributed evenly across each segment's original time span; timings stay on the source timeline so karaoke highlighting keeps working. Chunks of ≤120 segments per model call.
- **`segmentCaptions`** — sentence-ish grouping (≤24 words, ≤800ms gaps) shared by translation and highlight ranking.
- **`detectSilences`** — pure gap detection over word timings (the durable `cut.silences` executor shares it).

### Video-native intelligence — the Auto-Clips family

These four modalities power the long-form → vertical-shorts pipeline; the full workflow (prepare → detect → cut → render → captions/voice → library) is documented in [auto-clips-pipeline.md](auto-clips-pipeline.md). What matters here is the model layer:

**Highlight detection — transcript-ranked (`rankHighlights`).** `POST /api/captions/highlights` (`usage:content`): numbered transcript segments + timestamps go to Gemini text, which picks self-contained, hook-strong moments. Output is hard-validated — timestamps inside the span, sane durations, no overlaps, junk dropped — because a bad entry would cut a user's timeline in the wrong place. Up to ~1h of speech (400 segments).

**Highlight detection — video-native (`detectHighlightsFromVideo`).** `POST /api/highlights/detect` (`usage:content`, workspace-bound URL): **Gemini watches the raw video** and returns timestamped moments directly — humor, insight, a quiet emotional beat, things pure audio/motion heuristics can't see. One Vertex call replaces a whole GPU heuristic + scene-cut + ranking stack. Mechanics:

- Media rides the proven qa-analyzer convention: a `gs://` URI as `fileData.fileUri`; an https clip is SSRF/OOM-hardened-fetched (`fetchInlineMedia`) and inlined, capped at `AUTOCLIPS_MAX_INLINE_BYTES` (default ~20 MB — the Vertex inline request cap).
- Output is **schema-validated** (`DETECT_SCHEMA` via `responseSchema`): `{ startSeconds, endSeconds, type, score, title, hook, reason }`.
- The moment taxonomy is pluggable: `HIGHLIGHT_TYPE_PRESETS.general` (hook/insight/emotional/funny/surprising/action/payoff) is the default; `gaming` (clutch/wtf/epic_fail/hype/skill/…) is opt-in; callers may pass fully custom `types`.
- `selectDiverseHighlights` is the pure post-pass: normalize/clamp each candidate, greedily remove overlaps within `minGapMs`, then a **two-pass diversity pick** (top moment of each unique type first, then fill by score). Knobs: `AUTOCLIPS_MIN_SECONDS` (12) / `AUTOCLIPS_MAX_SECONDS` (60) / `AUTOCLIPS_MIN_GAP_MS` (500) / `AUTOCLIPS_TIMEOUT_MS` (120s).

**The long-video proxy.** An hour-long source can't inline into Gemini, so `POST /api/highlights/proxy` (`CLIP_PROXY_URL` / `CLIP_PROXY_MOCK`, adapter `src/autoclips-proxy.js`) has the render worker build a small **time-preserving** low-res proxy (default 1 fps) whose timestamps map 1:1 back to the source; the proxy's URL then feeds `/api/highlights/detect` and the resulting cut times apply to the original. Infra details in [auto-clips-infra.md](auto-clips-infra.md).

**Styled caption generation (`src/caption-generate.js` + `src/caption-styles.js`).** `POST /api/captions/generate` (`usage:content`): for footage with no useful speech (gameplay, b-roll), Gemini watches the clip and **invents** persona-styled captions timed across it. Nine personas in `CAPTION_STYLES` (`gaming`, `dramatic`, `funny`, `minimal`, `genz`, plus four narrative `story_*` styles); `style: 'auto'` maps a detected moment `type` to the best-fit persona via `TYPE_TO_STYLE`. Schema-validated output (`CAPTIONS_SCHEMA`), cadence auto-derived (~1 caption / 2.5s; story styles ~5.5s, longer 2–3-sentence segments), optional one-emoji-per-caption field, and `normalizeCaptions` clamps/orders/de-overlaps into the **same `{ startMs, endMs, text }` shape `/api/captions` produces** — so either path can be burned into a render.

**Story narrator (`src/story-narrator.js`).** `POST /api/auto-clips/story` (`usage:content`): generates **one coherent narrative arc across all N selected clips** (2–20) instead of independent per-clip captions — part 1 sets up, the middle escalates, the final part concludes. Text-only Gemini over the highlights' metadata (title/hook/type — no video upload needed). Returns `{ style, narrativeArc, segments: [{ clipIndex, narration }] }`; the pure `narrationToCaptions` then splits each narration into on-screen caption segments spread proportionally to sentence length (the last caption always ends exactly at clip end).

**Voice presets (`src/voice-presets.js`).** Each caption persona gets a matching voiceover brief: `VOICE_PRESETS` maps persona → a plain-language voice `description` + `speakingRate`, and `resolveVoicePreset` **reuses the caption persona resolver** so voice and captions always agree. The description is mapped onto a concrete Chirp HD voice by the voice-design seam (`/api/voice/design`) — no vendor voice ID is hard-coded. `DEFAULT_DUCK_DB = -10.5` (≈0.30 linear) is how far the source audio ducks while the voiceover plays; `POST /api/auto-clips/voiceover` (`usage:voice`) synthesizes one clip's VO.

### Prompt enhancement + brand voice (`src/enhance.js`)

- **`enhancePrompt`** (`POST /api/enhance`) — a pre-reflection pass that turns a vague ask into a strong, generation-ready prompt, callable by any surface (director brief, script topic, image/logo prompt). Rendered from the `prompt.enhance` spec; `ENHANCE_TIMEOUT_MS` (15s). **Never hard-fails:** any LLM error degrades to `mockEnhance` — an honest passthrough of the original text with a `degraded` notice. Note this route currently performs a Gemini call without metering (see gaps).
- **`synthesizeBrandVoice`** (`POST /api/brand-voice`) — turns a brand profile (role/goal/audience/style/avoid) into **one reusable system instruction** that conditions every generator, keeping copy/scripts/briefs coherent. Same mock/Gemini seam; the mock deterministically assembles the instruction from the profile.

### Procedural soundscapes (`src/soundscape.js`)

**No model call, zero vendor cost** — keyword-driven ambient audio synthesized on the CPU (rain / wind / forest / ocean / city layers, mixed with fades so it loops cleanly), 3–120s mono WAV at 22.05 kHz, landed like generated music (`kind='music'`) and returned as a signed-proxy URL. `GET /api/soundscapes` lists presets; `POST /api/soundscape` generates. Documented here because it sits beside the model-backed audio paths — do not describe it as AI-generated; it isn't.

### Embeddings + semantic media search (`src/embeddings.js`, `src/gallery.js`)

`gemini-embedding-001` (`EMBED_VERTEX_MODEL`) at `outputDimensionality` 1536 (`EMBED_DIM`) — sized for the `assets.embedding vector(1536)` column. `storeAssetEmbedding` writes a clip's prompt embedding at record time; `findSimilarPrompts` runs the HNSW cosine query for director recall. **Entirely best-effort:** if Vertex is unconfigured or any call fails, embeddings degrade silently and generation never blocks on them.

**Semantic media search** (competitive-gap V7, shipped): `GET /api/media/search?q=…` (rate-limited 30/min) → `searchWorkspaceAssets` — the natural-language query is embedded with `taskType: 'RETRIEVAL_QUERY'` and ranked by pgvector cosine distance (`embedding <=> $vec`) over the **caller's own workspace assets**, optionally scoped to a collection (an inner-join on membership so a search inside a curated collection can't leak non-member matches). Degrades to `{ items: [], degraded: true }` — never an error — when embeddings are unavailable. The Files pillar's `listWorkspaceFiles` delegates any non-empty `q` to this same ranking.

---

## The portable prompt-spec engine (`src/prompts/`)

The prompting logic — the "secret sauce" — lives in **versioned JSON specs under `src/prompts/specs/`**, *not* hardcoded in code. A spec is inspectable via `GET /api/prompts` / `GET /api/prompts/:id`, swappable without a code-logic deploy, and replayable by `id` + `version`.

`registry.js` exposes:

- `getSpec(id)` / `listSpecs()` — load and list (specs are cached and frozen on first read).
- `renderTemplate(template, values)` — `{{slot}}` / `{{a.b.c}}` substitution; missing slots → `''`; arrays joined with newlines (so instruction lists live as JSON arrays).
- `buildPrompt(id, values)` — the single place a spec turns into a runnable prompt: returns `{ spec, prompt, model, params }`.

### Spec catalog

| `id` | File | Task | Model |
|------|------|------|-------|
| `director.cinematic` | `director.json` | film-direction (Lock-Sheet method, anti-slop, shot craft, output schema) | `gemini-2.5-flash` |
| `visual.master` | `visual.json` | image-generation (master-prompt anatomy, quality/likeness suffixes) | `imagen-3.0-generate-002` |
| `visual.presets` / `design.presets` | `visual-presets.json` / `design-presets.json` | image-styling preset libraries | — |
| `music.lyria` | `music.json` | music-generation | `lyria-002` |
| `prompt.enhance` | `prompt-enhance.json` | prompt-enhancement | `gemini-2.5-flash` |
| `brand.voice` | `brand-voice.json` | brand-voice synthesis | `gemini-2.5-flash` |
| `content.write` | `content.write.json` | content-writing (the Write pillar) | `gemini-2.5-flash` |
| `copy.frameworks` | `copy-frameworks.json` | marketing-copy frameworks | `gemini-2.5-flash` |
| `script.natural` / `script.storyboard` | `script.json` / `script-storyboard.json` | script-writing | `gemini-2.5-flash` |
| `campaign.planner` | `campaign-planner.json` | campaign-planning | `gemini-2.5-flash` |
| `workflow.plan` | `workflow-plan.json` | workflow-planning (Autopilot) | `gemini-2.5-flash` |
| `meeting.summary` | `meeting.json` | meeting-inference | `gemini-2.5-flash` |
| `persona.talking_avatar` | `persona.json` | talking-avatar (consent rules, hook/CTA craft) | `chirp+lipsync` |

The director sources its continuity machinery, anti-slop discipline, shot craft, and output schema **verbatim** from `director.json` — the prompting IP lives in the spec, not in `director.js`. The **Lock-Sheet method** (character/location reference sentences pasted verbatim into every shot, explicit identity re-assertion, one character per shot) is what makes consecutive clips cut together. There is no deprecation lifecycle on specs yet — hot-swapping a spec makes old traces non-replayable.

---

## Inference tracing (`src/inference.js`)

Every model call emits a self-contained, portable **trace** — for tracking *and* portability. The trace is returned in the API response **and** embedded into `assets.provenance.trace` so any output can be audited or re-rendered anywhere with **no DB migration**.

`newTrace({ spec, model, task, params, inputs, prompt, workspaceId })` produces:

```js
{
  traceId, engine: { platform: 'corereflex', version: '1.0.0' },
  spec: { id, version },          // which prompt spec
  task, model, params,
  inputsDigest,                   // 16-char sha256 of structured inputs — WHAT was asked, no PII verbatim
  promptSha256,                   // hash of the exact rendered prompt — replayable without leaking it
  startedAt, ms, qa,              // timing + optional QA score
  provenance: { workspaceId },
}
```

`runTraced({ …, call })` wraps a model call: it times it, attaches `promptSha256` and optional `qa`, returns `{ result, trace }`, and on error stamps `trace.error` before re-throwing. `geminiReason`, `generateMusic`, `generateImage`, `generateLogoOptions`, `editImage`, `geminiEnhance`, `geminiBrandVoice`, and `geminiJudge` all run through `runTraced`; the trace then flows into `recordGeneration` → `assets.provenance.trace`. The director additionally returns its trace inline on the plan (`plan.trace`) — and the offline `mockReason` emits an equally-shaped trace (model `'offline'`), so even a key-free plan is fully traceable.

---

## Upscaling and the 8K master path

Where this stands today (the old "8K is not yet built" framing is half-stale):

- **Shipped:** the own-tech upscaler v1 — `src/upscale.js` policy + the render worker's ffmpeg pass (lanczos + unsharp), exposed at `POST /api/upscale` with tiers `720p/1080p/4k/8k`, auto-1080 lifting, a 4× magnification cap, and 8K gated behind the `UPSCALE_MAX_TIER` deploy ceiling via `clampTier`. The result is always a sibling derivative; the native source is never overwritten.
- **Seamed, not built:** `UPSCALER=sr` routes to a self-hosted super-resolution service (Real-ESRGAN/SeedVR2-class on the GPU fleet) behind the same plan → run contract — the filter swap is the only change when it lands.
- **Roadmap:** the governed "Master / Enhance" product stage from [../UPSCALING-8K.md](../UPSCALING-8K.md) — the editor pointing the timeline at an 8K master while rendering a 4K delivery composition, making 2× punch-in and multi-format reframes editor-side metadata rather than re-renders; per-workspace cost ceilings on the stage.

---

## Roadmap / not-yet-shipped

Re-baselined 2026-07-06. **Shipped since the last baseline** (previously listed here as roadmap):

- **LLM-judge scoring gate before Veo spend** — `src/eval.js` + `DIRECTOR_EVAL` (see [above](#llm-judge-plan-evaluation-srcevaljs)).
- **Gemini `responseSchema` structured planning** — `PLAN_SCHEMA` in `src/director.js`, on by default, plus grounding/thinking-budget toggles.
- **Render-time QA signal extraction** — `src/qa-analyzer.js` gives the producer's critic real multimodal signals; unjudgeable shots are honestly `unscored`, never rubber-stamped.
- **Budget governance** — `src/budget.js`: pre-flight credit/vendor-cents estimation plus a reserve → reconcile → refund ledger with `observe` / `warn` / `cap` modes for staged recipe runs.
- **Veo conditioning** — `instance.lastFrame` (veo-2/3.1+) and `instance.referenceImages` (veo-3.1+) are wired with capability gates and graceful 400 fallback (`src/vertex.js`).
- **Camera-move delivery** — the director's camera decision reaches every provider via `composeCameraPrompt` + optional Kling `camera_control` (`src/camera.js`); the "generated then dropped" gap is closed.
- **Own-tech upscaler + full engine seams** — upscale / remove-bg / vectorize on the render worker.
- **Semantic media search**, **video-native highlight detection**, **styled captions / story narrator / voice presets**, **image mask editing + conversational assist**, **logo concept batches**, **prompt enhancement + brand voice** — all live (sections above).

Still **Soon** — wired or planned in code, but not live today:

- **~~Structured capability `MODELS` registry~~ — SHIPPED** as [`src/models-registry.js`](../../src/models-registry.js): honest `nativeMaxResolution` / `upscaleTo` / mode / continuity per model, cost-aware `routePolicy`, `native-4k` vs `upscaled-4k` stamping, and the two self-hosted **CoreReflex** video lanes. The old Veo `native4k` mislabel is fixed (see [the registry section](#the-multi-model-capability-registry-and-self-hosted-lanes-srcmodels-registryjs)).
- **The GPU box (`OPEN-009`)** — the single unlock for every self-hosted AI *generation* lane. CoreReflex Image / 1.0 / Long-Form / avatar / LLM / music all report `configured: false` until a 24–48 GB GPU box runs the inference-worker GPU profile; only CoreReflex Voice (Kokoro/NeuTTS) is live on the CPU box today. Plan: [`planning/roadmap/OPEN_MODELS.md`](../../planning/roadmap/OPEN_MODELS.md).
- **Real last-frame extraction for producer continuity** — `defaultExtractLastFrame` still returns `null` (`src/producer.js:301`); `FRAME_EXTRACT_URL` is named but unread, so multi-shot continuity chains degrade to text-to-video until a fleet frame service is wired.
- **`veo-3.1-*` allowlist + model-level fallback to `veo-3.0`** — 3.1 is not the default and the only fallback today is provider-level (fal → Vertex), not model-level.
- **Dedicated generation worker** — `generation_jobs` persists status/result/error, but active video generation still runs fire-and-forget in the app process.
- **`UPSCALER=sr`** — the self-hosted super-resolution backend behind the existing seam.
- **Intent-based pro-model routing for reasoning** — `routeModel` can pick `geminiPro`, but no reasoning caller consumes it yet (only the prompt modifier lands, in Imagen).
- **Persona reference-image retrieval into director planning**, and persisting Lock-Sheet character/location cards in pgvector.
- **ADK orchestration** wrapping plan/produce/critique/assemble ([../AGENTIC-ADK.md](../AGENTIC-ADK.md)) — today the agentic loop is hand-rolled in `src/director.js` + `src/producer.js`.
