@hugpy/video-intelligence-ui 0.3.2 → 0.3.3

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.
Files changed (148) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +9 -0
  3. package/dist/assets/hugpy-mark-BXktFcOQ.png +0 -0
  4. package/dist/assets/index-DuT1AMLn.css +1 -0
  5. package/dist/assets/index-_13ZiBDP.js +4191 -0
  6. package/dist/index.html +71 -0
  7. package/entry.tsx +0 -0
  8. package/index.html +4 -4
  9. package/package.json +42 -4
  10. package/src/assets/hugpy-mark.png +0 -0
  11. package/src/config.ts +0 -0
  12. package/src/demo/DemoBanner.tsx +0 -0
  13. package/src/demo/demo.css +0 -0
  14. package/src/demo/demoFetch.ts +0 -0
  15. package/src/demo/fixtures.ts +0 -0
  16. package/src/demo/identityFixture.ts +0 -0
  17. package/src/demo/index.ts +0 -0
  18. package/src/demo/mediaBase.ts +0 -0
  19. package/src/demo/mode.ts +0 -0
  20. package/src/demo/promptAssist.ts +0 -0
  21. package/src/demo/sampleData.ts +0 -0
  22. package/src/demo/sampleSets.ts +0 -0
  23. package/src/demo/seed.ts +0 -0
  24. package/src/nav/BrandMark.css +0 -0
  25. package/src/nav/BrandMark.tsx +0 -0
  26. package/src/nav/Navbar.css +0 -0
  27. package/src/nav/Navbar.tsx +6 -1
  28. package/src/regions/SpatialRegionEditor.tsx +0 -0
  29. package/src/regions/TemporalRegionEditor.tsx +0 -0
  30. package/src/regions/types.ts +0 -0
  31. package/src/session.ts +0 -0
  32. package/src/share.ts +0 -0
  33. package/src/stations/ActivePanel.tsx +0 -0
  34. package/src/stations/ActiveProcessesStation.tsx +0 -0
  35. package/src/stations/AddFileBar.tsx +0 -0
  36. package/src/stations/AudioCropStation.tsx +0 -0
  37. package/src/stations/ComfyPanel.tsx +0 -0
  38. package/src/stations/ComfyStation.tsx +0 -0
  39. package/src/stations/CondensedKnobStrip.tsx +0 -0
  40. package/src/stations/ConsolePanel.tsx +0 -0
  41. package/src/stations/FrameExtractStation.tsx +0 -0
  42. package/src/stations/GenIdentityBar.tsx +0 -0
  43. package/src/stations/GenerateStation.tsx +2 -1
  44. package/src/stations/GoalComposer.tsx +0 -0
  45. package/src/stations/ImageCropStation.tsx +0 -0
  46. package/src/stations/KnobInfo.tsx +0 -0
  47. package/src/stations/PlaceholderStation.tsx +0 -0
  48. package/src/stations/ScriptFirstStation.tsx +4 -0
  49. package/src/stations/SectionTabs.tsx +0 -0
  50. package/src/stations/SharePanel.tsx +0 -0
  51. package/src/stations/SidebarRegistry.tsx +0 -0
  52. package/src/stations/StationShell.tsx +0 -0
  53. package/src/stations/StudioClipsStation.tsx +0 -0
  54. package/src/stations/TemplateStepsPanel.tsx +190 -0
  55. package/src/stations/ToksPanel.tsx +0 -0
  56. package/src/stations/WorkbenchDrawer.tsx +0 -0
  57. package/src/stations/WorkbenchSidebar.tsx +0 -0
  58. package/src/stations/WorkbenchStation.tsx +0 -0
  59. package/src/stations/comfyEmbed.ts +0 -0
  60. package/src/stations/drawerShell.tsx +0 -0
  61. package/src/stations/ops/AudioCropCore.tsx +0 -0
  62. package/src/stations/ops/CharacterGroupsPanel.tsx +0 -0
  63. package/src/stations/ops/FrameExtractCore.tsx +0 -0
  64. package/src/stations/ops/ImageCropCore.tsx +0 -0
  65. package/src/stations/ops/VideoCropCore.tsx +0 -0
  66. package/src/stations/ops/useVideoCropJobs.ts +0 -0
  67. package/src/stations/oracle/DagPanel.tsx +0 -0
  68. package/src/stations/oracle/OracleStation.tsx +2 -0
  69. package/src/stations/oracle/PerformanceComposer.tsx +119 -0
  70. package/src/stations/oracle/SpatialTab.tsx +0 -0
  71. package/src/stations/oracle/StewardPanel.tsx +0 -0
  72. package/src/stations/oracle/oracle.css +33 -0
  73. package/src/stations/oracle/oracleShared.tsx +0 -0
  74. package/src/stations/oracle/oracleTypes.ts +0 -0
  75. package/src/stations/oracle/useOracle.ts +0 -0
  76. package/src/stations/pollCaps.ts +0 -0
  77. package/src/stations/registry.ts +0 -0
  78. package/src/stations/scriptFirst.css +23 -0
  79. package/src/stations/studio/AngleRingEditor.tsx +0 -0
  80. package/src/stations/studio/IdentitiesStation.tsx +21 -2
  81. package/src/stations/studio/IdentityCreatePanel.tsx +0 -0
  82. package/src/stations/studio/IdentityProfileControls.tsx +0 -0
  83. package/src/stations/studio/StudioActiveProcesses.tsx +0 -0
  84. package/src/stations/studio/StudioAssistLogPanel.tsx +0 -0
  85. package/src/stations/studio/StudioGenerateMode.tsx +0 -0
  86. package/src/stations/studio/StudioGenerateTab.tsx +20 -2
  87. package/src/stations/studio/StudioItemCard.tsx +0 -0
  88. package/src/stations/studio/StudioLibraryTab.tsx +0 -0
  89. package/src/stations/studio/StudioMovieComposer.tsx +0 -0
  90. package/src/stations/studio/StudioMovieSessions.tsx +12 -19
  91. package/src/stations/studio/StudioPlane.tsx +25 -4
  92. package/src/stations/studio/StudioSplicedRow.tsx +0 -0
  93. package/src/stations/studio/StudioViewer.tsx +0 -0
  94. package/src/stations/studio/TurntableViewer.tsx +0 -0
  95. package/src/stations/studio/coordinationBadge.tsx +0 -0
  96. package/src/stations/studio/identityUpload.ts +0 -0
  97. package/src/stations/studio/movieTimeline.ts +0 -0
  98. package/src/stations/studio/promptCard.tsx +0 -0
  99. package/src/stations/studio/studioItem.ts +0 -0
  100. package/src/stations/studio/studioShared.tsx +0 -0
  101. package/src/stations/studio/useIdentityCreateJob.ts +0 -0
  102. package/src/stations/studio/useIdentityProfiles.ts +0 -0
  103. package/src/stations/studio/useReconstruction.ts +0 -0
  104. package/src/stations/types.ts +0 -0
  105. package/src/stations/useAudioCropJobs.ts +0 -0
  106. package/src/stations/useAudioExtractJob.ts +0 -0
  107. package/src/stations/useCropJobs.ts +0 -0
  108. package/src/stations/useFrameJobs.ts +0 -0
  109. package/src/stations/useGenerateJob.ts +0 -0
  110. package/src/stations/useGenerateMovieJob.ts +0 -0
  111. package/src/stations/useGenerateRowJobs.ts +0 -0
  112. package/src/stations/useGenerateSceneJob.ts +0 -0
  113. package/src/stations/useGenerateSceneRowJobs.ts +0 -0
  114. package/src/stations/useScriptFirstRun.ts +0 -0
  115. package/src/style/app.css +49 -0
  116. package/src/transport/client.ts +0 -0
  117. package/src/transport/telemetry.ts +0 -0
  118. package/src/video/DropReceptacle.tsx +0 -0
  119. package/src/video/LengthRow.tsx +0 -0
  120. package/src/video/LibraryGroups.tsx +0 -0
  121. package/src/video/PromptAssistButtons.tsx +6 -4
  122. package/src/video/UnnamedWarn.tsx +0 -0
  123. package/src/video/composerBridge.ts +0 -0
  124. package/src/video/contract.ts +0 -0
  125. package/src/video/coordinationReport.ts +0 -0
  126. package/src/video/jobClock.ts +0 -0
  127. package/src/video/jobTracker.ts +0 -0
  128. package/src/video/mediaLibrary.ts +0 -0
  129. package/src/video/renderCompat.ts +0 -0
  130. package/src/video/sessionForm.ts +0 -0
  131. package/src/video/spreadAssist.ts +0 -0
  132. package/src/video/studioBridge.ts +0 -0
  133. package/src/video/useMediaJobs.ts +0 -0
  134. package/src/video/useModels.ts +0 -0
  135. package/src/video/useMoviePresets.ts +0 -0
  136. package/src/video/useMovieSessions.ts +0 -0
  137. package/src/video/useOperatorHistory.ts +0 -0
  138. package/src/video/usePresets.ts +0 -0
  139. package/src/video/useProjects.ts +0 -0
  140. package/src/video/usePromptAssist.ts +65 -23
  141. package/src/video/useRenderPresets.ts +0 -0
  142. package/src/video/useStudioAssistLog.ts +0 -0
  143. package/src/video/useStudioPresets.ts +0 -0
  144. package/src/video/useToks.ts +0 -0
  145. package/src/video/useWorkerCalls.ts +0 -0
  146. package/tsconfig.json +0 -0
  147. package/vite.config.ts +0 -0
  148. package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
@@ -0,0 +1,119 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import { hugpyConfig } from "../../config";
4
+ import { describeAppError, errorOf, okValue, request } from "../../transport/client";
5
+
6
+ type EnqueueResult = { job_id?: string; error?: string };
7
+ type PerformanceProbe = { ready: boolean; bound?: string[]; unbound?: Array<{ seam: string; requirement: string }> };
8
+ type Stage = "authority" | "snapshot" | "audio" | "lock" | "segments" | "keyframes" | "clips" | "assembly";
9
+
10
+ /** The small, explicit entry point for the Oracle's durable performance job. */
11
+ export function PerformanceComposer({ onEnqueued }: { onEnqueued: (jobId: string) => void }) {
12
+ const [prompt, setPrompt] = useState("");
13
+ const [dialogue, setDialogue] = useState("");
14
+ const [speaker, setSpeaker] = useState("narrator");
15
+ const [voiceId, setVoiceId] = useState("narrator");
16
+ const [rawRef, setRawRef] = useState("");
17
+ const [quality, setQuality] = useState("balanced");
18
+ const [stopAfter, setStopAfter] = useState<Stage | "">("segments");
19
+ const [tts, setTts] = useState(3);
20
+ const [keyframes, setKeyframes] = useState(3);
21
+ const [clips, setClips] = useState(3);
22
+ const [maxSeconds, setMaxSeconds] = useState("");
23
+ const [seed, setSeed] = useState(0);
24
+ const [negative, setNegative] = useState("");
25
+ const [busy, setBusy] = useState(false);
26
+ const [message, setMessage] = useState("");
27
+ const [probe, setProbe] = useState<PerformanceProbe | null>(null);
28
+ const [probeGap, setProbeGap] = useState("");
29
+
30
+ useEffect(() => {
31
+ let active = true;
32
+ void (async () => {
33
+ const result = await request<PerformanceProbe>(`${hugpyConfig.apiBase}/video/performance/probe`, {
34
+ meta: { specKey: "oracle", operation: "oracle.performance.probe" },
35
+ });
36
+ if (!active) return;
37
+ if (result.ok) setProbe(okValue(result));
38
+ else setProbeGap(describeAppError(errorOf(result)));
39
+ })();
40
+ return () => { active = false; };
41
+ }, []);
42
+
43
+ async function enqueue() {
44
+ const lines = dialogue.split("\n").map((text) => text.trim()).filter(Boolean);
45
+ if (!prompt.trim() || !lines.length || !speaker.trim() || !voiceId.trim() || !rawRef.trim()) {
46
+ setMessage("Add the goal, at least one dialogue line, speaker, voice ID, and raw request reference.");
47
+ return;
48
+ }
49
+ const body = {
50
+ goal: { objective: prompt.trim(), raw_prompt: prompt, quality, planner_mode: "local_only" },
51
+ dialogue: { locked: true, lines: lines.map((text, i) => ({ line_id: `line-${i + 1}`, speaker: speaker.trim(), text })) },
52
+ casting: [[speaker.trim(), { voice_id: voiceId.trim(), kind: "synthetic" }]],
53
+ raw_request_ref: rawRef.trim(),
54
+ tts_candidates: tts,
55
+ keyframe_candidates: keyframes,
56
+ clip_candidates: clips,
57
+ seed_salt: seed,
58
+ negative_prompt: negative.trim() || null,
59
+ max_seconds: maxSeconds ? Number(maxSeconds) : null,
60
+ stop_after: stopAfter || null,
61
+ };
62
+ setBusy(true);
63
+ setMessage("");
64
+ const result = await request<EnqueueResult>(`${hugpyConfig.apiBase}/video/jobs/performance`, {
65
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
66
+ meta: { specKey: "oracle", operation: "oracle.performance.enqueue" },
67
+ });
68
+ setBusy(false);
69
+ if (!result.ok) {
70
+ setMessage(describeAppError(errorOf(result)));
71
+ return;
72
+ }
73
+ const jobId = okValue(result).job_id;
74
+ if (!jobId) {
75
+ setMessage("The server answered without a job id.");
76
+ return;
77
+ }
78
+ setMessage(`Queued ${jobId}`);
79
+ onEnqueued(jobId);
80
+ }
81
+
82
+ return <section className="vi-oracle-panel vi-oracle-composer" aria-label="New performance">
83
+ <h3>New performance <span className="vi-oracle-sub">audio first · durable media job · inspect each stage below</span></h3>
84
+ <div className="vi-oracle-fields">
85
+ <label className="vi-oracle-field vi-oracle-wide">Goal / visual direction
86
+ <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} rows={3} placeholder="Describe the film, characters, setting and action" />
87
+ </label>
88
+ <label className="vi-oracle-field vi-oracle-wide">Locked dialogue · one line per utterance
89
+ <textarea value={dialogue} onChange={(e) => setDialogue(e.target.value)} rows={3} placeholder="The words to speak" />
90
+ </label>
91
+ <label className="vi-oracle-field">Speaker <input value={speaker} onChange={(e) => setSpeaker(e.target.value)} /></label>
92
+ <label className="vi-oracle-field">Synthetic voice ID <input value={voiceId} onChange={(e) => setVoiceId(e.target.value)} /></label>
93
+ <label className="vi-oracle-field vi-oracle-wide">Raw request reference
94
+ <input value={rawRef} onChange={(e) => setRawRef(e.target.value)} placeholder="A durable reference to the original request" />
95
+ </label>
96
+ <label className="vi-oracle-field">Quality
97
+ <select value={quality} onChange={(e) => setQuality(e.target.value)}><option value="preview">Preview</option><option value="balanced">Balanced</option><option value="best">Best</option></select>
98
+ </label>
99
+ <label className="vi-oracle-field">Stop after
100
+ <select value={stopAfter} onChange={(e) => setStopAfter(e.target.value as Stage | "")}>
101
+ <option value="">Full run</option>{(["authority", "snapshot", "audio", "lock", "segments", "keyframes", "clips", "assembly"] as Stage[]).map((s) => <option key={s} value={s}>{s}</option>)}
102
+ </select>
103
+ </label>
104
+ <label className="vi-oracle-field">TTS candidates <input type="number" min={1} max={16} value={tts} onChange={(e) => setTts(Number(e.target.value))} /></label>
105
+ <label className="vi-oracle-field">Keyframe candidates <input type="number" min={1} max={16} value={keyframes} onChange={(e) => setKeyframes(Number(e.target.value))} /></label>
106
+ <label className="vi-oracle-field">Clip candidates <input type="number" min={1} max={16} value={clips} onChange={(e) => setClips(Number(e.target.value))} /></label>
107
+ <label className="vi-oracle-field">Max seconds <input type="number" min={1} value={maxSeconds} onChange={(e) => setMaxSeconds(e.target.value)} placeholder="No limit" /></label>
108
+ <label className="vi-oracle-field">Seed salt <input type="number" min={0} value={seed} onChange={(e) => setSeed(Number(e.target.value))} /></label>
109
+ <label className="vi-oracle-field vi-oracle-wide">Negative prompt <input value={negative} onChange={(e) => setNegative(e.target.value)} placeholder="Optional exclusions" /></label>
110
+ </div>
111
+ <div className="vi-oracle-note" role="status">
112
+ {probe ? <>
113
+ <strong>{probe.ready ? "Full recipe wired" : "Staged runs available; full recipe has gaps"}</strong>
114
+ {probe.unbound?.length ? <ul className="vi-oracle-limits">{probe.unbound.map((gap) => <li key={gap.seam}><code>{gap.seam}</code>: {gap.requirement}</li>)}</ul> : null}
115
+ </> : probeGap ? <>Could not read performance capability probe: {probeGap}</> : "Checking performance capabilities…"}
116
+ </div>
117
+ <div className="vi-oracle-row"><button type="button" className="vi-btn vi-btn-accent" disabled={busy} onClick={() => void enqueue()}>{busy ? "Queuing…" : "Queue performance"}</button>{message && <span role="status">{message}</span>}</div>
118
+ </section>;
119
+ }
File without changes
File without changes
@@ -22,6 +22,39 @@
22
22
  min-width: 0;
23
23
  }
24
24
 
25
+ .vi-oracle-fields {
26
+ display: grid;
27
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
28
+ gap: 0.75rem;
29
+ margin-bottom: 0.75rem;
30
+ }
31
+
32
+ .vi-oracle-field {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 0.3rem;
36
+ min-width: 0;
37
+ color: var(--vi-text-dim);
38
+ font-size: 0.76rem;
39
+ }
40
+
41
+ .vi-oracle-wide { grid-column: 1 / -1; }
42
+
43
+ .vi-oracle-field input,
44
+ .vi-oracle-field textarea,
45
+ .vi-oracle-field select {
46
+ width: 100%;
47
+ box-sizing: border-box;
48
+ padding: 0.48rem 0.58rem;
49
+ border: 1px solid var(--vi-border);
50
+ border-radius: 6px;
51
+ background: var(--vi-surface);
52
+ color: var(--vi-text);
53
+ font: inherit;
54
+ }
55
+
56
+ .vi-oracle-field textarea { resize: vertical; }
57
+
25
58
  .vi-oracle-panel > h3 {
26
59
  margin: 0 0 0.6rem;
27
60
  font-family: var(--vi-mono);
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -348,3 +348,26 @@
348
348
  grid-template-columns: 1fr;
349
349
  }
350
350
  }
351
+
352
+ /* ---- Template steps panel (TemplateStepsPanel.tsx) ---- */
353
+ .vi-template-steps {
354
+ margin: 0.5rem 0 0;
355
+ padding-left: 1.5rem;
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: 0.6rem;
359
+ }
360
+ .vi-template-step-head {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 0.6rem;
364
+ flex-wrap: wrap;
365
+ }
366
+ .vi-template-step-head > select {
367
+ max-width: 24rem;
368
+ }
369
+ .vi-template-step-desc {
370
+ margin: 0.15rem 0 0;
371
+ font-size: 0.85em;
372
+ opacity: 0.75;
373
+ }
File without changes
@@ -34,7 +34,7 @@ import * as THREE from "three";
34
34
  import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
35
35
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
36
36
  import { request, okValue, errorOf, describeAppError } from "../../transport/client";
37
- import { mediaBytesUrl, identityGenerateUrl, identityMeshStatusUrl, videoJobUrl } from "../../config";
37
+ import { hugpyConfig, mediaBytesUrl, identityGenerateUrl, identityMeshStatusUrl, videoJobUrl } from "../../config";
38
38
  import { useMediaLibrary } from "../../video/mediaLibrary";
39
39
  import type { LibraryItem } from "../../video/mediaLibrary";
40
40
  import type { MediaRef, IdentityReconstruction, IdentityAngleView } from "../../video/contract";
@@ -2149,6 +2149,17 @@ type IdTab = "active" | "session" | "settings";
2149
2149
 
2150
2150
  export function IdentitiesStation({ spec }: { spec: StationSpec }) {
2151
2151
  void spec;
2152
+ const [serviceProbe, setServiceProbe] = useState<{ configured: boolean; reachable: boolean; reason?: string; version?: string; capabilities?: Record<string, unknown> } | null>(null);
2153
+ useEffect(() => {
2154
+ let active = true;
2155
+ void (async () => {
2156
+ const result = await request<typeof serviceProbe>(`${hugpyConfig.apiBase}/video/identity-render/probe`, {
2157
+ meta: { specKey: "identities", operation: "identity.render.probe" },
2158
+ });
2159
+ if (active && result.ok) setServiceProbe(okValue(result));
2160
+ })();
2161
+ return () => { active = false; };
2162
+ }, []);
2152
2163
  const {
2153
2164
  profiles,
2154
2165
  loading,
@@ -2225,6 +2236,14 @@ export function IdentitiesStation({ spec }: { spec: StationSpec }) {
2225
2236
  return (
2226
2237
  <section className="station-wide vi-identities vi-identities-md" aria-label="Identities workspace"
2227
2238
  style={{ display: "grid", gridTemplateColumns: "300px minmax(0, 1fr)", gap: "1rem", alignItems: "start" }}>
2239
+ <div role="status" style={{ gridColumn: "1 / -1", padding: "0.55rem 0.75rem", border: "1px solid var(--vi-border)", borderRadius: "0.5rem", fontSize: "0.8rem" }}>
2240
+ <strong>Identity render service:</strong>{" "}
2241
+ {!serviceProbe ? "checking…" : serviceProbe.reachable
2242
+ ? `online${serviceProbe.version ? ` · ${serviceProbe.version}` : ""}`
2243
+ : serviceProbe.reason || "unavailable"}
2244
+ {serviceProbe?.reachable && serviceProbe.capabilities ?
2245
+ <span> · {Object.entries(serviceProbe.capabilities).filter(([, value]) => Boolean(value)).map(([name]) => name).join(", ")}</span> : null}
2246
+ </div>
2228
2247
  {/* ── SIDEBAR: tab filter + master list (wireframe b/c/d/f/e) ────────── */}
2229
2248
  <aside className="vi-id-sidebar" aria-label="Identity list"
2230
2249
  style={{ border: "1px solid var(--vi-border)", borderRadius: "0.5rem", padding: "0.5rem", position: "sticky", top: "0.5rem" }}>
@@ -2379,4 +2398,4 @@ export function IdentitiesStation({ spec }: { spec: StationSpec }) {
2379
2398
  </div>
2380
2399
  </section>
2381
2400
  );
2382
- }
2401
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -416,6 +416,11 @@ export function StudioGenerateSurface({
416
416
  height?: number;
417
417
  fps?: number;
418
418
  seed?: number;
419
+ startImage?: string;
420
+ steps?: number;
421
+ cfg?: number;
422
+ requestedFrames?: number;
423
+ negative?: string;
419
424
  }) => {
420
425
  const p = prompt.trim();
421
426
  if (!p) {
@@ -430,6 +435,11 @@ export function StudioGenerateSurface({
430
435
  if (opts.height != null) body.height = opts.height;
431
436
  if (opts.fps != null) body.fps = opts.fps;
432
437
  if (opts.seed != null) body.seed = opts.seed;
438
+ if (opts.startImage) body.start_image = opts.startImage;
439
+ if (opts.steps != null) body.steps = opts.steps;
440
+ if (opts.cfg != null) body.cfg = opts.cfg;
441
+ if (opts.requestedFrames != null) body.requested_frames = opts.requestedFrames;
442
+ if (opts.negative) body.negative = opts.negative;
433
443
  if (opts.models.length > 0) body.models = opts.models;
434
444
  const res = await request<unknown>(hugpyConfig.studioTesterUrl, {
435
445
  method: "POST",
@@ -2813,10 +2823,13 @@ export function StudioGenerateSurface({
2813
2823
  </div>
2814
2824
  {clipTesterOpen && (
2815
2825
  <div className="vi-gen-tester-actions">
2826
+ {!isT2V && !(isI2V && hasImage) && (
2827
+ <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>
2828
+ )}
2816
2829
  <button
2817
2830
  type="button"
2818
2831
  className="vi-btn vi-btn-sm vi-btn-tester"
2819
- disabled={testerBusy || !prompt.trim()}
2832
+ disabled={testerBusy || !prompt.trim() || (!isT2V && !(isI2V && hasImage))}
2820
2833
  onClick={() =>
2821
2834
  void onTestAllModels({
2822
2835
  category: surfaceMode,
@@ -2825,9 +2838,14 @@ export function StudioGenerateSurface({
2825
2838
  height,
2826
2839
  fps,
2827
2840
  seed,
2841
+ startImage: isI2V ? startImages[0]?.uri : undefined,
2842
+ steps: steps.trim() ? Number(steps) : undefined,
2843
+ cfg: cfg.trim() ? Number(cfg) : undefined,
2844
+ requestedFrames: requestedFrames.trim() ? Number(requestedFrames) : undefined,
2845
+ negative: negative.trim() || undefined,
2828
2846
  })
2829
2847
  }
2830
- title="Run this prompt across every model that FITS THE CURRENT FILTER (this surface's capability + geometry) — one row per model in the log below (a battery). Falls back to the whole roster when none match."
2848
+ 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
2849
  >
2832
2850
  {testerBusy ? "Starting…" : "🧪 Test all models"}
2833
2851
  </button>
File without changes
File without changes
File without changes
@@ -326,7 +326,7 @@ function SessionRowBody({
326
326
 
327
327
  /** The panel body — split out so the poll it drives exists ONLY while the panel is
328
328
  * open (mounting is the visibility signal; see the `active` gate below). */
329
- function SessionsBody() {
329
+ export function SessionsBody() {
330
330
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
331
331
  // A hidden browser tab is not a visible panel: pause the walk of the movies root
332
332
  // while the console is in the background, and reload the moment it comes back.
@@ -419,7 +419,7 @@ const SESSION_CATEGORIES: ReadonlyArray<{
419
419
  id: "cinema",
420
420
  label: "Cinema",
421
421
  kinds: ["generate_studio_movie"],
422
- hint: "movies on disk — resume, pause, or watch what already rendered",
422
+ hint: "cinema movies",
423
423
  },
424
424
  ];
425
425
 
@@ -450,7 +450,6 @@ function CategoryJobsBody({
450
450
  <p className="vi-knob-hint">
451
451
  No {label.toLowerCase()} builds in the feed — live ones appear here with
452
452
  progress and Cancel; tick “Show finished” above for recent history.
453
- (Durable resumable sessions exist for Cinema today.)
454
453
  </p>
455
454
  );
456
455
  }
@@ -491,7 +490,7 @@ export function SessionsPanel({
491
490
  cancelBusy: Set<string>;
492
491
  onCancel: (id: string) => void;
493
492
  }) {
494
- const [open, setOpen] = useState<Set<string>>(() => new Set(["cinema"]));
493
+ const [open, setOpen] = useState<Set<string>>(() => new Set());
495
494
  const toggle = (id: string) =>
496
495
  setOpen((prev) => {
497
496
  const next = new Set(prev);
@@ -504,7 +503,7 @@ export function SessionsPanel({
504
503
  <p className="vi-knob-headline" style={{ margin: "0.8rem 0 0.2rem" }}>
505
504
  Sessions{" "}
506
505
  <span className="vi-knob-headline-sub">
507
- previous & in-flight work, per surface — Cinema sessions resume from disk
506
+ previous & in-flight work, per surface
508
507
  </span>
509
508
  </p>
510
509
  {SESSION_CATEGORIES.map((cat) => {
@@ -526,24 +525,18 @@ export function SessionsPanel({
526
525
  {cat.label}{" "}
527
526
  <span className="vi-knob-headline-sub">
528
527
  {cat.hint}
529
- {cat.id !== "cinema" && catJobs.length > 0
530
- ? ` · ${catJobs.length} in feed`
531
- : ""}
528
+ {catJobs.length > 0 ? ` · ${catJobs.length} in feed` : ""}
532
529
  </span>
533
530
  </span>
534
531
  </button>
535
532
  {isOpen ? (
536
- cat.id === "cinema" ? (
537
- <SessionsBody />
538
- ) : (
539
- <CategoryJobsBody
540
- jobs={catJobs}
541
- nowMs={nowMs}
542
- cancelBusy={cancelBusy}
543
- onCancel={onCancel}
544
- label={cat.label}
545
- />
546
- )
533
+ <CategoryJobsBody
534
+ jobs={catJobs}
535
+ nowMs={nowMs}
536
+ cancelBusy={cancelBusy}
537
+ onCancel={onCancel}
538
+ label={cat.label}
539
+ />
547
540
  ) : null}
548
541
  </div>
549
542
  );
@@ -111,6 +111,9 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
111
111
  const [pinned, setPinned] = useState(false);
112
112
 
113
113
  const topRef = useRef<HTMLDivElement | null>(null);
114
+ const monitorRef = useRef<HTMLDivElement | null>(null);
115
+ const binRef = useRef<HTMLDivElement | null>(null);
116
+ const renderRef = useRef<HTMLDivElement | null>(null);
114
117
 
115
118
  // Round 9: fold into the ONE shell sidebar — reveal its guarded Settings tab (knobs +
116
119
  // template dropdown portal there). registerSettings is stable, so this runs once per
@@ -275,10 +278,24 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
275
278
 
276
279
  return (
277
280
  <div className="vi-studio-workbench" ref={topRef}>
281
+ <nav className="vi-studio-workflow" aria-label="Studio workflow">
282
+ <button type="button" onClick={() => monitorRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}>
283
+ <span className="vi-studio-workflow-step">01 · Monitor</span>
284
+ <span>Review the current take</span>
285
+ </button>
286
+ <button type="button" onClick={() => binRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}>
287
+ <span className="vi-studio-workflow-step">02 · Shot bin</span>
288
+ <span>{items.length} staged {items.length === 1 ? "item" : "items"}</span>
289
+ </button>
290
+ <button type="button" onClick={() => renderRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}>
291
+ <span className="vi-studio-workflow-step">03 · Render</span>
292
+ <span>Clip or cinema</span>
293
+ </button>
294
+ </nav>
278
295
  {/* CENTER, movie skeleton: viewer on top → generator below. Viewer header controls
279
296
  (Play from library / Clear / follow-newest toggle) are wired here so both
280
297
  plane mounts get them for free — see the `pinned` note above. */}
281
- <StudioViewer
298
+ <div ref={monitorRef} className="vi-studio-workflow-target"><StudioViewer
282
299
  clips={clipsState.clips}
283
300
  library={libraryMedia}
284
301
  selected={selected}
@@ -286,7 +303,7 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
286
303
  onPick={pickViewerClip}
287
304
  onClear={clearViewer}
288
305
  onToggleFollow={toggleFollowNewest}
289
- />
306
+ /></div>
290
307
 
291
308
  {/* Slice 2b: the staged studio items as a strip of first-class cards with inline
292
309
  per-item ops. Additive — it sits ABOVE the generate surface and does not touch
@@ -294,7 +311,8 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
294
311
  mounts identically (this is the one shared StudioPlane). Each card is wrapped in
295
312
  a keyed intrinsic <div> so the map key never lands on a component (which the
296
313
  bare-tsc env flags as a spurious prop). */}
297
- {items.length > 0 && (
314
+ <div ref={binRef} className="vi-studio-workflow-target" aria-label="Shot bin">
315
+ {items.length > 0 ? (
298
316
  <div className="vi-studio-item-strip" aria-label="Studio items">
299
317
  {items.map((it) => (
300
318
  <div className="vi-studio-item-cell" key={it.key}>
@@ -308,8 +326,10 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
308
326
  </div>
309
327
  ))}
310
328
  </div>
311
- )}
329
+ ) : <p className="vi-studio-bin-empty">Staged media will appear here. Send a library item to Studio or add a source in the render desk.</p>}
330
+ </div>
312
331
 
332
+ <div ref={renderRef} className="vi-studio-workflow-target">
313
333
  <StudioGenerateSurface
314
334
  presets={presets}
315
335
  presetsLoading={presetsLoading}
@@ -322,6 +342,7 @@ export function StudioPlane({ registerBridge = false, lockedSurfaceMode }: Studi
322
342
  settingsHost={reg.settingsHost}
323
343
  lockedSurfaceMode={lockedSurfaceMode}
324
344
  />
345
+ </div>
325
346
  {/* STUDIO-ASSIST LIVE LOG → portaled into the sidebar's ACTIVE (Active
326
347
  Processes) tab via the registry's activeExtra host (operator correction
327
348
  2026-08-05: "active tab" = Active Processes). Single mount, shared stream. */}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/style/app.css CHANGED
@@ -3406,6 +3406,55 @@ button.vi-studio-src-thumb {
3406
3406
  min-width: 0;
3407
3407
  }
3408
3408
 
3409
+ .vi-studio-workflow {
3410
+ display: grid;
3411
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3412
+ gap: 0.5rem;
3413
+ border-bottom: 1px solid var(--vi-border);
3414
+ padding-bottom: 0.7rem;
3415
+ }
3416
+
3417
+ .vi-studio-workflow button {
3418
+ display: flex;
3419
+ flex-direction: column;
3420
+ gap: 0.25rem;
3421
+ text-align: left;
3422
+ min-width: 0;
3423
+ padding: 0.65rem 0.75rem;
3424
+ border: 1px solid var(--vi-border);
3425
+ border-radius: 8px;
3426
+ background: var(--vi-surface-raised);
3427
+ color: var(--vi-text-dim);
3428
+ cursor: pointer;
3429
+ }
3430
+
3431
+ .vi-studio-workflow button:hover,
3432
+ .vi-studio-workflow button:focus-visible {
3433
+ border-color: var(--vi-accent);
3434
+ color: var(--vi-text);
3435
+ }
3436
+
3437
+ .vi-studio-workflow-step {
3438
+ color: var(--vi-text);
3439
+ font-family: var(--vi-mono);
3440
+ font-size: 0.76rem;
3441
+ font-weight: 600;
3442
+ }
3443
+
3444
+ .vi-studio-workflow-target { scroll-margin-top: 1rem; min-width: 0; }
3445
+ .vi-studio-bin-empty {
3446
+ margin: 0;
3447
+ padding: 0.7rem 0.85rem;
3448
+ border: 1px dashed var(--vi-border);
3449
+ border-radius: 8px;
3450
+ color: var(--vi-text-dim);
3451
+ font-size: 0.78rem;
3452
+ }
3453
+
3454
+ @media (max-width: 35rem) {
3455
+ .vi-studio-workflow { grid-template-columns: 1fr; }
3456
+ }
3457
+
3409
3458
  /* Round 9: the studio no longer carries its OWN sidebar — it folds into the arm shell's
3410
3459
  ONE left sidebar (Settings / Active / Library, via the sidebar registry). So the
3411
3460
  workbench is a SINGLE centre column (viewer on top → generator below), the Movie
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -71,12 +71,14 @@ export function PromptAssistButtons({
71
71
  >
72
72
  <option value="">fleet default</option>
73
73
  {models.map((m) => (
74
- // "· cold" is not decoration: a non-seated model pays a load on first use,
75
- // which is minutes, not milliseconds. Saying so up front stops that reading
76
- // as a hang.
74
+ // Not decoration (canonical STATE-MODEL.md #4): a non-ready model pays a
75
+ // load on first use. `hot` = already on a worker drive (a t_load, seconds);
76
+ // `cold` = central only (a download THEN a load). "cold" is reserved for
77
+ // not-on-hot-drive, so it never mislabels a hot or loaded model.
77
78
  <option key={m.model} value={m.model}>
78
79
  {m.model}
79
- {m.serving ? "" : " · cold"}
80
+ {m.state === "serving" || m.state === "loaded" ? ""
81
+ : m.state === "hot" ? " · hot (loads)" : " · cold (downloads)"}
80
82
  </option>
81
83
  ))}
82
84
  </select>
File without changes
File without changes
File without changes
File without changes
File without changes