@hugpy/video-intelligence-ui 0.1.1 → 0.3.0
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 -3
- package/src/config.ts +36 -0
- package/src/demo/assets/generate/cinema-cavern/movie.mp4 +0 -0
- package/src/demo/assets/generate/cinema-cavern/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/clips/cat-lounge.mp4 +0 -0
- package/src/demo/assets/generate/clips/drone-city.mp4 +0 -0
- package/src/demo/assets/generate/clips/paper-sails.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_02.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_03.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-desert/scene.mp4 +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-vintage/scene.mp4 +0 -0
- package/src/demo/assets/generate2/s0.png +0 -0
- package/src/demo/assets/generate2/s1.png +0 -0
- package/src/demo/assets/generate2/s10.png +0 -0
- package/src/demo/assets/generate2/s11.png +0 -0
- package/src/demo/assets/generate2/s12.png +0 -0
- package/src/demo/assets/generate2/s13.png +0 -0
- package/src/demo/assets/generate2/s14.png +0 -0
- package/src/demo/assets/generate2/s15.png +0 -0
- package/src/demo/assets/generate2/s16.png +0 -0
- package/src/demo/assets/generate2/s17.png +0 -0
- package/src/demo/assets/generate2/s18.png +0 -0
- package/src/demo/assets/generate2/s19.png +0 -0
- package/src/demo/assets/generate2/s2.png +0 -0
- package/src/demo/assets/generate2/s20.png +0 -0
- package/src/demo/assets/generate2/s21.mp4 +0 -0
- package/src/demo/assets/generate2/s22.mp4 +0 -0
- package/src/demo/assets/generate2/s23.mp4 +0 -0
- package/src/demo/assets/generate2/s24.mp4 +0 -0
- package/src/demo/assets/generate2/s25.mp4 +0 -0
- package/src/demo/assets/generate2/s26.mp4 +0 -0
- package/src/demo/assets/generate2/s27.mp4 +0 -0
- package/src/demo/assets/generate2/s28.mp4 +0 -0
- package/src/demo/assets/generate2/s29.mp4 +0 -0
- package/src/demo/assets/generate2/s3.png +0 -0
- package/src/demo/assets/generate2/s30.mp4 +0 -0
- package/src/demo/assets/generate2/s4.png +0 -0
- package/src/demo/assets/generate2/s5.png +0 -0
- package/src/demo/assets/generate2/s6.png +0 -0
- package/src/demo/assets/generate2/s7.png +0 -0
- package/src/demo/assets/generate2/s8.png +0 -0
- package/src/demo/assets/generate2/s9.png +0 -0
- package/src/demo/demoFetch.ts +126 -3
- package/src/demo/fixtures.ts +72 -4
- package/src/demo/promptAssist.ts +25 -0
- package/src/demo/sampleData.ts +960 -0
- package/src/demo/sampleSets.ts +172 -0
- package/src/stations/ActiveProcessesStation.tsx +18 -3
- package/src/stations/GenerateStation.tsx +901 -366
- package/src/stations/GoalComposer.tsx +9 -1
- package/src/stations/ScriptFirstStation.tsx +1081 -0
- package/src/stations/ToksPanel.tsx +188 -0
- package/src/stations/WorkbenchDrawer.tsx +4 -3
- package/src/stations/WorkbenchSidebar.tsx +6 -3
- package/src/stations/oracle/DagPanel.tsx +391 -0
- package/src/stations/oracle/OracleStation.tsx +304 -0
- package/src/stations/oracle/SpatialTab.tsx +185 -0
- package/src/stations/oracle/StewardPanel.tsx +165 -0
- package/src/stations/oracle/oracle.css +467 -0
- package/src/stations/oracle/oracleShared.tsx +160 -0
- package/src/stations/oracle/oracleTypes.ts +304 -0
- package/src/stations/oracle/useOracle.ts +560 -0
- package/src/stations/registry.ts +37 -0
- package/src/stations/scriptFirst.css +350 -0
- package/src/stations/studio/IdentitiesStation.tsx +219 -292
- package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
- package/src/stations/studio/StudioGenerateTab.tsx +10 -17
- package/src/stations/studio/StudioMovieComposer.tsx +296 -59
- package/src/stations/studio/StudioPlane.tsx +35 -3
- package/src/stations/studio/StudioViewer.tsx +116 -14
- package/src/stations/studio/coordinationBadge.tsx +206 -0
- package/src/stations/studio/identityUpload.ts +66 -0
- package/src/stations/studio/promptCard.tsx +362 -59
- package/src/stations/studio/useIdentityCreateJob.ts +184 -0
- package/src/stations/studio/useIdentityProfiles.ts +66 -0
- package/src/stations/useScriptFirstRun.ts +596 -0
- package/src/style/app.css +157 -0
- package/src/video/LengthRow.tsx +189 -0
- package/src/video/coordinationReport.ts +266 -0
- package/src/video/jobClock.ts +104 -0
- package/src/video/spreadAssist.ts +26 -0
- package/src/video/useMediaJobs.ts +26 -0
- package/src/video/useOperatorHistory.ts +5 -0
- package/src/video/useToks.ts +193 -0
|
@@ -55,8 +55,62 @@ export function composeNegative(negative: string, stdNegative: boolean): string
|
|
|
55
55
|
return terms.join(", ");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The standard set as individual lower-cased terms (the dedupe keys composeNegative uses). */
|
|
59
|
+
const STANDARD_NEGATIVE_TERMS: readonly string[] = STANDARD_NEGATIVE.split(",")
|
|
60
|
+
.map((t) => t.trim().replace(/\s+/g, " ").toLowerCase())
|
|
61
|
+
.filter((t) => t !== "");
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when EVERY standard term is present in `negative` (k93 — the explicit
|
|
65
|
+
* "std." tick reads its state from the visible field, so the checkbox can never
|
|
66
|
+
* claim something the wire won't carry).
|
|
67
|
+
*/
|
|
68
|
+
export function hasStandardNegative(negative: string): boolean {
|
|
69
|
+
const present = new Set(
|
|
70
|
+
negative
|
|
71
|
+
.split(",")
|
|
72
|
+
.map((t) => t.trim().replace(/\s+/g, " ").toLowerCase())
|
|
73
|
+
.filter((t) => t !== ""),
|
|
74
|
+
);
|
|
75
|
+
return STANDARD_NEGATIVE_TERMS.every((t) => present.has(t));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove exactly the standard terms from `negative`, leaving every other term
|
|
80
|
+
* (order preserved, whitespace normalized). The inverse of composeNegative(x, true).
|
|
81
|
+
*/
|
|
82
|
+
export function stripStandardNegative(negative: string): string {
|
|
83
|
+
const std = new Set(STANDARD_NEGATIVE_TERMS);
|
|
84
|
+
const kept: string[] = [];
|
|
85
|
+
for (const raw of negative.split(",")) {
|
|
86
|
+
const term = raw.trim().replace(/\s+/g, " ");
|
|
87
|
+
if (term === "") continue;
|
|
88
|
+
if (std.has(term.toLowerCase())) continue;
|
|
89
|
+
kept.push(term);
|
|
90
|
+
}
|
|
91
|
+
return kept.join(", ");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Explicit std. tick semantics: ticked ⇒ insert the standard set into the field; unticked ⇒ remove it. */
|
|
95
|
+
export function applyStandardNegative(negative: string, on: boolean): string {
|
|
96
|
+
return on ? composeNegative(negative, true) : stripStandardNegative(negative);
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
// ── PromptListToolbar ───────────────────────────────────────────────────────
|
|
59
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The k93 RANGE selector that replaces "Select all (n/m)" on the Scene toolbar:
|
|
103
|
+
* 1-based `[start] - [end]` over the text rows + a select-all tick. Optional —
|
|
104
|
+
* Movie/Cinema omit it and keep the original select-all label.
|
|
105
|
+
*/
|
|
106
|
+
export interface PromptListToolbarRange {
|
|
107
|
+
/** 1-based inclusive bounds of the CURRENT selection, or null when nothing is selected. */
|
|
108
|
+
start: number | null;
|
|
109
|
+
end: number | null;
|
|
110
|
+
/** The host re-selects rows `[start, end]` (already clamped by the toolbar). */
|
|
111
|
+
onChange: (start: number, end: number) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
/** The optional "how many rows" count input at the toolbar's left edge. */
|
|
61
115
|
export interface PromptListToolbarCount {
|
|
62
116
|
id: string;
|
|
@@ -64,7 +118,8 @@ export interface PromptListToolbarCount {
|
|
|
64
118
|
label: string;
|
|
65
119
|
value: number;
|
|
66
120
|
min?: number;
|
|
67
|
-
|
|
121
|
+
/** Omit for no upper bound. */
|
|
122
|
+
max?: number;
|
|
68
123
|
onChange: (n: number) => void;
|
|
69
124
|
title?: string;
|
|
70
125
|
}
|
|
@@ -95,6 +150,68 @@ export interface PromptListToolbarProps {
|
|
|
95
150
|
onStandardNegatives: () => void;
|
|
96
151
|
standardNegativesTitle?: string;
|
|
97
152
|
disabled?: boolean;
|
|
153
|
+
|
|
154
|
+
// ── k93 group layout (all OPTIONAL — Movie/Cinema pass none and render the
|
|
155
|
+
// original single-line toolbar byte-for-byte) ───────────────────────────────
|
|
156
|
+
/** Rendered on its own line ABOVE the selection line (the "prompt assist model" select). */
|
|
157
|
+
modelRow?: ReactNode;
|
|
158
|
+
/** When given, replaces "Select all (n/m)" with `[start] - [end] [ ] select all`. */
|
|
159
|
+
range?: PromptListToolbarRange;
|
|
160
|
+
/**
|
|
161
|
+
* The `negatives: [on/off]` toggle. When given, the separate "✨ Generate
|
|
162
|
+
* negatives" button is NOT rendered — the group actions write negatives
|
|
163
|
+
* themselves when the toggle is on (the host implements that rule).
|
|
164
|
+
*/
|
|
165
|
+
negativesOn?: boolean;
|
|
166
|
+
onNegativesOnChange?: (on: boolean) => void;
|
|
167
|
+
negativesOnTitle?: string;
|
|
168
|
+
/**
|
|
169
|
+
* The explicit `[ ] std.` checkbox (k93): ticked ⇒ the host INSERTS the
|
|
170
|
+
* standard set into every selected row's Negative field; unticked ⇒ removes
|
|
171
|
+
* it. When given, the "standard negatives" button is NOT rendered.
|
|
172
|
+
*/
|
|
173
|
+
stdChecked?: boolean;
|
|
174
|
+
onStdChange?: (on: boolean) => void;
|
|
175
|
+
stdTitle?: string;
|
|
176
|
+
/** The `✨ enhance (M selected)` group action; rendered only when `onEnhance` is given. */
|
|
177
|
+
enhanceLabel?: string;
|
|
178
|
+
enhanceTitle?: string;
|
|
179
|
+
onEnhance?: () => void;
|
|
180
|
+
/** Enhance is disabled when this is false (M = 0) — the host counts non-blank rows. */
|
|
181
|
+
canEnhance?: boolean;
|
|
182
|
+
/** Trailing content on the actions line (the `[+ video]` pretext attach). */
|
|
183
|
+
actionsExtra?: ReactNode;
|
|
184
|
+
/** Content rendered under the toolbar lines, full width (the expanded attach panel). */
|
|
185
|
+
below?: ReactNode;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Draft-tolerant count field: the host state is authoritative, but while the
|
|
189
|
+
* user is typing we show their raw draft — a controlled `value={count.value}`
|
|
190
|
+
* used to swallow any keystroke the host rejected (typing "10" past an old max,
|
|
191
|
+
* or clearing the field to retype, silently snapped back), which read as "the
|
|
192
|
+
* input only takes N digits". Valid integers commit on every keystroke; the
|
|
193
|
+
* draft resets to the host value on blur. */
|
|
194
|
+
function CountInput({ count, disabled }: { count: PromptListToolbarCount; disabled?: boolean }) {
|
|
195
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
196
|
+
return (
|
|
197
|
+
<input
|
|
198
|
+
id={count.id}
|
|
199
|
+
className="vi-knob-input"
|
|
200
|
+
type="number"
|
|
201
|
+
min={count.min ?? 1}
|
|
202
|
+
max={count.max}
|
|
203
|
+
value={draft ?? count.value}
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
title={count.title}
|
|
206
|
+
onChange={(e) => {
|
|
207
|
+
const raw = e.target.value;
|
|
208
|
+
setDraft(raw);
|
|
209
|
+
const n = Number(raw);
|
|
210
|
+
if (raw.trim() !== "" && Number.isInteger(n) && n >= (count.min ?? 1)) count.onChange(n);
|
|
211
|
+
}}
|
|
212
|
+
onBlur={() => setDraft(null)}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
98
215
|
}
|
|
99
216
|
|
|
100
217
|
export function PromptListToolbar({
|
|
@@ -115,70 +232,237 @@ export function PromptListToolbar({
|
|
|
115
232
|
onStandardNegatives,
|
|
116
233
|
standardNegativesTitle,
|
|
117
234
|
disabled = false,
|
|
235
|
+
modelRow,
|
|
236
|
+
range,
|
|
237
|
+
negativesOn,
|
|
238
|
+
onNegativesOnChange,
|
|
239
|
+
negativesOnTitle,
|
|
240
|
+
stdChecked,
|
|
241
|
+
onStdChange,
|
|
242
|
+
stdTitle,
|
|
243
|
+
enhanceLabel,
|
|
244
|
+
enhanceTitle,
|
|
245
|
+
onEnhance,
|
|
246
|
+
canEnhance = true,
|
|
247
|
+
actionsExtra,
|
|
248
|
+
below,
|
|
118
249
|
}: PromptListToolbarProps) {
|
|
119
250
|
const actionsDisabled = disabled || assistBusy || selectedCount === 0;
|
|
251
|
+
const grouped =
|
|
252
|
+
modelRow != null ||
|
|
253
|
+
range != null ||
|
|
254
|
+
onNegativesOnChange != null ||
|
|
255
|
+
onStdChange != null ||
|
|
256
|
+
onEnhance != null ||
|
|
257
|
+
actionsExtra != null ||
|
|
258
|
+
below != null;
|
|
259
|
+
|
|
260
|
+
// Clamp + drag rule (k93 §A.1): 1 ≤ start ≤ end ≤ itemCount; raising start above
|
|
261
|
+
// end drags end up; lowering end below start drags start down.
|
|
262
|
+
function setRangeStart(raw: string) {
|
|
263
|
+
if (!range || itemCount === 0) return;
|
|
264
|
+
const n = Number(raw);
|
|
265
|
+
if (!Number.isFinite(n)) return;
|
|
266
|
+
const s = Math.min(Math.max(1, Math.round(n)), itemCount);
|
|
267
|
+
const e = Math.max(range.end ?? s, s);
|
|
268
|
+
range.onChange(s, Math.min(e, itemCount));
|
|
269
|
+
}
|
|
270
|
+
function setRangeEnd(raw: string) {
|
|
271
|
+
if (!range || itemCount === 0) return;
|
|
272
|
+
const n = Number(raw);
|
|
273
|
+
if (!Number.isFinite(n)) return;
|
|
274
|
+
const e = Math.min(Math.max(1, Math.round(n)), itemCount);
|
|
275
|
+
const s = Math.min(range.start ?? e, e);
|
|
276
|
+
range.onChange(Math.max(s, 1), e);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const countKnob = count && (
|
|
280
|
+
<div className="vi-knob vi-prompt-toolbar-count">
|
|
281
|
+
<label htmlFor={count.id}>{count.label}</label>
|
|
282
|
+
<CountInput count={count} disabled={disabled} />
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const selectAll = (
|
|
287
|
+
<label className="vi-comfy-hint vi-prompt-toolbar-selectall">
|
|
288
|
+
<input
|
|
289
|
+
type="checkbox"
|
|
290
|
+
checked={allSelected}
|
|
291
|
+
disabled={disabled || itemCount === 0}
|
|
292
|
+
onChange={onToggleSelectAll}
|
|
293
|
+
aria-label={`Select every ${noun}`}
|
|
294
|
+
/>
|
|
295
|
+
{range ? "select all" : `Select all (${selectedCount}/${itemCount})`}
|
|
296
|
+
</label>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const generateBtn = (
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
className="vi-btn vi-btn-sm vi-btn-accent"
|
|
303
|
+
disabled={actionsDisabled}
|
|
304
|
+
title={selectedCount === 0 ? (generateDisabledTitle ?? generateTitle) : generateTitle}
|
|
305
|
+
onClick={onGenerate}
|
|
306
|
+
>
|
|
307
|
+
{generateLabel}
|
|
308
|
+
</button>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (!grouped) {
|
|
312
|
+
return (
|
|
313
|
+
<div
|
|
314
|
+
className="vi-prompt-toolbar"
|
|
315
|
+
role="group"
|
|
316
|
+
aria-label={`${noun} count and group prompt assist`}
|
|
317
|
+
>
|
|
318
|
+
{countKnob}
|
|
319
|
+
{selectAll}
|
|
320
|
+
{generateBtn}
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
324
|
+
disabled={actionsDisabled}
|
|
325
|
+
title={negativesTitle}
|
|
326
|
+
onClick={onNegatives}
|
|
327
|
+
>
|
|
328
|
+
{negativesLabel}
|
|
329
|
+
</button>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
333
|
+
disabled={disabled || itemCount === 0}
|
|
334
|
+
title={
|
|
335
|
+
standardNegativesTitle ??
|
|
336
|
+
"Tick the [standard negative] checkbox on for the selected rows (or every row when none are selected)."
|
|
337
|
+
}
|
|
338
|
+
onClick={onStandardNegatives}
|
|
339
|
+
>
|
|
340
|
+
standard negatives
|
|
341
|
+
</button>
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── k93 three-line group layout ──────────────────────────────────────────
|
|
347
|
+
// prompt assist model [select]
|
|
348
|
+
// [start] - [end] [ ] select all negatives: [on/off] [ ] std.
|
|
349
|
+
// (✨ generate (N selected)) (✨ enhance (M selected)) [+ video]
|
|
120
350
|
return (
|
|
121
351
|
<div
|
|
122
|
-
className="vi-prompt-toolbar"
|
|
352
|
+
className="vi-prompt-toolbar vi-prompt-toolbar-group"
|
|
123
353
|
role="group"
|
|
124
354
|
aria-label={`${noun} count and group prompt assist`}
|
|
125
355
|
>
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
356
|
+
{modelRow && <div className="vi-prompt-toolbar-line">{modelRow}</div>}
|
|
357
|
+
<div className="vi-prompt-toolbar-line">
|
|
358
|
+
{countKnob}
|
|
359
|
+
{range && (
|
|
360
|
+
<span className="vi-prompt-toolbar-range" title={`1-based ${noun} range the group actions target (text ${noun}s only).`}>
|
|
361
|
+
<input
|
|
362
|
+
type="number"
|
|
363
|
+
className="vi-knob-input vi-prompt-toolbar-range-input"
|
|
364
|
+
min={1}
|
|
365
|
+
max={Math.max(1, itemCount)}
|
|
366
|
+
value={range.start ?? ""}
|
|
367
|
+
placeholder="start"
|
|
368
|
+
disabled={disabled || itemCount === 0 || allSelected}
|
|
369
|
+
aria-label={`First ${noun} of the selection range`}
|
|
370
|
+
onChange={(e) => setRangeStart(e.target.value)}
|
|
371
|
+
/>
|
|
372
|
+
<span aria-hidden="true">-</span>
|
|
373
|
+
<input
|
|
374
|
+
type="number"
|
|
375
|
+
className="vi-knob-input vi-prompt-toolbar-range-input"
|
|
376
|
+
min={1}
|
|
377
|
+
max={Math.max(1, itemCount)}
|
|
378
|
+
value={range.end ?? ""}
|
|
379
|
+
placeholder="end"
|
|
380
|
+
disabled={disabled || itemCount === 0 || allSelected}
|
|
381
|
+
aria-label={`Last ${noun} of the selection range`}
|
|
382
|
+
onChange={(e) => setRangeEnd(e.target.value)}
|
|
383
|
+
/>
|
|
384
|
+
</span>
|
|
385
|
+
)}
|
|
386
|
+
{selectAll}
|
|
387
|
+
{onNegativesOnChange ? (
|
|
388
|
+
<label
|
|
389
|
+
className="vi-comfy-hint vi-prompt-toolbar-neg"
|
|
390
|
+
title={
|
|
391
|
+
negativesOnTitle ??
|
|
392
|
+
"On: group generate / enhance also write each selected row's Negative field. Off: prompts only."
|
|
393
|
+
}
|
|
394
|
+
>
|
|
395
|
+
negatives:
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
className={`vi-btn vi-btn-sm vi-toggle${negativesOn ? " vi-toggle-on" : ""}`}
|
|
399
|
+
role="switch"
|
|
400
|
+
aria-checked={!!negativesOn}
|
|
401
|
+
disabled={disabled}
|
|
402
|
+
onClick={() => onNegativesOnChange(!negativesOn)}
|
|
403
|
+
>
|
|
404
|
+
{negativesOn ? "on" : "off"}
|
|
405
|
+
</button>
|
|
406
|
+
</label>
|
|
407
|
+
) : (
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
411
|
+
disabled={actionsDisabled}
|
|
412
|
+
title={negativesTitle}
|
|
413
|
+
onClick={onNegatives}
|
|
414
|
+
>
|
|
415
|
+
{negativesLabel}
|
|
416
|
+
</button>
|
|
417
|
+
)}
|
|
418
|
+
{onStdChange ? (
|
|
419
|
+
<label
|
|
420
|
+
className="vi-comfy-hint vi-prompt-toolbar-std"
|
|
421
|
+
title={
|
|
422
|
+
stdTitle ??
|
|
423
|
+
"Tick: insert the standard exclusion set into every selected row's Negative field (visible, sent as-is). Untick: remove exactly those terms."
|
|
424
|
+
}
|
|
425
|
+
>
|
|
426
|
+
<input
|
|
427
|
+
type="checkbox"
|
|
428
|
+
checked={!!stdChecked}
|
|
429
|
+
disabled={disabled || selectedCount === 0}
|
|
430
|
+
onChange={(e) => onStdChange(e.target.checked)}
|
|
431
|
+
aria-label="Insert the standard negative into the selected rows"
|
|
432
|
+
/>
|
|
433
|
+
std.
|
|
434
|
+
</label>
|
|
435
|
+
) : (
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
439
|
+
disabled={disabled || itemCount === 0}
|
|
440
|
+
title={
|
|
441
|
+
standardNegativesTitle ??
|
|
442
|
+
"Tick the [standard negative] checkbox on for the selected rows (or every row when none are selected)."
|
|
443
|
+
}
|
|
444
|
+
onClick={onStandardNegatives}
|
|
445
|
+
>
|
|
446
|
+
standard negatives
|
|
447
|
+
</button>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
<div className="vi-prompt-toolbar-line">
|
|
451
|
+
{generateBtn}
|
|
452
|
+
{onEnhance && (
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
className="vi-btn vi-btn-sm vi-btn-ghost"
|
|
456
|
+
disabled={actionsDisabled || !canEnhance}
|
|
457
|
+
title={enhanceTitle}
|
|
458
|
+
onClick={onEnhance}
|
|
459
|
+
>
|
|
460
|
+
{enhanceLabel ?? "✨ enhance"}
|
|
461
|
+
</button>
|
|
462
|
+
)}
|
|
463
|
+
{actionsExtra}
|
|
464
|
+
</div>
|
|
465
|
+
{below}
|
|
182
466
|
</div>
|
|
183
467
|
);
|
|
184
468
|
}
|
|
@@ -349,6 +633,18 @@ export interface PromptFieldTabsProps {
|
|
|
349
633
|
negativeExtras?: ReactNode;
|
|
350
634
|
/** Rendered under the prompt pane when it is active. */
|
|
351
635
|
promptExtras?: ReactNode;
|
|
636
|
+
/**
|
|
637
|
+
* k93: render the editor labels ("prompt" / "negative prompt (optional)")
|
|
638
|
+
* visually hidden — the tabs already say it. aria-labels are kept.
|
|
639
|
+
*/
|
|
640
|
+
hideLabels?: boolean;
|
|
641
|
+
/**
|
|
642
|
+
* k93: omit the tab-strip [standard negative] tick (the host renders an
|
|
643
|
+
* explicit std. checkbox of its own in its assist row).
|
|
644
|
+
*/
|
|
645
|
+
hideStdTick?: boolean;
|
|
646
|
+
/** Tooltip on the Negative tab (Scene: "part #1's field is also the group negative"). */
|
|
647
|
+
negativeTabTitle?: string;
|
|
352
648
|
}
|
|
353
649
|
|
|
354
650
|
export function PromptFieldTabs({
|
|
@@ -370,6 +666,9 @@ export function PromptFieldTabs({
|
|
|
370
666
|
disabled = false,
|
|
371
667
|
negativeExtras,
|
|
372
668
|
promptExtras,
|
|
669
|
+
hideLabels = false,
|
|
670
|
+
hideStdTick = false,
|
|
671
|
+
negativeTabTitle,
|
|
373
672
|
}: PromptFieldTabsProps) {
|
|
374
673
|
// Ephemeral VIEW state only — which pane shows. All editor data rides props.
|
|
375
674
|
const [tab, setTab] = useState<"prompt" | "negative">("prompt");
|
|
@@ -393,13 +692,14 @@ export function PromptFieldTabs({
|
|
|
393
692
|
className="vi-prompt-tab"
|
|
394
693
|
aria-selected={active === "negative"}
|
|
395
694
|
disabled={negativeLocked}
|
|
396
|
-
title={negativeDisabledNote}
|
|
695
|
+
title={negativeDisabledNote ?? negativeTabTitle}
|
|
397
696
|
onClick={() => setTab("negative")}
|
|
398
697
|
>
|
|
399
698
|
Negative
|
|
400
699
|
{/* The content marker — tab state must not hide a written negative. */}
|
|
401
700
|
{negative.trim() !== "" && <span className="vi-prompt-tab-dot" aria-label="has content" />}
|
|
402
701
|
</button>
|
|
702
|
+
{!hideStdTick && (
|
|
403
703
|
<label
|
|
404
704
|
className="vi-comfy-hint vi-prompt-tab-std"
|
|
405
705
|
title={
|
|
@@ -415,12 +715,14 @@ export function PromptFieldTabs({
|
|
|
415
715
|
/>
|
|
416
716
|
standard negative
|
|
417
717
|
</label>
|
|
718
|
+
)}
|
|
418
719
|
</div>
|
|
419
720
|
{active === "prompt" ? (
|
|
420
721
|
<>
|
|
421
722
|
<GoalComposer
|
|
422
723
|
id={`${idPrefix}-prompt`}
|
|
423
724
|
label={promptLabel}
|
|
725
|
+
hideLabel={hideLabels}
|
|
424
726
|
value={prompt}
|
|
425
727
|
placeholder={promptPlaceholder}
|
|
426
728
|
onChange={onPromptChange}
|
|
@@ -435,6 +737,7 @@ export function PromptFieldTabs({
|
|
|
435
737
|
<GoalComposer
|
|
436
738
|
id={`${idPrefix}-negative`}
|
|
437
739
|
label={negativeLabel}
|
|
740
|
+
hideLabel={hideLabels}
|
|
438
741
|
value={negative}
|
|
439
742
|
placeholder={negativePlaceholder}
|
|
440
743
|
onChange={onNegativeChange}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// k94 — the ONE-PATH "Create identity" job watcher.
|
|
2
|
+
//
|
|
3
|
+
// Both create routes (from-video → identity_from_video, from-images →
|
|
4
|
+
// identity_mesh_build) hand back a bus job_id; this hook polls the generic
|
|
5
|
+
// GET /video/jobs/<id> (`videoJobUrl`) the way the other identity flows in this
|
|
6
|
+
// station do, and reduces the reply to what the panel shows on ONE line:
|
|
7
|
+
// stage · % · log tail
|
|
8
|
+
// `progress` is the relay's mirrored blob ({stage, progress 0..1, log_tail[]}
|
|
9
|
+
// — additive fields an older render service may omit), `progress_ratio` is the
|
|
10
|
+
// bus's numeric bar, `result` is the terminal JobResult ({ok, error?,
|
|
11
|
+
// identities?}). Terminal = `result.ok` is a boolean. Bounded (90 min — char360
|
|
12
|
+
// plus a textured Hunyuan3D mesh PER character runs minutes each; clownworld's
|
|
13
|
+
// own ceiling) so a wedged remote never spins the panel forever.
|
|
14
|
+
//
|
|
15
|
+
// Why not the session job tracker (video/jobTracker.ts)? Its JobKind/StationId
|
|
16
|
+
// unions + per-kind terminal parse are closed over the crop/frames/generate
|
|
17
|
+
// arms; widening them is a cross-station change outside this task's files. The
|
|
18
|
+
// poll shape here is the same one ExtractFromVideoPanel / generateFullIdentity
|
|
19
|
+
// already use in this station.
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { request, okValue } from "../../transport/client";
|
|
22
|
+
import { videoJobUrl } from "../../config";
|
|
23
|
+
|
|
24
|
+
export type CreateJobPhase = "idle" | "queued" | "running" | "done" | "error";
|
|
25
|
+
|
|
26
|
+
export interface CreatedIdentity {
|
|
27
|
+
char?: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
glb?: boolean;
|
|
31
|
+
n_views?: number;
|
|
32
|
+
canonical?: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreateJobView {
|
|
37
|
+
phase: CreateJobPhase;
|
|
38
|
+
jobId: string | null;
|
|
39
|
+
/** The remote stage ("detect", "mesh", …) or the bus's current_stage. */
|
|
40
|
+
stage: string | null;
|
|
41
|
+
/** 0..100 when the job reports anything measurable, else null. */
|
|
42
|
+
pct: number | null;
|
|
43
|
+
/** The last line of the relay's mirrored log tail. */
|
|
44
|
+
logLine: string | null;
|
|
45
|
+
/** Human error text on failure (enqueue or job). */
|
|
46
|
+
error: string | null;
|
|
47
|
+
/** from-video: the profiles the job minted (empty for from-images). */
|
|
48
|
+
identities: CreatedIdentity[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const IDLE: CreateJobView = {
|
|
52
|
+
phase: "idle", jobId: null, stage: null, pct: null, logLine: null, error: null, identities: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const POLL_MS = 4000;
|
|
56
|
+
const CAP_MS = 90 * 60 * 1000;
|
|
57
|
+
|
|
58
|
+
type JobReply = {
|
|
59
|
+
status?: string | null;
|
|
60
|
+
progress?: { stage?: unknown; progress?: unknown; log_tail?: unknown; message?: unknown } | null;
|
|
61
|
+
progress_ratio?: number | null;
|
|
62
|
+
current_stage?: string | null;
|
|
63
|
+
result?: {
|
|
64
|
+
ok?: boolean;
|
|
65
|
+
error?: { message?: string } | null;
|
|
66
|
+
identities?: { profiles?: CreatedIdentity[] } | null;
|
|
67
|
+
} | null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export interface CreateJobApi {
|
|
71
|
+
view: CreateJobView;
|
|
72
|
+
/** Start watching a job; `onDone(identities)` fires once on success. */
|
|
73
|
+
watch: (jobId: string, onDone?: (ids: CreatedIdentity[]) => void) => void;
|
|
74
|
+
/** Surface an enqueue-time failure on the same line. */
|
|
75
|
+
fail: (message: string) => void;
|
|
76
|
+
reset: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useIdentityCreateJob(): CreateJobApi {
|
|
80
|
+
const [view, setView] = useState<CreateJobView>(IDLE);
|
|
81
|
+
const timer = useRef<number | null>(null);
|
|
82
|
+
const active = useRef(false);
|
|
83
|
+
|
|
84
|
+
const stop = useCallback(() => {
|
|
85
|
+
active.current = false;
|
|
86
|
+
if (timer.current != null) {
|
|
87
|
+
window.clearTimeout(timer.current);
|
|
88
|
+
timer.current = null;
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => stop, [stop]);
|
|
93
|
+
|
|
94
|
+
const watch = useCallback<CreateJobApi["watch"]>(
|
|
95
|
+
(jobId, onDone) => {
|
|
96
|
+
stop();
|
|
97
|
+
active.current = true;
|
|
98
|
+
setView({ ...IDLE, phase: "queued", jobId });
|
|
99
|
+
const deadline = Date.now() + CAP_MS;
|
|
100
|
+
|
|
101
|
+
const pollOnce = async () => {
|
|
102
|
+
if (!active.current) return;
|
|
103
|
+
if (Date.now() > deadline) {
|
|
104
|
+
stop();
|
|
105
|
+
setView((v) => ({ ...v, phase: "error", error: "Timed out waiting for the identity build." }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const res = await request<unknown>(videoJobUrl(jobId), {
|
|
109
|
+
meta: { specKey: "studio", operation: "identity.create.status" },
|
|
110
|
+
});
|
|
111
|
+
if (!active.current) return;
|
|
112
|
+
if (res.ok) {
|
|
113
|
+
const job = okValue(res) as JobReply;
|
|
114
|
+
const result = job.result;
|
|
115
|
+
if (result && typeof result.ok === "boolean") {
|
|
116
|
+
stop();
|
|
117
|
+
if (result.ok) {
|
|
118
|
+
const ids = (result.identities && Array.isArray(result.identities.profiles)
|
|
119
|
+
? result.identities.profiles
|
|
120
|
+
: []).filter((p) => p && typeof p.slug === "string");
|
|
121
|
+
setView((v) => ({ ...v, phase: "done", pct: 100, identities: ids, error: null }));
|
|
122
|
+
if (onDone) onDone(ids);
|
|
123
|
+
} else {
|
|
124
|
+
setView((v) => ({
|
|
125
|
+
...v,
|
|
126
|
+
phase: "error",
|
|
127
|
+
error: (result.error && result.error.message) || "The identity build failed.",
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const blob = job.progress || {};
|
|
133
|
+
const stage =
|
|
134
|
+
(typeof blob.stage === "string" && blob.stage) ||
|
|
135
|
+
(typeof blob.message === "string" && blob.message) ||
|
|
136
|
+
job.current_stage ||
|
|
137
|
+
null;
|
|
138
|
+
const ratio =
|
|
139
|
+
typeof job.progress_ratio === "number"
|
|
140
|
+
? job.progress_ratio
|
|
141
|
+
: typeof blob.progress === "number"
|
|
142
|
+
? blob.progress
|
|
143
|
+
: null;
|
|
144
|
+
const tail = Array.isArray(blob.log_tail) ? blob.log_tail : [];
|
|
145
|
+
const last = tail.length ? tail[tail.length - 1] : null;
|
|
146
|
+
setView((v) => ({
|
|
147
|
+
...v,
|
|
148
|
+
phase: job.status === "running" ? "running" : "queued",
|
|
149
|
+
stage,
|
|
150
|
+
pct: ratio == null ? null : Math.max(0, Math.min(100, Math.round(ratio * 100))),
|
|
151
|
+
logLine: typeof last === "string" ? last : null,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
// queued / running / transient poll failure → keep polling.
|
|
155
|
+
timer.current = window.setTimeout(() => void pollOnce(), POLL_MS);
|
|
156
|
+
};
|
|
157
|
+
timer.current = window.setTimeout(() => void pollOnce(), POLL_MS);
|
|
158
|
+
},
|
|
159
|
+
[stop],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const fail = useCallback<CreateJobApi["fail"]>((message) => {
|
|
163
|
+
stop();
|
|
164
|
+
setView({ ...IDLE, phase: "error", error: message });
|
|
165
|
+
}, [stop]);
|
|
166
|
+
|
|
167
|
+
const reset = useCallback(() => {
|
|
168
|
+
stop();
|
|
169
|
+
setView(IDLE);
|
|
170
|
+
}, [stop]);
|
|
171
|
+
|
|
172
|
+
return { view, watch, fail, reset };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The one-line progress text: "stage · 42% · last log line". */
|
|
176
|
+
export function describeCreateJob(v: CreateJobView): string | null {
|
|
177
|
+
if (v.phase === "idle") return null;
|
|
178
|
+
if (v.phase === "error") return v.error || "failed";
|
|
179
|
+
if (v.phase === "done") return "done";
|
|
180
|
+
const parts: string[] = [v.phase === "queued" && !v.stage ? "queued" : (v.stage || "running")];
|
|
181
|
+
if (v.pct != null) parts.push(`${v.pct}%`);
|
|
182
|
+
if (v.logLine) parts.push(v.logLine);
|
|
183
|
+
return parts.join(" · ");
|
|
184
|
+
}
|