| 1 | /** |
| 2 | * media-manifest.ts — the real-session media surface for codewhale.net. |
| 3 | * |
| 4 | * Every real-session asset the site may show is declared here, with its |
| 5 | * poster, captions, transcript, GIF fallback, and budgets. The manifest is |
| 6 | * the contract: `web/components/session-media.tsx` renders whatever is |
| 7 | * declared, and `web/lib/media-manifest.test.ts` enforces the rules below. |
| 8 | * |
| 9 | * THE HONESTY CONTRACT: |
| 10 | * - A `pending` entry declares intent only. It has NO asset fields, and no |
| 11 | * files may exist for it under `web/public/media/`. The component renders |
| 12 | * a visible "recording pending release candidate" state — never a mock, |
| 13 | * staged, or recycled clip. (Release issue #4906: the dogfood recording |
| 14 | * happens after the v0.9.2 candidate is stable.) |
| 15 | * - A `published` entry is complete or it does not ship: poster, video, |
| 16 | * per-locale captions (WebVTT), a transcript, and a GIF fallback are all |
| 17 | * required. Tests verify presence and byte budgets, inspect PNG poster |
| 18 | * dimensions, and compare declared video metadata with MEDIA_BUDGETS. |
| 19 | * Actual video duration/dimensions remain a recording-checklist gate. |
| 20 | * - Reduced motion is structural, not a media query patch: the video never |
| 21 | * autoplays (`preload="none"`, user-initiated only), the poster is the |
| 22 | * static default, and the GIF fallback link is always visible. |
| 23 | * |
| 24 | * The exact post-dogfood recording procedure lives in |
| 25 | * docs/releases/v0.9.2-media-plan.md. Flip an entry to `published` only by |
| 26 | * following that checklist. |
| 27 | */ |
| 28 | |
| 29 | import type { LocalizedText } from "./content/vocabulary"; |
| 30 | |
| 31 | /** |
| 32 | * The homepage terminal. The site renders it as live text from the PTY cell |
| 33 | * capture (`capture`, via scripts/render-terminal-capture.mjs and |
| 34 | * components/terminal-capture.tsx); `src` is the same frame rasterized by |
| 35 | * that script for the README, which cannot run the site's renderer. Captured |
| 36 | * build identity is independent of the current source/release. |
| 37 | */ |
| 38 | export const TERMINAL_SCREENSHOT = { |
| 39 | capture: "web/lib/terminal-captures/website-home-100x24.json", |
| 40 | src: "/codewhale-tui-8ba2bbf.png", |
| 41 | width: 1000, |
| 42 | height: 480, |
| 43 | version: "0.10.1", |
| 44 | sourceCommit: "8ba2bbf805e1393ba27493bb5970db88b14ff7b3", |
| 45 | sha256: "7634edb50482ad098743ab14accc1b5836766211359ffa4fc763072cac396316", |
| 46 | } as const; |
| 47 | |
| 48 | /** Published-asset budgets; see the module contract for what tests inspect. */ |
| 49 | export const MEDIA_BUDGETS = { |
| 50 | poster: { width: 1280, height: 720, maxBytes: 500_000 }, |
| 51 | video: { width: 1280, height: 720, maxBytes: 10_000_000, maxDurationSeconds: 120 }, |
| 52 | gifFallback: { maxBytes: 6_000_000 }, |
| 53 | /** WebVTT caption tracks must exist per shipped locale and be non-empty. */ |
| 54 | captionLocales: ["en", "zh"], |
| 55 | } as const; |
| 56 | |
| 57 | /** |
| 58 | * The reduced-motion policy identifier, referenced by the component and |
| 59 | * asserted by the test: static poster, no autoplay, optional GIF link. |
| 60 | */ |
| 61 | export const REDUCED_MOTION_POLICY = "static-poster-no-autoplay" as const; |
| 62 | |
| 63 | /** Directory under web/public/ that holds published session media. */ |
| 64 | export const MEDIA_PUBLIC_DIR = "media"; |
| 65 | |
| 66 | export type MediaStatus = "pending" | "published"; |
| 67 | |
| 68 | export interface MediaPoster { |
| 69 | /** Path relative to web/public/ (e.g. "media/first-fleet-session.png"). */ |
| 70 | src: string; |
| 71 | width: number; |
| 72 | height: number; |
| 73 | alt: LocalizedText; |
| 74 | } |
| 75 | |
| 76 | export interface MediaVideo { |
| 77 | src: string; |
| 78 | /** Measured duration of the shipped file, seconds. */ |
| 79 | durationSeconds: number; |
| 80 | width: number; |
| 81 | height: number; |
| 82 | } |
| 83 | |
| 84 | export interface MediaCaptionsTrack { |
| 85 | /** Path relative to web/public/ (WebVTT). */ |
| 86 | src: string; |
| 87 | srclang: string; |
| 88 | label: string; |
| 89 | } |
| 90 | |
| 91 | export interface MediaAsset { |
| 92 | /** Stable identifier; also the file stem for every asset file. */ |
| 93 | id: string; |
| 94 | title: LocalizedText; |
| 95 | description: LocalizedText; |
| 96 | status: MediaStatus; |
| 97 | /** Shown in place of any imagery while status is "pending". */ |
| 98 | pendingLabel: LocalizedText; |
| 99 | /** Published-only fields; absent while pending (enforced by the test). */ |
| 100 | poster?: MediaPoster; |
| 101 | video?: MediaVideo; |
| 102 | captions?: MediaCaptionsTrack[]; |
| 103 | gifFallback?: { src: string }; |
| 104 | /** Repo-relative transcript document (e.g. "docs/evidence/..."). */ |
| 105 | transcript?: string; |
| 106 | } |
| 107 | |
| 108 | export const MEDIA_ASSETS: MediaAsset[] = [ |
| 109 | { |
| 110 | id: "first-fleet-session", |
| 111 | title: { |
| 112 | en: "A real Codewhale session, end to end", |
| 113 | zh: "一次真实的 Codewhale 端到端会话", |
| 114 | }, |
| 115 | description: { |
| 116 | en: "Install, a first session with no key, connecting a provider, and one fleet workflow on a local model. To be recorded from a release build.", |
| 117 | zh: "安装、无密钥的首次会话、接入提供商,以及在本地模型上跑一次 fleet workflow。将从发布版本录制。", |
| 118 | }, |
| 119 | status: "pending", |
| 120 | pendingLabel: { |
| 121 | en: "Recording pending release candidate", |
| 122 | zh: "待发布候选版录制", |
| 123 | }, |
| 124 | }, |
| 125 | ]; |
| 126 | |
| 127 | export function getMediaAsset(id: string): MediaAsset | undefined { |
| 128 | return MEDIA_ASSETS.find((asset) => asset.id === id); |
| 129 | } |
| 130 |