Skip to content

Preview Portal Audit ​

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

ExampleSurfaceControlsLightboxDownloadsNotes
guardians-of-the-dawn/review.htmleditor reviewyesyespartialSource for editor approve/regenerate controls and copyable agent handoff.
guardians-of-the-dawn/client-review.htmlclient reviewsimplifiedyespartialSource for client approve/decline/comment mode.
mirchi-mode/preview.htmlfinal previewnonoyesSource for final showcase and download behavior.
dhuaan-music-video/preview.htmlproject previewnononoShould 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.

FileAudiencePurpose
review.htmloperator ↔ 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.htmlfinal viewerPolished, 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.htmloperatorThe 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.htmloperatorPortfolio index across generated projects (links to each project's Review + Preview).
projects/clients/<client>/index.htmlclient/operatorClient-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 13-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. For the Seedance-2 gateway routes (seedance-direct, runway-useapi, dreamina-useapi): 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. It is a hand-written paraphrase of the gateway shape, so it is NOT rendered for a project whose run contract says the route was seedance-modelark: that transport freezes its own exact body into artifacts/run-contract.json (submittedProviderWire, from the transport's planner), and the run surface renders it per card as "Exact JSON → native-modelark · <endpoint>" — or the planner's refusal — with local reference paths shown as sources (they are inlined or hosted at submit). The review/edit surfaces show no exact body for that route: a wrong body is worse than none, and run.html is regenerated by every produce --dry-run.

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.

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.

Sync review (rap-avatar-mv) — a review surface for the ear ​

skills/rap-avatar-mv/scripts/sync_review.py writes projects/<slug>/build/sync-review.html (+ a sync-review.json twin with the same SYNC_REVIEW_DATA contract). It answers one question the portal's picture-first surfaces cannot: does the mouth match the record's words, and does the take's own voice match the record? Judging that from a take's own audio is circular (a model's mouth always matches its own voice), so:

  • the picture is silent — build/sync-review/<name>-silent.mp4, zero audio streams, asserted with ffprobe (a browser leaks a <video> element's own track on seek/fullscreen);
  • four faders carry the sound — the record (hook.wav), the instrumental stem, the vocal stem, and the take's own voice extracted to PCM WAV (AAC priming would offset it by tens of ms). "Instrumental + own voice" is a live preview of what vocal_swap ships;
  • the SHEET's words light up on whisper's clock, holding the current word through a gap until the next begins (lib/sync_player.cjs, inlined verbatim); a line with no whisper words in reach falls back to an even split and is tagged linear;
  • every verdict is read from what the lane wrote (word-sync-*.json, sync-profile-*.json, shift-report-*.json, sync-accepts.json, lane-render.log) with its file and mtime; a clip newer than its verdict is STALE; a missing file is a "not on disk yet" row, never a blank; under vocal-swap the column says "by construction".

One clockOffsetSec per picture source, with its provenance (plan.jobs[w].startSec for a raw take, selection-<sid>.vocalMap.start for a take as cut, 0 for the master body — verified against the song length, a mismatch is a red warning, -front_padding for the film).

Audio drift is nudged by ±2.5 % playbackRate between 30 and 450 ms and hard-seeked only past 450 ms (a scrub). Rebuilt after first-take (named in its gate), render-all, slide and board; listed in the lane menu as Sync. The player library is one file so a later portal "Sync" tab (discovery.ts → hasSyncReview, a data-tab="sync" panel inlining the same .cjs) cannot drift from the lane page.

Built to be driven by agent hosts like Claude Code, Claude Desktop, or Codex · Source-available, commercial use requires a paid license.