返回 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 /** Published-asset budgets; see the module contract for what tests inspect. */
32 export const MEDIA_BUDGETS = {
33 poster: { width: 1280, height: 720, maxBytes: 500_000 },
34 video: { width: 1280, height: 720, maxBytes: 10_000_000, maxDurationSeconds: 120 },
35 gifFallback: { maxBytes: 6_000_000 },
36 /** WebVTT caption tracks must exist per shipped locale and be non-empty. */
37 captionLocales: ["en", "zh"],
38 } as const;
39
40 /**
41 * The reduced-motion policy identifier, referenced by the component and
42 * asserted by the test: static poster, no autoplay, optional GIF link.
43 */
44 export const REDUCED_MOTION_POLICY = "static-poster-no-autoplay" as const;
45
46 /** Directory under web/public/ that holds published session media. */
47 export const MEDIA_PUBLIC_DIR = "media";
48
49 export type MediaStatus = "pending" | "published";
50
51 export interface MediaPoster {
52 /** Path relative to web/public/ (e.g. "media/first-fleet-session.png"). */
53 src: string;
54 width: number;
55 height: number;
56 alt: LocalizedText;
57 }
58
59 export interface MediaVideo {
60 src: string;
61 /** Measured duration of the shipped file, seconds. */
62 durationSeconds: number;
63 width: number;
64 height: number;
65 }
66
67 export interface MediaCaptionsTrack {
68 /** Path relative to web/public/ (WebVTT). */
69 src: string;
70 srclang: string;
71 label: string;
72 }
73
74 export interface MediaAsset {
75 /** Stable identifier; also the file stem for every asset file. */
76 id: string;
77 title: LocalizedText;
78 description: LocalizedText;
79 status: MediaStatus;
80 /** Shown in place of any imagery while status is "pending". */
81 pendingLabel: LocalizedText;
82 /** Published-only fields; absent while pending (enforced by the test). */
83 poster?: MediaPoster;
84 video?: MediaVideo;
85 captions?: MediaCaptionsTrack[];
86 gifFallback?: { src: string };
87 /** Repo-relative transcript document (e.g. "docs/evidence/..."). */
88 transcript?: string;
89 }
90
91 export const MEDIA_ASSETS: MediaAsset[] = [
92 {
93 id: "first-fleet-session",
94 title: {
95 en: "A real Codewhale session, end to end",
96 zh: "一次真实的 Codewhale 端到端会话",
97 },
98 description: {
99 en: "Will be recorded from the v0.9.4 release candidate during dogfood: install, first offline session, provider connection, and one Fleet workflow on a sealed local route. Until that recording exists, no footage is shown here — the manifest and budgets above are the contract it must satisfy.",
100 zh: "将在 dogfood 期间从 v0.9.4 发布候选版录制:安装、首次离线会话、连接提供商,以及在密封本地路由上运行一次 Fleet Workflow。在录制完成之前,这里不展示任何影像——上面的清单与预算就是它必须满足的契约。",
101 },
102 status: "pending",
103 pendingLabel: {
104 en: "Recording pending release candidate",
105 zh: "待发布候选版录制",
106 },
107 },
108 ];
109
110 export function getMediaAsset(id: string): MediaAsset | undefined {
111 return MEDIA_ASSETS.find((asset) => asset.id === id);
112 }
113
113 lines TYPESCRIPT