@hugpy/video-intelligence-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +41 -0
  2. package/assets/sampels/images/bycycle/bd8603bad35d4f179f4d1134d8faf2ee.mp4 +0 -0
  3. package/assets/sampels/images/bycycle/frame_00000.png +0 -0
  4. package/assets/sampels/images/bycycle/frame_00001.png +0 -0
  5. package/assets/sampels/images/bycycle/frame_00002.png +0 -0
  6. package/assets/sampels/images/bycycle/frame_00003.png +0 -0
  7. package/assets/sampels/images/bycycle/frame_00004.png +0 -0
  8. package/assets/sampels/images/bycycle/frame_00005.png +0 -0
  9. package/assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4 +0 -0
  10. package/assets/sampels/images/forrest/frame_00000.png +0 -0
  11. package/assets/sampels/images/forrest/frame_00001.png +0 -0
  12. package/assets/sampels/images/forrest/frame_00002.png +0 -0
  13. package/assets/sampels/images/forrest/frame_00003.png +0 -0
  14. package/assets/sampels/images/forrest/frame_00004.png +0 -0
  15. package/assets/sampels/images/forrest/frame_00005.png +0 -0
  16. package/assets/sampels/images/shenzo/e1a4fcd623084ab4a986d11c8cd00db8.mp4 +0 -0
  17. package/assets/sampels/images/shenzo/frame_00000.png +0 -0
  18. package/assets/sampels/images/shenzo/frame_00001.png +0 -0
  19. package/assets/sampels/images/shenzo/frame_00002.png +0 -0
  20. package/assets/sampels/images/shenzo/frame_00003.png +0 -0
  21. package/assets/sampels/images/shenzo/frame_00004.png +0 -0
  22. package/assets/sampels/images/shenzo/frame_00005.png +0 -0
  23. package/assets/sampels/scenes/man.mp4 +0 -0
  24. package/assets/sampels/scenes/woman.mp4 +0 -0
  25. package/assets/sampels/studio_movies/caverns/_assembly/contrib_00.mp4 +0 -0
  26. package/assets/sampels/studio_movies/caverns/movie.json +67 -0
  27. package/assets/sampels/studio_movies/caverns/movie.mp4 +0 -0
  28. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/clip.mp4 +0 -0
  29. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/manifest.json +87 -0
  30. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/provenance.json +7 -0
  31. package/assets/sampels/studio_movies/caverns/segment_01/branch.png +0 -0
  32. package/assets/sampels/studio_movies/keeper-matrix/_assembly/concat_list.txt +2 -0
  33. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_00.mp4 +0 -0
  34. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_01.mp4 +0 -0
  35. package/assets/sampels/studio_movies/keeper-matrix/movie.json +81 -0
  36. package/assets/sampels/studio_movies/keeper-matrix/movie.mp4 +0 -0
  37. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4 +0 -0
  38. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/manifest.json +87 -0
  39. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/provenance.json +7 -0
  40. package/assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png +0 -0
  41. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4 +0 -0
  42. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/manifest.json +87 -0
  43. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/provenance.json +7 -0
  44. package/assets/sampels/videos/a_red_paper_boat_drifting_on_a_calm_pond__gentle_ripples__mo_a62d6c28.mp4 +0 -0
  45. package/assets/sampels/videos/aurora.mp4 +0 -0
  46. package/assets/sampels/videos/dark_night.mp4 +0 -0
  47. package/assets/sampels/videos/fire_and_soul.mp4 +0 -0
  48. package/assets/sampels/videos/harsh_seas.mp4 +0 -0
  49. package/entry.tsx +74 -0
  50. package/index.html +53 -0
  51. package/package.json +42 -0
  52. package/src/assets/hugpy-mark.png +0 -0
  53. package/src/config.ts +676 -0
  54. package/src/demo/DemoBanner.tsx +54 -0
  55. package/src/demo/assets/demo-audio.mp3 +0 -0
  56. package/src/demo/assets/demo-clip.mp4 +0 -0
  57. package/src/demo/assets/demo-crop.mp4 +0 -0
  58. package/src/demo/assets/demo-poster.jpg +0 -0
  59. package/src/demo/assets/frame-1.jpg +0 -0
  60. package/src/demo/assets/frame-2.jpg +0 -0
  61. package/src/demo/assets/frame-3.jpg +0 -0
  62. package/src/demo/assets/frame-4.jpg +0 -0
  63. package/src/demo/assets/frame-5.jpg +0 -0
  64. package/src/demo/assets/frame-6.jpg +0 -0
  65. package/src/demo/assets/gen-1.jpg +0 -0
  66. package/src/demo/assets/gen-2.jpg +0 -0
  67. package/src/demo/assets.d.ts +14 -0
  68. package/src/demo/demo.css +64 -0
  69. package/src/demo/demoFetch.ts +305 -0
  70. package/src/demo/fixtures.ts +814 -0
  71. package/src/demo/index.ts +43 -0
  72. package/src/demo/mode.ts +81 -0
  73. package/src/demo/promptAssist.ts +181 -0
  74. package/src/demo/seed.ts +97 -0
  75. package/src/nav/BrandMark.css +66 -0
  76. package/src/nav/BrandMark.tsx +40 -0
  77. package/src/nav/Navbar.css +13 -0
  78. package/src/nav/Navbar.tsx +68 -0
  79. package/src/regions/SpatialRegionEditor.tsx +375 -0
  80. package/src/regions/TemporalRegionEditor.tsx +485 -0
  81. package/src/regions/types.ts +85 -0
  82. package/src/session.ts +85 -0
  83. package/src/share.ts +77 -0
  84. package/src/stations/ActivePanel.tsx +55 -0
  85. package/src/stations/ActiveProcessesStation.tsx +272 -0
  86. package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +228 -0
  87. package/src/stations/AddFileBar.tsx +59 -0
  88. package/src/stations/AudioCropStation.tsx +157 -0
  89. package/src/stations/ComfyPanel.tsx +210 -0
  90. package/src/stations/ComfyStation.tsx +196 -0
  91. package/src/stations/CondensedKnobStrip.tsx +245 -0
  92. package/src/stations/ConsolePanel.tsx +82 -0
  93. package/src/stations/FrameExtractStation.tsx +148 -0
  94. package/src/stations/GenIdentityBar.tsx +412 -0
  95. package/src/stations/GenerateStation.tsx +5603 -0
  96. package/src/stations/GenerateStation.tsx.bak-tester-removal +5170 -0
  97. package/src/stations/GoalComposer.tsx +139 -0
  98. package/src/stations/ImageCropStation.tsx +153 -0
  99. package/src/stations/KnobInfo.tsx +64 -0
  100. package/src/stations/PlaceholderStation.tsx +26 -0
  101. package/src/stations/SectionTabs.tsx +276 -0
  102. package/src/stations/SharePanel.tsx +298 -0
  103. package/src/stations/SidebarRegistry.tsx +105 -0
  104. package/src/stations/StationShell.tsx +96 -0
  105. package/src/stations/StudioClipsStation.tsx +28 -0
  106. package/src/stations/WorkbenchDrawer.tsx +66 -0
  107. package/src/stations/WorkbenchSidebar.tsx +1224 -0
  108. package/src/stations/WorkbenchStation.tsx +91 -0
  109. package/src/stations/comfyEmbed.ts +79 -0
  110. package/src/stations/drawerShell.tsx +419 -0
  111. package/src/stations/ops/AudioCropCore.tsx +218 -0
  112. package/src/stations/ops/CharacterGroupsPanel.tsx +716 -0
  113. package/src/stations/ops/FrameExtractCore.tsx +695 -0
  114. package/src/stations/ops/ImageCropCore.tsx +164 -0
  115. package/src/stations/ops/VideoCropCore.tsx +193 -0
  116. package/src/stations/ops/useVideoCropJobs.ts +224 -0
  117. package/src/stations/pollCaps.ts +87 -0
  118. package/src/stations/registry.ts +191 -0
  119. package/src/stations/studio/AngleRingEditor.tsx +159 -0
  120. package/src/stations/studio/IdentitiesStation.tsx +2455 -0
  121. package/src/stations/studio/IdentityProfileControls.tsx +262 -0
  122. package/src/stations/studio/StudioActiveProcesses.tsx +224 -0
  123. package/src/stations/studio/StudioAssistLogPanel.tsx +220 -0
  124. package/src/stations/studio/StudioGenerateMode.tsx +29 -0
  125. package/src/stations/studio/StudioGenerateTab.tsx +2688 -0
  126. package/src/stations/studio/StudioItemCard.tsx +334 -0
  127. package/src/stations/studio/StudioLibraryTab.tsx +506 -0
  128. package/src/stations/studio/StudioMovieComposer.tsx +2875 -0
  129. package/src/stations/studio/StudioMovieComposer.tsx.bak-sessions-20260812 +2729 -0
  130. package/src/stations/studio/StudioMovieSessions.tsx +553 -0
  131. package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +417 -0
  132. package/src/stations/studio/StudioPlane.tsx +302 -0
  133. package/src/stations/studio/StudioSplicedRow.tsx +476 -0
  134. package/src/stations/studio/StudioViewer.tsx +339 -0
  135. package/src/stations/studio/TurntableViewer.tsx +220 -0
  136. package/src/stations/studio/movieTimeline.ts +198 -0
  137. package/src/stations/studio/promptCard.tsx +498 -0
  138. package/src/stations/studio/studioItem.ts +87 -0
  139. package/src/stations/studio/studioShared.tsx +821 -0
  140. package/src/stations/studio/useIdentityProfiles.ts +575 -0
  141. package/src/stations/studio/useReconstruction.ts +283 -0
  142. package/src/stations/types.ts +43 -0
  143. package/src/stations/useAudioCropJobs.ts +202 -0
  144. package/src/stations/useAudioExtractJob.ts +144 -0
  145. package/src/stations/useCropJobs.ts +206 -0
  146. package/src/stations/useFrameJobs.ts +140 -0
  147. package/src/stations/useGenerateJob.ts +151 -0
  148. package/src/stations/useGenerateMovieJob.ts +177 -0
  149. package/src/stations/useGenerateRowJobs.ts +184 -0
  150. package/src/stations/useGenerateSceneJob.ts +156 -0
  151. package/src/stations/useGenerateSceneRowJobs.ts +282 -0
  152. package/src/style/app.css +4974 -0
  153. package/src/transport/client.ts +262 -0
  154. package/src/transport/telemetry.ts +97 -0
  155. package/src/video/DropReceptacle.tsx +122 -0
  156. package/src/video/PromptAssistButtons.tsx +118 -0
  157. package/src/video/composerBridge.ts +66 -0
  158. package/src/video/contract.ts +599 -0
  159. package/src/video/jobTracker.ts +702 -0
  160. package/src/video/mediaLibrary.ts +328 -0
  161. package/src/video/renderCompat.ts +512 -0
  162. package/src/video/sessionForm.ts +189 -0
  163. package/src/video/spreadAssist.ts +392 -0
  164. package/src/video/studioBridge.ts +82 -0
  165. package/src/video/useMediaJobs.ts +279 -0
  166. package/src/video/useModels.ts +413 -0
  167. package/src/video/useMoviePresets.ts +97 -0
  168. package/src/video/useMovieSessions.ts +292 -0
  169. package/src/video/usePresets.ts +167 -0
  170. package/src/video/useProjects.ts +87 -0
  171. package/src/video/usePromptAssist.ts +394 -0
  172. package/src/video/useRenderPresets.ts +221 -0
  173. package/src/video/useStudioAssistLog.ts +266 -0
  174. package/src/video/useStudioPresets.ts +128 -0
  175. package/src/video/useWorkerCalls.ts +165 -0
  176. package/tsconfig.json +17 -0
  177. package/vite.config.ts +33 -0
@@ -0,0 +1,54 @@
1
+ // DemoBanner — the slim demo-mode strip above the video stations, modeled on
2
+ // the media arm's banner MINUS the canned⇄live flip: the video arm has no live
3
+ // demo (jobs would drive real worker GPUs — see mode.ts). Announces sample-data
4
+ // mode, offers the install command, links to the platform. Suppressed when
5
+ // embedded (the host showroom provides this chrome).
6
+ import { useState } from "react";
7
+ import { getDemoConfig } from "./mode";
8
+ import { hugpyConfig } from "../config";
9
+ import "./demo.css";
10
+
11
+ const INSTALL_CMD = "pip install hugpy && hugpy serve --port 7002";
12
+
13
+ export default function DemoBanner() {
14
+ const [copied, setCopied] = useState(false);
15
+ const cfg = getDemoConfig();
16
+ if (cfg.mode !== "canned" || cfg.embed) return null;
17
+
18
+ const copy = () => {
19
+ try {
20
+ void navigator.clipboard.writeText(INSTALL_CMD);
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 1500);
23
+ } catch {
24
+ /* clipboard blocked */
25
+ }
26
+ };
27
+
28
+ return (
29
+ <div className="vi-demo-banner" role="note">
30
+ <span className="vi-demo-badge">DEMO</span>
31
+ <span className="vi-demo-text">
32
+ You&apos;re exploring with <strong>sample data</strong> — nothing here
33
+ runs a real model or touches a backend. Run it for real:
34
+ </span>
35
+ <code
36
+ className="vi-demo-cmd"
37
+ title="Click to copy"
38
+ onClick={copy}
39
+ role="button"
40
+ tabIndex={0}
41
+ >
42
+ {copied ? "copied ✓" : INSTALL_CMD}
43
+ </code>
44
+ <a
45
+ className="vi-demo-link"
46
+ href={hugpyConfig.siteUrl}
47
+ target="_blank"
48
+ rel="noreferrer"
49
+ >
50
+ hugpy.ai →
51
+ </a>
52
+ </div>
53
+ );
54
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,14 @@
1
+ // Vite bundles these as hashed asset URLs; the scoped strict tsconfig has no
2
+ // vite/client ambient types (see config.ts), so declare the demo assets here.
3
+ declare module "*.mp4" {
4
+ const url: string;
5
+ export default url;
6
+ }
7
+ declare module "*.mp3" {
8
+ const url: string;
9
+ export default url;
10
+ }
11
+ declare module "*.jpg" {
12
+ const url: string;
13
+ export default url;
14
+ }
@@ -0,0 +1,64 @@
1
+ /* Demo-mode banner for the video-intelligence arm — mirrors the media arm's
2
+ banner styling (mi-demo-*) so the two arms read as one product. */
3
+
4
+ /* Rides in .vi-header-stack directly under the nav — a plain in-flow block; the
5
+ 100dvh shell never scrolls, so no sticky of its own is needed. */
6
+ .vi-demo-banner {
7
+ display: flex;
8
+ align-items: center;
9
+ flex-wrap: wrap;
10
+ gap: 8px 14px;
11
+ padding: 8px 16px;
12
+ font-size: 13px;
13
+ line-height: 1.4;
14
+ color: var(--text-secondary, #c7c7cc);
15
+ background: linear-gradient(180deg, #14171f, #0b0d13);
16
+ border-bottom: 1px solid var(--border-light, rgba(255, 255, 255, 0.09));
17
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
18
+ }
19
+
20
+ .vi-demo-badge {
21
+ flex: 0 0 auto;
22
+ font-weight: 700;
23
+ font-size: 11px;
24
+ letter-spacing: 0.12em;
25
+ padding: 2px 8px;
26
+ border-radius: 5px;
27
+ color: #050505;
28
+ background: #7cc4ff;
29
+ }
30
+
31
+ .vi-demo-text {
32
+ flex: 1 1 280px;
33
+ min-width: 200px;
34
+ color: var(--text-tertiary, #8e8e93);
35
+ }
36
+ .vi-demo-text strong {
37
+ color: var(--text-primary, #f2f2f4);
38
+ }
39
+
40
+ .vi-demo-cmd {
41
+ flex: 0 1 auto;
42
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
43
+ font-size: 12px;
44
+ padding: 3px 10px;
45
+ border-radius: 6px;
46
+ cursor: pointer;
47
+ color: #a8d2ff;
48
+ background: rgba(124, 196, 255, 0.08);
49
+ border: 1px solid rgba(124, 196, 255, 0.25);
50
+ white-space: nowrap;
51
+ }
52
+ .vi-demo-cmd:hover {
53
+ background: rgba(124, 196, 255, 0.16);
54
+ }
55
+
56
+ .vi-demo-link {
57
+ flex: 0 0 auto;
58
+ color: #7cc4ff;
59
+ text-decoration: none;
60
+ font-weight: 600;
61
+ }
62
+ .vi-demo-link:hover {
63
+ text-decoration: underline;
64
+ }
@@ -0,0 +1,305 @@
1
+ // The canned-demo transport: ONE window.fetch interceptor answering every hugpy
2
+ // call from fixtures.ts. The real station shell + transport run unchanged; only
3
+ // the wire is faked. Mirrors media_intelligence_ui/src/demo/demoFetch.ts, minus
4
+ // the live pass-through flavor (the video arm has no live demo — see mode.ts).
5
+ //
6
+ // Patching window.fetch (not a config hook) is deliberate: the arm's typed
7
+ // transport, entry.tsx's contract probe, and any raw fetch all call the global
8
+ // fetch, so a single wrapper covers every path. Fully restorable on uninstall.
9
+ // <img>/<video> src loads do NOT pass through fetch — fixtures point those at
10
+ // bundled asset URLs instead (see fixtures.ts + config.ts mediaBytesUrl).
11
+
12
+ import { hugpyConfig } from "../config";
13
+ import * as fx from "./fixtures";
14
+ import type { JobKind } from "./fixtures";
15
+ import { nextGeneratedPrompt, enhancePrompt } from "./promptAssist";
16
+
17
+ // ── request introspection (same helpers as the media arm) ───────────────────
18
+ function pathOf(input: RequestInfo | URL): string {
19
+ let url =
20
+ typeof input === "string"
21
+ ? input
22
+ : input instanceof Request
23
+ ? input.url
24
+ : String((input as URL)?.toString?.() ?? input ?? "");
25
+ if (/^https?:\/\//i.test(url)) {
26
+ try {
27
+ const u = new URL(url);
28
+ url = u.pathname + u.search;
29
+ } catch {
30
+ /* keep raw */
31
+ }
32
+ }
33
+ return url;
34
+ }
35
+
36
+ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
37
+ return String(
38
+ init?.method ??
39
+ (input instanceof Request ? input.method : undefined) ??
40
+ "GET",
41
+ ).toUpperCase();
42
+ }
43
+
44
+ function apiPrefix(): string {
45
+ const b = hugpyConfig.apiBase || "/api";
46
+ if (/^https?:\/\//i.test(b)) {
47
+ try {
48
+ return new URL(b).pathname.replace(/\/+$/, "");
49
+ } catch {
50
+ return "/api";
51
+ }
52
+ }
53
+ return b.replace(/\/+$/, "");
54
+ }
55
+
56
+ function normalize(path: string): string {
57
+ const pre = apiPrefix();
58
+ if (pre && path.startsWith(pre)) return path.slice(pre.length) || "/";
59
+ return path;
60
+ }
61
+
62
+ async function readBody(
63
+ input: RequestInfo | URL,
64
+ init?: RequestInit,
65
+ ): Promise<string> {
66
+ if (init && typeof init.body === "string") return init.body;
67
+ if (input instanceof Request) {
68
+ try {
69
+ return await input.clone().text();
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
74
+ return "";
75
+ }
76
+
77
+ function jsonResponse(body: unknown, status = 200): Response {
78
+ return new Response(JSON.stringify(body), {
79
+ status,
80
+ headers: { "content-type": "application/json" },
81
+ });
82
+ }
83
+
84
+ // ── the router ───────────────────────────────────────────────────────────────
85
+ const JOB_ROUTES: Record<string, JobKind> = {
86
+ "/video/jobs/crop": "crop",
87
+ "/video/jobs/frame_extract": "frame_extract",
88
+ "/video/jobs/audio_extract": "audio_extract",
89
+ "/video/jobs/generate_image": "generate_image",
90
+ "/video/jobs/generate_scene": "generate_scene",
91
+ "/video/jobs/generate_movie": "generate_movie",
92
+ };
93
+
94
+ async function route(
95
+ input: RequestInfo | URL,
96
+ init?: RequestInit,
97
+ ): Promise<Response | null> {
98
+ const full = pathOf(input);
99
+ const [pathname, query = ""] = full.split("?");
100
+ const path = normalize(pathname);
101
+ const method = methodOf(input, init);
102
+
103
+ // Contract probe (entry.tsx) — always the expected version.
104
+ if (path === "/version") return jsonResponse({ api: 1 });
105
+
106
+ // Session metering — pretend to meter, touch nothing.
107
+ if (path.startsWith("/session/")) return jsonResponse({ ok: true });
108
+
109
+ // Upload: accept anything, answer with a plausible server file handle. The
110
+ // subsequent /video/ingest resolves it to the canned clip regardless.
111
+ if (path === "/uploads" && method === "POST") {
112
+ let name = "demo-clip.mp4";
113
+ const body = init?.body;
114
+ if (body instanceof FormData) {
115
+ const f = body.get("file");
116
+ if (f instanceof File && f.name) name = f.name;
117
+ }
118
+ return jsonResponse({ path: `/demo/uploads/${name}`, name, size: 55757 });
119
+ }
120
+
121
+ // Ingest: whatever was "uploaded", the demo's source is the canned clip.
122
+ if (path === "/video/ingest" && method === "POST") {
123
+ return jsonResponse(fx.demoClip);
124
+ }
125
+
126
+ // Job enqueues — remember the source kind so crop can answer image vs video.
127
+ const kind = JOB_ROUTES[path];
128
+ if (kind && method === "POST") {
129
+ let sourceKind: string | null = null;
130
+ try {
131
+ const body = JSON.parse((await readBody(input, init)) || "{}") as {
132
+ source?: { kind?: string };
133
+ };
134
+ sourceKind = body?.source?.kind ?? null;
135
+ } catch {
136
+ /* body optional */
137
+ }
138
+ return jsonResponse({ job_id: fx.enqueueJob(kind, sourceKind) });
139
+ }
140
+
141
+ // Job cancel (before poll — its path is a superset).
142
+ const cancelM = path.match(/^\/video\/jobs\/([^/]+)\/cancel$/);
143
+ if (cancelM && method === "POST") {
144
+ return jsonResponse(fx.cancelJob(decodeURIComponent(cancelM[1])));
145
+ }
146
+
147
+ // Job poll.
148
+ const poll = path.match(/^\/video\/jobs\/([^/]+)$/);
149
+ if (poll && method === "GET") {
150
+ return jsonResponse(fx.jobStatus(decodeURIComponent(poll[1])));
151
+ }
152
+
153
+ // Media bytes via fetch (waveform decode etc.): serve the bundled asset the
154
+ // handle already IS — through the REAL fetch, never back into the shim.
155
+ // Element src loads never reach here.
156
+ if (path === "/video/media" && method === "GET") {
157
+ const handle = new URLSearchParams(query).get("handle");
158
+ if (handle && _realFetch) return _realFetch(handle);
159
+ return jsonResponse({ error: "not found" }, 404);
160
+ }
161
+
162
+ // LLM PROMPT-ASSIST (Enhance / Generate) — the composers' ✨ buttons POST here. In
163
+ // canned mode we answer CLIENT-SIDE from the fragment banks in promptAssist.ts (zero
164
+ // backend contact): "generate" composes a fresh cinematic prompt (never the same one
165
+ // twice in a row), "detail" enriches the submitted draft with appended descriptors.
166
+ // The body shape { prompt, model, kind } matches what the stations parse
167
+ // (readAssistPrompt reads `.prompt`); `kind` echoes the request's context.kind.
168
+ if (path === "/video/prompt/assist" && method === "POST") {
169
+ let mode = "generate";
170
+ let draft = "";
171
+ let kind = "image";
172
+ try {
173
+ const body = JSON.parse((await readBody(input, init)) || "{}") as {
174
+ mode?: string;
175
+ draft?: string;
176
+ context?: { kind?: string };
177
+ };
178
+ if (body?.mode) mode = body.mode;
179
+ if (typeof body?.draft === "string") draft = body.draft;
180
+ if (body?.context?.kind) kind = body.context.kind;
181
+ } catch {
182
+ /* body optional — defaults stand */
183
+ }
184
+ const prompt = mode === "detail" ? enhancePrompt(draft) : nextGeneratedPrompt();
185
+ return jsonResponse({ prompt, model: "demo-showroom-llm", kind });
186
+ }
187
+
188
+ // Model registry + task defaults for the station dropdowns.
189
+ if (path === "/v1/models") return jsonResponse(fx.V1_MODELS);
190
+ if (path === "/prompt/tasks") return jsonResponse(fx.PROMPT_TASKS);
191
+
192
+ // Unified in-flight worker feed (Active Processes poller, useWorkerCalls.ts,
193
+ // ~2s cadence). The brochure runs nothing, so the honest envelope is an empty
194
+ // in-flight list — { jobs: [] } is exactly the shape parseJobs() projects, and
195
+ // an empty list keeps the panel truthful (no fabricated running render). Routed
196
+ // (not gated) so the poll simply never reaches the wire in canned mode.
197
+ if (path === "/llm/jobs" && method === "GET") {
198
+ return jsonResponse({ jobs: [] });
199
+ }
200
+
201
+ // Bus-wide MEDIA JOBS listing (Active Processes media poller, useMediaJobs.ts,
202
+ // ~2s cadence, GET bare /video/jobs — no id, so the job-poll regex below never
203
+ // matches it). Same honest empty in-flight list as /llm/jobs. Must sit ABOVE
204
+ // the /video/jobs/<id> poll route so the bare path resolves here first.
205
+ if (path === "/video/jobs" && method === "GET") {
206
+ return jsonResponse({ jobs: [] });
207
+ }
208
+
209
+ // Known PROJECT names (Settings project combobox, useProjects.ts, on mount) —
210
+ // GET → { projects: [name,…] }. The brochure has produced nothing durable, so
211
+ // an empty list is honest and passes projectsResponseSchema.
212
+ if (path === "/video/projects" && method === "GET") {
213
+ return jsonResponse({ projects: [] });
214
+ }
215
+
216
+ // Curated STUDIO clip presets (Studio Clips station, useStudioPresets.ts, on
217
+ // mount) — GET → { presets: [...] }. Empty envelope passes
218
+ // studioPresetsResponseSchema; the showroom's Studio curation is the movie/
219
+ // video presets already routed above.
220
+ if (path === "/video/studio/presets" && method === "GET") {
221
+ return jsonResponse({ presets: [] });
222
+ }
223
+
224
+ // Durable recent STUDIO CLIPS (Studio Clips list) — GET → { clips: [...] }.
225
+ // The brochure has no persisted renders; empty is honest.
226
+ if (path === "/video/studio/clips" && method === "GET") {
227
+ return jsonResponse({ clips: [] });
228
+ }
229
+
230
+ // CINEMA SESSIONS (the Cinema composer's sessions panel) — GET → { movies: [...] }.
231
+ // Same honesty as the clips list above: the brochure has no movie dirs on disk, so
232
+ // there is nothing to resume; an empty envelope passes the panel's schema and it
233
+ // says so in words rather than erroring.
234
+ if (path === "/video/studio/movies" && method === "GET") {
235
+ return jsonResponse({ movies: [] });
236
+ }
237
+
238
+ // IDENTITY PROFILES (Studio identity picker, useIdentityProfiles.ts, on mount)
239
+ // — GET → { profiles: [...] }. No durable identities in the showroom; empty.
240
+ if (path === "/video/identity-profiles" && method === "GET") {
241
+ return jsonResponse({ profiles: [] });
242
+ }
243
+
244
+ // k9 VIDEO-SHARE links (SharePanel probe on mount + mint/revoke). The public
245
+ // brochure principal is NOT an operator, so the honest answer to every verb is
246
+ // 401 — exactly what the real operator-gated route returns to a non-operator.
247
+ // The transport folds a shim 401 into a clean `unauthorized` Result (no throw,
248
+ // no browser console error, no wire hit), and SharePanel then hides the Share
249
+ // affordance (authed=false), matching production behaviour for a share/anon
250
+ // session. Covers the list GET, the mint POST, and the per-id revoke DELETE.
251
+ if (path === "/keys/video-share" || path.startsWith("/keys/video-share/")) {
252
+ return jsonResponse({ error: "operator authentication required" }, 401);
253
+ }
254
+
255
+ // Curated presets for the Generate-station dropdown + the per-id pre-warm.
256
+ if (path === "/video/presets" && method === "GET") {
257
+ return jsonResponse(fx.PRESETS);
258
+ }
259
+ const presetApply = path.match(/^\/video\/presets\/([^/]+)\/apply$/);
260
+ if (presetApply && method === "POST") {
261
+ const result = fx.applyPreset(decodeURIComponent(presetApply[1]));
262
+ // Mirror the contract's status codes: 404 for an unknown preset, else 200.
263
+ return jsonResponse(result, result.ok ? 200 : 404);
264
+ }
265
+
266
+ // Curated MOVIE TEMPLATES for the Movie-tab dropdown + the per-id apply. The list
267
+ // returns a {presets:[…]} envelope (matches /video/presets + the real backend);
268
+ // apply echoes the same full object (404 unknown).
269
+ if (path === "/movie/presets" && method === "GET") {
270
+ return jsonResponse({ presets: fx.MOVIE_PRESETS });
271
+ }
272
+ const moviePresetApply = path.match(/^\/movie\/presets\/([^/]+)\/apply$/);
273
+ if (moviePresetApply && method === "POST") {
274
+ const found = fx.applyMoviePreset(decodeURIComponent(moviePresetApply[1]));
275
+ return jsonResponse(found ?? { error: { code: "unknown_preset" } }, found ? 200 : 404);
276
+ }
277
+
278
+ return null; // not a hugpy call — pass through (bundled assets, HMR, …)
279
+ }
280
+
281
+ // ── install / uninstall ──────────────────────────────────────────────────────
282
+ let _realFetch: typeof window.fetch | null = null;
283
+
284
+ export function installDemoTransport(): void {
285
+ if (_realFetch) return; // already installed
286
+ _realFetch = window.fetch.bind(window);
287
+ window.fetch = async (
288
+ input: RequestInfo | URL,
289
+ init?: RequestInit,
290
+ ): Promise<Response> => {
291
+ try {
292
+ const canned = await route(input, init);
293
+ if (canned) return canned;
294
+ } catch {
295
+ /* fall through to the real fetch on any shim error */
296
+ }
297
+ return _realFetch!(input, init);
298
+ };
299
+ }
300
+
301
+ export function uninstallDemoTransport(): void {
302
+ if (!_realFetch) return;
303
+ window.fetch = _realFetch;
304
+ _realFetch = null;
305
+ }