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 editand--surface reviewboth renderreview.html(editor mode default);--surface client-reviewrenders it with client mode default;--surface comparerenders the standalone compare page;--surface runrenders 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) fromartifacts/filmmaking-prompts.json, matched to the scene bysceneIndex. The lighter storyboardscenePromptfields (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[].referencesslot is rendered with its role, aready/pendingbadge, and the binding. For character-sheet slots the binding is theAsset://URI resolved fromartifacts/seedance-assets.jsonby character name (preferringintlAssetUri); 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 avideoAssetId/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 playablesrcis the durablehostedUrlwhen present, else the localclipPath. - 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), atext_promptplaceholder, theimageAssetId1..Nfrom the scene's resolved reference slots (Asset:// URI or path, filename only) or discovered character thumbnails, and thevideoAssetId/videoAssetId2from 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 wasseedance-modelark: that transport freezes its own exact body intoartifacts/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, andrun.htmlis regenerated by everyproduce --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.
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.
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-urlis 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 whatvocal_swapships; - 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 taggedlinear; - 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.
