@hugpy/video-intelligence-ui 0.3.2 → 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/LICENSE +0 -0
- package/README.md +9 -0
- package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
- package/dist/assets/index-DMoT8G1q.css +1 -0
- package/dist/assets/index-GeD1mo16.js +4190 -0
- package/dist/index.html +71 -0
- package/entry.tsx +6 -27
- package/index.html +4 -4
- package/package.json +44 -5
- package/src/assets/hugpy-mark.png +0 -0
- package/src/config.ts +11 -27
- package/src/demo/DemoBanner.tsx +0 -0
- package/src/demo/demo.css +0 -0
- package/src/demo/demoFetch.ts +0 -9
- package/src/demo/fixtures.ts +0 -0
- package/src/demo/identityFixture.ts +0 -0
- package/src/demo/index.ts +0 -0
- package/src/demo/mediaBase.ts +0 -0
- package/src/demo/mode.ts +0 -0
- package/src/demo/promptAssist.ts +0 -0
- package/src/demo/sampleData.ts +0 -0
- package/src/demo/sampleSets.ts +0 -0
- package/src/demo/seed.ts +0 -0
- package/src/nav/BrandMark.css +0 -0
- package/src/nav/BrandMark.tsx +0 -0
- package/src/nav/Navbar.css +0 -0
- package/src/nav/Navbar.tsx +8 -3
- package/src/regions/SpatialRegionEditor.tsx +0 -0
- package/src/regions/TemporalRegionEditor.tsx +0 -0
- package/src/regions/types.ts +0 -0
- package/src/session.ts +8 -43
- package/src/share.ts +0 -0
- package/src/stations/ActivePanel.tsx +0 -0
- package/src/stations/ActiveProcessesStation.tsx +0 -0
- package/src/stations/AddFileBar.tsx +0 -0
- package/src/stations/AudioCropStation.tsx +0 -0
- package/src/stations/ComfyPanel.tsx +0 -0
- package/src/stations/ComfyStation.tsx +0 -0
- package/src/stations/CondensedKnobStrip.tsx +0 -0
- package/src/stations/ConsolePanel.tsx +0 -0
- package/src/stations/FrameExtractStation.tsx +0 -0
- package/src/stations/GenIdentityBar.tsx +0 -0
- package/src/stations/GenerateStation.tsx +10 -6
- package/src/stations/GoalComposer.tsx +0 -0
- package/src/stations/ImageCropStation.tsx +0 -0
- package/src/stations/KnobInfo.tsx +0 -0
- package/src/stations/PlaceholderStation.tsx +0 -0
- package/src/stations/ScriptFirstStation.tsx +4 -0
- package/src/stations/SectionTabs.tsx +0 -0
- package/src/stations/SharePanel.tsx +4 -4
- package/src/stations/SidebarRegistry.tsx +0 -0
- package/src/stations/StationShell.tsx +0 -0
- package/src/stations/StudioClipsStation.tsx +0 -0
- package/src/stations/TemplateStepsPanel.tsx +190 -0
- package/src/stations/ToksPanel.tsx +1 -2
- package/src/stations/WorkbenchDrawer.tsx +0 -0
- package/src/stations/WorkbenchSidebar.tsx +8 -261
- package/src/stations/WorkbenchStation.tsx +0 -0
- package/src/stations/comfyEmbed.ts +0 -0
- package/src/stations/drawerShell.tsx +0 -0
- package/src/stations/ops/AudioCropCore.tsx +0 -0
- package/src/stations/ops/CharacterGroupsPanel.tsx +75 -83
- package/src/stations/ops/FrameExtractCore.tsx +0 -0
- package/src/stations/ops/ImageCropCore.tsx +0 -0
- package/src/stations/ops/VideoCropCore.tsx +0 -0
- package/src/stations/ops/useVideoCropJobs.ts +0 -0
- package/src/stations/oracle/DagPanel.tsx +0 -0
- package/src/stations/oracle/OracleStation.tsx +2 -0
- package/src/stations/oracle/PerformanceComposer.tsx +119 -0
- package/src/stations/oracle/SpatialTab.tsx +0 -0
- package/src/stations/oracle/StewardPanel.tsx +0 -0
- package/src/stations/oracle/oracle.css +33 -0
- package/src/stations/oracle/oracleShared.tsx +0 -0
- package/src/stations/oracle/oracleTypes.ts +0 -0
- package/src/stations/oracle/useOracle.ts +0 -0
- package/src/stations/pollCaps.ts +0 -0
- package/src/stations/registry.ts +0 -0
- package/src/stations/scriptFirst.css +23 -0
- package/src/stations/studio/AngleRingEditor.tsx +0 -0
- package/src/stations/studio/IdentitiesStation.tsx +86 -76
- package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
- package/src/stations/studio/IdentityProfileControls.tsx +0 -0
- package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
- package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
- package/src/stations/studio/StudioGenerateMode.tsx +3 -2
- package/src/stations/studio/StudioGenerateTab.tsx +278 -190
- package/src/stations/studio/StudioItemCard.tsx +0 -0
- package/src/stations/studio/StudioLibraryTab.tsx +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx +321 -46
- package/src/stations/studio/StudioMovieSessions.tsx +12 -19
- package/src/stations/studio/StudioPlane.tsx +28 -5
- package/src/stations/studio/StudioSplicedRow.tsx +0 -0
- package/src/stations/studio/StudioViewer.tsx +6 -11
- package/src/stations/studio/TurntableViewer.tsx +0 -0
- package/src/stations/studio/coordinationBadge.tsx +0 -0
- package/src/stations/studio/identityUpload.ts +0 -0
- package/src/stations/studio/movieTimeline.ts +0 -0
- package/src/stations/studio/promptCard.tsx +0 -0
- package/src/stations/studio/studioItem.ts +0 -0
- package/src/stations/studio/studioShared.tsx +14 -2
- package/src/stations/studio/useIdentityCreateJob.ts +26 -21
- package/src/stations/studio/useIdentityProfiles.ts +0 -0
- package/src/stations/studio/useReconstruction.ts +11 -5
- package/src/stations/types.ts +0 -0
- package/src/stations/useAudioCropJobs.ts +0 -0
- package/src/stations/useAudioExtractJob.ts +0 -0
- package/src/stations/useCropJobs.ts +0 -0
- package/src/stations/useFrameJobs.ts +0 -0
- package/src/stations/useGenerateJob.ts +0 -0
- package/src/stations/useGenerateMovieJob.ts +0 -0
- package/src/stations/useGenerateRowJobs.ts +0 -0
- package/src/stations/useGenerateSceneJob.ts +0 -0
- package/src/stations/useGenerateSceneRowJobs.ts +0 -0
- package/src/stations/useScriptFirstRun.ts +0 -0
- package/src/style/app.css +49 -0
- package/src/transport/client.ts +0 -0
- package/src/transport/telemetry.ts +0 -0
- package/src/video/DropReceptacle.tsx +0 -0
- package/src/video/LengthRow.tsx +0 -0
- package/src/video/LibraryGroups.tsx +0 -0
- package/src/video/PromptAssistButtons.tsx +5 -4
- package/src/video/UnnamedWarn.tsx +0 -0
- package/src/video/composerBridge.ts +0 -0
- package/src/video/contract.ts +0 -0
- package/src/video/coordinationReport.ts +0 -0
- package/src/video/jobClock.ts +0 -0
- package/src/video/jobStream.ts +119 -0
- package/src/video/jobTracker.ts +67 -76
- package/src/video/mediaLibrary.ts +0 -0
- package/src/video/modelsFeed.ts +105 -0
- package/src/video/renderCompat.ts +0 -0
- package/src/video/sessionForm.ts +0 -0
- package/src/video/spreadAssist.ts +0 -0
- package/src/video/studioBridge.ts +0 -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/useMoviePresets.ts +0 -0
- package/src/video/useMovieSessions.ts +15 -6
- package/src/video/useOperatorHistory.ts +11 -101
- package/src/video/usePresets.ts +0 -0
- package/src/video/useProjects.ts +0 -0
- package/src/video/usePromptAssist.ts +70 -25
- package/src/video/useRenderPresets.ts +0 -0
- package/src/video/useStudioAssistLog.ts +0 -0
- package/src/video/useStudioPresets.ts +0 -0
- package/src/video/useToks.ts +14 -6
- package/src/video/watchVideoJob.ts +43 -0
- package/tsconfig.json +0 -0
- package/vite.config.ts +0 -0
- package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
- 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.
|
|
@@ -416,6 +437,11 @@ export function StudioGenerateSurface({
|
|
|
416
437
|
height?: number;
|
|
417
438
|
fps?: number;
|
|
418
439
|
seed?: number;
|
|
440
|
+
startImage?: string;
|
|
441
|
+
steps?: number;
|
|
442
|
+
cfg?: number;
|
|
443
|
+
requestedFrames?: number;
|
|
444
|
+
negative?: string;
|
|
419
445
|
}) => {
|
|
420
446
|
const p = prompt.trim();
|
|
421
447
|
if (!p) {
|
|
@@ -430,6 +456,11 @@ export function StudioGenerateSurface({
|
|
|
430
456
|
if (opts.height != null) body.height = opts.height;
|
|
431
457
|
if (opts.fps != null) body.fps = opts.fps;
|
|
432
458
|
if (opts.seed != null) body.seed = opts.seed;
|
|
459
|
+
if (opts.startImage) body.start_image = opts.startImage;
|
|
460
|
+
if (opts.steps != null) body.steps = opts.steps;
|
|
461
|
+
if (opts.cfg != null) body.cfg = opts.cfg;
|
|
462
|
+
if (opts.requestedFrames != null) body.requested_frames = opts.requestedFrames;
|
|
463
|
+
if (opts.negative) body.negative = opts.negative;
|
|
433
464
|
if (opts.models.length > 0) body.models = opts.models;
|
|
434
465
|
const res = await request<unknown>(hugpyConfig.studioTesterUrl, {
|
|
435
466
|
method: "POST",
|
|
@@ -476,6 +507,27 @@ export function StudioGenerateSurface({
|
|
|
476
507
|
const isV2V = capability === "v2v";
|
|
477
508
|
const isIdLock = capability === "id_lock";
|
|
478
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
|
+
};
|
|
479
531
|
|
|
480
532
|
// ── the fleet's measured render table + everything derived from it ───────────────
|
|
481
533
|
// One session-cached GET shared with the Movie composer (module singleton in the
|
|
@@ -928,19 +980,16 @@ export function StudioGenerateSurface({
|
|
|
928
980
|
|
|
929
981
|
// ── required-input gate + preformed SHAPE ──
|
|
930
982
|
// v2v: a source clip is REQUIRED (nothing to repaint otherwise) — always the hero.
|
|
931
|
-
// i2v
|
|
932
|
-
// 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.
|
|
933
984
|
// id_lock: >=1 REFERENCE image is required (the route 400s otherwise) — reference
|
|
934
985
|
// slots are the hero; the composition control is optional + both-or-neither.
|
|
935
986
|
// t2v / synthetic i2v / custom: no required input; inputs stay inline + optional.
|
|
936
|
-
const presetNeedsInput = preset?.requires_source === true;
|
|
937
987
|
const hasImage = startImages.length > 0;
|
|
938
988
|
const hasSource = !!source;
|
|
939
989
|
const hasReference = referenceImages.length > 0;
|
|
940
990
|
const canAddImage = startImages.length < MAX_START_IMAGES;
|
|
941
991
|
const canAddReference = referenceImages.length < MAX_REFERENCE_IMAGES;
|
|
942
992
|
const needSource = isV2V;
|
|
943
|
-
const needImageOrSource = isI2V && presetNeedsInput;
|
|
944
993
|
const needReference = isIdLock;
|
|
945
994
|
// MOTION requires a control still and it IS the capability — the exact mirror of
|
|
946
995
|
// id_lock's reference rule (video_routes: "a motion request with no control carries NO
|
|
@@ -956,40 +1005,26 @@ export function StudioGenerateSurface({
|
|
|
956
1005
|
const controlAndSource = isMotion && hasControl && hasSource;
|
|
957
1006
|
const missingRequired =
|
|
958
1007
|
(needSource && !hasSource) ||
|
|
959
|
-
(needImageOrSource && !hasImage && !hasSource) ||
|
|
960
1008
|
(needReference && !hasReference) ||
|
|
961
1009
|
(needControl && !hasControl) ||
|
|
962
1010
|
controlAndSource ||
|
|
963
1011
|
controlHalfSet;
|
|
964
1012
|
|
|
965
|
-
const hero: "
|
|
1013
|
+
const hero: "source" | "reference" | "control" | null = isMotion
|
|
966
1014
|
? "control"
|
|
967
1015
|
: isIdLock
|
|
968
1016
|
? "reference"
|
|
969
1017
|
: isV2V
|
|
970
1018
|
? "source"
|
|
971
|
-
:
|
|
972
|
-
? "start-image"
|
|
973
|
-
: null;
|
|
974
|
-
const startImageAttention = needImageOrSource && !hasImage && !hasSource;
|
|
1019
|
+
: null;
|
|
975
1020
|
const sourceAttention =
|
|
976
|
-
|
|
1021
|
+
needSource && !hasSource;
|
|
977
1022
|
const referenceAttention = needReference && !hasReference;
|
|
978
1023
|
|
|
979
1024
|
// ── Composer attach idiom (operator ask 2026-07-11, option A) ──
|
|
980
|
-
// The prompt
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
// removable chips) and wire its callbacks to THIS file's OWN pickers / MediaRef flows,
|
|
984
|
-
// offering only the inputs the ACTIVE shape accepts (mirroring the render* predicates
|
|
985
|
-
// above). The buttons open the SAME `showLibPicker` / `showSourcePicker` the hero uses,
|
|
986
|
-
// so the library pick routes to start (i2v) or reference (v2v / id_lock) exactly as the
|
|
987
|
-
// hero's own lib-picker routing does; the chips reflect the SAME startImages / source /
|
|
988
|
-
// referenceImages / controlImage state the hero renders, and reuse the hero's existing
|
|
989
|
-
// removes. Applicability decides whether a button EXISTS (t2v gets no toolbar — omitted
|
|
990
|
-
// cleanly by GoalComposer when the array is empty); capacity decides whether it is
|
|
991
|
-
// DISABLED (a filled slot mirrors the hero's "you're at the cap" note, not a vanished
|
|
992
|
-
// 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.
|
|
993
1028
|
//
|
|
994
1029
|
// 2026-07-29: these three are no longer hand-written capability unions — they are the
|
|
995
1030
|
// route's own input gates, transcribed once in renderCompat's ACCEPTS matrix and read
|
|
@@ -1093,7 +1128,7 @@ export function StudioGenerateSurface({
|
|
|
1093
1128
|
? "Composition control is both-or-neither — add a control image AND a kind, or clear both."
|
|
1094
1129
|
: needSource && !hasSource
|
|
1095
1130
|
? "Restyle (v2v) needs a source clip — add one above."
|
|
1096
|
-
: "
|
|
1131
|
+
: "Add the required input above.";
|
|
1097
1132
|
|
|
1098
1133
|
// v2v stays a HARD block (the router refuses an out-of-envelope restyle outright). The
|
|
1099
1134
|
// envelope itself is now the ratified preset's rather than a mirrored constant.
|
|
@@ -1180,14 +1215,18 @@ export function StudioGenerateSurface({
|
|
|
1180
1215
|
// on motion (required, definitional) — `slots.controlImage` is that gate.
|
|
1181
1216
|
const controlReady = (isIdLock || isMotion) && !!controlImage && controlKind !== "";
|
|
1182
1217
|
const body: Record<string, unknown> = {
|
|
1183
|
-
capability,
|
|
1218
|
+
capability: clipRoute,
|
|
1219
|
+
settings_mode: settingsMode,
|
|
1220
|
+
explicit_settings: manualSettings.current,
|
|
1184
1221
|
resolution: { width, height, fps },
|
|
1185
1222
|
seed,
|
|
1186
1223
|
vram_budget_gb: budgetNum,
|
|
1187
1224
|
steps: stepsNum,
|
|
1188
1225
|
cfg: cfgNum,
|
|
1189
1226
|
requested_frames: requestedFrames.trim() !== "" ? Number(requestedFrames) : undefined,
|
|
1190
|
-
|
|
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,
|
|
1191
1230
|
// Optional auto-archive NAME (choose-or-type). Blank = auto-named (default).
|
|
1192
1231
|
project: project.trim() ? project.trim() : undefined,
|
|
1193
1232
|
prompt: prompt.trim() ? prompt : undefined,
|
|
@@ -1220,6 +1259,15 @@ export function StudioGenerateSurface({
|
|
|
1220
1259
|
control_image: controlReady ? (controlImage as MediaRef).uri : undefined,
|
|
1221
1260
|
control_kind: controlReady ? controlKind : undefined,
|
|
1222
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
|
+
}
|
|
1223
1271
|
const res = await request<unknown>(hugpyConfig.studioI2VEnqueueUrl, {
|
|
1224
1272
|
method: "POST",
|
|
1225
1273
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1231,28 +1279,34 @@ export function StudioGenerateSurface({
|
|
|
1231
1279
|
setGenMsg(`Enqueue failed: ${describeAppError(errorOf(res))}`);
|
|
1232
1280
|
return;
|
|
1233
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);
|
|
1234
1295
|
// Subject-clip auto-stage (armed by onGenerateNewSubject): the FIRST successful
|
|
1235
1296
|
// enqueue after "Generate new clip" arms the wait on ITS job id — later manual
|
|
1236
1297
|
// generates (pendingSubjectStage already false by then) are plain renders.
|
|
1237
1298
|
if (pendingSubjectStage) {
|
|
1238
1299
|
setPendingSubjectStage(false);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
setAwaitingSubjectJobId(enq.data.job_id);
|
|
1242
|
-
setGenMsg(
|
|
1243
|
-
"Enqueued — once this clip is produced it becomes the staged subject automatically.",
|
|
1244
|
-
);
|
|
1245
|
-
} else {
|
|
1246
|
-
setGenMsg(
|
|
1247
|
-
"Enqueued, but no job id came back to track — stage the produced clip as the subject manually (From library) once it's done.",
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1300
|
+
setAwaitingSubjectJobId(enq.data.job_id);
|
|
1301
|
+
setGenMsg("Enqueued — once this clip is produced it becomes the staged subject automatically.");
|
|
1250
1302
|
} else {
|
|
1251
|
-
setGenMsg(
|
|
1303
|
+
setGenMsg(`${routedCapability === "i2v" ? "Image-to-video" : "Text-to-video"} queued — follow the job below or in Active Processes.`);
|
|
1252
1304
|
}
|
|
1253
1305
|
onEnqueued();
|
|
1254
1306
|
// A project name coined here should appear in the combobox next time.
|
|
1255
1307
|
if (project.trim()) refreshProjects();
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
setGenMsg(err instanceof Error ? err.message : "Oracle planning failed.");
|
|
1256
1310
|
} finally {
|
|
1257
1311
|
if (mounted.current) setGenBusy(false);
|
|
1258
1312
|
}
|
|
@@ -1261,6 +1315,7 @@ export function StudioGenerateSurface({
|
|
|
1261
1315
|
v2vBadGeom,
|
|
1262
1316
|
missingReason,
|
|
1263
1317
|
capability,
|
|
1318
|
+
clipRoute,
|
|
1264
1319
|
width,
|
|
1265
1320
|
height,
|
|
1266
1321
|
fps,
|
|
@@ -1285,7 +1340,72 @@ export function StudioGenerateSurface({
|
|
|
1285
1340
|
pendingSubjectStage,
|
|
1286
1341
|
onEnqueued,
|
|
1287
1342
|
project,
|
|
1288
|
-
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
|
+
}
|
|
1289
1409
|
|
|
1290
1410
|
const synthPresets = presets.filter((p) => isSyntheticPreset(p));
|
|
1291
1411
|
const realPresets = presets.filter((p) => !isSyntheticPreset(p));
|
|
@@ -1348,106 +1468,6 @@ export function StudioGenerateSurface({
|
|
|
1348
1468
|
);
|
|
1349
1469
|
}
|
|
1350
1470
|
|
|
1351
|
-
// ── start-image input (variant-driven: hero = dominant/accented; inline = subdued) ──
|
|
1352
|
-
function renderStartImage(variant: "hero" | "inline") {
|
|
1353
|
-
const isHero = variant === "hero";
|
|
1354
|
-
const drawEye = isHero && startImageAttention;
|
|
1355
|
-
const thumb = isHero ? "9rem" : "5rem";
|
|
1356
|
-
return (
|
|
1357
|
-
<section aria-label="Start image" style={{ marginTop: isHero ? 0 : "0.9rem" }}>
|
|
1358
|
-
<p
|
|
1359
|
-
className="vi-comfy-label"
|
|
1360
|
-
style={{ marginBottom: "0.35rem", opacity: isHero ? 1 : 0.75 }}
|
|
1361
|
-
>
|
|
1362
|
-
Start image{isHero ? " — required for this generator" : " (optional)"}
|
|
1363
|
-
</p>
|
|
1364
|
-
{drawEye && (
|
|
1365
|
-
<p className="vi-input-callout" role="note">
|
|
1366
|
-
This generator needs a start image — the subject is carried from it. Drop one below
|
|
1367
|
-
(or extend a source clip instead).
|
|
1368
|
-
</p>
|
|
1369
|
-
)}
|
|
1370
|
-
{hasImage && (
|
|
1371
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", marginBottom: "0.5rem" }}>
|
|
1372
|
-
{startImages.map((img, idx) => (
|
|
1373
|
-
<div key={img.uri} style={{ display: "flex", flexDirection: "column", gap: "0.2rem" }}>
|
|
1374
|
-
<img
|
|
1375
|
-
src={mediaBytesUrl(img.uri)}
|
|
1376
|
-
alt="start"
|
|
1377
|
-
style={{
|
|
1378
|
-
width: thumb,
|
|
1379
|
-
height: thumb,
|
|
1380
|
-
objectFit: "cover",
|
|
1381
|
-
borderRadius: "0.4rem",
|
|
1382
|
-
background: "#000",
|
|
1383
|
-
border: "1px solid var(--vi-border)",
|
|
1384
|
-
}}
|
|
1385
|
-
/>
|
|
1386
|
-
<button
|
|
1387
|
-
type="button"
|
|
1388
|
-
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
1389
|
-
onClick={() => removeStartImage(idx)}
|
|
1390
|
-
>
|
|
1391
|
-
Remove
|
|
1392
|
-
</button>
|
|
1393
|
-
</div>
|
|
1394
|
-
))}
|
|
1395
|
-
</div>
|
|
1396
|
-
)}
|
|
1397
|
-
{canAddImage ? (
|
|
1398
|
-
isHero && !hasImage ? (
|
|
1399
|
-
<DropReceptacle
|
|
1400
|
-
onFile={(f) => void onPickImageFile(f, "start")}
|
|
1401
|
-
accept="image/*"
|
|
1402
|
-
busy={imageBusy}
|
|
1403
|
-
title="Drop a start image or click to browse"
|
|
1404
|
-
hint="This generator carries the subject from this still."
|
|
1405
|
-
className={drawEye ? "vi-dropzone--required" : undefined}
|
|
1406
|
-
/>
|
|
1407
|
-
) : (
|
|
1408
|
-
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
|
|
1409
|
-
<label className="vi-btn vi-btn-accent vi-file-label">
|
|
1410
|
-
{imageBusy ? "Working…" : "Upload a still"}
|
|
1411
|
-
<input
|
|
1412
|
-
type="file"
|
|
1413
|
-
accept="image/*"
|
|
1414
|
-
className="vi-file-input"
|
|
1415
|
-
disabled={imageBusy}
|
|
1416
|
-
onChange={(e) => {
|
|
1417
|
-
const f = e.target.files?.[0];
|
|
1418
|
-
if (f) void onPickImageFile(f, "start");
|
|
1419
|
-
e.target.value = "";
|
|
1420
|
-
}}
|
|
1421
|
-
/>
|
|
1422
|
-
</label>
|
|
1423
|
-
<button
|
|
1424
|
-
type="button"
|
|
1425
|
-
className="vi-btn vi-btn-ghost"
|
|
1426
|
-
onClick={() => setShowLibPicker((v) => !v)}
|
|
1427
|
-
aria-expanded={showLibPicker}
|
|
1428
|
-
>
|
|
1429
|
-
{showLibPicker ? "Hide library" : `Pick from library (${libraryImages.length})`}
|
|
1430
|
-
</button>
|
|
1431
|
-
</div>
|
|
1432
|
-
)
|
|
1433
|
-
) : (
|
|
1434
|
-
<p className="vi-comfy-hint" role="note">
|
|
1435
|
-
Multi-image conditioning here is identity lock — switch capability to Identity lock to
|
|
1436
|
-
add 1–4 reference images.
|
|
1437
|
-
</p>
|
|
1438
|
-
)}
|
|
1439
|
-
{imageError && (
|
|
1440
|
-
<p className="vi-error" role="alert" style={{ marginTop: "0.4rem" }}>
|
|
1441
|
-
{imageError}
|
|
1442
|
-
</p>
|
|
1443
|
-
)}
|
|
1444
|
-
{showLibPicker && canAddImage && (
|
|
1445
|
-
<LibraryImageGrid images={libraryImages} onPick={(ref) => addStartImage(ref)} />
|
|
1446
|
-
)}
|
|
1447
|
-
</section>
|
|
1448
|
-
);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
1471
|
// ── reference-image SLOT LIST: 1–4 ordered stills, reorderable. The SAME list feeds
|
|
1452
1472
|
// two shapes — id_lock (variant "required": the identity to lock, accent-gated) and
|
|
1453
1473
|
// v2v scene-continuity (variant "optional": an identity held across the restyle). ──
|
|
@@ -1713,15 +1733,11 @@ export function StudioGenerateSurface({
|
|
|
1713
1733
|
);
|
|
1714
1734
|
}
|
|
1715
1735
|
|
|
1716
|
-
// ── source-clip input (hero = dominant/accented; inline = subdued
|
|
1717
|
-
|
|
1718
|
-
function renderSource(variant: "hero" | "inline" | "inline-alt") {
|
|
1736
|
+
// ── source-clip input (hero = dominant/accented; inline = subdued) ──
|
|
1737
|
+
function renderSource(variant: "hero" | "inline") {
|
|
1719
1738
|
const isHero = variant === "hero";
|
|
1720
|
-
const isAlt = variant === "inline-alt";
|
|
1721
1739
|
const drawEye = isHero && sourceAttention;
|
|
1722
|
-
const label =
|
|
1723
|
-
? "…or use a source clip (extend from its last frame)"
|
|
1724
|
-
: isHero
|
|
1740
|
+
const label = isHero
|
|
1725
1741
|
? "Source clip — required (the clip to repaint)"
|
|
1726
1742
|
: isV2V
|
|
1727
1743
|
? "Source clip — required"
|
|
@@ -1964,20 +1980,26 @@ export function StudioGenerateSurface({
|
|
|
1964
1980
|
|
|
1965
1981
|
const knobGrid = (
|
|
1966
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>
|
|
1967
1992
|
{/* ABOUT — the condensed what-is-this / how-to prose (k89), collapsed at the
|
|
1968
1993
|
top of the Settings-tab content. Absorbs the descriptive halves of the old
|
|
1969
1994
|
inline callouts; the conditional required-input warnings stay on the form. */}
|
|
1970
1995
|
<div className="vi-studio-knob-wide" style={{ flex: "1 1 100%" }}>
|
|
1971
1996
|
<AboutExpander title="About Clip">
|
|
1972
1997
|
<p>
|
|
1973
|
-
Generates
|
|
1974
|
-
generator needs.
|
|
1998
|
+
Generates one clip from your prompt and the inputs you provide.
|
|
1975
1999
|
</p>
|
|
1976
2000
|
<p>
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
extend), restyle a source clip, identity lock 1–4 reference images, and
|
|
1980
|
-
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.
|
|
1981
2003
|
</p>
|
|
1982
2004
|
<p>
|
|
1983
2005
|
A missing required input is flagged on the card and gates Generate until
|
|
@@ -2128,6 +2150,7 @@ export function StudioGenerateSurface({
|
|
|
2128
2150
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2129
2151
|
onChange={(e) => {
|
|
2130
2152
|
geomTouched.current = true;
|
|
2153
|
+
markManual("width");
|
|
2131
2154
|
setWidth(Number(e.target.value) || 0);
|
|
2132
2155
|
}}
|
|
2133
2156
|
/>
|
|
@@ -2145,6 +2168,7 @@ export function StudioGenerateSurface({
|
|
|
2145
2168
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2146
2169
|
onChange={(e) => {
|
|
2147
2170
|
geomTouched.current = true;
|
|
2171
|
+
markManual("height");
|
|
2148
2172
|
setHeight(Number(e.target.value) || 0);
|
|
2149
2173
|
}}
|
|
2150
2174
|
/>
|
|
@@ -2162,6 +2186,7 @@ export function StudioGenerateSurface({
|
|
|
2162
2186
|
title={isV2V ? geomLockTitle : geomEnvelopeTitle}
|
|
2163
2187
|
onChange={(e) => {
|
|
2164
2188
|
geomTouched.current = true;
|
|
2189
|
+
markManual("fps");
|
|
2165
2190
|
setFps(Number(e.target.value) || 0);
|
|
2166
2191
|
}}
|
|
2167
2192
|
/>
|
|
@@ -2180,7 +2205,7 @@ export function StudioGenerateSurface({
|
|
|
2180
2205
|
type="number"
|
|
2181
2206
|
min={0}
|
|
2182
2207
|
value={seed}
|
|
2183
|
-
onChange={(e) => setSeed(Number(e.target.value) || 0)}
|
|
2208
|
+
onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }}
|
|
2184
2209
|
/>
|
|
2185
2210
|
</div>
|
|
2186
2211
|
<div className="vi-knob">
|
|
@@ -2216,7 +2241,7 @@ export function StudioGenerateSurface({
|
|
|
2216
2241
|
max={100}
|
|
2217
2242
|
value={steps}
|
|
2218
2243
|
placeholder="model default"
|
|
2219
|
-
onChange={(e) => setSteps(e.target.value)}
|
|
2244
|
+
onChange={(e) => { markManual("steps"); setSteps(e.target.value); }}
|
|
2220
2245
|
/>
|
|
2221
2246
|
</div>
|
|
2222
2247
|
<div className="vi-knob">
|
|
@@ -2230,7 +2255,7 @@ export function StudioGenerateSurface({
|
|
|
2230
2255
|
step={0.5}
|
|
2231
2256
|
value={cfg}
|
|
2232
2257
|
placeholder="model default"
|
|
2233
|
-
onChange={(e) => setCfg(e.target.value)}
|
|
2258
|
+
onChange={(e) => { markManual("cfg"); setCfg(e.target.value); }}
|
|
2234
2259
|
/>
|
|
2235
2260
|
</div>
|
|
2236
2261
|
{/* MODEL PIN — FILTERED BY CAPABILITY (2026-07-29).
|
|
@@ -2251,9 +2276,12 @@ export function StudioGenerateSurface({
|
|
|
2251
2276
|
id="vi-studio-model"
|
|
2252
2277
|
className="vi-knob-select"
|
|
2253
2278
|
value={modelId}
|
|
2254
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2279
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2255
2280
|
>
|
|
2256
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
|
+
)}
|
|
2257
2285
|
{capModels.length > 0 && (
|
|
2258
2286
|
<optgroup label={`Ratified for ${CAP_LABELS[capability]}`}>
|
|
2259
2287
|
{capModels.map((id) => (
|
|
@@ -2458,21 +2486,35 @@ export function StudioGenerateSurface({
|
|
|
2458
2486
|
onStdNegativeChange={setStdNegative}
|
|
2459
2487
|
/>
|
|
2460
2488
|
{/* PROMPT ASSIST — Enhance enriches the clip prompt, Generate writes a fresh
|
|
2461
|
-
one
|
|
2489
|
+
one. Both update the same field. Same shared cluster as the Generate
|
|
2462
2490
|
station; errors surface inline and never clobber the existing prompt. */}
|
|
2463
2491
|
<PromptAssistButtons
|
|
2464
2492
|
busy={assist.assistBusy}
|
|
2465
2493
|
error={assist.assistError}
|
|
2466
2494
|
onDismissError={assist.dismissError}
|
|
2467
|
-
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
|
+
})()}
|
|
2468
2506
|
onGenerate={() =>
|
|
2469
2507
|
void (async () => {
|
|
2470
2508
|
let fresh = "";
|
|
2471
|
-
await assist.runAssist("generate",
|
|
2509
|
+
await assist.runAssist("generate", "", (p) => {
|
|
2472
2510
|
fresh = p;
|
|
2473
2511
|
setPrompt(p);
|
|
2474
2512
|
});
|
|
2475
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
|
+
}
|
|
2476
2518
|
// NEGATIVE RIDES ALONG (operator 2026-08-13): generated
|
|
2477
2519
|
// exclusions APPEND to any typed negative; std tick untouched.
|
|
2478
2520
|
await assist.runNegative({ subject: fresh }, (neg) => {
|
|
@@ -2533,6 +2575,22 @@ export function StudioGenerateSurface({
|
|
|
2533
2575
|
)}
|
|
2534
2576
|
{promptAttachments.length > 0 && (
|
|
2535
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
|
+
)}
|
|
2536
2594
|
{promptAttachments.map((a, i) => (
|
|
2537
2595
|
<button
|
|
2538
2596
|
key={`${a.label}-${i}`}
|
|
@@ -2547,6 +2605,12 @@ export function StudioGenerateSurface({
|
|
|
2547
2605
|
))}
|
|
2548
2606
|
</div>
|
|
2549
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
|
+
)}
|
|
2550
2614
|
|
|
2551
2615
|
{/* SETTINGS EXPANDER (operator ask 2026-08-13): the clip prompt
|
|
2552
2616
|
component hosts the FULL knob set, same as Scene/Movie/Cinema
|
|
@@ -2557,15 +2621,27 @@ export function StudioGenerateSurface({
|
|
|
2557
2621
|
on the Settings tab only: it morphs the form shape (required
|
|
2558
2622
|
inputs), it is not a render knob. */}
|
|
2559
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>
|
|
2560
2633
|
<div className="vi-knob vi-studio-knob-wide" style={{ minWidth: "16rem" }}>
|
|
2561
2634
|
<label htmlFor="vi-clipcard-model">model pin</label>
|
|
2562
2635
|
<select
|
|
2563
2636
|
id="vi-clipcard-model"
|
|
2564
2637
|
className="vi-knob-select"
|
|
2565
2638
|
value={modelId}
|
|
2566
|
-
onChange={(e) => setModelId(e.target.value)}
|
|
2639
|
+
onChange={(e) => { markManual("model_id"); setModelId(e.target.value); }}
|
|
2567
2640
|
>
|
|
2568
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
|
+
)}
|
|
2569
2645
|
{capModels.length > 0 && (
|
|
2570
2646
|
<optgroup label={`Ratified for ${CAP_LABELS[capability]}`}>
|
|
2571
2647
|
{capModels.map((id) => (
|
|
@@ -2590,36 +2666,36 @@ export function StudioGenerateSurface({
|
|
|
2590
2666
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2591
2667
|
<label htmlFor="vi-clipcard-width">width</label>
|
|
2592
2668
|
<input id="vi-clipcard-width" className="vi-knob-input" type="number" min={64} step={8}
|
|
2593
|
-
value={width} onChange={(e) => setWidth(Number(e.target.value) || 0)} />
|
|
2669
|
+
value={width} onChange={(e) => { markManual("width"); setWidth(Number(e.target.value) || 0); }} />
|
|
2594
2670
|
</div>
|
|
2595
2671
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2596
2672
|
<label htmlFor="vi-clipcard-height">height</label>
|
|
2597
2673
|
<input id="vi-clipcard-height" className="vi-knob-input" type="number" min={64} step={8}
|
|
2598
|
-
value={height} onChange={(e) => setHeight(Number(e.target.value) || 0)} />
|
|
2674
|
+
value={height} onChange={(e) => { markManual("height"); setHeight(Number(e.target.value) || 0); }} />
|
|
2599
2675
|
</div>
|
|
2600
2676
|
{/* LENGTH ROW (operator 2026-08-13): fps | frames | seconds — any two
|
|
2601
2677
|
set the third; the dimmed one is derived, last-touched wins. */}
|
|
2602
2678
|
<LengthRow
|
|
2603
2679
|
idPrefix="vi-clipcard-len"
|
|
2604
2680
|
fps={fps}
|
|
2605
|
-
onFps={(n) => setFps(n)}
|
|
2681
|
+
onFps={(n) => { markManual("fps"); setFps(n); }}
|
|
2606
2682
|
frames={requestedFrames}
|
|
2607
|
-
onFrames={setRequestedFrames}
|
|
2683
|
+
onFrames={(n) => { markManual("frames"); setRequestedFrames(n); }}
|
|
2608
2684
|
/>
|
|
2609
2685
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2610
2686
|
<label htmlFor="vi-clipcard-steps">steps</label>
|
|
2611
2687
|
<input id="vi-clipcard-steps" className="vi-knob-input" type="number" min={1} max={100}
|
|
2612
|
-
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); }} />
|
|
2613
2689
|
</div>
|
|
2614
2690
|
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2615
2691
|
<label htmlFor="vi-clipcard-cfg">cfg</label>
|
|
2616
2692
|
<input id="vi-clipcard-cfg" className="vi-knob-input" type="number" min={0} max={20} step={0.5}
|
|
2617
|
-
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); }} />
|
|
2618
2694
|
</div>
|
|
2619
2695
|
<div className="vi-knob" style={{ maxWidth: "8rem" }}>
|
|
2620
2696
|
<label htmlFor="vi-clipcard-seed">seed</label>
|
|
2621
2697
|
<input id="vi-clipcard-seed" className="vi-knob-input" type="number" min={0}
|
|
2622
|
-
value={seed} onChange={(e) => setSeed(Number(e.target.value) || 0)} />
|
|
2698
|
+
value={seed} onChange={(e) => { markManual("seed"); setSeed(Number(e.target.value) || 0); }} />
|
|
2623
2699
|
</div>
|
|
2624
2700
|
<div className="vi-knob" style={{ maxWidth: "9rem" }}>
|
|
2625
2701
|
<label htmlFor="vi-clipcard-vram">VRAM budget GB</label>
|
|
@@ -2635,6 +2711,15 @@ export function StudioGenerateSurface({
|
|
|
2635
2711
|
</PromptCardSettings>
|
|
2636
2712
|
</PromptCard>
|
|
2637
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>}
|
|
2638
2723
|
</section>
|
|
2639
2724
|
|
|
2640
2725
|
{/* Required-input callout — surfaces the missing prerequisite PROMINENTLY next to
|
|
@@ -2648,16 +2733,6 @@ export function StudioGenerateSurface({
|
|
|
2648
2733
|
|
|
2649
2734
|
{/* ── 3) SHAPE EDITOR (heroes) — morphs per preformed shape; stays dominant when
|
|
2650
2735
|
required (accent gating) even though it now sits below the knob grid. ── */}
|
|
2651
|
-
{hero === "start-image" && (
|
|
2652
|
-
<section
|
|
2653
|
-
aria-label="Required input"
|
|
2654
|
-
className="vi-studio-shape"
|
|
2655
|
-
style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
|
|
2656
|
-
>
|
|
2657
|
-
{renderStartImage("hero")}
|
|
2658
|
-
{renderSource("inline-alt")}
|
|
2659
|
-
</section>
|
|
2660
|
-
)}
|
|
2661
2736
|
{hero === "source" && (
|
|
2662
2737
|
<section
|
|
2663
2738
|
aria-label="Required input"
|
|
@@ -2693,7 +2768,6 @@ export function StudioGenerateSurface({
|
|
|
2693
2768
|
)}
|
|
2694
2769
|
|
|
2695
2770
|
{/* LOWER inputs — only when NOT a hero shape (custom / synthetic / t2v). */}
|
|
2696
|
-
{!hero && isI2V && <div className="vi-studio-shape">{renderStartImage("inline")}</div>}
|
|
2697
2771
|
{!hero && !isT2V && !isIdLock && <div className="vi-studio-shape">{renderSource("inline")}</div>}
|
|
2698
2772
|
{!hero && isT2V && (
|
|
2699
2773
|
<p className="vi-knob-hint" role="note" style={{ marginTop: "0.9rem" }}>
|
|
@@ -2753,6 +2827,12 @@ export function StudioGenerateSurface({
|
|
|
2753
2827
|
{genMsg}
|
|
2754
2828
|
</p>
|
|
2755
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
|
+
)}
|
|
2756
2836
|
</div>
|
|
2757
2837
|
{nameWarn && (
|
|
2758
2838
|
<UnnamedWarnPanel
|
|
@@ -2782,7 +2862,7 @@ export function StudioGenerateSurface({
|
|
|
2782
2862
|
: isMotion
|
|
2783
2863
|
? "Generate (pose / structure)"
|
|
2784
2864
|
: isI2V
|
|
2785
|
-
? "Generate (i2v)"
|
|
2865
|
+
? clipRoute === "i2v" ? "Generate (i2v)" : "Generate (t2v)"
|
|
2786
2866
|
: "Generate (t2v)"}
|
|
2787
2867
|
</button>
|
|
2788
2868
|
{/* (The cross-model "🧪 Test all models" ghost that sat here moved into
|
|
@@ -2813,10 +2893,13 @@ export function StudioGenerateSurface({
|
|
|
2813
2893
|
</div>
|
|
2814
2894
|
{clipTesterOpen && (
|
|
2815
2895
|
<div className="vi-gen-tester-actions">
|
|
2896
|
+
{!isT2V && !(isI2V && hasImage) && (
|
|
2897
|
+
<span className="vi-knob-hint" role="note">The current sweep supports text-to-video and image-to-video with a start image. Choose one of those shapes to run it.</span>
|
|
2898
|
+
)}
|
|
2816
2899
|
<button
|
|
2817
2900
|
type="button"
|
|
2818
2901
|
className="vi-btn vi-btn-sm vi-btn-tester"
|
|
2819
|
-
disabled={testerBusy || !prompt.trim()}
|
|
2902
|
+
disabled={testerBusy || !prompt.trim() || (!isT2V && !(isI2V && hasImage))}
|
|
2820
2903
|
onClick={() =>
|
|
2821
2904
|
void onTestAllModels({
|
|
2822
2905
|
category: surfaceMode,
|
|
@@ -2825,9 +2908,14 @@ export function StudioGenerateSurface({
|
|
|
2825
2908
|
height,
|
|
2826
2909
|
fps,
|
|
2827
2910
|
seed,
|
|
2911
|
+
startImage: isI2V ? startImages[0]?.uri : undefined,
|
|
2912
|
+
steps: steps.trim() ? Number(steps) : undefined,
|
|
2913
|
+
cfg: cfg.trim() ? Number(cfg) : undefined,
|
|
2914
|
+
requestedFrames: requestedFrames.trim() ? Number(requestedFrames) : undefined,
|
|
2915
|
+
negative: negative.trim() || undefined,
|
|
2828
2916
|
})
|
|
2829
2917
|
}
|
|
2830
|
-
title=
|
|
2918
|
+
title={!isT2V && !(isI2V && hasImage) ? "This sweep currently supports text-to-video or image-to-video with a start image." : "Run this prompt across each model that fits the current filter; one battery row per model."}
|
|
2831
2919
|
>
|
|
2832
2920
|
{testerBusy ? "Starting…" : "🧪 Test all models"}
|
|
2833
2921
|
</button>
|