@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,191 @@
|
|
|
1
|
+
// The station registry — every capability page of the video-intelligence arm,
|
|
2
|
+
// declared in one place. Order = tab order. All four start as "planned"
|
|
3
|
+
// placeholders (Phase 0); each flips to "active" when its roadmap phase lands.
|
|
4
|
+
// See ROADMAP.md for the sequence and hugpy_video_intelligence_map.md for the
|
|
5
|
+
// schemas/jobs each station drives.
|
|
6
|
+
import type { StationSpec } from "./types";
|
|
7
|
+
import { ImageCropStation } from "./ImageCropStation";
|
|
8
|
+
import { AudioCropStation } from "./AudioCropStation";
|
|
9
|
+
import { FrameExtractStation } from "./FrameExtractStation";
|
|
10
|
+
import { GenerateStation } from "./GenerateStation";
|
|
11
|
+
import { ComfyStation } from "./ComfyStation";
|
|
12
|
+
import { StudioClipsStation } from "./StudioClipsStation";
|
|
13
|
+
import { IdentitiesStation } from "./studio/IdentitiesStation";
|
|
14
|
+
import { ActiveProcessesStation } from "./ActiveProcessesStation";
|
|
15
|
+
|
|
16
|
+
export const STATIONS: readonly StationSpec[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "generate",
|
|
19
|
+
title: "Studio",
|
|
20
|
+
blurb:
|
|
21
|
+
"Ordered multimodal prompt (text + images + video) → image, with video parts resolved by the chain registry.",
|
|
22
|
+
phase: "Phases 6–7",
|
|
23
|
+
buildsSpec: "GenerateImageSpec",
|
|
24
|
+
planned: [
|
|
25
|
+
"MediaInputReceptacle collecting an ordered text/image/video prompt",
|
|
26
|
+
"Cropped frames from other stations selectable via FieldSpec propagation",
|
|
27
|
+
"Model dropdown filtered to text2image / image2image tasks",
|
|
28
|
+
"video → frame_extract → frame-pick → conditioning chain (never runner special-casing)",
|
|
29
|
+
],
|
|
30
|
+
group: "studio",
|
|
31
|
+
status: "active",
|
|
32
|
+
component: GenerateStation,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "studio-clips",
|
|
36
|
+
title: "Studio",
|
|
37
|
+
blurb:
|
|
38
|
+
"Cinema-studio workspace on a single plane: the clip viewer on top with tier-first generate controls (t2v/i2v/v2v, morphing param panel, start image + source clip) below. Produced clips land in the shared left-sidebar Session Library (extend / restyle from there).",
|
|
39
|
+
phase: "Studio",
|
|
40
|
+
buildsSpec: "StudioI2VSpec (enqueues + plays cinema-studio clips)",
|
|
41
|
+
planned: [
|
|
42
|
+
"Single plane: preset tiers (real vs synthetic) + morphing param panel + start-image/source-clip inputs → Generate (POST /video/studio/i2v)",
|
|
43
|
+
"Inline viewer + durable clip list (GET /video/studio/clips): list-left / player-right, autoplay follows the newest clip",
|
|
44
|
+
"Range-seekable playback via GET /video/studio/clip/<job_id> + per-row creation-params Details expander",
|
|
45
|
+
"Compact collapsible filters (status/capability/model) + per-clip actions: play, copy path, use-as-source, cancel",
|
|
46
|
+
],
|
|
47
|
+
group: "studio",
|
|
48
|
+
// Retired from the nav strip: this standalone "Studio" page mirrors the studio
|
|
49
|
+
// MODE of the renamed Studio (ex-Generate) tab, so it's redundant in the nav.
|
|
50
|
+
// Route kept live (WorkbenchStation still resolves /studio-clips) so the
|
|
51
|
+
// sidebar's "Send to Studio" / "Restyle" deep-links keep working.
|
|
52
|
+
navHidden: true,
|
|
53
|
+
status: "active",
|
|
54
|
+
component: StudioClipsStation,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "identities",
|
|
58
|
+
title: "Identities",
|
|
59
|
+
blurb:
|
|
60
|
+
"The identity-profile library: procure a character's curated reference set once — create, rename, edit its notes/references, and archive — for reuse anywhere via identity_profile:<slug>. The Studio Generate/Movie tabs' \"👤 From profile\" picker is where a saved one gets attached to a render; this station is where it's made and maintained.",
|
|
61
|
+
phase: "Studio",
|
|
62
|
+
buildsSpec: "IdentityProfile (create / PATCH update / archive)",
|
|
63
|
+
planned: [
|
|
64
|
+
"Create panel: name + notes + reference images, via upload or the session-library picker",
|
|
65
|
+
"Grid of saved profiles: thumbnail strip, name, created date, notes preview, reference count",
|
|
66
|
+
"Inline per-card edit: rename (display-only — the slug stays stable) + notes + add/remove references",
|
|
67
|
+
"Archive (never-delete doctrine) per profile",
|
|
68
|
+
"Stage (b), next: '✦ Generate character sheet' — turnaround canon generation from a profile",
|
|
69
|
+
],
|
|
70
|
+
group: "studio",
|
|
71
|
+
status: "active",
|
|
72
|
+
component: IdentitiesStation,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "image-crop",
|
|
76
|
+
title: "Image Crop",
|
|
77
|
+
blurb:
|
|
78
|
+
"Spatial crop station: draw one or many bboxes over an image, enqueue crop jobs through the bus.",
|
|
79
|
+
phase: "Phase 3",
|
|
80
|
+
buildsSpec: "CropSpec (spatial axis)",
|
|
81
|
+
planned: [
|
|
82
|
+
"SpatialRegionEditor — draggable/resizable bbox, aspect lock, zoom/pan",
|
|
83
|
+
"Numeric x/y/w/h inputs + model-native presets (1:1, 16:9, 1024²)",
|
|
84
|
+
"Multi-crop queue off one source",
|
|
85
|
+
"Enqueue crop → subscribe by job_id → result in the store",
|
|
86
|
+
],
|
|
87
|
+
group: "studio",
|
|
88
|
+
status: "active",
|
|
89
|
+
component: ImageCropStation,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "audio-crop",
|
|
93
|
+
title: "Audio Crop",
|
|
94
|
+
blurb:
|
|
95
|
+
"Temporal crop station: waveform region editing over uploaded audio or a video's extracted track.",
|
|
96
|
+
phase: "Phase 5",
|
|
97
|
+
buildsSpec: "CropSpec (temporal axis)",
|
|
98
|
+
planned: [
|
|
99
|
+
"TemporalRegionEditor — waveform, draggable region handles, timeline zoom",
|
|
100
|
+
"Scrub + region-loop playback, numeric start/end, spectrogram toggle",
|
|
101
|
+
"Video sources routed through the audio_extract chain first",
|
|
102
|
+
"Multi-region clip queue",
|
|
103
|
+
],
|
|
104
|
+
group: "studio",
|
|
105
|
+
status: "active",
|
|
106
|
+
component: AudioCropStation,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "frames",
|
|
110
|
+
title: "Frames & Models",
|
|
111
|
+
blurb:
|
|
112
|
+
"The Options panel: fps / quality / format / window / max-frames knobs plus the model dropdown, producing the frame grid.",
|
|
113
|
+
phase: "Phase 4",
|
|
114
|
+
buildsSpec: "FrameExtractSpec",
|
|
115
|
+
planned: [
|
|
116
|
+
"Explicit knobs — fps, quality (range per fmt), fmt, window, max_frames (loud cap)",
|
|
117
|
+
"Model dropdown sourced from the ModelConfig registry, filtered by task",
|
|
118
|
+
"Virtualized/paginated frame grid; frames land as MediaRefs, never inlined",
|
|
119
|
+
"Parallel ffmpeg fan-out on the worker (wmtrim pattern)",
|
|
120
|
+
],
|
|
121
|
+
group: "studio",
|
|
122
|
+
status: "active",
|
|
123
|
+
component: FrameExtractStation,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "comfy",
|
|
127
|
+
title: "ComfyUI",
|
|
128
|
+
blurb:
|
|
129
|
+
"Viewer station: embed a running ComfyUI (an HTTPS endpoint) in an iframe, with a user-editable, session-persisted URL.",
|
|
130
|
+
phase: "Viewer",
|
|
131
|
+
buildsSpec: "— (embeds an external endpoint; enqueues no job)",
|
|
132
|
+
planned: [
|
|
133
|
+
"Editable endpoint URL, persisted per-tab in sessionStorage (default from VITE_HUGPY_COMFY_URL)",
|
|
134
|
+
"Scheme-allowlisted iframe src (http/https only) — rejects javascript:/data:/blob:/file:",
|
|
135
|
+
"Mixed-content guidance: an HTTPS page can't frame http:// or localhost endpoints",
|
|
136
|
+
"Always-available Open-in-new-tab fallback for endpoints that refuse embedding (X-Frame-Options)",
|
|
137
|
+
],
|
|
138
|
+
group: "studio",
|
|
139
|
+
status: "active",
|
|
140
|
+
// 2026-08-04 (operator): ComfyUI is reached from the SIDEBAR OVERLAY
|
|
141
|
+
// (ComfyPanel, beside Console) rather than a studio sub-tab, so it leaves the
|
|
142
|
+
// strip. The station stays registered and navHidden so an existing /comfy
|
|
143
|
+
// bookmark still resolves here instead of bouncing to the default tab —
|
|
144
|
+
// WorkbenchStation resolves `/:workbenchTab` against the UNFILTERED list and
|
|
145
|
+
// only the strip filters on !navHidden.
|
|
146
|
+
navHidden: true,
|
|
147
|
+
component: ComfyStation,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "active",
|
|
151
|
+
title: "Active",
|
|
152
|
+
blurb:
|
|
153
|
+
"Console-wide Active Processes: every in-flight media build, its phase (incl. the awaiting_capacity GPU hold), and WHERE it physically executes (host · gpu · process) with reserved VRAM and Cancel.",
|
|
154
|
+
phase: "Observability",
|
|
155
|
+
buildsSpec: "— (reads GET /video/jobs; enqueues no job)",
|
|
156
|
+
planned: [
|
|
157
|
+
"Live in-flight jobs across every station/transport (GET /video/jobs, ~2s)",
|
|
158
|
+
"Placement line per job — ae · cuda:0 · P-studio — with an 'external' cue",
|
|
159
|
+
"awaiting_capacity hold phase with the shortfall reason + overtaken count",
|
|
160
|
+
"Reserved VRAM when a GPU reservation is held; honest progress (no fake bars); Cancel",
|
|
161
|
+
],
|
|
162
|
+
group: "studio",
|
|
163
|
+
status: "active",
|
|
164
|
+
component: ActiveProcessesStation,
|
|
165
|
+
},
|
|
166
|
+
] as const;
|
|
167
|
+
|
|
168
|
+
export function stationById(id: string): StationSpec | undefined {
|
|
169
|
+
return STATIONS.find((s) => s.id === id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Default landing station.
|
|
174
|
+
*
|
|
175
|
+
* PINNED, not derived from STATIONS[0]. It used to be `STATIONS[0].id`, which
|
|
176
|
+
* quietly coupled "which page you land on" to "which tab is leftmost" — so the
|
|
177
|
+
* 2026-08-04 tab reorder (crops/frames moved next to Active) would have silently
|
|
178
|
+
* moved the landing page too. Naming it makes the landing a decision rather than
|
|
179
|
+
* a side effect of array order; reorder the strip freely without touching where
|
|
180
|
+
* a bare "/" or an unknown tab resolves.
|
|
181
|
+
*
|
|
182
|
+
* Studio is the landing because it is the workbench people actually start in;
|
|
183
|
+
* the crop/frames utilities are secondary, which is exactly why they moved right.
|
|
184
|
+
*/
|
|
185
|
+
export const DEFAULT_STATION_ID = "generate";
|
|
186
|
+
|
|
187
|
+
// The two-tab top nav (Studio / Generate) is retired: every built station now
|
|
188
|
+
// shares the single `group:"studio"` workbench, whose ONE `.vi-subtabs` strip
|
|
189
|
+
// (rendered by WorkbenchStation) IS the whole tab bar. No SECTIONS model is
|
|
190
|
+
// derived here anymore — the shell renders the persistent sidebar + the dynamic
|
|
191
|
+
// `/:workbenchTab` route and nothing hardcodes a station list.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { mediaBytesUrl } from "../../config";
|
|
3
|
+
import type { IdentityReconstruction, IdentityAngleView } from "../../video/contract";
|
|
4
|
+
import type { ReconstructionApi } from "./useReconstruction";
|
|
5
|
+
|
|
6
|
+
interface AngleRingEditorProps {
|
|
7
|
+
reconstruction: IdentityReconstruction;
|
|
8
|
+
api: ReconstructionApi;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AngleRingEditor({ reconstruction, api }: AngleRingEditorProps) {
|
|
12
|
+
const [regeneratePrompt, setRegeneratePrompt] = useState("");
|
|
13
|
+
|
|
14
|
+
// Safely default to an empty array if views aren't populated yet
|
|
15
|
+
const views = reconstruction.views ?? [];
|
|
16
|
+
|
|
17
|
+
// The completion gate for Phase 4 (Mesh Building)
|
|
18
|
+
const allViewsApproved =
|
|
19
|
+
views.length > 0 && views.every((v) => v.status === "approved");
|
|
20
|
+
|
|
21
|
+
const meshRunning =
|
|
22
|
+
reconstruction.mesh?.status === "queued" ||
|
|
23
|
+
reconstruction.mesh?.status === "running";
|
|
24
|
+
|
|
25
|
+
// Derive an array of 8 geometrically useful anchor views for the mesh build
|
|
26
|
+
// (0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°)
|
|
27
|
+
const getMeshAnchorIds = () => {
|
|
28
|
+
const anchorDegrees = [0, 45, 90, 135, 180, 225, 270, 315];
|
|
29
|
+
return views
|
|
30
|
+
.filter((v) => anchorDegrees.includes(v.azimuthDeg))
|
|
31
|
+
.map((v) => v.viewId);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="vi-angle-ring">
|
|
36
|
+
<div className="vi-angle-ring-header">
|
|
37
|
+
<h3>Angle Ring Approval ({views.filter(v => v.status === "approved").length}/{views.length} Approved)</h3>
|
|
38
|
+
|
|
39
|
+
{/* The Final Gate: Build the 3D Asset */}
|
|
40
|
+
<button
|
|
41
|
+
className="vi-btn vi-btn-accent"
|
|
42
|
+
disabled={!allViewsApproved || meshRunning || api.running}
|
|
43
|
+
onClick={() => api.buildMesh(reconstruction.reconId, getMeshAnchorIds())}
|
|
44
|
+
>
|
|
45
|
+
{meshRunning ? "Building 3D Mesh..." : "Build Approved 3D Model"}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{reconstruction.mesh?.error && (
|
|
50
|
+
<p className="vi-error">Mesh Error: {reconstruction.mesh.error}</p>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{/* Global override prompt for regenerations */}
|
|
54
|
+
<div className="vi-knob vi-angle-ring-controls">
|
|
55
|
+
<label htmlFor="regen-prompt">Global Regeneration Prompt</label>
|
|
56
|
+
<input
|
|
57
|
+
id="regen-prompt"
|
|
58
|
+
type="text"
|
|
59
|
+
className="vi-knob-input"
|
|
60
|
+
placeholder="e.g. Preserve exact identity, clothing, and proportions..."
|
|
61
|
+
value={regeneratePrompt}
|
|
62
|
+
onChange={(e) => setRegeneratePrompt(e.target.value)}
|
|
63
|
+
/>
|
|
64
|
+
<span className="vi-knob-hint">
|
|
65
|
+
Used when regenerating specific rejected angles below. Conditioned on nearest approved neighbors.
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* The 36-Tile Grid */}
|
|
70
|
+
<div className="vi-frames-grid vi-studio-strip vi-angle-ring-grid">
|
|
71
|
+
{views.map((view) => (
|
|
72
|
+
<AngleRingTile
|
|
73
|
+
key={view.viewId}
|
|
74
|
+
view={view}
|
|
75
|
+
reconId={reconstruction.reconId}
|
|
76
|
+
api={api}
|
|
77
|
+
promptOverride={regeneratePrompt}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sub-component for individual view tiles to keep rendering clean
|
|
86
|
+
function AngleRingTile({
|
|
87
|
+
view,
|
|
88
|
+
reconId,
|
|
89
|
+
api,
|
|
90
|
+
promptOverride,
|
|
91
|
+
}: {
|
|
92
|
+
view: IdentityAngleView;
|
|
93
|
+
reconId: string;
|
|
94
|
+
api: ReconstructionApi;
|
|
95
|
+
promptOverride: string;
|
|
96
|
+
}) {
|
|
97
|
+
const isGenerating = view.status === "pending" || view.status === "generating";
|
|
98
|
+
const hasImage = view.imageUri != null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={`vi-scene-frame-cell vi-angle-tile vi-angle-tile-${view.status}`}>
|
|
102
|
+
{/* Visual Image Area */}
|
|
103
|
+
<div className="vi-angle-img-wrapper">
|
|
104
|
+
{hasImage ? (
|
|
105
|
+
<img
|
|
106
|
+
className="vi-scene-frame-img"
|
|
107
|
+
src={mediaBytesUrl(view.imageUri!)}
|
|
108
|
+
alt={`${view.azimuthDeg}° view`}
|
|
109
|
+
loading="lazy"
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="vi-angle-placeholder">
|
|
113
|
+
{isGenerating ? "Generating..." : "No Image"}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<span className="vi-frame-tag">{view.azimuthDeg}°</span>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Status & Actions */}
|
|
120
|
+
<div className="vi-angle-actions">
|
|
121
|
+
<span className={`vi-status vi-status-${view.status}`}>
|
|
122
|
+
{view.status} (v{view.version})
|
|
123
|
+
</span>
|
|
124
|
+
|
|
125
|
+
<div className="vi-angle-btn-row">
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className="vi-btn vi-btn-sm vi-btn-ghost vi-btn-approve"
|
|
129
|
+
disabled={!hasImage || view.status === "approved" || isGenerating}
|
|
130
|
+
onClick={() => api.setViewStatus(reconId, view.viewId, "approved")}
|
|
131
|
+
title="Approve this angle"
|
|
132
|
+
>
|
|
133
|
+
✓
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
className="vi-btn vi-btn-sm vi-btn-ghost vi-btn-reject"
|
|
139
|
+
disabled={!hasImage || view.status === "rejected" || isGenerating}
|
|
140
|
+
onClick={() => api.setViewStatus(reconId, view.viewId, "rejected")}
|
|
141
|
+
title="Reject this angle"
|
|
142
|
+
>
|
|
143
|
+
✕
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
149
|
+
disabled={isGenerating}
|
|
150
|
+
onClick={() => api.regenerateView(reconId, view.viewId, promptOverride, undefined, true)}
|
|
151
|
+
title="Regenerate using nearest approved neighbors"
|
|
152
|
+
>
|
|
153
|
+
↻ Regen
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|