@hugpy/video-intelligence-ui 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +43 -0
  2. package/index.html +2 -0
  3. package/package.json +1 -4
  4. package/src/config.ts +44 -0
  5. package/src/demo/demoFetch.ts +126 -3
  6. package/src/demo/fixtures.ts +97 -27
  7. package/src/demo/mediaBase.ts +62 -0
  8. package/src/demo/promptAssist.ts +25 -0
  9. package/src/demo/sampleData.ts +961 -0
  10. package/src/demo/sampleSets.ts +173 -0
  11. package/src/stations/ActiveProcessesStation.tsx +18 -3
  12. package/src/stations/GenerateStation.tsx +901 -366
  13. package/src/stations/GoalComposer.tsx +9 -1
  14. package/src/stations/ScriptFirstStation.tsx +1081 -0
  15. package/src/stations/ToksPanel.tsx +188 -0
  16. package/src/stations/WorkbenchDrawer.tsx +4 -3
  17. package/src/stations/WorkbenchSidebar.tsx +6 -3
  18. package/src/stations/oracle/DagPanel.tsx +391 -0
  19. package/src/stations/oracle/OracleStation.tsx +304 -0
  20. package/src/stations/oracle/SpatialTab.tsx +185 -0
  21. package/src/stations/oracle/StewardPanel.tsx +165 -0
  22. package/src/stations/oracle/oracle.css +467 -0
  23. package/src/stations/oracle/oracleShared.tsx +160 -0
  24. package/src/stations/oracle/oracleTypes.ts +304 -0
  25. package/src/stations/oracle/useOracle.ts +560 -0
  26. package/src/stations/registry.ts +37 -0
  27. package/src/stations/scriptFirst.css +350 -0
  28. package/src/stations/studio/IdentitiesStation.tsx +219 -292
  29. package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
  30. package/src/stations/studio/StudioGenerateTab.tsx +10 -17
  31. package/src/stations/studio/StudioMovieComposer.tsx +296 -59
  32. package/src/stations/studio/StudioPlane.tsx +35 -3
  33. package/src/stations/studio/StudioViewer.tsx +116 -14
  34. package/src/stations/studio/coordinationBadge.tsx +206 -0
  35. package/src/stations/studio/identityUpload.ts +66 -0
  36. package/src/stations/studio/promptCard.tsx +362 -59
  37. package/src/stations/studio/useIdentityCreateJob.ts +184 -0
  38. package/src/stations/studio/useIdentityProfiles.ts +66 -0
  39. package/src/stations/useScriptFirstRun.ts +596 -0
  40. package/src/style/app.css +157 -0
  41. package/src/video/LengthRow.tsx +189 -0
  42. package/src/video/coordinationReport.ts +266 -0
  43. package/src/video/jobClock.ts +104 -0
  44. package/src/video/spreadAssist.ts +26 -0
  45. package/src/video/useMediaJobs.ts +26 -0
  46. package/src/video/useOperatorHistory.ts +5 -0
  47. package/src/video/useToks.ts +193 -0
  48. package/assets/sampels/images/bycycle/bd8603bad35d4f179f4d1134d8faf2ee.mp4 +0 -0
  49. package/assets/sampels/images/bycycle/frame_00000.png +0 -0
  50. package/assets/sampels/images/bycycle/frame_00001.png +0 -0
  51. package/assets/sampels/images/bycycle/frame_00002.png +0 -0
  52. package/assets/sampels/images/bycycle/frame_00003.png +0 -0
  53. package/assets/sampels/images/bycycle/frame_00004.png +0 -0
  54. package/assets/sampels/images/bycycle/frame_00005.png +0 -0
  55. package/assets/sampels/images/forrest/ff23090611b844a6b85c224f388bd542.mp4 +0 -0
  56. package/assets/sampels/images/forrest/frame_00000.png +0 -0
  57. package/assets/sampels/images/forrest/frame_00001.png +0 -0
  58. package/assets/sampels/images/forrest/frame_00002.png +0 -0
  59. package/assets/sampels/images/forrest/frame_00003.png +0 -0
  60. package/assets/sampels/images/forrest/frame_00004.png +0 -0
  61. package/assets/sampels/images/forrest/frame_00005.png +0 -0
  62. package/assets/sampels/images/shenzo/e1a4fcd623084ab4a986d11c8cd00db8.mp4 +0 -0
  63. package/assets/sampels/images/shenzo/frame_00000.png +0 -0
  64. package/assets/sampels/images/shenzo/frame_00001.png +0 -0
  65. package/assets/sampels/images/shenzo/frame_00002.png +0 -0
  66. package/assets/sampels/images/shenzo/frame_00003.png +0 -0
  67. package/assets/sampels/images/shenzo/frame_00004.png +0 -0
  68. package/assets/sampels/images/shenzo/frame_00005.png +0 -0
  69. package/assets/sampels/scenes/man.mp4 +0 -0
  70. package/assets/sampels/scenes/woman.mp4 +0 -0
  71. package/assets/sampels/studio_movies/caverns/_assembly/contrib_00.mp4 +0 -0
  72. package/assets/sampels/studio_movies/caverns/movie.json +0 -67
  73. package/assets/sampels/studio_movies/caverns/movie.mp4 +0 -0
  74. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/clip.mp4 +0 -0
  75. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/manifest.json +0 -87
  76. package/assets/sampels/studio_movies/caverns/segment_00/5eea7770a554fef0c3bdc38f8f38b925dcf75f13c015d423e8eadec47d41bf6b/provenance.json +0 -7
  77. package/assets/sampels/studio_movies/caverns/segment_01/branch.png +0 -0
  78. package/assets/sampels/studio_movies/keeper-matrix/_assembly/concat_list.txt +0 -2
  79. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_00.mp4 +0 -0
  80. package/assets/sampels/studio_movies/keeper-matrix/_assembly/contrib_01.mp4 +0 -0
  81. package/assets/sampels/studio_movies/keeper-matrix/movie.json +0 -81
  82. package/assets/sampels/studio_movies/keeper-matrix/movie.mp4 +0 -0
  83. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/clip.mp4 +0 -0
  84. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/manifest.json +0 -87
  85. package/assets/sampels/studio_movies/keeper-matrix/segment_00/64854c6a21d47ce10b23e884e12de448b25bfb0a375d33238dd0bec8056c076d/provenance.json +0 -7
  86. package/assets/sampels/studio_movies/keeper-matrix/segment_01/branch.png +0 -0
  87. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/clip.mp4 +0 -0
  88. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/manifest.json +0 -87
  89. package/assets/sampels/studio_movies/keeper-matrix/segment_01/cc926b44a42b90d7d291a4e50e3859458306c207a17c718086a3466e4049d6c6/provenance.json +0 -7
  90. package/assets/sampels/videos/a_red_paper_boat_drifting_on_a_calm_pond__gentle_ripples__mo_a62d6c28.mp4 +0 -0
  91. package/assets/sampels/videos/aurora.mp4 +0 -0
  92. package/assets/sampels/videos/dark_night.mp4 +0 -0
  93. package/assets/sampels/videos/fire_and_soul.mp4 +0 -0
  94. package/assets/sampels/videos/harsh_seas.mp4 +0 -0
  95. package/src/demo/assets/demo-audio.mp3 +0 -0
  96. package/src/demo/assets/demo-clip.mp4 +0 -0
  97. package/src/demo/assets/demo-crop.mp4 +0 -0
  98. package/src/demo/assets/demo-poster.jpg +0 -0
  99. package/src/demo/assets/frame-1.jpg +0 -0
  100. package/src/demo/assets/frame-2.jpg +0 -0
  101. package/src/demo/assets/frame-3.jpg +0 -0
  102. package/src/demo/assets/frame-4.jpg +0 -0
  103. package/src/demo/assets/frame-5.jpg +0 -0
  104. package/src/demo/assets/frame-6.jpg +0 -0
  105. package/src/demo/assets/gen-1.jpg +0 -0
  106. package/src/demo/assets/gen-2.jpg +0 -0
  107. package/src/demo/assets.d.ts +0 -14
@@ -0,0 +1,1081 @@
1
+ /**
2
+ * k114 — the Script station: the script-first pipeline, made visible.
3
+ *
4
+ * The doc's "Interface behavior" list, one panel each:
5
+ *
6
+ * 1. Run header — id, start, registry version, and the selected model ROUTE
7
+ * with the router's own justification for it.
8
+ * 2. Snapshot — which prompts were captured with their hashes, and which were
9
+ * EXCLUDED for being persisted after the run began, with the reason. An
10
+ * exclusion is shown, never inferred from an absence.
11
+ * 3. Pre-production — plot and screenplay, authored or hand-edited as JSON,
12
+ * with the validator's errors rendered VERBATIM. After the lock the
13
+ * editors go read-only and the only control is revise-with-a-reason.
14
+ * 4. Continuity + shot plan — read-only tables.
15
+ * 5. Segments — one parent box (the locked artifacts) fanning out to N cards.
16
+ * Each card carries its prompt, its provenance digests, every attempt with
17
+ * the model/seed/params it actually used, and its own Regenerate button
18
+ * that busies ONLY that card. The sibling digests are printed on the card
19
+ * so "regenerating 1 did not change 2" is readable rather than promised.
20
+ * 6. Promote — with a confirmation that says what it actually does: start a
21
+ * NEW run's source, and never this one's.
22
+ *
23
+ * Every gap is rendered as a gap. Nothing on this screen is a spinner standing
24
+ * in for a capability this fleet does not have.
25
+ */
26
+ import { useEffect, useMemo, useState } from "react";
27
+
28
+ import type { StationSpec } from "./types";
29
+ import {
30
+ useScriptFirstRun,
31
+ type AttemptRow,
32
+ type RunState,
33
+ type SegmentRow,
34
+ } from "./useScriptFirstRun";
35
+ import "./scriptFirst.css";
36
+
37
+ const SHORT = 12;
38
+
39
+ function short(digest: unknown): string {
40
+ const text = String(digest ?? "");
41
+ return text ? `${text.slice(0, SHORT)}…` : "—";
42
+ }
43
+
44
+ function pretty(value: unknown): string {
45
+ try {
46
+ return JSON.stringify(value ?? {}, null, 2);
47
+ } catch {
48
+ return String(value ?? "");
49
+ }
50
+ }
51
+
52
+ function asRecord(v: unknown): Record<string, unknown> {
53
+ return v && typeof v === "object" && !Array.isArray(v)
54
+ ? (v as Record<string, unknown>)
55
+ : {};
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Panel 1 — the run header
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function RunHeader({ run }: { run: RunState }) {
63
+ const route = run.models?.authoring_route ?? {};
64
+ const reasons = route.reasons ?? [];
65
+ return (
66
+ <section className="vi-script-panel">
67
+ <h3>
68
+ Run
69
+ <span className="vi-script-sub">{run.deliverable}</span>
70
+ </h3>
71
+ <dl className="vi-script-kv">
72
+ <dt>run id</dt>
73
+ <dd>
74
+ <code>{run.run_id}</code>{" "}
75
+ <span
76
+ className={`vi-script-tag ${
77
+ run.locked ? "vi-script-tag-on" : ""
78
+ }`}
79
+ >
80
+ {run.locked
81
+ ? `locked · revision ${
82
+ asRecord(run.lock?.payload).revision ?? 0
83
+ }`
84
+ : "open"}
85
+ </span>
86
+ </dd>
87
+ <dt>started</dt>
88
+ <dd>{run.created_at}</dd>
89
+ <dt>registry_version</dt>
90
+ <dd className="vi-script-digest">
91
+ {run.models?.fleet?.registry_version ?? "— (the registry is unversioned)"}
92
+ </dd>
93
+ <dt>model route</dt>
94
+ <dd>
95
+ {route.model_id ? (
96
+ <>
97
+ <code>{route.model_id}</code> for <code>{route.capability}</code>{" "}
98
+ <span className="vi-script-tag">{route.execution}</span>
99
+ </>
100
+ ) : (
101
+ <span className="vi-script-tag vi-script-tag-off">
102
+ no model route for {route.capability ?? "text.chat"}
103
+ </span>
104
+ )}
105
+ </dd>
106
+ <dt>why</dt>
107
+ <dd>
108
+ {route.model_rationale || "—"}
109
+ {reasons.length > 0 && (
110
+ <ul className="vi-script-limits">
111
+ {reasons.map((r, i) => (
112
+ <li key={i}>{r}</li>
113
+ ))}
114
+ </ul>
115
+ )}
116
+ <p className="vi-script-note">
117
+ The routing justification available today is the router's own
118
+ rationale. A measured per-task benchmark (k109) has not landed, so
119
+ nothing here claims one.
120
+ </p>
121
+ </dd>
122
+ <dt>snapshot</dt>
123
+ <dd className="vi-script-digest">{run.snapshot_digest}</dd>
124
+ </dl>
125
+ {run.limitations?.length > 0 && (
126
+ <ul className="vi-script-limits">
127
+ {run.limitations.map((l, i) => (
128
+ <li key={i}>{l}</li>
129
+ ))}
130
+ </ul>
131
+ )}
132
+ </section>
133
+ );
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Panel 2 — the immutable snapshot
138
+ // ---------------------------------------------------------------------------
139
+
140
+ function SnapshotPanel({ run }: { run: RunState }) {
141
+ const excluded = run.sources.filter((s) => !s.included);
142
+ return (
143
+ <section className="vi-script-panel">
144
+ <h3>
145
+ Input snapshot
146
+ <span className="vi-script-sub">
147
+ captured before generation began — only these may influence this run
148
+ </span>
149
+ </h3>
150
+ <table className="vi-script-table">
151
+ <thead>
152
+ <tr>
153
+ <th>prompt id</th>
154
+ <th>text</th>
155
+ <th>content hash</th>
156
+ <th>persisted</th>
157
+ <th>in snapshot</th>
158
+ </tr>
159
+ </thead>
160
+ <tbody>
161
+ {run.sources.map((s) => (
162
+ <tr
163
+ key={s.prompt_id}
164
+ className={s.included ? "" : "vi-script-excluded"}
165
+ >
166
+ <td>
167
+ <code>{s.prompt_id}</code>
168
+ {s.origin === "promoted" && (
169
+ <>
170
+ {" "}
171
+ <span className="vi-script-tag">promoted</span>
172
+ </>
173
+ )}
174
+ </td>
175
+ <td>{s.text}</td>
176
+ <td className="vi-script-digest">{short(s.digest)}</td>
177
+ <td>{s.persisted_at ?? "—"}</td>
178
+ <td>
179
+ {s.included ? (
180
+ <span className="vi-script-tag vi-script-tag-on">captured</span>
181
+ ) : (
182
+ <span className="vi-script-tag vi-script-tag-off">excluded</span>
183
+ )}
184
+ {s.exclusion_reason && (
185
+ <p className="vi-script-note">{s.exclusion_reason}</p>
186
+ )}
187
+ </td>
188
+ </tr>
189
+ ))}
190
+ {run.sources.length === 0 && (
191
+ <tr>
192
+ <td colSpan={5}>
193
+ This run captured no source prompts. Doc Stage 5's "minimal"
194
+ mode builds a plot from the requirements alone.
195
+ </td>
196
+ </tr>
197
+ )}
198
+ </tbody>
199
+ </table>
200
+ {excluded.length > 0 && (
201
+ <p className="vi-script-note">
202
+ {excluded.length} prompt(s) were persisted after this run started and
203
+ are shown struck through. They are excluded from the snapshot, not
204
+ hidden — a prompt created during a run cannot become an input to a
205
+ segment of that run.
206
+ </p>
207
+ )}
208
+ <p className="vi-script-note">
209
+ {run.ledger.length} prompt(s) have been minted inside this run and are
210
+ recorded in its ledger. Any of them is refused re-entry into this
211
+ snapshot by digest.
212
+ </p>
213
+ </section>
214
+ );
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Panel 3 — the artifact editor
219
+ // ---------------------------------------------------------------------------
220
+
221
+ function ArtifactEditor({
222
+ stage,
223
+ title,
224
+ run,
225
+ busy,
226
+ canAuthor,
227
+ onAuthor,
228
+ onSave,
229
+ }: {
230
+ stage: string;
231
+ title: string;
232
+ run: RunState;
233
+ busy: string | null;
234
+ canAuthor: boolean;
235
+ onAuthor?: () => void;
236
+ onSave(json: unknown): void;
237
+ }) {
238
+ const entry = run.artifacts?.[stage];
239
+ const [draft, setDraft] = useState<string>("");
240
+ const [parseError, setParseError] = useState<string | null>(null);
241
+ const [open, setOpen] = useState(false);
242
+
243
+ useEffect(() => {
244
+ setDraft(entry?.payload ? pretty(entry.payload) : "");
245
+ setParseError(null);
246
+ }, [entry?.digest, stage]);
247
+
248
+ const locked = run.locked;
249
+ const gap = entry?.gap ?? null;
250
+
251
+ function save() {
252
+ let parsed: unknown;
253
+ try {
254
+ parsed = JSON.parse(draft);
255
+ } catch (e) {
256
+ setParseError(
257
+ `That is not JSON: ${e instanceof Error ? e.message : String(e)}`,
258
+ );
259
+ return;
260
+ }
261
+ setParseError(null);
262
+ onSave(parsed);
263
+ }
264
+
265
+ return (
266
+ <section className="vi-script-panel">
267
+ <h3>
268
+ {title}
269
+ <span className="vi-script-sub">
270
+ {entry?.digest ? (
271
+ <>
272
+ {entry.provenance} · <code>{short(entry.digest)}</code> ·{" "}
273
+ {entry.at}
274
+ </>
275
+ ) : gap ? (
276
+ "last attempt ended in a gap"
277
+ ) : (
278
+ "not built yet"
279
+ )}
280
+ </span>
281
+ </h3>
282
+
283
+ <div className="vi-script-row">
284
+ {onAuthor && (
285
+ <button
286
+ type="button"
287
+ className="vi-btn vi-btn-accent"
288
+ disabled={locked || !canAuthor || busy != null}
289
+ onClick={onAuthor}
290
+ >
291
+ {busy === `${stage}.author` ? "Authoring…" : `Author ${stage}`}
292
+ </button>
293
+ )}
294
+ <button
295
+ type="button"
296
+ className="vi-btn"
297
+ onClick={() => setOpen((v) => !v)}
298
+ >
299
+ {open ? "Hide JSON" : locked ? "View JSON (read-only)" : "Edit JSON"}
300
+ </button>
301
+ {locked && (
302
+ <span className="vi-script-tag vi-script-tag-on">
303
+ locked — edits go through a revision
304
+ </span>
305
+ )}
306
+ </div>
307
+
308
+ {gap && (
309
+ <div className="vi-script-gap">
310
+ <strong>{gap.code}</strong>
311
+ {gap.errors.map((e, i) => (
312
+ <div key={i}>{e}</div>
313
+ ))}
314
+ {gap.raw && (
315
+ <>
316
+ <p className="vi-script-note">
317
+ The model's raw reply is kept — nothing was coerced from it.
318
+ </p>
319
+ <pre className="vi-script-raw">{gap.raw}</pre>
320
+ </>
321
+ )}
322
+ </div>
323
+ )}
324
+
325
+ {open && (
326
+ <>
327
+ <textarea
328
+ className="vi-script-json"
329
+ value={draft}
330
+ readOnly={locked}
331
+ disabled={busy != null}
332
+ spellCheck={false}
333
+ onChange={(e) => setDraft(e.target.value)}
334
+ aria-label={`${title} JSON`}
335
+ />
336
+ {parseError && (
337
+ <p className="vi-error" role="alert">
338
+ {parseError}
339
+ </p>
340
+ )}
341
+ {!locked && (
342
+ <div className="vi-script-row">
343
+ <button
344
+ type="button"
345
+ className="vi-btn vi-btn-accent"
346
+ disabled={busy != null || !draft.trim()}
347
+ onClick={save}
348
+ >
349
+ {busy === `${stage}.put` ? "Validating…" : `Save ${stage}`}
350
+ </button>
351
+ <span className="vi-script-note">
352
+ Validated through the same constructor the model's reply goes
353
+ through. There is no lenient path.
354
+ </span>
355
+ </div>
356
+ )}
357
+ </>
358
+ )}
359
+ </section>
360
+ );
361
+ }
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // Panel 4 — continuity + shot plan viewers
365
+ // ---------------------------------------------------------------------------
366
+
367
+ function ContinuityViewer({ run }: { run: RunState }) {
368
+ const entry = run.artifacts?.continuity;
369
+ const payload = asRecord(entry?.payload);
370
+ const entries = (payload.entries as Array<Record<string, unknown>>) ?? [];
371
+ if (!entry) return null;
372
+ return (
373
+ <section className="vi-script-panel">
374
+ <h3>
375
+ Continuity bible
376
+ <span className="vi-script-sub">
377
+ derived from the screenplay, never authored ·{" "}
378
+ <code>{short(entry.digest)}</code>
379
+ </span>
380
+ </h3>
381
+ <table className="vi-script-table">
382
+ <thead>
383
+ <tr>
384
+ <th>segment</th>
385
+ <th>state_before</th>
386
+ <th>state_after</th>
387
+ </tr>
388
+ </thead>
389
+ <tbody>
390
+ {entries.map((e, i) => (
391
+ <tr key={String(e.segment_id ?? i)}>
392
+ <td>
393
+ <code>{String(e.segment_id ?? "")}</code>
394
+ </td>
395
+ <td>
396
+ <pre className="vi-script-seg-prompt">
397
+ {pretty(e.state_before)}
398
+ </pre>
399
+ </td>
400
+ <td>
401
+ <pre className="vi-script-seg-prompt">
402
+ {pretty(e.state_after)}
403
+ </pre>
404
+ </td>
405
+ </tr>
406
+ ))}
407
+ </tbody>
408
+ </table>
409
+ </section>
410
+ );
411
+ }
412
+
413
+ function ShotPlanViewer({ run }: { run: RunState }) {
414
+ const entry = run.artifacts?.shot_plan;
415
+ const payload = asRecord(entry?.payload);
416
+ const designs = (payload.designs as Array<Record<string, unknown>>) ?? [];
417
+ if (!entry) return null;
418
+ const audioFirst = Boolean(payload.audio_first);
419
+ return (
420
+ <section className="vi-script-panel">
421
+ <h3>
422
+ Shot plan
423
+ <span className="vi-script-sub">
424
+ <code>{short(entry.digest)}</code> ·{" "}
425
+ {audioFirst
426
+ ? "cut to the locked audio"
427
+ : "every window is an ESTIMATE (no audio master yet)"}
428
+ </span>
429
+ </h3>
430
+ <table className="vi-script-table">
431
+ <thead>
432
+ <tr>
433
+ <th>segment</th>
434
+ <th>scene</th>
435
+ <th>window</th>
436
+ <th>camera</th>
437
+ <th>block / light</th>
438
+ </tr>
439
+ </thead>
440
+ <tbody>
441
+ {designs.map((d, i) => (
442
+ <tr key={String(d.segment_id ?? i)}>
443
+ <td>
444
+ <code>{String(d.segment_id ?? "")}</code>
445
+ {d.estimated ? (
446
+ <>
447
+ {" "}
448
+ <span className="vi-script-tag vi-script-tag-off">
449
+ estimated
450
+ </span>
451
+ </>
452
+ ) : null}
453
+ </td>
454
+ <td>{String(d.scene_id ?? "")}</td>
455
+ <td>
456
+ {String(d.start_s ?? 0)}s – {String(d.end_s ?? 0)}s
457
+ </td>
458
+ <td className="vi-script-digest">{pretty(d.camera)}</td>
459
+ <td>
460
+ <div>{String(d.blocking ?? "")}</div>
461
+ <div className="vi-script-note">{String(d.lighting ?? "")}</div>
462
+ </td>
463
+ </tr>
464
+ ))}
465
+ </tbody>
466
+ </table>
467
+ </section>
468
+ );
469
+ }
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // Panel 5 — the sibling fan-out
473
+ // ---------------------------------------------------------------------------
474
+
475
+ /**
476
+ * `key` is declared on the props of the two components below because this
477
+ * project has no `@types/react` installed (see package.json — React's types are
478
+ * absent, not merely loose), so TS has no `JSX.IntrinsicAttributes` carrying
479
+ * `key` and rejects it as an excess property on a locally-typed component.
480
+ * Every other station only puts `key` on intrinsic elements or on react-router
481
+ * components that ship their own types, which is why nothing else hits this.
482
+ * Declaring it is the one-line fix that keeps `npx tsc --noEmit` honest without
483
+ * adding a dependency or restructuring the markup around a limitation.
484
+ */
485
+ function AttemptLine({ attempt }: { key?: string | number; attempt: AttemptRow }) {
486
+ const cls = attempt.gap
487
+ ? "vi-script-attempt-gap"
488
+ : attempt.ok
489
+ ? "vi-script-attempt-ok"
490
+ : "";
491
+ return (
492
+ <li className={cls}>
493
+ #{attempt.attempt} · {attempt.kind} · seed {attempt.seed} ·{" "}
494
+ {attempt.model_id ?? "no model"} ·{" "}
495
+ {Object.entries(attempt.params ?? {})
496
+ .map(([k, v]) => `${k}=${String(v)}`)
497
+ .join(" ") || "no params"}
498
+ {attempt.gap && (
499
+ <div className="vi-script-gap">
500
+ <strong>
501
+ {attempt.gap.code}
502
+ {attempt.gap.capability ? ` · ${attempt.gap.capability}` : ""}
503
+ </strong>
504
+ {(attempt.gap.reasons ?? []).map((r, i) => (
505
+ <div key={i}>{r}</div>
506
+ ))}
507
+ {attempt.gap.requirement && (
508
+ <p className="vi-script-note">{attempt.gap.requirement}</p>
509
+ )}
510
+ </div>
511
+ )}
512
+ {attempt.artifacts?.length > 0 && (
513
+ <div className="vi-script-digest">
514
+ {attempt.artifacts
515
+ .map((a) => String(asRecord(a).uri ?? ""))
516
+ .filter(Boolean)
517
+ .join(" ")}
518
+ </div>
519
+ )}
520
+ </li>
521
+ );
522
+ }
523
+
524
+ function SegmentCard({
525
+ row,
526
+ attempts,
527
+ busy,
528
+ onRegenerate,
529
+ onPromote,
530
+ }: {
531
+ key?: string | number;
532
+ row: SegmentRow;
533
+ attempts: AttemptRow[];
534
+ busy: boolean;
535
+ onRegenerate(kind: string): void;
536
+ onPromote(): void;
537
+ }) {
538
+ const [showParents, setShowParents] = useState(false);
539
+ const last = attempts.length > 0 ? attempts[attempts.length - 1] : null;
540
+ const siblings = last?.siblings_after ?? {};
541
+ return (
542
+ <article className={`vi-script-seg${busy ? " vi-script-seg-busy" : ""}`}>
543
+ <header className="vi-script-seg-head">
544
+ <span>
545
+ <code>{row.segment_id}</code>{" "}
546
+ <span className="vi-script-tag">#{row.index}</span>
547
+ </span>
548
+ <span className="vi-script-tag">seed {row.seed_base}</span>
549
+ </header>
550
+ <p className="vi-script-seg-prompt">{row.prompt}</p>
551
+ <div className="vi-script-digest">
552
+ spec {short(row.digest)} · lock {short(row.lock_digest)} · joint{" "}
553
+ {row.joint_mode} · window {row.window[0]}s–{row.window[1]}s
554
+ </div>
555
+ <div className="vi-script-row">
556
+ <button
557
+ type="button"
558
+ className="vi-btn vi-btn-sm"
559
+ onClick={() => setShowParents((v) => !v)}
560
+ >
561
+ {showParents ? "Hide" : "Show"} provenance ({row.parents.length})
562
+ </button>
563
+ <button
564
+ type="button"
565
+ className="vi-btn vi-btn-sm vi-btn-accent"
566
+ disabled={busy}
567
+ onClick={() => onRegenerate("keyframe")}
568
+ >
569
+ {busy ? "Regenerating…" : "Regenerate keyframe"}
570
+ </button>
571
+ <button
572
+ type="button"
573
+ className="vi-btn vi-btn-sm"
574
+ disabled={busy}
575
+ onClick={() => onRegenerate("clip")}
576
+ >
577
+ Render clip
578
+ </button>
579
+ </div>
580
+ {showParents && (
581
+ <div className="vi-script-digest">
582
+ {row.parents.map((p) => (
583
+ <div key={p}>{p}</div>
584
+ ))}
585
+ <p className="vi-script-note">
586
+ Every parent is a locked artifact. No sibling segment appears here,
587
+ and there is no field on a segment spec that one could appear in.
588
+ </p>
589
+ </div>
590
+ )}
591
+ {attempts.length > 0 ? (
592
+ <ul className="vi-script-attempts">
593
+ {attempts.map((a) => (
594
+ <AttemptLine key={a.attempt} attempt={a} />
595
+ ))}
596
+ </ul>
597
+ ) : (
598
+ <p className="vi-script-note">No attempt yet.</p>
599
+ )}
600
+ {last && (
601
+ <p className="vi-script-note">
602
+ siblings at the last attempt:{" "}
603
+ {Object.keys(siblings).length === 0
604
+ ? "none"
605
+ : Object.entries(siblings)
606
+ .map(([id, d]) => `${id} ${short(d)}`)
607
+ .join(" · ")}{" "}
608
+ — {last.siblings_unchanged ? "unchanged" : "CHANGED"}
609
+ </p>
610
+ )}
611
+ <div className="vi-script-row">
612
+ <button
613
+ type="button"
614
+ className="vi-btn vi-btn-sm"
615
+ disabled={busy}
616
+ onClick={onPromote}
617
+ >
618
+ Promote…
619
+ </button>
620
+ </div>
621
+ </article>
622
+ );
623
+ }
624
+
625
+ function SegmentsPanel({
626
+ run,
627
+ segmentBusy,
628
+ onCompile,
629
+ onRegenerate,
630
+ onPromote,
631
+ busy,
632
+ }: {
633
+ run: RunState;
634
+ segmentBusy: string | null;
635
+ busy: string | null;
636
+ onCompile(): void;
637
+ onRegenerate(id: string, kind: string): void;
638
+ onPromote(id: string): void;
639
+ }) {
640
+ const segments = run.segments;
641
+ return (
642
+ <section className="vi-script-panel">
643
+ <h3>
644
+ Segments
645
+ <span className="vi-script-sub">
646
+ siblings of one locked production — never a chain
647
+ </span>
648
+ </h3>
649
+ <div className="vi-script-row">
650
+ <button
651
+ type="button"
652
+ className="vi-btn vi-btn-accent"
653
+ disabled={!run.locked || busy != null}
654
+ onClick={onCompile}
655
+ >
656
+ {busy === "segments.compile"
657
+ ? "Compiling…"
658
+ : segments
659
+ ? "Recompile from the lock"
660
+ : "Compile segments"}
661
+ </button>
662
+ {!run.locked && (
663
+ <span className="vi-script-note">
664
+ Segments are compiled from the production lock. Lock the production
665
+ first.
666
+ </span>
667
+ )}
668
+ </div>
669
+
670
+ {segments && (
671
+ <>
672
+ <div className="vi-script-fanout">
673
+ <div className="vi-script-parent">
674
+ <h4>Locked artifacts</h4>
675
+ <div className="vi-script-digest">
676
+ production_lock {short(segments.lock_digest)} · revision{" "}
677
+ {segments.revision} · {segments.parent_digests.length} parent
678
+ digests
679
+ </div>
680
+ <p className="vi-script-note">{segments.sibling_shape.note}</p>
681
+ </div>
682
+ <div className="vi-script-stem" />
683
+ <div className="vi-script-children">
684
+ {segments.specs.map((row) => (
685
+ <SegmentCard
686
+ key={row.segment_id}
687
+ row={row}
688
+ attempts={run.attempts?.[row.segment_id] ?? []}
689
+ busy={segmentBusy === row.segment_id}
690
+ onRegenerate={(kind) => onRegenerate(row.segment_id, kind)}
691
+ onPromote={() => onPromote(row.segment_id)}
692
+ />
693
+ ))}
694
+ </div>
695
+ </div>
696
+
697
+ <h3 style={{ marginTop: "1rem" }}>
698
+ Plan graph
699
+ <span className="vi-script-sub">
700
+ {segments.graph.nodes.length} nodes ·{" "}
701
+ {segments.validation.ok === true
702
+ ? "validator: clean"
703
+ : segments.validation.ok === false
704
+ ? "validator: reported findings"
705
+ : "validator: could not run"}
706
+ </span>
707
+ </h3>
708
+ {(segments.validation.errors ?? []).length > 0 && (
709
+ <ul className="vi-script-limits">
710
+ {(segments.validation.errors ?? []).map((e, i) => (
711
+ <li key={i}>
712
+ <code>{String(asRecord(e).code ?? "")}</code>{" "}
713
+ {String(asRecord(e).message ?? "")}{" "}
714
+ {asRecord(e).node_id ? (
715
+ <em>({String(asRecord(e).node_id)})</em>
716
+ ) : null}
717
+ </li>
718
+ ))}
719
+ </ul>
720
+ )}
721
+ {segments.validation.note && (
722
+ <p className="vi-script-note">{segments.validation.note}</p>
723
+ )}
724
+ <p className="vi-script-note">
725
+ sequential batches: {segments.execution_order.sequential.length} ·
726
+ parallel batches: {segments.execution_order.parallel.length} — the
727
+ same graph, batched differently.
728
+ </p>
729
+ </>
730
+ )}
731
+ </section>
732
+ );
733
+ }
734
+
735
+ // ---------------------------------------------------------------------------
736
+ // The station
737
+ // ---------------------------------------------------------------------------
738
+
739
+ export function ScriptFirstStation({ spec }: { spec: StationSpec }) {
740
+ const api = useScriptFirstRun();
741
+ const { run } = api;
742
+
743
+ const [deliverable, setDeliverable] = useState("");
744
+ const [requirements, setRequirements] = useState("");
745
+ const [sourceText, setSourceText] = useState("");
746
+ const [reason, setReason] = useState("");
747
+ const [promoting, setPromoting] = useState<string | null>(null);
748
+ const [promoteNote, setPromoteNote] = useState("");
749
+ const [audioDraft, setAudioDraft] = useState("");
750
+ const [showAudio, setShowAudio] = useState(false);
751
+
752
+ const lastPromotion = useMemo(
753
+ () =>
754
+ run && run.promotions.length > 0
755
+ ? run.promotions[run.promotions.length - 1]
756
+ : null,
757
+ [run],
758
+ );
759
+
760
+ const journalled = run?.last_refusal ?? null;
761
+ const refusal = api.refusal;
762
+
763
+ return (
764
+ <section className="station-card station-wide vi-script" aria-label={spec.title}>
765
+ <p className="station-phase">{spec.phase}</p>
766
+ <p className="station-blurb">{spec.blurb}</p>
767
+
768
+ {/* ---- run picker + create ---- */}
769
+ <section className="vi-script-panel">
770
+ <h3>
771
+ Runs
772
+ <span className="vi-script-sub">
773
+ a run is immutable once started — a new source means a new run
774
+ </span>
775
+ </h3>
776
+ <div className="vi-script-runs">
777
+ {api.runs.map((r) => (
778
+ <button
779
+ key={r.run_id}
780
+ type="button"
781
+ className={`vi-btn vi-btn-sm vi-script-runbtn${
782
+ api.runId === r.run_id ? " vi-btn-on" : ""
783
+ }`}
784
+ onClick={() => api.select(r.run_id)}
785
+ >
786
+ <code>{r.run_id}</code> · {r.locked ? "locked" : "open"} ·{" "}
787
+ {r.segments} seg · {r.attempts} attempts
788
+ </button>
789
+ ))}
790
+ {api.runs.length === 0 && (
791
+ <span className="vi-script-note">No runs on this box yet.</span>
792
+ )}
793
+ </div>
794
+ <div className="vi-script-row">
795
+ <input
796
+ className="vi-knob-input vi-script-grow"
797
+ placeholder="Deliverable (what is being asked for)"
798
+ value={deliverable}
799
+ onChange={(e) => setDeliverable(e.target.value)}
800
+ />
801
+ <input
802
+ className="vi-knob-input vi-script-grow"
803
+ placeholder="Requirements"
804
+ value={requirements}
805
+ onChange={(e) => setRequirements(e.target.value)}
806
+ />
807
+ </div>
808
+ <div className="vi-script-row">
809
+ <input
810
+ className="vi-knob-input vi-script-grow"
811
+ placeholder="An existing prompt to capture (optional)"
812
+ value={sourceText}
813
+ onChange={(e) => setSourceText(e.target.value)}
814
+ />
815
+ <button
816
+ type="button"
817
+ className="vi-btn vi-btn-accent"
818
+ disabled={!deliverable.trim() || api.busy != null}
819
+ onClick={() => {
820
+ void api
821
+ .createRun({
822
+ deliverable,
823
+ requirements,
824
+ sources: sourceText.trim()
825
+ ? [{ prompt_id: "operator-1", text: sourceText }]
826
+ : [],
827
+ })
828
+ .then((id) => {
829
+ if (id) api.select(id);
830
+ });
831
+ }}
832
+ >
833
+ {api.busy === "runs.create" ? "Starting…" : "Start a run"}
834
+ </button>
835
+ </div>
836
+ {api.sources.length > 0 && (
837
+ <p className="vi-script-note">
838
+ {api.sources.length} promoted source(s) are available to a NEW run:{" "}
839
+ {api.sources.map((s) => s.source_id).join(", ")}
840
+ </p>
841
+ )}
842
+ </section>
843
+
844
+ {(refusal || journalled) && (
845
+ <section className="vi-script-panel" role="alert">
846
+ <h3>
847
+ Refused
848
+ <span className="vi-script-sub">
849
+ {(refusal ?? journalled)?.code}
850
+ </span>
851
+ </h3>
852
+ <p className="vi-error">{(refusal ?? journalled)?.message}</p>
853
+ <pre className="vi-script-pre">
854
+ {((refusal ?? journalled)?.errors ?? []).join("\n")}
855
+ </pre>
856
+ {asRecord(journalled?.detail).requirement && (
857
+ <p className="vi-script-note">
858
+ {String(asRecord(journalled?.detail).requirement)}
859
+ </p>
860
+ )}
861
+ <button type="button" className="vi-btn vi-btn-sm" onClick={api.clearRefusal}>
862
+ Dismiss
863
+ </button>
864
+ </section>
865
+ )}
866
+
867
+ {api.loading && <p className="vi-script-note">Loading run…</p>}
868
+
869
+ {run && (
870
+ <>
871
+ <RunHeader run={run} />
872
+ <SnapshotPanel run={run} />
873
+
874
+ <ArtifactEditor
875
+ stage="plot"
876
+ title="Plot"
877
+ run={run}
878
+ busy={api.busy}
879
+ canAuthor
880
+ onAuthor={() => api.authorPlot(requirements)}
881
+ onSave={api.putPlot}
882
+ />
883
+ <ArtifactEditor
884
+ stage="screenplay"
885
+ title="Screenplay"
886
+ run={run}
887
+ busy={api.busy}
888
+ canAuthor={Boolean(run.artifacts?.plot?.digest)}
889
+ onAuthor={api.authorScreenplay}
890
+ onSave={api.putScreenplay}
891
+ />
892
+
893
+ <section className="vi-script-panel">
894
+ <h3>
895
+ Lock
896
+ <span className="vi-script-sub">
897
+ doc Stage 11 — version and lock the screenplay, the continuity
898
+ bible, the audio master and the shot plan together
899
+ </span>
900
+ </h3>
901
+ <div className="vi-script-row">
902
+ <button
903
+ type="button"
904
+ className="vi-btn"
905
+ disabled={run.locked || api.busy != null}
906
+ onClick={api.buildPreproduction}
907
+ >
908
+ {api.busy === "preproduction"
909
+ ? "Deriving…"
910
+ : "Derive continuity + shot plan"}
911
+ </button>
912
+ <button
913
+ type="button"
914
+ className="vi-btn vi-btn-accent"
915
+ disabled={run.locked || api.busy != null}
916
+ onClick={() => api.lockRun()}
917
+ >
918
+ {api.busy === "lock" ? "Locking…" : "Lock production"}
919
+ </button>
920
+ <button
921
+ type="button"
922
+ className="vi-btn vi-btn-sm"
923
+ onClick={() => setShowAudio((v) => !v)}
924
+ >
925
+ {showAudio ? "Hide" : "Supply"} audio master JSON
926
+ </button>
927
+ </div>
928
+ {showAudio && (
929
+ <>
930
+ <textarea
931
+ className="vi-script-json"
932
+ value={audioDraft}
933
+ spellCheck={false}
934
+ placeholder='{"timeline_digest": "…", "line_timings": [], "tracks": [], "total_seconds": 0, "locked": true}'
935
+ onChange={(e) => setAudioDraft(e.target.value)}
936
+ aria-label="Audio master JSON"
937
+ />
938
+ <div className="vi-script-row">
939
+ <button
940
+ type="button"
941
+ className="vi-btn"
942
+ disabled={api.busy != null || !audioDraft.trim()}
943
+ onClick={() => {
944
+ try {
945
+ api.putAudioMaster(JSON.parse(audioDraft));
946
+ } catch {
947
+ /* the textarea shows its own parse error below */
948
+ }
949
+ }}
950
+ >
951
+ Save audio master
952
+ </button>
953
+ <span className="vi-script-note">
954
+ There is no synthesize button. audio.tts is ineligible on
955
+ this fleet, and a master with fabricated track refs would
956
+ make every downstream shot window a lie inspection cannot
957
+ catch.
958
+ </span>
959
+ </div>
960
+ </>
961
+ )}
962
+ {run.locked && (
963
+ <>
964
+ <table className="vi-script-table">
965
+ <thead>
966
+ <tr>
967
+ <th>revision</th>
968
+ <th>reason</th>
969
+ <th>digest</th>
970
+ <th>at</th>
971
+ </tr>
972
+ </thead>
973
+ <tbody>
974
+ {run.lock_history.map((h) => (
975
+ <tr key={h.digest}>
976
+ <td>{h.revision}</td>
977
+ <td>{h.reason}</td>
978
+ <td className="vi-script-digest">{short(h.digest)}</td>
979
+ <td>{h.at}</td>
980
+ </tr>
981
+ ))}
982
+ </tbody>
983
+ </table>
984
+ <div className="vi-script-row">
985
+ <input
986
+ className="vi-knob-input vi-script-grow"
987
+ placeholder="Reason for this revision (mandatory)"
988
+ value={reason}
989
+ onChange={(e) => setReason(e.target.value)}
990
+ />
991
+ <button
992
+ type="button"
993
+ className="vi-btn"
994
+ disabled={!reason.trim() || api.busy != null}
995
+ onClick={() => {
996
+ api.revise(reason);
997
+ setReason("");
998
+ }}
999
+ >
1000
+ {api.busy === "revise" ? "Revising…" : "Revise"}
1001
+ </button>
1002
+ </div>
1003
+ <p className="vi-script-note">
1004
+ A revision drops the previously compiled segments: they belong
1005
+ to the previous revision and are never re-pointed at this one.
1006
+ </p>
1007
+ </>
1008
+ )}
1009
+ </section>
1010
+
1011
+ <ContinuityViewer run={run} />
1012
+ <ShotPlanViewer run={run} />
1013
+
1014
+ <SegmentsPanel
1015
+ run={run}
1016
+ busy={api.busy}
1017
+ segmentBusy={api.segmentBusy}
1018
+ onCompile={api.compileSegments}
1019
+ onRegenerate={api.regenerate}
1020
+ onPromote={(id) => {
1021
+ setPromoting(id);
1022
+ setPromoteNote("");
1023
+ }}
1024
+ />
1025
+
1026
+ {promoting && (
1027
+ <section className="vi-script-confirm">
1028
+ <strong>
1029
+ Promote <code>{promoting}</code> as a persisted source?
1030
+ </strong>
1031
+ <p>
1032
+ This writes the segment's prompt as a source a NEW run can
1033
+ snapshot. It will be recorded in THIS run's ledger, which makes
1034
+ it structurally inadmissible here: feeding it back into this run
1035
+ is refused by digest. That is the doc's rule — an accepted
1036
+ output influences later work only through a new generation run.
1037
+ </p>
1038
+ <input
1039
+ className="vi-knob-input"
1040
+ placeholder="Why is this accepted? (recorded on the source)"
1041
+ value={promoteNote}
1042
+ onChange={(e) => setPromoteNote(e.target.value)}
1043
+ />
1044
+ <div className="vi-script-row">
1045
+ <button
1046
+ type="button"
1047
+ className="vi-btn vi-btn-accent"
1048
+ disabled={api.busy != null}
1049
+ onClick={() => {
1050
+ api.promote(promoting, "", promoteNote);
1051
+ setPromoting(null);
1052
+ }}
1053
+ >
1054
+ Promote for a new run
1055
+ </button>
1056
+ <button
1057
+ type="button"
1058
+ className="vi-btn"
1059
+ onClick={() => setPromoting(null)}
1060
+ >
1061
+ Cancel
1062
+ </button>
1063
+ </div>
1064
+ </section>
1065
+ )}
1066
+
1067
+ {lastPromotion && (
1068
+ <section className="vi-script-panel">
1069
+ <h3>
1070
+ Promoted
1071
+ <span className="vi-script-sub">{lastPromotion.source_id}</span>
1072
+ </h3>
1073
+ <p className="vi-script-note">{lastPromotion.usable_in}</p>
1074
+ <pre className="vi-script-raw">{lastPromotion.refused_here}</pre>
1075
+ </section>
1076
+ )}
1077
+ </>
1078
+ )}
1079
+ </section>
1080
+ );
1081
+ }