@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,417 @@
|
|
|
1
|
+
// CINEMA SESSIONS PANEL — "which of these movies is waiting on me", answered from
|
|
2
|
+
// the studio-movies root instead of from a job id that may be long gone.
|
|
3
|
+
//
|
|
4
|
+
// A movie is ONE bus job that can run for hours; when that job ended (cancelled,
|
|
5
|
+
// reaped, or lost with the browser tab) its rendered segments stayed on disk and
|
|
6
|
+
// became unreachable — real GPU hours with no way to see or continue them. This panel
|
|
7
|
+
// is the handle on those dirs: every session with its status, how far it got, what it
|
|
8
|
+
// has ALREADY rendered (each done segment playable inline, as it lands), and the two
|
|
9
|
+
// actions — Resume (re-enqueue the persisted spec; the clips are the checkpoint) and
|
|
10
|
+
// Pause (cooperative cancel + "parked, not abandoned").
|
|
11
|
+
//
|
|
12
|
+
// Idioms borrowed rather than reinvented: the row/chip/caret shapes are
|
|
13
|
+
// StudioActiveProcesses', the progress bar + latest-event line are the console-wide
|
|
14
|
+
// Active Processes renderers (useMediaJobs' `progressLabel`/`latestEvent`), and the
|
|
15
|
+
// list hook mirrors useStudioClips. Nothing here fabricates a number: a bar appears
|
|
16
|
+
// only when the server sent one, and a disabled Resume always says WHY.
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
import {
|
|
19
|
+
useMovieSessions,
|
|
20
|
+
segmentClipSrc,
|
|
21
|
+
movieMediaSrc,
|
|
22
|
+
isSessionLive,
|
|
23
|
+
resumeBlockedReason,
|
|
24
|
+
type MovieSession,
|
|
25
|
+
type MovieSegment,
|
|
26
|
+
} from "../../video/useMovieSessions";
|
|
27
|
+
import {
|
|
28
|
+
useMediaJobs,
|
|
29
|
+
progressLabel,
|
|
30
|
+
latestEvent,
|
|
31
|
+
} from "../../video/useMediaJobs";
|
|
32
|
+
import { shortId, agoText, fmtWhen } from "./studioShared";
|
|
33
|
+
|
|
34
|
+
// Segment statuses that mean "this one is finished" — the rest are pending (dimmed)
|
|
35
|
+
// or failed (error shown verbatim).
|
|
36
|
+
const SEG_DONE = new Set(["done", "resumed"]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The live pace of ONE in-flight movie job, from the bus feed the studio arm already
|
|
40
|
+
* polls: the folded 0..1 bar plus "segment 2/14 · step 18/32" and the freshest stage
|
|
41
|
+
* line. This is what makes a 14-segment render legible in its first minute instead of
|
|
42
|
+
* only when segment 1 lands.
|
|
43
|
+
*
|
|
44
|
+
* It owns its own `useMediaJobs` subscription and is therefore mounted ONLY while
|
|
45
|
+
* something is actually running (a live session row, or the composer's own render) —
|
|
46
|
+
* no in-flight movie, no poll. Shared by both call sites so there is one renderer for
|
|
47
|
+
* "how fast is this going".
|
|
48
|
+
*/
|
|
49
|
+
export function MovieLiveProgress({ jobId }: { jobId: string | null }) {
|
|
50
|
+
const { jobs } = useMediaJobs();
|
|
51
|
+
const job = jobId ? jobs.find((j) => j.id === jobId) ?? null : null;
|
|
52
|
+
if (!job) return null;
|
|
53
|
+
const label = progressLabel(job);
|
|
54
|
+
const event = latestEvent(job);
|
|
55
|
+
const pct = job.progress == null ? null : Math.max(0, Math.min(100, job.progress * 100));
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{(pct != null || label) && (
|
|
59
|
+
<div className="vi-job-progress">
|
|
60
|
+
{pct != null && (
|
|
61
|
+
<div className="vi-job-progress-track">
|
|
62
|
+
<div className="vi-job-progress-fill" style={{ width: `${pct}%` }} />
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
{label ? <span className="vi-job-progress-label">{label}</span> : null}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
{event ? <p className="vi-job-latest">{event}</p> : null}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** True for a segment that has not started — the spec's not-yet-rendered tail, which
|
|
74
|
+
* is listed (dimmed) so a 14-segment movie mid-render never reads as a 2-segment one. */
|
|
75
|
+
function isPending(seg: MovieSegment): boolean {
|
|
76
|
+
const status = seg.status ?? "pending";
|
|
77
|
+
return !SEG_DONE.has(status) && status !== "failed";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// NOTE ON `key`: the row bodies below are CONTENT, not the <li> itself — the list
|
|
81
|
+
// element (and its key) stays in the .map. This project's scoped tsconfig rejects a
|
|
82
|
+
// `key` prop on a locally-declared component (the same reason StudioSplicedRow is
|
|
83
|
+
// rendered once, never in a map), so keeping the key on the intrinsic element is the
|
|
84
|
+
// house idiom rather than a workaround invented here.
|
|
85
|
+
|
|
86
|
+
/** One segment's content: playable when its bytes exist, verbatim when it failed. */
|
|
87
|
+
function SegmentRowBody({ seg }: { seg: MovieSegment }) {
|
|
88
|
+
const src = segmentClipSrc(seg);
|
|
89
|
+
const status = seg.status ?? "pending";
|
|
90
|
+
const failed = status === "failed";
|
|
91
|
+
const pending = isPending(seg);
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{src ? (
|
|
95
|
+
<video
|
|
96
|
+
src={`${src}#t=0.1`}
|
|
97
|
+
controls
|
|
98
|
+
muted
|
|
99
|
+
playsInline
|
|
100
|
+
preload="metadata"
|
|
101
|
+
style={{
|
|
102
|
+
width: "10rem",
|
|
103
|
+
maxWidth: "34vw",
|
|
104
|
+
borderRadius: "0.4rem",
|
|
105
|
+
background: "#000",
|
|
106
|
+
border: "1px solid var(--vi-border)",
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
) : null}
|
|
110
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "0.15rem", minWidth: 0 }}>
|
|
111
|
+
<span style={{ fontSize: "0.85rem", fontWeight: 600 }}>
|
|
112
|
+
Segment {seg.index + 1}
|
|
113
|
+
<span className={`vi-status vi-status-${status}`} style={{ marginLeft: "0.5rem" }}>
|
|
114
|
+
{status}
|
|
115
|
+
{seg.resumed ? " · resumed" : ""}
|
|
116
|
+
</span>
|
|
117
|
+
</span>
|
|
118
|
+
{failed && (
|
|
119
|
+
// The runner's own envelope, unsummarised — a code + message an operator can
|
|
120
|
+
// act on beats a generic "failed".
|
|
121
|
+
<span className="vi-error" role="alert" style={{ fontSize: "0.8rem" }}>
|
|
122
|
+
{seg.error?.code ? `[${seg.error.code}] ` : ""}
|
|
123
|
+
{seg.error?.message ?? "failed (no message recorded)"}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
{!src && !failed && !pending && (
|
|
127
|
+
<span className="vi-comfy-hint" style={{ opacity: 0.7 }}>
|
|
128
|
+
rendered, but its clip is not on disk right now
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
{(seg.frames != null || seg.duration_s != null) && (
|
|
132
|
+
<span className="vi-comfy-hint" style={{ opacity: 0.7 }}>
|
|
133
|
+
{seg.frames != null ? `${seg.frames} frames` : ""}
|
|
134
|
+
{seg.duration_s != null ? `${seg.frames != null ? " · " : ""}${Math.round(seg.duration_s)}s` : ""}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
{seg.prompt ? (
|
|
138
|
+
<span className="vi-comfy-hint" style={{ opacity: 0.7, wordBreak: "break-word" }}>
|
|
139
|
+
“{seg.prompt}”
|
|
140
|
+
</span>
|
|
141
|
+
) : null}
|
|
142
|
+
</div>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One session's content — head line, meta line, its ONE action, live pace while it
|
|
148
|
+
* runs, and (expanded) the segment strip. The <li className="vi-studio-active-row">
|
|
149
|
+
* wrapper stays in the .map (see the key note above); these stay its direct children
|
|
150
|
+
* so the row's CSS (which orders the button and the full-width panel) still applies. */
|
|
151
|
+
function SessionRowBody({
|
|
152
|
+
session,
|
|
153
|
+
expanded,
|
|
154
|
+
onToggle,
|
|
155
|
+
busy,
|
|
156
|
+
actionError,
|
|
157
|
+
onPause,
|
|
158
|
+
onResume,
|
|
159
|
+
}: {
|
|
160
|
+
session: MovieSession;
|
|
161
|
+
expanded: boolean;
|
|
162
|
+
onToggle: () => void;
|
|
163
|
+
busy: boolean;
|
|
164
|
+
actionError: string | null;
|
|
165
|
+
onPause: () => void;
|
|
166
|
+
onResume: () => void;
|
|
167
|
+
}) {
|
|
168
|
+
const s = session;
|
|
169
|
+
const live = isSessionLive(s);
|
|
170
|
+
const blocked = resumeBlockedReason(s);
|
|
171
|
+
const status = s.status ?? "unknown";
|
|
172
|
+
const done = s.segments_completed ?? 0;
|
|
173
|
+
const total = s.segments_total ?? s.segments?.length ?? 0;
|
|
174
|
+
const movieSrc = movieMediaSrc(s.movie);
|
|
175
|
+
const geometry =
|
|
176
|
+
s.width != null && s.height != null
|
|
177
|
+
? `${s.width}×${s.height}${s.fps != null ? ` · ${s.fps}fps` : ""}`
|
|
178
|
+
: "";
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<div className="vi-studio-active-head">
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
className="vi-timeline-toggle"
|
|
185
|
+
onClick={onToggle}
|
|
186
|
+
aria-expanded={expanded}
|
|
187
|
+
title={expanded ? "Hide segments" : "Show what this movie has rendered"}
|
|
188
|
+
>
|
|
189
|
+
<span className="vi-timeline-caret" aria-hidden>
|
|
190
|
+
{expanded ? "▾" : "▸"}
|
|
191
|
+
</span>
|
|
192
|
+
</button>
|
|
193
|
+
<span className={`vi-status vi-status-${status}`}>{status}</span>
|
|
194
|
+
<span className="vi-studio-active-id">
|
|
195
|
+
{s.title?.trim() ? s.title : "untitled"} · {shortId(s.movie_id)}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="vi-studio-active-meta">
|
|
199
|
+
<span>
|
|
200
|
+
{done}/{total} segments
|
|
201
|
+
</span>
|
|
202
|
+
{geometry ? <span>· {geometry}</span> : null}
|
|
203
|
+
{s.id_lock ? <span>· id_lock</span> : null}
|
|
204
|
+
{s.project ? <span>· {s.project}</span> : null}
|
|
205
|
+
{s.updated != null ? (
|
|
206
|
+
<span title={fmtWhen(s.updated)}>· {agoText(s.updated, Date.now())} ago</span>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
{/* ONE action per row, and never both: a live movie gets Pause, everything else
|
|
210
|
+
gets Resume. `status === "running"` is folded into the live test defensively —
|
|
211
|
+
the route only reports running while the bus agrees, but a row that claims to
|
|
212
|
+
be rendering must always be stoppable, and pause is idempotent anyway. */}
|
|
213
|
+
{live || status === "running" ? (
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
217
|
+
disabled={busy}
|
|
218
|
+
title="Stop this movie between segments and park it — everything already rendered stays on disk."
|
|
219
|
+
onClick={onPause}
|
|
220
|
+
>
|
|
221
|
+
{busy ? "Pausing…" : "⏸ Pause"}
|
|
222
|
+
</button>
|
|
223
|
+
) : (
|
|
224
|
+
// The title rides the WRAPPER: a disabled button swallows pointer events in
|
|
225
|
+
// most browsers, so a tooltip on it is a reason nobody can read.
|
|
226
|
+
<span
|
|
227
|
+
title={
|
|
228
|
+
blocked ??
|
|
229
|
+
"Re-enqueue this movie's spec — segments already rendered come back from the " +
|
|
230
|
+
"clip store without touching a GPU, so it picks up where it stopped."
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className="vi-btn vi-btn-sm"
|
|
236
|
+
disabled={busy || blocked != null}
|
|
237
|
+
onClick={onResume}
|
|
238
|
+
>
|
|
239
|
+
{busy ? "Resuming…" : "▶ Resume"}
|
|
240
|
+
</button>
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
{/* An UNFINISHED movie that cannot be resumed says so in the row itself, not only
|
|
244
|
+
in a tooltip — that is the pre-feature (no spec.json) case, and it is the one
|
|
245
|
+
an operator will otherwise keep clicking. A finished movie needs no excuse. */}
|
|
246
|
+
{blocked && status !== "done" && !live ? (
|
|
247
|
+
<p
|
|
248
|
+
className="vi-knob-hint"
|
|
249
|
+
style={{ flexBasis: "100%", margin: "0.2rem 0 0", wordBreak: "break-word" }}
|
|
250
|
+
>
|
|
251
|
+
{blocked}
|
|
252
|
+
</p>
|
|
253
|
+
) : null}
|
|
254
|
+
{/* Live pace for a running session — mounted only while it IS running, so a list
|
|
255
|
+
of parked movies costs no job poll. */}
|
|
256
|
+
{live ? <MovieLiveProgress jobId={s.job_id ?? null} /> : null}
|
|
257
|
+
{actionError ? (
|
|
258
|
+
// Verbatim: the 409 bodies ("no persisted spec.json…", "already running —
|
|
259
|
+
// pause it first") are the whole explanation, and paraphrasing them would
|
|
260
|
+
// hide which of the two happened.
|
|
261
|
+
<p className="vi-error" role="alert" style={{ flexBasis: "100%", margin: "0.2rem 0 0" }}>
|
|
262
|
+
{actionError}
|
|
263
|
+
</p>
|
|
264
|
+
) : null}
|
|
265
|
+
{expanded ? (
|
|
266
|
+
<div className="vi-timeline-panel">
|
|
267
|
+
{movieSrc ? (
|
|
268
|
+
<div style={{ marginBottom: "0.6rem" }}>
|
|
269
|
+
<p className="vi-comfy-label" style={{ marginBottom: "0.3rem" }}>
|
|
270
|
+
Assembled movie
|
|
271
|
+
</p>
|
|
272
|
+
<video
|
|
273
|
+
src={movieSrc}
|
|
274
|
+
controls
|
|
275
|
+
playsInline
|
|
276
|
+
preload="metadata"
|
|
277
|
+
style={{
|
|
278
|
+
width: "18rem",
|
|
279
|
+
maxWidth: "60vw",
|
|
280
|
+
borderRadius: "0.4rem",
|
|
281
|
+
background: "#000",
|
|
282
|
+
border: "1px solid var(--vi-border)",
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
) : null}
|
|
287
|
+
{s.segments?.length ? (
|
|
288
|
+
<ul
|
|
289
|
+
style={{
|
|
290
|
+
listStyle: "none",
|
|
291
|
+
margin: 0,
|
|
292
|
+
padding: 0,
|
|
293
|
+
display: "flex",
|
|
294
|
+
flexDirection: "column",
|
|
295
|
+
gap: "0.6rem",
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{s.segments.map((seg) => (
|
|
299
|
+
<li
|
|
300
|
+
key={`${s.movie_id}:${seg.index}`}
|
|
301
|
+
style={{
|
|
302
|
+
display: "flex",
|
|
303
|
+
gap: "0.7rem",
|
|
304
|
+
alignItems: "flex-start",
|
|
305
|
+
// Not-yet-started segments are dimmed, not hidden: the strip shows
|
|
306
|
+
// the whole movie, so "how much is left" is visible at a glance.
|
|
307
|
+
opacity: isPending(seg) ? 0.55 : 1,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<SegmentRowBody seg={seg} />
|
|
311
|
+
</li>
|
|
312
|
+
))}
|
|
313
|
+
</ul>
|
|
314
|
+
) : (
|
|
315
|
+
<p className="vi-knob-hint" style={{ margin: 0 }}>
|
|
316
|
+
No segment records yet — this session was submitted but has not written one.
|
|
317
|
+
</p>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
) : null}
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The panel body — split out so the poll it drives exists ONLY while the panel is
|
|
326
|
+
* open (mounting is the visibility signal; see the `active` gate below). */
|
|
327
|
+
function SessionsBody() {
|
|
328
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
329
|
+
// A hidden browser tab is not a visible panel: pause the walk of the movies root
|
|
330
|
+
// while the console is in the background, and reload the moment it comes back.
|
|
331
|
+
const [docVisible, setDocVisible] = useState(
|
|
332
|
+
() => typeof document === "undefined" || document.visibilityState !== "hidden",
|
|
333
|
+
);
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
const onVis = () => setDocVisible(document.visibilityState !== "hidden");
|
|
336
|
+
document.addEventListener("visibilitychange", onVis);
|
|
337
|
+
return () => document.removeEventListener("visibilitychange", onVis);
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
const { sessions, loading, error, busy, actionError, pause, resume } =
|
|
341
|
+
useMovieSessions(docVisible);
|
|
342
|
+
|
|
343
|
+
const toggle = (movieId: string) =>
|
|
344
|
+
setExpanded((prev) => {
|
|
345
|
+
const next = new Set(prev);
|
|
346
|
+
if (next.has(movieId)) next.delete(movieId);
|
|
347
|
+
else next.add(movieId);
|
|
348
|
+
return next;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (loading && sessions.length === 0) {
|
|
352
|
+
return <p className="vi-knob-hint">Reading the movies root…</p>;
|
|
353
|
+
}
|
|
354
|
+
if (error && sessions.length === 0) {
|
|
355
|
+
return (
|
|
356
|
+
<p className="vi-error" role="alert">
|
|
357
|
+
{error}
|
|
358
|
+
</p>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (sessions.length === 0) {
|
|
362
|
+
return (
|
|
363
|
+
<p className="vi-knob-hint">
|
|
364
|
+
No movie sessions on disk yet — generate one below and it appears here, resumable
|
|
365
|
+
even if this tab goes away.
|
|
366
|
+
</p>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return (
|
|
370
|
+
<ul className="vi-studio-active-list">
|
|
371
|
+
{sessions.map((s) => (
|
|
372
|
+
<li key={s.movie_id} className="vi-studio-active-row">
|
|
373
|
+
<SessionRowBody
|
|
374
|
+
session={s}
|
|
375
|
+
expanded={expanded.has(s.movie_id)}
|
|
376
|
+
onToggle={() => toggle(s.movie_id)}
|
|
377
|
+
busy={busy.has(s.movie_id)}
|
|
378
|
+
actionError={actionError[s.movie_id] ?? null}
|
|
379
|
+
onPause={() => pause(s.movie_id)}
|
|
380
|
+
onResume={() => resume(s.movie_id)}
|
|
381
|
+
/>
|
|
382
|
+
</li>
|
|
383
|
+
))}
|
|
384
|
+
</ul>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The collapsible Cinema-sessions panel. Open by default (the recovery affordance is
|
|
390
|
+
* the point — an operator coming back to a lost render should not have to know to look
|
|
391
|
+
* for it), and the body unmounts when collapsed, which is what stops the poll.
|
|
392
|
+
*/
|
|
393
|
+
export function StudioMovieSessions() {
|
|
394
|
+
const [open, setOpen] = useState(true);
|
|
395
|
+
return (
|
|
396
|
+
<section className="vi-studio-active" aria-label="Cinema sessions">
|
|
397
|
+
<button
|
|
398
|
+
type="button"
|
|
399
|
+
className="vi-timeline-toggle"
|
|
400
|
+
aria-expanded={open}
|
|
401
|
+
onClick={() => setOpen((v) => !v)}
|
|
402
|
+
style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}
|
|
403
|
+
>
|
|
404
|
+
<span className="vi-timeline-caret" aria-hidden>
|
|
405
|
+
{open ? "▾" : "▸"}
|
|
406
|
+
</span>
|
|
407
|
+
<span className="vi-knob-headline" style={{ margin: 0 }}>
|
|
408
|
+
Cinema sessions{" "}
|
|
409
|
+
<span className="vi-knob-headline-sub">
|
|
410
|
+
movies on disk — resume, pause, or watch what already rendered
|
|
411
|
+
</span>
|
|
412
|
+
</span>
|
|
413
|
+
</button>
|
|
414
|
+
{open ? <SessionsBody /> : null}
|
|
415
|
+
</section>
|
|
416
|
+
);
|
|
417
|
+
}
|