返回 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 /** 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
123 lines TYPESCRIPT