# Preview Portal Audit

The review and delivery portal standardizes the separate HTML files that were
previously generated by project-specific scripts.

| Example | Surface | Controls | Lightbox | Downloads | Notes |
|---|---|---:|---:|---:|---|
| `guardians-of-the-dawn/review.html` | editor review | yes | yes | partial | Source for editor approve/regenerate controls and copyable agent handoff. |
| `guardians-of-the-dawn/client-review.html` | client review | simplified | yes | partial | Source for client approve/decline/comment mode. |
| `mirchi-mode/preview.html` | final preview | no | no | yes | Source for final showcase and download behavior. |
| `dhuaan-music-video/preview.html` | project preview | no | no | no | Should migrate to generated `music-video` preview. |

## Standard Surfaces

The portal generates **three** surfaces per project by default: a tabbed
**Review** (the feedback loop), a clean **Preview** (the deliverable), and a live
**Run** dashboard (the operations view). Review and Preview are opposite jobs —
Review exists to *change* the output (feedback → regenerate, via the
copy→paste-back-to-Claude protocol); Preview exists to *show* the finished output
for upload/delivery; Run exists to *watch* the render in flight. The former `edit`
/ `client-review` / `compare` pages are consolidated **into Review** (a
Decide/Compare tab set + an editor↔client mode toggle); they remain valid
`--surface` aliases for back-compat but are no longer generated by default.

| File | Audience | Purpose |
|---|---|---|
| `review.html` | operator ↔ Claude (+ client mode) | The single feedback surface. **Decide** tab: approve/regenerate per unit → `VIDEOCLAW_REVIEW_DECISIONS` copy. **Compare** tab: run/version comparison. A low-key **client mode** toggle swaps controls to approve/decline/comment → `VIDEOCLAW_CLIENT_FEEDBACK` copy. Absorbs the old edit/client-review/compare surfaces. |
| `preview.html` | final viewer | Polished, **controls-free** deliverable: click-to-fullscreen lightbox on every production image, a soundtrack `<audio controls preload="none">` player (when a soundtrack is discovered), aspect-aware sizing, and asset downloads. The upload artifact. |
| `run.html` | operator | The **live run dashboard**: one card per generation (storyboard scene) with a STATUS badge (`done`/`rendering`/`pending`/`failed`), the provider job id + error, the input keyframe, a playable in-progress clip (`outputs/scene-N.mp4`), the exact submit prompt + contract, a spend estimate chip, an event log (`events/events.jsonl`), per-card copy-command buttons, and a Show › Episode header from `show-bible.json`. Paints a RED **diff-vs-contract alarm** when the submitted payload diverged from the current contract (the `@tag`-hijacks-references bug), backed by the frozen `artifacts/run-contract.json` snapshot. Auto-refreshes via `<meta http-equiv="refresh">`, and is regenerated automatically on every `produce`/`execute` and `execute-status` poll (`VCLAW_NO_RUN_SURFACE=1` opts out). |
| `projects/index.html` | operator | Portfolio index across generated projects (links to each project's Review + Preview). |
| `projects/clients/<client>/index.html` | client/operator | Client-filtered index across that client's projects. |

> Aliases: `--surface edit` and `--surface review` both render `review.html`
> (editor mode default); `--surface client-review` renders it with client mode
> default; `--surface compare` renders the standalone compare page; `--surface
> run` renders the live run dashboard. Default generation is
> `['review','preview','run']`.

## Template Registry

The renderer keeps the HTML structure consistent while adapting section labels
and ordering for `music-video`, `story-film`, `documentary`, `product-ad`,
`sports-recap`, and `generic-video`. Projects can set `template` or
`previewTemplate` in `project.json`; unknown values fall back to
`generic-video`.

The portal also reads `artifacts/asset-manifest.json` for image assets used by
generation routes. Local project-scoped image inputs are rendered in a dedicated
generation-input section; for Seedance-backed assets this appears as
`Seedance Input Frames` in the music-video template. This makes the exact
start-frame/upscaled image being sent into Seedance visible beside the resulting
clip, instead of relying only on the generic `images/` folder.

When `artifacts/filmmaking-prompts.json` exists, the portal exposes it under the
prompt-packet section, labelled `Seedance Prompt Packets` for music-video
projects. That gives reviewers one place to inspect the generated `@image`
reference map, character-sheet prompts, 9-panel storyboard-grid prompt, and
Seedance packet text before or after video generation.

## Per-Scene Submit Contract

Each storyboard scene card renders the exact contract that will be submitted for
that scene, expanded inline (no click-to-reveal), so an operator can confirm
"exactly what you're going to get" before any render spend:

- **Submit prompt — what the model receives.** The resolved provider submit text
  is the `seedancePackets[].promptText` (the 10-block master prompt) from
  `artifacts/filmmaking-prompts.json`, matched to the scene by `sceneIndex`. The
  lighter storyboard `scenePrompt` fields (`imagePrompt`, `animationPrompt`,
  `styleFooter`) are shown alongside it. This is the convergence point where the
  multi-shot plan, the cinematography registers, and the story-bible continuity
  all land in one packet.
- **References — identity lock.** Each `seedancePackets[].references` slot is
  rendered with its role, a `ready`/`pending` badge, and the binding. For
  character-sheet slots the binding is the `Asset://` URI resolved from
  `artifacts/seedance-assets.json` by character name (preferring `intlAssetUri`);
  an unregistered character renders as `<name> · unregistered`. Discovered
  character reference thumbnails are shown beneath the slots. This lets a reviewer
  confirm which avatar locks which scene, and that every required reference is
  ready, before submitting.

The data is sourced entirely from on-disk artifacts (`storyboard.json`,
`filmmaking-prompts.json`, `seedance-assets.json`); the card falls back
gracefully (omitting the missing block) when an artifact is absent.

### Per-scene Image / Video toggle

When a scene has a rendered clip on disk at
`projects/<slug>/outputs/scene-<sceneIndex>.mp4` (0-based; resolved
`existsSync`-guarded at discovery into a project-relative `scene.clipPath`), the
storyboard card adds an inline
`<video class="clip" controls preload="none" playsinline>` (hidden by default)
plus a small two-button **Image / ▶ Video** toggle. The keyframe image shows by
default; clicking **▶ Video** swaps the card's media to the playing clip, and
**Image** swaps back. Show/hide is driven by a `data-mode` attribute on the card
plus CSS (no per-card inline scripts); a single shared `setPortalMode(btn, mode)`
in the portal JS flips the mode and pauses the clip on Image / plays it on Video,
wired once via the `data-portal-mode` buttons. The clip `src` mirrors the
keyframe `<img>` base (both project-relative). A scene with no rendered clip
renders byte-identically to before (keyframe image + download only — no toggle,
no `<video>`). The toggle appears on every surface that renders scene cards
(`preview` and `review`), automatically for every project.

### Voice design + exact-submit JSON

When `artifacts/voice-clones.json` exists, each storyboard scene card also
renders, after the references block:

- **Voice design — cloned voice.** For every character the scene casts that has
  a matching voice clone (matched on `voice.character`, case-insensitively), a
  labeled row is emitted with a `videoAssetId`/`videoAssetId2`-style pill, the
  voice label (name/character/description + duration), and a playable
  `<audio controls preload="none">` so the operator can HEAR the voice before
  spend. Voices are ordered by cast order (first → `videoAssetId`, second →
  `videoAssetId2`, …). The playable `src` is the durable `hostedUrl` when
  present, else the local `clipPath`.
- **Exact JSON → seedance-2.** A collapsed `<details>` showing the plain submit
  body that will hit the provider for this scene: `model`, `aspect_ratio`
  (the project aspect), `audio: true`, `duration` (from the scene), a
  `text_prompt` placeholder, the `imageAssetId1..N` from the scene's resolved
  reference slots (Asset:// URI or path, filename only) or discovered character
  thumbnails, and the `videoAssetId`/`videoAssetId2` from the voice clip
  filenames. This is the auditable payload contract, expanded on demand.

Both blocks are **default-off**: a project without `artifacts/voice-clones.json`
(and a scene with no references) renders byte-identically to before — no Voice
design block, no exact-submit JSON. This makes
`vclaw video portal --surface review --project <slug>` paint the full contract
(prompt + references + voice design + exact JSON) for every project
automatically, with no hand-rolled HTML.

## Soundtrack Player

The `preview` showcase discovers a project soundtrack and, when one exists,
renders a `<audio controls preload="none">` player in the header strip.
Discovery prefers an explicit `soundtrack` or `audio` field in `project.json`
that resolves to an existing project-local audio file, then falls back to the
first discovered audio asset (`.mp3`/`.m4a`/`.wav`/`.aac`). When no soundtrack
is found, no `<audio>` element is emitted (no broken/empty player). The player
is preview-only; the editor/review/client-review decision surfaces are
unchanged.

## Lightbox on Production Images

Every production image rendered by the showcase carries
`data-lightbox-group` (and `data-lb-caption` where a caption exists) so the
shared `initLightbox` makes each image click-to-fullscreen with caption and
Escape-to-close, with no per-project hand-wiring.

## Empty-preview guardrail

When a project has no renderable media, `generatePreviewPortalSurfaces` returns
a non-empty `warnings` array (echoed to stderr by the `vclaw video portal`
handler) instead of a silently empty page. The usual cause is that the finished
cut was staged at the workspace root rather than under
`projects/<slug>/final/{videos,images,audio}` — the only location the portal
scans. The pure, exported `previewPortalMediaWarnings()` computes the list;
`warnings` is always present (empty when media exists) and the stdout JSON stays
machine-readable.

## Publish Contract

`vclaw video publish-preview` builds a deterministic R2 upload plan from the
HTML file and its local `src`/`href` references. `vclaw video
publish-portal-index` uploads a client or global index whose links point into
the published run folders.

```bash
vclaw video publish-preview \
  --project <slug> \
  --client <name> \
  --bucket <bucket> \
  [--run <id>] \
  [--surface edit|review|client-review|preview|compare|index] \
  [--public-base-url <url>] \
  [--wrangler-bin <path>] \
  [--dry-run]
```

`--project`, `--client`, and `--bucket` are all **required** — the handler
throws if any is missing. `--surface` defaults to `preview`. The `client`,
`project`, and `run` path segments are slugified before forming the R2 key, so
the upload prefix is `clients/<slugified-client>/<slugified-project>/runs/<slugified-run>/`.

Each plan item includes:

- local file path
- remote R2 key under `clients/<client>/<project>/runs/<run>/`
- content type
- SHA-256 hash
- public URL when `--public-base-url` is provided

Use `--dry-run` first to inspect the plan. Running without `--dry-run` executes
`wrangler r2 object put` for each item and appends a `surface.published` event
to `project-audit.jsonl`. Use `--wrangler-bin <path>` to pin a specific
Wrangler executable in CI or agent automation.

Published project pages live under
`clients/<client>/<project>/runs/<run>/`. Published client indexes live at
`clients/<client>/index.html` and link to those project/run pages.
