返回 CodeWhale
media-manifest.ts
根目录 / web / lib / media-manifest.ts
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
130 lines TYPESCRIPT