@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.
- package/LICENSE +41 -0
- package/assets/sampels/images/bycycle/bd8603bad35d4f179f4d1134d8faf2ee.mp4 +0 -0
- package/assets/sampels/images/bycycle/frame_00000.png +0 -0
- package/assets/sampels/images/bycycle/frame_00001.png +0 -0
- package/assets/sampels/images/bycycle/frame_00002.png +0 -0
- package/assets/sampels/images/bycycle/frame_00003.png +0 -0
- package/assets/sampels/images/bycycle/frame_00004.png +0 -0
- package/assets/sampels/images/bycycle/frame_00005.png +0 -0
- package/assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4 +0 -0
- package/assets/sampels/images/forrest/frame_00000.png +0 -0
- package/assets/sampels/images/forrest/frame_00001.png +0 -0
- package/assets/sampels/images/forrest/frame_00002.png +0 -0
- package/assets/sampels/images/forrest/frame_00003.png +0 -0
- package/assets/sampels/images/forrest/frame_00004.png +0 -0
- package/assets/sampels/images/forrest/frame_00005.png +0 -0
- package/assets/sampels/images/shenzo/e1a4fcd623084ab4a986d11c8cd00db8.mp4 +0 -0
- package/assets/sampels/images/shenzo/frame_00000.png +0 -0
- package/assets/sampels/images/shenzo/frame_00001.png +0 -0
- package/assets/sampels/images/shenzo/frame_00002.png +0 -0
- package/assets/sampels/images/shenzo/frame_00003.png +0 -0
- package/assets/sampels/images/shenzo/frame_00004.png +0 -0
- package/assets/sampels/images/shenzo/frame_00005.png +0 -0
- package/assets/sampels/scenes/man.mp4 +0 -0
- package/assets/sampels/scenes/woman.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/_assembly/contrib_00.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/movie.json +67 -0
- package/assets/sampels/studio_movies/caverns/movie.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/manifest.json +87 -0
- package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/provenance.json +7 -0
- package/assets/sampels/studio_movies/caverns/segment_01/branch.png +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/concat_list.txt +2 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_00.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_01.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/movie.json +81 -0
- package/assets/sampels/studio_movies/keeper-matrix/movie.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/manifest.json +87 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/provenance.json +7 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4 +0 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/manifest.json +87 -0
- package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/provenance.json +7 -0
- package/assets/sampels/videos/a_red_paper_boat_drifting_on_a_calm_pond__gentle_ripples__mo_a62d6c28.mp4 +0 -0
- package/assets/sampels/videos/aurora.mp4 +0 -0
- package/assets/sampels/videos/dark_night.mp4 +0 -0
- package/assets/sampels/videos/fire_and_soul.mp4 +0 -0
- package/assets/sampels/videos/harsh_seas.mp4 +0 -0
- package/entry.tsx +74 -0
- package/index.html +53 -0
- package/package.json +42 -0
- package/src/assets/hugpy-mark.png +0 -0
- package/src/config.ts +676 -0
- package/src/demo/DemoBanner.tsx +54 -0
- package/src/demo/assets/demo-audio.mp3 +0 -0
- package/src/demo/assets/demo-clip.mp4 +0 -0
- package/src/demo/assets/demo-crop.mp4 +0 -0
- package/src/demo/assets/demo-poster.jpg +0 -0
- package/src/demo/assets/frame-1.jpg +0 -0
- package/src/demo/assets/frame-2.jpg +0 -0
- package/src/demo/assets/frame-3.jpg +0 -0
- package/src/demo/assets/frame-4.jpg +0 -0
- package/src/demo/assets/frame-5.jpg +0 -0
- package/src/demo/assets/frame-6.jpg +0 -0
- package/src/demo/assets/gen-1.jpg +0 -0
- package/src/demo/assets/gen-2.jpg +0 -0
- package/src/demo/assets.d.ts +14 -0
- package/src/demo/demo.css +64 -0
- package/src/demo/demoFetch.ts +305 -0
- package/src/demo/fixtures.ts +814 -0
- package/src/demo/index.ts +43 -0
- package/src/demo/mode.ts +81 -0
- package/src/demo/promptAssist.ts +181 -0
- package/src/demo/seed.ts +97 -0
- package/src/nav/BrandMark.css +66 -0
- package/src/nav/BrandMark.tsx +40 -0
- package/src/nav/Navbar.css +13 -0
- package/src/nav/Navbar.tsx +68 -0
- package/src/regions/SpatialRegionEditor.tsx +375 -0
- package/src/regions/TemporalRegionEditor.tsx +485 -0
- package/src/regions/types.ts +85 -0
- package/src/session.ts +85 -0
- package/src/share.ts +77 -0
- package/src/stations/ActivePanel.tsx +55 -0
- package/src/stations/ActiveProcessesStation.tsx +272 -0
- package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +228 -0
- package/src/stations/AddFileBar.tsx +59 -0
- package/src/stations/AudioCropStation.tsx +157 -0
- package/src/stations/ComfyPanel.tsx +210 -0
- package/src/stations/ComfyStation.tsx +196 -0
- package/src/stations/CondensedKnobStrip.tsx +245 -0
- package/src/stations/ConsolePanel.tsx +82 -0
- package/src/stations/FrameExtractStation.tsx +148 -0
- package/src/stations/GenIdentityBar.tsx +412 -0
- package/src/stations/GenerateStation.tsx +5603 -0
- package/src/stations/GenerateStation.tsx.bak-tester-removal +5170 -0
- package/src/stations/GoalComposer.tsx +139 -0
- package/src/stations/ImageCropStation.tsx +153 -0
- package/src/stations/KnobInfo.tsx +64 -0
- package/src/stations/PlaceholderStation.tsx +26 -0
- package/src/stations/SectionTabs.tsx +276 -0
- package/src/stations/SharePanel.tsx +298 -0
- package/src/stations/SidebarRegistry.tsx +105 -0
- package/src/stations/StationShell.tsx +96 -0
- package/src/stations/StudioClipsStation.tsx +28 -0
- package/src/stations/WorkbenchDrawer.tsx +66 -0
- package/src/stations/WorkbenchSidebar.tsx +1224 -0
- package/src/stations/WorkbenchStation.tsx +91 -0
- package/src/stations/comfyEmbed.ts +79 -0
- package/src/stations/drawerShell.tsx +419 -0
- package/src/stations/ops/AudioCropCore.tsx +218 -0
- package/src/stations/ops/CharacterGroupsPanel.tsx +716 -0
- package/src/stations/ops/FrameExtractCore.tsx +695 -0
- package/src/stations/ops/ImageCropCore.tsx +164 -0
- package/src/stations/ops/VideoCropCore.tsx +193 -0
- package/src/stations/ops/useVideoCropJobs.ts +224 -0
- package/src/stations/pollCaps.ts +87 -0
- package/src/stations/registry.ts +191 -0
- package/src/stations/studio/AngleRingEditor.tsx +159 -0
- package/src/stations/studio/IdentitiesStation.tsx +2455 -0
- package/src/stations/studio/IdentityProfileControls.tsx +262 -0
- package/src/stations/studio/StudioActiveProcesses.tsx +224 -0
- package/src/stations/studio/StudioAssistLogPanel.tsx +220 -0
- package/src/stations/studio/StudioGenerateMode.tsx +29 -0
- package/src/stations/studio/StudioGenerateTab.tsx +2688 -0
- package/src/stations/studio/StudioItemCard.tsx +334 -0
- package/src/stations/studio/StudioLibraryTab.tsx +506 -0
- package/src/stations/studio/StudioMovieComposer.tsx +2875 -0
- package/src/stations/studio/StudioMovieComposer.tsx.bak-sessions-20260812 +2729 -0
- package/src/stations/studio/StudioMovieSessions.tsx +553 -0
- package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +417 -0
- package/src/stations/studio/StudioPlane.tsx +302 -0
- package/src/stations/studio/StudioSplicedRow.tsx +476 -0
- package/src/stations/studio/StudioViewer.tsx +339 -0
- package/src/stations/studio/TurntableViewer.tsx +220 -0
- package/src/stations/studio/movieTimeline.ts +198 -0
- package/src/stations/studio/promptCard.tsx +498 -0
- package/src/stations/studio/studioItem.ts +87 -0
- package/src/stations/studio/studioShared.tsx +821 -0
- package/src/stations/studio/useIdentityProfiles.ts +575 -0
- package/src/stations/studio/useReconstruction.ts +283 -0
- package/src/stations/types.ts +43 -0
- package/src/stations/useAudioCropJobs.ts +202 -0
- package/src/stations/useAudioExtractJob.ts +144 -0
- package/src/stations/useCropJobs.ts +206 -0
- package/src/stations/useFrameJobs.ts +140 -0
- package/src/stations/useGenerateJob.ts +151 -0
- package/src/stations/useGenerateMovieJob.ts +177 -0
- package/src/stations/useGenerateRowJobs.ts +184 -0
- package/src/stations/useGenerateSceneJob.ts +156 -0
- package/src/stations/useGenerateSceneRowJobs.ts +282 -0
- package/src/style/app.css +4974 -0
- package/src/transport/client.ts +262 -0
- package/src/transport/telemetry.ts +97 -0
- package/src/video/DropReceptacle.tsx +122 -0
- package/src/video/PromptAssistButtons.tsx +118 -0
- package/src/video/composerBridge.ts +66 -0
- package/src/video/contract.ts +599 -0
- package/src/video/jobTracker.ts +702 -0
- package/src/video/mediaLibrary.ts +328 -0
- package/src/video/renderCompat.ts +512 -0
- package/src/video/sessionForm.ts +189 -0
- package/src/video/spreadAssist.ts +392 -0
- package/src/video/studioBridge.ts +82 -0
- package/src/video/useMediaJobs.ts +279 -0
- package/src/video/useModels.ts +413 -0
- package/src/video/useMoviePresets.ts +97 -0
- package/src/video/useMovieSessions.ts +292 -0
- package/src/video/usePresets.ts +167 -0
- package/src/video/useProjects.ts +87 -0
- package/src/video/usePromptAssist.ts +394 -0
- package/src/video/useRenderPresets.ts +221 -0
- package/src/video/useStudioAssistLog.ts +266 -0
- package/src/video/useStudioPresets.ts +128 -0
- package/src/video/useWorkerCalls.ts +165 -0
- package/tsconfig.json +17 -0
- 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'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
|
+
}
|