| 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 | /** Captured build identity is independent of the current source/release. */ |
| 32 | export const TERMINAL_SCREENSHOT = { |
| 33 | src: "/codewhale-tui-d7a9a1c.png", |
| 34 | width: 1132, |
| 35 | height: 632, |
| 36 | version: "0.10.0", |
| 37 | sourceCommit: "d7a9a1c8c15635a84099d5fc2ee965146cf197c7", |
| 38 | sha256: "73e56158d6ec6c04933ce787c23c07322ce4d509b54050b54b12d181b09314b8", |
| 39 | } as const; |
| 40 | |
| 41 | /** Published-asset budgets; see the module contract for what tests inspect. */ |
| 42 | export const MEDIA_BUDGETS = { |
| 43 | poster: { width: 1280, height: 720, maxBytes: 500_000 }, |
| 44 | video: { width: 1280, height: 720, maxBytes: 10_000_000, maxDurationSeconds: 120 }, |
| 45 | gifFallback: { maxBytes: 6_000_000 }, |
| 46 | /** WebVTT caption tracks must exist per shipped locale and be non-empty. */ |
| 47 | captionLocales: ["en", "zh"], |
| 48 | } as const; |
| 49 | |
| 50 | /** |
| 51 | * The reduced-motion policy identifier, referenced by the component and |
| 52 | * asserted by the test: static poster, no autoplay, optional GIF link. |
| 53 | */ |
| 54 | export const REDUCED_MOTION_POLICY = "static-poster-no-autoplay" as const; |
| 55 | |
| 56 | /** Directory under web/public/ that holds published session media. */ |
| 57 | export const MEDIA_PUBLIC_DIR = "media"; |
| 58 | |
| 59 | export type MediaStatus = "pending" | "published"; |
| 60 | |
| 61 | export interface MediaPoster { |
| 62 | /** Path relative to web/public/ (e.g. "media/first-fleet-session.png"). */ |
| 63 | src: string; |
| 64 | width: number; |
| 65 | height: number; |
| 66 | alt: LocalizedText; |
| 67 | } |
| 68 | |
| 69 | export interface MediaVideo { |
| 70 | src: string; |
| 71 | /** Measured duration of the shipped file, seconds. */ |
| 72 | durationSeconds: number; |
| 73 | width: number; |
| 74 | height: number; |
| 75 | } |
| 76 | |
| 77 | export interface MediaCaptionsTrack { |
| 78 | /** Path relative to web/public/ (WebVTT). */ |
| 79 | src: string; |
| 80 | srclang: string; |
| 81 | label: string; |
| 82 | } |
| 83 | |
| 84 | export interface MediaAsset { |
| 85 | /** Stable identifier; also the file stem for every asset file. */ |
| 86 | id: string; |
| 87 | title: LocalizedText; |
| 88 | description: LocalizedText; |
| 89 | status: MediaStatus; |
| 90 | /** Shown in place of any imagery while status is "pending". */ |
| 91 | pendingLabel: LocalizedText; |
| 92 | /** Published-only fields; absent while pending (enforced by the test). */ |
| 93 | poster?: MediaPoster; |
| 94 | video?: MediaVideo; |
| 95 | captions?: MediaCaptionsTrack[]; |
| 96 | gifFallback?: { src: string }; |
| 97 | /** Repo-relative transcript document (e.g. "docs/evidence/..."). */ |
| 98 | transcript?: string; |
| 99 | } |
| 100 | |
| 101 | export const MEDIA_ASSETS: MediaAsset[] = [ |
| 102 | { |
| 103 | id: "first-fleet-session", |
| 104 | title: { |
| 105 | en: "A real Codewhale session, end to end", |
| 106 | zh: "一次真实的 Codewhale 端到端会话", |
| 107 | }, |
| 108 | description: { |
| 109 | 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.", |
| 110 | zh: "安装、无密钥的首次会话、接入提供商,以及在本地模型上跑一次 fleet workflow。将从发布版本录制。", |
| 111 | }, |
| 112 | status: "pending", |
| 113 | pendingLabel: { |
| 114 | en: "Recording pending release candidate", |
| 115 | zh: "待发布候选版录制", |
| 116 | }, |
| 117 | }, |
| 118 | ]; |
| 119 | |
| 120 | export function getMediaAsset(id: string): MediaAsset | undefined { |
| 121 | return MEDIA_ASSETS.find((asset) => asset.id === id); |
| 122 | } |
| 123 |