@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,302 @@
|
|
|
1
|
+
// The single STUDIO WORKBENCH (Round 9) — the movie-page shape with ONE shared left
|
|
2
|
+
// sidebar. The studio no longer carries its own [ Library | Active | Settings ] sidebar
|
|
3
|
+
// (that produced the dual-sidebar redundancy): it now folds into the arm shell's ONE
|
|
4
|
+
// WorkbenchSidebar via the sidebar registry —
|
|
5
|
+
//
|
|
6
|
+
// • Settings — the geometry / sampler / model knobs + the template dropdown render
|
|
7
|
+
// into the shell sidebar's guarded "Settings" tab (the studio surface portals its
|
|
8
|
+
// knob grid into `reg.settingsHost`; the knob STATE stays in the surface).
|
|
9
|
+
// • Active — studio's in-flight renders portal into the shell "Active Processes"
|
|
10
|
+
// tab (studio jobs are not in the jobTracker, so they ride in as an extra section).
|
|
11
|
+
// • Library — the shell "Session Library" IS the one library; produced studio clips
|
|
12
|
+
// already flow into it (genKind studio_i2v) with extend ("Send to Studio") / restyle
|
|
13
|
+
// ("Restyle") on every clip row. (The former inline StudioLibrarySection is retained
|
|
14
|
+
// in the tree, unused, for history — per-row Details / capability+model filters stay
|
|
15
|
+
// deferred there. select-to-viewer is NO LONGER deferred: it exists in that dead
|
|
16
|
+
// file's code but was never reachable from either mount, which is exactly why the
|
|
17
|
+
// viewer read as stuck — see StudioLibraryTab.tsx's header note. The reachable
|
|
18
|
+
// version now lives on the viewer itself, below.)
|
|
19
|
+
//
|
|
20
|
+
// So the CENTER column is just the movie skeleton: the VIEWER on top (plays the selected
|
|
21
|
+
// clip, with its own header controls — Play from library / Clear / follow-newest, operator
|
|
22
|
+
// ask 2026-07-12 — so it never reads as a fixed, unchangeable default) → the GENERATOR
|
|
23
|
+
// below it (composer prompt + negative, with the shape heroes / reference slots / attach
|
|
24
|
+
// affordances as the input block, hero gating intact) → the Run row. Template selection
|
|
25
|
+
// lives in the Settings dropdown.
|
|
26
|
+
//
|
|
27
|
+
// This ONE component mounts in BOTH places the studio appears — the standalone Studio station
|
|
28
|
+
// (StudioClipsStation) and the Generate station's "studio" mode (StudioGenerateMode) — so the
|
|
29
|
+
// workbench is identical and a job enqueued from either is byte-identical. The only per-mount
|
|
30
|
+
// difference is `registerBridge` (the standalone station owns the "Send to Studio" target).
|
|
31
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
32
|
+
// Sub-tab input memory for the staged source + item strip — see
|
|
33
|
+
// video/sessionForm.ts.
|
|
34
|
+
import { useSessionState } from "../../video/sessionForm";
|
|
35
|
+
import { createPortal } from "react-dom";
|
|
36
|
+
import { registerStudioStager, type StudioMode } from "../../video/studioBridge";
|
|
37
|
+
import { StudioAssistLogPanel } from "./StudioAssistLogPanel";
|
|
38
|
+
import type { MediaRef } from "../../video/contract";
|
|
39
|
+
import { useStudioPresets } from "../../video/useStudioPresets";
|
|
40
|
+
import { useStudioClips } from "./studioShared";
|
|
41
|
+
import {
|
|
42
|
+
makeStudioItem,
|
|
43
|
+
applyOp,
|
|
44
|
+
revertItem,
|
|
45
|
+
type StudioItem,
|
|
46
|
+
type StudioItemOp,
|
|
47
|
+
} from "./studioItem";
|
|
48
|
+
import { StudioItemCard } from "./StudioItemCard";
|
|
49
|
+
import { StudioGenerateSurface } from "./StudioGenerateTab";
|
|
50
|
+
import { StudioViewer } from "./StudioViewer";
|
|
51
|
+
import { useSidebarRegistry } from "../SidebarRegistry";
|
|
52
|
+
|
|
53
|
+
export interface StudioPlaneProps {
|
|
54
|
+
/** Register the studioBridge stager (the "Send to Studio" target). The standalone
|
|
55
|
+
* Studio station passes true; the Generate-station studio mode passes false — the
|
|
56
|
+
* shell library's "Send to Studio" navigates to /studio-clips (the standalone station),
|
|
57
|
+
* so registering in the Generate mode would only race that navigation. */
|
|
58
|
+
registerBridge?: boolean;
|
|
59
|
+
/** When set by a top-level Generate tab (Clip / Cinema), lock the generate surface to
|
|
60
|
+
* that sub-mode and hide its internal Clip|Cinema switcher. Omitted by the standalone
|
|
61
|
+
* /studio-clips station, which keeps the internal switcher. */
|
|
62
|
+
lockedSurfaceMode?: "clip" | "movie";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: StudioPlaneProps) {
|
|
66
|
+
const reg = useSidebarRegistry();
|
|
67
|
+
const { presets, loading: presetsLoading } = useStudioPresets(true);
|
|
68
|
+
const clipsState = useStudioClips();
|
|
69
|
+
|
|
70
|
+
// Source clip + its staged consume mode; `stageSeq` bumps on each external stage so the
|
|
71
|
+
// surface adopts the capability exactly once per stage.
|
|
72
|
+
//
|
|
73
|
+
// The staged SOURCE and its mode are session-scoped (operator ask 2026-08-06:
|
|
74
|
+
// inputs survive a sub-tab switch) — they are operator-staged inputs to the
|
|
75
|
+
// generate surface below, and restoring the surface's capability/knobs while
|
|
76
|
+
// silently dropping the clip they were chosen for would be a half-restore.
|
|
77
|
+
// `stageSeq` deliberately is NOT persisted: it counts STAGE EVENTS, and the
|
|
78
|
+
// surface's capability-resolution effect keys off it. Restoring a non-zero
|
|
79
|
+
// value would re-fire that handoff on every mount and overwrite exactly the
|
|
80
|
+
// capability we just restored.
|
|
81
|
+
const [source, setSource] = useSessionState<MediaRef | null>("studio.plane.source", null);
|
|
82
|
+
const [sourceMode, setSourceMode] = useSessionState<StudioMode>(
|
|
83
|
+
"studio.plane.sourceMode",
|
|
84
|
+
"extend",
|
|
85
|
+
);
|
|
86
|
+
const [stageSeq, setStageSeq] = useState(0);
|
|
87
|
+
|
|
88
|
+
// First-class studio items (Studio redesign, slice 1). Each external stage mints a
|
|
89
|
+
// provenance-carrying StudioItem here. Slice 2b RENDERS these as a strip of
|
|
90
|
+
// StudioItemCards with inline per-item ops (crop / frames), but they still do NOT
|
|
91
|
+
// feed the generate surface — the legacy `source`/`setSource` wiring below is passed
|
|
92
|
+
// to StudioGenerateSurface exactly as before, so today's single-source behavior
|
|
93
|
+
// (extend vs restyle, cross-tier carry) is byte-identical. Slice 3 folds the surface
|
|
94
|
+
// into the item (a per-item composer) and makes generation item-driven.
|
|
95
|
+
const [items, setItems] = useSessionState<StudioItem[]>("studio.plane.items", []);
|
|
96
|
+
|
|
97
|
+
// LIFTED clip selection so the player-forward viewer (top of the centre column) follows
|
|
98
|
+
// the newest playable clip as a fresh enqueue completes — see `pinned` immediately
|
|
99
|
+
// below for the manual override (operator ask 2026-07-12: "can the change be made
|
|
100
|
+
// THROUGH to change the initial scene or video or make it 'new'" — the viewer used to
|
|
101
|
+
// have no controls of its own and would read as permanently stuck on one clip).
|
|
102
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
103
|
+
|
|
104
|
+
// PINNED = the operator has taken manual control of the viewer (a library pick via
|
|
105
|
+
// "▤ Play from library", or an explicit "✕ Clear viewer") — the auto-follow effect
|
|
106
|
+
// below stands down while this is true, so a manual choice sticks instead of being
|
|
107
|
+
// overwritten by the next 6s clip-list poll. "↻ follow newest" flips it back off and
|
|
108
|
+
// hands control back to auto-follow. This is the one semantic switch StudioViewer's
|
|
109
|
+
// header reads/toggles; see its matching half of this note.
|
|
110
|
+
const [pinned, setPinned] = useState(false);
|
|
111
|
+
|
|
112
|
+
const topRef = useRef<HTMLDivElement | null>(null);
|
|
113
|
+
|
|
114
|
+
// Round 9: fold into the ONE shell sidebar — reveal its guarded Settings tab (knobs +
|
|
115
|
+
// template dropdown portal there). registerSettings is stable, so this runs once per
|
|
116
|
+
// mount and unregisters on unmount.
|
|
117
|
+
//
|
|
118
|
+
// Studio's in-flight renders are NO LONGER portaled into the shared Active Processes
|
|
119
|
+
// tab: those same jobs now arrive fleet-wide via GET /llm/jobs (kinds studio_i2v /
|
|
120
|
+
// generate_studio_movie / generate_scene / generate_movie, transport "media"), which
|
|
121
|
+
// the panel polls and renders directly — so keeping the portal would double-count each
|
|
122
|
+
// studio render. The in-plane "Active renders" component (StudioActiveProcesses) is
|
|
123
|
+
// retained in the tree for history; it just no longer feeds the global sidebar.
|
|
124
|
+
useEffect(() => reg.registerSettings(), [reg.registerSettings]);
|
|
125
|
+
|
|
126
|
+
// GENERATION LOG → the sidebar's ACTIVE tab (operator, 2026-08-05): the
|
|
127
|
+
// studio-assist log rides the existing activeExtra portal (the always-rendered
|
|
128
|
+
// host at the foot of the Active Processes panel), so generation attempts sit
|
|
129
|
+
// with the in-flight renders — one "what is the studio doing" place. Register
|
|
130
|
+
// so the panel suppresses its empty copy while the log is mounted.
|
|
131
|
+
useEffect(() => reg.registerActiveExtra(), [reg.registerActiveExtra]);
|
|
132
|
+
|
|
133
|
+
// Auto-follow the newest playable clip WHILE UNPINNED. Previously this only re-picked
|
|
134
|
+
// when the selected job disappeared from the clip list entirely — since a completed
|
|
135
|
+
// job id never leaves the durable catalog, that meant it latched onto the FIRST
|
|
136
|
+
// playable clip of the session and then never moved again, no matter how many newer
|
|
137
|
+
// clips completed after it. That is the reported bug ("sits on the same clip
|
|
138
|
+
// indefinitely and reads as a stuck, unchangeable default"): the header comment always
|
|
139
|
+
// CLAIMED auto-follow, but the code only implemented sticky-keep-unless-deleted. Fixed:
|
|
140
|
+
// now it re-syncs to the newest playable clip on every clip-list change, same as the
|
|
141
|
+
// comment always claimed — gated on `pinned` so a manual pick/clear (see above) is
|
|
142
|
+
// respected instead of being clobbered by the next poll tick.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (pinned) return;
|
|
145
|
+
setSelected(clipsState.clips.find((c) => c.playable)?.job_id ?? null);
|
|
146
|
+
}, [clipsState.clips, pinned]);
|
|
147
|
+
|
|
148
|
+
// Manual pick from the viewer's "▤ Play from library" panel — pins so auto-follow
|
|
149
|
+
// leaves the choice alone.
|
|
150
|
+
const pickViewerClip = useCallback((jobId: string) => {
|
|
151
|
+
setSelected(jobId);
|
|
152
|
+
setPinned(true);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// "✕ Clear viewer" — empties AND pins, so StudioViewer's explicit empty state
|
|
156
|
+
// ("nothing playing") sticks instead of being silently repopulated by the next poll
|
|
157
|
+
// (StudioViewer no longer has an "any playable clip" fallback that would undo this —
|
|
158
|
+
// see that file).
|
|
159
|
+
const clearViewer = useCallback(() => {
|
|
160
|
+
setSelected(null);
|
|
161
|
+
setPinned(true);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
// "↻ follow newest" toggle — flips `pinned`. Turning it back on (true → false) hands
|
|
165
|
+
// control back to the auto-follow effect above, which fires on the very next render
|
|
166
|
+
// and re-syncs to the newest playable clip immediately.
|
|
167
|
+
const toggleFollowNewest = useCallback(() => {
|
|
168
|
+
setPinned((v) => !v);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
const applyStage = useCallback((ref: MediaRef | null, mode: StudioMode) => {
|
|
172
|
+
setSource(ref);
|
|
173
|
+
setSourceMode(mode);
|
|
174
|
+
setStageSeq((n) => n + 1);
|
|
175
|
+
// Bring the generator into view so the staged source shows.
|
|
176
|
+
topRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
// ── Slice-2b item mutation (matched by the item's stable `key`, NOT its uri —
|
|
180
|
+
// a ref may legitimately repeat) ────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
// SAME-KIND op → replace the matching item's `working` via applyOp (mutate in place).
|
|
183
|
+
const onApplyOp = useCallback((target: StudioItem, op: StudioItemOp) => {
|
|
184
|
+
setItems((prev) =>
|
|
185
|
+
prev.map((it) => (it.key === target.key ? applyOp(it, op) : it)),
|
|
186
|
+
);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
// KIND-CHANGE / FAN-OUT op → append a NEW child item per produced ref, carrying the
|
|
190
|
+
// forward-only provenance edge (parentUri = the parent's working uri) + the op kind
|
|
191
|
+
// as origin, and inheriting any prompt/model/genKind lineage from the parent.
|
|
192
|
+
const onSpawn = useCallback(
|
|
193
|
+
(parent: StudioItem, produced: MediaRef[], kind: StudioItemOp["kind"]) => {
|
|
194
|
+
if (produced.length === 0) return;
|
|
195
|
+
setItems((prev) => [
|
|
196
|
+
...prev,
|
|
197
|
+
...produced.map((ref) =>
|
|
198
|
+
makeStudioItem(ref, {
|
|
199
|
+
origin: `op:${kind}`,
|
|
200
|
+
parentUri: parent.working.uri,
|
|
201
|
+
...(parent.provenance.prompt != null
|
|
202
|
+
? { prompt: parent.provenance.prompt }
|
|
203
|
+
: {}),
|
|
204
|
+
...(parent.provenance.model != null
|
|
205
|
+
? { model: parent.provenance.model }
|
|
206
|
+
: {}),
|
|
207
|
+
...(parent.provenance.genKind != null
|
|
208
|
+
? { genKind: parent.provenance.genKind }
|
|
209
|
+
: {}),
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
]);
|
|
213
|
+
},
|
|
214
|
+
[],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Revert the item's `working` back to its immutable source (ops history preserved).
|
|
218
|
+
const onRevert = useCallback((target: StudioItem) => {
|
|
219
|
+
setItems((prev) =>
|
|
220
|
+
prev.map((it) => (it.key === target.key ? revertItem(it) : it)),
|
|
221
|
+
);
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
// Forget one item's card (local only — never deletes the underlying server asset).
|
|
225
|
+
const onRemove = useCallback((target: StudioItem) => {
|
|
226
|
+
setItems((prev) => prev.filter((it) => it.key !== target.key));
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
// studioBridge "Send to Studio" / "Restyle": only the registering mount (the standalone
|
|
230
|
+
// station) receives staged sources; the Generate-mode mount passes registerBridge=false.
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (!registerBridge) return;
|
|
233
|
+
return registerStudioStager((req) => {
|
|
234
|
+
const ref = req.sourceVideo ?? null;
|
|
235
|
+
applyStage(ref, req.mode ?? "extend");
|
|
236
|
+
// Mint the first-class StudioItem for this handoff, carrying the forwarded
|
|
237
|
+
// provenance (or a "send-to-studio" default). Additive — see the `items` note.
|
|
238
|
+
if (ref) {
|
|
239
|
+
const provenance = req.provenance ?? { origin: "send-to-studio", parentUri: ref.uri };
|
|
240
|
+
setItems((prev) => [...prev, makeStudioItem(ref, provenance)]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}, [registerBridge, applyStage]);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className="vi-studio-workbench" ref={topRef}>
|
|
247
|
+
{/* CENTER, movie skeleton: viewer on top → generator below. Viewer header controls
|
|
248
|
+
(Play from library / Clear / follow-newest toggle) are wired here so both
|
|
249
|
+
plane mounts get them for free — see the `pinned` note above. */}
|
|
250
|
+
<StudioViewer
|
|
251
|
+
clips={clipsState.clips}
|
|
252
|
+
selected={selected}
|
|
253
|
+
pinned={pinned}
|
|
254
|
+
onPick={pickViewerClip}
|
|
255
|
+
onClear={clearViewer}
|
|
256
|
+
onToggleFollow={toggleFollowNewest}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
{/* Slice 2b: the staged studio items as a strip of first-class cards with inline
|
|
260
|
+
per-item ops. Additive — it sits ABOVE the generate surface and does not touch
|
|
261
|
+
the surface or its props (slice 3 folds these together). Rendered in BOTH plane
|
|
262
|
+
mounts identically (this is the one shared StudioPlane). Each card is wrapped in
|
|
263
|
+
a keyed intrinsic <div> so the map key never lands on a component (which the
|
|
264
|
+
bare-tsc env flags as a spurious prop). */}
|
|
265
|
+
{items.length > 0 && (
|
|
266
|
+
<div className="vi-studio-item-strip" aria-label="Studio items">
|
|
267
|
+
{items.map((it) => (
|
|
268
|
+
<div className="vi-studio-item-cell" key={it.key}>
|
|
269
|
+
<StudioItemCard
|
|
270
|
+
item={it}
|
|
271
|
+
onApplyOp={onApplyOp}
|
|
272
|
+
onSpawn={onSpawn}
|
|
273
|
+
onRevert={onRevert}
|
|
274
|
+
onRemove={onRemove}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
<StudioGenerateSurface
|
|
282
|
+
presets={presets}
|
|
283
|
+
presetsLoading={presetsLoading}
|
|
284
|
+
clips={clipsState.clips}
|
|
285
|
+
source={source}
|
|
286
|
+
setSource={setSource}
|
|
287
|
+
sourceMode={sourceMode}
|
|
288
|
+
stageSeq={stageSeq}
|
|
289
|
+
onEnqueued={clipsState.refresh}
|
|
290
|
+
settingsHost={reg.settingsHost}
|
|
291
|
+
lockedSurfaceMode={lockedSurfaceMode}
|
|
292
|
+
/>
|
|
293
|
+
{/* STUDIO-ASSIST LIVE LOG → portaled into the sidebar's ACTIVE (Active
|
|
294
|
+
Processes) tab via the registry's activeExtra host (operator correction
|
|
295
|
+
2026-08-05: "active tab" = Active Processes). Single mount, shared stream. */}
|
|
296
|
+
{reg.activeExtraHost ? createPortal(<StudioAssistLogPanel />, reg.activeExtraHost) : null}
|
|
297
|
+
{/* Studio's in-flight renders are shown by the shared Active Processes panel via the
|
|
298
|
+
fleet-wide GET /llm/jobs feed (no per-plane portal — that would double-count each
|
|
299
|
+
render, since the same job appears in /llm/jobs with transport "media"). */}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|