@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
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from "../../video/contract";
|
|
68
68
|
import { getSessionId } from "../../session";
|
|
69
69
|
import { addToLibrary, useMediaLibrary } from "../../video/mediaLibrary";
|
|
70
|
+
import { subscribeJobStream } from "../../video/jobStream";
|
|
70
71
|
import { UnnamedWarnPanel, WarnUnnamedToggle, useWarnUnnamedPref } from "../../video/UnnamedWarn";
|
|
71
72
|
import { LengthRow } from "../../video/LengthRow";
|
|
72
73
|
import { GoalComposer } from "../GoalComposer";
|
|
@@ -109,6 +110,7 @@ import {
|
|
|
109
110
|
type CoordinationReport,
|
|
110
111
|
} from "../../video/coordinationReport";
|
|
111
112
|
import { CoordinationBadge, CoordinationSummary, decisionKey } from "./coordinationBadge";
|
|
113
|
+
import { planStudioSettings, planStudioMovie } from "../../video/studioOracle";
|
|
112
114
|
// B3: the visual spliced row + branch-frame scrubber (the picture half). Pure math
|
|
113
115
|
// lives in movieTimeline.ts; both are new files beside this composer.
|
|
114
116
|
import { StudioSplicedRow, StudioPendingRow } from "./StudioSplicedRow";
|
|
@@ -316,6 +318,40 @@ interface GoalRow {
|
|
|
316
318
|
frames: string;
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
/** A single Clip prompt promoted into the existing sequential Cinema composer. */
|
|
322
|
+
export interface ClipSequenceDraft {
|
|
323
|
+
id: number;
|
|
324
|
+
prompt: string;
|
|
325
|
+
negative: string;
|
|
326
|
+
stdNegative: boolean;
|
|
327
|
+
startImage: MediaRef | null;
|
|
328
|
+
width: number;
|
|
329
|
+
height: number;
|
|
330
|
+
fps: number;
|
|
331
|
+
seed: number;
|
|
332
|
+
frames: string;
|
|
333
|
+
modelId: string;
|
|
334
|
+
steps: string;
|
|
335
|
+
cfg: string;
|
|
336
|
+
vramBudget: string;
|
|
337
|
+
project: string;
|
|
338
|
+
plannedSegments?: Array<{
|
|
339
|
+
prompt: string;
|
|
340
|
+
negative: string;
|
|
341
|
+
joint: "still" | "cut";
|
|
342
|
+
settings?: {
|
|
343
|
+
model_id?: string; steps?: number; cfg?: number;
|
|
344
|
+
seed?: number; frames?: number; width?: number;
|
|
345
|
+
height?: number; fps?: number;
|
|
346
|
+
};
|
|
347
|
+
}>;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
interface CinemaImportBackup extends Omit<ClipSequenceDraft, "id" | "prompt" | "negative" | "stdNegative"> {
|
|
351
|
+
goals: GoalRow[];
|
|
352
|
+
movieTitle: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
319
355
|
// The counter is SESSION-persisted (not a module `let`) now that the goal rows
|
|
320
356
|
// themselves survive a reload: a module counter restarts at 0 while the restored
|
|
321
357
|
// rows keep their old keys, so `goal_1` could be minted twice — and this key
|
|
@@ -342,6 +378,16 @@ const producerPlanSchema = z.object({
|
|
|
342
378
|
negative: z.string().optional(),
|
|
343
379
|
seconds: z.number(),
|
|
344
380
|
joint: z.string(),
|
|
381
|
+
settings: z.object({
|
|
382
|
+
model_id: z.string().optional(),
|
|
383
|
+
width: z.number().optional(),
|
|
384
|
+
height: z.number().optional(),
|
|
385
|
+
fps: z.number().optional(),
|
|
386
|
+
frames: z.number().optional(),
|
|
387
|
+
steps: z.number().optional(),
|
|
388
|
+
cfg: z.number().optional(),
|
|
389
|
+
seed: z.number().optional(),
|
|
390
|
+
}).optional(),
|
|
345
391
|
}),
|
|
346
392
|
)
|
|
347
393
|
.min(1),
|
|
@@ -391,13 +437,15 @@ function useStudioMovieJob(): StudioMovieJobApi {
|
|
|
391
437
|
const [running, setRunning] = useState(false);
|
|
392
438
|
|
|
393
439
|
const mounted = useRef(true);
|
|
394
|
-
|
|
440
|
+
// Unsubscribe from the shared job stream while this movie job runs (replaces
|
|
441
|
+
// the retired 2.5s poll interval — a transition re-reads on push).
|
|
442
|
+
const unsub = useRef<null | (() => void)>(null);
|
|
395
443
|
const jobRef = useRef<string | null>(null);
|
|
396
444
|
|
|
397
445
|
const stopPoll = useCallback(() => {
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
446
|
+
if (unsub.current != null) {
|
|
447
|
+
unsub.current();
|
|
448
|
+
unsub.current = null;
|
|
401
449
|
}
|
|
402
450
|
}, []);
|
|
403
451
|
|
|
@@ -469,7 +517,8 @@ function useStudioMovieJob(): StudioMovieJobApi {
|
|
|
469
517
|
jobRef.current = id;
|
|
470
518
|
setStatus("queued");
|
|
471
519
|
void poll();
|
|
472
|
-
|
|
520
|
+
if (unsub.current != null) unsub.current();
|
|
521
|
+
unsub.current = subscribeJobStream(() => void poll());
|
|
473
522
|
});
|
|
474
523
|
},
|
|
475
524
|
[poll, stopPoll],
|
|
@@ -635,6 +684,9 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
635
684
|
blankGoal(),
|
|
636
685
|
blankGoal(),
|
|
637
686
|
]);
|
|
687
|
+
const [clipSequenceDraft, setClipSequenceDraft] = useSessionState<ClipSequenceDraft | null>(
|
|
688
|
+
"studio.movie.clipSequenceDraft", null,
|
|
689
|
+
);
|
|
638
690
|
// Rows restored from a session written BEFORE the per-segment knob round
|
|
639
691
|
// (2026-08-12) lack modelId/steps/cfg/contextFrames — normalize to the blank
|
|
640
692
|
// ("inherit") value so `.trim()` never meets undefined. useMemo, not a write:
|
|
@@ -684,6 +736,26 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
684
736
|
// surface's idiom. The movie route validates these at the movie level (studio_movie_schema
|
|
685
737
|
// _check_steps_cfg: steps int [1,100], cfg number [0,20]) — mirrored in buildBody below.
|
|
686
738
|
const [modelId, setModelId] = useSessionState("studio.movie.modelId", "");
|
|
739
|
+
const [settingsMode, setSettingsMode] = useSessionState<"oracle" | "manual">(
|
|
740
|
+
"studio.movie.settingsMode", "oracle",
|
|
741
|
+
);
|
|
742
|
+
const manualSettings = useSessionRef<string[]>("studio.movie.manualSettings", []);
|
|
743
|
+
const goalManualSettings = useSessionRef<Record<string, string[]>>(
|
|
744
|
+
"studio.movie.goalManualSettings", {},
|
|
745
|
+
);
|
|
746
|
+
const markManual = (field: string) => {
|
|
747
|
+
if (!manualSettings.current.includes(field)) {
|
|
748
|
+
manualSettings.current = [...manualSettings.current, field];
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const markGoalManual = (key: string, field: string) => {
|
|
752
|
+
const current = goalManualSettings.current[key] || [];
|
|
753
|
+
if (!current.includes(field)) {
|
|
754
|
+
goalManualSettings.current = {
|
|
755
|
+
...goalManualSettings.current, [key]: [...current, field],
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
};
|
|
687
759
|
const [steps, setSteps] = useSessionState("studio.movie.steps", "");
|
|
688
760
|
const [cfg, setCfg] = useSessionState("studio.movie.cfg", "");
|
|
689
761
|
// MERGE-NOT-OVERWRITE bookkeeping — true once the OPERATOR has typed a budget. A
|
|
@@ -1151,6 +1223,77 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1151
1223
|
// "extend motion" stays that way as it grows. No upper cap: the count takes
|
|
1152
1224
|
// any integer ≥ 1.
|
|
1153
1225
|
const [goalStash, setGoalStash] = useSessionState<GoalRow[]>("studio.movie.goalStash", () => []);
|
|
1226
|
+
const [clipImportBackup, setClipImportBackup] = useSessionState<CinemaImportBackup | null>(
|
|
1227
|
+
"studio.movie.clipImportBackup", null,
|
|
1228
|
+
);
|
|
1229
|
+
const importedClipDraftRef = useRef<number | null>(null);
|
|
1230
|
+
useEffect(() => {
|
|
1231
|
+
const draft = clipSequenceDraft;
|
|
1232
|
+
if (!draft || importedClipDraftRef.current === draft.id) return;
|
|
1233
|
+
importedClipDraftRef.current = draft.id;
|
|
1234
|
+
// A Clip import starts a new sequence. Preserve the previous Cinema draft
|
|
1235
|
+
// separately so the goal-count stash cannot inject old rows into this one.
|
|
1236
|
+
if (goalsRaw.some((g) => g.prompt.trim() !== "")) {
|
|
1237
|
+
setClipImportBackup({ goals: goalsRaw, startImage, width, height, fps, seed,
|
|
1238
|
+
frames: framesDefault, modelId, steps, cfg, vramBudget, project, movieTitle });
|
|
1239
|
+
}
|
|
1240
|
+
setGoals(draft.plannedSegments?.length
|
|
1241
|
+
? draft.plannedSegments.map((s, i) => ({
|
|
1242
|
+
...blankGoal(i === 0 ? "still" : s.joint),
|
|
1243
|
+
prompt: s.prompt, negative: s.negative,
|
|
1244
|
+
stdNegative: draft.stdNegative,
|
|
1245
|
+
modelId: s.settings?.model_id || "",
|
|
1246
|
+
steps: s.settings?.steps == null ? "" : String(s.settings.steps),
|
|
1247
|
+
cfg: s.settings?.cfg == null ? "" : String(s.settings.cfg),
|
|
1248
|
+
seed: s.settings?.seed == null ? "" : String(s.settings.seed),
|
|
1249
|
+
frames: s.settings?.frames == null ? "" : String(s.settings.frames),
|
|
1250
|
+
}))
|
|
1251
|
+
: [
|
|
1252
|
+
{ ...blankGoal(), prompt: draft.prompt, negative: draft.negative,
|
|
1253
|
+
stdNegative: draft.stdNegative },
|
|
1254
|
+
blankGoal("still"),
|
|
1255
|
+
]);
|
|
1256
|
+
setStartImage(draft.startImage);
|
|
1257
|
+
setWidth(draft.width);
|
|
1258
|
+
setHeight(draft.height);
|
|
1259
|
+
setFps(draft.fps);
|
|
1260
|
+
setSeed(draft.seed);
|
|
1261
|
+
setFramesDefault(draft.frames);
|
|
1262
|
+
setModelId(draft.modelId);
|
|
1263
|
+
setSteps(draft.steps);
|
|
1264
|
+
setCfg(draft.cfg);
|
|
1265
|
+
setVramBudget(draft.vramBudget);
|
|
1266
|
+
setProject(draft.project);
|
|
1267
|
+
setMovieTitle(draft.project);
|
|
1268
|
+
setFormMsg(draft.plannedSegments?.length
|
|
1269
|
+
? `Oracle generated ${draft.plannedSegments.length} sequential prompt cards. Review them, then Generate movie.`
|
|
1270
|
+
: "Clip prompt is goal 1. Add the next prompt in goal 2; it continues from goal 1's last frame by default.");
|
|
1271
|
+
setClipSequenceDraft(null);
|
|
1272
|
+
}, [clipSequenceDraft, goalsRaw, startImage, width, height, fps, seed,
|
|
1273
|
+
framesDefault, modelId, steps, cfg, vramBudget, project, movieTitle,
|
|
1274
|
+
setClipSequenceDraft, setClipImportBackup, setGoals,
|
|
1275
|
+
setStartImage, setWidth, setHeight, setFps, setSeed, setFramesDefault,
|
|
1276
|
+
setModelId, setSteps, setCfg, setVramBudget, setProject, setMovieTitle]);
|
|
1277
|
+
|
|
1278
|
+
function restorePreviousCinemaDraft() {
|
|
1279
|
+
const prior = clipImportBackup;
|
|
1280
|
+
if (!prior) return;
|
|
1281
|
+
setGoals(prior.goals);
|
|
1282
|
+
setStartImage(prior.startImage);
|
|
1283
|
+
setWidth(prior.width);
|
|
1284
|
+
setHeight(prior.height);
|
|
1285
|
+
setFps(prior.fps);
|
|
1286
|
+
setSeed(prior.seed);
|
|
1287
|
+
setFramesDefault(prior.frames);
|
|
1288
|
+
setModelId(prior.modelId);
|
|
1289
|
+
setSteps(prior.steps);
|
|
1290
|
+
setCfg(prior.cfg);
|
|
1291
|
+
setVramBudget(prior.vramBudget);
|
|
1292
|
+
setProject(prior.project);
|
|
1293
|
+
setMovieTitle(prior.movieTitle);
|
|
1294
|
+
setClipImportBackup(null);
|
|
1295
|
+
setFormMsg("Previous Cinema draft restored.");
|
|
1296
|
+
}
|
|
1154
1297
|
|
|
1155
1298
|
// ── 🎬 PRODUCER (k120 slice 1) — premise → /video/producer/plan → rows. ──
|
|
1156
1299
|
// The plan's per-segment seconds convert to frames against the CURRENT movie
|
|
@@ -1163,21 +1306,18 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1163
1306
|
);
|
|
1164
1307
|
const [producing, setProducing] = useState(false);
|
|
1165
1308
|
const [produceError, setProduceError] = useState<string | null>(null);
|
|
1166
|
-
// k120 slice 2 — render-time continuity: the runner rewrites each next segment
|
|
1167
|
-
// from the previous segment's ACTUAL closing frame. Auto-enabled by a
|
|
1168
|
-
// successful Produce (a scripted movie wants it); always operator-untickable.
|
|
1169
|
-
const [continuityRefresh, setContinuityRefresh] = useSessionState<boolean>(
|
|
1170
|
-
"studio.movie.continuityRefresh",
|
|
1171
|
-
false,
|
|
1172
|
-
);
|
|
1173
1309
|
const produceMovie = useCallback(async () => {
|
|
1174
|
-
const p = premise.trim();
|
|
1310
|
+
const p = premise.trim() || movieQuery.trim();
|
|
1175
1311
|
if (!p || producing) return;
|
|
1176
1312
|
setProducing(true);
|
|
1177
1313
|
setProduceError(null);
|
|
1178
1314
|
const res = await request<unknown>(hugpyConfig.producerPlanUrl, {
|
|
1179
1315
|
method: "POST",
|
|
1180
|
-
body: JSON.stringify({
|
|
1316
|
+
body: JSON.stringify({
|
|
1317
|
+
premise: p,
|
|
1318
|
+
model: assist.assistModel || undefined,
|
|
1319
|
+
start_image: startImage?.uri,
|
|
1320
|
+
}),
|
|
1181
1321
|
headers: { "Content-Type": "application/json" },
|
|
1182
1322
|
meta: { specKey: "studio", operation: "studio.producer.plan" },
|
|
1183
1323
|
});
|
|
@@ -1197,23 +1337,33 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1197
1337
|
const joint: JointMode =
|
|
1198
1338
|
i === 0
|
|
1199
1339
|
? "still"
|
|
1200
|
-
: s.joint === "cut" || s.joint === "still"
|
|
1340
|
+
: s.joint === "cut" || s.joint === "still"
|
|
1201
1341
|
? (s.joint as JointMode)
|
|
1202
|
-
: "
|
|
1342
|
+
: "still";
|
|
1203
1343
|
return {
|
|
1204
1344
|
...blankGoal(joint),
|
|
1205
1345
|
prompt: s.prompt,
|
|
1206
1346
|
negative: s.negative ?? "",
|
|
1207
|
-
|
|
1347
|
+
modelId: s.settings?.model_id ?? "",
|
|
1348
|
+
frames: String(s.settings?.frames ?? Math.max(1, Math.round(s.seconds * effFps))),
|
|
1349
|
+
steps: s.settings?.steps == null ? "" : String(s.settings.steps),
|
|
1350
|
+
cfg: s.settings?.cfg == null ? "" : String(s.settings.cfg),
|
|
1351
|
+
seed: s.settings?.seed == null ? "" : String(s.settings.seed),
|
|
1208
1352
|
};
|
|
1209
1353
|
});
|
|
1210
1354
|
if (goals.some((g) => g.prompt.trim() !== "")) {
|
|
1211
1355
|
setGoalStash((st) => [...goals, ...st]);
|
|
1212
1356
|
}
|
|
1213
1357
|
setGoals(rows);
|
|
1358
|
+
const first = plan.segments[0]?.settings;
|
|
1359
|
+
if (first) {
|
|
1360
|
+
if (first.width != null && !manualSettings.current.includes("width")) setWidth(first.width);
|
|
1361
|
+
if (first.height != null && !manualSettings.current.includes("height")) setHeight(first.height);
|
|
1362
|
+
if (first.fps != null && !manualSettings.current.includes("fps")) setFps(first.fps);
|
|
1363
|
+
}
|
|
1214
1364
|
setProducerNote([plan.title, plan.logline].filter(Boolean).join(" — "));
|
|
1215
|
-
|
|
1216
|
-
|
|
1365
|
+
}, [premise, movieQuery, producing, goals, fps, assist.assistModel, startImage,
|
|
1366
|
+
setWidth, setHeight, setFps]);
|
|
1217
1367
|
|
|
1218
1368
|
const setSegmentCount = useCallback(
|
|
1219
1369
|
(n: number) => {
|
|
@@ -1328,6 +1478,42 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1328
1478
|
// name this ref minted may be replaced by the next Generate (operator
|
|
1329
1479
|
// 2026-08-13: "purely a product of the same generation method").
|
|
1330
1480
|
const autoTitleRef = useRef("");
|
|
1481
|
+
const planGoalPrompt = useCallback(async (
|
|
1482
|
+
i: number, text: string,
|
|
1483
|
+
coordinated?: { seed?: number; frames?: number; joint_mode?: string },
|
|
1484
|
+
) => {
|
|
1485
|
+
if (settingsMode !== "oracle") return;
|
|
1486
|
+
const g = goals[i];
|
|
1487
|
+
if (!g) return;
|
|
1488
|
+
const capability = i === 0
|
|
1489
|
+
? (startImage ? "i2v" : "t2v")
|
|
1490
|
+
: ((coordinated?.joint_mode || g.joint) === "cut" ? "t2v" : "i2v");
|
|
1491
|
+
const explicit = [...new Set([
|
|
1492
|
+
...manualSettings.current,
|
|
1493
|
+
...(goalManualSettings.current[g.key] || []),
|
|
1494
|
+
...(coordinated?.seed != null ? ["seed"] : []),
|
|
1495
|
+
...(coordinated?.frames != null ? ["frames"] : []),
|
|
1496
|
+
])];
|
|
1497
|
+
const planned = await planStudioSettings(capability, text, {
|
|
1498
|
+
model_id: g.modelId || undefined,
|
|
1499
|
+
width, height, fps,
|
|
1500
|
+
frames: coordinated?.frames ?? (g.frames ? Number(g.frames) : framesDefault ? Number(framesDefault) : undefined),
|
|
1501
|
+
steps: g.steps ? Number(g.steps) : steps ? Number(steps) : undefined,
|
|
1502
|
+
cfg: g.cfg ? Number(g.cfg) : cfg ? Number(cfg) : undefined,
|
|
1503
|
+
seed: coordinated?.seed ?? (g.seed ? Number(g.seed) : seed),
|
|
1504
|
+
}, explicit);
|
|
1505
|
+
setGoalField(i, {
|
|
1506
|
+
modelId: planned.model_id || "",
|
|
1507
|
+
frames: planned.frames == null ? g.frames : String(planned.frames),
|
|
1508
|
+
steps: planned.steps == null ? g.steps : String(planned.steps),
|
|
1509
|
+
cfg: planned.cfg == null ? g.cfg : String(planned.cfg),
|
|
1510
|
+
seed: planned.seed == null ? g.seed : String(planned.seed),
|
|
1511
|
+
});
|
|
1512
|
+
if (planned.width != null) setWidth(planned.width);
|
|
1513
|
+
if (planned.height != null) setHeight(planned.height);
|
|
1514
|
+
if (planned.fps != null) setFps(planned.fps);
|
|
1515
|
+
}, [settingsMode, goals, startImage, width, height, fps, framesDefault, steps, cfg,
|
|
1516
|
+
seed, setGoalField, setWidth, setHeight, setFps]);
|
|
1331
1517
|
|
|
1332
1518
|
const runRowAssist = useCallback(
|
|
1333
1519
|
(i: number, mode: "detail" | "generate") => {
|
|
@@ -1335,10 +1521,14 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1335
1521
|
if (!g) return;
|
|
1336
1522
|
void (async () => {
|
|
1337
1523
|
let fresh = "";
|
|
1338
|
-
await assist.runAssist(mode, g.prompt, (p) => {
|
|
1524
|
+
await assist.runAssist(mode, mode === "generate" ? "" : g.prompt, (p) => {
|
|
1339
1525
|
fresh = p;
|
|
1340
1526
|
setGoalField(i, { prompt: p });
|
|
1341
1527
|
}, { context: rowContext(i) });
|
|
1528
|
+
if (fresh.trim() && settingsMode === "oracle") {
|
|
1529
|
+
try { await planGoalPrompt(i, fresh); }
|
|
1530
|
+
catch (err) { setFormMsg(err instanceof Error ? err.message : "Oracle planning failed."); }
|
|
1531
|
+
}
|
|
1342
1532
|
if (mode !== "generate" || !fresh.trim()) return;
|
|
1343
1533
|
// NEGATIVE RIDES ALONG (operator 2026-08-13): the generated exclusion
|
|
1344
1534
|
// list APPENDS to this goal's typed negative, never clobbers it
|
|
@@ -1371,7 +1561,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1371
1561
|
intentText.current[g.key] = "";
|
|
1372
1562
|
setIntents((prev) => ({ ...prev, [g.key]: null }));
|
|
1373
1563
|
},
|
|
1374
|
-
[assist, goals, rowContext, setGoalField, movieTitle, setMovieTitle
|
|
1564
|
+
[assist, goals, rowContext, setGoalField, movieTitle, setMovieTitle,
|
|
1565
|
+
settingsMode, planGoalPrompt],
|
|
1375
1566
|
);
|
|
1376
1567
|
|
|
1377
1568
|
// ── negative generation (§1b) — an exclusion list for THIS shot ──
|
|
@@ -1444,6 +1635,25 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1444
1635
|
};
|
|
1445
1636
|
}),
|
|
1446
1637
|
);
|
|
1638
|
+
let oracleNotice: string | null = null;
|
|
1639
|
+
if (settingsMode === "oracle") {
|
|
1640
|
+
for (const key of applied) {
|
|
1641
|
+
const index = goals.findIndex((g) => g.key === key);
|
|
1642
|
+
const hit = bySeg.get(key);
|
|
1643
|
+
if (index < 0 || !hit) continue;
|
|
1644
|
+
try {
|
|
1645
|
+
const k = hit.knobs || {};
|
|
1646
|
+
await planGoalPrompt(index, hit.prompt, {
|
|
1647
|
+
seed: typeof k.seed === "number" ? k.seed : undefined,
|
|
1648
|
+
frames: typeof k.frames === "number" ? k.frames : undefined,
|
|
1649
|
+
joint_mode: typeof k.joint_mode === "string" ? k.joint_mode : undefined,
|
|
1650
|
+
});
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
oracleNotice = err instanceof Error ? err.message : "Oracle planning failed.";
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1447
1657
|
for (const key of applied) {
|
|
1448
1658
|
intentText.current[key] = "";
|
|
1449
1659
|
}
|
|
@@ -1458,7 +1668,10 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1458
1668
|
// this one.
|
|
1459
1669
|
setCoordination(readCoordinationReport(res.coordination));
|
|
1460
1670
|
setCoordSettled(new Set());
|
|
1461
|
-
setSpreadNotice(
|
|
1671
|
+
setSpreadNotice([
|
|
1672
|
+
...spreadNoticeLines(res, applied),
|
|
1673
|
+
...(oracleNotice ? [`Oracle settings: ${oracleNotice}`] : []),
|
|
1674
|
+
]);
|
|
1462
1675
|
}, [
|
|
1463
1676
|
assist,
|
|
1464
1677
|
goalInputs,
|
|
@@ -1470,6 +1683,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1470
1683
|
selectedKeys,
|
|
1471
1684
|
selectedProfile,
|
|
1472
1685
|
steeringSeed,
|
|
1686
|
+
settingsMode,
|
|
1687
|
+
planGoalPrompt,
|
|
1473
1688
|
]);
|
|
1474
1689
|
|
|
1475
1690
|
// ── GROUP ACTION: negatives for the selected rows ──
|
|
@@ -1655,6 +1870,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1655
1870
|
});
|
|
1656
1871
|
|
|
1657
1872
|
const body: Record<string, unknown> = {
|
|
1873
|
+
settings_mode: settingsMode,
|
|
1874
|
+
explicit_settings: manualSettings.current,
|
|
1658
1875
|
resolution: { width, height, fps },
|
|
1659
1876
|
frames: framesDefNum,
|
|
1660
1877
|
seed,
|
|
@@ -1684,13 +1901,9 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1684
1901
|
reference_images:
|
|
1685
1902
|
!selectedProfile && references.length ? references.map((r) => r.uri) : undefined,
|
|
1686
1903
|
goals: goalBodies,
|
|
1687
|
-
// k120 slice 2 — PRODUCER CONTINUITY REFRESH: the runner rewrites each
|
|
1688
|
-
// non-root segment's prompt from a vision read of the frame the previous
|
|
1689
|
-
// segment actually ended on. Off drops the key (schema default False).
|
|
1690
|
-
continuity_refresh: continuityRefresh || undefined,
|
|
1691
1904
|
};
|
|
1692
1905
|
return { body };
|
|
1693
|
-
}, [goals, width, height, fps, seed, vramBudget, modelId, steps, cfg, negative, project, movieTitle, startImage, references, selectedProfile,
|
|
1906
|
+
}, [goals, width, height, fps, seed, vramBudget, modelId, steps, cfg, negative, project, movieTitle, startImage, references, selectedProfile, settingsMode]);
|
|
1694
1907
|
|
|
1695
1908
|
// NAME NUDGE (k91) — set when a submit was held back only because the movie has no
|
|
1696
1909
|
// name. It is a warning, not a gate: the run row then offers "Generate without a
|
|
@@ -1701,7 +1914,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1701
1914
|
const [warnUnnamed, setWarnUnnamed] = useWarnUnnamedPref("cinema");
|
|
1702
1915
|
|
|
1703
1916
|
const onGenerate = useCallback(
|
|
1704
|
-
(skipNameCheck = false, titleOverride?: string) => {
|
|
1917
|
+
async (skipNameCheck = false, titleOverride?: string) => {
|
|
1705
1918
|
const built = buildBody(titleOverride);
|
|
1706
1919
|
if ("err" in built) {
|
|
1707
1920
|
setFormMsg(built.err);
|
|
@@ -1718,9 +1931,36 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1718
1931
|
setNameWarn(false);
|
|
1719
1932
|
setFormMsg(null);
|
|
1720
1933
|
if (titleOverride && titleOverride.trim()) setMovieTitle(titleOverride.trim());
|
|
1721
|
-
|
|
1934
|
+
let body = built.body;
|
|
1935
|
+
if (settingsMode === "oracle") {
|
|
1936
|
+
try {
|
|
1937
|
+
body = await planStudioMovie(body);
|
|
1938
|
+
const resolution = body.resolution as { width?: number; height?: number; fps?: number } | undefined;
|
|
1939
|
+
if (resolution?.width != null) setWidth(resolution.width);
|
|
1940
|
+
if (resolution?.height != null) setHeight(resolution.height);
|
|
1941
|
+
if (resolution?.fps != null) setFps(resolution.fps);
|
|
1942
|
+
const plannedGoals = body.goals as Array<Record<string, unknown>> | undefined;
|
|
1943
|
+
if (plannedGoals) setGoals((prev) => prev.map((g, i) => {
|
|
1944
|
+
const p = plannedGoals[i];
|
|
1945
|
+
if (!p) return g;
|
|
1946
|
+
return {
|
|
1947
|
+
...g,
|
|
1948
|
+
modelId: typeof p.model_id === "string" ? p.model_id : g.modelId,
|
|
1949
|
+
steps: typeof p.steps === "number" ? String(p.steps) : g.steps,
|
|
1950
|
+
cfg: typeof p.cfg === "number" ? String(p.cfg) : g.cfg,
|
|
1951
|
+
seed: typeof p.seed === "number" ? String(p.seed) : g.seed,
|
|
1952
|
+
frames: typeof p.frames === "number" ? String(p.frames) : g.frames,
|
|
1953
|
+
};
|
|
1954
|
+
}));
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
setFormMsg(err instanceof Error ? err.message : "Oracle planning failed.");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
job.run(body);
|
|
1722
1961
|
},
|
|
1723
|
-
[buildBody, job, movieTitle, setMovieTitle, warnUnnamed
|
|
1962
|
+
[buildBody, job, movieTitle, setMovieTitle, warnUnnamed, settingsMode,
|
|
1963
|
+
setWidth, setHeight, setFps, setGoals],
|
|
1724
1964
|
);
|
|
1725
1965
|
|
|
1726
1966
|
// SESSION LIBRARY PUSH (operator 2026-08-13): a finished movie's outputs
|
|
@@ -1920,9 +2160,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1920
2160
|
className="vi-knob-select"
|
|
1921
2161
|
value={modelId}
|
|
1922
2162
|
disabled={disableSubmit}
|
|
1923
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2163
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
1924
2164
|
>
|
|
1925
2165
|
<option value="">Auto — router picks by capability/budget</option>
|
|
2166
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2167
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2168
|
+
)}
|
|
1926
2169
|
{movieModels.length > 0 && (
|
|
1927
2170
|
<optgroup label="Bound by a ratified movie segment">
|
|
1928
2171
|
{movieModels.map((id) => (
|
|
@@ -1973,7 +2216,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1973
2216
|
value={steps}
|
|
1974
2217
|
placeholder="model default"
|
|
1975
2218
|
disabled={disableSubmit}
|
|
1976
|
-
onChange={(e) => setSteps(e.target.value)}
|
|
2219
|
+
onChange={(e) => { markManual("steps"); setSteps(e.target.value); }}
|
|
1977
2220
|
/>
|
|
1978
2221
|
</div>
|
|
1979
2222
|
<div className="vi-knob">
|
|
@@ -1988,7 +2231,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1988
2231
|
value={cfg}
|
|
1989
2232
|
placeholder="model default"
|
|
1990
2233
|
disabled={disableSubmit}
|
|
1991
|
-
onChange={(e) => setCfg(e.target.value)}
|
|
2234
|
+
onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }}
|
|
1992
2235
|
/>
|
|
1993
2236
|
</div>
|
|
1994
2237
|
|
|
@@ -2004,7 +2247,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2004
2247
|
step={8}
|
|
2005
2248
|
value={width}
|
|
2006
2249
|
disabled={disableSubmit}
|
|
2007
|
-
onChange={(e) => setWidth(Number(e.target.value) || 0)}
|
|
2250
|
+
onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }}
|
|
2008
2251
|
/>
|
|
2009
2252
|
</div>
|
|
2010
2253
|
<div className="vi-knob">
|
|
@@ -2017,7 +2260,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2017
2260
|
step={8}
|
|
2018
2261
|
value={height}
|
|
2019
2262
|
disabled={disableSubmit}
|
|
2020
|
-
onChange={(e) => setHeight(Number(e.target.value) || 0)}
|
|
2263
|
+
onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }}
|
|
2021
2264
|
/>
|
|
2022
2265
|
</div>
|
|
2023
2266
|
<div className="vi-knob">
|
|
@@ -2035,7 +2278,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2035
2278
|
? `Ratified movie envelope: ${movieGeometry.width}×${movieGeometry.height} @ ${movieGeometry.fps}fps (“${movieGeometry.presetTitle}”).`
|
|
2036
2279
|
: undefined
|
|
2037
2280
|
}
|
|
2038
|
-
onChange={(e) => setFps(Number(e.target.value) || 0)}
|
|
2281
|
+
onChange={(e) => { markManual("fps"); setFps(Number(e.target.value) || 0); }}
|
|
2039
2282
|
/>
|
|
2040
2283
|
{movieGeomOutOfEnvelope && (
|
|
2041
2284
|
<span className="vi-knob-flag">
|
|
@@ -2056,7 +2299,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2056
2299
|
min={0}
|
|
2057
2300
|
value={seed}
|
|
2058
2301
|
disabled={disableSubmit}
|
|
2059
|
-
onChange={(e) => setSeed(Number(e.target.value) || 0)}
|
|
2302
|
+
onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }}
|
|
2060
2303
|
/>
|
|
2061
2304
|
<span className="vi-knob-hint">A segment with no seed uses base seed + its index.</span>
|
|
2062
2305
|
</div>
|
|
@@ -2101,6 +2344,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2101
2344
|
|
|
2102
2345
|
return (
|
|
2103
2346
|
<div className="vi-studio-generate">
|
|
2347
|
+
{clipImportBackup && (
|
|
2348
|
+
<button type="button" className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
2349
|
+
onClick={restorePreviousCinemaDraft}>
|
|
2350
|
+
Restore previous Cinema draft
|
|
2351
|
+
</button>
|
|
2352
|
+
)}
|
|
2104
2353
|
{/* The movie Settings panel lives in the left-sidebar Settings tab (portaled into
|
|
2105
2354
|
settingsHost, like the Clip surface's knobGrid): templates + model pin + sampler
|
|
2106
2355
|
+ geometry/seed/budget/project; null until that panel mounts. */}
|
|
@@ -2147,6 +2396,15 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2147
2396
|
miss (and absent when no host mounts). This expander mirrors the
|
|
2148
2397
|
SAME state — two views, one truth, like the Clip card. ── */}
|
|
2149
2398
|
<PromptCardSettings label="movie settings — model · size · fps · steps · cfg · budget">
|
|
2399
|
+
<div className="vi-knob vi-studio-knob-wide">
|
|
2400
|
+
<label htmlFor="vi-movie-settings-mode">settings</label>
|
|
2401
|
+
<select id="vi-movie-settings-mode" className="vi-knob-select"
|
|
2402
|
+
value={settingsMode}
|
|
2403
|
+
onChange={(e) => setSettingsMode(e.target.value as "oracle" | "manual")}>
|
|
2404
|
+
<option value="oracle">Oracle-generated (default)</option>
|
|
2405
|
+
<option value="manual">Manual</option>
|
|
2406
|
+
</select>
|
|
2407
|
+
</div>
|
|
2150
2408
|
<div className="vi-knob vi-studio-knob-wide" style={{ minWidth: "16rem" }}>
|
|
2151
2409
|
<label htmlFor="vi-moviecol-model">model — whole movie</label>
|
|
2152
2410
|
<select
|
|
@@ -2154,9 +2412,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2154
2412
|
className="vi-knob-select"
|
|
2155
2413
|
value={modelId}
|
|
2156
2414
|
disabled={disableSubmit}
|
|
2157
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2415
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2158
2416
|
>
|
|
2159
2417
|
<option value="">Auto — router picks by capability/budget</option>
|
|
2418
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2419
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2420
|
+
)}
|
|
2160
2421
|
{movieModels.length > 0 && (
|
|
2161
2422
|
<optgroup label="Bound by a ratified movie segment">
|
|
2162
2423
|
{movieModels.map((id) => (
|
|
@@ -2176,12 +2437,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2176
2437
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2177
2438
|
<label htmlFor="vi-moviecol-width">width</label>
|
|
2178
2439
|
<input id="vi-moviecol-width" className="vi-knob-input" type="number" min={64} step={8}
|
|
2179
|
-
value={width} onChange={(e) => setWidth(Number(e.target.value) || 0)} />
|
|
2440
|
+
value={width} onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }} />
|
|
2180
2441
|
</div>
|
|
2181
2442
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2182
2443
|
<label htmlFor="vi-moviecol-height">height</label>
|
|
2183
2444
|
<input id="vi-moviecol-height" className="vi-knob-input" type="number" min={64} step={8}
|
|
2184
|
-
value={height} onChange={(e) => setHeight(Number(e.target.value) || 0)} />
|
|
2445
|
+
value={height} onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }} />
|
|
2185
2446
|
</div>
|
|
2186
2447
|
{/* LENGTH ROW (operator 2026-08-13): fps | frames/segment | seconds —
|
|
2187
2448
|
any two set the third; dimmed = derived, last-touched wins. This is
|
|
@@ -2189,20 +2450,20 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2189
2450
|
<LengthRow
|
|
2190
2451
|
idPrefix="vi-moviecol-len"
|
|
2191
2452
|
fps={fps}
|
|
2192
|
-
onFps={(n) => setFps(n)}
|
|
2453
|
+
onFps={(n) => { markManual("fps"); setFps(n); }}
|
|
2193
2454
|
frames={framesDefault}
|
|
2194
|
-
onFrames={setFramesDefault}
|
|
2455
|
+
onFrames={(v) => { markManual("frames"); setFramesDefault(v); }}
|
|
2195
2456
|
framesPlaceholder="model default (81) / segment"
|
|
2196
2457
|
/>
|
|
2197
2458
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2198
2459
|
<label htmlFor="vi-moviecol-steps">steps</label>
|
|
2199
2460
|
<input id="vi-moviecol-steps" className="vi-knob-input" type="number" min={1} max={100}
|
|
2200
|
-
value={steps} placeholder="model default" onChange={(e) => setSteps(e.target.value)} />
|
|
2461
|
+
value={steps} placeholder="model default" onChange={(e) => { markManual("steps"); setSteps(e.target.value); }} />
|
|
2201
2462
|
</div>
|
|
2202
2463
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2203
2464
|
<label htmlFor="vi-moviecol-cfg">cfg</label>
|
|
2204
2465
|
<input id="vi-moviecol-cfg" className="vi-knob-input" type="number" min={0} max={20} step={0.5}
|
|
2205
|
-
value={cfg} placeholder="model default" onChange={(e) => setCfg(e.target.value)} />
|
|
2466
|
+
value={cfg} placeholder="model default" onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }} />
|
|
2206
2467
|
</div>
|
|
2207
2468
|
<div className="vi-knob" style={{ maxWidth: "9rem" }}>
|
|
2208
2469
|
<label htmlFor="vi-moviecol-vram">VRAM budget GB</label>
|
|
@@ -2237,7 +2498,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2237
2498
|
className="vi-knob-input"
|
|
2238
2499
|
rows={2}
|
|
2239
2500
|
style={{ flex: "1 1 24rem", resize: "vertical" }}
|
|
2240
|
-
placeholder="
|
|
2501
|
+
placeholder="Describe the whole production — Oracle creates the needed sequential prompt cards and settings"
|
|
2241
2502
|
value={premise}
|
|
2242
2503
|
onChange={(e) => setPremise(e.target.value)}
|
|
2243
2504
|
disabled={producing}
|
|
@@ -2245,25 +2506,24 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2245
2506
|
<button
|
|
2246
2507
|
type="button"
|
|
2247
2508
|
className="vi-btn vi-btn-accent"
|
|
2248
|
-
disabled={producing || premise.trim() === ""}
|
|
2509
|
+
disabled={producing || (premise.trim() === "" && movieQuery.trim() === "")}
|
|
2249
2510
|
onClick={() => void produceMovie()}
|
|
2250
2511
|
title="Generate a full script from the premise: segment count, per-segment prompts, negatives, lengths and joins. Current rows are parked in the stash."
|
|
2251
2512
|
>
|
|
2252
|
-
{producing ? "
|
|
2513
|
+
{producing ? "Generating production…" : "✨ Generate production"}
|
|
2253
2514
|
</button>
|
|
2254
2515
|
</div>
|
|
2255
2516
|
<label
|
|
2256
2517
|
className="vi-comfy-hint"
|
|
2257
2518
|
style={{ display: "inline-flex", alignItems: "center", gap: "0.35rem" }}
|
|
2258
|
-
|
|
2519
|
+
title="Render-time continuity refresh is not yet available on the orbit route. The default still joint continues from the previous last frame."
|
|
2259
2520
|
>
|
|
2260
2521
|
<input
|
|
2261
2522
|
type="checkbox"
|
|
2262
|
-
checked={
|
|
2263
|
-
|
|
2523
|
+
checked={false}
|
|
2524
|
+
disabled
|
|
2264
2525
|
/>
|
|
2265
|
-
continuity refresh
|
|
2266
|
-
last frame
|
|
2526
|
+
render-time continuity refresh unavailable; still joints carry the last frame
|
|
2267
2527
|
</label>
|
|
2268
2528
|
{producerNote ? <span className="vi-knob-hint">{producerNote}</span> : null}
|
|
2269
2529
|
{produceError ? (
|
|
@@ -2831,7 +3091,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2831
3091
|
min={0}
|
|
2832
3092
|
value={g.seed}
|
|
2833
3093
|
placeholder="movie seed + index"
|
|
2834
|
-
onChange={(e) => setGoalField(i, { seed: e.target.value })}
|
|
3094
|
+
onChange={(e) => { markGoalManual(g.key, "seed"); setGoalField(i, { seed: e.target.value }); }}
|
|
2835
3095
|
/>
|
|
2836
3096
|
</div>
|
|
2837
3097
|
<div className="vi-knob" style={{ maxWidth: "16rem" }}>
|
|
@@ -2840,11 +3100,14 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2840
3100
|
id={`vi-movie-goalmodel-${g.key}`}
|
|
2841
3101
|
className="vi-knob-select"
|
|
2842
3102
|
value={g.modelId}
|
|
2843
|
-
onChange={(e) => setGoalField(i, { modelId: e.target.value })}
|
|
3103
|
+
onChange={(e) => { markGoalManual(g.key, "model_id"); setGoalField(i, { modelId: e.target.value }); }}
|
|
2844
3104
|
>
|
|
2845
3105
|
<option value="">
|
|
2846
3106
|
{modelId.trim() ? `Inherit — ${modelId.trim()}` : "Inherit — router picks"}
|
|
2847
3107
|
</option>
|
|
3108
|
+
{g.modelId && !STUDIO_MODEL_IDS.includes(g.modelId) && (
|
|
3109
|
+
<option value={g.modelId}>{g.modelId} · current bound model</option>
|
|
3110
|
+
)}
|
|
2848
3111
|
{movieModels.length > 0 && (
|
|
2849
3112
|
<optgroup label="Bound by a ratified movie segment">
|
|
2850
3113
|
{movieModels.map((id) => (
|
|
@@ -2880,7 +3143,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2880
3143
|
max={100}
|
|
2881
3144
|
value={g.steps}
|
|
2882
3145
|
placeholder={steps.trim() || "inherit"}
|
|
2883
|
-
onChange={(e) => setGoalField(i, { steps: e.target.value })}
|
|
3146
|
+
onChange={(e) => { markGoalManual(g.key, "steps"); setGoalField(i, { steps: e.target.value }); }}
|
|
2884
3147
|
/>
|
|
2885
3148
|
<span className="vi-knob-hint">1–100 · blank = movie/model default</span>
|
|
2886
3149
|
</div>
|
|
@@ -2895,7 +3158,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2895
3158
|
step={0.5}
|
|
2896
3159
|
value={g.cfg}
|
|
2897
3160
|
placeholder={cfg.trim() || "inherit"}
|
|
2898
|
-
onChange={(e) => setGoalField(i, { cfg: e.target.value })}
|
|
3161
|
+
onChange={(e) => { markGoalManual(g.key, "cfg"); setGoalField(i, { cfg: e.target.value }); }}
|
|
2899
3162
|
/>
|
|
2900
3163
|
<span className="vi-knob-hint">0–20 · blank = movie/model default</span>
|
|
2901
3164
|
</div>
|
|
@@ -2906,7 +3169,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2906
3169
|
fps={fps}
|
|
2907
3170
|
fpsFixedReason="Movie-level — one fps per movie; change it in movie settings above."
|
|
2908
3171
|
frames={g.frames}
|
|
2909
|
-
onFrames={(v) => setGoalField(i, { frames: v })}
|
|
3172
|
+
onFrames={(v) => { markGoalManual(g.key, "frames"); setGoalField(i, { frames: v }); }}
|
|
2910
3173
|
framesPlaceholder={framesDefault.trim() || "movie default (81)"}
|
|
2911
3174
|
/>
|
|
2912
3175
|
{Number(g.frames.trim() || framesDefault.trim()) > MODEL_FRAME_CEILING && (
|