@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,43 @@
1
+ // Public surface of the video-intelligence demo layer (canned-only — no live
2
+ // mode; see mode.ts). entry.tsx calls installVideoDemo() ONCE, before anything
3
+ // that could touch the network.
4
+ import { isCanned } from "./mode";
5
+ import { installDemoTransport } from "./demoFetch";
6
+ import { demoClip, demoAudio } from "./fixtures";
7
+ import { seedDemoWorkbench } from "./seed";
8
+ import { addToLibrary, getLibrary } from "../video/mediaLibrary";
9
+
10
+ /**
11
+ * Boot the canned demo: install the fetch shim, then seed the workbench so every
12
+ * station opens with SHOWROOM FLAVOR instead of empty — the worked example (clip +
13
+ * audio) PLUS sample generations (a completed image gen, a couple of extracted
14
+ * frames, and one live-feeling in-flight generation; see seed.ts). No-op outside
15
+ * ?demo=1. Idempotent across in-tab reloads (the library + tracker are
16
+ * sessionStorage-backed under their demo-namespaced keys — addToLibrary/trackJob
17
+ * de-dup, so don't double-seed).
18
+ */
19
+ export function installVideoDemo(): void {
20
+ if (!isCanned()) return;
21
+ installDemoTransport();
22
+ try {
23
+ if (getLibrary().length === 0) {
24
+ addToLibrary(demoClip, "demo", "sample clip");
25
+ addToLibrary(demoAudio, "demo", "sample audio");
26
+ }
27
+ } catch {
28
+ /* library seeding is best-effort — the shim alone still demos */
29
+ }
30
+ // Sample gens + one live-feeling in-flight job (its own idempotent guards).
31
+ seedDemoWorkbench();
32
+ }
33
+
34
+ export {
35
+ getDemoConfig,
36
+ isDemo,
37
+ isCanned,
38
+ isEmbedded,
39
+ type DemoMode,
40
+ type DemoConfig,
41
+ } from "./mode";
42
+ export { installDemoTransport, uninstallDemoTransport } from "./demoFetch";
43
+ export { default as DemoBanner } from "./DemoBanner";
@@ -0,0 +1,81 @@
1
+ // Demo-mode configuration for the video-intelligence arm.
2
+ //
3
+ // Mirrors media_intelligence_ui/src/demo/mode.ts with ONE deliberate cut: there
4
+ // is NO live mode here. The video arm's jobs mutate real worker GPUs (ffmpeg,
5
+ // diffusion), so the only demo flavor is the self-contained showroom:
6
+ //
7
+ // ?demo=1 → CANNED: a window.fetch shim answers every hugpy call from canned
8
+ // fixtures, and the media library is seeded with a worked example.
9
+ // Works anywhere — the public brochure, a static host. No backend
10
+ // is ever contacted.
11
+ //
12
+ // &embed=1 → embedded inside the dev UI showroom (an <iframe>). The host
13
+ // provides the demo banner, so the arm suppresses its own.
14
+ //
15
+ // `?demo=live` is intentionally treated as OFF — a shared demo link must never
16
+ // drive real video jobs.
17
+ //
18
+ // Host default: on the dedicated demo host (demo.hugpy.ai) a BARE hit — no
19
+ // `demo` param — defaults to CANNED, so any build served there is demo-first
20
+ // without needing host-side redirect rules. An explicit `?demo=...` still wins
21
+ // (and the live-is-off rule above still applies).
22
+ //
23
+ // Resolved once at module load; query params persist across in-SPA navigation,
24
+ // so the mode sticks for the session.
25
+
26
+ export type DemoMode = "off" | "canned";
27
+
28
+ export interface DemoConfig {
29
+ mode: DemoMode;
30
+ embed: boolean;
31
+ }
32
+
33
+ function truthy(v: string | null): boolean {
34
+ return v != null && /^(1|true|yes|on)$/i.test(v.trim());
35
+ }
36
+
37
+ // The dedicated demo host — a bare hit here defaults to CANNED (see below).
38
+ // Narrow on purpose: only the exact `demo.hugpy.ai` label, not any *.hugpy.ai.
39
+ function isDemoHost(): boolean {
40
+ try {
41
+ return (
42
+ typeof window !== "undefined" &&
43
+ /^demo\.hugpy\.ai$/i.test(window.location.hostname)
44
+ );
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ function parse(): DemoConfig {
51
+ let params: URLSearchParams;
52
+ try {
53
+ params = new URLSearchParams(
54
+ typeof window !== "undefined" ? window.location.search : "",
55
+ );
56
+ } catch {
57
+ params = new URLSearchParams();
58
+ }
59
+
60
+ const raw = (params.get("demo") || "").trim().toLowerCase();
61
+ // NOTE: "live" is NOT accepted — canned or nothing (see module comment).
62
+ let mode: DemoMode = /^(1|true|yes|on|canned|demo)$/.test(raw)
63
+ ? "canned"
64
+ : "off";
65
+ // Host default: bare hit on demo.hugpy.ai (no explicit `demo` param) → canned.
66
+ // An explicit param always wins — including "live", which stays off here.
67
+ if (mode === "off" && !params.has("demo") && isDemoHost()) mode = "canned";
68
+
69
+ return { mode, embed: truthy(params.get("embed")) };
70
+ }
71
+
72
+ let _config: DemoConfig | null = null;
73
+
74
+ export function getDemoConfig(): DemoConfig {
75
+ if (!_config) _config = parse();
76
+ return _config;
77
+ }
78
+
79
+ export const isDemo = (): boolean => getDemoConfig().mode !== "off";
80
+ export const isCanned = (): boolean => getDemoConfig().mode === "canned";
81
+ export const isEmbedded = (): boolean => getDemoConfig().embed;
@@ -0,0 +1,181 @@
1
+ // Canned PROMPT-ASSIST generator for the showroom (?demo=1). The demoFetch shim
2
+ // answers POST /video/prompt/assist entirely CLIENT-SIDE from these fragment banks
3
+ // — ZERO backend contact — so the Enhance / Generate buttons are genuinely live in
4
+ // the brochure without an LLM.
5
+ //
6
+ // Two behaviours, mirroring the real route's { mode } contract:
7
+ // • "generate" — composes a WHOLE cinematic prompt from the fragment banks below.
8
+ // Combinations are numerous (subject × setting × light × camera × style × grade),
9
+ // so successive clicks (almost) never repeat; nextGeneratedPrompt() additionally
10
+ // GUARANTEES the next prompt differs from the last one it handed out.
11
+ // • "detail" (Enhance) — takes the submitted draft and APPENDS a few randomized
12
+ // cinematic descriptors, so the box visibly grows with richer detail while keeping
13
+ // whatever the visitor already wrote.
14
+ //
15
+ // Flavor is deliberately kept in the same cinematic world as the seeded amber-dune
16
+ // sample (src/demo/seed.ts DEMO_GENERATE_PROMPT): warm golden-hour / dusk light,
17
+ // volumetric atmosphere, film-grain grades — so a Generate result reads as a sibling
18
+ // of the prefilled prompt, not a jarring genre jump. Math.random is fine here (demo).
19
+
20
+ // ── fragment banks ──────────────────────────────────────────────────────────
21
+ // Each bank is a coherent slot in "SUBJECT, SETTING, LIGHT, CAMERA, STYLE, GRADE".
22
+ // Every combination reads as a plausible cinematic still/shot.
23
+ const SUBJECTS = [
24
+ "a lone figure in a flowing cloak",
25
+ "a weathered lighthouse keeper",
26
+ "a wanderer with a lantern",
27
+ "a solitary rider on horseback",
28
+ "an astronaut adrift from the hatch",
29
+ "a child chasing paper kites",
30
+ "a violinist mid-performance",
31
+ "a fox threading through tall grass",
32
+ "twin travelers on a ridge line",
33
+ "a diver breaking the surface",
34
+ "a monk crossing a stone bridge",
35
+ "a dancer caught mid-turn",
36
+ ];
37
+
38
+ const SETTINGS = [
39
+ "on a windswept dune",
40
+ "at the edge of a mirror-still lake",
41
+ "among towering redwoods wreathed in mist",
42
+ "on a rain-slick city street",
43
+ "beneath a cathedral of ice",
44
+ "across a field of swaying wildflowers",
45
+ "on a cliff above a churning sea",
46
+ "inside an abandoned glasshouse",
47
+ "along a deserted railway platform",
48
+ "under a canopy of drifting cherry blossom",
49
+ "in a canyon of red sandstone",
50
+ "on a fog-bound harbor pier",
51
+ ];
52
+
53
+ const LIGHTS = [
54
+ "golden hour, amber sun low on the horizon",
55
+ "soft dusk light deepening from orange to indigo",
56
+ "cold blue pre-dawn glow",
57
+ "warm lantern light against deep shadow",
58
+ "shafts of volumetric sunlight through haze",
59
+ "overcast silver light, gentle and diffuse",
60
+ "neon reflections shimmering on wet ground",
61
+ "moonlight silvering every edge",
62
+ "backlit rim light haloing the subject",
63
+ "candle-warm firelight flickering nearby",
64
+ ];
65
+
66
+ const CAMERAS = [
67
+ "cinematic wide shot",
68
+ "intimate close-up, shallow depth of field",
69
+ "slow dolly push-in",
70
+ "sweeping aerial vantage",
71
+ "low-angle hero framing",
72
+ "over-the-shoulder tracking shot",
73
+ "static locked-off medium shot",
74
+ "drifting handheld follow",
75
+ "dramatic Dutch-tilt angle",
76
+ ];
77
+
78
+ const STYLES = [
79
+ "35mm film grain",
80
+ "anamorphic lens flare",
81
+ "hyper-detailed photoreal render",
82
+ "painterly matte-painting finish",
83
+ "muted teal-and-orange color grade",
84
+ "high dynamic range, crisp micro-contrast",
85
+ "dreamy soft-focus bloom",
86
+ "rich chiaroscuro lighting",
87
+ ];
88
+
89
+ const GRADES = [
90
+ "long violet shadows",
91
+ "warm highlights, cool shadows",
92
+ "deep cinematic contrast",
93
+ "atmospheric depth haze",
94
+ "delicate lens bokeh in the background",
95
+ "subtle vignette drawing the eye inward",
96
+ ];
97
+
98
+ // Extra descriptor fragments Enhance appends to an existing draft (kept distinct from
99
+ // the whole-prompt banks so the enrichment reads as ADDED cinematic polish).
100
+ const ENHANCERS = [
101
+ "volumetric light",
102
+ "ultra-detailed textures",
103
+ "dramatic rim lighting",
104
+ "shallow depth of field",
105
+ "atmospheric haze",
106
+ "rich color grading",
107
+ "cinematic composition",
108
+ "soft golden-hour glow",
109
+ "fine film grain",
110
+ "gentle lens bloom",
111
+ "sweeping sense of scale",
112
+ "crisp foreground detail",
113
+ ];
114
+
115
+ // ── random helpers ────────────────────────────────────────────────────────────
116
+ function pick<T>(bank: readonly T[]): T {
117
+ return bank[Math.floor(Math.random() * bank.length)];
118
+ }
119
+
120
+ // Pick `n` DISTINCT items from a bank (n clamped to the bank size), order preserved
121
+ // by draw. Used by Enhance to append a small handful of non-repeating descriptors.
122
+ function pickSome<T>(bank: readonly T[], n: number): T[] {
123
+ const pool = bank.slice();
124
+ const out: T[] = [];
125
+ const count = Math.min(n, pool.length);
126
+ for (let i = 0; i < count; i++) {
127
+ const idx = Math.floor(Math.random() * pool.length);
128
+ out.push(pool.splice(idx, 1)[0]);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ // ── generate: a whole cinematic prompt from the banks ──────────────────────────
134
+ function composePrompt(): string {
135
+ const subject = pick(SUBJECTS);
136
+ const setting = pick(SETTINGS);
137
+ const light = pick(LIGHTS);
138
+ const camera = pick(CAMERAS);
139
+ const style = pick(STYLES);
140
+ const grade = pick(GRADES);
141
+ // Sentence-cased subject, comma-joined clauses — reads like the seeded sample.
142
+ const head = subject.charAt(0).toUpperCase() + subject.slice(1);
143
+ return `${head} ${setting}, ${light}, ${camera}, ${grade}, ${style}`;
144
+ }
145
+
146
+ // The last prompt handed out by nextGeneratedPrompt — so we never return the SAME
147
+ // prompt twice in a row (the operator's "genuinely new each click" requirement).
148
+ let _lastGenerated: string | null = null;
149
+
150
+ /**
151
+ * A freshly composed cinematic prompt, GUARANTEED to differ from the previous one.
152
+ * With thousands of combinations a repeat is already unlikely; this makes "never the
153
+ * same one twice in a row" a hard guarantee (bounded retry, then a forced tail
154
+ * descriptor so it always terminates even in the degenerate single-bank case).
155
+ */
156
+ export function nextGeneratedPrompt(): string {
157
+ let candidate = composePrompt();
158
+ for (let tries = 0; tries < 8 && candidate === _lastGenerated; tries++) {
159
+ candidate = composePrompt();
160
+ }
161
+ if (candidate === _lastGenerated) {
162
+ // Extremely unlikely; force a difference so we never violate the guarantee.
163
+ candidate = `${candidate}, ${pick(ENHANCERS)}`;
164
+ }
165
+ _lastGenerated = candidate;
166
+ return candidate;
167
+ }
168
+
169
+ /**
170
+ * Enhance ("detail"): take the submitted draft and return it enriched with a few
171
+ * appended cinematic descriptors (randomized, distinct from each other). A blank
172
+ * draft falls back to a freshly generated prompt (mirrors the real route, which
173
+ * treats Enhance-with-no-draft leniently rather than erroring). The visible effect
174
+ * is the box GROWING with detail while keeping whatever the visitor already wrote.
175
+ */
176
+ export function enhancePrompt(draft: string): string {
177
+ const base = draft.trim();
178
+ if (base === "") return nextGeneratedPrompt();
179
+ const added = pickSome(ENHANCERS, 2 + Math.floor(Math.random() * 2)); // 2–3 descriptors
180
+ return `${base}, ${added.join(", ")}`;
181
+ }
@@ -0,0 +1,97 @@
1
+ // Showroom SEED values for the canned demo (?demo=1) — the "sample gens +
2
+ // prefilled prompts" flavor the brochure opens with. Everything here is
3
+ // demo-only: it is imported ONLY behind an isCanned() gate (installVideoDemo for
4
+ // the workbench seeding, and the station composers for the prefilled prompt), so
5
+ // none of it can leak into a live session's normal path.
6
+ //
7
+ // Coherence: the prompts describe scenes that plausibly match the bundled
8
+ // gen-1/gen-2 stills (see fixtures.demoGenerated) — gen-1 is a warm amber/indigo
9
+ // sunset gradient, gen-2 a green/teal one — so the prefilled prompt, the completed
10
+ // sample generation, and the still the visitor sees all tell ONE story.
11
+ import {
12
+ demoGenerated,
13
+ demoFrames,
14
+ enqueueJob,
15
+ type JobKind,
16
+ } from "./fixtures";
17
+ import { addToLibrary } from "../video/mediaLibrary";
18
+ import { trackJob } from "../video/jobTracker";
19
+
20
+ /**
21
+ * The prompt the Generate (Image/Scene) composer opens PRE-FILLED with in canned
22
+ * mode — a cinematic scene that matches gen-1 (the primary sample still). The
23
+ * station reads this only when isCanned(); its live path stays an empty prompt.
24
+ */
25
+ export const DEMO_GENERATE_PROMPT =
26
+ "A lone figure on a windswept dune at golden hour, amber sun low on the horizon, " +
27
+ "long violet shadows, cinematic wide shot, volumetric light, 35mm film grain";
28
+
29
+ /**
30
+ * The prompt the Studio composer opens PRE-FILLED with in canned mode — a motion-
31
+ * flavored variant of the same world (a clip prompt reads better with camera/motion
32
+ * phrasing). Matches the sunset palette so the whole demo stays coherent.
33
+ */
34
+ export const DEMO_STUDIO_PROMPT =
35
+ "Slow dolly across golden dunes at sunset, drifting sand catching amber light, " +
36
+ "the sky deepening from orange to indigo, cinematic, smooth camera move";
37
+
38
+ /** The model the sample generation is attributed to (matches the fixtures registry). */
39
+ const DEMO_MODEL = "sd-turbo";
40
+
41
+ // A stable groupId for the seeded completed generation so its Session Library work
42
+ // section is a real gen run (prompt + model header, Continue/Replicate affordances)
43
+ // rather than a loose "demo asset" tile. Namespaced so it can never collide with a
44
+ // live job id.
45
+ const SEED_IMAGE_GROUP = "demo-seed-generate-image";
46
+
47
+ /**
48
+ * Seed the workbench so the showroom opens with SAMPLE WORK already present:
49
+ *
50
+ * • one COMPLETED "generate image" run — gen-1 pushed into the Session Library
51
+ * with full generation provenance (prompt + model + group), so it renders as a
52
+ * proper gen work section, not a bare demo tile; and
53
+ * • one COMPLETED frame-extract of the sample clip — a couple of stills tied to
54
+ * the bundled clip, so the library reads as a session that has done real work;
55
+ * • one slowly-progressing in-flight "generate image" job, enqueued through the
56
+ * SAME canned staged-poll machinery (enqueueJob → the shim's staged jobStatus →
57
+ * the real jobTracker polls it to done), so the visitor sees live-feeling
58
+ * progress in Active Processes that is entirely fake and never touches a backend.
59
+ *
60
+ * Best-effort + idempotent: addToLibrary de-dups by uri and trackJob de-dups by
61
+ * jobId, so a double-call (in-tab reload) can't double-seed. No-op on any throw.
62
+ */
63
+ export function seedDemoWorkbench(): void {
64
+ try {
65
+ // ── the completed sample generation (Session Library) ────────────────────
66
+ addToLibrary(demoGenerated[0], "generate", "generated", {
67
+ prompt: DEMO_GENERATE_PROMPT,
68
+ model: DEMO_MODEL,
69
+ groupId: SEED_IMAGE_GROUP,
70
+ genKind: "generate_image",
71
+ });
72
+
73
+ // ── a completed frame-extract of the sample clip (a couple of stills) ─────
74
+ addToLibrary(demoFrames[0], "frames", "frame 1");
75
+ addToLibrary(demoFrames[1], "frames", "frame 2");
76
+
77
+ // ── one live-feeling in-flight generation (Active Processes) ─────────────
78
+ // Enqueue a canned generate_image job and register it with the real tracker.
79
+ // The tracker's module poll loop then drives it through the shim's staged
80
+ // jobStatus (unclaimed → running-with-progress → done), and on `done` its
81
+ // output lands in the Session Library via the tracker's normal pushOutputs —
82
+ // exactly the path a real generation takes, with zero backend contact.
83
+ const kind: JobKind = "generate_image";
84
+ const jobId = enqueueJob(kind, null);
85
+ trackJob({
86
+ jobId,
87
+ kind: "generate_image",
88
+ station: "generate",
89
+ label: DEMO_GENERATE_PROMPT,
90
+ enqueuedAt: Date.now(),
91
+ prompt: DEMO_GENERATE_PROMPT,
92
+ model: DEMO_MODEL,
93
+ });
94
+ } catch {
95
+ /* seeding is best-effort — the shim + prefilled prompts still demo without it */
96
+ }
97
+ }
@@ -0,0 +1,66 @@
1
+ /* BrandMark — replicated 1:1 from the sitewide BrandMark.css so the brand mark
2
+ reads identically in the video arm's nav: hexagon symbol + the two-row
3
+ "H U G P Y" / rule / "inference you own" lockup. Colors resolve against the
4
+ dark token context carried on `.hugpy-navbar` (an ancestor), with literal
5
+ fallbacks here so the mark is legible even standalone. */
6
+ .brandmark {
7
+ color: var(--text-primary, #f4f4f5);
8
+ font-family: "Fira Code", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
9
+ font-weight: 700;
10
+ font-size: 14px;
11
+ white-space: nowrap;
12
+ display: inline-flex;
13
+ align-items: center;
14
+ gap: 9px;
15
+ text-decoration: none;
16
+ cursor: pointer;
17
+ }
18
+ .brandmark:hover { color: var(--link, #a7c7ff); }
19
+ .brandmark-img {
20
+ /* scaled up to stand in proportion to the taller two-row text lockup */
21
+ height: 34px;
22
+ width: auto;
23
+ align-self: center;
24
+ /* the mark sits on solid black in the source PNG; blend onto our bg + brighten
25
+ so the thin hexagon carries weight rather than reading as a faint outline. */
26
+ mix-blend-mode: screen;
27
+ filter: brightness(1.6) saturate(1.2);
28
+ }
29
+
30
+ /* wordmark + tagline stack vertically, split by a hairline rule, all sharing one
31
+ width (the wordmark is tracked out below to match the tagline). */
32
+ .brandmark-text {
33
+ display: inline-flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ text-align: center;
37
+ gap: 4px;
38
+ }
39
+ .brandmark-word {
40
+ font-size: 16px;
41
+ line-height: 1;
42
+ letter-spacing: 0.92em;
43
+ margin-right: -0.92em;
44
+ }
45
+ .brandmark-rule {
46
+ height: 1px;
47
+ width: 100%;
48
+ background: currentColor;
49
+ opacity: 0.35;
50
+ }
51
+ .brandmark-sub {
52
+ color: var(--text-tertiary, #8e8e93);
53
+ font-weight: 500;
54
+ font-size: 11px;
55
+ letter-spacing: 0.08em;
56
+ line-height: 1;
57
+ }
58
+
59
+ /* On phones drop the tagline + rule, collapsing to the compact single-line mark
60
+ (symbol + wordmark) at the original symbol size — matches the sitewide mark. */
61
+ @media (max-width: 640px) {
62
+ .brandmark-rule,
63
+ .brandmark-sub { display: none; }
64
+ .brandmark-img { height: 22px; }
65
+ .brandmark-word { letter-spacing: 0.12em; margin-right: 0; }
66
+ }
@@ -0,0 +1,40 @@
1
+ /*
2
+ * BrandMark — the canonical hugpy brand mark for the video-intelligence arm.
3
+ *
4
+ * Replicates the sitewide BrandMark 1:1 — the hexagon symbol + the two-row
5
+ * "H U G P Y" / hairline rule / "inference you own" lockup (same class names +
6
+ * a copied BrandMark.css) — so the brand reads identically across every page.
7
+ * The arm is a separate SPA served under /video/, so this is a plain <a> (NOT a
8
+ * react-router Link): clicking it leaves the arm and returns to the site root.
9
+ *
10
+ * Destination follows the SAME runtime logic as the Navbar's site links: on a
11
+ * hugpy origin the home page is at the CURRENT origin's root; on any other
12
+ * origin it falls back to the public platform (hugpyConfig.siteUrl).
13
+ *
14
+ * Ported from media_intelligence_ui/src/chat/src/ui/BrandMark.tsx, dropping the
15
+ * demo/mode (`isEmbedded`) import — this arm has no iframe showroom.
16
+ */
17
+ import markUrl from "../assets/hugpy-mark.png";
18
+ import { hugpyConfig } from "../config";
19
+ import "./BrandMark.css";
20
+
21
+ // Runtime hostname, not build-time base: on a hugpy origin return to that
22
+ // origin's root; otherwise the public platform (see Navbar.tsx for the full
23
+ // rationale).
24
+ const onHugpyOrigin =
25
+ typeof window !== "undefined" &&
26
+ /(^|\.)hugpy\.ai$/i.test(window.location.hostname);
27
+ const welcomeHref = onHugpyOrigin ? "/" : hugpyConfig.siteUrl;
28
+
29
+ export default function BrandMark() {
30
+ return (
31
+ <a href={welcomeHref} title="Back to the welcome page" className="brandmark">
32
+ <img className="brandmark-img" src={markUrl} alt="" />
33
+ <span className="brandmark-text">
34
+ <span className="brandmark-word">HUGPY</span>
35
+ <span className="brandmark-rule" aria-hidden="true" />
36
+ <span className="brandmark-sub">inference you own</span>
37
+ </span>
38
+ </a>
39
+ );
40
+ }
@@ -0,0 +1,13 @@
1
+ /* Video-intelligence arm — nav LOOK now comes from the sitewide shared stylesheet
2
+ (ui_shared/navbar/navbar.css, imported by Navbar.tsx). Only this arm-specific
3
+ header-stack wrapper stays local.
4
+
5
+ Nav + demo banner ride together in this header stack. .vi-shell is a 100dvh
6
+ flex column that itself never scrolls (content scrolls inside the workbench
7
+ body), so the stack just needs to be the non-growing first flex item — the
8
+ whole header stays pinned at the top and the banner sits directly under the
9
+ nav (operator directive 2026-07-21). */
10
+ .vi-header-stack {
11
+ flex-shrink: 0;
12
+ z-index: 30;
13
+ }
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Navbar — the shared hugpy top nav, ported into the video-intelligence arm.
3
+ *
4
+ * A faithful copy of the media arm's shared Navbar (COPIED, never imported
5
+ * cross-arm): a three-zone bar — brand pinned left, the primary links
6
+ * (Docs / Console / Media / Video) optically centered, and a page-specific
7
+ * controls slot pinned right — so this arm reads as part of the same product.
8
+ * Sticky, blurred, and bottom-bordered, matching the sitewide nav.
9
+ *
10
+ * Cross-SPA links: this arm is a SEPARATE SPA mounted under /video (its
11
+ * react-router basename is "/video"). The other destinations (/docs, /console,
12
+ * /media, the site root) are routes of OTHER hugpy SPAs, NOT of this one — so
13
+ * they are plain full-navigation <a> links, never react-router <Link>s (a Link
14
+ * here would resolve under /video and dead-end). On a hugpy origin the site
15
+ * links are origin-relative (dev.hugpy.ai/video → dev.hugpy.ai/docs); on any
16
+ * OTHER origin they point at hugpyConfig.siteUrl (hugpy.ai) instead.
17
+ *
18
+ * Ported from media_intelligence_ui/src/chat/src/ui/Navbar.tsx, dropping the
19
+ * demo/mode (`isEmbedded`) import — this arm has no iframe showroom, so the
20
+ * `target="_top"` escape hatch is unnecessary and every link is a normal nav.
21
+ */
22
+ import type { ReactNode } from "react";
23
+ import BrandMark from "./BrandMark";
24
+ import { hugpyConfig } from "../config";
25
+ // The link SET + order come from the ONE shared manifest so this arm can never
26
+ // drift from the rest of the site. Rendering stays local (runtime siteBase,
27
+ // current-marking). Plain .js, imported via allowJs with no .d.ts.
28
+ import { buildNavItems } from "../../../ui_shared/navbar/links";
29
+ import "../../../ui_shared/navbar/navbar.css";
30
+ import "./Navbar.css";
31
+
32
+ // Origin-relative on a hugpy origin; the public hugpy site (siteUrl) otherwise.
33
+ // Strip a trailing slash so `${base}/x` never doubles up. Decided at RUNTIME
34
+ // from the live hostname (the same build can be served at more than one origin).
35
+ // Exported so any other in-arm component that needs a same-origin link to a
36
+ // sibling hugpy SPA (e.g. ConsolePanel's iframe target) can reuse the exact
37
+ // same rule instead of re-deriving it — this is the one place it's computed.
38
+ export const onHugpyOrigin =
39
+ typeof window !== "undefined" &&
40
+ /(^|\.)hugpy\.ai$/i.test(window.location.hostname);
41
+ export const siteBase = onHugpyOrigin ? "" : hugpyConfig.siteUrl.replace(/\/$/, "");
42
+
43
+ export default function Navbar({ children }: { children?: ReactNode }) {
44
+ return (
45
+ <nav className="hugpy-navbar">
46
+ <span className="hugpy-navbar-brand">
47
+ <BrandMark />
48
+ </span>
49
+ <span className="hugpy-navbar-links">
50
+ {/* Link SET + order come from the ONE shared manifest so they stay
51
+ identical sitewide. Rendering is local: every destination is a plain
52
+ full-navigation <a> to a sibling hugpy SPA, prefixed with the runtime
53
+ siteBase. Video is the current surface. */}
54
+ {buildNavItems({ currentKey: "video", siteBase }).map((item) => (
55
+ <a
56
+ key={item.key}
57
+ href={item.href}
58
+ aria-current={item.current ? "page" : undefined}
59
+ className={item.current ? "is-current" : undefined}
60
+ >
61
+ {item.label}
62
+ </a>
63
+ ))}
64
+ </span>
65
+ <span className="hugpy-navbar-side">{children}</span>
66
+ </nav>
67
+ );
68
+ }