@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,512 @@
|
|
|
1
|
+
// COMPATIBILITY DERIVATIONS over the measured render-preset table (useRenderPresets).
|
|
2
|
+
//
|
|
3
|
+
// THE RULE THIS MODULE IMPLEMENTS (operator ruling 2026-07-29): the studio must be
|
|
4
|
+
// COMPATIBILITY-AWARE of all user inputs — every input the user makes filters the
|
|
5
|
+
// options subsequently offered — and a preset must be a WORKFLOW guaranteed to render,
|
|
6
|
+
// not a canned scene, absorbing the session's inputs into their proper slots.
|
|
7
|
+
//
|
|
8
|
+
// Everything here is a PURE function of the payload + the current form state. No React,
|
|
9
|
+
// no fetch, no component coupling — the Clip surface and the Movie composer both derive
|
|
10
|
+
// their pickers from these, so the two surfaces can never disagree about what the fleet
|
|
11
|
+
// can do.
|
|
12
|
+
//
|
|
13
|
+
// ── THE COURTESY-LAYER BOUNDARY, STATED ONCE ────────────────────────────────────────
|
|
14
|
+
// These filters decide what to OFFER. They are NOT a gate and must never be treated as
|
|
15
|
+
// one: POST /video/studio/i2v's capability gate (video_routes.py) and the router's own
|
|
16
|
+
// refusal stay authoritative, and every consumer here falls back to its UNFILTERED list
|
|
17
|
+
// when the discovery fetch has not landed (`loaded === false`). A filter that empties a
|
|
18
|
+
// picker because a GET was slow is a worse failure than the one it prevents.
|
|
19
|
+
//
|
|
20
|
+
// ── WHY SOME RULES ARE NOT IN THE PRESET TABLE ──────────────────────────────────────
|
|
21
|
+
// A `RenderPreset.inputs[]` names what the caller MUST supply to reach that binding. The
|
|
22
|
+
// route additionally accepts several OPTIONAL inputs the table has no field for, and
|
|
23
|
+
// refuses some combinations the table cannot express. Both are transcribed below with
|
|
24
|
+
// the route line that owns them, because a UI that only read `inputs[]` would delete
|
|
25
|
+
// working features (v2v scene-continuity would lose its identity references) and offer
|
|
26
|
+
// broken ones (a control still beside a source clip, which the route refuses by name).
|
|
27
|
+
import type { RenderPreset, UnavailableCapability } from "./useRenderPresets";
|
|
28
|
+
|
|
29
|
+
/** The capability strings the studio's CLIP surface can author end to end. Every one is
|
|
30
|
+
* servable on the fleet today; `motion` is the newest (video_routes widened the
|
|
31
|
+
* control_image gate from id_lock-only on 2026-07-27, which is what turned it from a
|
|
32
|
+
* phantom into a route). Ordered as the picker shows them. */
|
|
33
|
+
export const CLIP_CAPABILITIES = ["t2v", "i2v", "v2v", "id_lock", "motion"] as const;
|
|
34
|
+
export type ClipCapability = (typeof CLIP_CAPABILITIES)[number];
|
|
35
|
+
|
|
36
|
+
/** The MOVIE composer's capability — the multi-segment orchestration stage. */
|
|
37
|
+
export const MOVIE_CAPABILITY = "assemble";
|
|
38
|
+
|
|
39
|
+
export const CAPABILITY_LABELS: Record<string, string> = {
|
|
40
|
+
t2v: "Text → video (t2v)",
|
|
41
|
+
i2v: "Image → video (i2v)",
|
|
42
|
+
v2v: "Video → video (v2v restyle)",
|
|
43
|
+
id_lock: "Identity lock (reference → video)",
|
|
44
|
+
motion: "Pose / structure control (motion)",
|
|
45
|
+
assemble: "Multi-segment movie (assemble)",
|
|
46
|
+
upres: "Upscale a clip (upres)",
|
|
47
|
+
interp: "Interpolate frames (interp)",
|
|
48
|
+
keyframe: "Keyframe (start + end frame)",
|
|
49
|
+
inpaint: "Inpaint a region",
|
|
50
|
+
outpaint: "Outpaint the canvas",
|
|
51
|
+
retake: "Re-take a frame range",
|
|
52
|
+
audio: "Audio track",
|
|
53
|
+
lipsync: "Lip sync",
|
|
54
|
+
restore: "Restore / face fix",
|
|
55
|
+
stream: "Streaming generation",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function capabilityLabel(cap: string): string {
|
|
59
|
+
return CAPABILITY_LABELS[cap] ?? cap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── the four attachment slots the Clip surface owns ─────────────────────────────────
|
|
63
|
+
// Named for the ROUTE FIELD each one becomes, so the matrix below can be read against
|
|
64
|
+
// video_routes.py directly.
|
|
65
|
+
export interface Attachments {
|
|
66
|
+
/** i2v `start_image` — a single first frame. */
|
|
67
|
+
startImage: boolean;
|
|
68
|
+
/** `source_video` — the clip to extend (i2v) or repaint (v2v). */
|
|
69
|
+
sourceVideo: boolean;
|
|
70
|
+
/** `reference_images` — 1–4 ordered stills (identity). */
|
|
71
|
+
referenceImages: boolean;
|
|
72
|
+
/** `control_image` (+ `control_kind`) — one pose/depth/sketch still. */
|
|
73
|
+
controlImage: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const NO_ATTACHMENTS: Attachments = {
|
|
77
|
+
startImage: false,
|
|
78
|
+
sourceVideo: false,
|
|
79
|
+
referenceImages: false,
|
|
80
|
+
controlImage: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// WHICH CAPABILITIES ACCEPT WHICH INPUT — transcribed from the route's own gates so the
|
|
84
|
+
// UI narrows to exactly what the server admits, and never one option wider.
|
|
85
|
+
//
|
|
86
|
+
// start_image i2v only. t2v DELIBERATELY drops it (video_routes: "a t2v
|
|
87
|
+
// clip is a pure function of prompt + seed +
|
|
88
|
+
// geometry"); the VACE capabilities have no
|
|
89
|
+
// start-frame channel.
|
|
90
|
+
// source_video i2v | v2v. t2v drops it; refused alongside control_image.
|
|
91
|
+
// reference_images id_lock | v2v. _REF_CAPS — "rejected for any other capability so
|
|
92
|
+
// a non-VACE runner can never silently ignore them".
|
|
93
|
+
// REQUIRED on id_lock, OPTIONAL on v2v (the
|
|
94
|
+
// scene-continuity identity carry).
|
|
95
|
+
// control_image id_lock | motion. _CONTROL_CAPS — OPTIONAL composition blocking on
|
|
96
|
+
// id_lock, REQUIRED and definitional on motion.
|
|
97
|
+
const ACCEPTS: Record<keyof Attachments, readonly string[]> = {
|
|
98
|
+
startImage: ["i2v"],
|
|
99
|
+
sourceVideo: ["i2v", "v2v"],
|
|
100
|
+
referenceImages: ["id_lock", "v2v"],
|
|
101
|
+
controlImage: ["id_lock", "motion"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Human copy for "why this capability is greyed out given what you have attached". */
|
|
105
|
+
const ATTACHMENT_NOUNS: Record<keyof Attachments, string> = {
|
|
106
|
+
startImage: "a start image",
|
|
107
|
+
sourceVideo: "a source clip",
|
|
108
|
+
referenceImages: "reference images",
|
|
109
|
+
controlImage: "a control still",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ── the table, sliced ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** Every capability at least one ratified preset covers — what the fleet can do today. */
|
|
115
|
+
export function servableCapabilities(presets: RenderPreset[]): string[] {
|
|
116
|
+
const out = new Set<string>();
|
|
117
|
+
for (const p of presets) {
|
|
118
|
+
out.add(p.capability);
|
|
119
|
+
for (const c of p.capabilities ?? []) out.add(c);
|
|
120
|
+
}
|
|
121
|
+
return [...out].sort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Every ratified preset serving `cap`, in the registry's own (meaningful) order. */
|
|
125
|
+
export function presetsForCapability(presets: RenderPreset[], cap: string): RenderPreset[] {
|
|
126
|
+
return presets.filter((p) => p.capability === cap || (p.capabilities ?? []).includes(cap));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isServable(presets: RenderPreset[], cap: string): boolean {
|
|
130
|
+
return presetsForCapability(presets, cap).length > 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* PROVEN = this capability has a ratified preset that has produced pixels on this fleet.
|
|
135
|
+
* `false` is not a defect and must not be hidden — it is the row telling the truth about
|
|
136
|
+
* itself (clip-i2v-480p is 0 of 47 landed Wan clips; clip-motion-480p is a real branch
|
|
137
|
+
* the route only unlocked on 2026-07-27). The UI badges it rather than suppressing it.
|
|
138
|
+
* `null` = no preset at all, i.e. unservable — a different answer from "unproven".
|
|
139
|
+
*/
|
|
140
|
+
export function provenForCapability(presets: RenderPreset[], cap: string): boolean | null {
|
|
141
|
+
const rows = presetsForCapability(presets, cap);
|
|
142
|
+
if (rows.length === 0) return null;
|
|
143
|
+
return rows.some((p) => p.proven === true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The model ids a pin may name for `cap` without leaving the ratified set. For the movie
|
|
148
|
+
* capability the segment bindings count too — `composes` names the preset ids whose
|
|
149
|
+
* models a multi-segment render actually binds, so pinning one of those is legitimate
|
|
150
|
+
* even though the movie row itself declares a single `model`.
|
|
151
|
+
*/
|
|
152
|
+
export function modelsForCapability(presets: RenderPreset[], cap: string): string[] {
|
|
153
|
+
const rows = presetsForCapability(presets, cap);
|
|
154
|
+
const byId = new Map(presets.map((p) => [p.id, p]));
|
|
155
|
+
const out = new Set<string>();
|
|
156
|
+
for (const p of rows) {
|
|
157
|
+
out.add(p.model);
|
|
158
|
+
for (const composedId of p.composes ?? []) {
|
|
159
|
+
const c = byId.get(composedId);
|
|
160
|
+
if (c) out.add(c.model);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return [...out].sort();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Geometry {
|
|
167
|
+
width: number;
|
|
168
|
+
height: number;
|
|
169
|
+
fps: number;
|
|
170
|
+
/** The preset the envelope came from — named in the UI so a lock is never anonymous. */
|
|
171
|
+
presetId: string;
|
|
172
|
+
presetTitle: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The geometry envelope a capability renders at, or null when its presets declare none
|
|
177
|
+
* (the two ffmpeg enhance rows inherit the source clip's geometry — `width: null` is the
|
|
178
|
+
* table's honest encoding of that, and a 0 would be a lie with a shape).
|
|
179
|
+
*
|
|
180
|
+
* Every video row on this fleet is 832x480@16 today, so "the first row that declares
|
|
181
|
+
* one" is unambiguous; if a second envelope is ever ratified for the same capability
|
|
182
|
+
* this returns the first and `withinGeometry` widens to accept either.
|
|
183
|
+
*/
|
|
184
|
+
export function geometryForCapability(presets: RenderPreset[], cap: string): Geometry | null {
|
|
185
|
+
for (const p of presetsForCapability(presets, cap)) {
|
|
186
|
+
if (p.width != null && p.height != null && p.fps != null) {
|
|
187
|
+
return {
|
|
188
|
+
width: p.width,
|
|
189
|
+
height: p.height,
|
|
190
|
+
fps: p.fps,
|
|
191
|
+
presetId: p.id,
|
|
192
|
+
presetTitle: p.title,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Is w×h inside SOME ratified envelope for this capability? A capability with no
|
|
200
|
+
* declared geometry accepts anything (the enhance rows take the source's). */
|
|
201
|
+
export function withinGeometry(
|
|
202
|
+
presets: RenderPreset[],
|
|
203
|
+
cap: string,
|
|
204
|
+
width: number,
|
|
205
|
+
height: number,
|
|
206
|
+
): boolean {
|
|
207
|
+
const rows = presetsForCapability(presets, cap).filter(
|
|
208
|
+
(p) => p.width != null && p.height != null,
|
|
209
|
+
);
|
|
210
|
+
if (rows.length === 0) return true;
|
|
211
|
+
if (width <= 0 || height <= 0) return false;
|
|
212
|
+
return rows.some((p) => width <= (p.width as number) && height <= (p.height as number));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The SMALLEST `vram_budget_gb` that can bind a real model for this capability.
|
|
217
|
+
*
|
|
218
|
+
* This is the number the studio console used to mirror by hand as
|
|
219
|
+
* `STUDIO_REAL_FLOOR_GB = 6` — a constant that had drifted BELOW every real row in the
|
|
220
|
+
* table (the cheapest is 8.2), so the honesty banner it fed under-warned. It is derived
|
|
221
|
+
* here from `vram_envelope_gb`, which the backend reads off the same
|
|
222
|
+
* `ModelConfig.vram` map the router compares a budget against (`cfg.vram.fits(budget)`
|
|
223
|
+
* is `gb <= budget`).
|
|
224
|
+
*
|
|
225
|
+
* Returns null when the payload has not landed or carries no envelope — callers then
|
|
226
|
+
* keep their fallback constant rather than inventing a floor.
|
|
227
|
+
*/
|
|
228
|
+
export function minBudgetGbForCapability(
|
|
229
|
+
presets: RenderPreset[],
|
|
230
|
+
cap: string,
|
|
231
|
+
): number | null {
|
|
232
|
+
const vals = presetsForCapability(presets, cap)
|
|
233
|
+
.map((p) => p.vram_envelope_gb)
|
|
234
|
+
.filter((v): v is number => typeof v === "number" && v > 0);
|
|
235
|
+
return vals.length ? Math.min(...vals) : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The budget to PREFILL for a capability: its cheapest binding, rounded up to a whole
|
|
239
|
+
* GB so the field reads like something a person typed and can never land a hair under
|
|
240
|
+
* the envelope through float display. */
|
|
241
|
+
export function suggestedBudgetGb(presets: RenderPreset[], cap: string): number | null {
|
|
242
|
+
const min = minBudgetGbForCapability(presets, cap);
|
|
243
|
+
return min == null ? null : Math.ceil(min);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── input-driven filtering (the "every input filters what comes next" half) ─────────
|
|
247
|
+
|
|
248
|
+
export interface CapabilityOffer {
|
|
249
|
+
capability: string;
|
|
250
|
+
label: string;
|
|
251
|
+
/** Servable on the fleet AND compatible with what is currently attached. */
|
|
252
|
+
offered: boolean;
|
|
253
|
+
/** Covered by a ratified preset at all. */
|
|
254
|
+
servable: boolean;
|
|
255
|
+
/** Has produced pixels on this fleet (null when unservable). */
|
|
256
|
+
proven: boolean | null;
|
|
257
|
+
/** Why it is not offered — the backend's refusal reason for an unservable
|
|
258
|
+
* capability, or this module's attachment explanation for a servable-but-
|
|
259
|
+
* incompatible one. Empty when offered. */
|
|
260
|
+
reason: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* THE MATRIX, applied: which capabilities remain reachable given what the user has
|
|
265
|
+
* already attached. A capability is offered iff it is servable AND every attachment the
|
|
266
|
+
* user is holding is an input that capability accepts.
|
|
267
|
+
*
|
|
268
|
+
* This is the "filters the options subsequently offered" direction. It is deliberately
|
|
269
|
+
* ADVISORY at the picker level: an incompatible capability is shown DISABLED with the
|
|
270
|
+
* reason rather than vanishing, because a user who attached a start image and then
|
|
271
|
+
* wonders where restyle went is worse off than one who can see it greyed out saying
|
|
272
|
+
* "restyle does not take a start image".
|
|
273
|
+
*
|
|
274
|
+
* Attachments are never auto-dropped to make a capability fit — clobbering the user's
|
|
275
|
+
* inputs is the defect class this whole slice deletes.
|
|
276
|
+
*/
|
|
277
|
+
export function capabilityOffers(
|
|
278
|
+
presets: RenderPreset[],
|
|
279
|
+
unavailable: UnavailableCapability[],
|
|
280
|
+
attached: Attachments,
|
|
281
|
+
candidates: readonly string[] = CLIP_CAPABILITIES,
|
|
282
|
+
): CapabilityOffer[] {
|
|
283
|
+
const refusals = new Map(unavailable.map((u) => [u.capability, u]));
|
|
284
|
+
return candidates.map((cap) => {
|
|
285
|
+
const servable = isServable(presets, cap);
|
|
286
|
+
if (!servable) {
|
|
287
|
+
const u = refusals.get(cap);
|
|
288
|
+
return {
|
|
289
|
+
capability: cap,
|
|
290
|
+
label: capabilityLabel(cap),
|
|
291
|
+
offered: false,
|
|
292
|
+
servable: false,
|
|
293
|
+
proven: null,
|
|
294
|
+
// The backend's own wording, verbatim — re-phrasing it here would give the
|
|
295
|
+
// console and the log two different explanations of one fact.
|
|
296
|
+
reason: u?.reason || u?.refusal || "no ratified preset covers it on this fleet",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const blockers: string[] = [];
|
|
300
|
+
(Object.keys(ACCEPTS) as Array<keyof Attachments>).forEach((slot) => {
|
|
301
|
+
if (attached[slot] && !ACCEPTS[slot].includes(cap)) {
|
|
302
|
+
blockers.push(ATTACHMENT_NOUNS[slot]);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// The route refuses this pair by name (the VACE control channel takes ONE input and
|
|
306
|
+
// source_video wins, so the control still would be silently dropped). Surface it as
|
|
307
|
+
// an offer-level blocker rather than letting the user build it and get a 400.
|
|
308
|
+
if (attached.controlImage && attached.sourceVideo && cap === "motion") {
|
|
309
|
+
blockers.push("both a control still and a source clip (motion takes one control)");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
capability: cap,
|
|
313
|
+
label: capabilityLabel(cap),
|
|
314
|
+
offered: blockers.length === 0,
|
|
315
|
+
servable: true,
|
|
316
|
+
proven: provenForCapability(presets, cap),
|
|
317
|
+
reason: blockers.length
|
|
318
|
+
? `${capabilityLabel(cap)} does not take ${blockers.join(" or ")} — remove it to switch here.`
|
|
319
|
+
: "",
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The inverse direction, for the AUTO-PICK on an external stage: the best capability
|
|
325
|
+
* for what is attached right now, or null when the current one is already fine.
|
|
326
|
+
* Ordered by specificity — the most constrained attachment wins, because it is the one
|
|
327
|
+
* the user went to the most trouble to provide. */
|
|
328
|
+
export function capabilityForAttachments(
|
|
329
|
+
presets: RenderPreset[],
|
|
330
|
+
attached: Attachments,
|
|
331
|
+
current: string,
|
|
332
|
+
): string | null {
|
|
333
|
+
const offers = capabilityOffers(presets, [], attached);
|
|
334
|
+
const ok = (cap: string) => offers.find((o) => o.capability === cap)?.offered === true;
|
|
335
|
+
if (ok(current)) return null; // the user's choice already fits — never override it
|
|
336
|
+
if (attached.controlImage && ok("motion")) return "motion";
|
|
337
|
+
if (attached.referenceImages && ok("id_lock")) return "id_lock";
|
|
338
|
+
if (attached.sourceVideo && ok("v2v")) return "v2v";
|
|
339
|
+
if (attached.startImage && ok("i2v")) return "i2v";
|
|
340
|
+
return offers.find((o) => o.offered)?.capability ?? null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Which attachment slots the given capability accepts — drives whether an input widget
|
|
344
|
+
* is even OFFERED (the surface's existing accepts* predicates, now data-derived). */
|
|
345
|
+
export function acceptedSlots(cap: string): Attachments {
|
|
346
|
+
return {
|
|
347
|
+
startImage: ACCEPTS.startImage.includes(cap),
|
|
348
|
+
sourceVideo: ACCEPTS.sourceVideo.includes(cap),
|
|
349
|
+
referenceImages: ACCEPTS.referenceImages.includes(cap),
|
|
350
|
+
controlImage: ACCEPTS.controlImage.includes(cap),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Which inputs the ratified preset for this capability REQUIRES (its `inputs[]`, minus
|
|
355
|
+
* `prompt` which every row lists and no picker gates on). Used for the required-input
|
|
356
|
+
* callouts so "what this generator needs" comes from the measured table. */
|
|
357
|
+
export function requiredInputs(presets: RenderPreset[], cap: string): string[] {
|
|
358
|
+
const rows = presetsForCapability(presets, cap);
|
|
359
|
+
if (rows.length === 0) return [];
|
|
360
|
+
return (rows[0].inputs ?? []).filter((i) => i !== "prompt" && i !== "goals");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── merge-not-overwrite: the preset-apply philosophy, in one place ──────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* A preset supplies the WORKFLOW (capability, geometry, cadence, budget, model). The
|
|
367
|
+
* user's SESSION INPUTS (prompt, negative, identity, attachments, a seed they chose) are
|
|
368
|
+
* theirs and survive every apply.
|
|
369
|
+
*
|
|
370
|
+
* `fillIfEmpty` is the whole rule for text: a preset's example prompt is a scaffold for
|
|
371
|
+
* an empty box, never a replacement for something the user wrote. Before 2026-07-29 the
|
|
372
|
+
* studio's `onPickPreset` assigned `p.prompt ?? ""` — so picking a preset with no prompt
|
|
373
|
+
* of its own BLANKED whatever the user had typed. That is the defect class this deletes.
|
|
374
|
+
*/
|
|
375
|
+
export function fillIfEmpty(current: string, offered: string | null | undefined): string {
|
|
376
|
+
if (current.trim() !== "") return current;
|
|
377
|
+
return offered ?? "";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Same rule for a numeric knob held as a string ("" = unset). A preset fills a blank
|
|
381
|
+
* field; it never overwrites a number the user typed. */
|
|
382
|
+
export function fillNumberIfEmpty(
|
|
383
|
+
current: string,
|
|
384
|
+
offered: number | null | undefined,
|
|
385
|
+
): string {
|
|
386
|
+
if (current.trim() !== "") return current;
|
|
387
|
+
return offered == null ? current : String(offered);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── generalized MOVIE workflow presets ──────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/** The joint modes the movie preset ratifies, with what each one MEANS and what it
|
|
393
|
+
* costs. Copy is derived from the movie row's own evidence, not invented here. */
|
|
394
|
+
const JOINT_WORKFLOWS: Record<
|
|
395
|
+
string,
|
|
396
|
+
{ name: string; hint: string; segmentCapability: string }
|
|
397
|
+
> = {
|
|
398
|
+
cut: {
|
|
399
|
+
name: "Multi-shot movie — hard cuts",
|
|
400
|
+
hint:
|
|
401
|
+
"Each shot is a fresh render joined by a scene cut; no frames carry across. Every "
|
|
402
|
+
+ "segment stays on the proven 1.3B binding, so this is the cheapest and most "
|
|
403
|
+
+ "reliable way to chain shots. Attach an identity below and every shot locks to it.",
|
|
404
|
+
segmentCapability: "t2v",
|
|
405
|
+
},
|
|
406
|
+
vace_extend: {
|
|
407
|
+
name: "Continuous motion — VACE extend",
|
|
408
|
+
hint:
|
|
409
|
+
"Each shot continues the previous one's motion from a branch frame. Same proven "
|
|
410
|
+
+ "1.3B weights as a cut, reached through the restyle wiring — use it when the "
|
|
411
|
+
+ "camera or the action should carry across the joint.",
|
|
412
|
+
segmentCapability: "v2v",
|
|
413
|
+
},
|
|
414
|
+
still: {
|
|
415
|
+
name: "Still-carry chain",
|
|
416
|
+
hint:
|
|
417
|
+
"Each shot starts from the previous one's last frame as an image. Honest cost: this "
|
|
418
|
+
+ "joint binds the 14B image-to-video row per segment, which has never completed a "
|
|
419
|
+
+ "clip on this fleet and re-reads ~90 GB of weights per segment. Prefer cuts or "
|
|
420
|
+
+ "VACE extend unless you specifically need a still hand-off.",
|
|
421
|
+
segmentCapability: "i2v",
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export interface MovieWorkflowPreset {
|
|
426
|
+
id: string;
|
|
427
|
+
name: string;
|
|
428
|
+
hint: string;
|
|
429
|
+
/** Applied to segments 1..N (segment 0 is the root and has no joint). */
|
|
430
|
+
joint: string;
|
|
431
|
+
/** The geometry the ratified movie preset renders at. */
|
|
432
|
+
geometry: Geometry | null;
|
|
433
|
+
/** The minimum budget for the binding THIS joint actually uses. */
|
|
434
|
+
minBudgetGb: number | null;
|
|
435
|
+
/** Has the segment binding this joint uses produced pixels on this fleet? */
|
|
436
|
+
proven: boolean;
|
|
437
|
+
/** The fewest segments this workflow means anything with. NEVER a replacement for
|
|
438
|
+
* the user's existing rows — the apply path grows to this and no further. */
|
|
439
|
+
minSegments: number;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The movie composer's preset list, DERIVED — one workflow per joint mode the ratified
|
|
444
|
+
* `movie-480p` row declares, sized against the binding that joint actually uses.
|
|
445
|
+
*
|
|
446
|
+
* This replaces a hardcoded array of scene prose ("Beach & volleyball — character day
|
|
447
|
+
* out", with a baked-in subject, a baked-in identity slug and two baked-in scene
|
|
448
|
+
* prompts). A preset here names a WORKFLOW SHAPE and nothing about the content: the
|
|
449
|
+
* user's own goal prompts, identity and start image are the content, and the apply path
|
|
450
|
+
* slots the workflow around them without touching any of it.
|
|
451
|
+
*
|
|
452
|
+
* Returns [] when the payload has not landed — the caller shows its loading state rather
|
|
453
|
+
* than a picker built on guesses.
|
|
454
|
+
*/
|
|
455
|
+
export function movieWorkflowPresets(presets: RenderPreset[]): MovieWorkflowPreset[] {
|
|
456
|
+
const movieRows = presetsForCapability(presets, MOVIE_CAPABILITY);
|
|
457
|
+
if (movieRows.length === 0) return [];
|
|
458
|
+
const byId = new Map(presets.map((p) => [p.id, p]));
|
|
459
|
+
const out: MovieWorkflowPreset[] = [];
|
|
460
|
+
for (const row of movieRows) {
|
|
461
|
+
const geometry = geometryForCapability(presets, MOVIE_CAPABILITY);
|
|
462
|
+
for (const joint of row.joints ?? []) {
|
|
463
|
+
const wf = JOINT_WORKFLOWS[joint];
|
|
464
|
+
if (!wf) continue; // a joint the console does not model yet — never invent copy
|
|
465
|
+
// The segment binding this joint really uses, found among the row's `composes`.
|
|
466
|
+
const segRow = (row.composes ?? [])
|
|
467
|
+
.map((id) => byId.get(id))
|
|
468
|
+
.find((p): p is RenderPreset => !!p && p.capability === wf.segmentCapability);
|
|
469
|
+
out.push({
|
|
470
|
+
id: `${row.id}:${joint}`,
|
|
471
|
+
name: wf.name,
|
|
472
|
+
hint: wf.hint,
|
|
473
|
+
joint,
|
|
474
|
+
geometry,
|
|
475
|
+
minBudgetGb: segRow?.vram_envelope_gb ?? row.vram_envelope_gb ?? null,
|
|
476
|
+
// A composite can never be more proven than the weakest thing it composes —
|
|
477
|
+
// the same rule the backend's test_presets.py enforces on the table itself.
|
|
478
|
+
proven: (segRow?.proven ?? row.proven) === true,
|
|
479
|
+
minSegments: 2,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The budget a movie actually needs, given the joint AND whether an identity is attached.
|
|
488
|
+
*
|
|
489
|
+
* The identity case is the one a static number gets wrong: the moment a movie carries
|
|
490
|
+
* references, EVERY segment renders id_lock (studio_movie.py) and binds the VACE row, so
|
|
491
|
+
* the floor is that row's envelope regardless of which joint was chosen.
|
|
492
|
+
*/
|
|
493
|
+
export function movieBudgetGb(
|
|
494
|
+
presets: RenderPreset[],
|
|
495
|
+
opts: { joint: string; identity: boolean },
|
|
496
|
+
): number | null {
|
|
497
|
+
if (opts.identity) return suggestedBudgetGb(presets, "id_lock");
|
|
498
|
+
const wf = JOINT_WORKFLOWS[opts.joint];
|
|
499
|
+
if (!wf) return suggestedBudgetGb(presets, MOVIE_CAPABILITY);
|
|
500
|
+
const seg = suggestedBudgetGb(presets, wf.segmentCapability);
|
|
501
|
+
const base = suggestedBudgetGb(presets, MOVIE_CAPABILITY);
|
|
502
|
+
if (seg == null) return base;
|
|
503
|
+
if (base == null) return seg;
|
|
504
|
+
return Math.max(seg, base);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The Wan frame cadence check — `num_frames` must be 4k+1 or the runner snaps down.
|
|
508
|
+
* Exposed so a surface can say so BEFORE six minutes of denoise. */
|
|
509
|
+
export function isWanCadence(frames: number, cadence: number | null): boolean {
|
|
510
|
+
if (!cadence || cadence < 1) return true;
|
|
511
|
+
return frames >= 1 && (frames - 1) % cadence === 0;
|
|
512
|
+
}
|