@hugpy/video-intelligence-ui 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) 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-DMoT8G1q.css +1 -0
  5. package/dist/assets/index-GeD1mo16.js +4190 -0
  6. package/dist/index.html +71 -0
  7. package/entry.tsx +6 -27
  8. package/index.html +4 -4
  9. package/package.json +44 -5
  10. package/src/assets/hugpy-mark.png +0 -0
  11. package/src/config.ts +11 -27
  12. package/src/demo/DemoBanner.tsx +0 -0
  13. package/src/demo/demo.css +0 -0
  14. package/src/demo/demoFetch.ts +0 -9
  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 +8 -3
  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 +8 -43
  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 +10 -6
  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 +4 -4
  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 +1 -2
  56. package/src/stations/WorkbenchDrawer.tsx +0 -0
  57. package/src/stations/WorkbenchSidebar.tsx +8 -261
  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 +75 -83
  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 +86 -76
  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 +3 -2
  86. package/src/stations/studio/StudioGenerateTab.tsx +278 -190
  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 +321 -46
  90. package/src/stations/studio/StudioMovieSessions.tsx +12 -19
  91. package/src/stations/studio/StudioPlane.tsx +28 -5
  92. package/src/stations/studio/StudioSplicedRow.tsx +0 -0
  93. package/src/stations/studio/StudioViewer.tsx +6 -11
  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 +14 -2
  101. package/src/stations/studio/useIdentityCreateJob.ts +26 -21
  102. package/src/stations/studio/useIdentityProfiles.ts +0 -0
  103. package/src/stations/studio/useReconstruction.ts +11 -5
  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 +5 -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/jobStream.ts +119 -0
  128. package/src/video/jobTracker.ts +67 -76
  129. package/src/video/mediaLibrary.ts +0 -0
  130. package/src/video/modelsFeed.ts +105 -0
  131. package/src/video/renderCompat.ts +0 -0
  132. package/src/video/sessionForm.ts +0 -0
  133. package/src/video/spreadAssist.ts +0 -0
  134. package/src/video/studioBridge.ts +0 -0
  135. package/src/video/studioOracle.ts +48 -0
  136. package/src/video/useMediaJobs.ts +17 -3
  137. package/src/video/useModels.ts +12 -53
  138. package/src/video/useMoviePresets.ts +0 -0
  139. package/src/video/useMovieSessions.ts +15 -6
  140. package/src/video/useOperatorHistory.ts +11 -101
  141. package/src/video/usePresets.ts +0 -0
  142. package/src/video/useProjects.ts +0 -0
  143. package/src/video/usePromptAssist.ts +70 -25
  144. package/src/video/useRenderPresets.ts +0 -0
  145. package/src/video/useStudioAssistLog.ts +0 -0
  146. package/src/video/useStudioPresets.ts +0 -0
  147. package/src/video/useToks.ts +14 -6
  148. package/src/video/watchVideoJob.ts +43 -0
  149. package/tsconfig.json +0 -0
  150. package/vite.config.ts +0 -0
  151. package/src/stations/studio/StudioMovieComposer.tsx.bak-declutter-20260813 +0 -3031
  152. package/src/video/useWorkerCalls.ts +0 -165
@@ -20,7 +20,6 @@
20
20
  import { useEffect, useRef, useState } from "react";
21
21
  import {
22
22
  mediaBytesUrl,
23
- videoJobUrl,
24
23
  hugpyConfig,
25
24
  identityGenerateUrl,
26
25
  identityMeshStatusUrl,
@@ -28,6 +27,7 @@ import {
28
27
  import { request, okValue, errorOf, describeAppError } from "../../transport/client";
29
28
  import type { MediaRef } from "../../video/contract";
30
29
  import type { IdentityProfilesState } from "../studio/useIdentityProfiles";
30
+ import { watchVideoJob } from "../../video/watchVideoJob";
31
31
  import { MeshViewer } from "../studio/IdentitiesStation";
32
32
 
33
33
  // ── S1 review manifest shapes (tolerant reads — the poll result is cast, exactly
@@ -134,18 +134,18 @@ export function CharacterGroupsPanel({
134
134
  // slug can run concurrently, mirrors the group-level state being a map too).
135
135
  const [mesh3D, setMesh3D] = useState<Record<string, Mesh3DState>>({});
136
136
  const meshActive = useRef<Record<string, boolean>>({});
137
- const meshTimer = useRef<Record<string, number>>({});
137
+ const meshStop = useRef<Record<string, () => void>>({});
138
138
 
139
139
  const active = useRef(false);
140
- const timer = useRef<number | null>(null);
140
+ const reviewStop = useRef<(() => void) | null>(null);
141
141
  const idSeq = useRef(0);
142
142
 
143
143
  useEffect(() => {
144
144
  return () => {
145
145
  active.current = false;
146
- if (timer.current != null) window.clearTimeout(timer.current);
146
+ reviewStop.current?.();
147
147
  for (const slug of Object.keys(meshActive.current)) meshActive.current[slug] = false;
148
- for (const slug of Object.keys(meshTimer.current)) window.clearTimeout(meshTimer.current[slug]);
148
+ for (const stop of Object.values(meshStop.current)) stop();
149
149
  };
150
150
  }, []);
151
151
 
@@ -194,56 +194,42 @@ export function CharacterGroupsPanel({
194
194
  }
195
195
 
196
196
  const jobId = enq.jobId;
197
- const deadline = Date.now() + 20 * 60 * 1000; // ~20 min bound, mirrors ExtractFromVideoPanel
198
- const pollOnce = async () => {
199
- if (!active.current) return;
200
- if (Date.now() > deadline) {
197
+ reviewStop.current?.();
198
+ reviewStop.current = watchVideoJob(jobId, (row) => {
199
+ if (!active.current) return true;
200
+ const job = row as {
201
+ progress?: number | null;
202
+ result?: {
203
+ ok?: boolean;
204
+ groups?: ReviewManifest;
205
+ error?: { code?: string; message?: string; retryable?: boolean };
206
+ } | null;
207
+ stage?: string | null;
208
+ message?: string | null;
209
+ };
210
+ const result = job.result;
211
+ if (result && typeof result.ok === "boolean") {
201
212
  active.current = false;
202
213
  setBusy(false);
203
- setStatus(null);
204
- setError("Timed out waiting for the character review.");
205
- return;
206
- }
207
- const res = await request<unknown>(videoJobUrl(jobId), {
208
- meta: { specKey: "studio", operation: "identity.groups.review.status" },
209
- });
210
- if (!active.current) return;
211
- if (res.ok) {
212
- const job = okValue(res) as {
213
- progress?: number | null;
214
- result?: {
215
- ok?: boolean;
216
- groups?: ReviewManifest;
217
- error?: { code?: string; message?: string; retryable?: boolean };
218
- } | null;
219
- stage?: string | null;
220
- message?: string | null;
221
- };
222
- const result = job.result;
223
- if (result && typeof result.ok === "boolean") {
224
- active.current = false;
225
- setBusy(false);
226
- if (result.ok) {
227
- setStatus("done");
228
- if (result.groups && Array.isArray(result.groups.groups)) {
229
- loadManifest(result.groups);
230
- } else {
231
- setError("Review finished but returned no character groups.");
232
- }
233
- } else {
234
- setStatus(null);
235
- setError((result.error && result.error.message) || "Character review failed.");
236
- }
237
- return;
214
+ if (result.ok) {
215
+ setStatus("done");
216
+ if (result.groups && Array.isArray(result.groups.groups)) loadManifest(result.groups);
217
+ else setError("Review finished but returned no character groups.");
218
+ } else {
219
+ setStatus(null);
220
+ setError((result.error && result.error.message) || "Character review failed.");
238
221
  }
239
- const pct = typeof job.progress === "number" ? ` (${Math.round(job.progress * 100)}%)` : "";
240
- const stage = job.stage || job.message;
241
- setStatus(`Reviewing…${pct}${stage ? ` — ${stage}` : ""}`);
222
+ return true;
242
223
  }
243
- // queued/running/transient-poll-failure → keep polling.
244
- timer.current = window.setTimeout(() => void pollOnce(), 5000);
245
- };
246
- timer.current = window.setTimeout(() => void pollOnce(), 5000);
224
+ const pct = typeof job.progress === "number" ? ` (${Math.round(job.progress * 100)}%)` : "";
225
+ const stage = job.stage || job.message;
226
+ setStatus(`Reviewing…${pct}${stage ? ` — ${stage}` : ""}`);
227
+ }, () => {
228
+ active.current = false;
229
+ setBusy(false);
230
+ setStatus(null);
231
+ setError("Timed out waiting for the character review.");
232
+ }, 20 * 60 * 1000);
247
233
  }
248
234
 
249
235
  // ── selection helpers ─────────────────────────────────────────────────────
@@ -414,41 +400,47 @@ export function CharacterGroupsPanel({
414
400
  return;
415
401
  }
416
402
 
417
- const deadline = Date.now() + 20 * 60 * 1000; // ~20 min bound, mirrors Studio's generate poll
418
- const pollOnce = async () => {
419
- if (!meshActive.current[slug]) return;
420
- if (Date.now() > deadline) {
421
- meshActive.current[slug] = false;
422
- setMesh(slug, { phase: "error", status: null, error: "Timed out waiting for the 3D build." });
423
- return;
424
- }
425
- const sres = await request<unknown>(identityMeshStatusUrl(slug, reconId), {
426
- meta: { specKey: "studio", operation: "identity.groups.mesh.status" },
427
- });
428
- if (!meshActive.current[slug]) return;
429
- if (sres.ok) {
430
- const ms = okValue(sres) as { status?: string; error?: string | null; glb_path?: string | null };
431
- const st = ms.status ?? "queued";
432
- if (st === "done") {
433
- meshActive.current[slug] = false;
434
- if (ms.glb_path) {
435
- setMesh(slug, { phase: "done", status: "done", glbUrl: mediaBytesUrl(ms.glb_path), error: null });
436
- } else {
437
- setMesh(slug, { phase: "error", status: null, error: "Build finished but returned no model." });
438
- }
439
- return;
440
- }
441
- if (st === "error" || st === "cancelled") {
403
+ if (!enq.job_id) {
404
+ meshActive.current[slug] = false;
405
+ setMesh(slug, { phase: "error", status: null, error: "Generate response had no job_id." });
406
+ return;
407
+ }
408
+ meshStop.current[slug]?.();
409
+ meshStop.current[slug] = watchVideoJob(enq.job_id, (row) => {
410
+ if (!meshActive.current[slug]) return true;
411
+ const result = row.result as { ok?: boolean; error?: { message?: string } } | null;
412
+ if (result && typeof result.ok === "boolean") {
413
+ if (!result.ok) {
442
414
  meshActive.current[slug] = false;
443
- setMesh(slug, { phase: "error", status: null, error: ms.error || `Build ${st}.` });
444
- return;
415
+ setMesh(slug, { phase: "error", status: null,
416
+ error: result.error?.message || "3D build failed." });
417
+ } else {
418
+ void request<unknown>(identityMeshStatusUrl(slug, reconId), {
419
+ meta: { specKey: "studio", operation: "identity.groups.mesh.final" },
420
+ }).then((sres) => {
421
+ if (!meshActive.current[slug]) return;
422
+ meshActive.current[slug] = false;
423
+ if (!sres.ok) {
424
+ setMesh(slug, { phase: "error", status: null,
425
+ error: describeAppError(errorOf(sres)) });
426
+ return;
427
+ }
428
+ const ms = okValue(sres) as { status?: string; error?: string | null; glb_path?: string | null };
429
+ if (ms.status === "done" && ms.glb_path) {
430
+ setMesh(slug, { phase: "done", status: "done", glbUrl: mediaBytesUrl(ms.glb_path), error: null });
431
+ } else {
432
+ setMesh(slug, { phase: "error", status: null,
433
+ error: ms.error || "Build finished but returned no model." });
434
+ }
435
+ });
445
436
  }
446
- setMesh(slug, { status: `Generating… (${st})` });
437
+ return true;
447
438
  }
448
- // queued/running/transient-poll-failure → keep polling.
449
- meshTimer.current[slug] = window.setTimeout(() => void pollOnce(), 5000);
450
- };
451
- meshTimer.current[slug] = window.setTimeout(() => void pollOnce(), 5000);
439
+ setMesh(slug, { status: `Generating… (${String(row.current_stage || row.status || "running")})` });
440
+ }, () => {
441
+ meshActive.current[slug] = false;
442
+ setMesh(slug, { phase: "error", status: null, error: "Timed out waiting for the 3D build." });
443
+ }, 20 * 60 * 1000);
452
444
  }
453
445
 
454
446
  // ── render ─────────────────────────────────────────────────────────────────
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -19,6 +19,7 @@ import { useState } from "react";
19
19
 
20
20
  import type { StationSpec } from "../types";
21
21
  import { DagPanel } from "./DagPanel";
22
+ import { PerformanceComposer } from "./PerformanceComposer";
22
23
  import { SpatialTab } from "./SpatialTab";
23
24
  import { StewardPanel } from "./StewardPanel";
24
25
  import { GapNote, RawJson, StateChip, Tag, short, when } from "./oracleShared";
@@ -273,6 +274,7 @@ export function OracleStation({ spec }: { spec: StationSpec }) {
273
274
  <p className="station-blurb">{spec.blurb}</p>
274
275
 
275
276
  <StewardPanel api={steward} embedded={runs.manifest?.dag?.steward ?? null} />
277
+ <PerformanceComposer onEnqueued={(jobId) => runs.select(jobId)} />
276
278
  <RunPicker runs={runs} />
277
279
  <RunHeader runs={runs} />
278
280
 
@@ -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