@hugpy/video-intelligence-ui 0.3.3 → 0.3.4

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 (38) hide show
  1. package/dist/assets/index-DMoT8G1q.css +1 -0
  2. package/dist/assets/index-GeD1mo16.js +4190 -0
  3. package/dist/index.html +2 -2
  4. package/entry.tsx +6 -27
  5. package/package.json +3 -2
  6. package/src/config.ts +11 -27
  7. package/src/demo/demoFetch.ts +0 -9
  8. package/src/nav/Navbar.tsx +3 -3
  9. package/src/session.ts +8 -43
  10. package/src/stations/GenerateStation.tsx +8 -5
  11. package/src/stations/SharePanel.tsx +4 -4
  12. package/src/stations/ToksPanel.tsx +1 -2
  13. package/src/stations/WorkbenchSidebar.tsx +8 -261
  14. package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
  15. package/src/stations/studio/IdentitiesStation.tsx +66 -75
  16. package/src/stations/studio/StudioGenerateMode.tsx +3 -2
  17. package/src/stations/studio/StudioGenerateTab.tsx +258 -188
  18. package/src/stations/studio/StudioMovieComposer.tsx +321 -46
  19. package/src/stations/studio/StudioPlane.tsx +3 -1
  20. package/src/stations/studio/StudioViewer.tsx +6 -11
  21. package/src/stations/studio/studioShared.tsx +14 -2
  22. package/src/stations/studio/useIdentityCreateJob.ts +26 -21
  23. package/src/stations/studio/useReconstruction.ts +11 -5
  24. package/src/video/PromptAssistButtons.tsx +5 -6
  25. package/src/video/jobStream.ts +119 -0
  26. package/src/video/jobTracker.ts +67 -76
  27. package/src/video/modelsFeed.ts +105 -0
  28. package/src/video/studioOracle.ts +48 -0
  29. package/src/video/useMediaJobs.ts +17 -3
  30. package/src/video/useModels.ts +12 -53
  31. package/src/video/useMovieSessions.ts +15 -6
  32. package/src/video/useOperatorHistory.ts +11 -101
  33. package/src/video/usePromptAssist.ts +6 -3
  34. package/src/video/useToks.ts +14 -6
  35. package/src/video/watchVideoJob.ts +43 -0
  36. package/dist/assets/index-DuT1AMLn.css +0 -1
  37. package/dist/assets/index-_13ZiBDP.js +0 -4191
  38. package/src/video/useWorkerCalls.ts +0 -165
package/dist/index.html CHANGED
@@ -49,8 +49,8 @@
49
49
  overflow-x: hidden;
50
50
  }
51
51
  </style>
52
- <script type="module" crossorigin src="/video/assets/index-_13ZiBDP.js"></script>
53
- <link rel="stylesheet" crossorigin href="/video/assets/index-DuT1AMLn.css">
52
+ <script type="module" crossorigin src="/video/assets/index-GeD1mo16.js"></script>
53
+ <link rel="stylesheet" crossorigin href="/video/assets/index-DMoT8G1q.css">
54
54
  </head>
55
55
  <body>
56
56
  <div id="root"></div>
package/entry.tsx CHANGED
@@ -6,13 +6,12 @@ import { createRoot } from "react-dom/client";
6
6
  import { BrowserRouter } from "react-router-dom";
7
7
  import { StationShell } from "./src/stations/StationShell";
8
8
  import { hugpyConfig } from "./src/config";
9
- import { startSessionMetering } from "./src/session";
10
9
  import { initShare } from "./src/share";
11
10
  import { installVideoDemo } from "./src/demo";
12
11
  // Sitewide floating Help button. ONE shared framework-free DOM module mounted
13
12
  // identically by all four surfaces (main SPA + media/video/fleet arms) — see
14
13
  // ../ui_shared/help/helpWidget.js for why it isn't a copied React component.
15
- import { mountHelpWidget } from "../ui_shared/help/helpWidget";
14
+ import { mountHelpWidget } from "@hugpy/ui-shared/help/helpWidget.js";
16
15
  import "./src/style/app.css";
17
16
 
18
17
  // k9: lift a `?share=<key>` share-link credential out of the URL (stash + strip)
@@ -24,31 +23,11 @@ initShare();
24
23
  // anything below can touch the network. No-op outside demo mode.
25
24
  installVideoDemo();
26
25
 
27
- // Per-session upload lifecycle: heartbeat the server so it keeps this session's
28
- // uploads, and beacon a wipe on tab close. (No-op in canned demo mode.)
29
- startSessionMetering();
30
-
31
- // Contract guard: this arm depends ENTIRELY on the current hugpy API, whose
32
- // /version reports an integer `api` contract version. Fire-and-forget on boot —
33
- // never block or delay rendering, and swallow any fetch/parse error.
34
- const EXPECTED_API_CONTRACT = 1;
35
- void (async () => {
36
- try {
37
- const res = await fetch(`${hugpyConfig.apiBase}/version`, {
38
- credentials: hugpyConfig.withCredentials ? "include" : "same-origin",
39
- });
40
- if (!res.ok) return;
41
- const body = (await res.json()) as { api?: unknown };
42
- const api = body?.api;
43
- if (api !== EXPECTED_API_CONTRACT) {
44
- console.warn(
45
- `hugpy API contract mismatch (got ${String(api)}, expected ${EXPECTED_API_CONTRACT})`,
46
- );
47
- }
48
- } catch {
49
- // swallow: never let the contract check disrupt boot.
50
- }
51
- })();
26
+ // Session ping (/session/ping) and the boot /version contract probe were dropped
27
+ // (2026-10-05): neither route has a backend in this build, so both only produced
28
+ // a repeating 404 and a one-shot 404. The page is action-driven now — it contacts
29
+ // the server on a real read (a seed) or a pushed event, never on a timer or a boot
30
+ // liveness poke.
52
31
 
53
32
  const el = document.getElementById("root");
54
33
  if (!el) throw new Error("video-intelligence: #root not found");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hugpy/video-intelligence-ui",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "description": "Hugpy video-intelligence console: image crop, audio crop, frames/models and generate stations over the hugpy-video job bus; a standalone Vite + Tailwind v4 arm served at /video",
@@ -39,7 +39,8 @@
39
39
  "react-dom": "^18.3.1",
40
40
  "react-router-dom": "^7.16.0",
41
41
  "three": "^0.185.1",
42
- "zod": "^3.23.8"
42
+ "zod": "^3.23.8",
43
+ "@hugpy/ui-shared": "^0.3.1"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@tailwindcss/vite": "^4.0.0",
package/src/config.ts CHANGED
@@ -149,6 +149,8 @@ export const hugpyConfig = {
149
149
  */
150
150
  studioI2VEnqueueUrl:
151
151
  read("VITE_HUGPY_STUDIO_I2V_URL") ?? `${apiBase}/video/studio/i2v`,
152
+ studioPlanUrl:
153
+ read("VITE_HUGPY_STUDIO_PLAN_URL") ?? `${apiBase}/video/studio/plan`,
152
154
  /**
153
155
  * Enqueue a STUDIO MOVIE (Studio Movie composer, slice B2/B3) — POST an ordered
154
156
  * goal timeline {resolution:{width,height,fps}, seed, vram_budget_gb, negative_prompt,
@@ -202,13 +204,19 @@ export const hugpyConfig = {
202
204
  * (?all=1 appends recent terminal rows; ?limit=N, default 50) → { jobs: [{ job_id,
203
205
  * name, status, created, updated, principal, progress:{…|phase:"awaiting_capacity",
204
206
  * reason, held_since, overtaken}|null, placement?:{source:"reservation"|"template",
205
- * host, worker_id, gpu, process, reserved_bytes} }] }. Unlike `llmJobsUrl` (the
206
- * unified cross-transport feed, terminal rows dropped after ~600s) this is the
207
- * media CATALOG projection and carries PLACEMENT — WHERE each in-flight render
207
+ * host, worker_id, gpu, process, reserved_bytes} }] }. This is the media
208
+ * CATALOG projection and carries PLACEMENT — WHERE each in-flight render
208
209
  * physically executes (ae · cuda:0 · P-studio). Declared here so the Active
209
210
  * Processes surface never derives the path inline.
210
211
  */
211
212
  mediaJobsUrl: read("VITE_HUGPY_MEDIA_JOBS_URL") ?? `${apiBase}/video/jobs`,
213
+ /**
214
+ * SSE: every media-bus job row the moment a worker reports a transition — the
215
+ * push that replaced the /video/jobs poll loops. The page seeds ONCE from
216
+ * mediaJobsUrl, then listens here; see video/jobStream.ts. 404-tolerant: if the
217
+ * stream is absent the page keeps the one-time seed and never falls back to a poll.
218
+ */
219
+ jobsStreamUrl: read("VITE_HUGPY_JOBS_STREAM_URL") ?? `${apiBase}/video/jobs/stream`,
212
220
  /**
213
221
  * LIVE tok/s telemetry for the generate workbench's Toks panel. Two endpoints,
214
222
  * both same-origin `/api`, polled together every 2s by `useToks`:
@@ -362,19 +370,6 @@ export const hugpyConfig = {
362
370
  modelsUrl: read("VITE_HUGPY_MODELS_URL") ?? `${apiBase}/v1/models`,
363
371
  /** Task defaults — GET → { defaults: { "text-to-image": <id>, ... }, tasks:[...] }. */
364
372
  promptTasksUrl: read("VITE_HUGPY_PROMPT_TASKS_URL") ?? `${apiBase}/prompt/tasks`,
365
- /**
366
- * The AUTHORITATIVE unified feed of EVERY in-flight worker call across ALL
367
- * transports (web / v1 / discord / cli / media) — GET → { jobs: [{ id, kind,
368
- * status, transport, model, model_name, worker, principal, elapsed, tokens,
369
- * error, ... }] }. Unlike the per-session jobTracker (which only knows jobs THIS
370
- * browser enqueued) this shows fleet-wide work — identity reconstructions
371
- * (kind:"identity_reconstruction", transport:"media"), studio renders, and any
372
- * call from another station/transport. The Active Processes panel polls it,
373
- * keeps the in-flight rows (pending/processing/streaming, minus `download`
374
- * provisioning), and merges them with the tracker rows. Declared here so the
375
- * panel never derives the path inline.
376
- */
377
- llmJobsUrl: read("VITE_HUGPY_LLM_JOBS_URL") ?? `${apiBase}/llm/jobs`,
378
373
  /**
379
374
  * k9 VIDEO-SHARE links — GET → { keys:[{id,label,prefix,created_at,expires_at,
380
375
  * expired,revoked,last_used}] } (OPERATOR-ONLY; a share/anon session gets 401,
@@ -464,17 +459,6 @@ export function llmChatCancelUrl(id: string): string {
464
459
  return `${apiBase}/llm/chat/cancel/${encodeURIComponent(id)}`;
465
460
  }
466
461
 
467
- /**
468
- * HARD-CANCEL a job by id on the unified feed: POST → {cancelled, status}. Unlike
469
- * the transport-specific routes above, this is the ONE cancel the fleet-wide
470
- * `/llm/jobs` Active-Processes rows hit regardless of which transport launched the
471
- * job — the backend fans it out to the right bus. Per-id like `jobCancelUrl` —
472
- * derived from apiBase, the one module permitted to hold URL literals.
473
- */
474
- export function llmJobCancelUrl(id: string): string {
475
- return `${apiBase}/llm/jobs/${encodeURIComponent(id)}/cancel`;
476
- }
477
-
478
462
  /**
479
463
  * Apply (pre-warm) a curated Generate-station preset by id: POST → on success
480
464
  * {ok:true, worker:{name,id}, model_key, mode, defaults{...}, warming}; on failure
@@ -199,15 +199,6 @@ async function route(
199
199
  if (path === "/v1/models") return jsonResponse(fx.V1_MODELS);
200
200
  if (path === "/prompt/tasks") return jsonResponse(fx.PROMPT_TASKS);
201
201
 
202
- // Unified in-flight worker feed (Active Processes poller, useWorkerCalls.ts,
203
- // ~2s cadence). The brochure runs nothing, so the honest envelope is an empty
204
- // in-flight list — { jobs: [] } is exactly the shape parseJobs() projects, and
205
- // an empty list keeps the panel truthful (no fabricated running render). Routed
206
- // (not gated) so the poll simply never reaches the wire in canned mode.
207
- if (path === "/llm/jobs" && method === "GET") {
208
- return jsonResponse({ jobs: [] });
209
- }
210
-
211
202
  // Bus-wide MEDIA JOBS listing (Active Processes media poller, useMediaJobs.ts,
212
203
  // ~2s cadence, GET bare /video/jobs — no id, so the job-poll regex below never
213
204
  // matches it). Same honest empty in-flight list as /llm/jobs. Must sit ABOVE
@@ -21,14 +21,14 @@
21
21
  */
22
22
  import React from "react";
23
23
  import type { ReactNode } from "react";
24
- import { makeUseArms } from "../../../ui_shared/arms";
24
+ import { makeUseArms } from "@hugpy/ui-shared/arms";
25
25
  import BrandMark from "./BrandMark";
26
26
  import { hugpyConfig } from "../config";
27
27
  // The link SET + order come from the ONE shared manifest so this arm can never
28
28
  // drift from the rest of the site. Rendering stays local (runtime siteBase,
29
29
  // current-marking). Plain .js, imported via allowJs with no .d.ts.
30
- import { buildNavItems } from "../../../ui_shared/navbar/links";
31
- import "../../../ui_shared/navbar/navbar.css";
30
+ import { buildNavItems } from "@hugpy/ui-shared/navbar/links.js";
31
+ import "@hugpy/ui-shared/navbar/navbar.css";
32
32
  import "./Navbar.css";
33
33
 
34
34
  // Origin-relative on a hugpy origin; the public hugpy site (siteUrl) otherwise.
package/src/session.ts CHANGED
@@ -7,11 +7,7 @@
7
7
  // wipe them immediately. A server-side 1h sweep is the safety net for missed
8
8
  // beacons. Disabled entirely in canned demo mode (no backend is contacted, and
9
9
  // sendBeacon bypasses the demo fetch shim).
10
- import { hugpyConfig } from "./config";
11
- import { isCanned } from "./demo/mode";
12
-
13
10
  const SID_KEY = "hugpy.video.sid";
14
- const HEARTBEAT_MS = 60_000;
15
11
 
16
12
  function makeSid(): string {
17
13
  try {
@@ -42,44 +38,13 @@ export function getSessionId(): string {
42
38
  }
43
39
  }
44
40
 
45
- let started = false;
46
-
47
- /** Start heartbeat + tab-close beacon so the server can wipe uploads per session. */
41
+ /**
42
+ * Session metering was removed (2026-10-05). It heartbeat /session/ping every 60s
43
+ * and beaconed /session/end on close, but neither route has a backend in this
44
+ * build (the ping 404'd on a timer). The per-tab session id below still tags
45
+ * uploads; the server's own sweep is the upload-lifecycle backstop. Kept as an
46
+ * exported no-op so any caller/import stays valid without reintroducing a poll.
47
+ */
48
48
  export function startSessionMetering(): void {
49
- if (started || typeof window === "undefined") return;
50
- if (isCanned()) return;
51
- started = true;
52
-
53
- const sid = getSessionId();
54
- const base = hugpyConfig.apiBase.replace(/\/$/, "");
55
- const q = `sid=${encodeURIComponent(sid)}`;
56
-
57
- // Heartbeat — a "simple" POST (query param, no custom header / JSON) so it never
58
- // triggers a CORS preflight in cross-origin deploys. Failures are ignored.
59
- const ping = () => {
60
- try {
61
- void fetch(`${base}/session/ping?${q}`, {
62
- method: "POST",
63
- credentials: hugpyConfig.withCredentials ? "include" : "same-origin",
64
- keepalive: true,
65
- }).catch(() => {});
66
- } catch {
67
- /* ignore */
68
- }
69
- };
70
-
71
- ping(); // announce on load
72
- window.setInterval(ping, HEARTBEAT_MS);
73
-
74
- // Tab close → wipe now (best-effort). Skip bfcache navigations (persisted),
75
- // which may be restored; the heartbeat resumes if so, and the 1h sweep is the
76
- // backstop either way.
77
- window.addEventListener("pagehide", (e) => {
78
- if ((e as PageTransitionEvent).persisted) return;
79
- try {
80
- navigator.sendBeacon?.(`${base}/session/end?${q}`);
81
- } catch {
82
- /* ignore */
83
- }
84
- });
49
+ /* intentionally empty — no session ping, no tab-close beacon (no backend) */
85
50
  }
@@ -1456,10 +1456,10 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
1456
1456
  // This part's OWN assist model (falls back to the shared pick / fleet default).
1457
1457
  const model = p.assistModel ?? assist.assistModel ?? null;
1458
1458
  const context = opts.media ? { media: opts.media } : undefined;
1459
- let newText = p.text;
1459
+ let newText = "";
1460
1460
  await assist.runAssist(
1461
1461
  mode,
1462
- p.text,
1462
+ mode === "generate" ? "" : p.text,
1463
1463
  (t) => {
1464
1464
  newText = t;
1465
1465
  editText(p.key, t);
@@ -3793,7 +3793,7 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
3793
3793
  // apply (replace the primary text part). On ANY failure the hook surfaces .error
3794
3794
  // inline and never calls apply, so the existing prompt stays intact.
3795
3795
  function runAssist(assistMode: "detail" | "generate"): void {
3796
- void assist.runAssist(assistMode, assembledPrompt, applyAssistedPrompt);
3796
+ void assist.runAssist(assistMode, assistMode === "generate" ? "" : assembledPrompt, applyAssistedPrompt);
3797
3797
  }
3798
3798
 
3799
3799
  useEffect(() => {
@@ -4010,7 +4010,10 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
4010
4010
  SectionTabs + Options-rail + Run-row layout. The surface is LOCKED to the
4011
4011
  matching sub-mode: Clip → "clip", Cinema → "movie" (the StudioMovieComposer). */}
4012
4012
  {isStudioSurface && (
4013
- <StudioGenerateMode lockedSurfaceMode={mode === "cinema" ? "movie" : "clip"} />
4013
+ <StudioGenerateMode
4014
+ lockedSurfaceMode={mode === "cinema" ? "movie" : "clip"}
4015
+ onSelectSurfaceMode={(next) => setMode(next === "movie" ? "cinema" : "clip")}
4016
+ />
4014
4017
  )}
4015
4018
 
4016
4019
  {!isStudioSurface && (
@@ -5270,7 +5273,7 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
5270
5273
  onGenerate={() =>
5271
5274
  void (async () => {
5272
5275
  let fresh = "";
5273
- await assist.runAssist("generate", g.prompt, (p) => {
5276
+ await assist.runAssist("generate", "", (p) => {
5274
5277
  fresh = p;
5275
5278
  editGoal(g.id, "prompt", p);
5276
5279
  });
@@ -80,10 +80,10 @@ export function SharePanel() {
80
80
  }
81
81
  }, []);
82
82
 
83
- // Probe once on mount to decide visibility (and seed the existing-links list).
84
- useEffect(() => {
85
- void refresh();
86
- }, [refresh]);
83
+ // The mount probe of GET /keys/video-share was dropped (2026-10-05): that route
84
+ // has no backend in this build, so the probe only 404'd. `authed` therefore
85
+ // stays null and the Share affordance renders nothing — the same outcome the
86
+ // 401/403 branch produced, with no request. refresh() still backs mint/revoke.
87
87
 
88
88
  const mint = useCallback(async () => {
89
89
  setMinting(true);
@@ -18,7 +18,6 @@ import {
18
18
  useToks,
19
19
  rollupByWorker,
20
20
  tokAgo,
21
- TOKS_POLL_MS,
22
21
  type TokEntry,
23
22
  type TokGroup,
24
23
  } from "../video/useToks";
@@ -153,7 +152,7 @@ export function ToksStation() {
153
152
  className={`vi-assistlog-dot ${live ? "vi-assistlog-dot--live" : "vi-assistlog-dot--disconnected"}`}
154
153
  aria-hidden
155
154
  />
156
- {live ? "live" : "paused"} · every {Math.round(TOKS_POLL_MS / 1000)}s
155
+ {live ? "live" : "paused"} · on activity
157
156
  </span>
158
157
  </div>
159
158
  <p className="vi-knob-hint">