@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,334 @@
|
|
|
1
|
+
// StudioItemCard — the first VISIBLE piece of the Studio redesign (slice 2b). It
|
|
2
|
+
// renders a first-class StudioItem inline with its own per-item OPS, so a staged
|
|
3
|
+
// studio item is no longer a loose `source` ref but a card that shows explicit,
|
|
4
|
+
// kind-gated operations (Image Crop / Video Crop / Audio Crop / Frames & Models)
|
|
5
|
+
// which either MUTATE the item in place or BRANCH a new child item.
|
|
6
|
+
//
|
|
7
|
+
// The card reuses the slice-2a op "cores" verbatim (ImageCropCore / VideoCropCore /
|
|
8
|
+
// AudioCropCore / FrameExtractCore): selecting an op opens the matching Core in an
|
|
9
|
+
// inline drawer, wired via each Core's additive `onProduced` sink. The card contains
|
|
10
|
+
// NO composer (prompt / negative / parts) — that is slice 3's per-item composer,
|
|
11
|
+
// deliberately kept out here to bound the recursion. Ops only.
|
|
12
|
+
//
|
|
13
|
+
// The DAG rule (routing): an op whose PRODUCED media kind equals the item's working
|
|
14
|
+
// kind (image item + Image Crop; video item + Video Crop; audio item + Audio Crop) is
|
|
15
|
+
// SAME-KIND → onApplyOp replaces the parent's `working`. An op that CHANGES the kind
|
|
16
|
+
// or FANS OUT (video item + Audio Crop → audio; video item + Frames → images) →
|
|
17
|
+
// onSpawn mints NEW child items and never overwrites the parent's `working` (that
|
|
18
|
+
// would corrupt its kind).
|
|
19
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
20
|
+
import { mediaBytesUrl } from "../../config";
|
|
21
|
+
import type { MediaRef } from "../../video/contract";
|
|
22
|
+
import type { SpatialRegion, TemporalRegion } from "../../regions/types";
|
|
23
|
+
import { getJobs, type StationId } from "../../video/jobTracker";
|
|
24
|
+
import { updateLibraryProvenance } from "../../video/mediaLibrary";
|
|
25
|
+
import type { StudioItem, StudioItemOp } from "./studioItem";
|
|
26
|
+
import { ImageCropCore } from "../ops/ImageCropCore";
|
|
27
|
+
import { VideoCropCore } from "../ops/VideoCropCore";
|
|
28
|
+
import { AudioCropCore } from "../ops/AudioCropCore";
|
|
29
|
+
import { FrameExtractCore } from "../ops/FrameExtractCore";
|
|
30
|
+
|
|
31
|
+
type OpKind = StudioItemOp["kind"];
|
|
32
|
+
|
|
33
|
+
/** A produced ref + the axes the op ran over — the shape every Core's onProduced
|
|
34
|
+
* hands us (the frame sink omits `meta`, which is why it is optional). */
|
|
35
|
+
type ProducedMeta = { spatial?: SpatialRegion | null; temporal?: TemporalRegion | null };
|
|
36
|
+
type ProducedSink = (op: OpKind, ref: MediaRef, meta?: ProducedMeta) => void;
|
|
37
|
+
|
|
38
|
+
// Fresh op id — mirrors the arm's monotonic-counter + base-36-timestamp idiom
|
|
39
|
+
// (studioItem.ts itemKey / useCropJobs.makeKey), unique within a session with no
|
|
40
|
+
// new dependency.
|
|
41
|
+
let _opSeq = 0;
|
|
42
|
+
function opKey(): string {
|
|
43
|
+
_opSeq += 1;
|
|
44
|
+
return `op_${_opSeq.toString(36)}_${Date.now().toString(36)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The media kind an op PRODUCES — the DAG routing pivot. When it equals the item's
|
|
48
|
+
// working kind the op mutates in place (same-kind); otherwise it branches a child.
|
|
49
|
+
function producedKind(op: OpKind): string {
|
|
50
|
+
switch (op) {
|
|
51
|
+
case "image_crop":
|
|
52
|
+
return "image";
|
|
53
|
+
case "video_crop":
|
|
54
|
+
return "video";
|
|
55
|
+
case "audio_crop":
|
|
56
|
+
return "audio";
|
|
57
|
+
case "frame_extract":
|
|
58
|
+
return "image";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Which tracker station each op enqueues on — used to SEED the re-emit guard below.
|
|
63
|
+
const STATION_FOR_OP: Record<OpKind, StationId> = {
|
|
64
|
+
image_crop: "image-crop",
|
|
65
|
+
video_crop: "video-crop",
|
|
66
|
+
audio_crop: "audio-crop",
|
|
67
|
+
frame_extract: "frames",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const OP_LABEL: Record<OpKind, string> = {
|
|
71
|
+
image_crop: "Image Crop",
|
|
72
|
+
video_crop: "Video Crop",
|
|
73
|
+
audio_crop: "Audio Crop",
|
|
74
|
+
frame_extract: "Frames & Models",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// The ops offered for a given working kind (the menu gate).
|
|
78
|
+
// image → Image Crop · video → Video Crop / Audio Crop / Frames · audio → Audio Crop
|
|
79
|
+
function opsFor(kind: string): OpKind[] {
|
|
80
|
+
if (kind === "image") return ["image_crop"];
|
|
81
|
+
if (kind === "video") return ["video_crop", "audio_crop", "frame_extract"];
|
|
82
|
+
if (kind === "audio") return ["audio_crop"];
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Last path segment of a uri, for the compact (immutable) provenance line.
|
|
87
|
+
function shortUri(uri: string): string {
|
|
88
|
+
const clean = uri.split(/[?#]/)[0];
|
|
89
|
+
const seg = clean.split("/").filter(Boolean).pop() ?? uri;
|
|
90
|
+
return seg.length > 40 ? `…${seg.slice(-39)}` : seg;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Poster/preview for the item's working ref, per kind — the arm's inline idiom (a
|
|
94
|
+
// muted seeked <video>, an <img>, or an <audio>), not the non-exported StudioGenerateTab
|
|
95
|
+
// SourceThumb (kept private to that surface).
|
|
96
|
+
function renderMedia(ref: MediaRef): ReactNode {
|
|
97
|
+
const src = mediaBytesUrl(ref.uri);
|
|
98
|
+
if (ref.kind === "image") {
|
|
99
|
+
return <img className="vi-studio-item-img" src={src} alt="Studio item" />;
|
|
100
|
+
}
|
|
101
|
+
if (ref.kind === "video") {
|
|
102
|
+
return (
|
|
103
|
+
<video
|
|
104
|
+
className="vi-studio-item-img"
|
|
105
|
+
src={`${src}#t=0.1`}
|
|
106
|
+
muted
|
|
107
|
+
playsInline
|
|
108
|
+
controls
|
|
109
|
+
preload="metadata"
|
|
110
|
+
aria-label="Studio item preview"
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (ref.kind === "audio") {
|
|
115
|
+
return (
|
|
116
|
+
<audio
|
|
117
|
+
className="vi-studio-item-audio"
|
|
118
|
+
src={src}
|
|
119
|
+
controls
|
|
120
|
+
preload="metadata"
|
|
121
|
+
aria-label="Studio item preview"
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return <span className="vi-crop-hint">No preview for {ref.kind}.</span>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The immutable "where it came from" line — read-only, never editable.
|
|
129
|
+
function ProvenanceLine({ item }: { item: StudioItem }) {
|
|
130
|
+
const { provenance } = item;
|
|
131
|
+
return (
|
|
132
|
+
<div className="vi-studio-item-prov" aria-label="Provenance (immutable)">
|
|
133
|
+
<span className="vi-studio-item-prov-origin">{provenance.origin}</span>
|
|
134
|
+
<code className="vi-studio-item-prov-kind">{item.working.kind}</code>
|
|
135
|
+
{provenance.parentUri && (
|
|
136
|
+
<span className="vi-studio-item-prov-parent" title={provenance.parentUri}>
|
|
137
|
+
← {shortUri(provenance.parentUri)}
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
{provenance.model && (
|
|
141
|
+
<span
|
|
142
|
+
className="vi-studio-item-prov-model"
|
|
143
|
+
title={`model: ${provenance.model}`}
|
|
144
|
+
>
|
|
145
|
+
· {provenance.model}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Mount the matching Core in the drawer. Each arrow's `meta` is contextually typed
|
|
153
|
+
// from the Core's own onProduced prop, so the axes flow through with their exact
|
|
154
|
+
// shape; the crop cores run SINGLE-region (multiRegion={false}) inline-on-item.
|
|
155
|
+
function renderCore(op: OpKind, working: MediaRef, onProduced: ProducedSink): ReactNode {
|
|
156
|
+
switch (op) {
|
|
157
|
+
case "image_crop":
|
|
158
|
+
return (
|
|
159
|
+
<ImageCropCore
|
|
160
|
+
source={working}
|
|
161
|
+
multiRegion={false}
|
|
162
|
+
onProduced={(ref, meta) => onProduced(op, ref, meta)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
case "video_crop":
|
|
166
|
+
return (
|
|
167
|
+
<VideoCropCore
|
|
168
|
+
source={working}
|
|
169
|
+
onProduced={(ref, meta) => onProduced(op, ref, meta)}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
case "audio_crop":
|
|
173
|
+
return (
|
|
174
|
+
<AudioCropCore
|
|
175
|
+
source={working}
|
|
176
|
+
multiRegion={false}
|
|
177
|
+
onProduced={(ref, meta) => onProduced(op, ref, meta)}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
case "frame_extract":
|
|
181
|
+
return (
|
|
182
|
+
<FrameExtractCore
|
|
183
|
+
source={working}
|
|
184
|
+
busy={false}
|
|
185
|
+
addFile={null}
|
|
186
|
+
onProduced={(ref) => onProduced(op, ref)}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface StudioItemCardProps {
|
|
193
|
+
item: StudioItem;
|
|
194
|
+
/** same-kind op → parent working replaced (mutate in place). */
|
|
195
|
+
onApplyOp: (item: StudioItem, op: StudioItemOp) => void;
|
|
196
|
+
/** kind-change / fan-out → new child item(s) carrying parent provenance. */
|
|
197
|
+
onSpawn: (parent: StudioItem, produced: MediaRef[], kind: OpKind) => void;
|
|
198
|
+
onRevert: (item: StudioItem) => void;
|
|
199
|
+
onRemove: (item: StudioItem) => void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function StudioItemCard({
|
|
203
|
+
item,
|
|
204
|
+
onApplyOp,
|
|
205
|
+
onSpawn,
|
|
206
|
+
onRevert,
|
|
207
|
+
onRemove,
|
|
208
|
+
}: StudioItemCardProps) {
|
|
209
|
+
const [activeOp, setActiveOp] = useState<OpKind | null>(null);
|
|
210
|
+
|
|
211
|
+
// Per-card guard against the shared per-station tracker RE-EMITTING pre-existing
|
|
212
|
+
// done jobs. The crop cores fire onProduced from a useEffect over the tracker's
|
|
213
|
+
// rows for their station, so a freshly-mounted core (every drawer open) would
|
|
214
|
+
// re-notify EVERY historical done crop of that station — spawning phantom children
|
|
215
|
+
// or re-applying stale results. We remember each uri this card has already routed,
|
|
216
|
+
// and SEED that set at drawer-open with the station's current outputs, so only NEW
|
|
217
|
+
// completions route. (frame_extract's sink is pick-triggered, not job-triggered, so
|
|
218
|
+
// it is inherently immune; seeding it is harmless.)
|
|
219
|
+
const routedUris = useRef<Set<string>>(new Set());
|
|
220
|
+
|
|
221
|
+
const working = item.working;
|
|
222
|
+
const ops = opsFor(working.kind);
|
|
223
|
+
// Revert is meaningful only once an op has diverged `working` from the source.
|
|
224
|
+
const canRevert = working !== item.source;
|
|
225
|
+
|
|
226
|
+
function openOp(op: OpKind) {
|
|
227
|
+
const station = STATION_FOR_OP[op];
|
|
228
|
+
const seen = routedUris.current;
|
|
229
|
+
for (const rec of getJobs()) {
|
|
230
|
+
if (rec.station === station) {
|
|
231
|
+
for (const out of rec.outputs) seen.add(out.uri);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
setActiveOp(op);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The single onProduced sink the mounted core drives. Routes ONCE per uri.
|
|
238
|
+
function handleProduced(op: OpKind, ref: MediaRef, meta?: ProducedMeta) {
|
|
239
|
+
if (!ref || routedUris.current.has(ref.uri)) return;
|
|
240
|
+
routedUris.current.add(ref.uri);
|
|
241
|
+
|
|
242
|
+
// Light provenance enrichment: tie the produced library entry back to its parent
|
|
243
|
+
// item (groupId) and carry any inherited prompt/model. LibraryProvenance has no
|
|
244
|
+
// free "op kind" field (genKind is a closed generation union), so the op kind
|
|
245
|
+
// itself is recorded on the StudioItemOp / the spawned child's provenance instead.
|
|
246
|
+
updateLibraryProvenance(ref.uri, {
|
|
247
|
+
groupId: item.key,
|
|
248
|
+
...(item.provenance.prompt != null ? { prompt: item.provenance.prompt } : {}),
|
|
249
|
+
...(item.provenance.model != null ? { model: item.provenance.model } : {}),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const sameKind = op !== "frame_extract" && producedKind(op) === working.kind;
|
|
253
|
+
if (sameKind) {
|
|
254
|
+
onApplyOp(item, {
|
|
255
|
+
id: opKey(),
|
|
256
|
+
kind: op,
|
|
257
|
+
at: Date.now(),
|
|
258
|
+
spatial: meta?.spatial ?? null,
|
|
259
|
+
temporal: meta?.temporal ?? null,
|
|
260
|
+
result: ref,
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
onSpawn(item, [ref], op);
|
|
264
|
+
}
|
|
265
|
+
// The op resolved — close the drawer.
|
|
266
|
+
setActiveOp(null);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className="vi-studio-item">
|
|
271
|
+
<div className="vi-studio-item-body">
|
|
272
|
+
<div className="vi-studio-item-media">{renderMedia(working)}</div>
|
|
273
|
+
<div className="vi-studio-item-meta">
|
|
274
|
+
<ProvenanceLine item={item} />
|
|
275
|
+
<div
|
|
276
|
+
className="vi-studio-item-ops"
|
|
277
|
+
role="group"
|
|
278
|
+
aria-label="Item operations"
|
|
279
|
+
>
|
|
280
|
+
{ops.map((op) => (
|
|
281
|
+
<button
|
|
282
|
+
key={op}
|
|
283
|
+
type="button"
|
|
284
|
+
className={
|
|
285
|
+
activeOp === op
|
|
286
|
+
? "vi-btn vi-btn-sm vi-btn-accent"
|
|
287
|
+
: "vi-btn vi-btn-sm"
|
|
288
|
+
}
|
|
289
|
+
aria-pressed={activeOp === op}
|
|
290
|
+
onClick={() => (activeOp === op ? setActiveOp(null) : openOp(op))}
|
|
291
|
+
>
|
|
292
|
+
{OP_LABEL[op]}
|
|
293
|
+
</button>
|
|
294
|
+
))}
|
|
295
|
+
{canRevert && (
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
299
|
+
onClick={() => onRevert(item)}
|
|
300
|
+
title="Restore the working ref back to the immutable source"
|
|
301
|
+
>
|
|
302
|
+
Revert to source
|
|
303
|
+
</button>
|
|
304
|
+
)}
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
308
|
+
onClick={() => onRemove(item)}
|
|
309
|
+
title="Remove this item from the studio (forgets the local card only)"
|
|
310
|
+
>
|
|
311
|
+
Remove
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{activeOp && (
|
|
318
|
+
<div className="vi-studio-item-drawer">
|
|
319
|
+
<div className="vi-studio-item-drawer-head">
|
|
320
|
+
<span className="vi-studio-item-drawer-title">{OP_LABEL[activeOp]}</span>
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
324
|
+
onClick={() => setActiveOp(null)}
|
|
325
|
+
>
|
|
326
|
+
Close
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
{renderCore(activeOp, working, handleProduced)}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|