@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,328 @@
|
|
|
1
|
+
// The media library — a session-scoped, cross-station store of produced MediaRefs.
|
|
2
|
+
//
|
|
3
|
+
// This is the FieldSpec-propagation analog for the video-intelligence arm: a
|
|
4
|
+
// station (Frames today) pushes the frames the user picks in here, and a later
|
|
5
|
+
// station (Generate) reads them back as selectable prompt inputs. Backed by
|
|
6
|
+
// sessionStorage (per-tab, keyed by session id) so a page reload keeps the picks
|
|
7
|
+
// but a new tab/session starts clean. `clearLibrary` empties the SESSION list
|
|
8
|
+
// only — it NEVER deletes the underlying server assets (house policy: no deletion).
|
|
9
|
+
//
|
|
10
|
+
// The public surface below is FROZEN — the Generate station imports it verbatim.
|
|
11
|
+
import { useSyncExternalStore } from "react";
|
|
12
|
+
import { getSessionId } from "../session";
|
|
13
|
+
import { isCanned } from "../demo/mode";
|
|
14
|
+
import type { MediaRef } from "./contract";
|
|
15
|
+
|
|
16
|
+
/** The generation kinds whose outputs carry prompt/model provenance. */
|
|
17
|
+
export type LibraryGenKind =
|
|
18
|
+
| "generate_image"
|
|
19
|
+
| "generate_scene"
|
|
20
|
+
| "generate_movie"
|
|
21
|
+
| "studio_i2v";
|
|
22
|
+
|
|
23
|
+
export interface LibraryItem {
|
|
24
|
+
/** The produced asset. */
|
|
25
|
+
ref: MediaRef;
|
|
26
|
+
/** Which station/flow produced it (e.g. "frames"). */
|
|
27
|
+
origin: string;
|
|
28
|
+
/** Optional human label (e.g. "frame 12"). */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** When it landed in the library (ms epoch). */
|
|
31
|
+
addedAt: number;
|
|
32
|
+
/**
|
|
33
|
+
* Generation provenance — present ONLY for Generate-station outputs (image &
|
|
34
|
+
* scene). Captured client-side at generation time (the composer already holds
|
|
35
|
+
* the prompt + model), so the session-library can annotate each output and
|
|
36
|
+
* re-stage its prompt/model for Replicate / Continue. Only the server MediaRef
|
|
37
|
+
* (not the pixels) is persisted, per the library's storage-quota policy.
|
|
38
|
+
*/
|
|
39
|
+
prompt?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
/** Groups a single generation run's outputs together (the producing job id). */
|
|
42
|
+
groupId?: string;
|
|
43
|
+
/** The generation kind of the producing run — enables Continue for scenes. */
|
|
44
|
+
genKind?: LibraryGenKind;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Optional generation provenance recorded alongside a produced MediaRef. */
|
|
48
|
+
export interface LibraryProvenance {
|
|
49
|
+
prompt?: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
groupId?: string;
|
|
52
|
+
genKind?: LibraryGenKind;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The immutable "where did this come from" half of a first-class StudioItem (see
|
|
57
|
+
* stations/studio/studioItem.ts). Defined HERE — next to the library provenance
|
|
58
|
+
* types — rather than in studioItem.ts so the video/ layer (studioBridge's
|
|
59
|
+
* StudioStageRequest) can reference it WITHOUT importing from stations/, preserving
|
|
60
|
+
* the stations/ → video/ dependency direction. studioItem.ts re-exports it as the
|
|
61
|
+
* canonical StudioItem surface.
|
|
62
|
+
*/
|
|
63
|
+
export interface StudioItemProvenance {
|
|
64
|
+
/** How this item entered the studio: "upload" | "library" | "send-to-studio" |
|
|
65
|
+
* "restyle" | "op:<kind>" | … */
|
|
66
|
+
origin: string;
|
|
67
|
+
prompt?: string;
|
|
68
|
+
model?: string;
|
|
69
|
+
genKind?: LibraryGenKind;
|
|
70
|
+
/** uri of the ref this item derived from — the forward-only provenance DAG edge. */
|
|
71
|
+
parentUri?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const KEY_PREFIX = "vi.mediaLibrary.v1";
|
|
75
|
+
|
|
76
|
+
// Session-scoped key so two tabs (two sessions) keep independent libraries, and
|
|
77
|
+
// the persisted list travels with the same session id the uploads are tagged by.
|
|
78
|
+
//
|
|
79
|
+
// The canned demo (?demo=1) gets a `:demo`-SUFFIXED key (matching jobTracker) so
|
|
80
|
+
// the brochure's seeded library is hermetic — it never reads or writes the SAME
|
|
81
|
+
// tab's live-session library, and the tombstone key (derived below from
|
|
82
|
+
// storageKey) follows suit. isCanned() is URL-derived and available at module
|
|
83
|
+
// load, so the key is right from the first read regardless of import order.
|
|
84
|
+
function storageKey(): string {
|
|
85
|
+
const suffix = isCanned() ? ":demo" : "";
|
|
86
|
+
try {
|
|
87
|
+
return `${KEY_PREFIX}:${getSessionId()}${suffix}`;
|
|
88
|
+
} catch {
|
|
89
|
+
return `${KEY_PREFIX}${suffix}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// In-memory cache is the source of truth for getSnapshot — its reference only
|
|
94
|
+
// changes when the list actually changes, which keeps useSyncExternalStore stable
|
|
95
|
+
// (returning a fresh array every read would loop React 18).
|
|
96
|
+
let cache: LibraryItem[] | null = null;
|
|
97
|
+
const listeners = new Set<() => void>();
|
|
98
|
+
let storageBound = false;
|
|
99
|
+
|
|
100
|
+
function readStorage(): LibraryItem[] {
|
|
101
|
+
try {
|
|
102
|
+
const raw = sessionStorage.getItem(storageKey());
|
|
103
|
+
if (!raw) return [];
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (!Array.isArray(parsed)) return [];
|
|
106
|
+
// Drop malformed entries defensively — a stored item must at least carry a uri.
|
|
107
|
+
return parsed.filter(
|
|
108
|
+
(it) =>
|
|
109
|
+
it &&
|
|
110
|
+
typeof it === "object" &&
|
|
111
|
+
it.ref &&
|
|
112
|
+
typeof it.ref === "object" &&
|
|
113
|
+
typeof it.ref.uri === "string",
|
|
114
|
+
) as LibraryItem[];
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeStorage(items: LibraryItem[]): void {
|
|
121
|
+
try {
|
|
122
|
+
sessionStorage.setItem(storageKey(), JSON.stringify(items));
|
|
123
|
+
} catch {
|
|
124
|
+
// storage unavailable / quota exceeded — the in-memory cache still serves this
|
|
125
|
+
// session; persistence is best-effort.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureLoaded(): LibraryItem[] {
|
|
130
|
+
if (cache == null) cache = readStorage();
|
|
131
|
+
return cache;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function emit(): void {
|
|
135
|
+
for (const l of listeners) l();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Tombstones ─────────────────────────────────────────────────────────────
|
|
139
|
+
// A "Remove" that comes back on the next poll isn't a remove. The list has TWO
|
|
140
|
+
// re-populators that fire AFTER a local forget: jobTracker's terminal pushOutputs
|
|
141
|
+
// (re-runs on a reload / a re-poll of a done job) and the studio-clips list poll
|
|
142
|
+
// (re-adds every ~6s). addToLibrary's only guard was uri-dedup, which a *removed*
|
|
143
|
+
// uri no longer trips — so the item silently returned. Tombstones fix this at the
|
|
144
|
+
// single choke point: a removed uri is remembered and addToLibrary refuses to
|
|
145
|
+
// re-add it. Session-scoped (same key family as the list) so removal survives
|
|
146
|
+
// reloads within the session; studio clips ALSO archive server-side in the row
|
|
147
|
+
// handler, which stops the server from serving them across sessions too.
|
|
148
|
+
function tombstoneKey(): string {
|
|
149
|
+
return `${storageKey()}:removed`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let tombstones: Set<string> | null = null;
|
|
153
|
+
|
|
154
|
+
function ensureTombstones(): Set<string> {
|
|
155
|
+
if (tombstones == null) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = sessionStorage.getItem(tombstoneKey());
|
|
158
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
159
|
+
tombstones = new Set(
|
|
160
|
+
Array.isArray(parsed) ? parsed.filter((u) => typeof u === "string") : [],
|
|
161
|
+
);
|
|
162
|
+
} catch {
|
|
163
|
+
tombstones = new Set();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return tombstones;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeTombstones(): void {
|
|
170
|
+
try {
|
|
171
|
+
sessionStorage.setItem(tombstoneKey(), JSON.stringify([...ensureTombstones()]));
|
|
172
|
+
} catch {
|
|
173
|
+
// best-effort — the in-memory set still suppresses re-adds this session.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Mark `uri` as removed so no re-populator can re-add it. */
|
|
178
|
+
function tombstone(uri: string): void {
|
|
179
|
+
ensureTombstones().add(uri);
|
|
180
|
+
writeTombstones();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Whether `uri` was explicitly removed (and must not be re-added). */
|
|
184
|
+
export function isRemoved(uri: string): boolean {
|
|
185
|
+
return ensureTombstones().has(uri);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The current library snapshot (stable reference until it changes). */
|
|
189
|
+
export function getLibrary(): LibraryItem[] {
|
|
190
|
+
return ensureLoaded();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a produced MediaRef to the library. De-duplicates by `ref.uri` (no-op if
|
|
195
|
+
* present). The optional `provenance` argument is additive (existing 3-arg
|
|
196
|
+
* callers are unchanged) — it records the prompt/model/group that produced a
|
|
197
|
+
* Generate-station output so the session-library can annotate + re-stage it.
|
|
198
|
+
*/
|
|
199
|
+
export function addToLibrary(
|
|
200
|
+
ref: MediaRef,
|
|
201
|
+
origin: string,
|
|
202
|
+
label?: string,
|
|
203
|
+
provenance?: LibraryProvenance,
|
|
204
|
+
): void {
|
|
205
|
+
if (!ref || typeof ref.uri !== "string") return;
|
|
206
|
+
// A uri the user explicitly removed stays gone — never let a re-populator
|
|
207
|
+
// (jobTracker terminal push / studio-clips poll) resurrect it. See tombstones.
|
|
208
|
+
if (isRemoved(ref.uri)) return;
|
|
209
|
+
const current = ensureLoaded();
|
|
210
|
+
if (current.some((it) => it.ref.uri === ref.uri)) return;
|
|
211
|
+
const item: LibraryItem = { ref, origin, addedAt: Date.now() };
|
|
212
|
+
if (label != null) item.label = label;
|
|
213
|
+
if (provenance?.prompt != null) item.prompt = provenance.prompt;
|
|
214
|
+
if (provenance?.model != null) item.model = provenance.model;
|
|
215
|
+
if (provenance?.groupId != null) item.groupId = provenance.groupId;
|
|
216
|
+
if (provenance?.genKind != null) item.genKind = provenance.genKind;
|
|
217
|
+
cache = [...current, item];
|
|
218
|
+
writeStorage(cache);
|
|
219
|
+
emit();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Merge generation provenance into an EXISTING library item (matched by `ref.uri`).
|
|
224
|
+
* Additive back-fill for provenance discovered AFTER the ref was first added: the
|
|
225
|
+
* studio clip catalog adds each produced clip on its list poll (keyed by uri, but the
|
|
226
|
+
* clips-list DTO carries no prompt), then fills the REAL prompt/model in from the
|
|
227
|
+
* per-clip detail fetch — this is that seam. Only writes a field the item does not
|
|
228
|
+
* already carry (never clobbers a value captured at generation time), and no-ops if
|
|
229
|
+
* the uri is absent or nothing would change. Never touches server assets.
|
|
230
|
+
*/
|
|
231
|
+
export function updateLibraryProvenance(
|
|
232
|
+
uri: string,
|
|
233
|
+
provenance: LibraryProvenance,
|
|
234
|
+
): void {
|
|
235
|
+
if (typeof uri !== "string" || !provenance) return;
|
|
236
|
+
const current = ensureLoaded();
|
|
237
|
+
const idx = current.findIndex((it) => it.ref.uri === uri);
|
|
238
|
+
if (idx < 0) return;
|
|
239
|
+
const item = current[idx];
|
|
240
|
+
const next: LibraryItem = { ...item };
|
|
241
|
+
let changed = false;
|
|
242
|
+
if (provenance.prompt != null && item.prompt == null) {
|
|
243
|
+
next.prompt = provenance.prompt;
|
|
244
|
+
changed = true;
|
|
245
|
+
}
|
|
246
|
+
if (provenance.model != null && item.model == null) {
|
|
247
|
+
next.model = provenance.model;
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
if (provenance.groupId != null && item.groupId == null) {
|
|
251
|
+
next.groupId = provenance.groupId;
|
|
252
|
+
changed = true;
|
|
253
|
+
}
|
|
254
|
+
if (provenance.genKind != null && item.genKind == null) {
|
|
255
|
+
next.genKind = provenance.genKind;
|
|
256
|
+
changed = true;
|
|
257
|
+
}
|
|
258
|
+
if (!changed) return;
|
|
259
|
+
cache = [...current.slice(0, idx), next, ...current.slice(idx + 1)];
|
|
260
|
+
writeStorage(cache);
|
|
261
|
+
emit();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove a SINGLE library entry by its `ref.uri`, PERMANENTLY for the session:
|
|
266
|
+
* the uri is tombstoned so no re-populator (jobTracker terminal push / studio-clips
|
|
267
|
+
* poll) can ever bring it back — a removal that repopulates isn't a removal. Does
|
|
268
|
+
* NOT itself delete the underlying server asset (house policy: no deletion; studio
|
|
269
|
+
* clips archive server-side in the row handler). Tombstone is recorded even when
|
|
270
|
+
* the row is momentarily absent, so a remove that races a re-add still sticks.
|
|
271
|
+
*/
|
|
272
|
+
export function removeFromLibrary(uri: string): void {
|
|
273
|
+
if (typeof uri !== "string") return;
|
|
274
|
+
tombstone(uri); // record the removal FIRST so a concurrent re-add can't win.
|
|
275
|
+
const current = ensureLoaded();
|
|
276
|
+
const next = current.filter((it) => it.ref.uri !== uri);
|
|
277
|
+
if (next.length === current.length) {
|
|
278
|
+
emit(); // not in the list, but the tombstone changed — let subscribers re-render.
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
cache = next;
|
|
282
|
+
writeStorage(cache);
|
|
283
|
+
emit();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Empty the SESSION library list. Tombstones every current uri so the studio-clips
|
|
288
|
+
* poll (and any terminal re-push) can't repopulate what was just cleared — "Clear
|
|
289
|
+
* all" that refills on the next tick isn't clearing. Does NOT delete server assets
|
|
290
|
+
* (house policy: no deletion).
|
|
291
|
+
*/
|
|
292
|
+
export function clearLibrary(): void {
|
|
293
|
+
for (const it of ensureLoaded()) tombstone(it.ref.uri);
|
|
294
|
+
cache = [];
|
|
295
|
+
try {
|
|
296
|
+
sessionStorage.removeItem(storageKey());
|
|
297
|
+
} catch {
|
|
298
|
+
// ignore — cache is already cleared for this session.
|
|
299
|
+
}
|
|
300
|
+
emit();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Bind the cross-tab storage listener once. sessionStorage is per-tab so this
|
|
304
|
+
// mainly future-proofs the surface, but if the key ever changes underneath us we
|
|
305
|
+
// re-sync the cache and notify rather than serving a stale list.
|
|
306
|
+
function bindStorageOnce(): void {
|
|
307
|
+
if (storageBound || typeof window === "undefined") return;
|
|
308
|
+
storageBound = true;
|
|
309
|
+
window.addEventListener("storage", (e) => {
|
|
310
|
+
if (e.key !== null && e.key !== storageKey()) return;
|
|
311
|
+
cache = readStorage();
|
|
312
|
+
emit();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Subscribe to add/clear (and cross-tab storage) changes. Returns an unsubscribe. */
|
|
317
|
+
export function subscribeLibrary(listener: () => void): () => void {
|
|
318
|
+
listeners.add(listener);
|
|
319
|
+
bindStorageOnce();
|
|
320
|
+
return () => {
|
|
321
|
+
listeners.delete(listener);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** React hook: the live library list, re-rendering on every add/clear. */
|
|
326
|
+
export function useMediaLibrary(): LibraryItem[] {
|
|
327
|
+
return useSyncExternalStore(subscribeLibrary, getLibrary, getLibrary);
|
|
328
|
+
}
|