@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,245 @@
|
|
|
1
|
+
// CONDENSED KNOB STRIP + CARRY TICKS — extracted from GenerateStation.tsx (task
|
|
2
|
+
// k88) so the Scene/Clip prompt-card migration (k89) can consume the same strip
|
|
3
|
+
// without importing the whole station. Everything below moved VERBATIM (values,
|
|
4
|
+
// flags, ticks, layout); GenerateStation imports it back unchanged.
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { KnobInfo } from "./KnobInfo";
|
|
7
|
+
import type { CarrySettingKey, CarryTicks } from "../video/contract";
|
|
8
|
+
|
|
9
|
+
// Parse a text field into a number, or null when blank/invalid (mirrors the
|
|
10
|
+
// Frames station's helper — no silent coercion of garbage to 0). Exported so
|
|
11
|
+
// GenerateStation (its original home) keeps its single source.
|
|
12
|
+
export function toNumberOrNull(raw: string): number | null {
|
|
13
|
+
const s = raw.trim();
|
|
14
|
+
if (s === "") return null;
|
|
15
|
+
const n = Number(s);
|
|
16
|
+
return Number.isFinite(n) ? n : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The five numeric knobs the condensed strip edits, as the live text they are. */
|
|
20
|
+
export interface KnobValues {
|
|
21
|
+
width: string;
|
|
22
|
+
height: string;
|
|
23
|
+
steps: string;
|
|
24
|
+
guidance: string;
|
|
25
|
+
seed: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The strip's validation flags, derived from the raw text — the SAME rules the
|
|
30
|
+
* station's Run gate uses (widthValid/heightValid/stepsValid/guidanceValid/
|
|
31
|
+
* seedValid + the multiple-of-8 warning), restated here so the component is
|
|
32
|
+
* self-contained and every host (base composer, every tester row) flags identically.
|
|
33
|
+
* Returned as a list so the strip itself stays ONE line and the flags stack under it.
|
|
34
|
+
*/
|
|
35
|
+
function knobFlags(v: KnobValues): string[] {
|
|
36
|
+
const out: string[] = [];
|
|
37
|
+
const w = toNumberOrNull(v.width);
|
|
38
|
+
const h = toNumberOrNull(v.height);
|
|
39
|
+
const st = toNumberOrNull(v.steps);
|
|
40
|
+
const g = toNumberOrNull(v.guidance);
|
|
41
|
+
const sd = toNumberOrNull(v.seed);
|
|
42
|
+
const wOk = w != null && Number.isInteger(w) && w > 0;
|
|
43
|
+
const hOk = h != null && Number.isInteger(h) && h > 0;
|
|
44
|
+
if (!wOk) out.push("width: enter a positive integer.");
|
|
45
|
+
else if (w % 8 !== 0) out.push("width: not a multiple of 8 — the model may round it.");
|
|
46
|
+
if (!hOk) out.push("height: enter a positive integer.");
|
|
47
|
+
else if (h % 8 !== 0) out.push("height: not a multiple of 8 — the model may round it.");
|
|
48
|
+
if (!(st != null && Number.isInteger(st) && st > 0))
|
|
49
|
+
out.push("steps: enter a positive integer.");
|
|
50
|
+
if (!(g != null && g >= 0)) out.push("guidance: enter a number ≥ 0.");
|
|
51
|
+
if (!(v.seed.trim() === "" || (sd != null && Number.isInteger(sd) && sd >= 0)))
|
|
52
|
+
out.push("seed: enter a non-negative integer or leave blank.");
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ONE carry tick — the small checkbox that rides in front of a setting's label
|
|
58
|
+
* (operator ask 2026-08-04, k63). Checked = this setting is carried into the
|
|
59
|
+
* component created from this one; unchecked = that component gets the mode
|
|
60
|
+
* default instead. Deliberately label-less and title-only: the strip is condensed,
|
|
61
|
+
* and a word of copy per tick would undo the condensing.
|
|
62
|
+
*/
|
|
63
|
+
export function CarryTick({
|
|
64
|
+
id,
|
|
65
|
+
setting,
|
|
66
|
+
checked,
|
|
67
|
+
onChange,
|
|
68
|
+
}: {
|
|
69
|
+
id: string;
|
|
70
|
+
setting: string;
|
|
71
|
+
checked: boolean;
|
|
72
|
+
onChange: (v: boolean) => void;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<input
|
|
76
|
+
id={id}
|
|
77
|
+
type="checkbox"
|
|
78
|
+
className="vi-carry-tick"
|
|
79
|
+
checked={checked}
|
|
80
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
81
|
+
aria-label={`Carry ${setting} into the next component`}
|
|
82
|
+
title={`Carry ${setting} into the component created from this one. Unticked = that component gets the default instead.`}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* CONDENSED KNOB STRIP (operator ask 2026-08-04, k63) — the generation knobs as
|
|
89
|
+
* ONE inline line rather than a column of full-width blocks:
|
|
90
|
+
*
|
|
91
|
+
* [model ▾] ☑ size [512] × [512] ☑ steps [4] ☑ guidance [0] ☑ seed [random]
|
|
92
|
+
*
|
|
93
|
+
* WHY a component and not just JSX: the tester rows must look and behave EXACTLY
|
|
94
|
+
* like the base composer's settings, because the whole point of a row is "the same
|
|
95
|
+
* generation with one thing changed". Two hand-written copies of the same seven
|
|
96
|
+
* inputs is how they drift. Both hosts render this; there is one visual language.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately presentational: it owns no state. Values come in, `onPatch` goes
|
|
99
|
+
* out (the base host maps it onto the station setters, a tester row onto
|
|
100
|
+
* patchTestRow), and the carry ticks are the host's state too. `lead`/`trail` are
|
|
101
|
+
* the host-specific extremities — the model select cluster (which differs: the base
|
|
102
|
+
* carries a refresh button + capability line, a row does not) and the row's inline
|
|
103
|
+
* negative field.
|
|
104
|
+
*/
|
|
105
|
+
export function CondensedKnobStrip({
|
|
106
|
+
idPrefix,
|
|
107
|
+
values,
|
|
108
|
+
onPatch,
|
|
109
|
+
carry,
|
|
110
|
+
onCarry,
|
|
111
|
+
lead,
|
|
112
|
+
trail,
|
|
113
|
+
footer,
|
|
114
|
+
}: {
|
|
115
|
+
/** DOM id namespace — must be unique per instance (ids collide across rows otherwise). */
|
|
116
|
+
idPrefix: string;
|
|
117
|
+
values: KnobValues;
|
|
118
|
+
onPatch: (patch: Partial<KnobValues>) => void;
|
|
119
|
+
/** Omit to render the strip without carry ticks (nothing is seeded from it). */
|
|
120
|
+
carry?: CarryTicks;
|
|
121
|
+
onCarry?: (key: CarrySettingKey, value: boolean) => void;
|
|
122
|
+
lead?: ReactNode;
|
|
123
|
+
trail?: ReactNode;
|
|
124
|
+
footer?: ReactNode;
|
|
125
|
+
}) {
|
|
126
|
+
const flags = knobFlags(values);
|
|
127
|
+
const tick = (key: CarrySettingKey, setting: string) =>
|
|
128
|
+
carry ? (
|
|
129
|
+
<CarryTick
|
|
130
|
+
id={`${idPrefix}-carry-${key}`}
|
|
131
|
+
setting={setting}
|
|
132
|
+
checked={carry[key]}
|
|
133
|
+
onChange={(v) => onCarry?.(key, v)}
|
|
134
|
+
/>
|
|
135
|
+
) : null;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="vi-knob-strip">
|
|
139
|
+
<div className="vi-knob-strip-line">
|
|
140
|
+
{lead}
|
|
141
|
+
|
|
142
|
+
{/* width × height read as ONE setting ("size"), so they pair behind one
|
|
143
|
+
tick and one ⓘ — a literal × between two mini inputs. */}
|
|
144
|
+
<span className="vi-knob-mini-group">
|
|
145
|
+
{tick("size", "size")}
|
|
146
|
+
<label className="vi-knob-mini-label" htmlFor={`${idPrefix}-width`}>
|
|
147
|
+
size
|
|
148
|
+
</label>
|
|
149
|
+
<input
|
|
150
|
+
id={`${idPrefix}-width`}
|
|
151
|
+
type="text"
|
|
152
|
+
inputMode="numeric"
|
|
153
|
+
className="vi-knob-input vi-knob-mini"
|
|
154
|
+
aria-label="width"
|
|
155
|
+
value={values.width}
|
|
156
|
+
onChange={(e) => onPatch({ width: e.target.value })}
|
|
157
|
+
/>
|
|
158
|
+
<span className="vi-knob-mini-x" aria-hidden>
|
|
159
|
+
×
|
|
160
|
+
</span>
|
|
161
|
+
<input
|
|
162
|
+
id={`${idPrefix}-height`}
|
|
163
|
+
type="text"
|
|
164
|
+
inputMode="numeric"
|
|
165
|
+
className="vi-knob-input vi-knob-mini"
|
|
166
|
+
aria-label="height"
|
|
167
|
+
value={values.height}
|
|
168
|
+
onChange={(e) => onPatch({ height: e.target.value })}
|
|
169
|
+
/>
|
|
170
|
+
<KnobInfo
|
|
171
|
+
label="size"
|
|
172
|
+
text="Output width × height in pixels. Multiples of 8 avoid the model silently rounding them."
|
|
173
|
+
/>
|
|
174
|
+
</span>
|
|
175
|
+
|
|
176
|
+
<span className="vi-knob-mini-group">
|
|
177
|
+
{tick("steps", "steps")}
|
|
178
|
+
<label className="vi-knob-mini-label" htmlFor={`${idPrefix}-steps`}>
|
|
179
|
+
steps
|
|
180
|
+
</label>
|
|
181
|
+
<input
|
|
182
|
+
id={`${idPrefix}-steps`}
|
|
183
|
+
type="text"
|
|
184
|
+
inputMode="numeric"
|
|
185
|
+
className="vi-knob-input vi-knob-mini"
|
|
186
|
+
value={values.steps}
|
|
187
|
+
onChange={(e) => onPatch({ steps: e.target.value })}
|
|
188
|
+
/>
|
|
189
|
+
<KnobInfo label="steps" text="Denoising steps. sd-turbo runs well at ~4." />
|
|
190
|
+
</span>
|
|
191
|
+
|
|
192
|
+
<span className="vi-knob-mini-group">
|
|
193
|
+
{tick("guidance", "guidance")}
|
|
194
|
+
<label className="vi-knob-mini-label" htmlFor={`${idPrefix}-guidance`}>
|
|
195
|
+
guidance
|
|
196
|
+
</label>
|
|
197
|
+
<input
|
|
198
|
+
id={`${idPrefix}-guidance`}
|
|
199
|
+
type="text"
|
|
200
|
+
inputMode="decimal"
|
|
201
|
+
className="vi-knob-input vi-knob-mini"
|
|
202
|
+
value={values.guidance}
|
|
203
|
+
onChange={(e) => onPatch({ guidance: e.target.value })}
|
|
204
|
+
/>
|
|
205
|
+
<KnobInfo
|
|
206
|
+
label="guidance"
|
|
207
|
+
text="CFG scale. sd-turbo uses 0 (guidance-free)."
|
|
208
|
+
/>
|
|
209
|
+
</span>
|
|
210
|
+
|
|
211
|
+
<span className="vi-knob-mini-group">
|
|
212
|
+
{tick("seed", "seed")}
|
|
213
|
+
<label className="vi-knob-mini-label" htmlFor={`${idPrefix}-seed`}>
|
|
214
|
+
seed
|
|
215
|
+
</label>
|
|
216
|
+
<input
|
|
217
|
+
id={`${idPrefix}-seed`}
|
|
218
|
+
type="text"
|
|
219
|
+
inputMode="numeric"
|
|
220
|
+
className="vi-knob-input vi-knob-mini vi-knob-mini-seed"
|
|
221
|
+
placeholder="random"
|
|
222
|
+
value={values.seed}
|
|
223
|
+
onChange={(e) => onPatch({ seed: e.target.value })}
|
|
224
|
+
/>
|
|
225
|
+
<KnobInfo label="seed" text="Optional. Blank = random each run." />
|
|
226
|
+
</span>
|
|
227
|
+
|
|
228
|
+
{trail}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Flags stack BELOW the line so a warning never breaks the strip's single
|
|
232
|
+
row. Same rules, same amber .vi-knob-flag as the rail knobs had. */}
|
|
233
|
+
{flags.length > 0 && (
|
|
234
|
+
<div className="vi-knob-strip-flags">
|
|
235
|
+
{flags.map((f) => (
|
|
236
|
+
<span className="vi-knob-flag" key={f}>
|
|
237
|
+
{f}
|
|
238
|
+
</span>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
{footer}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ConsolePanel — the hugpy console (dev/ui's `/console`) embedded in the shared
|
|
2
|
+
// expand-into-the-screen drawer, so the operator never has to keep a second
|
|
3
|
+
// browser tab open just to watch it. Reachable from every station (the drawer is
|
|
4
|
+
// mounted once by WorkbenchSidebar, the shared sidebar), without duplicating the
|
|
5
|
+
// affordance per-station.
|
|
6
|
+
//
|
|
7
|
+
// Since 2026-08-06 this file no longer renders a drawer of its own: it
|
|
8
|
+
// contributes ONE TAB to the single unified drawer (drawerShell.tsx →
|
|
9
|
+
// WorkbenchDrawer.tsx), alongside ComfyUI and Active. The shell owns the window
|
|
10
|
+
// (portal-to-<body>, half/full sizing, Esc-to-close, persisted open/size/tab);
|
|
11
|
+
// this file owns only the console-specific part, i.e. the iframe below and its
|
|
12
|
+
// security posture.
|
|
13
|
+
//
|
|
14
|
+
// ── SAME-ORIGIN, NOT SANDBOXED (read before touching the iframe) ────────────
|
|
15
|
+
// The console SPA (dev/ui App.jsx) is a SIBLING route of THIS one, served from
|
|
16
|
+
// the SAME origin on dev.hugpy.ai (and on a local `hugpy serve`, per the
|
|
17
|
+
// product's self-hosted-first posture) — never a foreign site. Reusing
|
|
18
|
+
// nav/Navbar's own `siteBase`/`onHugpyOrigin` rule (the SAME computation the
|
|
19
|
+
// Navbar's plain `<a href="/console">` link already uses) means the href is
|
|
20
|
+
// "/console" (origin-relative → guaranteed same-origin) on every real hugpy
|
|
21
|
+
// deployment; it only falls through to the full `siteUrl` off a hugpy hostname
|
|
22
|
+
// (e.g. a bare local Vite preview), exactly like the Navbar link already does.
|
|
23
|
+
// Same-origin means the browser sends the operator's existing console session
|
|
24
|
+
// cookie — logged in outside, logged in inside the panel; logged out outside,
|
|
25
|
+
// the panel shows the console's own front door/login, same as opening the tab
|
|
26
|
+
// would. No new auth surface, nothing to weaken.
|
|
27
|
+
//
|
|
28
|
+
// Deliberately UNSANDBOXED: the console (ApiAccess "Revoke key", ModelTable
|
|
29
|
+
// "Delete"/"Prune", …) relies on window.confirm()/alert() for destructive
|
|
30
|
+
// actions and navigator.clipboard for copy buttons. A sandboxed iframe without
|
|
31
|
+
// `allow-modals` silently swallows confirm()/alert() (the dialog never shows;
|
|
32
|
+
// confirm() just returns false) — Delete/Revoke would look broken. Since this
|
|
33
|
+
// is first-party same-origin content (not a foreign/untrusted embed like
|
|
34
|
+
// ComfyStation's user-supplied URL), there is no isolation benefit to
|
|
35
|
+
// sandboxing it, only breakage — so `sandbox` is omitted entirely. Sharing a
|
|
36
|
+
// window with the SANDBOXED ComfyUI tab changes nothing here: they are separate
|
|
37
|
+
// <iframe> elements with independent sandbox attributes, never one reused frame.
|
|
38
|
+
//
|
|
39
|
+
// ── FRAME-ANCESTORS (pre-checked, see task notes) ────────────────────────────
|
|
40
|
+
// Neither the Flask app (abstract_hugpy_dev/flask_app) nor the dev.hugpy.ai
|
|
41
|
+
// nginx vhost sends X-Frame-Options or a frame-ancestors CSP, so the browser
|
|
42
|
+
// does not block this frame. Re-verify after any nginx/Flask header change.
|
|
43
|
+
import { siteBase } from "../nav/Navbar";
|
|
44
|
+
import type { DrawerTab } from "./drawerShell";
|
|
45
|
+
|
|
46
|
+
// Same origin-relative rule the Navbar's own Console link uses.
|
|
47
|
+
const consoleHref = `${siteBase}/console`;
|
|
48
|
+
|
|
49
|
+
/** The console's tab in the unified drawer. */
|
|
50
|
+
export function consoleDrawerTab(): DrawerTab<"console"> {
|
|
51
|
+
return {
|
|
52
|
+
id: "console",
|
|
53
|
+
label: "Console",
|
|
54
|
+
hint: "Open the hugpy console alongside this station — no tab switch",
|
|
55
|
+
toggleClassName: "vi-console-toggle",
|
|
56
|
+
toggleLabel: "▤ Console",
|
|
57
|
+
ariaLabel: "hugpy console",
|
|
58
|
+
// Stays in the DOM while another tab shows: an iframe that is merely
|
|
59
|
+
// `display:none` keeps its document, its session and its scroll position, so
|
|
60
|
+
// switching to Active and back does not throw the operator out to a reload.
|
|
61
|
+
keepMounted: true,
|
|
62
|
+
headerActions: (
|
|
63
|
+
<a
|
|
64
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
65
|
+
href={consoleHref}
|
|
66
|
+
target="_blank"
|
|
67
|
+
rel="noreferrer"
|
|
68
|
+
title="Open the console in its own tab"
|
|
69
|
+
>
|
|
70
|
+
Open in new tab ↗
|
|
71
|
+
</a>
|
|
72
|
+
),
|
|
73
|
+
body: (
|
|
74
|
+
<iframe
|
|
75
|
+
className="vi-console-iframe"
|
|
76
|
+
src={consoleHref}
|
|
77
|
+
title="hugpy console"
|
|
78
|
+
allow="clipboard-read; clipboard-write; fullscreen"
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Frames & Models station (Phase 4) — the sample image's left rail + frame grid.
|
|
2
|
+
// Flow:
|
|
3
|
+
// video file → POST /uploads (multipart) → {path}
|
|
4
|
+
// → POST /video/ingest {path} → MediaRef (guard kind==="video")
|
|
5
|
+
// → FrameExtractCore: knob rail + frame_extract job + frame grid + pick
|
|
6
|
+
// Every hop goes through request<T>() + config URLs; errors surface via
|
|
7
|
+
// describeAppError.
|
|
8
|
+
//
|
|
9
|
+
// Studio slice 2a: the Options knob rail + useFrameJobs + frame grid + frame-pick
|
|
10
|
+
// were extracted VERBATIM into stations/ops/FrameExtractCore. Because the Frames
|
|
11
|
+
// station has an Options panel (whose knob state is shared with the working view),
|
|
12
|
+
// the Core owns the whole SectionTabs and takes this station's add-file bar node +
|
|
13
|
+
// `busy` flag as props. This station now owns only the upload/ingest lifecycle.
|
|
14
|
+
// Behaviour is unchanged.
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import type { StationSpec } from "./types";
|
|
17
|
+
import { AddFileBar } from "./AddFileBar";
|
|
18
|
+
import { FrameExtractCore } from "./ops/FrameExtractCore";
|
|
19
|
+
import { request, okValue, errorOf, describeAppError } from "../transport/client";
|
|
20
|
+
import { hugpyConfig } from "../config";
|
|
21
|
+
import { getSessionId } from "../session";
|
|
22
|
+
import {
|
|
23
|
+
mediaRefSchema,
|
|
24
|
+
uploadResultSchema,
|
|
25
|
+
type MediaRef,
|
|
26
|
+
} from "../video/contract";
|
|
27
|
+
|
|
28
|
+
type Phase = "idle" | "uploading" | "ingesting" | "ready";
|
|
29
|
+
|
|
30
|
+
const PHASE_LABEL: Record<Phase, string> = {
|
|
31
|
+
idle: "",
|
|
32
|
+
uploading: "Uploading…",
|
|
33
|
+
ingesting: "Ingesting…",
|
|
34
|
+
ready: "",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function FrameExtractStation({ spec }: { spec: StationSpec }) {
|
|
38
|
+
const [source, setSource] = useState<MediaRef | null>(null);
|
|
39
|
+
const [fileName, setFileName] = useState<string | null>(null);
|
|
40
|
+
const [phase, setPhase] = useState<Phase>("idle");
|
|
41
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const busy = phase === "uploading" || phase === "ingesting";
|
|
44
|
+
|
|
45
|
+
async function onPick(file: File) {
|
|
46
|
+
setError(null);
|
|
47
|
+
setFileName(file.name);
|
|
48
|
+
setSource(null);
|
|
49
|
+
setPhase("uploading");
|
|
50
|
+
|
|
51
|
+
// (a) multipart upload — field `file` + field `sid`. No Content-Type header:
|
|
52
|
+
// the browser sets the multipart boundary itself.
|
|
53
|
+
const fd = new FormData();
|
|
54
|
+
fd.append("file", file);
|
|
55
|
+
fd.append("sid", getSessionId());
|
|
56
|
+
const up = await request<unknown>(hugpyConfig.uploadUrl, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: fd,
|
|
59
|
+
meta: { specKey: "frames", operation: "upload" },
|
|
60
|
+
});
|
|
61
|
+
if (!up.ok) {
|
|
62
|
+
setError(describeAppError(errorOf(up)));
|
|
63
|
+
setPhase("idle");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const upParsed = uploadResultSchema.safeParse(okValue(up));
|
|
67
|
+
if (!upParsed.success) {
|
|
68
|
+
setError("Malformed upload response.");
|
|
69
|
+
setPhase("idle");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// (b) ingest the server path into a typed MediaRef.
|
|
74
|
+
setPhase("ingesting");
|
|
75
|
+
const ing = await request<unknown>(hugpyConfig.videoIngestUrl, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify({ path: upParsed.data.path }),
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
meta: { specKey: "frames", operation: "ingest" },
|
|
80
|
+
});
|
|
81
|
+
if (!ing.ok) {
|
|
82
|
+
setError(describeAppError(errorOf(ing)));
|
|
83
|
+
setPhase("idle");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const media = mediaRefSchema.safeParse(okValue(ing));
|
|
87
|
+
if (!media.success) {
|
|
88
|
+
setError("Malformed ingest response.");
|
|
89
|
+
setPhase("idle");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (media.data.kind !== "video") {
|
|
93
|
+
setError(
|
|
94
|
+
`Ingested asset is "${media.data.kind}", not a video — use the Image Crop or Audio station for that.`,
|
|
95
|
+
);
|
|
96
|
+
setPhase("idle");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setSource(media.data);
|
|
100
|
+
setPhase("ready");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clear the loaded source back to the empty state. FrameExtractCore resets the job
|
|
104
|
+
// + selection when the source reference changes (here → null).
|
|
105
|
+
function clearSource() {
|
|
106
|
+
setSource(null);
|
|
107
|
+
setFileName(null);
|
|
108
|
+
setError(null);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<section className="station-card station-wide">
|
|
113
|
+
<FrameExtractCore
|
|
114
|
+
source={source}
|
|
115
|
+
busy={busy}
|
|
116
|
+
addFile={
|
|
117
|
+
<>
|
|
118
|
+
<AddFileBar
|
|
119
|
+
accept="video/*"
|
|
120
|
+
onFile={onPick}
|
|
121
|
+
busy={busy}
|
|
122
|
+
buttonLabel={source ? "Choose another video" : "Choose video"}
|
|
123
|
+
onClear={source && !busy ? clearSource : undefined}
|
|
124
|
+
>
|
|
125
|
+
{busy && <span className="vi-crop-status">{PHASE_LABEL[phase]}</span>}
|
|
126
|
+
{!busy && source && (
|
|
127
|
+
<>
|
|
128
|
+
{fileName && (
|
|
129
|
+
<span className="vi-addbar-name" title={fileName}>
|
|
130
|
+
{fileName}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
<span className="vi-crop-meta">
|
|
134
|
+
<code>{source.mime}</code>
|
|
135
|
+
{source.width && source.height ? ` · ${source.width}×${source.height}` : ""}
|
|
136
|
+
{source.duration_s != null ? ` · ${source.duration_s}s` : ""}
|
|
137
|
+
{source.fps_native != null ? ` · ${source.fps_native} fps native` : ""}
|
|
138
|
+
</span>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</AddFileBar>
|
|
142
|
+
{error && <p className="vi-error" role="alert">{error}</p>}
|
|
143
|
+
</>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
</section>
|
|
147
|
+
);
|
|
148
|
+
}
|