@hugpy/video-intelligence-ui 0.3.2 → 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.
- package/LICENSE +0 -0
- package/README.md +9 -0
- package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
- package/dist/assets/index-DMoT8G1q.css +1 -0
- package/dist/assets/index-GeD1mo16.js +4190 -0
- package/dist/index.html +71 -0
- package/entry.tsx +6 -27
- package/index.html +4 -4
- package/package.json +44 -5
- package/src/assets/hugpy-mark.png +0 -0
- package/src/config.ts +11 -27
- package/src/demo/DemoBanner.tsx +0 -0
- package/src/demo/demo.css +0 -0
- package/src/demo/demoFetch.ts +0 -9
- package/src/demo/fixtures.ts +0 -0
- package/src/demo/identityFixture.ts +0 -0
- package/src/demo/index.ts +0 -0
- package/src/demo/mediaBase.ts +0 -0
- package/src/demo/mode.ts +0 -0
- package/src/demo/promptAssist.ts +0 -0
- package/src/demo/sampleData.ts +0 -0
- package/src/demo/sampleSets.ts +0 -0
- package/src/demo/seed.ts +0 -0
- package/src/nav/BrandMark.css +0 -0
- package/src/nav/BrandMark.tsx +0 -0
- package/src/nav/Navbar.css +0 -0
- package/src/nav/Navbar.tsx +8 -3
- package/src/regions/SpatialRegionEditor.tsx +0 -0
- package/src/regions/TemporalRegionEditor.tsx +0 -0
- package/src/regions/types.ts +0 -0
- package/src/session.ts +8 -43
- package/src/share.ts +0 -0
- package/src/stations/ActivePanel.tsx +0 -0
- package/src/stations/ActiveProcessesStation.tsx +0 -0
- package/src/stations/AddFileBar.tsx +0 -0
- package/src/stations/AudioCropStation.tsx +0 -0
- package/src/stations/ComfyPanel.tsx +0 -0
- package/src/stations/ComfyStation.tsx +0 -0
- package/src/stations/CondensedKnobStrip.tsx +0 -0
- package/src/stations/ConsolePanel.tsx +0 -0
- package/src/stations/FrameExtractStation.tsx +0 -0
- package/src/stations/GenIdentityBar.tsx +0 -0
- package/src/stations/GenerateStation.tsx +10 -6
- package/src/stations/GoalComposer.tsx +0 -0
- package/src/stations/ImageCropStation.tsx +0 -0
- package/src/stations/KnobInfo.tsx +0 -0
- package/src/stations/PlaceholderStation.tsx +0 -0
- package/src/stations/ScriptFirstStation.tsx +4 -0
- package/src/stations/SectionTabs.tsx +0 -0
- package/src/stations/SharePanel.tsx +4 -4
- package/src/stations/SidebarRegistry.tsx +0 -0
- package/src/stations/StationShell.tsx +0 -0
- package/src/stations/StudioClipsStation.tsx +0 -0
- package/src/stations/TemplateStepsPanel.tsx +190 -0
- package/src/stations/ToksPanel.tsx +1 -2
- package/src/stations/WorkbenchDrawer.tsx +0 -0
- package/src/stations/WorkbenchSidebar.tsx +8 -261
- package/src/stations/WorkbenchStation.tsx +0 -0
- package/src/stations/comfyEmbed.ts +0 -0
- package/src/stations/drawerShell.tsx +0 -0
- package/src/stations/ops/AudioCropCore.tsx +0 -0
- package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
- package/src/stations/ops/FrameExtractCore.tsx +0 -0
- package/src/stations/ops/ImageCropCore.tsx +0 -0
- package/src/stations/ops/VideoCropCore.tsx +0 -0
- package/src/stations/ops/useVideoCropJobs.ts +0 -0
- package/src/stations/oracle/DagPanel.tsx +0 -0
- package/src/stations/oracle/OracleStation.tsx +2 -0
- package/src/stations/oracle/PerformanceComposer.tsx +119 -0
- package/src/stations/oracle/SpatialTab.tsx +0 -0
- package/src/stations/oracle/StewardPanel.tsx +0 -0
- package/src/stations/oracle/oracle.css +33 -0
- package/src/stations/oracle/oracleShared.tsx +0 -0
- package/src/stations/oracle/oracleTypes.ts +0 -0
- package/src/stations/oracle/useOracle.ts +0 -0
- package/src/stations/pollCaps.ts +0 -0
- package/src/stations/registry.ts +0 -0
- package/src/stations/scriptFirst.css +23 -0
- package/src/stations/studio/AngleRingEditor.tsx +0 -0
- package/src/stations/studio/IdentitiesStation.tsx +86 -76
- package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
- package/src/stations/studio/IdentityProfileControls.tsx +0 -0
- package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
- package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
- package/src/stations/studio/StudioGenerateMode.tsx +3 -2
- package/src/stations/studio/StudioGenerateTab.tsx +278 -190
- package/src/stations/studio/StudioItemCard.tsx +0 -0
- package/src/stations/studio/StudioLibraryTab.tsx +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx +321 -46
- package/src/stations/studio/StudioMovieSessions.tsx +12 -19
- package/src/stations/studio/StudioPlane.tsx +28 -5
- package/src/stations/studio/StudioSplicedRow.tsx +0 -0
- package/src/stations/studio/StudioViewer.tsx +6 -11
- package/src/stations/studio/TurntableViewer.tsx +0 -0
- package/src/stations/studio/coordinationBadge.tsx +0 -0
- package/src/stations/studio/identityUpload.ts +0 -0
- package/src/stations/studio/movieTimeline.ts +0 -0
- package/src/stations/studio/promptCard.tsx +0 -0
- package/src/stations/studio/studioItem.ts +0 -0
- package/src/stations/studio/studioShared.tsx +14 -2
- package/src/stations/studio/useIdentityCreateJob.ts +26 -21
- package/src/stations/studio/useIdentityProfiles.ts +0 -0
- package/src/stations/studio/useReconstruction.ts +11 -5
- package/src/stations/types.ts +0 -0
- package/src/stations/useAudioCropJobs.ts +0 -0
- package/src/stations/useAudioExtractJob.ts +0 -0
- package/src/stations/useCropJobs.ts +0 -0
- package/src/stations/useFrameJobs.ts +0 -0
- package/src/stations/useGenerateJob.ts +0 -0
- package/src/stations/useGenerateMovieJob.ts +0 -0
- package/src/stations/useGenerateRowJobs.ts +0 -0
- package/src/stations/useGenerateSceneJob.ts +0 -0
- package/src/stations/useGenerateSceneRowJobs.ts +0 -0
- package/src/stations/useScriptFirstRun.ts +0 -0
- package/src/style/app.css +49 -0
- package/src/transport/client.ts +0 -0
- package/src/transport/telemetry.ts +0 -0
- package/src/video/DropReceptacle.tsx +0 -0
- package/src/video/LengthRow.tsx +0 -0
- package/src/video/LibraryGroups.tsx +0 -0
- package/src/video/PromptAssistButtons.tsx +5 -4
- package/src/video/UnnamedWarn.tsx +0 -0
- package/src/video/composerBridge.ts +0 -0
- package/src/video/contract.ts +0 -0
- package/src/video/coordinationReport.ts +0 -0
- package/src/video/jobClock.ts +0 -0
- package/src/video/jobStream.ts +119 -0
- package/src/video/jobTracker.ts +67 -76
- package/src/video/mediaLibrary.ts +0 -0
- package/src/video/modelsFeed.ts +105 -0
- package/src/video/renderCompat.ts +0 -0
- package/src/video/sessionForm.ts +0 -0
- package/src/video/spreadAssist.ts +0 -0
- package/src/video/studioBridge.ts +0 -0
- package/src/video/studioOracle.ts +48 -0
- package/src/video/useMediaJobs.ts +17 -3
- package/src/video/useModels.ts +12 -53
- package/src/video/useMoviePresets.ts +0 -0
- package/src/video/useMovieSessions.ts +15 -6
- package/src/video/useOperatorHistory.ts +11 -101
- package/src/video/usePresets.ts +0 -0
- package/src/video/useProjects.ts +0 -0
- package/src/video/usePromptAssist.ts +70 -25
- package/src/video/useRenderPresets.ts +0 -0
- package/src/video/useStudioAssistLog.ts +0 -0
- package/src/video/useStudioPresets.ts +0 -0
- package/src/video/useToks.ts +14 -6
- package/src/video/watchVideoJob.ts +43 -0
- package/tsconfig.json +0 -0
- package/vite.config.ts +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
- package/src/video/useWorkerCalls.ts +0 -165
package/src/video/jobTracker.ts
CHANGED
|
@@ -25,9 +25,10 @@ import { useSyncExternalStore } from "react";
|
|
|
25
25
|
import { getSessionId } from "../session";
|
|
26
26
|
import { isCanned } from "../demo/mode";
|
|
27
27
|
import { installDemoTransport } from "../demo/demoFetch";
|
|
28
|
-
import {
|
|
28
|
+
import { jobCancelUrl, videoJobUrl } from "../config";
|
|
29
29
|
import { request, okValue } from "../transport/client";
|
|
30
30
|
import { addToLibrary } from "./mediaLibrary";
|
|
31
|
+
import { subscribeJobStream, type JobEvent } from "./jobStream";
|
|
31
32
|
import {
|
|
32
33
|
jobRecordSchema,
|
|
33
34
|
isTerminal,
|
|
@@ -38,12 +39,6 @@ import {
|
|
|
38
39
|
type AnyProgress,
|
|
39
40
|
} from "./contract";
|
|
40
41
|
import type { SpatialRegion, TemporalRegion } from "../regions/types";
|
|
41
|
-
import {
|
|
42
|
-
GENERATE_POLL_CAP_MS,
|
|
43
|
-
JOB_POLL_CAP_MS,
|
|
44
|
-
generateScenePollCapMs,
|
|
45
|
-
generateMoviePollCapMs,
|
|
46
|
-
} from "../stations/pollCaps";
|
|
47
42
|
|
|
48
43
|
/** The job kinds this arm enqueues, one poll-parse branch per kind. */
|
|
49
44
|
export type JobKind =
|
|
@@ -52,7 +47,8 @@ export type JobKind =
|
|
|
52
47
|
| "audio_extract"
|
|
53
48
|
| "generate_image"
|
|
54
49
|
| "generate_scene"
|
|
55
|
-
| "generate_movie"
|
|
50
|
+
| "generate_movie"
|
|
51
|
+
| "studio_i2v";
|
|
56
52
|
|
|
57
53
|
/** The station a job belongs to — also the library origin for crop pushes.
|
|
58
54
|
* "video-crop" (Studio slice 2b) is the dedicated id for the one-pass spatial+
|
|
@@ -69,7 +65,8 @@ export type StationId =
|
|
|
69
65
|
| "frames"
|
|
70
66
|
| "generate"
|
|
71
67
|
| "generate-tester"
|
|
72
|
-
| "generate-scene-parts"
|
|
68
|
+
| "generate-scene-parts"
|
|
69
|
+
| "studio";
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
72
|
* One tracked job. Persisted to sessionStorage, so — because a record may be
|
|
@@ -172,9 +169,6 @@ export interface TrackedJob {
|
|
|
172
169
|
/** The single sessionStorage key prefix (build marker — must reach the bundle). */
|
|
173
170
|
const KEY_PREFIX = "vi.jobTracker.v1";
|
|
174
171
|
|
|
175
|
-
/** Module poll cadence — matches the retired per-hook POLL_MS exactly. */
|
|
176
|
-
const POLL_MS = 800;
|
|
177
|
-
|
|
178
172
|
// Session-scoped key so two tabs keep independent trackers, and the persisted
|
|
179
173
|
// list travels with the same session id the uploads/jobs are tagged by.
|
|
180
174
|
//
|
|
@@ -199,7 +193,9 @@ function storageKey(): string {
|
|
|
199
193
|
let cache: TrackedJob[] | null = null;
|
|
200
194
|
const listeners = new Set<() => void>();
|
|
201
195
|
let storageBound = false;
|
|
202
|
-
|
|
196
|
+
// The shared job-stream unsubscribe while ≥1 tracked job is active (null = not
|
|
197
|
+
// subscribed). The tracker no longer polls; it applies pushed rows by job_id.
|
|
198
|
+
let streamUnsub: (() => void) | null = null;
|
|
203
199
|
|
|
204
200
|
function readStorage(): TrackedJob[] {
|
|
205
201
|
try {
|
|
@@ -253,36 +249,6 @@ function anyActive(): boolean {
|
|
|
253
249
|
return ensureLoaded().some(isJobActive);
|
|
254
250
|
}
|
|
255
251
|
|
|
256
|
-
function capFor(
|
|
257
|
-
kind: JobKind,
|
|
258
|
-
nFrames?: number | null,
|
|
259
|
-
chain?: boolean | null,
|
|
260
|
-
segments?: number | null,
|
|
261
|
-
): number {
|
|
262
|
-
if (kind === "generate_movie")
|
|
263
|
-
return generateMoviePollCapMs(segments ?? 1, nFrames ?? undefined, chain ?? false);
|
|
264
|
-
if (kind === "generate_scene")
|
|
265
|
-
return generateScenePollCapMs(nFrames ?? 1, chain ?? false);
|
|
266
|
-
return kind === "generate_image" ? GENERATE_POLL_CAP_MS : JOB_POLL_CAP_MS;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function opFor(kind: JobKind): string {
|
|
270
|
-
switch (kind) {
|
|
271
|
-
case "crop":
|
|
272
|
-
return "crop.poll";
|
|
273
|
-
case "frame_extract":
|
|
274
|
-
return "frame_extract.poll";
|
|
275
|
-
case "audio_extract":
|
|
276
|
-
return "audio_extract.poll";
|
|
277
|
-
case "generate_image":
|
|
278
|
-
return "generate_image.poll";
|
|
279
|
-
case "generate_scene":
|
|
280
|
-
return "generate_scene.poll";
|
|
281
|
-
case "generate_movie":
|
|
282
|
-
return "generate_movie.poll";
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
252
|
function failMsgFor(kind: JobKind): string {
|
|
287
253
|
switch (kind) {
|
|
288
254
|
case "crop":
|
|
@@ -297,6 +263,8 @@ function failMsgFor(kind: JobKind): string {
|
|
|
297
263
|
return "Scene generation failed.";
|
|
298
264
|
case "generate_movie":
|
|
299
265
|
return "Movie generation failed.";
|
|
266
|
+
case "studio_i2v":
|
|
267
|
+
return "Clip generation failed.";
|
|
300
268
|
}
|
|
301
269
|
}
|
|
302
270
|
|
|
@@ -383,6 +351,11 @@ function pushOutputs(rec: TrackedJob, outputs: MediaRef[]): void {
|
|
|
383
351
|
});
|
|
384
352
|
return;
|
|
385
353
|
}
|
|
354
|
+
if (rec.kind === "studio_i2v") {
|
|
355
|
+
const out = outputs.find((m) => m.kind === "video");
|
|
356
|
+
if (out) addToLibrary(out, "studio", rec.libLabel ?? "studio clip");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
386
359
|
// audio_extract: intermediate (feeds the temporal editor) — never pushed.
|
|
387
360
|
}
|
|
388
361
|
|
|
@@ -432,51 +405,57 @@ function applyPoll(rec: TrackedJob, raw: unknown): void {
|
|
|
432
405
|
}
|
|
433
406
|
}
|
|
434
407
|
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
408
|
+
// Apply one pushed row to its tracked record. The stream event carries `job` —
|
|
409
|
+
// the GET /video/jobs/<id> view applyPoll already parses — so a transition is
|
|
410
|
+
// applied the moment a worker reports it, with no poll. Events for untracked or
|
|
411
|
+
// already-terminal ids are ignored; a burst self-coalesces (each is cheap).
|
|
412
|
+
function onStreamEvent(ev: JobEvent): void {
|
|
413
|
+
if (ev.stage === "stream.ready") {
|
|
414
|
+
for (const rec of ensureLoaded().filter((row) => !isJobTerminal(row))) {
|
|
415
|
+
void request<unknown>(videoJobUrl(rec.jobId), {
|
|
416
|
+
meta: { specKey: rec.station, operation: "video.job.reconcile" },
|
|
417
|
+
}).then((res) => {
|
|
418
|
+
const current = ensureLoaded().find((row) => row.jobId === rec.jobId);
|
|
419
|
+
if (res.ok && current && !isJobTerminal(current))
|
|
420
|
+
applyPoll(current, okValue(res));
|
|
421
|
+
disarmIfIdle();
|
|
422
|
+
});
|
|
447
423
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const jobId = ev.job_id;
|
|
427
|
+
if (!jobId) return;
|
|
428
|
+
const rec = ensureLoaded().find((r) => r.jobId === jobId);
|
|
429
|
+
if (!rec || !isJobActive(rec)) return;
|
|
430
|
+
if (ev.job == null) {
|
|
431
|
+
// The server no longer holds this job (gone) — mark expired, stop watching it.
|
|
432
|
+
mutate(rec.jobId, { expired: true });
|
|
433
|
+
} else {
|
|
434
|
+
applyPoll(rec, ev.job);
|
|
453
435
|
}
|
|
454
436
|
disarmIfIdle();
|
|
455
437
|
}
|
|
456
438
|
|
|
457
|
-
//
|
|
458
|
-
// nothing is active.
|
|
439
|
+
// Subscribe to the shared job stream while ≥1 record is active; a no-op if
|
|
440
|
+
// already subscribed or nothing is active. Replaces the retired poll interval —
|
|
441
|
+
// there is no timer anywhere now; rows arrive on push.
|
|
459
442
|
function armPolling(): void {
|
|
460
443
|
if (typeof window === "undefined") return;
|
|
461
|
-
if (
|
|
444
|
+
if (streamUnsub != null) return;
|
|
462
445
|
if (!anyActive()) return;
|
|
463
|
-
// Ordering guard for the canned demo:
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// brochure must never do that). installDemoTransport is idempotent, so calling it
|
|
468
|
-
// here guarantees the shim is installed before ANY canned poll fires, whatever the
|
|
469
|
-
// module import order. No-op outside canned mode.
|
|
446
|
+
// Ordering guard for the canned demo: initTracker() runs at IMPORT time (before
|
|
447
|
+
// entry.tsx's installVideoDemo()); installDemoTransport is idempotent and also
|
|
448
|
+
// shims EventSource, so calling it here guarantees the canned stream shim is up
|
|
449
|
+
// before we subscribe, whatever the module import order. No-op outside canned mode.
|
|
470
450
|
if (isCanned()) installDemoTransport();
|
|
471
|
-
|
|
472
|
-
void pollTick();
|
|
451
|
+
streamUnsub = subscribeJobStream(onStreamEvent);
|
|
473
452
|
}
|
|
474
453
|
|
|
475
|
-
//
|
|
454
|
+
// Drop the subscription once no record is active (reopened by armPolling later).
|
|
476
455
|
function disarmIfIdle(): void {
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
456
|
+
if (streamUnsub != null && !anyActive()) {
|
|
457
|
+
streamUnsub();
|
|
458
|
+
streamUnsub = null;
|
|
480
459
|
}
|
|
481
460
|
}
|
|
482
461
|
|
|
@@ -547,6 +526,13 @@ export function trackJob(input: TrackJobInput): void {
|
|
|
547
526
|
writeStorage(cache);
|
|
548
527
|
emit();
|
|
549
528
|
armPolling();
|
|
529
|
+
void request<unknown>(videoJobUrl(input.jobId), {
|
|
530
|
+
meta: { specKey: "studio", operation: "video.job.seed" },
|
|
531
|
+
}).then((res) => {
|
|
532
|
+
const active = ensureLoaded().find((row) => row.jobId === input.jobId);
|
|
533
|
+
if (res.ok && active && isJobActive(active)) applyPoll(active, okValue(res));
|
|
534
|
+
disarmIfIdle();
|
|
535
|
+
});
|
|
550
536
|
}
|
|
551
537
|
|
|
552
538
|
/**
|
|
@@ -645,6 +631,10 @@ function bindStorageOnce(): void {
|
|
|
645
631
|
cache = readStorage();
|
|
646
632
|
emit();
|
|
647
633
|
armPolling();
|
|
634
|
+
onStreamEvent({ stage: "stream.ready" });
|
|
635
|
+
});
|
|
636
|
+
window.addEventListener("focus", () => {
|
|
637
|
+
onStreamEvent({ stage: "stream.ready" });
|
|
648
638
|
});
|
|
649
639
|
}
|
|
650
640
|
|
|
@@ -697,6 +687,7 @@ function initTracker(): void {
|
|
|
697
687
|
writeStorage(cache);
|
|
698
688
|
}
|
|
699
689
|
armPolling();
|
|
690
|
+
onStreamEvent({ stage: "stream.ready" });
|
|
700
691
|
}
|
|
701
692
|
|
|
702
693
|
initTracker();
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SHARED model catalog feed (2026-10-05) — the models twin of jobStream.ts.
|
|
2
|
+
// Every model picker on the page SEEDS from ONE shared GET of /v1/models and
|
|
3
|
+
// /prompt/tasks (the first hook to ask fetches; the rest reuse that answer), then
|
|
4
|
+
// subscribes here. One EventSource on /api/llm/models/rows/stream (core pushes a
|
|
5
|
+
// row the moment a box's `models` / `residents` row changes — DB trigger ->
|
|
6
|
+
// pg_notify) marks the cached answer stale and tells subscribers to re-read it.
|
|
7
|
+
// No focus/visibility revalidation and no timer: the catalog moves only when the
|
|
8
|
+
// DB says it moved. Same reconnect rule as jobStream: re-attach only after a
|
|
9
|
+
// stream that HAD opened drops (the server caps it at an hour).
|
|
10
|
+
import { hugpyConfig } from "../config";
|
|
11
|
+
import { request } from "../transport/client";
|
|
12
|
+
|
|
13
|
+
type Result = Awaited<ReturnType<typeof request<unknown>>>;
|
|
14
|
+
type Listener = () => void;
|
|
15
|
+
|
|
16
|
+
const cache = new Map<string, Promise<Result>>();
|
|
17
|
+
const listeners = new Set<Listener>();
|
|
18
|
+
let es: EventSource | null = null;
|
|
19
|
+
let refs = 0;
|
|
20
|
+
let everOpened = false;
|
|
21
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
+
let notifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
23
|
+
const RETRY_MS = 5000;
|
|
24
|
+
const COALESCE_MS = 500; // one action touches several rows; re-read once
|
|
25
|
+
|
|
26
|
+
/** The shared answer for `url` (one request for the whole page until the DB
|
|
27
|
+
* reports a change). A failed answer is not kept, so the next caller retries. */
|
|
28
|
+
export function sharedRequest(url: string, meta: Record<string, string>): Promise<Result> {
|
|
29
|
+
let p = cache.get(url);
|
|
30
|
+
if (!p) {
|
|
31
|
+
p = request<unknown>(url, { meta } as Parameters<typeof request>[1]);
|
|
32
|
+
cache.set(url, p);
|
|
33
|
+
p.then((r) => {
|
|
34
|
+
if (!r.ok && cache.get(url) === p) cache.delete(url);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function changed(): void {
|
|
41
|
+
if (notifyTimer) return;
|
|
42
|
+
notifyTimer = setTimeout(() => {
|
|
43
|
+
notifyTimer = null;
|
|
44
|
+
cache.clear();
|
|
45
|
+
for (const fn of listeners) {
|
|
46
|
+
try {
|
|
47
|
+
fn();
|
|
48
|
+
} catch {
|
|
49
|
+
/* one bad subscriber must not stall the rest */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, COALESCE_MS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function openStream(): void {
|
|
56
|
+
if (es || typeof window === "undefined" || typeof EventSource === "undefined") return;
|
|
57
|
+
let source: EventSource;
|
|
58
|
+
try {
|
|
59
|
+
source = new EventSource(`${hugpyConfig.apiBase}/llm/models/rows/stream`, {
|
|
60
|
+
withCredentials: true,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return; // no EventSource — pickers keep their one-time seed
|
|
64
|
+
}
|
|
65
|
+
es = source;
|
|
66
|
+
source.onopen = () => {
|
|
67
|
+
everOpened = true;
|
|
68
|
+
};
|
|
69
|
+
source.onmessage = (e: MessageEvent) => {
|
|
70
|
+
let ev: { stage?: string };
|
|
71
|
+
try {
|
|
72
|
+
ev = JSON.parse(e.data);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (ev.stage === "stream.ready") return; // transport hello, not a row
|
|
77
|
+
changed();
|
|
78
|
+
};
|
|
79
|
+
source.onerror = () => {
|
|
80
|
+
source.close();
|
|
81
|
+
es = null;
|
|
82
|
+
if (!everOpened || refs === 0 || retryTimer) return; // absent endpoint: stay on the seed
|
|
83
|
+
retryTimer = setTimeout(() => {
|
|
84
|
+
retryTimer = null;
|
|
85
|
+
if (refs > 0) openStream();
|
|
86
|
+
}, RETRY_MS);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Call `fn` whenever the DB reports a model-row change. Returns the unsubscribe. */
|
|
91
|
+
export function subscribeModelChanges(fn: Listener): () => void {
|
|
92
|
+
listeners.add(fn);
|
|
93
|
+
refs += 1;
|
|
94
|
+
openStream();
|
|
95
|
+
return () => {
|
|
96
|
+
listeners.delete(fn);
|
|
97
|
+
refs -= 1;
|
|
98
|
+
if (refs <= 0) {
|
|
99
|
+
refs = 0;
|
|
100
|
+
es?.close();
|
|
101
|
+
es = null;
|
|
102
|
+
everOpened = false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
File without changes
|
package/src/video/sessionForm.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { hugpyConfig } from "../config";
|
|
2
|
+
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
3
|
+
|
|
4
|
+
export type OracleSettings = {
|
|
5
|
+
model_id?: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
fps?: number;
|
|
9
|
+
frames?: number;
|
|
10
|
+
steps?: number;
|
|
11
|
+
cfg?: number;
|
|
12
|
+
seed?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function planStudioSettings(
|
|
16
|
+
capability: "t2v" | "i2v",
|
|
17
|
+
prompt: string,
|
|
18
|
+
settings: OracleSettings,
|
|
19
|
+
explicit: string[],
|
|
20
|
+
): Promise<OracleSettings> {
|
|
21
|
+
const res = await request<unknown>(hugpyConfig.studioPlanUrl, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ capability, prompt, settings, explicit_settings: explicit }),
|
|
25
|
+
meta: { specKey: "studio", operation: "studio.oracle.plan" },
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) throw new Error(describeAppError(errorOf(res)));
|
|
28
|
+
const result = okValue(res) as { settings?: OracleSettings };
|
|
29
|
+
if (!result || !result.settings || !result.settings.model_id) {
|
|
30
|
+
throw new Error("Oracle returned no executable Studio settings plan.");
|
|
31
|
+
}
|
|
32
|
+
return result.settings;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function planStudioMovie(body: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
36
|
+
const res = await request<unknown>(hugpyConfig.studioPlanUrl, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
meta: { specKey: "studio", operation: "studio.oracle.movie.plan" },
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) throw new Error(describeAppError(errorOf(res)));
|
|
43
|
+
const result = okValue(res) as { body?: Record<string, unknown>; planning?: unknown[] };
|
|
44
|
+
if (!result || !result.body || !Array.isArray(result.planning)) {
|
|
45
|
+
throw new Error("Oracle returned no executable Cinema plan.");
|
|
46
|
+
}
|
|
47
|
+
return result.body;
|
|
48
|
+
}
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
13
|
import { hugpyConfig } from "../config";
|
|
14
14
|
import { request, okValue } from "../transport/client";
|
|
15
|
+
import { subscribeJobStream } from "./jobStream";
|
|
15
16
|
// The stage-timeline + failure shapes are the SAME wire objects the studio tab
|
|
16
17
|
// already renders through `ProcessTimeline`; reusing its types (and, in the panel,
|
|
17
18
|
// the component itself) keeps one renderer for "what it did / where it broke"
|
|
18
19
|
// instead of a second, divergent one for the console-wide view.
|
|
19
20
|
import type { StageEntry, JobFailure } from "../stations/studio/studioShared";
|
|
20
21
|
|
|
21
|
-
const POLL_MS = 2000;
|
|
22
22
|
const IN_FLIGHT = new Set(["queued", "claimed", "running", "cancelling"]);
|
|
23
23
|
|
|
24
24
|
/** The numbers behind the bar (omit-when-unset on the wire). */
|
|
@@ -230,13 +230,27 @@ export function useMediaJobs(
|
|
|
230
230
|
setJobs(parseJobs(okValue(r), includeTerminal));
|
|
231
231
|
}, [includeTerminal]);
|
|
232
232
|
|
|
233
|
+
// Seed ONCE, then refresh on PUSH: a worker-reported transition arrives on the
|
|
234
|
+
// shared job stream and we re-read the catalog projection once (coalesced), so
|
|
235
|
+
// placement / stage-log / phase stay the server's list shape — never a timer.
|
|
233
236
|
useEffect(() => {
|
|
234
237
|
mounted.current = true;
|
|
235
238
|
void load();
|
|
236
|
-
|
|
239
|
+
let coalesce: number | null = null;
|
|
240
|
+
const unsub = subscribeJobStream(() => {
|
|
241
|
+
if (coalesce != null) return; // already a reload pending this burst
|
|
242
|
+
coalesce = window.setTimeout(() => {
|
|
243
|
+
coalesce = null;
|
|
244
|
+
void load();
|
|
245
|
+
}, 300);
|
|
246
|
+
});
|
|
247
|
+
const onFocus = () => { void load(); };
|
|
248
|
+
window.addEventListener("focus", onFocus);
|
|
237
249
|
return () => {
|
|
238
250
|
mounted.current = false;
|
|
239
|
-
window.
|
|
251
|
+
if (coalesce != null) window.clearTimeout(coalesce);
|
|
252
|
+
unsub();
|
|
253
|
+
window.removeEventListener("focus", onFocus);
|
|
240
254
|
};
|
|
241
255
|
}, [load]);
|
|
242
256
|
|
package/src/video/useModels.ts
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
// Staleness fix: the registry is no longer fetched only once on mount. Models
|
|
8
8
|
// adopted MID-SESSION (e.g. ae ComfyUI checkpoints that appear once ComfyUI comes
|
|
9
9
|
// up) must enter the picker WITHOUT a full page reload, so the hook now
|
|
10
|
-
// stale-while-revalidates — it re-fetches on window focus / tab visibility
|
|
11
|
-
// gentle background interval (skipped while the tab is hidden), and on demand via
|
|
10
|
+
// stale-while-revalidates — it re-fetches on window focus / tab visibility and on demand via
|
|
12
11
|
// the exposed `refresh()`. Background revalidations never blank a working picker:
|
|
13
12
|
// `loading` flips only for the very first load, and a failed poll keeps the last
|
|
14
13
|
// good list. The public surface gained `refresh` (additive — existing consumers
|
|
@@ -17,6 +16,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
17
16
|
import { z } from "zod";
|
|
18
17
|
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
19
18
|
import { hugpyConfig } from "../config";
|
|
19
|
+
import { sharedRequest, subscribeModelChanges } from "./modelsFeed";
|
|
20
20
|
|
|
21
21
|
export interface ModelOption {
|
|
22
22
|
/** The model id sent to the backend. */
|
|
@@ -119,10 +119,6 @@ function nonTargetReason(m: z.infer<typeof modelEntrySchema>): string {
|
|
|
119
119
|
return "Unclassified — no task derived from this model's files yet.";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Gentle background revalidation cadence (ms). Cheap same-origin GET; only ONE
|
|
123
|
-
// picker instance is mounted at a time (studio sub-tabs share one WorkbenchStation),
|
|
124
|
-
// and polls are skipped while the tab is hidden.
|
|
125
|
-
const REFRESH_INTERVAL_MS = 45_000;
|
|
126
122
|
|
|
127
123
|
// Capability = the FULL `tasks` list; the single `task` (primary) is only the
|
|
128
124
|
// fallback for older payloads. Exported so task-specific pickers (e.g. the Movie
|
|
@@ -167,7 +163,7 @@ export function useImageModels(): ImageModelsState {
|
|
|
167
163
|
});
|
|
168
164
|
|
|
169
165
|
// `mounted` gates setState after unmount; `inFlight` collapses overlapping
|
|
170
|
-
// revalidations (
|
|
166
|
+
// revalidations (focus + manual) into one; `ctrlRef` lets every
|
|
171
167
|
// trigger — including a manual refresh — abort on unmount.
|
|
172
168
|
const mounted = useRef(true);
|
|
173
169
|
const inFlight = useRef(false);
|
|
@@ -176,17 +172,10 @@ export function useImageModels(): ImageModelsState {
|
|
|
176
172
|
const load = useCallback(async (background: boolean) => {
|
|
177
173
|
if (inFlight.current) return;
|
|
178
174
|
inFlight.current = true;
|
|
179
|
-
const signal = ctrlRef.current?.signal;
|
|
180
175
|
try {
|
|
181
176
|
const [mRes, tRes] = await Promise.all([
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
meta: { specKey: "frames", operation: "models.list" },
|
|
185
|
-
}),
|
|
186
|
-
request<unknown>(hugpyConfig.promptTasksUrl, {
|
|
187
|
-
signal,
|
|
188
|
-
meta: { specKey: "frames", operation: "prompt.tasks" },
|
|
189
|
-
}),
|
|
177
|
+
sharedRequest(hugpyConfig.modelsUrl, { specKey: "frames", operation: "models.list" }),
|
|
178
|
+
sharedRequest(hugpyConfig.promptTasksUrl, { specKey: "frames", operation: "prompt.tasks" }),
|
|
190
179
|
]);
|
|
191
180
|
if (!mounted.current) return;
|
|
192
181
|
|
|
@@ -284,28 +273,13 @@ export function useImageModels(): ImageModelsState {
|
|
|
284
273
|
// Foreground load (the only one that toggles `loading` / surfaces an error).
|
|
285
274
|
void load(false);
|
|
286
275
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
const revalidate = () => {
|
|
290
|
-
if (document.visibilityState === "hidden") return;
|
|
291
|
-
void load(true);
|
|
292
|
-
};
|
|
293
|
-
window.addEventListener("focus", revalidate);
|
|
294
|
-
document.addEventListener("visibilitychange", revalidate);
|
|
295
|
-
|
|
296
|
-
// Gentle interval backstop for the same-tab case (ComfyUI comes up while the
|
|
297
|
-
// operator is looking at the station). Skipped while the tab is hidden.
|
|
298
|
-
const timer = setInterval(() => {
|
|
299
|
-
if (document.visibilityState === "hidden") return;
|
|
300
|
-
void load(true);
|
|
301
|
-
}, REFRESH_INTERVAL_MS);
|
|
276
|
+
// Re-read only when the DB reports a model-row change (modelsFeed).
|
|
277
|
+
const off = subscribeModelChanges(() => void load(true));
|
|
302
278
|
|
|
303
279
|
return () => {
|
|
304
280
|
mounted.current = false;
|
|
305
281
|
ctrl.abort();
|
|
306
|
-
|
|
307
|
-
document.removeEventListener("visibilitychange", revalidate);
|
|
308
|
-
clearInterval(timer);
|
|
282
|
+
off();
|
|
309
283
|
};
|
|
310
284
|
}, [load]);
|
|
311
285
|
|
|
@@ -328,7 +302,7 @@ export interface TaskModelsState {
|
|
|
328
302
|
* task — the generic sibling of useImageModels for non-generation pickers. The
|
|
329
303
|
* Movie director's judge-model select uses it with "image-text-to-text" to surface
|
|
330
304
|
* the vision-language models that can SCORE a segment. Same tolerant fetch +
|
|
331
|
-
* stale-while-revalidate posture (focus / visibility /
|
|
305
|
+
* stale-while-revalidate posture (focus / visibility / manual refresh);
|
|
332
306
|
* a failed background revalidation keeps the last good list. No task-defaults fetch
|
|
333
307
|
* (there is no canonical default for an arbitrary task) — the caller supplies its
|
|
334
308
|
* own default and keeps it selectable even when the list is empty.
|
|
@@ -348,12 +322,8 @@ export function useModelsByTask(task: string): TaskModelsState {
|
|
|
348
322
|
async (background: boolean) => {
|
|
349
323
|
if (inFlight.current) return;
|
|
350
324
|
inFlight.current = true;
|
|
351
|
-
const signal = ctrlRef.current?.signal;
|
|
352
325
|
try {
|
|
353
|
-
const mRes = await
|
|
354
|
-
signal,
|
|
355
|
-
meta: { specKey: "generate", operation: "models.list" },
|
|
356
|
-
});
|
|
326
|
+
const mRes = await sharedRequest(hugpyConfig.modelsUrl, { specKey: "generate", operation: "models.list" });
|
|
357
327
|
if (!mounted.current) return;
|
|
358
328
|
if (!mRes.ok) {
|
|
359
329
|
if (background) return;
|
|
@@ -386,22 +356,11 @@ export function useModelsByTask(task: string): TaskModelsState {
|
|
|
386
356
|
const ctrl = new AbortController();
|
|
387
357
|
ctrlRef.current = ctrl;
|
|
388
358
|
void load(false);
|
|
389
|
-
const
|
|
390
|
-
if (document.visibilityState === "hidden") return;
|
|
391
|
-
void load(true);
|
|
392
|
-
};
|
|
393
|
-
window.addEventListener("focus", revalidate);
|
|
394
|
-
document.addEventListener("visibilitychange", revalidate);
|
|
395
|
-
const timer = setInterval(() => {
|
|
396
|
-
if (document.visibilityState === "hidden") return;
|
|
397
|
-
void load(true);
|
|
398
|
-
}, REFRESH_INTERVAL_MS);
|
|
359
|
+
const off = subscribeModelChanges(() => void load(true));
|
|
399
360
|
return () => {
|
|
400
361
|
mounted.current = false;
|
|
401
362
|
ctrl.abort();
|
|
402
|
-
|
|
403
|
-
document.removeEventListener("visibilitychange", revalidate);
|
|
404
|
-
clearInterval(timer);
|
|
363
|
+
off();
|
|
405
364
|
};
|
|
406
365
|
}, [load]);
|
|
407
366
|
|
|
File without changes
|
|
@@ -23,10 +23,7 @@ import {
|
|
|
23
23
|
studioMovieResumeUrl,
|
|
24
24
|
} from "../config";
|
|
25
25
|
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
26
|
-
|
|
27
|
-
// 6s — the same background cadence useStudioClips polls the clip catalog at. A movie
|
|
28
|
-
// segment takes minutes, so anything faster only costs a filesystem walk.
|
|
29
|
-
const POLL_MS = 6000;
|
|
26
|
+
import { subscribeJobStream } from "./jobStream";
|
|
30
27
|
|
|
31
28
|
// The bus statuses that mean "this session's job is still going to do something" —
|
|
32
29
|
// the client-side mirror of the route's _MOVIE_INFLIGHT_STATES, used to decide
|
|
@@ -223,11 +220,23 @@ export function useMovieSessions(active: boolean): MovieSessionsState {
|
|
|
223
220
|
}
|
|
224
221
|
}, []);
|
|
225
222
|
|
|
223
|
+
// Seed once when the panel opens, then re-read the movies root only when a
|
|
224
|
+
// worker reports a job transition (coalesced) — no 6s filesystem-walk poll.
|
|
226
225
|
useEffect(() => {
|
|
227
226
|
if (!active) return;
|
|
228
227
|
void load();
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
let coalesce: number | null = null;
|
|
229
|
+
const unsub = subscribeJobStream(() => {
|
|
230
|
+
if (coalesce != null) return;
|
|
231
|
+
coalesce = window.setTimeout(() => {
|
|
232
|
+
coalesce = null;
|
|
233
|
+
void load();
|
|
234
|
+
}, 400);
|
|
235
|
+
});
|
|
236
|
+
return () => {
|
|
237
|
+
if (coalesce != null) window.clearTimeout(coalesce);
|
|
238
|
+
unsub();
|
|
239
|
+
};
|
|
231
240
|
}, [active, load]);
|
|
232
241
|
|
|
233
242
|
// Pause and resume differ only in their url and their telemetry op, so they share
|