@hugpy/video-intelligence-ui 0.3.3 → 0.3.4
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-DMoT8G1q.css +1 -0
- package/dist/assets/index-GeD1mo16.js +4190 -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 +321 -46
- 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
|
|
@@ -1171,13 +1314,17 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1171
1314
|
false,
|
|
1172
1315
|
);
|
|
1173
1316
|
const produceMovie = useCallback(async () => {
|
|
1174
|
-
const p = premise.trim();
|
|
1317
|
+
const p = premise.trim() || movieQuery.trim();
|
|
1175
1318
|
if (!p || producing) return;
|
|
1176
1319
|
setProducing(true);
|
|
1177
1320
|
setProduceError(null);
|
|
1178
1321
|
const res = await request<unknown>(hugpyConfig.producerPlanUrl, {
|
|
1179
1322
|
method: "POST",
|
|
1180
|
-
body: JSON.stringify({
|
|
1323
|
+
body: JSON.stringify({
|
|
1324
|
+
premise: p,
|
|
1325
|
+
model: assist.assistModel || undefined,
|
|
1326
|
+
start_image: startImage?.uri,
|
|
1327
|
+
}),
|
|
1181
1328
|
headers: { "Content-Type": "application/json" },
|
|
1182
1329
|
meta: { specKey: "studio", operation: "studio.producer.plan" },
|
|
1183
1330
|
});
|
|
@@ -1197,23 +1344,34 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1197
1344
|
const joint: JointMode =
|
|
1198
1345
|
i === 0
|
|
1199
1346
|
? "still"
|
|
1200
|
-
: s.joint === "cut" || s.joint === "still"
|
|
1347
|
+
: s.joint === "cut" || s.joint === "still"
|
|
1201
1348
|
? (s.joint as JointMode)
|
|
1202
|
-
: "
|
|
1349
|
+
: "still";
|
|
1203
1350
|
return {
|
|
1204
1351
|
...blankGoal(joint),
|
|
1205
1352
|
prompt: s.prompt,
|
|
1206
1353
|
negative: s.negative ?? "",
|
|
1207
|
-
|
|
1354
|
+
modelId: s.settings?.model_id ?? "",
|
|
1355
|
+
frames: String(s.settings?.frames ?? Math.max(1, Math.round(s.seconds * effFps))),
|
|
1356
|
+
steps: s.settings?.steps == null ? "" : String(s.settings.steps),
|
|
1357
|
+
cfg: s.settings?.cfg == null ? "" : String(s.settings.cfg),
|
|
1358
|
+
seed: s.settings?.seed == null ? "" : String(s.settings.seed),
|
|
1208
1359
|
};
|
|
1209
1360
|
});
|
|
1210
1361
|
if (goals.some((g) => g.prompt.trim() !== "")) {
|
|
1211
1362
|
setGoalStash((st) => [...goals, ...st]);
|
|
1212
1363
|
}
|
|
1213
1364
|
setGoals(rows);
|
|
1365
|
+
const first = plan.segments[0]?.settings;
|
|
1366
|
+
if (first) {
|
|
1367
|
+
if (first.width != null && !manualSettings.current.includes("width")) setWidth(first.width);
|
|
1368
|
+
if (first.height != null && !manualSettings.current.includes("height")) setHeight(first.height);
|
|
1369
|
+
if (first.fps != null && !manualSettings.current.includes("fps")) setFps(first.fps);
|
|
1370
|
+
}
|
|
1214
1371
|
setProducerNote([plan.title, plan.logline].filter(Boolean).join(" — "));
|
|
1215
|
-
setContinuityRefresh(
|
|
1216
|
-
}, [premise, producing, goals, fps
|
|
1372
|
+
setContinuityRefresh(false);
|
|
1373
|
+
}, [premise, movieQuery, producing, goals, fps, assist.assistModel, startImage,
|
|
1374
|
+
setWidth, setHeight, setFps, setContinuityRefresh]);
|
|
1217
1375
|
|
|
1218
1376
|
const setSegmentCount = useCallback(
|
|
1219
1377
|
(n: number) => {
|
|
@@ -1328,6 +1486,42 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1328
1486
|
// name this ref minted may be replaced by the next Generate (operator
|
|
1329
1487
|
// 2026-08-13: "purely a product of the same generation method").
|
|
1330
1488
|
const autoTitleRef = useRef("");
|
|
1489
|
+
const planGoalPrompt = useCallback(async (
|
|
1490
|
+
i: number, text: string,
|
|
1491
|
+
coordinated?: { seed?: number; frames?: number; joint_mode?: string },
|
|
1492
|
+
) => {
|
|
1493
|
+
if (settingsMode !== "oracle") return;
|
|
1494
|
+
const g = goals[i];
|
|
1495
|
+
if (!g) return;
|
|
1496
|
+
const capability = i === 0
|
|
1497
|
+
? (startImage ? "i2v" : "t2v")
|
|
1498
|
+
: ((coordinated?.joint_mode || g.joint) === "cut" ? "t2v" : "i2v");
|
|
1499
|
+
const explicit = [...new Set([
|
|
1500
|
+
...manualSettings.current,
|
|
1501
|
+
...(goalManualSettings.current[g.key] || []),
|
|
1502
|
+
...(coordinated?.seed != null ? ["seed"] : []),
|
|
1503
|
+
...(coordinated?.frames != null ? ["frames"] : []),
|
|
1504
|
+
])];
|
|
1505
|
+
const planned = await planStudioSettings(capability, text, {
|
|
1506
|
+
model_id: g.modelId || undefined,
|
|
1507
|
+
width, height, fps,
|
|
1508
|
+
frames: coordinated?.frames ?? (g.frames ? Number(g.frames) : framesDefault ? Number(framesDefault) : undefined),
|
|
1509
|
+
steps: g.steps ? Number(g.steps) : steps ? Number(steps) : undefined,
|
|
1510
|
+
cfg: g.cfg ? Number(g.cfg) : cfg ? Number(cfg) : undefined,
|
|
1511
|
+
seed: coordinated?.seed ?? (g.seed ? Number(g.seed) : seed),
|
|
1512
|
+
}, explicit);
|
|
1513
|
+
setGoalField(i, {
|
|
1514
|
+
modelId: planned.model_id || "",
|
|
1515
|
+
frames: planned.frames == null ? g.frames : String(planned.frames),
|
|
1516
|
+
steps: planned.steps == null ? g.steps : String(planned.steps),
|
|
1517
|
+
cfg: planned.cfg == null ? g.cfg : String(planned.cfg),
|
|
1518
|
+
seed: planned.seed == null ? g.seed : String(planned.seed),
|
|
1519
|
+
});
|
|
1520
|
+
if (planned.width != null) setWidth(planned.width);
|
|
1521
|
+
if (planned.height != null) setHeight(planned.height);
|
|
1522
|
+
if (planned.fps != null) setFps(planned.fps);
|
|
1523
|
+
}, [settingsMode, goals, startImage, width, height, fps, framesDefault, steps, cfg,
|
|
1524
|
+
seed, setGoalField, setWidth, setHeight, setFps]);
|
|
1331
1525
|
|
|
1332
1526
|
const runRowAssist = useCallback(
|
|
1333
1527
|
(i: number, mode: "detail" | "generate") => {
|
|
@@ -1335,10 +1529,14 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1335
1529
|
if (!g) return;
|
|
1336
1530
|
void (async () => {
|
|
1337
1531
|
let fresh = "";
|
|
1338
|
-
await assist.runAssist(mode, g.prompt, (p) => {
|
|
1532
|
+
await assist.runAssist(mode, mode === "generate" ? "" : g.prompt, (p) => {
|
|
1339
1533
|
fresh = p;
|
|
1340
1534
|
setGoalField(i, { prompt: p });
|
|
1341
1535
|
}, { context: rowContext(i) });
|
|
1536
|
+
if (fresh.trim() && settingsMode === "oracle") {
|
|
1537
|
+
try { await planGoalPrompt(i, fresh); }
|
|
1538
|
+
catch (err) { setFormMsg(err instanceof Error ? err.message : "Oracle planning failed."); }
|
|
1539
|
+
}
|
|
1342
1540
|
if (mode !== "generate" || !fresh.trim()) return;
|
|
1343
1541
|
// NEGATIVE RIDES ALONG (operator 2026-08-13): the generated exclusion
|
|
1344
1542
|
// list APPENDS to this goal's typed negative, never clobbers it
|
|
@@ -1371,7 +1569,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1371
1569
|
intentText.current[g.key] = "";
|
|
1372
1570
|
setIntents((prev) => ({ ...prev, [g.key]: null }));
|
|
1373
1571
|
},
|
|
1374
|
-
[assist, goals, rowContext, setGoalField, movieTitle, setMovieTitle
|
|
1572
|
+
[assist, goals, rowContext, setGoalField, movieTitle, setMovieTitle,
|
|
1573
|
+
settingsMode, planGoalPrompt],
|
|
1375
1574
|
);
|
|
1376
1575
|
|
|
1377
1576
|
// ── negative generation (§1b) — an exclusion list for THIS shot ──
|
|
@@ -1444,6 +1643,25 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1444
1643
|
};
|
|
1445
1644
|
}),
|
|
1446
1645
|
);
|
|
1646
|
+
let oracleNotice: string | null = null;
|
|
1647
|
+
if (settingsMode === "oracle") {
|
|
1648
|
+
for (const key of applied) {
|
|
1649
|
+
const index = goals.findIndex((g) => g.key === key);
|
|
1650
|
+
const hit = bySeg.get(key);
|
|
1651
|
+
if (index < 0 || !hit) continue;
|
|
1652
|
+
try {
|
|
1653
|
+
const k = hit.knobs || {};
|
|
1654
|
+
await planGoalPrompt(index, hit.prompt, {
|
|
1655
|
+
seed: typeof k.seed === "number" ? k.seed : undefined,
|
|
1656
|
+
frames: typeof k.frames === "number" ? k.frames : undefined,
|
|
1657
|
+
joint_mode: typeof k.joint_mode === "string" ? k.joint_mode : undefined,
|
|
1658
|
+
});
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
oracleNotice = err instanceof Error ? err.message : "Oracle planning failed.";
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1447
1665
|
for (const key of applied) {
|
|
1448
1666
|
intentText.current[key] = "";
|
|
1449
1667
|
}
|
|
@@ -1458,7 +1676,10 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1458
1676
|
// this one.
|
|
1459
1677
|
setCoordination(readCoordinationReport(res.coordination));
|
|
1460
1678
|
setCoordSettled(new Set());
|
|
1461
|
-
setSpreadNotice(
|
|
1679
|
+
setSpreadNotice([
|
|
1680
|
+
...spreadNoticeLines(res, applied),
|
|
1681
|
+
...(oracleNotice ? [`Oracle settings: ${oracleNotice}`] : []),
|
|
1682
|
+
]);
|
|
1462
1683
|
}, [
|
|
1463
1684
|
assist,
|
|
1464
1685
|
goalInputs,
|
|
@@ -1470,6 +1691,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1470
1691
|
selectedKeys,
|
|
1471
1692
|
selectedProfile,
|
|
1472
1693
|
steeringSeed,
|
|
1694
|
+
settingsMode,
|
|
1695
|
+
planGoalPrompt,
|
|
1473
1696
|
]);
|
|
1474
1697
|
|
|
1475
1698
|
// ── GROUP ACTION: negatives for the selected rows ──
|
|
@@ -1655,6 +1878,8 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1655
1878
|
});
|
|
1656
1879
|
|
|
1657
1880
|
const body: Record<string, unknown> = {
|
|
1881
|
+
settings_mode: settingsMode,
|
|
1882
|
+
explicit_settings: manualSettings.current,
|
|
1658
1883
|
resolution: { width, height, fps },
|
|
1659
1884
|
frames: framesDefNum,
|
|
1660
1885
|
seed,
|
|
@@ -1690,7 +1915,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1690
1915
|
continuity_refresh: continuityRefresh || undefined,
|
|
1691
1916
|
};
|
|
1692
1917
|
return { body };
|
|
1693
|
-
}, [goals, width, height, fps, seed, vramBudget, modelId, steps, cfg, negative, project, movieTitle, startImage, references, selectedProfile, continuityRefresh]);
|
|
1918
|
+
}, [goals, width, height, fps, seed, vramBudget, modelId, steps, cfg, negative, project, movieTitle, startImage, references, selectedProfile, continuityRefresh, settingsMode]);
|
|
1694
1919
|
|
|
1695
1920
|
// NAME NUDGE (k91) — set when a submit was held back only because the movie has no
|
|
1696
1921
|
// name. It is a warning, not a gate: the run row then offers "Generate without a
|
|
@@ -1701,7 +1926,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1701
1926
|
const [warnUnnamed, setWarnUnnamed] = useWarnUnnamedPref("cinema");
|
|
1702
1927
|
|
|
1703
1928
|
const onGenerate = useCallback(
|
|
1704
|
-
(skipNameCheck = false, titleOverride?: string) => {
|
|
1929
|
+
async (skipNameCheck = false, titleOverride?: string) => {
|
|
1705
1930
|
const built = buildBody(titleOverride);
|
|
1706
1931
|
if ("err" in built) {
|
|
1707
1932
|
setFormMsg(built.err);
|
|
@@ -1718,9 +1943,36 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1718
1943
|
setNameWarn(false);
|
|
1719
1944
|
setFormMsg(null);
|
|
1720
1945
|
if (titleOverride && titleOverride.trim()) setMovieTitle(titleOverride.trim());
|
|
1721
|
-
|
|
1946
|
+
let body = built.body;
|
|
1947
|
+
if (settingsMode === "oracle") {
|
|
1948
|
+
try {
|
|
1949
|
+
body = await planStudioMovie(body);
|
|
1950
|
+
const resolution = body.resolution as { width?: number; height?: number; fps?: number } | undefined;
|
|
1951
|
+
if (resolution?.width != null) setWidth(resolution.width);
|
|
1952
|
+
if (resolution?.height != null) setHeight(resolution.height);
|
|
1953
|
+
if (resolution?.fps != null) setFps(resolution.fps);
|
|
1954
|
+
const plannedGoals = body.goals as Array<Record<string, unknown>> | undefined;
|
|
1955
|
+
if (plannedGoals) setGoals((prev) => prev.map((g, i) => {
|
|
1956
|
+
const p = plannedGoals[i];
|
|
1957
|
+
if (!p) return g;
|
|
1958
|
+
return {
|
|
1959
|
+
...g,
|
|
1960
|
+
modelId: typeof p.model_id === "string" ? p.model_id : g.modelId,
|
|
1961
|
+
steps: typeof p.steps === "number" ? String(p.steps) : g.steps,
|
|
1962
|
+
cfg: typeof p.cfg === "number" ? String(p.cfg) : g.cfg,
|
|
1963
|
+
seed: typeof p.seed === "number" ? String(p.seed) : g.seed,
|
|
1964
|
+
frames: typeof p.frames === "number" ? String(p.frames) : g.frames,
|
|
1965
|
+
};
|
|
1966
|
+
}));
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
setFormMsg(err instanceof Error ? err.message : "Oracle planning failed.");
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
job.run(body);
|
|
1722
1973
|
},
|
|
1723
|
-
[buildBody, job, movieTitle, setMovieTitle, warnUnnamed
|
|
1974
|
+
[buildBody, job, movieTitle, setMovieTitle, warnUnnamed, settingsMode,
|
|
1975
|
+
setWidth, setHeight, setFps, setGoals],
|
|
1724
1976
|
);
|
|
1725
1977
|
|
|
1726
1978
|
// SESSION LIBRARY PUSH (operator 2026-08-13): a finished movie's outputs
|
|
@@ -1920,9 +2172,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1920
2172
|
className="vi-knob-select"
|
|
1921
2173
|
value={modelId}
|
|
1922
2174
|
disabled={disableSubmit}
|
|
1923
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2175
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
1924
2176
|
>
|
|
1925
2177
|
<option value="">Auto — router picks by capability/budget</option>
|
|
2178
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2179
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2180
|
+
)}
|
|
1926
2181
|
{movieModels.length > 0 && (
|
|
1927
2182
|
<optgroup label="Bound by a ratified movie segment">
|
|
1928
2183
|
{movieModels.map((id) => (
|
|
@@ -1973,7 +2228,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1973
2228
|
value={steps}
|
|
1974
2229
|
placeholder="model default"
|
|
1975
2230
|
disabled={disableSubmit}
|
|
1976
|
-
onChange={(e) => setSteps(e.target.value)}
|
|
2231
|
+
onChange={(e) => { markManual("steps"); setSteps(e.target.value); }}
|
|
1977
2232
|
/>
|
|
1978
2233
|
</div>
|
|
1979
2234
|
<div className="vi-knob">
|
|
@@ -1988,7 +2243,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
1988
2243
|
value={cfg}
|
|
1989
2244
|
placeholder="model default"
|
|
1990
2245
|
disabled={disableSubmit}
|
|
1991
|
-
onChange={(e) => setCfg(e.target.value)}
|
|
2246
|
+
onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }}
|
|
1992
2247
|
/>
|
|
1993
2248
|
</div>
|
|
1994
2249
|
|
|
@@ -2004,7 +2259,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2004
2259
|
step={8}
|
|
2005
2260
|
value={width}
|
|
2006
2261
|
disabled={disableSubmit}
|
|
2007
|
-
onChange={(e) => setWidth(Number(e.target.value) || 0)}
|
|
2262
|
+
onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }}
|
|
2008
2263
|
/>
|
|
2009
2264
|
</div>
|
|
2010
2265
|
<div className="vi-knob">
|
|
@@ -2017,7 +2272,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2017
2272
|
step={8}
|
|
2018
2273
|
value={height}
|
|
2019
2274
|
disabled={disableSubmit}
|
|
2020
|
-
onChange={(e) => setHeight(Number(e.target.value) || 0)}
|
|
2275
|
+
onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }}
|
|
2021
2276
|
/>
|
|
2022
2277
|
</div>
|
|
2023
2278
|
<div className="vi-knob">
|
|
@@ -2035,7 +2290,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2035
2290
|
? `Ratified movie envelope: ${movieGeometry.width}×${movieGeometry.height} @ ${movieGeometry.fps}fps (“${movieGeometry.presetTitle}”).`
|
|
2036
2291
|
: undefined
|
|
2037
2292
|
}
|
|
2038
|
-
onChange={(e) => setFps(Number(e.target.value) || 0)}
|
|
2293
|
+
onChange={(e) => { markManual("fps"); setFps(Number(e.target.value) || 0); }}
|
|
2039
2294
|
/>
|
|
2040
2295
|
{movieGeomOutOfEnvelope && (
|
|
2041
2296
|
<span className="vi-knob-flag">
|
|
@@ -2056,7 +2311,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2056
2311
|
min={0}
|
|
2057
2312
|
value={seed}
|
|
2058
2313
|
disabled={disableSubmit}
|
|
2059
|
-
onChange={(e) => setSeed(Number(e.target.value) || 0)}
|
|
2314
|
+
onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }}
|
|
2060
2315
|
/>
|
|
2061
2316
|
<span className="vi-knob-hint">A segment with no seed uses base seed + its index.</span>
|
|
2062
2317
|
</div>
|
|
@@ -2101,6 +2356,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2101
2356
|
|
|
2102
2357
|
return (
|
|
2103
2358
|
<div className="vi-studio-generate">
|
|
2359
|
+
{clipImportBackup && (
|
|
2360
|
+
<button type="button" className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
2361
|
+
onClick={restorePreviousCinemaDraft}>
|
|
2362
|
+
Restore previous Cinema draft
|
|
2363
|
+
</button>
|
|
2364
|
+
)}
|
|
2104
2365
|
{/* The movie Settings panel lives in the left-sidebar Settings tab (portaled into
|
|
2105
2366
|
settingsHost, like the Clip surface's knobGrid): templates + model pin + sampler
|
|
2106
2367
|
+ geometry/seed/budget/project; null until that panel mounts. */}
|
|
@@ -2147,6 +2408,15 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2147
2408
|
miss (and absent when no host mounts). This expander mirrors the
|
|
2148
2409
|
SAME state — two views, one truth, like the Clip card. ── */}
|
|
2149
2410
|
<PromptCardSettings label="movie settings — model · size · fps · steps · cfg · budget">
|
|
2411
|
+
<div className="vi-knob vi-studio-knob-wide">
|
|
2412
|
+
<label htmlFor="vi-movie-settings-mode">settings</label>
|
|
2413
|
+
<select id="vi-movie-settings-mode" className="vi-knob-select"
|
|
2414
|
+
value={settingsMode}
|
|
2415
|
+
onChange={(e) => setSettingsMode(e.target.value as "oracle" | "manual")}>
|
|
2416
|
+
<option value="oracle">Oracle-generated (default)</option>
|
|
2417
|
+
<option value="manual">Manual</option>
|
|
2418
|
+
</select>
|
|
2419
|
+
</div>
|
|
2150
2420
|
<div className="vi-knob vi-studio-knob-wide" style={{ minWidth: "16rem" }}>
|
|
2151
2421
|
<label htmlFor="vi-moviecol-model">model — whole movie</label>
|
|
2152
2422
|
<select
|
|
@@ -2154,9 +2424,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2154
2424
|
className="vi-knob-select"
|
|
2155
2425
|
value={modelId}
|
|
2156
2426
|
disabled={disableSubmit}
|
|
2157
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2427
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2158
2428
|
>
|
|
2159
2429
|
<option value="">Auto — router picks by capability/budget</option>
|
|
2430
|
+
{modelId && !STUDIO_MODEL_IDS.includes(modelId) && (
|
|
2431
|
+
<option value={modelId}>{modelId} · current bound model</option>
|
|
2432
|
+
)}
|
|
2160
2433
|
{movieModels.length > 0 && (
|
|
2161
2434
|
<optgroup label="Bound by a ratified movie segment">
|
|
2162
2435
|
{movieModels.map((id) => (
|
|
@@ -2176,12 +2449,12 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2176
2449
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2177
2450
|
<label htmlFor="vi-moviecol-width">width</label>
|
|
2178
2451
|
<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)} />
|
|
2452
|
+
value={width} onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }} />
|
|
2180
2453
|
</div>
|
|
2181
2454
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2182
2455
|
<label htmlFor="vi-moviecol-height">height</label>
|
|
2183
2456
|
<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)} />
|
|
2457
|
+
value={height} onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }} />
|
|
2185
2458
|
</div>
|
|
2186
2459
|
{/* LENGTH ROW (operator 2026-08-13): fps | frames/segment | seconds —
|
|
2187
2460
|
any two set the third; dimmed = derived, last-touched wins. This is
|
|
@@ -2189,20 +2462,20 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2189
2462
|
<LengthRow
|
|
2190
2463
|
idPrefix="vi-moviecol-len"
|
|
2191
2464
|
fps={fps}
|
|
2192
|
-
onFps={(n) => setFps(n)}
|
|
2465
|
+
onFps={(n) => { markManual("fps"); setFps(n); }}
|
|
2193
2466
|
frames={framesDefault}
|
|
2194
|
-
onFrames={setFramesDefault}
|
|
2467
|
+
onFrames={(v) => { markManual("frames"); setFramesDefault(v); }}
|
|
2195
2468
|
framesPlaceholder="model default (81) / segment"
|
|
2196
2469
|
/>
|
|
2197
2470
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2198
2471
|
<label htmlFor="vi-moviecol-steps">steps</label>
|
|
2199
2472
|
<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)} />
|
|
2473
|
+
value={steps} placeholder="model default" onChange={(e) => { markManual("steps"); setSteps(e.target.value); }} />
|
|
2201
2474
|
</div>
|
|
2202
2475
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2203
2476
|
<label htmlFor="vi-moviecol-cfg">cfg</label>
|
|
2204
2477
|
<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)} />
|
|
2478
|
+
value={cfg} placeholder="model default" onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }} />
|
|
2206
2479
|
</div>
|
|
2207
2480
|
<div className="vi-knob" style={{ maxWidth: "9rem" }}>
|
|
2208
2481
|
<label htmlFor="vi-moviecol-vram">VRAM budget GB</label>
|
|
@@ -2237,7 +2510,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2237
2510
|
className="vi-knob-input"
|
|
2238
2511
|
rows={2}
|
|
2239
2512
|
style={{ flex: "1 1 24rem", resize: "vertical" }}
|
|
2240
|
-
placeholder="
|
|
2513
|
+
placeholder="Describe the whole production — Oracle creates the needed sequential prompt cards and settings"
|
|
2241
2514
|
value={premise}
|
|
2242
2515
|
onChange={(e) => setPremise(e.target.value)}
|
|
2243
2516
|
disabled={producing}
|
|
@@ -2245,25 +2518,24 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2245
2518
|
<button
|
|
2246
2519
|
type="button"
|
|
2247
2520
|
className="vi-btn vi-btn-accent"
|
|
2248
|
-
disabled={producing || premise.trim() === ""}
|
|
2521
|
+
disabled={producing || (premise.trim() === "" && movieQuery.trim() === "")}
|
|
2249
2522
|
onClick={() => void produceMovie()}
|
|
2250
2523
|
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
2524
|
>
|
|
2252
|
-
{producing ? "
|
|
2525
|
+
{producing ? "Generating production…" : "✨ Generate production"}
|
|
2253
2526
|
</button>
|
|
2254
2527
|
</div>
|
|
2255
2528
|
<label
|
|
2256
2529
|
className="vi-comfy-hint"
|
|
2257
2530
|
style={{ display: "inline-flex", alignItems: "center", gap: "0.35rem" }}
|
|
2258
|
-
|
|
2531
|
+
title="Render-time continuity refresh is not yet available on the orbit route. The default still joint continues from the previous last frame."
|
|
2259
2532
|
>
|
|
2260
2533
|
<input
|
|
2261
2534
|
type="checkbox"
|
|
2262
|
-
checked={
|
|
2263
|
-
|
|
2535
|
+
checked={false}
|
|
2536
|
+
disabled
|
|
2264
2537
|
/>
|
|
2265
|
-
continuity refresh
|
|
2266
|
-
last frame
|
|
2538
|
+
render-time continuity refresh unavailable; still joints carry the last frame
|
|
2267
2539
|
</label>
|
|
2268
2540
|
{producerNote ? <span className="vi-knob-hint">{producerNote}</span> : null}
|
|
2269
2541
|
{produceError ? (
|
|
@@ -2831,7 +3103,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2831
3103
|
min={0}
|
|
2832
3104
|
value={g.seed}
|
|
2833
3105
|
placeholder="movie seed + index"
|
|
2834
|
-
onChange={(e) => setGoalField(i, { seed: e.target.value })}
|
|
3106
|
+
onChange={(e) => { markGoalManual(g.key, "seed"); setGoalField(i, { seed: e.target.value }); }}
|
|
2835
3107
|
/>
|
|
2836
3108
|
</div>
|
|
2837
3109
|
<div className="vi-knob" style={{ maxWidth: "16rem" }}>
|
|
@@ -2840,11 +3112,14 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2840
3112
|
id={`vi-movie-goalmodel-${g.key}`}
|
|
2841
3113
|
className="vi-knob-select"
|
|
2842
3114
|
value={g.modelId}
|
|
2843
|
-
onChange={(e) => setGoalField(i, { modelId: e.target.value })}
|
|
3115
|
+
onChange={(e) => { markGoalManual(g.key, "model_id"); setGoalField(i, { modelId: e.target.value }); }}
|
|
2844
3116
|
>
|
|
2845
3117
|
<option value="">
|
|
2846
3118
|
{modelId.trim() ? `Inherit — ${modelId.trim()}` : "Inherit — router picks"}
|
|
2847
3119
|
</option>
|
|
3120
|
+
{g.modelId && !STUDIO_MODEL_IDS.includes(g.modelId) && (
|
|
3121
|
+
<option value={g.modelId}>{g.modelId} · current bound model</option>
|
|
3122
|
+
)}
|
|
2848
3123
|
{movieModels.length > 0 && (
|
|
2849
3124
|
<optgroup label="Bound by a ratified movie segment">
|
|
2850
3125
|
{movieModels.map((id) => (
|
|
@@ -2880,7 +3155,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2880
3155
|
max={100}
|
|
2881
3156
|
value={g.steps}
|
|
2882
3157
|
placeholder={steps.trim() || "inherit"}
|
|
2883
|
-
onChange={(e) => setGoalField(i, { steps: e.target.value })}
|
|
3158
|
+
onChange={(e) => { markGoalManual(g.key, "steps"); setGoalField(i, { steps: e.target.value }); }}
|
|
2884
3159
|
/>
|
|
2885
3160
|
<span className="vi-knob-hint">1–100 · blank = movie/model default</span>
|
|
2886
3161
|
</div>
|
|
@@ -2895,7 +3170,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2895
3170
|
step={0.5}
|
|
2896
3171
|
value={g.cfg}
|
|
2897
3172
|
placeholder={cfg.trim() || "inherit"}
|
|
2898
|
-
onChange={(e) => setGoalField(i, { cfg: e.target.value })}
|
|
3173
|
+
onChange={(e) => { markGoalManual(g.key, "cfg"); setGoalField(i, { cfg: e.target.value }); }}
|
|
2899
3174
|
/>
|
|
2900
3175
|
<span className="vi-knob-hint">0–20 · blank = movie/model default</span>
|
|
2901
3176
|
</div>
|
|
@@ -2906,7 +3181,7 @@ export function StudioMovieComposer({ settingsHost }: StudioMovieComposerProps =
|
|
|
2906
3181
|
fps={fps}
|
|
2907
3182
|
fpsFixedReason="Movie-level — one fps per movie; change it in movie settings above."
|
|
2908
3183
|
frames={g.frames}
|
|
2909
|
-
onFrames={(v) => setGoalField(i, { frames: v })}
|
|
3184
|
+
onFrames={(v) => { markGoalManual(g.key, "frames"); setGoalField(i, { frames: v }); }}
|
|
2910
3185
|
framesPlaceholder={framesDefault.trim() || "movie default (81)"}
|
|
2911
3186
|
/>
|
|
2912
3187
|
{Number(g.frames.trim() || framesDefault.trim()) > MODEL_FRAME_CEILING && (
|