@hugpy/video-intelligence-ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/src/config.ts +36 -0
- package/src/demo/assets/generate/cinema-cavern/movie.mp4 +0 -0
- package/src/demo/assets/generate/cinema-cavern/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/clips/cat-lounge.mp4 +0 -0
- package/src/demo/assets/generate/clips/drone-city.mp4 +0 -0
- package/src/demo/assets/generate/clips/paper-sails.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_02.mp4 +0 -0
- package/src/demo/assets/generate/movie-galaxy/seg_03.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/movie.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_00.mp4 +0 -0
- package/src/demo/assets/generate/movie-medow/seg_01.mp4 +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-desert/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-desert/scene.mp4 +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00000.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00001.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00002.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00003.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00004.png +0 -0
- package/src/demo/assets/generate/scene-vintage/frame_00005.png +0 -0
- package/src/demo/assets/generate/scene-vintage/scene.mp4 +0 -0
- package/src/demo/assets/generate2/s0.png +0 -0
- package/src/demo/assets/generate2/s1.png +0 -0
- package/src/demo/assets/generate2/s10.png +0 -0
- package/src/demo/assets/generate2/s11.png +0 -0
- package/src/demo/assets/generate2/s12.png +0 -0
- package/src/demo/assets/generate2/s13.png +0 -0
- package/src/demo/assets/generate2/s14.png +0 -0
- package/src/demo/assets/generate2/s15.png +0 -0
- package/src/demo/assets/generate2/s16.png +0 -0
- package/src/demo/assets/generate2/s17.png +0 -0
- package/src/demo/assets/generate2/s18.png +0 -0
- package/src/demo/assets/generate2/s19.png +0 -0
- package/src/demo/assets/generate2/s2.png +0 -0
- package/src/demo/assets/generate2/s20.png +0 -0
- package/src/demo/assets/generate2/s21.mp4 +0 -0
- package/src/demo/assets/generate2/s22.mp4 +0 -0
- package/src/demo/assets/generate2/s23.mp4 +0 -0
- package/src/demo/assets/generate2/s24.mp4 +0 -0
- package/src/demo/assets/generate2/s25.mp4 +0 -0
- package/src/demo/assets/generate2/s26.mp4 +0 -0
- package/src/demo/assets/generate2/s27.mp4 +0 -0
- package/src/demo/assets/generate2/s28.mp4 +0 -0
- package/src/demo/assets/generate2/s29.mp4 +0 -0
- package/src/demo/assets/generate2/s3.png +0 -0
- package/src/demo/assets/generate2/s30.mp4 +0 -0
- package/src/demo/assets/generate2/s4.png +0 -0
- package/src/demo/assets/generate2/s5.png +0 -0
- package/src/demo/assets/generate2/s6.png +0 -0
- package/src/demo/assets/generate2/s7.png +0 -0
- package/src/demo/assets/generate2/s8.png +0 -0
- package/src/demo/assets/generate2/s9.png +0 -0
- package/src/demo/demoFetch.ts +126 -3
- package/src/demo/fixtures.ts +72 -4
- package/src/demo/promptAssist.ts +25 -0
- package/src/demo/sampleData.ts +960 -0
- package/src/demo/sampleSets.ts +172 -0
- package/src/stations/ActiveProcessesStation.tsx +18 -3
- package/src/stations/GenerateStation.tsx +1031 -364
- package/src/stations/GoalComposer.tsx +9 -1
- package/src/stations/ScriptFirstStation.tsx +1081 -0
- package/src/stations/ToksPanel.tsx +188 -0
- package/src/stations/WorkbenchDrawer.tsx +4 -3
- package/src/stations/WorkbenchSidebar.tsx +60 -12
- package/src/stations/oracle/DagPanel.tsx +391 -0
- package/src/stations/oracle/OracleStation.tsx +304 -0
- package/src/stations/oracle/SpatialTab.tsx +185 -0
- package/src/stations/oracle/StewardPanel.tsx +165 -0
- package/src/stations/oracle/oracle.css +467 -0
- package/src/stations/oracle/oracleShared.tsx +160 -0
- package/src/stations/oracle/oracleTypes.ts +304 -0
- package/src/stations/oracle/useOracle.ts +560 -0
- package/src/stations/registry.ts +37 -0
- package/src/stations/scriptFirst.css +350 -0
- package/src/stations/studio/IdentitiesStation.tsx +219 -292
- package/src/stations/studio/IdentityCreatePanel.tsx +390 -0
- package/src/stations/studio/StudioGenerateTab.tsx +162 -6
- package/src/stations/studio/StudioMovieComposer.tsx +763 -318
- package/src/stations/studio/{StudioMovieComposer.tsx.bak-sessions-20260812 → StudioMovieComposer.tsx.bak-declutter-20260813} +350 -48
- package/src/stations/studio/StudioPlane.tsx +35 -3
- package/src/stations/studio/StudioViewer.tsx +116 -14
- package/src/stations/studio/coordinationBadge.tsx +206 -0
- package/src/stations/studio/identityUpload.ts +66 -0
- package/src/stations/studio/promptCard.tsx +362 -59
- package/src/stations/studio/useIdentityCreateJob.ts +184 -0
- package/src/stations/studio/useIdentityProfiles.ts +66 -0
- package/src/stations/useScriptFirstRun.ts +596 -0
- package/src/style/app.css +176 -0
- package/src/video/LengthRow.tsx +189 -0
- package/src/video/LibraryGroups.tsx +81 -0
- package/src/video/UnnamedWarn.tsx +146 -0
- package/src/video/coordinationReport.ts +266 -0
- package/src/video/jobClock.ts +104 -0
- package/src/video/spreadAssist.ts +26 -0
- package/src/video/useMediaJobs.ts +26 -0
- package/src/video/useOperatorHistory.ts +105 -0
- package/src/video/useToks.ts +193 -0
- package/src/stations/ActiveProcessesStation.tsx.bak-sessions-20260812 +0 -228
- package/src/stations/GenerateStation.tsx.bak-tester-removal +0 -5170
- package/src/stations/studio/StudioMovieSessions.tsx.bak-sessions-20260812 +0 -417
|
@@ -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
|
+
}
|