@hugpy/video-intelligence-ui 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/stations/GenerateStation.tsx +146 -14
- package/src/stations/WorkbenchSidebar.tsx +57 -12
- package/src/stations/studio/StudioGenerateTab.tsx +169 -6
- package/src/stations/studio/StudioMovieComposer.tsx +507 -299
- package/src/stations/studio/{StudioMovieComposer.tsx.bak-sessions-20260812 → StudioMovieComposer.tsx.bak-declutter-20260813} +350 -48
- package/src/style/app.css +19 -0
- package/src/video/LibraryGroups.tsx +81 -0
- package/src/video/UnnamedWarn.tsx +146 -0
- package/src/video/useOperatorHistory.ts +100 -0
- package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +0 -228
- package/src/stations/GenerateStation.tsx.bak-tester-removal +0 -5170
- package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +0 -417
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hugpy/video-intelligence-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Video-intelligence console \u2014 sibling arm of media_intelligence_ui (Vite + Tailwind v4), served at /video. Four stations (image crop, audio crop, frames/models, generate) over the hugpy job bus. Dials the same-origin current-hugpy API (/api).",
|
|
@@ -79,6 +79,9 @@ import { getSessionId } from "../session";
|
|
|
79
79
|
import { isCanned } from "../demo/mode";
|
|
80
80
|
import { DEMO_GENERATE_PROMPT } from "../demo/seed";
|
|
81
81
|
import { usePromptAssist } from "../video/usePromptAssist";
|
|
82
|
+
import { useOperatorHistory } from "../video/useOperatorHistory";
|
|
83
|
+
import { UnnamedWarnPanel, WarnUnnamedToggle, useWarnUnnamedPref, deriveNameFrom } from "../video/UnnamedWarn";
|
|
84
|
+
import { LibraryGroups } from "../video/LibraryGroups";
|
|
82
85
|
import { PromptAssistButtons } from "../video/PromptAssistButtons";
|
|
83
86
|
import { registerComposer, type StageRequest } from "../video/composerBridge";
|
|
84
87
|
import {
|
|
@@ -631,6 +634,20 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
631
634
|
|
|
632
635
|
// ---- optional auto-archive project name (blank = backend auto-names) ----
|
|
633
636
|
const [projectName, setProjectName] = useSessionState<string>("gen.projectName", "");
|
|
637
|
+
// UNNAMED WARNING (operator 2026-08-13): explicit PER-SECTION option, default
|
|
638
|
+
// OFF, localStorage-remembered — an image that renders in 2 seconds should
|
|
639
|
+
// never suffer a 30-second warning unless its operator opted in.
|
|
640
|
+
const [warnImagePref, setWarnImagePref] = useWarnUnnamedPref("gen.image");
|
|
641
|
+
const [warnScenePref, setWarnScenePref] = useWarnUnnamedPref("gen.scene");
|
|
642
|
+
const [warnMoviePref, setWarnMoviePref] = useWarnUnnamedPref("gen.movie");
|
|
643
|
+
const warnPref =
|
|
644
|
+
mode === "image" ? warnImagePref : mode === "scene" ? warnScenePref : mode === "movie" ? warnMoviePref : false;
|
|
645
|
+
const setWarnPref =
|
|
646
|
+
mode === "image" ? setWarnImagePref : mode === "scene" ? setWarnScenePref : setWarnMoviePref;
|
|
647
|
+
const [nameWarn, setNameWarn] = useState(false);
|
|
648
|
+
const [pendingRun, setPendingRun] = useState(false);
|
|
649
|
+
// Auto-derived names may be replaced by the next ✨ Generate; typed ones never.
|
|
650
|
+
const autoProjectRef = useRef("");
|
|
634
651
|
|
|
635
652
|
// ---- STUDIO TESTER cross-model battery (operator 2026-08-05) ----
|
|
636
653
|
// The SAME across-models sweep the studio surfaces expose (POST studioTesterUrl),
|
|
@@ -984,6 +1001,18 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
984
1001
|
|
|
985
1002
|
// ---- library (images are pickable prompt parts; videos too) ----
|
|
986
1003
|
const library = useMediaLibrary();
|
|
1004
|
+
// OPERATOR HISTORY (2026-08-13): a superadmin session back-fills the library
|
|
1005
|
+
// from the server record once per session (complete visibility); see the hook.
|
|
1006
|
+
useOperatorHistory();
|
|
1007
|
+
// GROUPED LIBRARY (operator ask 2026-08-13, v2): the pickers render the
|
|
1008
|
+
// library as TYPE-CATEGORIZED collapsible sections (LibraryGroups) instead
|
|
1009
|
+
// of a flat, section-filtered grid — everything stays visible + pickable,
|
|
1010
|
+
// organized; the CURRENT section's own group (and Inputs) starts expanded.
|
|
1011
|
+
const MODE_GROUP: Record<GenMode, string> = {
|
|
1012
|
+
image: "images", scene: "scenes", movie: "movies",
|
|
1013
|
+
clip: "clips", cinema: "clips",
|
|
1014
|
+
};
|
|
1015
|
+
const libraryDefaultOpen: readonly string[] = ["inputs", MODE_GROUP[mode]];
|
|
987
1016
|
const libraryImages = library.filter((it) => it.ref.kind === "image");
|
|
988
1017
|
const libraryVideos = library.filter((it) => it.ref.kind === "video");
|
|
989
1018
|
|
|
@@ -1353,12 +1382,30 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
1353
1382
|
// composition never doubles a term.
|
|
1354
1383
|
(neg) => {
|
|
1355
1384
|
const std = p.stdNegative ?? sceneStdNegative;
|
|
1385
|
+
// APPEND, never clobber (operator 2026-08-13): a GENERATED negative adds
|
|
1386
|
+
// to whatever the operator already typed (Enhance still refines-in-place —
|
|
1387
|
+
// its draft was the existing text, so appending would double it).
|
|
1388
|
+
// composeNegative dedupes, so submit-time composition never doubles a term.
|
|
1389
|
+
const cur = (p.negative ?? "").trim().replace(/,\s*$/, "");
|
|
1390
|
+
const mergedRaw = mode === "generate" && cur ? `${cur}, ${neg}` : neg;
|
|
1356
1391
|
patchPart(p.key, {
|
|
1357
|
-
negative: composeNegative(
|
|
1392
|
+
negative: composeNegative(mergedRaw, std),
|
|
1358
1393
|
stdNegative: std,
|
|
1359
1394
|
});
|
|
1360
1395
|
},
|
|
1361
1396
|
);
|
|
1397
|
+
// NAME RIDES ALONG (operator 2026-08-13): derive the project name from the
|
|
1398
|
+
// fresh prompt when blank — or when the current one was itself derived.
|
|
1399
|
+
if (mode === "generate" && newText.trim()) {
|
|
1400
|
+
const t = projectName.trim();
|
|
1401
|
+
if (t === "" || t === autoProjectRef.current) {
|
|
1402
|
+
const derived = deriveNameFrom(newText);
|
|
1403
|
+
if (derived) {
|
|
1404
|
+
autoProjectRef.current = derived;
|
|
1405
|
+
setProjectName(derived);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1362
1409
|
}
|
|
1363
1410
|
|
|
1364
1411
|
// "Test all models" / "per preset" as COMPONENTS — the single reusable
|
|
@@ -2366,6 +2413,34 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
2366
2413
|
</span>
|
|
2367
2414
|
</div>
|
|
2368
2415
|
</div>
|
|
2416
|
+
{/* UNIFORM-CARD PLACEHOLDERS (operator 2026-08-13): the core knob set
|
|
2417
|
+
renders on EVERY prompt component. Slots this tier serves at the
|
|
2418
|
+
MOVIE level (fps, VRAM budget) — or expresses differently (a movie
|
|
2419
|
+
goal's LENGTH is its start/end times, frames = duration x fps) —
|
|
2420
|
+
appear GREYED with the reason, never hidden, so every card in every
|
|
2421
|
+
section reads identically. */}
|
|
2422
|
+
<div className="vi-knob-strip">
|
|
2423
|
+
<div className="vi-knob-strip-line">
|
|
2424
|
+
<span className="vi-knob-mini-group">
|
|
2425
|
+
<label className="vi-knob-mini-label" htmlFor={gid("na-fps")}>fps</label>
|
|
2426
|
+
<input id={gid("na-fps")} className="vi-knob-input" style={{ maxWidth: "4.5rem" }} disabled
|
|
2427
|
+
value={fpsNum ?? ""}
|
|
2428
|
+
title="Movie-level — one fps for the whole timeline; set it in the station knobs." />
|
|
2429
|
+
</span>
|
|
2430
|
+
<span className="vi-knob-mini-group">
|
|
2431
|
+
<label className="vi-knob-mini-label" htmlFor={gid("na-length")}>length</label>
|
|
2432
|
+
<input id={gid("na-length")} className="vi-knob-input" style={{ maxWidth: "6.5rem" }} disabled
|
|
2433
|
+
value={`${g.start || "0"}–${g.end || "?"}s`}
|
|
2434
|
+
title="A movie goal's length IS its start/end times (frames = duration x fps) — edit those on the card; a frames knob does not apply here." />
|
|
2435
|
+
</span>
|
|
2436
|
+
<span className="vi-knob-mini-group">
|
|
2437
|
+
<label className="vi-knob-mini-label" htmlFor={gid("na-vram")}>VRAM</label>
|
|
2438
|
+
<input id={gid("na-vram")} className="vi-knob-input" style={{ maxWidth: "5.5rem" }} disabled
|
|
2439
|
+
value="autofit"
|
|
2440
|
+
title="Movie-level — renders size to the serving worker's free VRAM (autofit); the movie tier has no per-goal budget." />
|
|
2441
|
+
</span>
|
|
2442
|
+
</div>
|
|
2443
|
+
</div>
|
|
2369
2444
|
</>
|
|
2370
2445
|
);
|
|
2371
2446
|
}
|
|
@@ -3331,7 +3406,23 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
3331
3406
|
void assist.runAssist(assistMode, assembledPrompt, applyAssistedPrompt);
|
|
3332
3407
|
}
|
|
3333
3408
|
|
|
3334
|
-
|
|
3409
|
+
useEffect(() => {
|
|
3410
|
+
// Post-commit re-run after the warn panel names the run — fresh closure,
|
|
3411
|
+
// settled state, so the submit sees the new project name.
|
|
3412
|
+
if (!pendingRun) return;
|
|
3413
|
+
setPendingRun(false);
|
|
3414
|
+
onRun(true);
|
|
3415
|
+
});
|
|
3416
|
+
|
|
3417
|
+
function onRun(skipNameCheck = false) {
|
|
3418
|
+
if (
|
|
3419
|
+
!skipNameCheck && warnPref && projectName.trim() === "" &&
|
|
3420
|
+
(mode === "image" || mode === "scene" || mode === "movie")
|
|
3421
|
+
) {
|
|
3422
|
+
setNameWarn(true);
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
setNameWarn(false);
|
|
3335
3426
|
// This guard is for the STATION-level knobs (movie + the image fallback).
|
|
3336
3427
|
// Scene validates every knob PER-PART in buildSceneRow (station values may be
|
|
3337
3428
|
// blank/unused when knobs are unshared), so it must not be gated here — else a
|
|
@@ -3613,6 +3704,7 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
3613
3704
|
Choose an existing project or type a new name. Saves this run's frames +
|
|
3614
3705
|
clip to assets/<project>. Blank = an auto-named folder.
|
|
3615
3706
|
</span>
|
|
3707
|
+
<WarnUnnamedToggle checked={warnPref} onChange={setWarnPref} />
|
|
3616
3708
|
</div>
|
|
3617
3709
|
|
|
3618
3710
|
{/* PRESET — a curated "ideal default load". Selecting one prefills every
|
|
@@ -4396,8 +4488,11 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
4396
4488
|
land here and become pickable.
|
|
4397
4489
|
</p>
|
|
4398
4490
|
) : (
|
|
4399
|
-
<
|
|
4400
|
-
{
|
|
4491
|
+
<LibraryGroups
|
|
4492
|
+
seed={mode}
|
|
4493
|
+
items={libraryImages}
|
|
4494
|
+
defaultOpen={libraryDefaultOpen}
|
|
4495
|
+
renderItem={(it) => (
|
|
4401
4496
|
<button
|
|
4402
4497
|
type="button"
|
|
4403
4498
|
key={it.ref.uri}
|
|
@@ -4416,8 +4511,8 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
4416
4511
|
{it.label ? ` · ${it.label}` : ""}
|
|
4417
4512
|
</span>
|
|
4418
4513
|
</button>
|
|
4419
|
-
)
|
|
4420
|
-
|
|
4514
|
+
)}
|
|
4515
|
+
/>
|
|
4421
4516
|
)}
|
|
4422
4517
|
</div>
|
|
4423
4518
|
)}
|
|
@@ -4450,8 +4545,11 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
4450
4545
|
{videoError && <span className="vi-error">{videoError}</span>}
|
|
4451
4546
|
</div>
|
|
4452
4547
|
{libraryVideos.length > 0 && (
|
|
4453
|
-
<
|
|
4454
|
-
{
|
|
4548
|
+
<LibraryGroups
|
|
4549
|
+
seed={mode}
|
|
4550
|
+
items={libraryVideos}
|
|
4551
|
+
defaultOpen={libraryDefaultOpen}
|
|
4552
|
+
renderItem={(it) => (
|
|
4455
4553
|
<button
|
|
4456
4554
|
type="button"
|
|
4457
4555
|
key={it.ref.uri}
|
|
@@ -4466,8 +4564,8 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
4466
4564
|
<code>{it.ref.mime}</code> · {it.origin}
|
|
4467
4565
|
</span>
|
|
4468
4566
|
</button>
|
|
4469
|
-
)
|
|
4470
|
-
|
|
4567
|
+
)}
|
|
4568
|
+
/>
|
|
4471
4569
|
)}
|
|
4472
4570
|
</div>
|
|
4473
4571
|
)}
|
|
@@ -4860,9 +4958,32 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
4860
4958
|
)
|
|
4861
4959
|
}
|
|
4862
4960
|
onGenerate={() =>
|
|
4863
|
-
void
|
|
4864
|
-
|
|
4865
|
-
|
|
4961
|
+
void (async () => {
|
|
4962
|
+
let fresh = "";
|
|
4963
|
+
await assist.runAssist("generate", g.prompt, (p) => {
|
|
4964
|
+
fresh = p;
|
|
4965
|
+
editGoal(g.id, "prompt", p);
|
|
4966
|
+
});
|
|
4967
|
+
if (!fresh.trim()) return;
|
|
4968
|
+
// NEGATIVE RIDES ALONG (operator 2026-08-13): the
|
|
4969
|
+
// generated exclusion list APPENDS to the goal's (or
|
|
4970
|
+
// shared) typed negative, never clobbers it.
|
|
4971
|
+
await assist.runNegative({ subject: fresh }, (neg) => {
|
|
4972
|
+
const cur = (movieShare.negative ? negative : g.negative ?? "")
|
|
4973
|
+
.trim().replace(/,\s*$/, "");
|
|
4974
|
+
const merged = cur ? `${cur}, ${neg}` : neg;
|
|
4975
|
+
if (movieShare.negative) setNegative(merged);
|
|
4976
|
+
else patchGoal(g.id, { negative: merged });
|
|
4977
|
+
});
|
|
4978
|
+
const t = projectName.trim();
|
|
4979
|
+
if (t === "" || t === autoProjectRef.current) {
|
|
4980
|
+
const derived = deriveNameFrom(fresh);
|
|
4981
|
+
if (derived) {
|
|
4982
|
+
autoProjectRef.current = derived;
|
|
4983
|
+
setProjectName(derived);
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
})()
|
|
4866
4987
|
}
|
|
4867
4988
|
canEnhance={g.prompt.trim() !== ""}
|
|
4868
4989
|
models={assist.assistModels}
|
|
@@ -5557,12 +5678,23 @@ export function GenerateStation({ spec }: { spec: StationSpec }) {
|
|
|
5557
5678
|
</div>
|
|
5558
5679
|
)}
|
|
5559
5680
|
</div>
|
|
5681
|
+
{nameWarn && (
|
|
5682
|
+
<UnnamedWarnPanel
|
|
5683
|
+
noun={mode}
|
|
5684
|
+
onProceed={(name) => {
|
|
5685
|
+
setNameWarn(false);
|
|
5686
|
+
if (name) setProjectName(name);
|
|
5687
|
+
setPendingRun(true);
|
|
5688
|
+
}}
|
|
5689
|
+
onDismiss={() => setNameWarn(false)}
|
|
5690
|
+
/>
|
|
5691
|
+
)}
|
|
5560
5692
|
<div className="vi-station-footer-run">
|
|
5561
5693
|
<button
|
|
5562
5694
|
type="button"
|
|
5563
5695
|
className="vi-btn vi-btn-accent"
|
|
5564
5696
|
disabled={!canRun}
|
|
5565
|
-
onClick={onRun}
|
|
5697
|
+
onClick={() => onRun()}
|
|
5566
5698
|
>
|
|
5567
5699
|
{running
|
|
5568
5700
|
? mode === "movie"
|
|
@@ -1029,7 +1029,30 @@ function LibraryPanel({
|
|
|
1029
1029
|
items: LibraryItem[];
|
|
1030
1030
|
onGenerate: boolean;
|
|
1031
1031
|
}) {
|
|
1032
|
-
|
|
1032
|
+
// TYPE SECTIONS (operator 2026-08-13): the library splits into Images and
|
|
1033
|
+
// Videos (plus a rare Other for audio/unknown), each holding its work groups
|
|
1034
|
+
// newest-first — groupLibrary already sorts both levels freshest-first, so
|
|
1035
|
+
// the per-type ordering is "most recent on top" by construction.
|
|
1036
|
+
const sections = useMemo(() => {
|
|
1037
|
+
const imgs = items.filter((it) => it.ref.kind === "image");
|
|
1038
|
+
const vids = items.filter((it) => it.ref.kind === "video");
|
|
1039
|
+
const rest = items.filter((it) => it.ref.kind !== "image" && it.ref.kind !== "video");
|
|
1040
|
+
return [
|
|
1041
|
+
{ id: "images", label: "🖼 Images", count: imgs.length, works: groupLibrary(imgs) },
|
|
1042
|
+
{ id: "videos", label: "🎞 Videos", count: vids.length, works: groupLibrary(vids) },
|
|
1043
|
+
{ id: "other", label: "📎 Other", count: rest.length, works: groupLibrary(rest) },
|
|
1044
|
+
].filter((sec) => sec.count > 0);
|
|
1045
|
+
}, [items]);
|
|
1046
|
+
// Both type sections start OPEN (hiding freshly-landed work behind a closed
|
|
1047
|
+
// header is how "it never populates" reports happen); the caret remembers.
|
|
1048
|
+
const [closedTypes, setClosedTypes] = useState<Set<string>>(() => new Set());
|
|
1049
|
+
const toggleType = (id: string) =>
|
|
1050
|
+
setClosedTypes((prev) => {
|
|
1051
|
+
const next = new Set(prev);
|
|
1052
|
+
if (next.has(id)) next.delete(id);
|
|
1053
|
+
else next.add(id);
|
|
1054
|
+
return next;
|
|
1055
|
+
});
|
|
1033
1056
|
// Expand state keyed by work key — the established discipline (a Set that
|
|
1034
1057
|
// survives the library's refresh cadence: the works array is rebuilt on every
|
|
1035
1058
|
// change, but the keys are stable, so an expanded work stays open across a
|
|
@@ -1052,17 +1075,39 @@ function LibraryPanel({
|
|
|
1052
1075
|
}
|
|
1053
1076
|
|
|
1054
1077
|
return (
|
|
1055
|
-
|
|
1056
|
-
{
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1078
|
+
<>
|
|
1079
|
+
{sections.map((sec) => {
|
|
1080
|
+
const isOpen = !closedTypes.has(sec.id);
|
|
1081
|
+
return (
|
|
1082
|
+
<div key={sec.id} className="vi-lib-typegroup">
|
|
1083
|
+
<button
|
|
1084
|
+
type="button"
|
|
1085
|
+
className="vi-lib-group-toggle vi-lib-typegroup-head"
|
|
1086
|
+
aria-expanded={isOpen}
|
|
1087
|
+
onClick={() => toggleType(sec.id)}
|
|
1088
|
+
>
|
|
1089
|
+
<span className="vi-lib-group-caret" aria-hidden>
|
|
1090
|
+
{isOpen ? "▾" : "▸"}
|
|
1091
|
+
</span>{" "}
|
|
1092
|
+
{sec.label} <span className="vi-proc-count">{sec.count}</span>
|
|
1093
|
+
</button>
|
|
1094
|
+
{isOpen && (
|
|
1095
|
+
<ul className="vi-lib-list">
|
|
1096
|
+
{sec.works.map((w) => (
|
|
1097
|
+
<WorkSection
|
|
1098
|
+
key={w.key}
|
|
1099
|
+
group={w}
|
|
1100
|
+
open={openKeys.has(w.key)}
|
|
1101
|
+
onToggle={() => toggle(w.key)}
|
|
1102
|
+
onGenerate={onGenerate}
|
|
1103
|
+
/>
|
|
1104
|
+
))}
|
|
1105
|
+
</ul>
|
|
1106
|
+
)}
|
|
1107
|
+
</div>
|
|
1108
|
+
);
|
|
1109
|
+
})}
|
|
1110
|
+
</>
|
|
1066
1111
|
);
|
|
1067
1112
|
}
|
|
1068
1113
|
|
|
@@ -49,6 +49,7 @@ import { createPortal } from "react-dom";
|
|
|
49
49
|
import { request, okValue, errorOf, describeAppError } from "../../transport/client";
|
|
50
50
|
import { hugpyConfig, mediaBytesUrl } from "../../config";
|
|
51
51
|
import { addToLibrary, useMediaLibrary } from "../../video/mediaLibrary";
|
|
52
|
+
import { UnnamedWarnPanel, WarnUnnamedToggle, useWarnUnnamedPref, deriveNameFrom } from "../../video/UnnamedWarn";
|
|
52
53
|
import { enqueueResultSchema, mediaRefSchema, uploadResultSchema } from "../../video/contract";
|
|
53
54
|
import type { MediaRef } from "../../video/contract";
|
|
54
55
|
import { DropReceptacle } from "../../video/DropReceptacle";
|
|
@@ -85,7 +86,7 @@ import { IdentityProfileControls, type SelectedProfile } from "./IdentityProfile
|
|
|
85
86
|
// The SHARED prompt-card system (k88/k89): the Clip surface's single prompt card
|
|
86
87
|
// rides the same shell + [Prompt][Negative] tabs the Scene/Movie/Cinema lists use,
|
|
87
88
|
// plus the standard-negative composition helper and the sidebar About expander.
|
|
88
|
-
import { AboutExpander, PromptCard, PromptFieldTabs, composeNegative } from "./promptCard";
|
|
89
|
+
import { AboutExpander, PromptCard, PromptCardSettings, PromptFieldTabs, composeNegative } from "./promptCard";
|
|
89
90
|
// ── COMPATIBILITY AWARENESS (operator ruling 2026-07-29) ───────────────────────────
|
|
90
91
|
// The fleet's MEASURED render table (GET /video/render/presets) + the pure derivations
|
|
91
92
|
// over it. Everything this surface offers — capabilities, model pins, geometry, the
|
|
@@ -304,7 +305,7 @@ export function StudioGenerateSurface({
|
|
|
304
305
|
// k89: the card's [standard negative] tick — default OFF to preserve the Clip
|
|
305
306
|
// surface's blank-negative default; when on, the standard artifact/junk set is
|
|
306
307
|
// COMPOSED into the payload at submit (composeNegative — the wire is unchanged).
|
|
307
|
-
const [stdNegative, setStdNegative] = useSessionState("studio.clip.stdNegative",
|
|
308
|
+
const [stdNegative, setStdNegative] = useSessionState("studio.clip.stdNegative", true);
|
|
308
309
|
// k89: the bottom "[▸ Tester]" collapsible that now hosts the cross-model sweep
|
|
309
310
|
// (the run row's ghost 🧪 button is retired).
|
|
310
311
|
const [clipTesterOpen, setClipTesterOpen] = useState(false);
|
|
@@ -317,6 +318,20 @@ export function StudioGenerateSurface({
|
|
|
317
318
|
// `project` (auto-archive NAME); blank = auto-named (the default). The list of
|
|
318
319
|
// existing names backs the Settings datalist; free-typing a new one creates it.
|
|
319
320
|
const [project, setProject] = useSessionState("studio.clip.project", "");
|
|
321
|
+
// EXPLICIT CLIP LENGTH (operator 2026-08-13: "the length of the video
|
|
322
|
+
// generations has been ambiguous") — requested_frames, blank = the bound
|
|
323
|
+
// model's default (81 for real models). The spine clamps to the model
|
|
324
|
+
// ceiling and snaps to the 4k+1 temporal cadence.
|
|
325
|
+
const [requestedFrames, setRequestedFrames] = useSessionState("studio.clip.frames", "");
|
|
326
|
+
// UNNAMED WARNING (operator 2026-08-13): explicit per-section option,
|
|
327
|
+
// default OFF, localStorage-remembered. The clip's NAME is its `project`
|
|
328
|
+
// (the auto-archive name every generate route accepts).
|
|
329
|
+
const [warnUnnamed, setWarnUnnamed] = useWarnUnnamedPref("clip");
|
|
330
|
+
const [nameWarn, setNameWarn] = useState(false);
|
|
331
|
+
const [pendingRun, setPendingRun] = useState(false);
|
|
332
|
+
// Auto-derived project names may be replaced by the next ✨ Generate; a
|
|
333
|
+
// human-typed project is never overwritten.
|
|
334
|
+
const autoProjectRef = useRef("");
|
|
320
335
|
|
|
321
336
|
// i2v start image (cap 1) — the `start_image` field.
|
|
322
337
|
const [startImages, setStartImages] = useSessionState<MediaRef[]>(
|
|
@@ -1111,7 +1126,20 @@ export function StudioGenerateSurface({
|
|
|
1111
1126
|
const wouldBindSynthetic =
|
|
1112
1127
|
(isT2V || isI2V) && bindsSyntheticTier(vramBudget, realFloorGb);
|
|
1113
1128
|
|
|
1114
|
-
|
|
1129
|
+
// Post-commit re-run after the warn panel names the clip: state has settled,
|
|
1130
|
+
// this effect's closure is fresh, so the submit sees the new project name.
|
|
1131
|
+
useEffect(() => {
|
|
1132
|
+
if (!pendingRun) return;
|
|
1133
|
+
setPendingRun(false);
|
|
1134
|
+
void onGenerate(true);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
const onGenerate = useCallback(async (skipNameCheck = false) => {
|
|
1138
|
+
if (!skipNameCheck && warnUnnamed && project.trim() === "") {
|
|
1139
|
+
setNameWarn(true);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
setNameWarn(false);
|
|
1115
1143
|
if (blockSubmit) {
|
|
1116
1144
|
setGenMsg(
|
|
1117
1145
|
v2vBadGeom
|
|
@@ -1125,6 +1153,11 @@ export function StudioGenerateSurface({
|
|
|
1125
1153
|
setGenMsg("VRAM budget must be a positive number (or blank for the route default).");
|
|
1126
1154
|
return;
|
|
1127
1155
|
}
|
|
1156
|
+
const framesNum = requestedFrames.trim() === "" ? undefined : Number(requestedFrames);
|
|
1157
|
+
if (framesNum !== undefined && (!Number.isInteger(framesNum) || framesNum < 1)) {
|
|
1158
|
+
setGenMsg("Length must be a whole number of frames \u2265 1 (or blank for the model default).");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1128
1161
|
const stepsNum = steps.trim() === "" ? undefined : Number(steps);
|
|
1129
1162
|
if (
|
|
1130
1163
|
stepsNum !== undefined &&
|
|
@@ -1152,6 +1185,7 @@ export function StudioGenerateSurface({
|
|
|
1152
1185
|
vram_budget_gb: budgetNum,
|
|
1153
1186
|
steps: stepsNum,
|
|
1154
1187
|
cfg: cfgNum,
|
|
1188
|
+
requested_frames: requestedFrames.trim() !== "" ? Number(requestedFrames) : undefined,
|
|
1155
1189
|
model_id: modelId.trim() ? modelId.trim() : undefined,
|
|
1156
1190
|
// Optional auto-archive NAME (choose-or-type). Blank = auto-named (default).
|
|
1157
1191
|
project: project.trim() ? project.trim() : undefined,
|
|
@@ -1250,8 +1284,7 @@ export function StudioGenerateSurface({
|
|
|
1250
1284
|
pendingSubjectStage,
|
|
1251
1285
|
onEnqueued,
|
|
1252
1286
|
project,
|
|
1253
|
-
refreshProjects,
|
|
1254
|
-
]);
|
|
1287
|
+
refreshProjects,, warnUnnamed, project]);
|
|
1255
1288
|
|
|
1256
1289
|
const synthPresets = presets.filter((p) => isSyntheticPreset(p));
|
|
1257
1290
|
const realPresets = presets.filter((p) => !isSyntheticPreset(p));
|
|
@@ -2431,7 +2464,32 @@ export function StudioGenerateSurface({
|
|
|
2431
2464
|
error={assist.assistError}
|
|
2432
2465
|
onDismissError={assist.dismissError}
|
|
2433
2466
|
onEnhance={() => void assist.runAssist("detail", prompt, setPrompt)}
|
|
2434
|
-
onGenerate={() =>
|
|
2467
|
+
onGenerate={() =>
|
|
2468
|
+
void (async () => {
|
|
2469
|
+
let fresh = "";
|
|
2470
|
+
await assist.runAssist("generate", prompt, (p) => {
|
|
2471
|
+
fresh = p;
|
|
2472
|
+
setPrompt(p);
|
|
2473
|
+
});
|
|
2474
|
+
if (!fresh.trim()) return;
|
|
2475
|
+
// NEGATIVE RIDES ALONG (operator 2026-08-13): generated
|
|
2476
|
+
// exclusions APPEND to any typed negative; std tick untouched.
|
|
2477
|
+
await assist.runNegative({ subject: fresh }, (neg) => {
|
|
2478
|
+
const cur = negative.trim().replace(/,\s*$/, "");
|
|
2479
|
+
setNegative(cur ? `${cur}, ${neg}` : neg);
|
|
2480
|
+
});
|
|
2481
|
+
// NAME RIDES ALONG: derive the project name from the fresh
|
|
2482
|
+
// prompt when blank or itself auto-derived.
|
|
2483
|
+
const t = project.trim();
|
|
2484
|
+
if (t === "" || t === autoProjectRef.current) {
|
|
2485
|
+
const derived = deriveNameFrom(fresh);
|
|
2486
|
+
if (derived) {
|
|
2487
|
+
autoProjectRef.current = derived;
|
|
2488
|
+
setProject(derived);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
})()
|
|
2492
|
+
}
|
|
2435
2493
|
canEnhance={prompt.trim() !== ""}
|
|
2436
2494
|
models={assist.assistModels}
|
|
2437
2495
|
model={assist.assistModel}
|
|
@@ -2488,6 +2546,100 @@ export function StudioGenerateSurface({
|
|
|
2488
2546
|
))}
|
|
2489
2547
|
</div>
|
|
2490
2548
|
)}
|
|
2549
|
+
|
|
2550
|
+
{/* SETTINGS EXPANDER (operator ask 2026-08-13): the clip prompt
|
|
2551
|
+
component hosts the FULL knob set, same as Scene/Movie/Cinema
|
|
2552
|
+
prompt components. Clip is a SINGLE-prompt surface, so these
|
|
2553
|
+
bind the SAME state as the left-column Settings tab — change a
|
|
2554
|
+
knob in either place and both update; comprehensive by
|
|
2555
|
+
construction (model pin included). The TEMPLATE chooser stays
|
|
2556
|
+
on the Settings tab only: it morphs the form shape (required
|
|
2557
|
+
inputs), it is not a render knob. */}
|
|
2558
|
+
<PromptCardSettings label="settings — model · size · steps · cfg · seed · budget">
|
|
2559
|
+
<div className="vi-knob vi-studio-knob-wide" style={{ minWidth: "16rem" }}>
|
|
2560
|
+
<label htmlFor="vi-clipcard-model">model pin</label>
|
|
2561
|
+
<select
|
|
2562
|
+
id="vi-clipcard-model"
|
|
2563
|
+
className="vi-knob-select"
|
|
2564
|
+
value={modelId}
|
|
2565
|
+
onChange={(e) => setModelId(e.target.value)}
|
|
2566
|
+
>
|
|
2567
|
+
<option value="">Auto — router picks by budget + resolution</option>
|
|
2568
|
+
{capModels.length > 0 && (
|
|
2569
|
+
<optgroup label={`Ratified for ${CAP_LABELS[capability]}`}>
|
|
2570
|
+
{capModels.map((id) => (
|
|
2571
|
+
<option key={id} value={id}>
|
|
2572
|
+
{id}
|
|
2573
|
+
</option>
|
|
2574
|
+
))}
|
|
2575
|
+
</optgroup>
|
|
2576
|
+
)}
|
|
2577
|
+
{(showAllModels || !renderLoaded || modelOffMenu) && (
|
|
2578
|
+
<optgroup label={renderLoaded ? "Other studio models — unverified for this capability" : "Studio models"}>
|
|
2579
|
+
{STUDIO_MODEL_IDS.filter((id) => !capModels.includes(id)).map((id) => (
|
|
2580
|
+
<option key={id} value={id}>
|
|
2581
|
+
{id}
|
|
2582
|
+
{renderLoaded ? " · unverified" : ""}
|
|
2583
|
+
</option>
|
|
2584
|
+
))}
|
|
2585
|
+
</optgroup>
|
|
2586
|
+
)}
|
|
2587
|
+
</select>
|
|
2588
|
+
</div>
|
|
2589
|
+
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2590
|
+
<label htmlFor="vi-clipcard-width">width</label>
|
|
2591
|
+
<input id="vi-clipcard-width" className="vi-knob-input" type="number" min={64} step={8}
|
|
2592
|
+
value={width} onChange={(e) => setWidth(Number(e.target.value) || 0)} />
|
|
2593
|
+
</div>
|
|
2594
|
+
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2595
|
+
<label htmlFor="vi-clipcard-height">height</label>
|
|
2596
|
+
<input id="vi-clipcard-height" className="vi-knob-input" type="number" min={64} step={8}
|
|
2597
|
+
value={height} onChange={(e) => setHeight(Number(e.target.value) || 0)} />
|
|
2598
|
+
</div>
|
|
2599
|
+
<div className="vi-knob" style={{ maxWidth: "6rem" }}>
|
|
2600
|
+
<label htmlFor="vi-clipcard-fps">fps</label>
|
|
2601
|
+
<input id="vi-clipcard-fps" className="vi-knob-input" type="number" min={1}
|
|
2602
|
+
value={fps} onChange={(e) => setFps(Number(e.target.value) || 0)} />
|
|
2603
|
+
</div>
|
|
2604
|
+
<div className="vi-knob" style={{ maxWidth: "11rem" }}>
|
|
2605
|
+
<label htmlFor="vi-clipcard-frames">length — frames</label>
|
|
2606
|
+
<input id="vi-clipcard-frames" className="vi-knob-input" type="number" min={1}
|
|
2607
|
+
value={requestedFrames} placeholder="model default (81)"
|
|
2608
|
+
onChange={(e) => setRequestedFrames(e.target.value)} />
|
|
2609
|
+
<span className="vi-knob-hint">
|
|
2610
|
+
{requestedFrames.trim() !== "" && Number(requestedFrames) > 0 && fps > 0
|
|
2611
|
+
? `\u2248 ${(Number(requestedFrames) / fps).toFixed(1)}s @ ${fps}fps`
|
|
2612
|
+
: `blank = model default \u2014 81f \u2248 ${fps > 0 ? (81 / fps).toFixed(1) : "?"}s @ ${fps}fps`}
|
|
2613
|
+
{" \u00b7 clamped to the model ceiling, snapped to 4k+1"}
|
|
2614
|
+
</span>
|
|
2615
|
+
</div>
|
|
2616
|
+
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2617
|
+
<label htmlFor="vi-clipcard-steps">steps</label>
|
|
2618
|
+
<input id="vi-clipcard-steps" className="vi-knob-input" type="number" min={1} max={100}
|
|
2619
|
+
value={steps} placeholder="model default" onChange={(e) => setSteps(e.target.value)} />
|
|
2620
|
+
</div>
|
|
2621
|
+
<div className="vi-knob" style={{ maxWidth: "7rem" }}>
|
|
2622
|
+
<label htmlFor="vi-clipcard-cfg">cfg</label>
|
|
2623
|
+
<input id="vi-clipcard-cfg" className="vi-knob-input" type="number" min={0} max={20} step={0.5}
|
|
2624
|
+
value={cfg} placeholder="model default" onChange={(e) => setCfg(e.target.value)} />
|
|
2625
|
+
</div>
|
|
2626
|
+
<div className="vi-knob" style={{ maxWidth: "8rem" }}>
|
|
2627
|
+
<label htmlFor="vi-clipcard-seed">seed</label>
|
|
2628
|
+
<input id="vi-clipcard-seed" className="vi-knob-input" type="number" min={0}
|
|
2629
|
+
value={seed} onChange={(e) => setSeed(Number(e.target.value) || 0)} />
|
|
2630
|
+
</div>
|
|
2631
|
+
<div className="vi-knob" style={{ maxWidth: "9rem" }}>
|
|
2632
|
+
<label htmlFor="vi-clipcard-vram">VRAM budget GB</label>
|
|
2633
|
+
<input id="vi-clipcard-vram" className="vi-knob-input" type="number" min={1}
|
|
2634
|
+
value={vramBudget} placeholder="autofit" onChange={(e) => setVramBudget(e.target.value)} />
|
|
2635
|
+
</div>
|
|
2636
|
+
<div className="vi-knob" style={{ maxWidth: "12rem" }}>
|
|
2637
|
+
<label htmlFor="vi-clipcard-project">project (archive name)</label>
|
|
2638
|
+
<input id="vi-clipcard-project" className="vi-knob-input" type="text"
|
|
2639
|
+
value={project} placeholder="optional" onChange={(e) => setProject(e.target.value)} />
|
|
2640
|
+
<WarnUnnamedToggle checked={warnUnnamed} onChange={setWarnUnnamed} />
|
|
2641
|
+
</div>
|
|
2642
|
+
</PromptCardSettings>
|
|
2491
2643
|
</PromptCard>
|
|
2492
2644
|
</ol>
|
|
2493
2645
|
</section>
|
|
@@ -2609,6 +2761,17 @@ export function StudioGenerateSurface({
|
|
|
2609
2761
|
</p>
|
|
2610
2762
|
)}
|
|
2611
2763
|
</div>
|
|
2764
|
+
{nameWarn && (
|
|
2765
|
+
<UnnamedWarnPanel
|
|
2766
|
+
noun="clip"
|
|
2767
|
+
onProceed={(name) => {
|
|
2768
|
+
setNameWarn(false);
|
|
2769
|
+
if (name) setProject(name);
|
|
2770
|
+
setPendingRun(true);
|
|
2771
|
+
}}
|
|
2772
|
+
onDismiss={() => setNameWarn(false)}
|
|
2773
|
+
/>
|
|
2774
|
+
)}
|
|
2612
2775
|
<div className="vi-station-footer-run">
|
|
2613
2776
|
<button
|
|
2614
2777
|
type="button"
|