@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,476 @@
|
|
|
1
|
+
// The VISUAL SPLICED ROW + branch-frame scrubber (slice B3) — the picture half of
|
|
2
|
+
// the operator's ask: "in the video edit row it displays a spliced video that can
|
|
3
|
+
// easily be conjoined at those points … the splice being the parent video |
|
|
4
|
+
// divergence, and the user's options decide."
|
|
5
|
+
//
|
|
6
|
+
// A studio movie is ONE NLE timeline row: `[segment 0 | segment 1 | …]`. This draws
|
|
7
|
+
// that row from the terminal manifest — each block sized to its EFFECTIVE frames in
|
|
8
|
+
// the assembly (a parent trimmed at its child's splice; the leaf full), with a
|
|
9
|
+
// scissors NOTCH at every join labelled with the parent frame the child diverged
|
|
10
|
+
// at. Clicking a rendered block opens a frame scrubber over THAT segment's clip; its
|
|
11
|
+
// one primary action appends a new branch goal to the composer's timeline.
|
|
12
|
+
//
|
|
13
|
+
// V1 BOUNDARIES (stated plainly; carried in the composer header too):
|
|
14
|
+
// • The row reads the LAST render. Branching edits the AUTHORED goal timeline and
|
|
15
|
+
// the user re-Generates — a studio job is IMMUTABLE, so extending a done movie
|
|
16
|
+
// re-enqueues a NEW job (it never mutates the old one).
|
|
17
|
+
// • v0 authors a LINEAR chain. The schema's take-tree (sibling divergence from one
|
|
18
|
+
// parent) is planned growth; until then branching from a NON-final block cannot
|
|
19
|
+
// keep the old tail as a sibling, so it TRUNCATES (see the composer's onBranch +
|
|
20
|
+
// the destructive-note wiring below).
|
|
21
|
+
//
|
|
22
|
+
// Rendered ONCE by the composer (never inside a .map) so no `key` ever lands on this
|
|
23
|
+
// custom component — the repo's JSX typing rejects that (see StudioGenerateTab). All
|
|
24
|
+
// internal lists key on intrinsic elements.
|
|
25
|
+
import { useEffect, useRef, useState } from "react";
|
|
26
|
+
import { mediaBytesUrl } from "../../config";
|
|
27
|
+
import type { MediaRef } from "../../video/contract";
|
|
28
|
+
import {
|
|
29
|
+
deriveRow,
|
|
30
|
+
resolveFps,
|
|
31
|
+
clampFrame,
|
|
32
|
+
frameToTime,
|
|
33
|
+
timeToFrame,
|
|
34
|
+
type RowSegment,
|
|
35
|
+
type RowJoint,
|
|
36
|
+
} from "./movieTimeline";
|
|
37
|
+
|
|
38
|
+
export interface StudioSplicedRowProps {
|
|
39
|
+
/** Terminal manifest segments (index-aligned with `segmentRefs`). */
|
|
40
|
+
segments: RowSegment[];
|
|
41
|
+
/** Terminal manifest joints (per-splice trim ledger). */
|
|
42
|
+
joints: RowJoint[];
|
|
43
|
+
/** Per-segment clip MediaRefs, in order (outputs sans the trailing movie.mp4). */
|
|
44
|
+
segmentRefs: MediaRef[];
|
|
45
|
+
/** Movie fps from the manifest (falls back to the composer knob). */
|
|
46
|
+
fps: number;
|
|
47
|
+
fpsFallback: number;
|
|
48
|
+
/** Authored goal-row count RIGHT NOW — clean append (last) vs truncate (mid-row). */
|
|
49
|
+
goalCount: number;
|
|
50
|
+
/** Append a branch goal after `segmentIndex`, conditioned on `frame` of its clip. */
|
|
51
|
+
onBranch: (segmentIndex: number, frame: number) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const BLOCK_MIN_H = "3.4rem";
|
|
55
|
+
|
|
56
|
+
export function StudioSplicedRow({
|
|
57
|
+
segments,
|
|
58
|
+
joints,
|
|
59
|
+
segmentRefs,
|
|
60
|
+
fps,
|
|
61
|
+
fpsFallback,
|
|
62
|
+
goalCount,
|
|
63
|
+
onBranch,
|
|
64
|
+
}: StudioSplicedRowProps) {
|
|
65
|
+
const effFps = resolveFps(fps, fpsFallback);
|
|
66
|
+
const { blocks, splices } = deriveRow(segments, joints);
|
|
67
|
+
const spliceAfter = new Map<number, (typeof splices)[number]>();
|
|
68
|
+
splices.forEach((s) => spliceAfter.set(s.afterIndex, s));
|
|
69
|
+
|
|
70
|
+
// Which block's scrubber is open (by segment index), and its live frame cursor.
|
|
71
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
72
|
+
const [frame, setFrame] = useState(0);
|
|
73
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
74
|
+
|
|
75
|
+
const openBlock = blocks.find((b) => b.index === openIndex) ?? null;
|
|
76
|
+
const openRef = openIndex != null ? segmentRefs[openIndex] : undefined;
|
|
77
|
+
const openFullFrames = openBlock?.fullFrames ?? null;
|
|
78
|
+
|
|
79
|
+
// A fresh segment resets the cursor to frame 0.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setFrame(0);
|
|
82
|
+
const v = videoRef.current;
|
|
83
|
+
if (v) {
|
|
84
|
+
try {
|
|
85
|
+
v.currentTime = 0;
|
|
86
|
+
} catch {
|
|
87
|
+
/* not yet seekable — the metadata load handles the first paint */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [openIndex]);
|
|
91
|
+
|
|
92
|
+
function seekTo(f: number) {
|
|
93
|
+
const clamped = clampFrame(f, openFullFrames);
|
|
94
|
+
setFrame(clamped);
|
|
95
|
+
const v = videoRef.current;
|
|
96
|
+
if (v) {
|
|
97
|
+
try {
|
|
98
|
+
v.currentTime = frameToTime(clamped, effFps);
|
|
99
|
+
} catch {
|
|
100
|
+
/* ignore — a not-yet-ready element seeks on its own metadata load */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The native <video> scrubber is a second way to move the cursor: mirror its time
|
|
106
|
+
// back into the frame readout/slider. We NEVER re-seek here (only explicit slider /
|
|
107
|
+
// step handlers seek), so there is no feedback loop.
|
|
108
|
+
function syncFromVideo() {
|
|
109
|
+
const v = videoRef.current;
|
|
110
|
+
if (!v) return;
|
|
111
|
+
const f = clampFrame(timeToFrame(v.currentTime, effFps), openFullFrames);
|
|
112
|
+
setFrame((cur) => (cur === f ? cur : f));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const maxFrame = openFullFrames != null && openFullFrames > 0 ? openFullFrames - 1 : 0;
|
|
116
|
+
// Branching from a NON-final authored row cannot keep the old tail as a sibling in a
|
|
117
|
+
// linear v0 — it truncates. Surface that in the action so the click is never silent.
|
|
118
|
+
const droppedCount = openIndex != null ? Math.max(0, goalCount - (openIndex + 1)) : 0;
|
|
119
|
+
const willTruncate = droppedCount > 0;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<section aria-label="Spliced timeline row" style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
|
123
|
+
<p className="vi-comfy-label" style={{ margin: 0 }}>
|
|
124
|
+
Spliced timeline — {blocks.length} segment(s), conjoined at {splices.length} splice
|
|
125
|
+
{splices.length === 1 ? "" : "s"}
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
{/* ── the row: blocks sized to EFFECTIVE frames, scissor notch at each join ── */}
|
|
129
|
+
<div
|
|
130
|
+
role="list"
|
|
131
|
+
aria-label="Movie segments"
|
|
132
|
+
style={{
|
|
133
|
+
display: "flex",
|
|
134
|
+
alignItems: "stretch",
|
|
135
|
+
width: "100%",
|
|
136
|
+
border: "1px solid var(--vi-border)",
|
|
137
|
+
borderRadius: "0.5rem",
|
|
138
|
+
overflow: "hidden",
|
|
139
|
+
background: "#000",
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{/* One flatMap over the blocks: each yields its block button, then (when the
|
|
143
|
+
block is a joint parent) a splice NOTCH — flat siblings in the flex row, each
|
|
144
|
+
keyed on its own intrinsic element (never on a custom component). */}
|
|
145
|
+
{blocks.flatMap((b, i) => {
|
|
146
|
+
const ref = segmentRefs[b.index];
|
|
147
|
+
const isOpen = openIndex === b.index;
|
|
148
|
+
const splice = spliceAfter.get(b.index);
|
|
149
|
+
const pct = Math.max(2, b.widthPct); // never let a hard-trimmed block vanish
|
|
150
|
+
const blockEl = (
|
|
151
|
+
<button
|
|
152
|
+
key={b.segmentId ?? `blk-${b.index}`}
|
|
153
|
+
type="button"
|
|
154
|
+
role="listitem"
|
|
155
|
+
onClick={() => setOpenIndex((cur) => (cur === b.index ? null : b.index))}
|
|
156
|
+
title={
|
|
157
|
+
ref
|
|
158
|
+
? `Segment ${b.index + 1} — scrub & branch (${b.effectiveFrames} frame${
|
|
159
|
+
b.effectiveFrames === 1 ? "" : "s"
|
|
160
|
+
} in the cut${b.trimmed ? ", trimmed at the splice" : ""})`
|
|
161
|
+
: `Segment ${b.index + 1} — no streamable clip`
|
|
162
|
+
}
|
|
163
|
+
aria-pressed={isOpen}
|
|
164
|
+
disabled={!ref}
|
|
165
|
+
style={{
|
|
166
|
+
position: "relative",
|
|
167
|
+
flex: `${pct} 1 0`,
|
|
168
|
+
minWidth: 0,
|
|
169
|
+
minHeight: BLOCK_MIN_H,
|
|
170
|
+
padding: 0,
|
|
171
|
+
margin: 0,
|
|
172
|
+
border: "none",
|
|
173
|
+
borderRight:
|
|
174
|
+
i < blocks.length - 1 && !splice ? "1px solid var(--vi-border)" : "none",
|
|
175
|
+
cursor: ref ? "pointer" : "default",
|
|
176
|
+
background: isOpen ? "var(--vi-accent)" : "transparent",
|
|
177
|
+
outline: isOpen ? "2px solid var(--vi-accent)" : "none",
|
|
178
|
+
outlineOffset: "-2px",
|
|
179
|
+
overflow: "hidden",
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{ref ? (
|
|
183
|
+
<video
|
|
184
|
+
src={`${mediaBytesUrl(ref.uri)}#t=0.1`}
|
|
185
|
+
muted
|
|
186
|
+
playsInline
|
|
187
|
+
preload="metadata"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
style={{
|
|
190
|
+
position: "absolute",
|
|
191
|
+
inset: 0,
|
|
192
|
+
width: "100%",
|
|
193
|
+
height: "100%",
|
|
194
|
+
objectFit: "cover",
|
|
195
|
+
opacity: isOpen ? 0.55 : 0.8,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
) : null}
|
|
199
|
+
<span
|
|
200
|
+
aria-hidden="true"
|
|
201
|
+
style={{
|
|
202
|
+
position: "absolute",
|
|
203
|
+
left: 4,
|
|
204
|
+
top: 3,
|
|
205
|
+
fontSize: "0.7rem",
|
|
206
|
+
fontWeight: 700,
|
|
207
|
+
fontVariantNumeric: "tabular-nums",
|
|
208
|
+
color: "#fff",
|
|
209
|
+
textShadow: "0 1px 2px #000",
|
|
210
|
+
padding: "0 0.25rem",
|
|
211
|
+
borderRadius: "0.25rem",
|
|
212
|
+
background: "rgba(0,0,0,0.35)",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{b.index + 1}
|
|
216
|
+
{b.trimmed ? " ✂" : ""}
|
|
217
|
+
</span>
|
|
218
|
+
<span
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
style={{
|
|
221
|
+
position: "absolute",
|
|
222
|
+
left: 4,
|
|
223
|
+
bottom: 3,
|
|
224
|
+
fontSize: "0.62rem",
|
|
225
|
+
fontVariantNumeric: "tabular-nums",
|
|
226
|
+
color: "#fff",
|
|
227
|
+
textShadow: "0 1px 2px #000",
|
|
228
|
+
opacity: 0.9,
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{b.effectiveFrames}f
|
|
232
|
+
</span>
|
|
233
|
+
</button>
|
|
234
|
+
);
|
|
235
|
+
if (!splice) return [blockEl];
|
|
236
|
+
// A "cut" splice is a HARD scene cut: the parent plays in FULL (no trim) and the
|
|
237
|
+
// next segment is a fresh render — in an identity movie the SUBJECT carries across
|
|
238
|
+
// the cut even though no frame does. Every other splice trims the parent at a frame.
|
|
239
|
+
const isCut = splice.mode === "cut";
|
|
240
|
+
const notchAria = isCut
|
|
241
|
+
? "Scene cut — identity carried, no frame carry"
|
|
242
|
+
: `Splice — parent frame ${splice.branchFrame}`;
|
|
243
|
+
const notchTitle = isCut
|
|
244
|
+
? "✂ scene cut — identity carried, no frame carry (the parent plays in full; the next segment is a fresh render)"
|
|
245
|
+
: `✂ Splice at frame ${splice.branchFrame} — the parent is trimmed to ${splice.trimFrames} frame${
|
|
246
|
+
splice.trimFrames === 1 ? "" : "s"
|
|
247
|
+
}; the next segment diverges here.`;
|
|
248
|
+
const notch = (
|
|
249
|
+
<span
|
|
250
|
+
key={`splice-${splice.afterIndex}-${splice.childIndex}`}
|
|
251
|
+
role="separator"
|
|
252
|
+
aria-label={notchAria}
|
|
253
|
+
title={notchTitle}
|
|
254
|
+
style={{
|
|
255
|
+
flex: "0 0 auto",
|
|
256
|
+
width: "1.15rem",
|
|
257
|
+
alignSelf: "stretch",
|
|
258
|
+
display: "flex",
|
|
259
|
+
alignItems: "center",
|
|
260
|
+
justifyContent: "center",
|
|
261
|
+
background:
|
|
262
|
+
"repeating-linear-gradient(45deg, var(--vi-accent) 0 3px, transparent 3px 6px)",
|
|
263
|
+
borderLeft: "1px solid var(--vi-border)",
|
|
264
|
+
borderRight: "1px solid var(--vi-border)",
|
|
265
|
+
color: "#fff",
|
|
266
|
+
fontSize: "0.7rem",
|
|
267
|
+
cursor: "help",
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
✂
|
|
271
|
+
</span>
|
|
272
|
+
);
|
|
273
|
+
return [blockEl, notch];
|
|
274
|
+
})}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* ── the branch-frame scrubber (opens over the clicked segment's clip) ── */}
|
|
278
|
+
{openBlock && openRef ? (
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
border: "1px solid var(--vi-border)",
|
|
282
|
+
borderRadius: "0.5rem",
|
|
283
|
+
padding: "0.6rem 0.7rem",
|
|
284
|
+
display: "flex",
|
|
285
|
+
flexDirection: "column",
|
|
286
|
+
gap: "0.5rem",
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
290
|
+
<span style={{ fontWeight: 600 }}>
|
|
291
|
+
Scrub segment {openBlock.index + 1}
|
|
292
|
+
{openBlock.capability ? ` · ${openBlock.capability}` : ""}
|
|
293
|
+
</span>
|
|
294
|
+
<span style={{ flex: 1 }} />
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
298
|
+
onClick={() => setOpenIndex(null)}
|
|
299
|
+
title="Close scrubber"
|
|
300
|
+
>
|
|
301
|
+
✕
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<video
|
|
306
|
+
key={openRef.uri}
|
|
307
|
+
ref={videoRef}
|
|
308
|
+
src={mediaBytesUrl(openRef.uri)}
|
|
309
|
+
controls
|
|
310
|
+
playsInline
|
|
311
|
+
preload="metadata"
|
|
312
|
+
onSeeked={syncFromVideo}
|
|
313
|
+
onTimeUpdate={syncFromVideo}
|
|
314
|
+
onLoadedMetadata={syncFromVideo}
|
|
315
|
+
style={{
|
|
316
|
+
width: "100%",
|
|
317
|
+
maxHeight: "40vh",
|
|
318
|
+
borderRadius: "0.4rem",
|
|
319
|
+
background: "#000",
|
|
320
|
+
}}
|
|
321
|
+
/>
|
|
322
|
+
|
|
323
|
+
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", flexWrap: "wrap" }}>
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
327
|
+
onClick={() => seekTo(frame - 1)}
|
|
328
|
+
disabled={frame <= 0}
|
|
329
|
+
title="Step back one frame"
|
|
330
|
+
>
|
|
331
|
+
−1
|
|
332
|
+
</button>
|
|
333
|
+
<input
|
|
334
|
+
type="range"
|
|
335
|
+
min={0}
|
|
336
|
+
max={maxFrame}
|
|
337
|
+
step={1}
|
|
338
|
+
value={Math.min(frame, maxFrame)}
|
|
339
|
+
onChange={(e) => seekTo(Number(e.target.value))}
|
|
340
|
+
disabled={maxFrame <= 0}
|
|
341
|
+
aria-label="Frame"
|
|
342
|
+
style={{ flex: "1 1 12rem", minWidth: "8rem" }}
|
|
343
|
+
/>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
347
|
+
onClick={() => seekTo(frame + 1)}
|
|
348
|
+
disabled={maxFrame > 0 && frame >= maxFrame}
|
|
349
|
+
title="Step forward one frame"
|
|
350
|
+
>
|
|
351
|
+
+1
|
|
352
|
+
</button>
|
|
353
|
+
<span
|
|
354
|
+
className="vi-knob-hint"
|
|
355
|
+
style={{ fontVariantNumeric: "tabular-nums", minWidth: "9rem", textAlign: "right" }}
|
|
356
|
+
>
|
|
357
|
+
frame {frame}
|
|
358
|
+
{openFullFrames != null ? ` / ${openFullFrames - 1}` : ""} · t=
|
|
359
|
+
{frameToTime(frame, effFps).toFixed(2)}s
|
|
360
|
+
</span>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div style={{ display: "flex", alignItems: "center", gap: "0.6rem", flexWrap: "wrap" }}>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
className="vi-btn vi-btn-accent"
|
|
367
|
+
onClick={() => onBranch(openBlock.index, frame)}
|
|
368
|
+
>
|
|
369
|
+
Branch from frame {frame}
|
|
370
|
+
{willTruncate ? ` — replaces segment${droppedCount === 1 ? "" : "s"} ${openBlock.index + 2}+` : ""}
|
|
371
|
+
</button>
|
|
372
|
+
<span className="vi-comfy-hint" role="note" style={{ margin: 0 }}>
|
|
373
|
+
{willTruncate
|
|
374
|
+
? `Adds a new segment branching from frame ${frame} of this clip and DROPS the ${droppedCount} authored segment(s) after it — a linear timeline can't hold the old tail as a sibling take (take-trees are the next slice). Then edit its prompt and re-Generate.`
|
|
375
|
+
: `Adds a new segment branching i2v from frame ${frame} of this clip. Then edit its prompt and re-Generate — a studio job is immutable, so this renders a NEW extended movie.`}
|
|
376
|
+
</span>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
) : null}
|
|
380
|
+
</section>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── the PENDING row (shown WHILE a movie renders): one block per authored goal,
|
|
385
|
+
// tinted by live per-segment progress. Equal widths — effective frames aren't
|
|
386
|
+
// known until each clip exists. Rendered once by the composer, never in a .map. ──
|
|
387
|
+
export interface StudioPendingRowProps {
|
|
388
|
+
goalPrompts: string[];
|
|
389
|
+
/** Clean per-segment progress (the composer normalizes the zod-passthrough poll blob
|
|
390
|
+
* into this shape before passing it — keeps the row free of zod-internal typing). */
|
|
391
|
+
progressSegments?: Array<{
|
|
392
|
+
index: number;
|
|
393
|
+
status?: string;
|
|
394
|
+
resumed?: boolean;
|
|
395
|
+
}>;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function StudioPendingRow({ goalPrompts, progressSegments }: StudioPendingRowProps) {
|
|
399
|
+
const statusByIndex = new Map<number, string>();
|
|
400
|
+
(progressSegments ?? []).forEach((s) => {
|
|
401
|
+
const label = s.resumed ? "resumed" : s.status ?? "";
|
|
402
|
+
statusByIndex.set(s.index, label);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Static tints only — the "generating" block reads as active via a brighter fill
|
|
406
|
+
// (no CSS keyframe: app.css is read-only in this slice, so nothing animated is added).
|
|
407
|
+
function tint(status: string): { bg: string; active: boolean } {
|
|
408
|
+
if (status === "done" || status === "resumed") return { bg: "var(--vi-accent)", active: false };
|
|
409
|
+
if (status === "generating" || status === "branching")
|
|
410
|
+
return { bg: "rgba(138,180,255,0.5)", active: true };
|
|
411
|
+
return { bg: "rgba(255,255,255,0.06)", active: false }; // pending / queued
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<section aria-label="Spliced timeline row (rendering)" style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
|
|
416
|
+
<p className="vi-comfy-label" style={{ margin: 0 }}>
|
|
417
|
+
Spliced timeline — rendering {goalPrompts.length} segment(s)
|
|
418
|
+
</p>
|
|
419
|
+
<div
|
|
420
|
+
role="list"
|
|
421
|
+
style={{
|
|
422
|
+
display: "flex",
|
|
423
|
+
alignItems: "stretch",
|
|
424
|
+
width: "100%",
|
|
425
|
+
border: "1px solid var(--vi-border)",
|
|
426
|
+
borderRadius: "0.5rem",
|
|
427
|
+
overflow: "hidden",
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
{goalPrompts.map((prompt, i) => {
|
|
431
|
+
const status = statusByIndex.get(i) ?? "";
|
|
432
|
+
const { bg, active } = tint(status);
|
|
433
|
+
return (
|
|
434
|
+
<div
|
|
435
|
+
key={`pending-${i}`}
|
|
436
|
+
role="listitem"
|
|
437
|
+
title={`Segment ${i + 1}${status ? ` · ${status}` : " · pending"}${prompt ? ` — ${prompt}` : ""}`}
|
|
438
|
+
style={{
|
|
439
|
+
position: "relative",
|
|
440
|
+
flex: "1 1 0",
|
|
441
|
+
minWidth: 0,
|
|
442
|
+
minHeight: BLOCK_MIN_H,
|
|
443
|
+
background: bg,
|
|
444
|
+
borderRight: i < goalPrompts.length - 1 ? "1px solid var(--vi-border)" : "none",
|
|
445
|
+
display: "flex",
|
|
446
|
+
alignItems: "center",
|
|
447
|
+
justifyContent: "center",
|
|
448
|
+
gap: "0.3rem",
|
|
449
|
+
boxShadow: active ? "inset 0 0 0 2px var(--vi-accent)" : undefined,
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
<span
|
|
453
|
+
style={{
|
|
454
|
+
fontSize: "0.7rem",
|
|
455
|
+
fontWeight: 700,
|
|
456
|
+
fontVariantNumeric: "tabular-nums",
|
|
457
|
+
color: "#fff",
|
|
458
|
+
textShadow: "0 1px 2px #000",
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
{i + 1}
|
|
462
|
+
</span>
|
|
463
|
+
<span style={{ fontSize: "0.62rem", color: "#fff", opacity: 0.85, textShadow: "0 1px 2px #000" }}>
|
|
464
|
+
{status || "pending"}
|
|
465
|
+
</span>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
})}
|
|
469
|
+
</div>
|
|
470
|
+
<p className="vi-comfy-hint" role="note" style={{ margin: 0 }}>
|
|
471
|
+
Blocks fill as each segment renders; the spliced row with per-splice frame markers
|
|
472
|
+
and the branch scrubber appear once the movie completes.
|
|
473
|
+
</p>
|
|
474
|
+
</section>
|
|
475
|
+
);
|
|
476
|
+
}
|