@hugpy/video-intelligence-ui 0.3.3 → 0.3.5
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/dist/assets/index-C_av7KDW.js +4190 -0
- package/dist/assets/index-DMoT8G1q.css +1 -0
- package/dist/index.html +2 -2
- package/entry.tsx +6 -27
- package/package.json +3 -2
- package/src/config.ts +11 -27
- package/src/demo/demoFetch.ts +0 -9
- package/src/nav/Navbar.tsx +3 -3
- package/src/session.ts +8 -43
- package/src/stations/GenerateStation.tsx +8 -5
- package/src/stations/SharePanel.tsx +4 -4
- package/src/stations/ToksPanel.tsx +1 -2
- package/src/stations/WorkbenchSidebar.tsx +8 -261
- package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
- package/src/stations/studio/IdentitiesStation.tsx +66 -75
- package/src/stations/studio/StudioGenerateMode.tsx +3 -2
- package/src/stations/studio/StudioGenerateTab.tsx +258 -188
- package/src/stations/studio/StudioMovieComposer.tsx +320 -57
- package/src/stations/studio/StudioPlane.tsx +3 -1
- package/src/stations/studio/StudioViewer.tsx +6 -11
- package/src/stations/studio/studioShared.tsx +14 -2
- package/src/stations/studio/useIdentityCreateJob.ts +26 -21
- package/src/stations/studio/useReconstruction.ts +11 -5
- package/src/video/PromptAssistButtons.tsx +5 -6
- package/src/video/jobStream.ts +119 -0
- package/src/video/jobTracker.ts +67 -76
- package/src/video/modelsFeed.ts +105 -0
- package/src/video/studioOracle.ts +48 -0
- package/src/video/useMediaJobs.ts +17 -3
- package/src/video/useModels.ts +12 -53
- package/src/video/useMovieSessions.ts +15 -6
- package/src/video/useOperatorHistory.ts +11 -101
- package/src/video/usePromptAssist.ts +6 -3
- package/src/video/useToks.ts +14 -6
- package/src/video/watchVideoJob.ts +43 -0
- package/dist/assets/index-DuT1AMLn.css +0 -1
- package/dist/assets/index-_13ZiBDP.js +0 -4191
- package/src/video/useWorkerCalls.ts +0 -165
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
// uniform panel:
|
|
14
14
|
//
|
|
15
15
|
// • t2v shape — image inputs are hidden entirely (text-only note);
|
|
16
|
-
// • i2v
|
|
17
|
-
//
|
|
18
|
-
// clip" alternative;
|
|
16
|
+
// • i2v shape — the prompt card accepts an optional start image, and the
|
|
17
|
+
// source-clip picker offers extension from a clip;
|
|
19
18
|
// • v2v (restyle) shape — the SOURCE CLIP picker gets that hero prominence;
|
|
20
19
|
// • id_lock (identity lock) shape — the REFERENCE-IMAGE slot list (1–4, ordered) is
|
|
21
20
|
// the dominant accent-bordered element, with an OPTIONAL composition-control input
|
|
@@ -28,7 +27,7 @@
|
|
|
28
27
|
// Every affordance reuses an EXISTING pattern: presets from useStudioPresets; the
|
|
29
28
|
// image UPLOAD path is the Generate station's upload→ingest→guard-kind flow
|
|
30
29
|
// (POST /uploads → POST /video/ingest → MediaRef) and the hero drop surface is the
|
|
31
|
-
//
|
|
30
|
+
// prompt-card file input; the LIBRARY pick reads useMediaLibrary; the source-clip pick
|
|
32
31
|
// reads the plane's clip list; "Send to Studio" / "use as source" staging arrives via
|
|
33
32
|
// studioBridge as the `source` prop; the enqueue body uses the exact route field names,
|
|
34
33
|
// dropped by JSON.stringify when unset.
|
|
@@ -52,8 +51,8 @@ import { addToLibrary, useMediaLibrary } from "../../video/mediaLibrary";
|
|
|
52
51
|
import { UnnamedWarnPanel, WarnUnnamedToggle, useWarnUnnamedPref, deriveNameFrom } from "../../video/UnnamedWarn";
|
|
53
52
|
import { LengthRow } from "../../video/LengthRow";
|
|
54
53
|
import { enqueueResultSchema, mediaRefSchema, uploadResultSchema } from "../../video/contract";
|
|
54
|
+
import { trackJob, useJobTracker } from "../../video/jobTracker";
|
|
55
55
|
import type { MediaRef } from "../../video/contract";
|
|
56
|
-
import { DropReceptacle } from "../../video/DropReceptacle";
|
|
57
56
|
import type { GoalComposerAttach, GoalComposerAttached } from "../GoalComposer";
|
|
58
57
|
import { usePromptAssist } from "../../video/usePromptAssist";
|
|
59
58
|
import { PromptAssistButtons } from "../../video/PromptAssistButtons";
|
|
@@ -80,7 +79,7 @@ import {
|
|
|
80
79
|
} from "./studioShared";
|
|
81
80
|
// B2: the Movie authoring surface the mode switch renders. Self-contained (owns its
|
|
82
81
|
// own goal state, enqueue + poll, and playback) — see StudioMovieComposer.tsx.
|
|
83
|
-
import { StudioMovieComposer } from "./StudioMovieComposer";
|
|
82
|
+
import { StudioMovieComposer, type ClipSequenceDraft } from "./StudioMovieComposer";
|
|
84
83
|
// IDENTITY PROFILES (stage a): the shared "Identity" control block (From profile /
|
|
85
84
|
// Save as identity profile) — one additive row in the reference area below.
|
|
86
85
|
import { IdentityProfileControls, type SelectedProfile } from "./IdentityProfileControls";
|
|
@@ -97,6 +96,7 @@ import { AboutExpander, PromptCard, PromptCardSettings, PromptFieldTabs, compose
|
|
|
97
96
|
// authoritative, and every consumer below falls back to its prior list while
|
|
98
97
|
// `renderLoaded` is false, so a slow discovery GET can never empty a picker.
|
|
99
98
|
import { useRenderPresets } from "../../video/useRenderPresets";
|
|
99
|
+
import { planStudioSettings, type OracleSettings } from "../../video/studioOracle";
|
|
100
100
|
import {
|
|
101
101
|
CLIP_CAPABILITIES,
|
|
102
102
|
MOVIE_CAPABILITY,
|
|
@@ -165,6 +165,7 @@ export interface StudioGenerateSurfaceProps {
|
|
|
165
165
|
* its own internal switcher + useState. "movie" is the Cinema surface (label only — the
|
|
166
166
|
* wire/tester value stays "movie"). */
|
|
167
167
|
lockedSurfaceMode?: "clip" | "movie";
|
|
168
|
+
onSelectSurfaceMode?: (mode: "clip" | "movie") => void;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
// Labels now come from renderCompat (ONE table both studio surfaces read) so a
|
|
@@ -243,6 +244,7 @@ export function StudioGenerateSurface({
|
|
|
243
244
|
onEnqueued,
|
|
244
245
|
settingsHost,
|
|
245
246
|
lockedSurfaceMode,
|
|
247
|
+
onSelectSurfaceMode,
|
|
246
248
|
}: StudioGenerateSurfaceProps) {
|
|
247
249
|
// ── SUB-TAB INPUT MEMORY (operator ask 2026-08-06) ──────────────────────────
|
|
248
250
|
// Every OPERATOR-AUTHORED field below is `useSessionState` rather than
|
|
@@ -264,10 +266,13 @@ export function StudioGenerateSurface({
|
|
|
264
266
|
"clip",
|
|
265
267
|
);
|
|
266
268
|
const surfaceMode = lockedSurfaceMode ?? internalSurfaceMode;
|
|
269
|
+
const [, setClipSequenceDraft] = useSessionState<ClipSequenceDraft | null>(
|
|
270
|
+
"studio.movie.clipSequenceDraft", null,
|
|
271
|
+
);
|
|
267
272
|
|
|
268
273
|
const [capability, setCapability] = useSessionState<Capability>(
|
|
269
274
|
"studio.clip.capability",
|
|
270
|
-
"
|
|
275
|
+
"t2v",
|
|
271
276
|
);
|
|
272
277
|
const [presetId, setPresetId] = useSessionState("studio.clip.presetId", "");
|
|
273
278
|
|
|
@@ -295,6 +300,15 @@ export function StudioGenerateSurface({
|
|
|
295
300
|
const [steps, setSteps] = useSessionState("studio.clip.steps", "");
|
|
296
301
|
const [cfg, setCfg] = useSessionState("studio.clip.cfg", "");
|
|
297
302
|
const [modelId, setModelId] = useSessionState("studio.clip.modelId", "");
|
|
303
|
+
const [settingsMode, setSettingsMode] = useSessionState<"oracle" | "manual">(
|
|
304
|
+
"studio.clip.settingsMode", "oracle",
|
|
305
|
+
);
|
|
306
|
+
const manualSettings = useSessionRef<string[]>("studio.clip.manualSettings", []);
|
|
307
|
+
const markManual = (field: string) => {
|
|
308
|
+
if (!manualSettings.current.includes(field)) {
|
|
309
|
+
manualSettings.current = [...manualSettings.current, field];
|
|
310
|
+
}
|
|
311
|
+
};
|
|
298
312
|
// Showroom flavor (?demo=1 only): open with a cinematic example prompt (matches
|
|
299
313
|
// the sunset sample gens) so the brochure's Studio surface tells a coherent story
|
|
300
314
|
// out of the box. Seed lives in src/demo/ and is injected ONLY behind isCanned() —
|
|
@@ -310,11 +324,13 @@ export function StudioGenerateSurface({
|
|
|
310
324
|
// k89: the bottom "[▸ Tester]" collapsible that now hosts the cross-model sweep
|
|
311
325
|
// (the run row's ghost 🧪 button is retired).
|
|
312
326
|
const [clipTesterOpen, setClipTesterOpen] = useState(false);
|
|
327
|
+
const [sequenceMsg, setSequenceMsg] = useState<string | null>(null);
|
|
328
|
+
const [produceBusy, setProduceBusy] = useState(false);
|
|
313
329
|
// LLM PROMPT-ASSIST (Enhance / Generate) — the same shared hook + button cluster the
|
|
314
330
|
// Generate station uses (POSTs promptAssistUrl). Here the draft is the single clip
|
|
315
331
|
// prompt and apply replaces it wholesale. context.kind = "scene" (a clip is a short
|
|
316
332
|
// motion sequence, so the assistant leans into camera/motion phrasing).
|
|
317
|
-
const assist = usePromptAssist({ kind: "
|
|
333
|
+
const assist = usePromptAssist({ kind: "clip", specKey: "studio" });
|
|
318
334
|
// Optional human PROJECT name (choose-or-type). Threaded into the enqueue as
|
|
319
335
|
// `project` (auto-archive NAME); blank = auto-named (the default). The list of
|
|
320
336
|
// existing names backs the Settings datalist; free-typing a new one creates it.
|
|
@@ -393,6 +409,11 @@ export function StudioGenerateSurface({
|
|
|
393
409
|
|
|
394
410
|
const [genBusy, setGenBusy] = useState(false);
|
|
395
411
|
const [genMsg, setGenMsg] = useState<string | null>(null);
|
|
412
|
+
const [activeClipJobId, setActiveClipJobId] = useSessionState<string | null>(
|
|
413
|
+
"studio.clip.activeJobId", null,
|
|
414
|
+
);
|
|
415
|
+
const clipJobs = useJobTracker();
|
|
416
|
+
const activeClipJob = clipJobs.find((row) => row.jobId === activeClipJobId);
|
|
396
417
|
|
|
397
418
|
// STUDIO TESTER (operator 2026-08-05) — fan the CURRENT prompt across every
|
|
398
419
|
// servable model of this surface's type (clip/movie) via POST studioTesterUrl.
|
|
@@ -486,6 +507,27 @@ export function StudioGenerateSurface({
|
|
|
486
507
|
const isV2V = capability === "v2v";
|
|
487
508
|
const isIdLock = capability === "id_lock";
|
|
488
509
|
const isMotion = capability === "motion";
|
|
510
|
+
const clipRoute = isI2V && (startImages[0] || source?.uri || source?.asset_id) ? "i2v" : isI2V ? "t2v" : capability;
|
|
511
|
+
const oraclePlan = async (text: string): Promise<OracleSettings> => {
|
|
512
|
+
if (clipRoute !== "t2v" && clipRoute !== "i2v") {
|
|
513
|
+
throw new Error("Oracle has no executable model for this Clip capability.");
|
|
514
|
+
}
|
|
515
|
+
const planned = await planStudioSettings(clipRoute, text, {
|
|
516
|
+
model_id: modelId || undefined, width, height, fps,
|
|
517
|
+
frames: requestedFrames === "" ? undefined : Number(requestedFrames),
|
|
518
|
+
steps: steps === "" ? undefined : Number(steps),
|
|
519
|
+
cfg: cfg === "" ? undefined : Number(cfg), seed,
|
|
520
|
+
}, manualSettings.current);
|
|
521
|
+
if (planned.model_id != null) setModelId(planned.model_id);
|
|
522
|
+
if (planned.width != null) setWidth(planned.width);
|
|
523
|
+
if (planned.height != null) setHeight(planned.height);
|
|
524
|
+
if (planned.fps != null) setFps(planned.fps);
|
|
525
|
+
if (planned.frames != null) setRequestedFrames(String(planned.frames));
|
|
526
|
+
if (planned.steps != null) setSteps(String(planned.steps));
|
|
527
|
+
if (planned.cfg != null) setCfg(String(planned.cfg));
|
|
528
|
+
if (planned.seed != null) setSeed(planned.seed);
|
|
529
|
+
return planned;
|
|
530
|
+
};
|
|
489
531
|
|
|
490
532
|
// ── the fleet's measured render table + everything derived from it ───────────────
|
|
491
533
|
// One session-cached GET shared with the Movie composer (module singleton in the
|
|
@@ -938,19 +980,16 @@ export function StudioGenerateSurface({
|
|
|
938
980
|
|
|
939
981
|
// ── required-input gate + preformed SHAPE ──
|
|
940
982
|
// v2v: a source clip is REQUIRED (nothing to repaint otherwise) — always the hero.
|
|
941
|
-
// i2v
|
|
942
|
-
// required — the start image is the hero, the source clip the alternative.
|
|
983
|
+
// i2v offers a start image; without one the orbit route uses text-to-video.
|
|
943
984
|
// id_lock: >=1 REFERENCE image is required (the route 400s otherwise) — reference
|
|
944
985
|
// slots are the hero; the composition control is optional + both-or-neither.
|
|
945
986
|
// t2v / synthetic i2v / custom: no required input; inputs stay inline + optional.
|
|
946
|
-
const presetNeedsInput = preset?.requires_source === true;
|
|
947
987
|
const hasImage = startImages.length > 0;
|
|
948
988
|
const hasSource = !!source;
|
|
949
989
|
const hasReference = referenceImages.length > 0;
|
|
950
990
|
const canAddImage = startImages.length < MAX_START_IMAGES;
|
|
951
991
|
const canAddReference = referenceImages.length < MAX_REFERENCE_IMAGES;
|
|
952
992
|
const needSource = isV2V;
|
|
953
|
-
const needImageOrSource = isI2V && presetNeedsInput;
|
|
954
993
|
const needReference = isIdLock;
|
|
955
994
|
// MOTION requires a control still and it IS the capability — the exact mirror of
|
|
956
995
|
// id_lock's reference rule (video_routes: "a motion request with no control carries NO
|
|
@@ -966,40 +1005,26 @@ export function StudioGenerateSurface({
|
|
|
966
1005
|
const controlAndSource = isMotion && hasControl && hasSource;
|
|
967
1006
|
const missingRequired =
|
|
968
1007
|
(needSource && !hasSource) ||
|
|
969
|
-
(needImageOrSource && !hasImage && !hasSource) ||
|
|
970
1008
|
(needReference && !hasReference) ||
|
|
971
1009
|
(needControl && !hasControl) ||
|
|
972
1010
|
controlAndSource ||
|
|
973
1011
|
controlHalfSet;
|
|
974
1012
|
|
|
975
|
-
const hero: "
|
|
1013
|
+
const hero: "source" | "reference" | "control" | null = isMotion
|
|
976
1014
|
? "control"
|
|
977
1015
|
: isIdLock
|
|
978
1016
|
? "reference"
|
|
979
1017
|
: isV2V
|
|
980
1018
|
? "source"
|
|
981
|
-
:
|
|
982
|
-
? "start-image"
|
|
983
|
-
: null;
|
|
984
|
-
const startImageAttention = needImageOrSource && !hasImage && !hasSource;
|
|
1019
|
+
: null;
|
|
985
1020
|
const sourceAttention =
|
|
986
|
-
|
|
1021
|
+
needSource && !hasSource;
|
|
987
1022
|
const referenceAttention = needReference && !hasReference;
|
|
988
1023
|
|
|
989
1024
|
// ── Composer attach idiom (operator ask 2026-07-11, option A) ──
|
|
990
|
-
// The prompt
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
// removable chips) and wire its callbacks to THIS file's OWN pickers / MediaRef flows,
|
|
994
|
-
// offering only the inputs the ACTIVE shape accepts (mirroring the render* predicates
|
|
995
|
-
// above). The buttons open the SAME `showLibPicker` / `showSourcePicker` the hero uses,
|
|
996
|
-
// so the library pick routes to start (i2v) or reference (v2v / id_lock) exactly as the
|
|
997
|
-
// hero's own lib-picker routing does; the chips reflect the SAME startImages / source /
|
|
998
|
-
// referenceImages / controlImage state the hero renders, and reuse the hero's existing
|
|
999
|
-
// removes. Applicability decides whether a button EXISTS (t2v gets no toolbar — omitted
|
|
1000
|
-
// cleanly by GoalComposer when the array is empty); capacity decides whether it is
|
|
1001
|
-
// DISABLED (a filled slot mirrors the hero's "you're at the cap" note, not a vanished
|
|
1002
|
-
// affordance). The negative composer stays plain.
|
|
1025
|
+
// The prompt card owns the start-image upload, library pick, and removable chip.
|
|
1026
|
+
// Other route-specific inputs use their existing pickers below. All callbacks
|
|
1027
|
+
// update the same MediaRef state consumed by the enqueue route.
|
|
1003
1028
|
//
|
|
1004
1029
|
// 2026-07-29: these three are no longer hand-written capability unions — they are the
|
|
1005
1030
|
// route's own input gates, transcribed once in renderCompat's ACCEPTS matrix and read
|
|
@@ -1103,7 +1128,7 @@ export function StudioGenerateSurface({
|
|
|
1103
1128
|
? "Composition control is both-or-neither — add a control image AND a kind, or clear both."
|
|
1104
1129
|
: needSource && !hasSource
|
|
1105
1130
|
? "Restyle (v2v) needs a source clip — add one above."
|
|
1106
|
-
: "
|
|
1131
|
+
: "Add the required input above.";
|
|
1107
1132
|
|
|
1108
1133
|
// v2v stays a HARD block (the router refuses an out-of-envelope restyle outright). The
|
|
1109
1134
|
// envelope itself is now the ratified preset's rather than a mirrored constant.
|
|
@@ -1190,14 +1215,18 @@ export function StudioGenerateSurface({
|
|
|
1190
1215
|
// on motion (required, definitional) — `slots.controlImage` is that gate.
|
|
1191
1216
|
const controlReady = (isIdLock || isMotion) && !!controlImage && controlKind !== "";
|
|
1192
1217
|
const body: Record<string, unknown> = {
|
|
1193
|
-
capability,
|
|
1218
|
+
capability: clipRoute,
|
|
1219
|
+
settings_mode: settingsMode,
|
|
1220
|
+
explicit_settings: manualSettings.current,
|
|
1194
1221
|
resolution: { width, height, fps },
|
|
1195
1222
|
seed,
|
|
1196
1223
|
vram_budget_gb: budgetNum,
|
|
1197
1224
|
steps: stepsNum,
|
|
1198
1225
|
cfg: cfgNum,
|
|
1199
1226
|
requested_frames: requestedFrames.trim() !== "" ? Number(requestedFrames) : undefined,
|
|
1200
|
-
|
|
1227
|
+
// A chosen model is an explicit pin. The engine validates it against the
|
|
1228
|
+
// task picked from inputs; silently dropping it would change the request.
|
|
1229
|
+
model_id: modelId.trim() || undefined,
|
|
1201
1230
|
// Optional auto-archive NAME (choose-or-type). Blank = auto-named (default).
|
|
1202
1231
|
project: project.trim() ? project.trim() : undefined,
|
|
1203
1232
|
prompt: prompt.trim() ? prompt : undefined,
|
|
@@ -1230,6 +1259,15 @@ export function StudioGenerateSurface({
|
|
|
1230
1259
|
control_image: controlReady ? (controlImage as MediaRef).uri : undefined,
|
|
1231
1260
|
control_kind: controlReady ? controlKind : undefined,
|
|
1232
1261
|
};
|
|
1262
|
+
if (settingsMode === "oracle") {
|
|
1263
|
+
const planned = await oraclePlan(prompt);
|
|
1264
|
+
body.resolution = { width: planned.width, height: planned.height, fps: planned.fps };
|
|
1265
|
+
body.model_id = planned.model_id;
|
|
1266
|
+
body.steps = planned.steps;
|
|
1267
|
+
body.cfg = planned.cfg;
|
|
1268
|
+
body.seed = planned.seed;
|
|
1269
|
+
body.requested_frames = planned.frames;
|
|
1270
|
+
}
|
|
1233
1271
|
const res = await request<unknown>(hugpyConfig.studioI2VEnqueueUrl, {
|
|
1234
1272
|
method: "POST",
|
|
1235
1273
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1241,28 +1279,34 @@ export function StudioGenerateSurface({
|
|
|
1241
1279
|
setGenMsg(`Enqueue failed: ${describeAppError(errorOf(res))}`);
|
|
1242
1280
|
return;
|
|
1243
1281
|
}
|
|
1282
|
+
const enq = enqueueResultSchema.safeParse(okValue(res));
|
|
1283
|
+
if (!enq.success) {
|
|
1284
|
+
setGenMsg("The server accepted the request but returned no job id to track.");
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const routedCapability = enq.data.capability === "i2v" || enq.data.capability === "t2v"
|
|
1288
|
+
? enq.data.capability : clipRoute;
|
|
1289
|
+
trackJob({
|
|
1290
|
+
jobId: enq.data.job_id, kind: "studio_i2v", station: "studio",
|
|
1291
|
+
label: routedCapability === "i2v" ? "Studio image-to-video" : "Studio text-to-video",
|
|
1292
|
+
libLabel: "studio clip", enqueuedAt: Date.now(),
|
|
1293
|
+
});
|
|
1294
|
+
setActiveClipJobId(enq.data.job_id);
|
|
1244
1295
|
// Subject-clip auto-stage (armed by onGenerateNewSubject): the FIRST successful
|
|
1245
1296
|
// enqueue after "Generate new clip" arms the wait on ITS job id — later manual
|
|
1246
1297
|
// generates (pendingSubjectStage already false by then) are plain renders.
|
|
1247
1298
|
if (pendingSubjectStage) {
|
|
1248
1299
|
setPendingSubjectStage(false);
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
setAwaitingSubjectJobId(enq.data.job_id);
|
|
1252
|
-
setGenMsg(
|
|
1253
|
-
"Enqueued — once this clip is produced it becomes the staged subject automatically.",
|
|
1254
|
-
);
|
|
1255
|
-
} else {
|
|
1256
|
-
setGenMsg(
|
|
1257
|
-
"Enqueued, but no job id came back to track — stage the produced clip as the subject manually (From library) once it's done.",
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1300
|
+
setAwaitingSubjectJobId(enq.data.job_id);
|
|
1301
|
+
setGenMsg("Enqueued — once this clip is produced it becomes the staged subject automatically.");
|
|
1260
1302
|
} else {
|
|
1261
|
-
setGenMsg(
|
|
1303
|
+
setGenMsg(`${routedCapability === "i2v" ? "Image-to-video" : "Text-to-video"} queued — follow the job below or in Active Processes.`);
|
|
1262
1304
|
}
|
|
1263
1305
|
onEnqueued();
|
|
1264
1306
|
// A project name coined here should appear in the combobox next time.
|
|
1265
1307
|
if (project.trim()) refreshProjects();
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
setGenMsg(err instanceof Error ? err.message : "Oracle planning failed.");
|
|
1266
1310
|
} finally {
|
|
1267
1311
|
if (mounted.current) setGenBusy(false);
|
|
1268
1312
|
}
|
|
@@ -1271,6 +1315,7 @@ export function StudioGenerateSurface({
|
|
|
1271
1315
|
v2vBadGeom,
|
|
1272
1316
|
missingReason,
|
|
1273
1317
|
capability,
|
|
1318
|
+
clipRoute,
|
|
1274
1319
|
width,
|
|
1275
1320
|
height,
|
|
1276
1321
|
fps,
|
|
@@ -1295,7 +1340,72 @@ export function StudioGenerateSurface({
|
|
|
1295
1340
|
pendingSubjectStage,
|
|
1296
1341
|
onEnqueued,
|
|
1297
1342
|
project,
|
|
1298
|
-
refreshProjects
|
|
1343
|
+
refreshProjects,
|
|
1344
|
+
warnUnnamed,
|
|
1345
|
+
settingsMode,
|
|
1346
|
+
oraclePlan,
|
|
1347
|
+
setActiveClipJobId]);
|
|
1348
|
+
|
|
1349
|
+
function addNextPrompt() {
|
|
1350
|
+
// Cinema's existing sequential route owns last-frame carry. Preserve every
|
|
1351
|
+
// Clip input we can transfer; refuse shapes it cannot represent as its root.
|
|
1352
|
+
if (source || isV2V || isIdLock || isMotion || referenceImages.length || controlImage) {
|
|
1353
|
+
setSequenceMsg("This Clip has a source or control input that Cinema cannot use as its first frame. Finish this clip, then start a sequence from its result; no attachment was discarded.");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
setSequenceMsg(null);
|
|
1357
|
+
setClipSequenceDraft({
|
|
1358
|
+
id: nextSessionSeq("studio.movie.clipSequenceDraftSeq"),
|
|
1359
|
+
prompt, negative, stdNegative,
|
|
1360
|
+
startImage: startImages[0] ?? null,
|
|
1361
|
+
width, height, fps, seed, frames: requestedFrames,
|
|
1362
|
+
modelId, steps, cfg, vramBudget, project,
|
|
1363
|
+
});
|
|
1364
|
+
setInternalSurfaceMode("movie");
|
|
1365
|
+
onSelectSurfaceMode?.("movie");
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function generateProductionFromClip() {
|
|
1369
|
+
if (source || isV2V || isIdLock || isMotion || referenceImages.length || controlImage) {
|
|
1370
|
+
setSequenceMsg("Cinema cannot carry this Clip's source or control inputs into a new production.");
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
setProduceBusy(true);
|
|
1374
|
+
setSequenceMsg(null);
|
|
1375
|
+
try {
|
|
1376
|
+
const res = await request<unknown>(hugpyConfig.producerPlanUrl, {
|
|
1377
|
+
method: "POST",
|
|
1378
|
+
headers: { "Content-Type": "application/json" },
|
|
1379
|
+
body: JSON.stringify({
|
|
1380
|
+
premise: prompt.trim() || "Create an original short cinematic sequence.",
|
|
1381
|
+
model: assist.assistModel || undefined,
|
|
1382
|
+
start_image: startImages[0]?.uri,
|
|
1383
|
+
}),
|
|
1384
|
+
meta: { specKey: "studio", operation: "studio.oracle.production" },
|
|
1385
|
+
});
|
|
1386
|
+
if (!res.ok) throw new Error(describeAppError(errorOf(res)));
|
|
1387
|
+
const result = okValue(res) as {
|
|
1388
|
+
segments?: ClipSequenceDraft["plannedSegments"];
|
|
1389
|
+
};
|
|
1390
|
+
if (!result?.segments?.length) throw new Error("Oracle returned no production segments.");
|
|
1391
|
+
const first = result.segments[0]?.settings;
|
|
1392
|
+
setClipSequenceDraft({
|
|
1393
|
+
id: nextSessionSeq("studio.movie.clipSequenceDraftSeq"),
|
|
1394
|
+
prompt, negative, stdNegative,
|
|
1395
|
+
startImage: startImages[0] ?? null,
|
|
1396
|
+
width: first?.width ?? width, height: first?.height ?? height,
|
|
1397
|
+
fps: first?.fps ?? fps, seed, frames: requestedFrames,
|
|
1398
|
+
modelId, steps, cfg, vramBudget, project,
|
|
1399
|
+
plannedSegments: result.segments,
|
|
1400
|
+
});
|
|
1401
|
+
setInternalSurfaceMode("movie");
|
|
1402
|
+
onSelectSurfaceMode?.("movie");
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
setSequenceMsg(err instanceof Error ? err.message : "Oracle production generation failed.");
|
|
1405
|
+
} finally {
|
|
1406
|
+
setProduceBusy(false);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1299
1409
|
|
|
1300
1410
|
const synthPresets = presets.filter((p) => isSyntheticPreset(p));
|
|
1301
1411
|
const realPresets = presets.filter((p) => !isSyntheticPreset(p));
|
|
@@ -1358,106 +1468,6 @@ export function StudioGenerateSurface({
|
|
|
1358
1468
|
);
|
|
1359
1469
|
}
|
|
1360
1470
|
|
|
1361
|
-
// ── start-image input (variant-driven: hero = dominant/accented; inline = subdued) ──
|
|
1362
|
-
function renderStartImage(variant: "hero" | "inline") {
|
|
1363
|
-
const isHero = variant === "hero";
|
|
1364
|
-
const drawEye = isHero && startImageAttention;
|
|
1365
|
-
const thumb = isHero ? "9rem" : "5rem";
|
|
1366
|
-
return (
|
|
1367
|
-
<section aria-label="Start image" style={{ marginTop: isHero ? 0 : "0.9rem" }}>
|
|
1368
|
-
<p
|
|
1369
|
-
className="vi-comfy-label"
|
|
1370
|
-
style={{ marginBottom: "0.35rem", opacity: isHero ? 1 : 0.75 }}
|
|
1371
|
-
>
|
|
1372
|
-
Start image{isHero ? " — required for this generator" : " (optional)"}
|
|
1373
|
-
</p>
|
|
1374
|
-
{drawEye && (
|
|
1375
|
-
<p className="vi-input-callout" role="note">
|
|
1376
|
-
This generator needs a start image — the subject is carried from it. Drop one below
|
|
1377
|
-
(or extend a source clip instead).
|
|
1378
|
-
</p>
|
|
1379
|
-
)}
|
|
1380
|
-
{hasImage && (
|
|
1381
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
|
1382
|
-
{startImages.map((img, idx) => (
|
|
1383
|
-
<div key={img.uri} style={{ display: "flex", flexDirection: "column", gap: "0.2rem" }}>
|
|
1384
|
-
<img
|
|
1385
|
-
src={mediaBytesUrl(img.uri)}
|
|
1386
|
-
alt="start"
|
|
1387
|
-
style={{
|
|
1388
|
-
width: thumb,
|
|
1389
|
-
height: thumb,
|
|
1390
|
-
objectFit: "cover",
|
|
1391
|
-
borderRadius: "0.4rem",
|
|
1392
|
-
background: "#000",
|
|
1393
|
-
border: "1px solid var(--vi-border)",
|
|
1394
|
-
}}
|
|
1395
|
-
/>
|
|
1396
|
-
<button
|
|
1397
|
-
type="button"
|
|
1398
|
-
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
1399
|
-
onClick={() => removeStartImage(idx)}
|
|
1400
|
-
>
|
|
1401
|
-
Remove
|
|
1402
|
-
</button>
|
|
1403
|
-
</div>
|
|
1404
|
-
))}
|
|
1405
|
-
</div>
|
|
1406
|
-
)}
|
|
1407
|
-
{canAddImage ? (
|
|
1408
|
-
isHero && !hasImage ? (
|
|
1409
|
-
<DropReceptacle
|
|
1410
|
-
onFile={(f) => void onPickImageFile(f, "start")}
|
|
1411
|
-
accept="image/*"
|
|
1412
|
-
busy={imageBusy}
|
|
1413
|
-
title="Drop a start image or click to browse"
|
|
1414
|
-
hint="This generator carries the subject from this still."
|
|
1415
|
-
className={drawEye ? "vi-dropzone--required" : undefined}
|
|
1416
|
-
/>
|
|
1417
|
-
) : (
|
|
1418
|
-
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
|
|
1419
|
-
<label className="vi-btn vi-btn-accent vi-file-label">
|
|
1420
|
-
{imageBusy ? "Working…" : "Upload a still"}
|
|
1421
|
-
<input
|
|
1422
|
-
type="file"
|
|
1423
|
-
accept="image/*"
|
|
1424
|
-
className="vi-file-input"
|
|
1425
|
-
disabled={imageBusy}
|
|
1426
|
-
onChange={(e) => {
|
|
1427
|
-
const f = e.target.files?.[0];
|
|
1428
|
-
if (f) void onPickImageFile(f, "start");
|
|
1429
|
-
e.target.value = "";
|
|
1430
|
-
}}
|
|
1431
|
-
/>
|
|
1432
|
-
</label>
|
|
1433
|
-
<button
|
|
1434
|
-
type="button"
|
|
1435
|
-
className="vi-btn vi-btn-ghost"
|
|
1436
|
-
onClick={() => setShowLibPicker((v) => !v)}
|
|
1437
|
-
aria-expanded={showLibPicker}
|
|
1438
|
-
>
|
|
1439
|
-
{showLibPicker ? "Hide library" : `Pick from library (${libraryImages.length})`}
|
|
1440
|
-
</button>
|
|
1441
|
-
</div>
|
|
1442
|
-
)
|
|
1443
|
-
) : (
|
|
1444
|
-
<p className="vi-comfy-hint" role="note">
|
|
1445
|
-
Multi-image conditioning here is identity lock — switch capability to Identity lock to
|
|
1446
|
-
add 1–4 reference images.
|
|
1447
|
-
</p>
|
|
1448
|
-
)}
|
|
1449
|
-
{imageError && (
|
|
1450
|
-
<p className="vi-error" role="alert" style={{ marginTop: "0.4rem" }}>
|
|
1451
|
-
{imageError}
|
|
1452
|
-
</p>
|
|
1453
|
-
)}
|
|
1454
|
-
{showLibPicker && canAddImage && (
|
|
1455
|
-
<LibraryImageGrid images={libraryImages} onPick={(ref) => addStartImage(ref)} />
|
|
1456
|
-
)}
|
|
1457
|
-
</section>
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
1471
|
// ── reference-image SLOT LIST: 1–4 ordered stills, reorderable. The SAME list feeds
|
|
1462
1472
|
// two shapes — id_lock (variant "required": the identity to lock, accent-gated) and
|
|
1463
1473
|
// v2v scene-continuity (variant "optional": an identity held across the restyle). ──
|
|
@@ -1723,15 +1733,11 @@ export function StudioGenerateSurface({
|
|
|
1723
1733
|
);
|
|
1724
1734
|
}
|
|
1725
1735
|
|
|
1726
|
-
// ── source-clip input (hero = dominant/accented; inline = subdued
|
|
1727
|
-
|
|
1728
|
-
function renderSource(variant: "hero" | "inline" | "inline-alt") {
|
|
1736
|
+
// ── source-clip input (hero = dominant/accented; inline = subdued) ──
|
|
1737
|
+
function renderSource(variant: "hero" | "inline") {
|
|
1729
1738
|
const isHero = variant === "hero";
|
|
1730
|
-
const isAlt = variant === "inline-alt";
|
|
1731
1739
|
const drawEye = isHero && sourceAttention;
|
|
1732
|
-
const label =
|
|
1733
|
-
? "…or use a source clip (extend from its last frame)"
|
|
1734
|
-
: isHero
|
|
1740
|
+
const label = isHero
|
|
1735
1741
|
? "Source clip — required (the clip to repaint)"
|
|
1736
1742
|
: isV2V
|
|
1737
1743
|
? "Source clip — required"
|
|
@@ -1974,20 +1980,26 @@ export function StudioGenerateSurface({
|
|
|
1974
1980
|
|
|
1975
1981
|
const knobGrid = (
|
|
1976
1982
|
<div className="vi-studio-knobs">
|
|
1983
|
+
<div className="vi-knob vi-studio-knob-wide">
|
|
1984
|
+
<label htmlFor="vi-clip-settings-mode">settings</label>
|
|
1985
|
+
<select id="vi-clip-settings-mode" className="vi-knob-select"
|
|
1986
|
+
value={settingsMode}
|
|
1987
|
+
onChange={(e) => setSettingsMode(e.target.value as "oracle" | "manual")}>
|
|
1988
|
+
<option value="oracle">Oracle-generated (default)</option>
|
|
1989
|
+
<option value="manual">Manual</option>
|
|
1990
|
+
</select>
|
|
1991
|
+
</div>
|
|
1977
1992
|
{/* ABOUT — the condensed what-is-this / how-to prose (k89), collapsed at the
|
|
1978
1993
|
top of the Settings-tab content. Absorbs the descriptive halves of the old
|
|
1979
1994
|
inline callouts; the conditional required-input warnings stay on the form. */}
|
|
1980
1995
|
<div className="vi-studio-knob-wide" style={{ flex: "1 1 100%" }}>
|
|
1981
1996
|
<AboutExpander title="About Clip">
|
|
1982
1997
|
<p>
|
|
1983
|
-
Generates
|
|
1984
|
-
generator needs.
|
|
1998
|
+
Generates one clip from your prompt and the inputs you provide.
|
|
1985
1999
|
</p>
|
|
1986
2000
|
<p>
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
extend), restyle a source clip, identity lock 1–4 reference images, and
|
|
1990
|
-
pose/structure a control still.
|
|
2001
|
+
A prompt alone uses text-to-video. Add a start image or source clip to
|
|
2002
|
+
use image-to-video; a source clip continues from its last frame.
|
|
1991
2003
|
</p>
|
|
1992
2004
|
<p>
|
|
1993
2005
|
A missing required input is flagged on the card and gates Generate until
|
|
@@ -2138,6 +2150,7 @@ export function StudioGenerateSurface({
|
|
|
2138
2150
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2139
2151
|
onChange={(e) => {
|
|
2140
2152
|
geomTouched.current = true;
|
|
2153
|
+
markManual("width");
|
|
2141
2154
|
setWidth(Number(e.target.value) || 0);
|
|
2142
2155
|
}}
|
|
2143
2156
|
/>
|
|
@@ -2155,6 +2168,7 @@ export function StudioGenerateSurface({
|
|
|
2155
2168
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2156
2169
|
onChange={(e) => {
|
|
2157
2170
|
geomTouched.current = true;
|
|
2171
|
+
markManual("height");
|
|
2158
2172
|
setHeight(Number(e.target.value) || 0);
|
|
2159
2173
|
}}
|
|
2160
2174
|
/>
|
|
@@ -2172,6 +2186,7 @@ export function StudioGenerateSurface({
|
|
|
2172
2186
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2173
2187
|
onChange={(e) => {
|
|
2174
2188
|
geomTouched.current = true;
|
|
2189
|
+
markManual("fps");
|
|
2175
2190
|
setFps(Number(e.target.value) || 0);
|
|
2176
2191
|
}}
|
|
2177
2192
|
/>
|
|
@@ -2190,7 +2205,7 @@ export function StudioGenerateSurface({
|
|
|
2190
2205
|
type="number"
|
|
2191
2206
|
min={0}
|
|
2192
2207
|
value={seed}
|
|
2193
|
-
onChange={(e) => setSeed(Number(e.target.value) || 0)}
|
|
2208
|
+
onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }}
|
|
2194
2209
|
/>
|
|
2195
2210
|
</div>
|
|
2196
2211
|
<div className="vi-knob">
|
|
@@ -2226,7 +2241,7 @@ export function StudioGenerateSurface({
|
|
|
2226
2241
|
max={100}
|
|
2227
2242
|
value={steps}
|
|
2228
2243
|
placeholder="model default"
|
|
2229
|
-
onChange={(e) => setSteps(e.target.value)}
|
|
2244
|
+
onChange={(e) => { markManual("steps"); setSteps(e.target.value); }}
|
|
2230
2245
|
/>
|
|
2231
2246
|
</div>
|
|
2232
2247
|
<div className="vi-knob">
|
|
@@ -2240,7 +2255,7 @@ export function StudioGenerateSurface({
|
|
|
2240
2255
|
step={0.5}
|
|
2241
2256
|
value={cfg}
|
|
2242
2257
|
placeholder="model default"
|
|
2243
|
-
onChange={(e) => setCfg(e.target.value)}
|
|
2258
|
+
onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }}
|
|
2244
2259
|
/>
|
|
2245
2260
|
</div>
|
|
2246
2261
|
{/* MODEL PIN — FILTERED BY CAPABILITY (2026-07-29).
|
|
@@ -2261,9 +2276,12 @@ export function StudioGenerateSurface({
|
|
|
2261
2276
|
id="vi-studio-model"
|
|
2262
2277
|
className="vi-knob-select"
|
|
2263
2278
|
value={modelId}
|
|
2264
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2279
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2265
2280
|
>
|
|
2266
2281
|
<option value="">Auto — router picks by budget + resolution</option>
|
|
2282
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2283
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2284
|
+
)}
|
|
2267
2285
|
{capModels.length > 0 && (
|
|
2268
2286
|
<optgroup label={`Ratified for ${CAP_LABELS[capability]}`}>
|
|
2269
2287
|
{capModels.map((id) => (
|
|
@@ -2468,21 +2486,35 @@ export function StudioGenerateSurface({
|
|
|
2468
2486
|
onStdNegativeChange={setStdNegative}
|
|
2469
2487
|
/>
|
|
2470
2488
|
{/* PROMPT ASSIST — Enhance enriches the clip prompt, Generate writes a fresh
|
|
2471
|
-
one
|
|
2489
|
+
one. Both update the same field. Same shared cluster as the Generate
|
|
2472
2490
|
station; errors surface inline and never clobber the existing prompt. */}
|
|
2473
2491
|
<PromptAssistButtons
|
|
2474
2492
|
busy={assist.assistBusy}
|
|
2475
2493
|
error={assist.assistError}
|
|
2476
2494
|
onDismissError={assist.dismissError}
|
|
2477
|
-
onEnhance={() => void
|
|
2495
|
+
onEnhance={() => void (async () => {
|
|
2496
|
+
let enhanced = "";
|
|
2497
|
+
await assist.runAssist("detail", prompt, (p) => {
|
|
2498
|
+
enhanced = p;
|
|
2499
|
+
setPrompt(p);
|
|
2500
|
+
});
|
|
2501
|
+
if (enhanced && settingsMode === "oracle") {
|
|
2502
|
+
try { await oraclePlan(enhanced); }
|
|
2503
|
+
catch (err) { setGenMsg(err instanceof Error ? err.message : "Oracle planning failed."); }
|
|
2504
|
+
}
|
|
2505
|
+
})()}
|
|
2478
2506
|
onGenerate={() =>
|
|
2479
2507
|
void (async () => {
|
|
2480
2508
|
let fresh = "";
|
|
2481
|
-
await assist.runAssist("generate",
|
|
2509
|
+
await assist.runAssist("generate", "", (p) => {
|
|
2482
2510
|
fresh = p;
|
|
2483
2511
|
setPrompt(p);
|
|
2484
2512
|
});
|
|
2485
2513
|
if (!fresh.trim()) return;
|
|
2514
|
+
if (settingsMode === "oracle") {
|
|
2515
|
+
try { await oraclePlan(fresh); }
|
|
2516
|
+
catch (err) { setGenMsg(err instanceof Error ? err.message : "Oracle planning failed."); }
|
|
2517
|
+
}
|
|
2486
2518
|
// NEGATIVE RIDES ALONG (operator 2026-08-13): generated
|
|
2487
2519
|
// exclusions APPEND to any typed negative; std tick untouched.
|
|
2488
2520
|
await assist.runNegative({ subject: fresh }, (neg) => {
|
|
@@ -2543,6 +2575,22 @@ export function StudioGenerateSurface({
|
|
|
2543
2575
|
)}
|
|
2544
2576
|
{promptAttachments.length > 0 && (
|
|
2545
2577
|
<div className="vi-gen-part-attach">
|
|
2578
|
+
{acceptsStartImage && canAddImage && (
|
|
2579
|
+
<label className="vi-btn vi-btn-sm vi-btn-ghost vi-file-label">
|
|
2580
|
+
{imageBusy ? "Adding image…" : "+ image file"}
|
|
2581
|
+
<input
|
|
2582
|
+
type="file"
|
|
2583
|
+
accept="image/*"
|
|
2584
|
+
className="vi-file-input"
|
|
2585
|
+
disabled={imageBusy}
|
|
2586
|
+
onChange={(e) => {
|
|
2587
|
+
const file = e.target.files?.[0];
|
|
2588
|
+
if (file) void onPickImageFile(file, "start");
|
|
2589
|
+
e.target.value = "";
|
|
2590
|
+
}}
|
|
2591
|
+
/>
|
|
2592
|
+
</label>
|
|
2593
|
+
)}
|
|
2546
2594
|
{promptAttachments.map((a, i) => (
|
|
2547
2595
|
<button
|
|
2548
2596
|
key={`${a.label}-${i}`}
|
|
@@ -2557,6 +2605,12 @@ export function StudioGenerateSurface({
|
|
|
2557
2605
|
))}
|
|
2558
2606
|
</div>
|
|
2559
2607
|
)}
|
|
2608
|
+
{acceptsStartImage && imageError && (
|
|
2609
|
+
<p className="vi-error" role="alert">{imageError}</p>
|
|
2610
|
+
)}
|
|
2611
|
+
{acceptsStartImage && showLibPicker && canAddImage && (
|
|
2612
|
+
<LibraryImageGrid images={libraryImages} onPick={addStartImage} />
|
|
2613
|
+
)}
|
|
2560
2614
|
|
|
2561
2615
|
{/* SETTINGS EXPANDER (operator ask 2026-08-13): the clip prompt
|
|
2562
2616
|
component hosts the FULL knob set, same as Scene/Movie/Cinema
|
|
@@ -2567,15 +2621,27 @@ export function StudioGenerateSurface({
|
|
|
2567
2621
|
on the Settings tab only: it morphs the form shape (required
|
|
2568
2622
|
inputs), it is not a render knob. */}
|
|
2569
2623
|
<PromptCardSettings label="settings — model · size · steps · cfg · seed · budget">
|
|
2624
|
+
<div className="vi-knob vi-studio-knob-wide">
|
|
2625
|
+
<label htmlFor="vi-clipcard-settings-mode">settings</label>
|
|
2626
|
+
<select id="vi-clipcard-settings-mode" className="vi-knob-select"
|
|
2627
|
+
value={settingsMode}
|
|
2628
|
+
onChange={(e) => setSettingsMode(e.target.value as "oracle" | "manual")}>
|
|
2629
|
+
<option value="oracle">Oracle-generated (default)</option>
|
|
2630
|
+
<option value="manual">Manual</option>
|
|
2631
|
+
</select>
|
|
2632
|
+
</div>
|
|
2570
2633
|
<div className="vi-knob vi-studio-knob-wide" style={{ minWidth: "16rem" }}>
|
|
2571
2634
|
<label htmlFor="vi-clipcard-model">model pin</label>
|
|
2572
2635
|
<select
|
|
2573
2636
|
id="vi-clipcard-model"
|
|
2574
2637
|
className="vi-knob-select"
|
|
2575
2638
|
value={modelId}
|
|
2576
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2639
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2577
2640
|
>
|
|
2578
2641
|
<option value="">Auto — router picks by budget + resolution</option>
|
|
2642
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2643
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2644
|
+
)}
|
|
2579
2645
|
{capModels.length > 0 && (
|
|
2580
2646
|
<optgroup label={`Ratified for ${CAP_LABELS[capability]}`}>
|
|
2581
2647
|
{capModels.map((id) => (
|
|
@@ -2600,36 +2666,36 @@ export function StudioGenerateSurface({
|
|
|
2600
2666
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2601
2667
|
<label htmlFor="vi-clipcard-width">width</label>
|
|
2602
2668
|
<input id="vi-clipcard-width" className="vi-knob-input" type="number" min={64} step={8}
|
|
2603
|
-
value={width} onChange={(e) => setWidth(Number(e.target.value) || 0)} />
|
|
2669
|
+
value={width} onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }} />
|
|
2604
2670
|
</div>
|
|
2605
2671
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2606
2672
|
<label htmlFor="vi-clipcard-height">height</label>
|
|
2607
2673
|
<input id="vi-clipcard-height" className="vi-knob-input" type="number" min={64} step={8}
|
|
2608
|
-
value={height} onChange={(e) => setHeight(Number(e.target.value) || 0)} />
|
|
2674
|
+
value={height} onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }} />
|
|
2609
2675
|
</div>
|
|
2610
2676
|
{/* LENGTH ROW (operator 2026-08-13): fps | frames | seconds — any two
|
|
2611
2677
|
set the third; the dimmed one is derived, last-touched wins. */}
|
|
2612
2678
|
<LengthRow
|
|
2613
2679
|
idPrefix="vi-clipcard-len"
|
|
2614
2680
|
fps={fps}
|
|
2615
|
-
onFps={(n) => setFps(n)}
|
|
2681
|
+
onFps={(n) => { markManual("fps"); setFps(n); }}
|
|
2616
2682
|
frames={requestedFrames}
|
|
2617
|
-
onFrames={setRequestedFrames}
|
|
2683
|
+
onFrames={(n) => { markManual("frames"); setRequestedFrames(n); }}
|
|
2618
2684
|
/>
|
|
2619
2685
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2620
2686
|
<label htmlFor="vi-clipcard-steps">steps</label>
|
|
2621
2687
|
<input id="vi-clipcard-steps" className="vi-knob-input" type="number" min={1} max={100}
|
|
2622
|
-
value={steps} placeholder="model default" onChange={(e) => setSteps(e.target.value)} />
|
|
2688
|
+
value={steps} placeholder="model default" onChange={(e) => { markManual("steps"); setSteps(e.target.value); }} />
|
|
2623
2689
|
</div>
|
|
2624
2690
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2625
2691
|
<label htmlFor="vi-clipcard-cfg">cfg</label>
|
|
2626
2692
|
<input id="vi-clipcard-cfg" className="vi-knob-input" type="number" min={0} max={20} step={0.5}
|
|
2627
|
-
value={cfg} placeholder="model default" onChange={(e) => setCfg(e.target.value)} />
|
|
2693
|
+
value={cfg} placeholder="model default" onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }} />
|
|
2628
2694
|
</div>
|
|
2629
2695
|
<div className="vi-knob" style={{ maxWidth: "8rem" }}>
|
|
2630
2696
|
<label htmlFor="vi-clipcard-seed">seed</label>
|
|
2631
2697
|
<input id="vi-clipcard-seed" className="vi-knob-input" type="number" min={0}
|
|
2632
|
-
value={seed} onChange={(e) => setSeed(Number(e.target.value) || 0)} />
|
|
2698
|
+
value={seed} onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }} />
|
|
2633
2699
|
</div>
|
|
2634
2700
|
<div className="vi-knob" style={{ maxWidth: "9rem" }}>
|
|
2635
2701
|
<label htmlFor="vi-clipcard-vram">VRAM budget GB</label>
|
|
@@ -2645,6 +2711,15 @@ export function StudioGenerateSurface({
|
|
|
2645
2711
|
</PromptCardSettings>
|
|
2646
2712
|
</PromptCard>
|
|
2647
2713
|
</ol>
|
|
2714
|
+
<button type="button" className="vi-btn vi-btn-sm" onClick={addNextPrompt}>
|
|
2715
|
+
+ Add next prompt
|
|
2716
|
+
</button>
|
|
2717
|
+
<button type="button" className="vi-btn vi-btn-sm"
|
|
2718
|
+
disabled={produceBusy}
|
|
2719
|
+
onClick={() => void generateProductionFromClip()}>
|
|
2720
|
+
{produceBusy ? "Generating production…" : "✨ Generate production"}
|
|
2721
|
+
</button>
|
|
2722
|
+
{sequenceMsg && <p className="vi-knob-hint" role="status">{sequenceMsg}</p>}
|
|
2648
2723
|
</section>
|
|
2649
2724
|
|
|
2650
2725
|
{/* Required-input callout — surfaces the missing prerequisite PROMINENTLY next to
|
|
@@ -2658,16 +2733,6 @@ export function StudioGenerateSurface({
|
|
|
2658
2733
|
|
|
2659
2734
|
{/* ── 3) SHAPE EDITOR (heroes) — morphs per preformed shape; stays dominant when
|
|
2660
2735
|
required (accent gating) even though it now sits below the knob grid. ── */}
|
|
2661
|
-
{hero === "start-image" && (
|
|
2662
|
-
<section
|
|
2663
|
-
aria-label="Required input"
|
|
2664
|
-
className="vi-studio-shape"
|
|
2665
|
-
style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
|
|
2666
|
-
>
|
|
2667
|
-
{renderStartImage("hero")}
|
|
2668
|
-
{renderSource("inline-alt")}
|
|
2669
|
-
</section>
|
|
2670
|
-
)}
|
|
2671
2736
|
{hero === "source" && (
|
|
2672
2737
|
<section
|
|
2673
2738
|
aria-label="Required input"
|
|
@@ -2703,7 +2768,6 @@ export function StudioGenerateSurface({
|
|
|
2703
2768
|
)}
|
|
2704
2769
|
|
|
2705
2770
|
{/* LOWER inputs — only when NOT a hero shape (custom / synthetic / t2v). */}
|
|
2706
|
-
{!hero && isI2V && <div className="vi-studio-shape">{renderStartImage("inline")}</div>}
|
|
2707
2771
|
{!hero && !isT2V && !isIdLock && <div className="vi-studio-shape">{renderSource("inline")}</div>}
|
|
2708
2772
|
{!hero && isT2V && (
|
|
2709
2773
|
<p className="vi-knob-hint" role="note" style={{ marginTop: "0.9rem" }}>
|
|
@@ -2763,6 +2827,12 @@ export function StudioGenerateSurface({
|
|
|
2763
2827
|
{genMsg}
|
|
2764
2828
|
</p>
|
|
2765
2829
|
)}
|
|
2830
|
+
{activeClipJob && (
|
|
2831
|
+
<p className="vi-knob-hint" role="status" style={{ margin: 0 }}>
|
|
2832
|
+
Clip {activeClipJob.status ?? "queued"} · {activeClipJob.jobId.slice(0, 8)}
|
|
2833
|
+
{activeClipJob.error ? ` — ${activeClipJob.error}` : ""}
|
|
2834
|
+
</p>
|
|
2835
|
+
)}
|
|
2766
2836
|
</div>
|
|
2767
2837
|
{nameWarn && (
|
|
2768
2838
|
<UnnamedWarnPanel
|
|
@@ -2792,7 +2862,7 @@ export function StudioGenerateSurface({
|
|
|
2792
2862
|
: isMotion
|
|
2793
2863
|
? "Generate (pose / structure)"
|
|
2794
2864
|
: isI2V
|
|
2795
|
-
? "Generate (i2v)"
|
|
2865
|
+
? clipRoute === "i2v" ? "Generate (i2v)" : "Generate (t2v)"
|
|
2796
2866
|
: "Generate (t2v)"}
|
|
2797
2867
|
</button>
|
|
2798
2868
|
{/* (The cross-model "🧪 Test all models" ghost that sat here moved into
|