@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,262 @@
|
|
|
1
|
+
// IDENTITY PROFILES — the shared "Identity" control block both studio surfaces
|
|
2
|
+
// (the Clip Generate tab + the Movie composer) embed as ONE additive row in their
|
|
3
|
+
// existing identity/reference area. It does NOT re-draw the reference thumbnails —
|
|
4
|
+
// the host surface's own chip UI already shows whatever is attached; this block adds
|
|
5
|
+
// only the profile affordances on top:
|
|
6
|
+
//
|
|
7
|
+
// • "👤 From profile" — a picker of saved profiles (name + first-ref thumbnail);
|
|
8
|
+
// picking one RESOLVES its saved paths back to MediaRefs and hands them to the
|
|
9
|
+
// host (replace-semantics: a profile IS the whole identity), and binds the slug.
|
|
10
|
+
// • "💾 Save as identity profile" — enabled once ≥1 reference is attached and the
|
|
11
|
+
// current identity is UNSAVED; prompts for a name (window.prompt, v0) and POSTs.
|
|
12
|
+
//
|
|
13
|
+
// DOCTRINE (operator 2026-07-12): present ONE concept — "Identity" — backed by
|
|
14
|
+
// profiles. A chosen profile shows as a named, saved identity; ad-hoc uploads are an
|
|
15
|
+
// honest "unsaved identity" with the save affordance (the incomplete case is STILL
|
|
16
|
+
// the identity, just unnamed). The host prefers sending `identity_profile:<slug>` when
|
|
17
|
+
// a profile is bound; raw reference_images only for the unsaved case. (Stage (b) —
|
|
18
|
+
// turnaround generation + the re-edit loop that promotes an approved rendering to the
|
|
19
|
+
// canonical reference — layers on top of this next.)
|
|
20
|
+
import { useState, type MouseEvent } from "react";
|
|
21
|
+
import { mediaBytesUrl } from "../../config";
|
|
22
|
+
import type { MediaRef } from "../../video/contract";
|
|
23
|
+
import { useIdentityProfiles } from "./useIdentityProfiles";
|
|
24
|
+
import type { IdentityProfile } from "./useIdentityProfiles";
|
|
25
|
+
|
|
26
|
+
/** The profile currently bound as the identity (or null for an unsaved/empty one). */
|
|
27
|
+
export interface SelectedProfile {
|
|
28
|
+
slug: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IdentityProfileControlsProps {
|
|
33
|
+
/** How many identity references are attached right now (gates Save + status copy). */
|
|
34
|
+
refCount: number;
|
|
35
|
+
/** The raw jailed URIs of the attached references — what Save persists. */
|
|
36
|
+
currentRefUris: string[];
|
|
37
|
+
/** The bound profile, if the current identity came from one (else null = unsaved). */
|
|
38
|
+
selectedProfile: SelectedProfile | null;
|
|
39
|
+
/** Attach a profile's resolved reference set into the host's OWN reference state.
|
|
40
|
+
* `replace` is true for a profile pick (the profile is the whole identity). */
|
|
41
|
+
onAttach: (refs: MediaRef[], replace: boolean) => void;
|
|
42
|
+
/** Bind (or clear, with null) the profile slug the host sends as identity_profile. */
|
|
43
|
+
onSelectProfile: (p: SelectedProfile | null) => void;
|
|
44
|
+
/** Upper bound on references (4) — advisory copy only. */
|
|
45
|
+
maxRefs: number;
|
|
46
|
+
/** Disable all affordances (host is busy / submitting). */
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function IdentityProfileControls({
|
|
51
|
+
refCount,
|
|
52
|
+
currentRefUris,
|
|
53
|
+
selectedProfile,
|
|
54
|
+
onAttach,
|
|
55
|
+
onSelectProfile,
|
|
56
|
+
maxRefs,
|
|
57
|
+
disabled,
|
|
58
|
+
}: IdentityProfileControlsProps) {
|
|
59
|
+
const { profiles, loading, error, create, remove, resolveRefs } = useIdentityProfiles();
|
|
60
|
+
const [open, setOpen] = useState(false);
|
|
61
|
+
const [busy, setBusy] = useState(false);
|
|
62
|
+
const [msg, setMsg] = useState<string | null>(null);
|
|
63
|
+
|
|
64
|
+
const canSave = refCount > 0 && selectedProfile == null && !disabled && !busy;
|
|
65
|
+
|
|
66
|
+
async function pick(profile: IdentityProfile) {
|
|
67
|
+
if (disabled || busy) return;
|
|
68
|
+
setBusy(true);
|
|
69
|
+
setMsg(null);
|
|
70
|
+
try {
|
|
71
|
+
const refs = await resolveRefs(profile);
|
|
72
|
+
if (refs.length === 0) {
|
|
73
|
+
setMsg("Could not load that profile's reference images.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// A profile IS the identity — replace the current set with the profile's.
|
|
77
|
+
onAttach(refs.slice(0, maxRefs), true);
|
|
78
|
+
onSelectProfile({ slug: profile.slug, name: profile.name });
|
|
79
|
+
setOpen(false);
|
|
80
|
+
setMsg(`Identity set to “${profile.name}”.`);
|
|
81
|
+
} finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function save() {
|
|
87
|
+
if (!canSave) return;
|
|
88
|
+
const name = window.prompt("Name this identity profile:");
|
|
89
|
+
if (name == null) return; // cancelled
|
|
90
|
+
const trimmed = name.trim();
|
|
91
|
+
if (!trimmed) {
|
|
92
|
+
setMsg("A name is required to save an identity profile.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setBusy(true);
|
|
96
|
+
setMsg(null);
|
|
97
|
+
try {
|
|
98
|
+
const res = await create(trimmed, currentRefUris);
|
|
99
|
+
if (res.ok && res.profile) {
|
|
100
|
+
// The just-saved profile becomes the bound identity (its refs are already
|
|
101
|
+
// attached), flipping the surface from "unsaved" to a named profile.
|
|
102
|
+
onSelectProfile({ slug: res.profile.slug, name: res.profile.name });
|
|
103
|
+
setMsg(`Saved identity profile “${res.profile.name}”.`);
|
|
104
|
+
} else if (res.duplicate) {
|
|
105
|
+
setMsg(`An identity profile named “${trimmed}” already exists — pick it from 👤 From profile, or choose another name.`);
|
|
106
|
+
} else {
|
|
107
|
+
setMsg(res.message || "Could not save the identity profile.");
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy(false);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function archive(profile: IdentityProfile, e: MouseEvent) {
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
if (disabled || busy) return;
|
|
117
|
+
if (!window.confirm(`Archive identity profile “${profile.name}”? (It is kept, not erased.)`)) return;
|
|
118
|
+
setBusy(true);
|
|
119
|
+
try {
|
|
120
|
+
await remove(profile.slug);
|
|
121
|
+
if (selectedProfile?.slug === profile.slug) onSelectProfile(null);
|
|
122
|
+
} finally {
|
|
123
|
+
setBusy(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div style={{ marginTop: "0.4rem" }}>
|
|
129
|
+
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
133
|
+
disabled={disabled || busy}
|
|
134
|
+
aria-expanded={open}
|
|
135
|
+
onClick={() => setOpen((v) => !v)}
|
|
136
|
+
title="Attach a saved identity profile's reference set"
|
|
137
|
+
>
|
|
138
|
+
{open ? "Hide profiles" : `👤 From profile${profiles.length ? ` (${profiles.length})` : ""}`}
|
|
139
|
+
</button>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
143
|
+
disabled={!canSave}
|
|
144
|
+
onClick={() => void save()}
|
|
145
|
+
title={
|
|
146
|
+
refCount === 0
|
|
147
|
+
? "Attach at least one reference image first"
|
|
148
|
+
: selectedProfile != null
|
|
149
|
+
? "This identity is already a saved profile"
|
|
150
|
+
: "Save these references as a reusable identity profile"
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
💾 Save as identity profile
|
|
154
|
+
</button>
|
|
155
|
+
{selectedProfile != null ? (
|
|
156
|
+
<span className="vi-comfy-hint" role="status" style={{ opacity: 0.85 }}>
|
|
157
|
+
Identity: <strong>{selectedProfile.name}</strong> — saved profile ✓
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
161
|
+
disabled={disabled || busy}
|
|
162
|
+
onClick={() => onSelectProfile(null)}
|
|
163
|
+
title="Unbind the profile (keep the images as an unsaved identity)"
|
|
164
|
+
style={{ marginLeft: "0.4rem", padding: "0 0.35rem" }}
|
|
165
|
+
>
|
|
166
|
+
unbind
|
|
167
|
+
</button>
|
|
168
|
+
</span>
|
|
169
|
+
) : refCount > 0 ? (
|
|
170
|
+
<span className="vi-comfy-hint" role="status" style={{ opacity: 0.75 }}>
|
|
171
|
+
Unsaved identity — save it to reuse across clips & movies.
|
|
172
|
+
</span>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{open && (
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
display: "flex",
|
|
180
|
+
flexWrap: "wrap",
|
|
181
|
+
gap: "0.5rem",
|
|
182
|
+
marginTop: "0.5rem",
|
|
183
|
+
maxHeight: "12rem",
|
|
184
|
+
overflowY: "auto",
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{loading ? (
|
|
188
|
+
<p className="vi-comfy-hint">Loading identity profiles…</p>
|
|
189
|
+
) : error ? (
|
|
190
|
+
<p className="vi-error" role="alert">{error}</p>
|
|
191
|
+
) : profiles.length === 0 ? (
|
|
192
|
+
<p className="vi-comfy-hint">
|
|
193
|
+
No saved identity profiles yet — attach reference image(s) and press
|
|
194
|
+
“💾 Save as identity profile” to create one.
|
|
195
|
+
</p>
|
|
196
|
+
) : (
|
|
197
|
+
profiles.map((p) => (
|
|
198
|
+
<button
|
|
199
|
+
key={p.slug}
|
|
200
|
+
type="button"
|
|
201
|
+
className="vi-btn vi-btn-ghost"
|
|
202
|
+
disabled={disabled || busy}
|
|
203
|
+
onClick={() => void pick(p)}
|
|
204
|
+
title={`Use “${p.name}” (${p.reference_images.length} reference${
|
|
205
|
+
p.reference_images.length === 1 ? "" : "s"
|
|
206
|
+
})`}
|
|
207
|
+
style={{
|
|
208
|
+
display: "flex",
|
|
209
|
+
flexDirection: "column",
|
|
210
|
+
alignItems: "center",
|
|
211
|
+
gap: "0.2rem",
|
|
212
|
+
padding: "0.3rem",
|
|
213
|
+
position: "relative",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<img
|
|
217
|
+
src={mediaBytesUrl(p.reference_images[0])}
|
|
218
|
+
alt={p.name}
|
|
219
|
+
style={{
|
|
220
|
+
width: "4.5rem",
|
|
221
|
+
height: "4.5rem",
|
|
222
|
+
objectFit: "cover",
|
|
223
|
+
borderRadius: "0.35rem",
|
|
224
|
+
background: "#000",
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
<span style={{ fontSize: "0.75rem", maxWidth: "5rem", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
228
|
+
{p.name}
|
|
229
|
+
</span>
|
|
230
|
+
<span
|
|
231
|
+
role="button"
|
|
232
|
+
tabIndex={-1}
|
|
233
|
+
aria-label={`Archive ${p.name}`}
|
|
234
|
+
onClick={(e) => void archive(p, e)}
|
|
235
|
+
title="Archive (kept, not erased)"
|
|
236
|
+
style={{
|
|
237
|
+
position: "absolute",
|
|
238
|
+
top: -4,
|
|
239
|
+
right: -4,
|
|
240
|
+
background: "var(--vi-border)",
|
|
241
|
+
borderRadius: "0.3rem",
|
|
242
|
+
padding: "0 0.3rem",
|
|
243
|
+
fontSize: "0.72rem",
|
|
244
|
+
lineHeight: 1.4,
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
✕
|
|
248
|
+
</span>
|
|
249
|
+
</button>
|
|
250
|
+
))
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{msg && (
|
|
256
|
+
<p className="vi-comfy-hint" role="status" style={{ marginTop: "0.3rem" }}>
|
|
257
|
+
{msg}
|
|
258
|
+
</p>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// ACTIVE PROCESSES tab — the in-flight STUDIO jobs (queued / claimed / running) as a
|
|
2
|
+
// focused live panel. Each row is EXPANDABLE into the exhaustive per-process view:
|
|
3
|
+
// the stage TIMELINE (what it did + what it's doing now), the current-activity line,
|
|
4
|
+
// an honest stall indicator tied to the current stage, and — on failure — the exact
|
|
5
|
+
// failing stage + error code + message. This replaces the old "expands to a loading
|
|
6
|
+
// bar that never populates" with real, server-sent detail (no fabricated bars).
|
|
7
|
+
//
|
|
8
|
+
// SCOPE (operator, 2026-07-31): this panel is fed by GET /video/studio/clips, which is
|
|
9
|
+
// `WHERE name='studio_i2v'` — so it lists ONLY studio-originated renders, never general
|
|
10
|
+
// fleet model calls. Cancel here therefore only ever stops a studio render.
|
|
11
|
+
//
|
|
12
|
+
// The GET /video/studio/clips route projects `progress` (incl. the reservation
|
|
13
|
+
// awaiting_capacity HOLD marker), a `placement` object, and the new stage-timeline
|
|
14
|
+
// telemetry (stage_log / failure / current_stage / last_movement_ts). Terminal rows
|
|
15
|
+
// (done / failed / cancelled) stay in the Library tab, where their full timeline +
|
|
16
|
+
// failure detail is inspectable via the Details expander.
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
18
|
+
import { request } from "../../transport/client";
|
|
19
|
+
import { jobCancelUrl } from "../../config";
|
|
20
|
+
import {
|
|
21
|
+
shortId,
|
|
22
|
+
ProcessTimeline,
|
|
23
|
+
isStalled,
|
|
24
|
+
type Clip,
|
|
25
|
+
type StageEntry,
|
|
26
|
+
type JobFailure,
|
|
27
|
+
} from "./studioShared";
|
|
28
|
+
import { placementLine, isExternal, reservedGib, type Placement } from "../../video/useMediaJobs";
|
|
29
|
+
|
|
30
|
+
const IN_FLIGHT = new Set(["queued", "claimed", "running", "cancelling"]);
|
|
31
|
+
const TERMINAL = new Set(["done", "failed", "cancelled"]);
|
|
32
|
+
|
|
33
|
+
function elapsed(c: Clip): string {
|
|
34
|
+
const t = c.created ?? c.updated;
|
|
35
|
+
if (t == null) return "";
|
|
36
|
+
const secs = Math.max(0, Math.floor(Date.now() / 1000 - t));
|
|
37
|
+
if (secs < 60) return `${secs}s`;
|
|
38
|
+
const m = Math.floor(secs / 60);
|
|
39
|
+
return `${m}m ${secs % 60}s`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The clip's `placement` rides the wire in snake_case; adapt it to the camelCase
|
|
43
|
+
// Placement the shared render helpers expect. null when the clip carries none.
|
|
44
|
+
function clipPlacement(c: Clip): Placement | null {
|
|
45
|
+
const p = (c as { placement?: Record<string, unknown> | null }).placement;
|
|
46
|
+
if (!p || typeof p !== "object") return null;
|
|
47
|
+
const s = (v: unknown) => (typeof v === "string" && v ? v : null);
|
|
48
|
+
const n = (v: unknown) => (typeof v === "number" && isFinite(v) ? v : null);
|
|
49
|
+
return {
|
|
50
|
+
source: s(p.source),
|
|
51
|
+
host: s(p.host),
|
|
52
|
+
workerId: s(p.worker_id),
|
|
53
|
+
gpu: s(p.gpu),
|
|
54
|
+
process: s(p.process),
|
|
55
|
+
reservedBytes: n(p.reserved_bytes),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The effective phase chip: the reservation awaiting_capacity HOLD overrides the
|
|
60
|
+
// raw status (the clip's progress blob carries the marker), else the raw status.
|
|
61
|
+
function clipPhase(c: Clip): string {
|
|
62
|
+
const prog = (c as { progress?: Record<string, unknown> | null }).progress;
|
|
63
|
+
if (prog && typeof prog === "object" && prog.phase === "awaiting_capacity")
|
|
64
|
+
return "awaiting_capacity";
|
|
65
|
+
return c.status ?? "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function StudioActiveProcesses({
|
|
69
|
+
clips,
|
|
70
|
+
refresh,
|
|
71
|
+
}: {
|
|
72
|
+
clips: Clip[];
|
|
73
|
+
refresh: () => void;
|
|
74
|
+
}) {
|
|
75
|
+
const [cancelBusy, setCancelBusy] = useState<Set<string>>(new Set());
|
|
76
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
77
|
+
// A ~1s tick so the stall clock + "since last movement" advance live (independent
|
|
78
|
+
// of the 6s clip poll), without any fabricated progress.
|
|
79
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
80
|
+
const mounted = useRef(true);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
mounted.current = true;
|
|
83
|
+
const iv = window.setInterval(() => setNowMs(Date.now()), 1000);
|
|
84
|
+
return () => {
|
|
85
|
+
mounted.current = false;
|
|
86
|
+
window.clearInterval(iv);
|
|
87
|
+
};
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const toggle = useCallback((jobId: string) => {
|
|
91
|
+
setExpanded((prev) => {
|
|
92
|
+
const n = new Set(prev);
|
|
93
|
+
if (n.has(jobId)) n.delete(jobId);
|
|
94
|
+
else n.add(jobId);
|
|
95
|
+
return n;
|
|
96
|
+
});
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const onCancel = useCallback(
|
|
100
|
+
async (jobId: string) => {
|
|
101
|
+
setCancelBusy((prev) => new Set(prev).add(jobId));
|
|
102
|
+
try {
|
|
103
|
+
await request<unknown>(jobCancelUrl(jobId), {
|
|
104
|
+
method: "POST",
|
|
105
|
+
meta: { specKey: "studio", operation: "studio.clip.cancel" },
|
|
106
|
+
});
|
|
107
|
+
if (!mounted.current) return;
|
|
108
|
+
refresh();
|
|
109
|
+
} finally {
|
|
110
|
+
if (mounted.current)
|
|
111
|
+
setCancelBusy((prev) => {
|
|
112
|
+
const n = new Set(prev);
|
|
113
|
+
n.delete(jobId);
|
|
114
|
+
return n;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
[refresh],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const active = clips.filter((c) => IN_FLIGHT.has(c.status ?? ""));
|
|
122
|
+
// Queue position: Nth among QUEUED jobs (oldest first) — a client-side, honest index.
|
|
123
|
+
const queuePos = new Map<string, number>();
|
|
124
|
+
active
|
|
125
|
+
.filter((c) => (c.status ?? "") === "queued")
|
|
126
|
+
.slice()
|
|
127
|
+
.sort((a, b) => (a.created ?? 0) - (b.created ?? 0))
|
|
128
|
+
.forEach((c, i) => queuePos.set(c.job_id, i + 1));
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="vi-studio-active">
|
|
132
|
+
<p className="vi-knob-headline">
|
|
133
|
+
Active renders{" "}
|
|
134
|
+
<span className="vi-knob-headline-sub">running & queued studio jobs</span>
|
|
135
|
+
</p>
|
|
136
|
+
{active.length === 0 ? (
|
|
137
|
+
<p className="vi-knob-hint">No active renders — enqueue one and it appears here.</p>
|
|
138
|
+
) : (
|
|
139
|
+
<ul className="vi-studio-active-list">
|
|
140
|
+
{active.map((c) => {
|
|
141
|
+
const phase = clipPhase(c);
|
|
142
|
+
const pos = queuePos.get(c.job_id);
|
|
143
|
+
const el = elapsed(c);
|
|
144
|
+
const pl = clipPlacement(c);
|
|
145
|
+
const line = placementLine(pl);
|
|
146
|
+
const gib = reservedGib(pl);
|
|
147
|
+
const external = isExternal(pl);
|
|
148
|
+
const phaseLabel =
|
|
149
|
+
phase === "awaiting_capacity" ? "awaiting capacity" : phase;
|
|
150
|
+
const isOpen = expanded.has(c.job_id);
|
|
151
|
+
const terminal = TERMINAL.has(c.status ?? "");
|
|
152
|
+
const stageLog = (c.stage_log ?? null) as StageEntry[] | null;
|
|
153
|
+
const failure = (c.failure ?? null) as JobFailure | null;
|
|
154
|
+
const stalled = isStalled(c.last_movement_ts, terminal, nowMs);
|
|
155
|
+
return (
|
|
156
|
+
<li key={c.job_id} className="vi-studio-active-row">
|
|
157
|
+
<div className="vi-studio-active-head">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
className="vi-timeline-toggle"
|
|
161
|
+
onClick={() => toggle(c.job_id)}
|
|
162
|
+
aria-expanded={isOpen}
|
|
163
|
+
title={isOpen ? "Hide detail" : "Show what it's doing"}
|
|
164
|
+
>
|
|
165
|
+
<span className="vi-timeline-caret" aria-hidden>
|
|
166
|
+
{isOpen ? "▾" : "▸"}
|
|
167
|
+
</span>
|
|
168
|
+
</button>
|
|
169
|
+
<span
|
|
170
|
+
className={`vi-status vi-status-${phase}`}
|
|
171
|
+
title={
|
|
172
|
+
phase === "awaiting_capacity"
|
|
173
|
+
? "Held — waiting for GPU capacity"
|
|
174
|
+
: undefined
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
{phaseLabel}
|
|
178
|
+
</span>
|
|
179
|
+
{stalled ? <span className="vi-timeline-stall-badge">stalled</span> : null}
|
|
180
|
+
<span className="vi-studio-active-id">{shortId(c.job_id)}</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div className="vi-studio-active-meta">
|
|
183
|
+
{line ? (
|
|
184
|
+
<span
|
|
185
|
+
className={`vi-placement${external ? " vi-placement-external" : ""}`}
|
|
186
|
+
>
|
|
187
|
+
{external ? "⇄ " : ""}
|
|
188
|
+
{line}
|
|
189
|
+
{external ? " · external" : ""}
|
|
190
|
+
</span>
|
|
191
|
+
) : null}
|
|
192
|
+
{c.current_stage ? <span>· {c.current_stage.replace(/_/g, " ")}</span> : null}
|
|
193
|
+
{gib ? <span>· {gib}</span> : null}
|
|
194
|
+
{pos != null ? <span>· queue #{pos}</span> : null}
|
|
195
|
+
{el ? <span>· {el}</span> : null}
|
|
196
|
+
</div>
|
|
197
|
+
{isOpen ? (
|
|
198
|
+
<div className="vi-timeline-panel">
|
|
199
|
+
<ProcessTimeline
|
|
200
|
+
stageLog={stageLog}
|
|
201
|
+
failure={failure}
|
|
202
|
+
currentStage={c.current_stage}
|
|
203
|
+
lastMovementTs={c.last_movement_ts}
|
|
204
|
+
terminal={terminal}
|
|
205
|
+
nowMs={nowMs}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
) : null}
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
212
|
+
onClick={() => void onCancel(c.job_id)}
|
|
213
|
+
disabled={cancelBusy.has(c.job_id)}
|
|
214
|
+
>
|
|
215
|
+
{cancelBusy.has(c.job_id) ? "Cancelling…" : "✕ Cancel"}
|
|
216
|
+
</button>
|
|
217
|
+
</li>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</ul>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|