@plaintake/scenario 1.23.0 → 1.30.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/README.md +10 -0
- package/dist/index.js +819 -440
- package/dist/schema/baseline.d.ts +182 -0
- package/dist/schema/errors.d.ts +1 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/manifest.d.ts +1 -0
- package/dist/schema/narration-index.d.ts +1 -1
- package/dist/schema/render-plan.d.ts +174 -17
- package/dist/schema/results.d.ts +118 -9
- package/dist/schema/scenario.d.ts +62 -0
- package/dist/types.d.ts +94 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
// ../schema/src/baseline.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var Sha256 = z.string().regex(/^[0-9a-f]{64}$/);
|
|
4
|
+
var DiffCategorySchema = z.enum([
|
|
5
|
+
"step",
|
|
6
|
+
"assertion",
|
|
7
|
+
"target-name",
|
|
8
|
+
"target-role",
|
|
9
|
+
"target-bounds",
|
|
10
|
+
"timing",
|
|
11
|
+
"caption",
|
|
12
|
+
"actor"
|
|
13
|
+
]);
|
|
14
|
+
var SeveritySchema = z.enum(["fail", "report"]);
|
|
15
|
+
var DifferenceSchema = z.object({
|
|
16
|
+
category: DiffCategorySchema,
|
|
17
|
+
severity: SeveritySchema,
|
|
18
|
+
detail: z.string()
|
|
19
|
+
});
|
|
20
|
+
var RectSchema = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() });
|
|
21
|
+
var SnapshotTargetSchema = z.object({
|
|
22
|
+
role: z.string().nullable(),
|
|
23
|
+
accessibleName: z.string().nullable(),
|
|
24
|
+
rect: RectSchema.nullable()
|
|
25
|
+
});
|
|
26
|
+
var SnapshotStepSchema = z.object({
|
|
27
|
+
stableId: z.string().min(1),
|
|
28
|
+
target: SnapshotTargetSchema.nullable(),
|
|
29
|
+
/** `null` for a step that started and never finished. */
|
|
30
|
+
durationMs: z.number().nullable()
|
|
31
|
+
});
|
|
32
|
+
var SemanticSnapshotSchema = z.object({
|
|
33
|
+
/** sha256 of the bundle's `scenario/scenario.ts`; `null` when the bundle has none. */
|
|
34
|
+
scenarioSha256: Sha256.nullable(),
|
|
35
|
+
steps: z.array(SnapshotStepSchema),
|
|
36
|
+
assertions: z.array(z.object({ stableId: z.string().min(1), status: z.enum(["passed", "failed"]) })),
|
|
37
|
+
/** Absent when the bundle carries no render plan — a `check` bundle. */
|
|
38
|
+
captions: z.array(z.object({ id: z.string(), lines: z.array(z.string()) })).optional(),
|
|
39
|
+
actors: z.object({
|
|
40
|
+
ids: z.array(z.string()),
|
|
41
|
+
turnOrder: z.array(z.string()),
|
|
42
|
+
/** Absent without a render plan, or for a single-actor plan. */
|
|
43
|
+
labels: z.array(z.object({ id: z.string(), label: z.string() })).optional()
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
var BASELINE_SCHEMA = "plaintake.baseline/v1";
|
|
47
|
+
var BaselineFileSchema = z.object({
|
|
48
|
+
schema: z.literal(BASELINE_SCHEMA),
|
|
49
|
+
/** Informational only: which PlainTake wrote the file. */
|
|
50
|
+
plaintakeVersion: z.string().min(1),
|
|
51
|
+
snapshot: SemanticSnapshotSchema.extend({ scenarioSha256: Sha256 })
|
|
52
|
+
});
|
|
53
|
+
var BaselineStatusSchema = z.enum(["match", "drift", "scenario-changed", "absent", "skipped", "updated"]);
|
|
54
|
+
var BaselineReportSchema = z.object({
|
|
55
|
+
path: z.string().min(1),
|
|
56
|
+
status: BaselineStatusSchema,
|
|
57
|
+
differences: z.array(DifferenceSchema)
|
|
58
|
+
});
|
|
59
|
+
|
|
1
60
|
// ../schema/src/errors.ts
|
|
2
61
|
var EXIT_CODES = {
|
|
3
62
|
ok: 0,
|
|
@@ -6,7 +65,8 @@ var EXIT_CODES = {
|
|
|
6
65
|
toolchain: 3,
|
|
7
66
|
capture: 4,
|
|
8
67
|
render: 5,
|
|
9
|
-
verification: 6
|
|
68
|
+
verification: 6,
|
|
69
|
+
drift: 7
|
|
10
70
|
};
|
|
11
71
|
var DemoError = class extends Error {
|
|
12
72
|
kind;
|
|
@@ -22,9 +82,9 @@ var DemoError = class extends Error {
|
|
|
22
82
|
};
|
|
23
83
|
|
|
24
84
|
// ../schema/src/event.ts
|
|
25
|
-
import { z } from "zod";
|
|
26
|
-
var NanosString =
|
|
27
|
-
var DemoEventTypeSchema =
|
|
85
|
+
import { z as z2 } from "zod";
|
|
86
|
+
var NanosString = z2.string().regex(/^\d+$/, "must be a digit-only nanosecond string");
|
|
87
|
+
var DemoEventTypeSchema = z2.enum([
|
|
28
88
|
"session.start",
|
|
29
89
|
"session.finish",
|
|
30
90
|
"chapter",
|
|
@@ -54,7 +114,7 @@ var DemoEventTypeSchema = z.enum([
|
|
|
54
114
|
"turn",
|
|
55
115
|
/*
|
|
56
116
|
* A full-frame motion-graphics cut-away: `demo.explain(...)` stops the screencast, splices
|
|
57
|
-
* in a
|
|
117
|
+
* in a rendered motion-graphics scene at this point on the timeline, and resumes the recording
|
|
58
118
|
* on the same page, same actor. A member of its own for the same reason `turn` is one: a
|
|
59
119
|
* cut-away is not a step — no target, no verb, no hold — and its `payload` carries the
|
|
60
120
|
* whole authored request (id, narration, scene props, cues) because the scene is compiled
|
|
@@ -64,23 +124,23 @@ var DemoEventTypeSchema = z.enum([
|
|
|
64
124
|
*/
|
|
65
125
|
"explain"
|
|
66
126
|
]);
|
|
67
|
-
var TargetSchema =
|
|
68
|
-
role:
|
|
69
|
-
accessibleName:
|
|
70
|
-
selector:
|
|
71
|
-
rect:
|
|
127
|
+
var TargetSchema = z2.object({
|
|
128
|
+
role: z2.string().optional(),
|
|
129
|
+
accessibleName: z2.string().optional(),
|
|
130
|
+
selector: z2.string().optional(),
|
|
131
|
+
rect: z2.object({ x: z2.number(), y: z2.number(), width: z2.number(), height: z2.number() }).optional()
|
|
72
132
|
});
|
|
73
|
-
var DemoEventSchema =
|
|
74
|
-
schema:
|
|
75
|
-
seq:
|
|
133
|
+
var DemoEventSchema = z2.object({
|
|
134
|
+
schema: z2.literal("agent-demo.event/v1"),
|
|
135
|
+
seq: z2.number().int().nonnegative(),
|
|
76
136
|
tStartNs: NanosString,
|
|
77
137
|
tEndNs: NanosString.optional(),
|
|
78
138
|
type: DemoEventTypeSchema,
|
|
79
|
-
stableId:
|
|
80
|
-
title:
|
|
81
|
-
subtitle:
|
|
139
|
+
stableId: z2.string().min(1).optional(),
|
|
140
|
+
title: z2.string().optional(),
|
|
141
|
+
subtitle: z2.string().optional(),
|
|
82
142
|
target: TargetSchema.optional(),
|
|
83
|
-
payload:
|
|
143
|
+
payload: z2.record(z2.string(), z2.unknown()).optional(),
|
|
84
144
|
/**
|
|
85
145
|
* Which actor performed this event, for a `turn` hand-off or a verb step taken during one.
|
|
86
146
|
* Absent means the scenario's default actor — a single-actor event log never sets this, so
|
|
@@ -88,27 +148,34 @@ var DemoEventSchema = z.object({
|
|
|
88
148
|
* here: the default actor's id is an application-layer notion the recorder resolves, not a
|
|
89
149
|
* value this schema should invent and materialise on parse.
|
|
90
150
|
*/
|
|
91
|
-
actor:
|
|
151
|
+
actor: z2.string().min(1).optional()
|
|
92
152
|
});
|
|
93
153
|
|
|
94
154
|
// ../schema/src/json-schema.ts
|
|
95
|
-
import { z as
|
|
155
|
+
import { z as z4 } from "zod";
|
|
96
156
|
|
|
97
157
|
// ../schema/src/scenario.ts
|
|
98
|
-
import { z as
|
|
99
|
-
var ScenarioIntroSchema =
|
|
100
|
-
lines:
|
|
101
|
-
narration:
|
|
102
|
-
durationMs:
|
|
158
|
+
import { z as z3 } from "zod";
|
|
159
|
+
var ScenarioIntroSchema = z3.object({
|
|
160
|
+
lines: z3.array(z3.string().min(1)).min(1).max(2, "intro.lines takes at most two lines \u2014 a third would have to shrink the type to fit"),
|
|
161
|
+
narration: z3.string().min(1).optional(),
|
|
162
|
+
durationMs: z3.number().int().positive().max(15e3, "intro.durationMs must be at most 15000ms").optional()
|
|
103
163
|
});
|
|
104
|
-
var
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
164
|
+
var ScenarioHookSchema = z3.union([
|
|
165
|
+
z3.string().min(1),
|
|
166
|
+
z3.object({
|
|
167
|
+
text: z3.string().min(1),
|
|
168
|
+
durationMs: z3.number().int().min(1e3, "hook.durationMs must be at least 1000ms").max(1e4, "hook.durationMs must be at most 10000ms \u2014 past that it is a title, not a hook").optional()
|
|
169
|
+
})
|
|
170
|
+
]);
|
|
171
|
+
var ScenarioCameraSchema = z3.object({
|
|
172
|
+
maxZoom: z3.number().min(1, "camera.maxZoom must be at least 1 (no zoom)").max(1.6, "camera.maxZoom must be at most 1.6 \u2014 beyond that the upscaled capture turns UI text to mush").optional(),
|
|
173
|
+
margin: z3.number().min(0, "camera.margin must be at least 0").max(2, "camera.margin must be at most 2 (as a fraction of the target, per side)").optional(),
|
|
174
|
+
easeMs: z3.number().int().min(133, "camera.easeMs must be at least 133ms").max(2e3, "camera.easeMs must be at most 2000ms").optional(),
|
|
175
|
+
minDwellMs: z3.number().int().min(0, "camera.minDwellMs must be at least 0").max(5e3, "camera.minDwellMs must be at most 5000ms").optional()
|
|
109
176
|
});
|
|
110
|
-
var ScenarioSpeechSchema =
|
|
111
|
-
speed:
|
|
177
|
+
var ScenarioSpeechSchema = z3.object({
|
|
178
|
+
speed: z3.number().min(0.5, "speech.speed must be at least 0.5 (half speed)").max(2, "speech.speed must be at most 2.0 (double speed) \u2014 an unmeasured, conservative ceiling").optional(),
|
|
112
179
|
/*
|
|
113
180
|
* The voices this scenario may switch to mid-run, beyond the run-wide default `--voice`
|
|
114
181
|
* picks. A step names one with `demo.step({ voice })` or an actor with `demo.actor(id,
|
|
@@ -135,24 +202,63 @@ var ScenarioSpeechSchema = z2.object({
|
|
|
135
202
|
* A mix is refused at record time with both sides named — the run's pronunciation
|
|
136
203
|
* dictionary is configured once, for the one language it speaks.
|
|
137
204
|
*/
|
|
138
|
-
voices:
|
|
205
|
+
voices: z3.array(z3.string().min(1, "speech.voices entries must be non-empty voice names")).min(1, "speech.voices must name at least one voice \u2014 an empty list declares nothing, so omit it").optional()
|
|
139
206
|
});
|
|
140
|
-
var
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
207
|
+
var EnvName = z3.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
208
|
+
var TerminalMetaSchema = z3.object({
|
|
209
|
+
shell: z3.enum(["bash", "zsh", "sh"]).default("bash"),
|
|
210
|
+
command: z3.array(z3.string().min(1)).min(1).optional(),
|
|
211
|
+
cols: z3.number().int().min(40).max(200).default(120),
|
|
212
|
+
rows: z3.number().int().min(12).max(60).default(34),
|
|
213
|
+
cwd: z3.string().min(1).optional(),
|
|
214
|
+
env: z3.array(EnvName).default([]),
|
|
215
|
+
secrets: z3.array(EnvName).default([]),
|
|
216
|
+
browser: z3.boolean().optional(),
|
|
217
|
+
label: z3.string().trim().min(1).optional(),
|
|
218
|
+
transition: z3.enum(["cut", "card"]).optional()
|
|
219
|
+
}).superRefine((t, ctx) => {
|
|
220
|
+
for (const field of ["label", "transition"]) {
|
|
221
|
+
if (t[field] !== void 0 && t.browser !== true) {
|
|
222
|
+
ctx.addIssue({ code: "custom", path: [field], message: `terminal.${field} needs terminal.browser: true` });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const name of t.secrets) {
|
|
226
|
+
if (!t.env.includes(name)) {
|
|
227
|
+
ctx.addIssue({ code: "custom", path: ["secrets"], message: `secret ${name} must also be listed in terminal.env` });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
var ScenarioMetaSchema = z3.object({
|
|
232
|
+
schema: z3.literal("agent-demo.scenario/v1"),
|
|
233
|
+
id: z3.string().regex(/^[a-z0-9][a-z0-9-]*$/, "must be a lowercase kebab-case id"),
|
|
234
|
+
title: z3.string().min(1),
|
|
235
|
+
language: z3.string().min(2).default("en"),
|
|
236
|
+
viewport: z3.object({
|
|
237
|
+
width: z3.literal(1920),
|
|
238
|
+
height: z3.literal(1080),
|
|
239
|
+
deviceScaleFactor: z3.literal(1)
|
|
149
240
|
}),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
241
|
+
/*
|
|
242
|
+
* How large the page is drawn inside the fixed 1920x1080 capture. At `s`, the page lays out
|
|
243
|
+
* at a 1920/s x 1080/s CSS viewport with deviceScaleFactor `s`, so Chromium paints every
|
|
244
|
+
* CSS pixel as s x s device pixels: the frame is still 1920x1080, the UI in it is s times
|
|
245
|
+
* larger and sharp, and the app's own responsive layout sees the narrower viewport. It is
|
|
246
|
+
* what a person gets from browser zoom, and what a vertical (`--aspect 9:16`) cut needs to
|
|
247
|
+
* stay legible once the whole frame is letterboxed to 1080 wide.
|
|
248
|
+
*
|
|
249
|
+
* `viewport` above still describes the capture, not the layout, which is why it stays a
|
|
250
|
+
* literal. Only 1.5 and 2 are offered besides 1: both divide 1920x1080 into whole CSS
|
|
251
|
+
* pixels, and both were measured (`ui-scale-dpr-spike.spec.ts`) to capture device-pixel
|
|
252
|
+
* frames headless and headed. Optional and inert when absent: every scenario written
|
|
253
|
+
* before this records exactly as it always did.
|
|
254
|
+
*/
|
|
255
|
+
uiScale: z3.union([z3.literal(1), z3.literal(1.5), z3.literal(2)]).optional(),
|
|
256
|
+
locale: z3.literal("en-US"),
|
|
257
|
+
timezoneId: z3.literal("UTC"),
|
|
258
|
+
colorScheme: z3.literal("light"),
|
|
259
|
+
reducedMotion: z3.literal("reduce"),
|
|
154
260
|
/** Exact console error strings the scenario tolerates. Anything else fails the run. */
|
|
155
|
-
allowedConsoleErrors:
|
|
261
|
+
allowedConsoleErrors: z3.array(z3.string()).default([]),
|
|
156
262
|
/*
|
|
157
263
|
* Whether this scenario hands the real browser window to a person, and in which phase.
|
|
158
264
|
*
|
|
@@ -170,7 +276,7 @@ var ScenarioMetaSchema = z2.object({
|
|
|
170
276
|
* The same precedent as everywhere else here: a `.default()`, so every scenario written
|
|
171
277
|
* before this parses unchanged.
|
|
172
278
|
*/
|
|
173
|
-
handoff:
|
|
279
|
+
handoff: z3.enum(["none", "preflight", "session"]).default("none"),
|
|
174
280
|
/*
|
|
175
281
|
* How long a handoff waits for the person before giving up. Not a patience limit —
|
|
176
282
|
* past a few minutes, a block is indistinguishable from a hang, and the person is better
|
|
@@ -179,7 +285,7 @@ var ScenarioMetaSchema = z2.object({
|
|
|
179
285
|
* In the schema rather than only in the runtime so `validate` catches an out-of-range
|
|
180
286
|
* value with no browser and no waiting.
|
|
181
287
|
*/
|
|
182
|
-
handoffTimeoutMs:
|
|
288
|
+
handoffTimeoutMs: z3.number().int().min(5e3, "handoffTimeoutMs must be at least 5000ms").max(
|
|
183
289
|
3e5,
|
|
184
290
|
"handoffTimeoutMs must be at most 300000ms (5 minutes) \u2014 past that a wait is indistinguishable from a hang"
|
|
185
291
|
).default(12e4),
|
|
@@ -193,6 +299,12 @@ var ScenarioMetaSchema = z2.object({
|
|
|
193
299
|
* discarded in silence rather than refused.
|
|
194
300
|
*/
|
|
195
301
|
intro: ScenarioIntroSchema.optional(),
|
|
302
|
+
/*
|
|
303
|
+
* The hook, drawn over the top band of a portrait render. Optional on the same precedent as
|
|
304
|
+
* `intro`; a scenario without one renders exactly what it always did, and a 16:9 render of one
|
|
305
|
+
* that has it carries it undrawn.
|
|
306
|
+
*/
|
|
307
|
+
hook: ScenarioHookSchema.optional(),
|
|
196
308
|
/*
|
|
197
309
|
* Framing overrides for this scenario's pages. Optional like everything above it, and
|
|
198
310
|
* inert when absent: the defaults are the constants the camera has always used.
|
|
@@ -208,6 +320,8 @@ var ScenarioMetaSchema = z2.object({
|
|
|
208
320
|
* comment for the field's shape and the reasoning behind `speed`'s bounds.
|
|
209
321
|
*/
|
|
210
322
|
speech: ScenarioSpeechSchema.optional(),
|
|
323
|
+
/* A live terminal as the recorded source instead of a web page; see `TerminalMetaSchema`. */
|
|
324
|
+
terminal: TerminalMetaSchema.optional(),
|
|
211
325
|
/*
|
|
212
326
|
* A text-substitution dictionary applied to narration immediately before synthesis — not the
|
|
213
327
|
* vendored en-us phoneme dictionary `install-voice` fetches (a different thing entirely: that
|
|
@@ -227,120 +341,167 @@ var ScenarioMetaSchema = z2.object({
|
|
|
227
341
|
*
|
|
228
342
|
* Optional and inert when absent, the same precedent as everything else on this schema.
|
|
229
343
|
*/
|
|
230
|
-
pronunciations:
|
|
344
|
+
pronunciations: z3.record(z3.string().min(1, "pronunciation keys must not be empty"), z3.string()).optional()
|
|
231
345
|
});
|
|
232
346
|
|
|
233
347
|
// ../schema/src/manifest.ts
|
|
234
|
-
import { z as
|
|
235
|
-
var
|
|
236
|
-
var BundleManifestSchema =
|
|
237
|
-
schema:
|
|
238
|
-
status:
|
|
239
|
-
scenario:
|
|
240
|
-
toolchain:
|
|
241
|
-
node:
|
|
242
|
-
playwright:
|
|
243
|
-
chromiumRevision:
|
|
244
|
-
ffmpeg:
|
|
348
|
+
import { z as z5 } from "zod";
|
|
349
|
+
var Sha2562 = z5.string().regex(/^[0-9a-f]{64}$/);
|
|
350
|
+
var BundleManifestSchema = z5.object({
|
|
351
|
+
schema: z5.literal("agent-demo.bundle/v1"),
|
|
352
|
+
status: z5.enum(["passed", "failed"]),
|
|
353
|
+
scenario: z5.object({ id: z5.string().min(1), sourceSha256: Sha2562 }),
|
|
354
|
+
toolchain: z5.object({
|
|
355
|
+
node: z5.string(),
|
|
356
|
+
playwright: z5.string(),
|
|
357
|
+
chromiumRevision: z5.string(),
|
|
358
|
+
ffmpeg: z5.string(),
|
|
245
359
|
/** May be the literal "unknown" — never a fabricated version. */
|
|
246
|
-
libass:
|
|
247
|
-
fontSha256:
|
|
248
|
-
containerImage:
|
|
360
|
+
libass: z5.string(),
|
|
361
|
+
fontSha256: Sha2562,
|
|
362
|
+
containerImage: z5.string().optional(),
|
|
249
363
|
/**
|
|
250
|
-
* The plainmotion CLI's own `--version` string, present
|
|
251
|
-
* `demo.explain()`
|
|
252
|
-
*
|
|
253
|
-
*
|
|
364
|
+
* The plainmotion CLI's own `--version` string, present on bundles recorded by 1.28.0 or earlier
|
|
365
|
+
* whose scenario called `demo.explain()` — explain scenes were then rendered by that
|
|
366
|
+
* external CLI. Read-only now: explain scenes render in process, so nothing writes it, and
|
|
367
|
+
* it stays optional so those bundles keep verifying.
|
|
254
368
|
*/
|
|
255
|
-
plainmotion:
|
|
369
|
+
plainmotion: z5.string().optional()
|
|
256
370
|
}),
|
|
257
|
-
environment:
|
|
258
|
-
os:
|
|
371
|
+
environment: z5.object({
|
|
372
|
+
os: z5.string(),
|
|
259
373
|
/** Part of the reproducibility claim: byte-identity holds per architecture. */
|
|
260
|
-
architecture:
|
|
261
|
-
locale:
|
|
262
|
-
timezone:
|
|
263
|
-
|
|
264
|
-
|
|
374
|
+
architecture: z5.string(),
|
|
375
|
+
locale: z5.literal("en-US"),
|
|
376
|
+
timezone: z5.literal("UTC"),
|
|
377
|
+
/*
|
|
378
|
+
* The captured frame: 1920x1080 pixels at one pixel per video pixel, always. `uiScale`
|
|
379
|
+
* below says how the page was laid out into that frame; it never changes these two.
|
|
380
|
+
*/
|
|
381
|
+
viewport: z5.tuple([z5.literal(1920), z5.literal(1080)]),
|
|
382
|
+
deviceScaleFactor: z5.literal(1),
|
|
383
|
+
/*
|
|
384
|
+
* The scenario's `uiScale`, written only when it is not 1 — so every manifest recorded
|
|
385
|
+
* before the option existed, and every one recorded without it, stays byte-identical.
|
|
386
|
+
* Recorded because it changes what every target rect in the bundle was measured against
|
|
387
|
+
* (CSS pixels times this), which a diff between two bundles must not mistake for drift.
|
|
388
|
+
*/
|
|
389
|
+
uiScale: z5.union([z5.literal(1.5), z5.literal(2)]).optional()
|
|
265
390
|
}),
|
|
266
|
-
assertions:
|
|
267
|
-
artifacts:
|
|
268
|
-
|
|
269
|
-
path:
|
|
270
|
-
sha256:
|
|
271
|
-
bytes:
|
|
272
|
-
role:
|
|
391
|
+
assertions: z5.array(z5.object({ id: z5.string().min(1), status: z5.enum(["passed", "failed"]) })),
|
|
392
|
+
artifacts: z5.array(
|
|
393
|
+
z5.object({
|
|
394
|
+
path: z5.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "must be a relative POSIX path"),
|
|
395
|
+
sha256: Sha2562,
|
|
396
|
+
bytes: z5.number().int().nonnegative(),
|
|
397
|
+
role: z5.enum(["source", "derived"])
|
|
273
398
|
})
|
|
274
399
|
)
|
|
275
400
|
});
|
|
276
401
|
|
|
277
402
|
// ../schema/src/narration-index.ts
|
|
278
|
-
import { z as
|
|
403
|
+
import { z as z6 } from "zod";
|
|
279
404
|
var NARRATION_INDEX_SCHEMA = "agent-demo.narration-index/v1";
|
|
280
|
-
var NarrationIndexClipSchema =
|
|
405
|
+
var NarrationIndexClipSchema = z6.object({
|
|
281
406
|
/** The step id, which is also the clip id and the cache filename stem. */
|
|
282
|
-
id:
|
|
407
|
+
id: z6.string().min(1),
|
|
283
408
|
/**
|
|
284
409
|
* The marker-free, normalised line — `clipFor`'s `text`. Never empty: the runtime does not call
|
|
285
410
|
* the narrator for a step that normalises to nothing, so an empty line here is a corrupt index.
|
|
286
411
|
*/
|
|
287
|
-
text:
|
|
412
|
+
text: z6.string().min(1),
|
|
288
413
|
/**
|
|
289
414
|
* The author's `[pause]` segments, present only when a marker actually split the line — which is
|
|
290
415
|
* two or more pieces, never one. When present it is what the cache key is built from (the pieces
|
|
291
416
|
* join by NUL), so it must round-trip exactly or a warmed marked line lands under a different key
|
|
292
417
|
* from the one the recording will look for.
|
|
293
418
|
*/
|
|
294
|
-
breaks:
|
|
419
|
+
breaks: z6.array(z6.string().min(1)).min(2).optional(),
|
|
295
420
|
/**
|
|
296
421
|
* The resolved voice, present only when it was not the run's default (`speech.voice` below).
|
|
297
422
|
* Absent means "spoken by the default", exactly as it does on the frozen plan's per-clip voice.
|
|
298
423
|
*/
|
|
299
|
-
voice:
|
|
424
|
+
voice: z6.string().min(1).optional()
|
|
300
425
|
});
|
|
301
|
-
var NarrationIndexSchema =
|
|
302
|
-
schema:
|
|
303
|
-
speech:
|
|
304
|
-
voice:
|
|
305
|
-
voices:
|
|
306
|
-
speed:
|
|
307
|
-
dtype:
|
|
308
|
-
pronunciations:
|
|
426
|
+
var NarrationIndexSchema = z6.object({
|
|
427
|
+
schema: z6.literal(NARRATION_INDEX_SCHEMA),
|
|
428
|
+
speech: z6.object({
|
|
429
|
+
voice: z6.string().min(1),
|
|
430
|
+
voices: z6.array(z6.string().min(1)).optional(),
|
|
431
|
+
speed: z6.number().positive(),
|
|
432
|
+
dtype: z6.string().min(1),
|
|
433
|
+
pronunciations: z6.record(z6.string().min(1), z6.string()).optional()
|
|
309
434
|
}),
|
|
310
|
-
clips:
|
|
435
|
+
clips: z6.array(NarrationIndexClipSchema).min(1)
|
|
311
436
|
});
|
|
312
437
|
|
|
313
438
|
// ../schema/src/render-plan.ts
|
|
314
|
-
import { z as
|
|
315
|
-
var CueSchema =
|
|
316
|
-
id:
|
|
317
|
-
startMs:
|
|
318
|
-
endMs:
|
|
319
|
-
lines:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
439
|
+
import { z as z7 } from "zod";
|
|
440
|
+
var CueSchema = z7.object({
|
|
441
|
+
id: z7.string().min(1),
|
|
442
|
+
startMs: z7.number().int().nonnegative(),
|
|
443
|
+
endMs: z7.number().int().positive(),
|
|
444
|
+
lines: z7.array(z7.string()).min(1).max(2),
|
|
445
|
+
/**
|
|
446
|
+
* When each word of `lines` is spoken, in video milliseconds — one entry per space-separated
|
|
447
|
+
* word of the lines joined, in order. Frozen only by a narrated recording under the social
|
|
448
|
+
* layout, whose captions light each word as the voice reaches it (`toAss`); absent everywhere
|
|
449
|
+
* else, so every other plan's cues serialise exactly as they did.
|
|
450
|
+
*
|
|
451
|
+
* Times, never invented: they are the synthesiser's own word timestamps, mapped back through
|
|
452
|
+
* the pronunciation dictionary onto the caption's words. A cue whose words those timestamps
|
|
453
|
+
* do not describe gets no `words` at all and is drawn whole, which is what a silent recording
|
|
454
|
+
* gets too.
|
|
455
|
+
*/
|
|
456
|
+
words: z7.array(z7.number().int().nonnegative()).min(1).optional()
|
|
457
|
+
}).refine(
|
|
458
|
+
({ words, lines, startMs, endMs }) => words === void 0 || words.length === lines.join(" ").split(" ").filter((word) => word !== "").length && words.every((at, index) => at >= startMs && at < endMs && (index === 0 || at >= words[index - 1])),
|
|
459
|
+
{
|
|
460
|
+
message: "words must time every word of the cue, in order, inside the cue",
|
|
461
|
+
path: ["words"]
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
var HEX_COLOUR = z7.string().regex(/^#[0-9A-Fa-f]{6}$/, "must be a #RRGGBB colour");
|
|
465
|
+
var OutroSchema = z7.object({
|
|
466
|
+
durationMs: z7.number().int().positive().max(15e3),
|
|
467
|
+
lines: z7.array(z7.string().min(1)).min(1).max(2),
|
|
325
468
|
backgroundColor: HEX_COLOUR,
|
|
326
469
|
textColor: HEX_COLOUR,
|
|
327
|
-
assPath:
|
|
470
|
+
assPath: z7.literal("captions/outro.ass")
|
|
328
471
|
});
|
|
329
|
-
var IntroSchema =
|
|
330
|
-
durationMs:
|
|
331
|
-
lines:
|
|
472
|
+
var IntroSchema = z7.object({
|
|
473
|
+
durationMs: z7.number().int().positive().max(15e3),
|
|
474
|
+
lines: z7.array(z7.string().min(1)).min(1).max(2),
|
|
332
475
|
backgroundColor: HEX_COLOUR,
|
|
333
476
|
textColor: HEX_COLOUR,
|
|
334
|
-
assPath:
|
|
477
|
+
assPath: z7.literal("captions/intro.ass")
|
|
478
|
+
});
|
|
479
|
+
var HookSchema = z7.object({
|
|
480
|
+
lines: z7.array(z7.string().min(1)).min(1).max(2),
|
|
481
|
+
durationMs: z7.number().int().min(1e3).max(1e4),
|
|
482
|
+
assPath: z7.literal("captions/hook.ass"),
|
|
483
|
+
/**
|
|
484
|
+
* The hook drawn as a picture rather than as ASS: a transparent PNG the width of the output
|
|
485
|
+
* and as tall as the bottom of its band, rasterised in Chromium when the hook was set (at
|
|
486
|
+
* record time, or by a `render --hook`/`--aspect` re-cut) so its emoji are drawn in colour by
|
|
487
|
+
* the system's emoji face — libass can only draw them as outlines. Frozen as a source file:
|
|
488
|
+
* a plain re-render overlays it and never opens a browser. Absent, the hook is drawn from
|
|
489
|
+
* `assPath` exactly as it was before 1.30, so older bundles re-render byte-identically.
|
|
490
|
+
*/
|
|
491
|
+
imagePath: z7.literal("captions/hook.png").optional()
|
|
492
|
+
});
|
|
493
|
+
var FrameSchema = z7.object({
|
|
494
|
+
radius: z7.number().int().positive().max(96),
|
|
495
|
+
assPath: z7.literal("captions/frame.ass")
|
|
335
496
|
});
|
|
336
|
-
var ChapterMarkSchema =
|
|
337
|
-
startMs:
|
|
338
|
-
endMs:
|
|
339
|
-
title:
|
|
497
|
+
var ChapterMarkSchema = z7.object({
|
|
498
|
+
startMs: z7.number().int().nonnegative(),
|
|
499
|
+
endMs: z7.number().int().positive(),
|
|
500
|
+
title: z7.string().min(1)
|
|
340
501
|
});
|
|
341
|
-
var ChaptersSchema =
|
|
342
|
-
metadataPath:
|
|
343
|
-
marks:
|
|
502
|
+
var ChaptersSchema = z7.object({
|
|
503
|
+
metadataPath: z7.literal("render/chapters.ffmetadata"),
|
|
504
|
+
marks: z7.array(ChapterMarkSchema).min(1)
|
|
344
505
|
}).refine(
|
|
345
506
|
({ marks }) => marks[0]?.startMs === 0 && marks.every(
|
|
346
507
|
(mark, index) => mark.endMs > mark.startMs && (index === 0 || marks[index - 1]?.endMs === mark.startMs)
|
|
@@ -353,14 +514,14 @@ var ChaptersSchema = z6.object({
|
|
|
353
514
|
path: ["marks"]
|
|
354
515
|
}
|
|
355
516
|
);
|
|
356
|
-
var CursorPointSchema =
|
|
357
|
-
id:
|
|
358
|
-
x:
|
|
359
|
-
y:
|
|
360
|
-
arriveMs:
|
|
361
|
-
departMs:
|
|
362
|
-
action:
|
|
363
|
-
rippleMs:
|
|
517
|
+
var CursorPointSchema = z7.object({
|
|
518
|
+
id: z7.string().min(1),
|
|
519
|
+
x: z7.number().int().min(0).max(1920),
|
|
520
|
+
y: z7.number().int().min(0).max(1080),
|
|
521
|
+
arriveMs: z7.number().int().nonnegative(),
|
|
522
|
+
departMs: z7.number().int().nonnegative(),
|
|
523
|
+
action: z7.enum(["click", "type", "point"]),
|
|
524
|
+
rippleMs: z7.number().int().nonnegative().optional(),
|
|
364
525
|
/**
|
|
365
526
|
* Size-correction percent, applied by the emitter with `\fscx`/`\fscy`, so the arrow
|
|
366
527
|
* stays one size on screen whatever the frame does to it between the cursor filter and
|
|
@@ -391,11 +552,21 @@ var CursorPointSchema = z6.object({
|
|
|
391
552
|
* always did — so a cursor bundle from before the camera, or from before the letterbox,
|
|
392
553
|
* still parses and regenerates byte-for-byte.
|
|
393
554
|
*/
|
|
394
|
-
scale:
|
|
555
|
+
scale: z7.number().int().min(1).max(400).optional(),
|
|
556
|
+
/**
|
|
557
|
+
* Present only on the first point after a hard cut (`TransitionCutSchema`): the output
|
|
558
|
+
* instant the picture changes to the next actor's segment. The pointer then does not glide
|
|
559
|
+
* from the previous point — it parks there until `cutMs` and is parked on this point from
|
|
560
|
+
* `cutMs` on, so it never slides from a terminal row into a web page over live frames, nor
|
|
561
|
+
* moves over a frozen picture. Bounded by the previous point's `departMs` and this point's
|
|
562
|
+
* `arriveMs` (`CursorSchema`'s refine). Absent everywhere else, so a cut-free plan's points
|
|
563
|
+
* are byte-identical.
|
|
564
|
+
*/
|
|
565
|
+
cutMs: z7.number().int().nonnegative().optional()
|
|
395
566
|
});
|
|
396
|
-
var CursorSchema =
|
|
397
|
-
assPath:
|
|
398
|
-
points:
|
|
567
|
+
var CursorSchema = z7.object({
|
|
568
|
+
assPath: z7.literal("captions/cursor.ass"),
|
|
569
|
+
points: z7.array(CursorPointSchema).min(1)
|
|
399
570
|
}).refine(
|
|
400
571
|
({ points }) => points.every(
|
|
401
572
|
(point, index) => point.arriveMs <= point.departMs && (point.rippleMs === void 0 || point.rippleMs >= point.arriveMs) && (index === 0 || points[index - 1].departMs <= point.arriveMs && points[index - 1].arriveMs < point.arriveMs)
|
|
@@ -407,19 +578,29 @@ var CursorSchema = z6.object({
|
|
|
407
578
|
message: "points must be ordered, arrive before departing, and not overlap",
|
|
408
579
|
path: ["points"]
|
|
409
580
|
}
|
|
581
|
+
).refine(
|
|
582
|
+
({ points }) => points.every(
|
|
583
|
+
(point, index) => point.cutMs === void 0 || index > 0 && points[index - 1].departMs <= point.cutMs && point.cutMs <= point.arriveMs
|
|
584
|
+
),
|
|
585
|
+
{
|
|
586
|
+
// A jump needs somewhere to jump from, and it must happen between leaving the previous
|
|
587
|
+
// point and reaching this one — anything else would show two arrows or none.
|
|
588
|
+
message: "cutMs must fall within [previous point departMs, arriveMs], never on the first point",
|
|
589
|
+
path: ["points"]
|
|
590
|
+
}
|
|
410
591
|
);
|
|
411
|
-
var CameraShotSchema =
|
|
412
|
-
id:
|
|
413
|
-
enterMs:
|
|
414
|
-
holdFromMs:
|
|
415
|
-
x:
|
|
416
|
-
y:
|
|
417
|
-
w:
|
|
418
|
-
h:
|
|
592
|
+
var CameraShotSchema = z7.object({
|
|
593
|
+
id: z7.string().min(1),
|
|
594
|
+
enterMs: z7.number().int().nonnegative(),
|
|
595
|
+
holdFromMs: z7.number().int().nonnegative(),
|
|
596
|
+
x: z7.number().int().min(0),
|
|
597
|
+
y: z7.number().int().min(0),
|
|
598
|
+
w: z7.number().int().positive(),
|
|
599
|
+
h: z7.number().int().positive()
|
|
419
600
|
});
|
|
420
|
-
var CameraSchema =
|
|
421
|
-
commandPath:
|
|
422
|
-
shots:
|
|
601
|
+
var CameraSchema = z7.object({
|
|
602
|
+
commandPath: z7.literal("render/camera.cmd"),
|
|
603
|
+
shots: z7.array(CameraShotSchema).min(1)
|
|
423
604
|
}).refine(
|
|
424
605
|
({ shots }) => shots.every(
|
|
425
606
|
(shot) => shot.x % 2 === 0 && shot.y % 2 === 0 && // Implied by the %32 and 9w/16 rules below; kept so a future relaxation of
|
|
@@ -447,39 +628,87 @@ var CameraSchema = z6.object({
|
|
|
447
628
|
path: ["shots"]
|
|
448
629
|
}
|
|
449
630
|
);
|
|
450
|
-
var ActorSchema =
|
|
451
|
-
id:
|
|
452
|
-
label:
|
|
631
|
+
var ActorSchema = z7.object({
|
|
632
|
+
id: z7.string().min(1),
|
|
633
|
+
label: z7.string().min(1)
|
|
453
634
|
});
|
|
454
|
-
var SegmentSchema =
|
|
455
|
-
id:
|
|
456
|
-
actorId:
|
|
457
|
-
startMs:
|
|
458
|
-
endMs:
|
|
635
|
+
var SegmentSchema = z7.object({
|
|
636
|
+
id: z7.string().min(1),
|
|
637
|
+
actorId: z7.string().min(1),
|
|
638
|
+
startMs: z7.number().int().nonnegative(),
|
|
639
|
+
endMs: z7.number().int().positive(),
|
|
640
|
+
/**
|
|
641
|
+
* Where this segment's frames actually sit inside the concatenated `raw/session.webm`,
|
|
642
|
+
* measured from the segment files themselves (`probeSegmentSpanMs`, last packet's pts plus
|
|
643
|
+
* its duration — the offset the concat demuxer gives the next file). Present only on plans
|
|
644
|
+
* with a hard cut or a `terminal.browser` scenario, and then on every segment (`RenderPlanSchema`'s refine): the splice trims
|
|
645
|
+
* `[sourceStartMs, sourceEndMs)` instead of the capture-derived `[startMs, endMs)`, because
|
|
646
|
+
* Chromium's WebM writer can end a segment file short of its capture window, or round a
|
|
647
|
+
* sub-second capture up to a 1 s file, and a cut's freeze would otherwise clone a frame of
|
|
648
|
+
* the wrong actor. `sourceEndMs` is the usable end — the file's span capped at `captureMs` for
|
|
649
|
+
* every segment but the last (which reads past its file's end into the head tail-pad clone, as
|
|
650
|
+
* the capture-based trim always has) — so it may stop short of the next `sourceStartMs`.
|
|
651
|
+
* `startMs`/`endMs` keep their capture-derived values regardless.
|
|
652
|
+
*/
|
|
653
|
+
sourceStartMs: z7.number().int().nonnegative().optional(),
|
|
654
|
+
sourceEndMs: z7.number().int().positive().optional()
|
|
459
655
|
});
|
|
460
|
-
var TransitionCardSchema =
|
|
461
|
-
fromActorId:
|
|
462
|
-
toActorId:
|
|
463
|
-
durationMs:
|
|
464
|
-
lines:
|
|
656
|
+
var TransitionCardSchema = z7.object({
|
|
657
|
+
fromActorId: z7.string().min(1),
|
|
658
|
+
toActorId: z7.string().min(1),
|
|
659
|
+
durationMs: z7.number().int().positive().max(15e3),
|
|
660
|
+
lines: z7.array(z7.string().min(1)).min(1).max(2),
|
|
465
661
|
backgroundColor: HEX_COLOUR,
|
|
466
662
|
textColor: HEX_COLOUR,
|
|
467
|
-
assPath:
|
|
663
|
+
assPath: z7.string().regex(/^captions\/turn-[0-9]+\.ass$/),
|
|
664
|
+
/**
|
|
665
|
+
* The outgoing segment's last-frame hold before this card, exactly as on a cut
|
|
666
|
+
* (`TransitionCutSchema.freezeMs`): `contentMs − trimmedSpanMs(source window)`. Only plans
|
|
667
|
+
* with measured source bounds (every plan with a cut, and every `terminal.browser` plan) carry it; a card in any other plan
|
|
668
|
+
* never does, so it parses and serialises as it always has.
|
|
669
|
+
*/
|
|
670
|
+
freezeMs: z7.number().int().positive().optional()
|
|
468
671
|
});
|
|
469
|
-
var
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
672
|
+
var TransitionCutSchema = z7.object({
|
|
673
|
+
kind: z7.literal("cut"),
|
|
674
|
+
fromActorId: z7.string().min(1),
|
|
675
|
+
toActorId: z7.string().min(1),
|
|
676
|
+
/**
|
|
677
|
+
* How long the outgoing segment's last frame is held before the cut (`tpad=stop_mode=clone`),
|
|
678
|
+
* so the video reaches the joint the derivation placed at the segment's content length:
|
|
679
|
+
* `contentMs − trimmedSpanMs(source window)`, present only when that is at least one frame —
|
|
680
|
+
* usually a trailing cue's overrun, but possibly a single frame (33ms) with no overrun at
|
|
681
|
+
* all, when the trim keeps one frame fewer than the content slot reserves. The joint itself
|
|
682
|
+
* still claims 0 (`transitionGapMs`). Cards and explain scenes in a source-bound plan (a cut, or `terminal.browser`) carry the
|
|
683
|
+
* same optional `freezeMs`; in a plan without source bounds nothing carries one.
|
|
684
|
+
*/
|
|
685
|
+
freezeMs: z7.number().int().positive().optional()
|
|
686
|
+
}).refine((cut) => cut.fromActorId !== cut.toActorId, {
|
|
687
|
+
// The same reason `runtime.ts` refuses a turn to the actor already holding the browser:
|
|
688
|
+
// a joint where nobody changes is an explain scene's, never a hand-off's.
|
|
689
|
+
message: "a cut must hand off between two different actors (fromActorId !== toActorId)",
|
|
690
|
+
path: ["toActorId"]
|
|
473
691
|
});
|
|
474
|
-
var
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
692
|
+
var TransitionSchema = z7.union([TransitionCardSchema, TransitionCutSchema]);
|
|
693
|
+
var ExplainSegmentPlanSchema = z7.object({
|
|
694
|
+
id: z7.string().min(1),
|
|
695
|
+
segmentPath: z7.string().regex(/^explain\/[a-z0-9][a-z0-9-]*\/segment\.mp4$/),
|
|
696
|
+
durationMs: z7.number().int().positive(),
|
|
697
|
+
/**
|
|
698
|
+
* The hold on the capture segment *before* this scene, as on a cut or card joint
|
|
699
|
+
* (`TransitionCutSchema.freezeMs`). Only in plans with measured source bounds.
|
|
700
|
+
*/
|
|
701
|
+
freezeMs: z7.number().int().positive().optional()
|
|
702
|
+
});
|
|
703
|
+
var HighlightRectSchema = z7.object({
|
|
704
|
+
id: z7.string().min(1),
|
|
705
|
+
x: z7.number().int().min(0),
|
|
706
|
+
y: z7.number().int().min(0),
|
|
707
|
+
width: z7.number().int().positive(),
|
|
708
|
+
height: z7.number().int().positive(),
|
|
709
|
+
startMs: z7.number().int().nonnegative(),
|
|
710
|
+
endMs: z7.number().int().positive(),
|
|
711
|
+
label: z7.string().min(1).optional(),
|
|
483
712
|
/**
|
|
484
713
|
* The rect the spotlight glides *from*, when this window's cutout should animate rather
|
|
485
714
|
* than cut straight to the held `x`/`y`/`width`/`height` — the pre-reveal position a
|
|
@@ -491,11 +720,11 @@ var HighlightRectSchema = z6.object({
|
|
|
491
720
|
* whole span — so every plan frozen before this field existed, and every window a future
|
|
492
721
|
* derivation decides not to glide, is unaffected by construction.
|
|
493
722
|
*/
|
|
494
|
-
from:
|
|
495
|
-
x:
|
|
496
|
-
y:
|
|
497
|
-
width:
|
|
498
|
-
height:
|
|
723
|
+
from: z7.object({
|
|
724
|
+
x: z7.number().int().min(0),
|
|
725
|
+
y: z7.number().int().min(0),
|
|
726
|
+
width: z7.number().int().positive(),
|
|
727
|
+
height: z7.number().int().positive()
|
|
499
728
|
}).optional(),
|
|
500
729
|
/**
|
|
501
730
|
* The absolute video-ms at which the glide from `from` completes and the spotlight
|
|
@@ -504,7 +733,7 @@ var HighlightRectSchema = z6.object({
|
|
|
504
733
|
* has to be real time left to glide across) and no later than `endMs` (the window
|
|
505
734
|
* cannot settle after it has already closed).
|
|
506
735
|
*/
|
|
507
|
-
settleMs:
|
|
736
|
+
settleMs: z7.number().int().positive().optional()
|
|
508
737
|
}).refine(({ x, y, width, height }) => x + width <= 1920 && y + height <= 1080, {
|
|
509
738
|
// Unlike CameraShotSchema's crop window, a highlight rect is drawn by libass, not fed to
|
|
510
739
|
// `crop` — there is no yuv420p even-pixel rule to enforce — but it still has to fit the
|
|
@@ -534,9 +763,9 @@ var HighlightRectSchema = z6.object({
|
|
|
534
763
|
message: "settleMs must be strictly after startMs and no later than endMs",
|
|
535
764
|
path: ["settleMs"]
|
|
536
765
|
});
|
|
537
|
-
var HighlightSchema =
|
|
538
|
-
assPath:
|
|
539
|
-
rects:
|
|
766
|
+
var HighlightSchema = z7.object({
|
|
767
|
+
assPath: z7.literal("captions/highlight.ass"),
|
|
768
|
+
rects: z7.array(HighlightRectSchema).min(1)
|
|
540
769
|
}).refine(
|
|
541
770
|
({ rects }) => rects.every(
|
|
542
771
|
(rect, index) => rect.endMs > rect.startMs && (index === 0 || rects[index - 1].endMs <= rect.startMs)
|
|
@@ -546,8 +775,8 @@ var HighlightSchema = z6.object({
|
|
|
546
775
|
path: ["rects"]
|
|
547
776
|
}
|
|
548
777
|
);
|
|
549
|
-
var SpeechClipSchema =
|
|
550
|
-
id:
|
|
778
|
+
var SpeechClipSchema = z7.object({
|
|
779
|
+
id: z7.string().min(1),
|
|
551
780
|
/*
|
|
552
781
|
* Bundle-relative and unescapable by construction: no slash, no leading dot, so `..` and
|
|
553
782
|
* an absolute path are both unrepresentable. The renderer's `assertBundleRelative` says
|
|
@@ -561,10 +790,10 @@ var SpeechClipSchema = z6.object({
|
|
|
561
790
|
* character the recorder could have replaced. `narration.test.ts` asserts the two agree on a
|
|
562
791
|
* table of candidates, because a comment cannot.
|
|
563
792
|
*/
|
|
564
|
-
path:
|
|
565
|
-
atMs:
|
|
566
|
-
durationMs:
|
|
567
|
-
source:
|
|
793
|
+
path: z7.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
|
|
794
|
+
atMs: z7.number().int().nonnegative(),
|
|
795
|
+
durationMs: z7.number().int().positive(),
|
|
796
|
+
source: z7.enum(["synth", "file", "explain"]),
|
|
568
797
|
/*
|
|
569
798
|
* Which voice synthesised this clip, when it was not the plan-wide default that
|
|
570
799
|
* `engine.voice` names. Absent on every clip of a one-voice recording and on every
|
|
@@ -580,16 +809,16 @@ var SpeechClipSchema = z6.object({
|
|
|
580
809
|
* Optional and additive: every bundle frozen before per-step voices existed carries no
|
|
581
810
|
* `voice` on any clip and must keep verifying, which is what `optional()` buys.
|
|
582
811
|
*/
|
|
583
|
-
voice:
|
|
812
|
+
voice: z7.string().min(1).optional()
|
|
584
813
|
});
|
|
585
|
-
var SpeechEngineSchema =
|
|
586
|
-
name:
|
|
587
|
-
version:
|
|
588
|
-
modelSha256:
|
|
589
|
-
voice:
|
|
814
|
+
var SpeechEngineSchema = z7.object({
|
|
815
|
+
name: z7.string().min(1),
|
|
816
|
+
version: z7.string().min(1),
|
|
817
|
+
modelSha256: z7.string().regex(/^[0-9a-f]{64}$/),
|
|
818
|
+
voice: z7.string().min(1)
|
|
590
819
|
});
|
|
591
|
-
var SpeechSchema =
|
|
592
|
-
trackPath:
|
|
820
|
+
var SpeechSchema = z7.object({
|
|
821
|
+
trackPath: z7.literal("speech/narration.wav"),
|
|
593
822
|
/*
|
|
594
823
|
* 24 kHz mono, because that is what Kokoro emits and nothing on this path resamples.
|
|
595
824
|
* Written as literals rather than imported from `@plaintake/audio`, which owns the
|
|
@@ -597,10 +826,26 @@ var SpeechSchema = z6.object({
|
|
|
597
826
|
* be the cycle `no-circular` refuses. `packages/audio/src/wav.test.ts` asserts the two
|
|
598
827
|
* agree, since a comment cannot.
|
|
599
828
|
*/
|
|
600
|
-
sampleRate:
|
|
601
|
-
channels:
|
|
602
|
-
clips:
|
|
603
|
-
engine: SpeechEngineSchema.optional()
|
|
829
|
+
sampleRate: z7.literal(24e3),
|
|
830
|
+
channels: z7.literal(1),
|
|
831
|
+
clips: z7.array(SpeechClipSchema).min(1),
|
|
832
|
+
engine: SpeechEngineSchema.optional(),
|
|
833
|
+
/**
|
|
834
|
+
* Normalise the assembled track's integrated loudness (ITU-R BS.1770) to `targetLufs`, with
|
|
835
|
+
* a make-up gain and a lookahead limiter that holds every sample at or under `maxPeakDbfs`.
|
|
836
|
+
* The short-form platforms play every upload at about -14 LUFS and turn a quieter one down
|
|
837
|
+
* no further — they only ever turn a louder one *down* — so an unnormalised narration simply
|
|
838
|
+
* plays quieter than everything around it.
|
|
839
|
+
*
|
|
840
|
+
* A target, not a gain: the gain and the limiting are a pure function of the frozen clips
|
|
841
|
+
* (`normalizeLoudness`, `@plaintake/audio`) recomputed by every freeze in a fixed sequence of
|
|
842
|
+
* double-precision operations, so the same clips always produce the same track. Absent means the clips are laid out at the level they
|
|
843
|
+
* were synthesised at, which is every plan before this field.
|
|
844
|
+
*/
|
|
845
|
+
loudness: z7.object({
|
|
846
|
+
targetLufs: z7.number().min(-30).max(-5),
|
|
847
|
+
maxPeakDbfs: z7.number().min(-12).max(0)
|
|
848
|
+
}).optional()
|
|
604
849
|
}).refine(
|
|
605
850
|
({ clips }) => new Set(clips.map((clip) => clip.id)).size === clips.length && clips.every(
|
|
606
851
|
(clip, index) => index === 0 || clips[index - 1].atMs + clips[index - 1].durationMs <= clip.atMs
|
|
@@ -620,19 +865,19 @@ var SpeechSchema = z6.object({
|
|
|
620
865
|
// A synthesised clip with no record of what synthesised it is the one state this block
|
|
621
866
|
// exists to prevent. The converse is fine: an engine recorded on an all-file track is
|
|
622
867
|
// merely redundant, not misleading. An `explain` clip is exempt rather than overlooked:
|
|
623
|
-
//
|
|
624
|
-
//
|
|
868
|
+
// its scene's own frozen plan records what synthesised it — the `engine` here names the
|
|
869
|
+
// step narrator's identity, which an explain scene's voice need not share.
|
|
625
870
|
message: "engine must be recorded whenever any clip was synthesised",
|
|
626
871
|
path: ["engine"]
|
|
627
872
|
});
|
|
628
|
-
var ThemeSchema =
|
|
873
|
+
var ThemeSchema = z7.object({ accentColor: HEX_COLOUR });
|
|
629
874
|
var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
|
|
630
|
-
var RenderPlanSchema =
|
|
631
|
-
schema:
|
|
632
|
-
source:
|
|
633
|
-
path:
|
|
634
|
-
sha256:
|
|
635
|
-
durationMs:
|
|
875
|
+
var RenderPlanSchema = z7.object({
|
|
876
|
+
schema: z7.literal(RENDER_PLAN_SCHEMA),
|
|
877
|
+
source: z7.object({
|
|
878
|
+
path: z7.literal("raw/session.webm"),
|
|
879
|
+
sha256: z7.string().regex(/^[0-9a-f]{64}$/),
|
|
880
|
+
durationMs: z7.number().int().nonnegative()
|
|
636
881
|
}),
|
|
637
882
|
/**
|
|
638
883
|
* The frame. **Three geometries live here and they are not the same thing**, which is the
|
|
@@ -694,23 +939,47 @@ var RenderPlanSchema = z6.object({
|
|
|
694
939
|
* one would be backwards — a plan is frozen once and rendered later, so both belong at the
|
|
695
940
|
* parse, not at the encode.
|
|
696
941
|
*/
|
|
697
|
-
video:
|
|
698
|
-
width:
|
|
699
|
-
height:
|
|
700
|
-
fps:
|
|
701
|
-
pixelFormat:
|
|
702
|
-
capture:
|
|
703
|
-
letterbox:
|
|
704
|
-
width:
|
|
705
|
-
height:
|
|
706
|
-
x:
|
|
707
|
-
y:
|
|
942
|
+
video: z7.object({
|
|
943
|
+
width: z7.number().int().positive().multipleOf(2),
|
|
944
|
+
height: z7.number().int().positive().multipleOf(2),
|
|
945
|
+
fps: z7.literal(30),
|
|
946
|
+
pixelFormat: z7.literal("yuv420p"),
|
|
947
|
+
capture: z7.object({ width: z7.literal(1920), height: z7.literal(1080) }).default({ width: 1920, height: 1080 }),
|
|
948
|
+
letterbox: z7.object({
|
|
949
|
+
width: z7.number().int().positive().multipleOf(2),
|
|
950
|
+
height: z7.number().int().positive().multipleOf(2),
|
|
951
|
+
x: z7.number().int().nonnegative().multipleOf(2),
|
|
952
|
+
y: z7.number().int().nonnegative().multipleOf(2),
|
|
708
953
|
/**
|
|
709
954
|
* The fill for everything the box does not cover. `#RRGGBB` for the reason
|
|
710
955
|
* `OutroSchema`'s colours are: the value reaches an FFmpeg filtergraph, and that
|
|
711
956
|
* grammar admits no metacharacter.
|
|
712
957
|
*/
|
|
713
|
-
padColor: HEX_COLOUR
|
|
958
|
+
padColor: HEX_COLOUR,
|
|
959
|
+
/**
|
|
960
|
+
* `follow` — the `--reframe follow` opt-in — fills the box with a 4:3 window of the
|
|
961
|
+
* capture that follows the camera shots (or a fixed centre window with no camera),
|
|
962
|
+
* instead of the whole 16:9 frame. The one place this plan crops the picture for its
|
|
963
|
+
* shape rather than for a zoom, and still a render-time decision: the window is a
|
|
964
|
+
* pure function of `camera.shots` and this box (`followWindow`, `renderer-ffmpeg`),
|
|
965
|
+
* so a bundle re-cuts into or out of it without re-recording. Absent means the whole
|
|
966
|
+
* frame is scaled in — every plan before this field, unchanged.
|
|
967
|
+
*/
|
|
968
|
+
reframe: z7.literal("follow").optional(),
|
|
969
|
+
/**
|
|
970
|
+
* The two layouts that treat the picture as an object on a background rather than as
|
|
971
|
+
* the background itself — `--reframe social` and `--reframe inset`:
|
|
972
|
+
*
|
|
973
|
+
* - `social` (9:16, always `follow`): a 9:8 follow window in a box sized and placed for
|
|
974
|
+
* short-form feeds — clear of the platforms' own top bar, bottom caption block and
|
|
975
|
+
* right-hand action rail — with a band above it for the `hook`, bold captions below it,
|
|
976
|
+
* and rounded corners on a branded pad;
|
|
977
|
+
* - `inset` (16:9, never `follow`): the whole frame scaled into a box inset from every
|
|
978
|
+
* edge, with rounded corners on a branded pad.
|
|
979
|
+
*
|
|
980
|
+
* Absent means the plain letterbox or follow box — every plan before these, unchanged.
|
|
981
|
+
*/
|
|
982
|
+
layout: z7.enum(["social", "inset"]).optional()
|
|
714
983
|
}).optional(),
|
|
715
984
|
/*
|
|
716
985
|
* Everything before the closing card: the opening card, if there is one, plus the
|
|
@@ -727,8 +996,8 @@ var RenderPlanSchema = z6.object({
|
|
|
727
996
|
* With no intro it is exactly what it always was, which is what keeps a plan without
|
|
728
997
|
* one byte-identical to what earlier versions wrote.
|
|
729
998
|
*/
|
|
730
|
-
durationMs:
|
|
731
|
-
tailPadMs:
|
|
999
|
+
durationMs: z7.number().int().positive(),
|
|
1000
|
+
tailPadMs: z7.number().int().nonnegative()
|
|
732
1001
|
}).refine(
|
|
733
1002
|
({ width, height, letterbox }) => letterbox === void 0 || letterbox.x + letterbox.width <= width && letterbox.y + letterbox.height <= height,
|
|
734
1003
|
{
|
|
@@ -741,13 +1010,28 @@ var RenderPlanSchema = z6.object({
|
|
|
741
1010
|
message: "the letterbox box must fit inside the output frame",
|
|
742
1011
|
path: ["letterbox"]
|
|
743
1012
|
}
|
|
1013
|
+
).refine(
|
|
1014
|
+
({ letterbox }) => letterbox?.reframe === void 0 || (letterbox.layout === "social" ? letterbox.width * 8 === letterbox.height * 9 : letterbox.width * 3 === letterbox.height * 4),
|
|
1015
|
+
{
|
|
1016
|
+
// The follow window is cut to the box's own shape and scaled into it with no further
|
|
1017
|
+
// correction, so a box of any other shape would freeze a stretched picture: 4:3 for the
|
|
1018
|
+
// plain follow box, 9:8 for the social one.
|
|
1019
|
+
message: "a follow letterbox must be exactly 4:3 (9:8 under the social layout)",
|
|
1020
|
+
path: ["letterbox"]
|
|
1021
|
+
}
|
|
1022
|
+
).refine(
|
|
1023
|
+
({ letterbox }) => letterbox?.layout === void 0 || (letterbox.layout === "social" ? letterbox.reframe === "follow" : letterbox.reframe === void 0),
|
|
1024
|
+
{
|
|
1025
|
+
message: "the social layout always follows the camera and the inset layout never does",
|
|
1026
|
+
path: ["letterbox"]
|
|
1027
|
+
}
|
|
744
1028
|
),
|
|
745
|
-
captions:
|
|
746
|
-
language:
|
|
747
|
-
srtPath:
|
|
748
|
-
vttPath:
|
|
749
|
-
assPath:
|
|
750
|
-
cues:
|
|
1029
|
+
captions: z7.object({
|
|
1030
|
+
language: z7.string().min(2),
|
|
1031
|
+
srtPath: z7.literal("captions/captions.srt"),
|
|
1032
|
+
vttPath: z7.literal("captions/captions.vtt"),
|
|
1033
|
+
assPath: z7.literal("captions/captions.ass"),
|
|
1034
|
+
cues: z7.array(CueSchema)
|
|
751
1035
|
}),
|
|
752
1036
|
/**
|
|
753
1037
|
* How the burned-in captions were drawn. This is the **source** of
|
|
@@ -761,18 +1045,18 @@ var RenderPlanSchema = z6.object({
|
|
|
761
1045
|
* older reader ignoring fields it does not know still executes the same frozen arguments
|
|
762
1046
|
* over the same frozen ASS. That observation generalises to the schema as a whole.
|
|
763
1047
|
*/
|
|
764
|
-
style:
|
|
765
|
-
fontFile:
|
|
766
|
-
fontName:
|
|
767
|
-
fontSize:
|
|
768
|
-
textColor:
|
|
769
|
-
outlineColor:
|
|
1048
|
+
style: z7.object({
|
|
1049
|
+
fontFile: z7.literal("assets/fonts/NotoSans-Regular.ttf"),
|
|
1050
|
+
fontName: z7.literal("Noto Sans"),
|
|
1051
|
+
fontSize: z7.number().int().positive(),
|
|
1052
|
+
textColor: z7.string(),
|
|
1053
|
+
outlineColor: z7.string(),
|
|
770
1054
|
/** ASS `Outline`: a stroke width under `BorderStyle 1`, box padding under `BorderStyle 4`. */
|
|
771
|
-
outlineWidth:
|
|
772
|
-
marginBottom:
|
|
773
|
-
marginSide:
|
|
774
|
-
borderStyle:
|
|
775
|
-
outlineOpacity:
|
|
1055
|
+
outlineWidth: z7.number().int().nonnegative(),
|
|
1056
|
+
marginBottom: z7.number().int().nonnegative(),
|
|
1057
|
+
marginSide: z7.number().int().nonnegative().default(80),
|
|
1058
|
+
borderStyle: z7.union([z7.literal(1), z7.literal(4)]).default(1),
|
|
1059
|
+
outlineOpacity: z7.number().min(0).max(1).default(1),
|
|
776
1060
|
boxColor: HEX_COLOUR.default("#000000"),
|
|
777
1061
|
/*
|
|
778
1062
|
* Opaque, which under the `BorderStyle 1` default draws nothing at all: `BackColour` is
|
|
@@ -780,7 +1064,7 @@ var RenderPlanSchema = z6.object({
|
|
|
780
1064
|
* and ASS's own default, so a plan frozen before this field existed regenerates its ASS
|
|
781
1065
|
* byte-for-byte rather than byte-for-byte-except-one-field-nobody-can-see.
|
|
782
1066
|
*/
|
|
783
|
-
boxOpacity:
|
|
1067
|
+
boxOpacity: z7.number().min(0).max(1).default(1),
|
|
784
1068
|
/**
|
|
785
1069
|
* ASS `\an` alignment: which edge of the frame the caption block is anchored to, and
|
|
786
1070
|
* therefore which margin `marginBottom` is measured from.
|
|
@@ -799,15 +1083,25 @@ var RenderPlanSchema = z6.object({
|
|
|
799
1083
|
* remaining code, middle-centre (5), would put the words back over the picture — which
|
|
800
1084
|
* is the thing the letterbox exists to stop.
|
|
801
1085
|
*/
|
|
802
|
-
alignment:
|
|
1086
|
+
alignment: z7.union([z7.literal(2), z7.literal(8)]).default(2),
|
|
1087
|
+
/**
|
|
1088
|
+
* The social layout's captions: bold, every character drawn by an explicitly chosen face
|
|
1089
|
+
* (`faceRuns`, `@plaintake/subtitles`, so an emoji or an arrow never depends on the font
|
|
1090
|
+
* provider's fallback), and — on a cue that carries `words` — each word lit in
|
|
1091
|
+
* `highlightColor` as it is spoken, the short-form convention. Absent means the caption style
|
|
1092
|
+
* every other plan has, so its ASS is byte-for-byte what it was.
|
|
1093
|
+
*/
|
|
1094
|
+
social: z7.object({ highlightColor: HEX_COLOUR }).optional()
|
|
803
1095
|
}),
|
|
804
|
-
ffmpeg:
|
|
805
|
-
base:
|
|
806
|
-
soft:
|
|
807
|
-
hard:
|
|
1096
|
+
ffmpeg: z7.object({
|
|
1097
|
+
base: z7.array(z7.string()),
|
|
1098
|
+
soft: z7.array(z7.string()),
|
|
1099
|
+
hard: z7.array(z7.string())
|
|
808
1100
|
}),
|
|
809
1101
|
intro: IntroSchema.optional(),
|
|
810
1102
|
outro: OutroSchema.optional(),
|
|
1103
|
+
hook: HookSchema.optional(),
|
|
1104
|
+
frame: FrameSchema.optional(),
|
|
811
1105
|
chapters: ChaptersSchema.optional(),
|
|
812
1106
|
cursor: CursorSchema.optional(),
|
|
813
1107
|
camera: CameraSchema.optional(),
|
|
@@ -862,15 +1156,15 @@ var RenderPlanSchema = z6.object({
|
|
|
862
1156
|
* argument arrays from the plan, and read this field (via `outputOf`) so a re-cut writes
|
|
863
1157
|
* the file the original recording wrote rather than the fallback name.
|
|
864
1158
|
*/
|
|
865
|
-
outputPath:
|
|
1159
|
+
outputPath: z7.string().regex(/^output\/[a-z0-9][a-z0-9-]*\.mp4$/, "must be output/<scenario-id>.mp4, a kebab-case id").optional(),
|
|
866
1160
|
/**
|
|
867
1161
|
* The capture timeline cut into windows — of a genuine multi-actor cast, of an implicit
|
|
868
1162
|
* single actor pausing for explain cut-aways, or both at once. `segments` is the one
|
|
869
1163
|
* field of the five below that a segmented capture *always* carries; the other four are
|
|
870
1164
|
* two independent, narrower capabilities layered on top of it:
|
|
871
1165
|
*
|
|
872
|
-
* - `actors`/`transitionCards`/`badgesAssPath` — the multi-actor demo's cast, the
|
|
873
|
-
* drawn at each hand-off, and the badge track naming the active actor throughout. Three
|
|
1166
|
+
* - `actors`/`transitionCards`/`badgesAssPath` — the multi-actor demo's cast, the card
|
|
1167
|
+
* drawn (or the hard cut taken — `TransitionCutSchema`, zero screen time) at each hand-off, and the badge track naming the active actor throughout. Three
|
|
874
1168
|
* fields that carry one capability and so, like every other optional block above, are
|
|
875
1169
|
* present or absent together (the first refinement below) — and never without
|
|
876
1170
|
* `segments`, which they reference by `actorId` (the second). Absent means no genuine
|
|
@@ -879,18 +1173,18 @@ var RenderPlanSchema = z6.object({
|
|
|
879
1173
|
* whenever a scenario called `demo.explain()` at all, with or without a cast alongside
|
|
880
1174
|
* it — never without `segments` either (the third refinement), for the same reason.
|
|
881
1175
|
*
|
|
882
|
-
* Every gap between adjacent `segments` is claimed by exactly one occupant, a card or
|
|
883
|
-
* explain scene, never both and never neither (the fifth refinement) — which is also why
|
|
1176
|
+
* Every gap between adjacent `segments` is claimed by exactly one occupant, a card, a cut or
|
|
1177
|
+
* an explain scene — a cut claims its gap with zero screen time, but it still claims it — never both and never neither (the fifth refinement) — which is also why
|
|
884
1178
|
* `segments` can appear with `transitionCards` and `explainSegments` both absent only when
|
|
885
1179
|
* `segments.length === 1` never occurs (`SegmentSchema`'s own `.min(2)` on the array rules
|
|
886
1180
|
* it out): a lone, unsegmented capture has no gap to claim in the first place, and needs
|
|
887
1181
|
* none of these five fields at all.
|
|
888
1182
|
*/
|
|
889
|
-
actors:
|
|
890
|
-
segments:
|
|
891
|
-
transitionCards:
|
|
892
|
-
badgesAssPath:
|
|
893
|
-
explainSegments:
|
|
1183
|
+
actors: z7.array(ActorSchema).min(2).optional(),
|
|
1184
|
+
segments: z7.array(SegmentSchema).min(2).optional(),
|
|
1185
|
+
transitionCards: z7.array(TransitionSchema).min(1).optional(),
|
|
1186
|
+
badgesAssPath: z7.literal("captions/badges.ass").optional(),
|
|
1187
|
+
explainSegments: z7.array(ExplainSegmentPlanSchema).min(1).optional()
|
|
894
1188
|
}).refine(
|
|
895
1189
|
(plan) => {
|
|
896
1190
|
const castFields = [plan.actors, plan.transitionCards, plan.badgesAssPath];
|
|
@@ -903,7 +1197,12 @@ var RenderPlanSchema = z6.object({
|
|
|
903
1197
|
message: "actors, transitionCards and badgesAssPath must be present together or absent together",
|
|
904
1198
|
path: ["actors"]
|
|
905
1199
|
}
|
|
906
|
-
).refine((plan) => plan.
|
|
1200
|
+
).refine((plan) => plan.frame === void 0 || plan.video.letterbox?.layout !== void 0, {
|
|
1201
|
+
// Corners are drawn on the box a layout lays the picture in; a plan with no such box has no
|
|
1202
|
+
// corner to round, and drawing one anyway would paint pad colour over the picture itself.
|
|
1203
|
+
message: "frame needs a social or inset letterbox to round the corners of",
|
|
1204
|
+
path: ["frame"]
|
|
1205
|
+
}).refine((plan) => plan.actors === void 0 || plan.segments !== void 0, {
|
|
907
1206
|
// A cast with nowhere to draw its own windows — `segment.actorId` is what a badge or a
|
|
908
1207
|
// transition card is drawn against — cannot render.
|
|
909
1208
|
message: "actors requires segments \u2014 a cast with no capture windows of its own cannot render",
|
|
@@ -928,6 +1227,24 @@ var RenderPlanSchema = z6.object({
|
|
|
928
1227
|
message: "segments must tile raw/session.webm contiguously from 0",
|
|
929
1228
|
path: ["segments"]
|
|
930
1229
|
}
|
|
1230
|
+
).refine(
|
|
1231
|
+
(plan) => {
|
|
1232
|
+
if (plan.segments === void 0) return true;
|
|
1233
|
+
const { segments } = plan;
|
|
1234
|
+
const sourced = segments.filter(
|
|
1235
|
+
(segment) => segment.sourceStartMs !== void 0 || segment.sourceEndMs !== void 0
|
|
1236
|
+
);
|
|
1237
|
+
if (sourced.length === 0) return true;
|
|
1238
|
+
return segments.every(
|
|
1239
|
+
(segment, index) => segment.sourceStartMs !== void 0 && segment.sourceEndMs !== void 0 && segment.sourceEndMs > segment.sourceStartMs && (index === 0 || segment.sourceStartMs >= segments[index - 1].sourceEndMs)
|
|
1240
|
+
);
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
// All or none: a splice that trimmed some segments by measured source position and the
|
|
1244
|
+
// rest by capture position would mix two coordinate systems in one graph.
|
|
1245
|
+
message: "sourceStartMs/sourceEndMs must be present on every segment or on none, each window non-empty and in order without overlap",
|
|
1246
|
+
path: ["segments"]
|
|
1247
|
+
}
|
|
931
1248
|
).refine(
|
|
932
1249
|
(plan) => {
|
|
933
1250
|
if (plan.segments === void 0) return true;
|
|
@@ -961,95 +1278,108 @@ var RenderPlanSchema = z6.object({
|
|
|
961
1278
|
);
|
|
962
1279
|
|
|
963
1280
|
// ../schema/src/results.ts
|
|
964
|
-
import { z as
|
|
965
|
-
var RELATIVE_POSIX =
|
|
966
|
-
var
|
|
967
|
-
var DISPLAY_PATH =
|
|
1281
|
+
import { z as z8 } from "zod";
|
|
1282
|
+
var RELATIVE_POSIX = z8.string().regex(/^[A-Za-z0-9.][A-Za-z0-9._/-]*$/, "must be a relative POSIX path").refine((value) => !value.split("/").includes(".."), "must not traverse upwards");
|
|
1283
|
+
var Sha2563 = z8.string().regex(/^[0-9a-f]{64}$/);
|
|
1284
|
+
var DISPLAY_PATH = z8.string().min(1);
|
|
968
1285
|
var envelope = (kind) => ({
|
|
969
|
-
schema:
|
|
970
|
-
kind:
|
|
971
|
-
ok:
|
|
1286
|
+
schema: z8.literal("agent-demo.result/v1"),
|
|
1287
|
+
kind: z8.literal(kind),
|
|
1288
|
+
ok: z8.boolean(),
|
|
972
1289
|
/** Human-readable failures. Empty iff ok. */
|
|
973
|
-
problems:
|
|
1290
|
+
problems: z8.array(z8.string())
|
|
974
1291
|
});
|
|
975
|
-
var ArtifactRefSchema =
|
|
1292
|
+
var ArtifactRefSchema = z8.object({
|
|
976
1293
|
path: RELATIVE_POSIX,
|
|
977
|
-
sha256:
|
|
978
|
-
bytes:
|
|
1294
|
+
sha256: Sha2563,
|
|
1295
|
+
bytes: z8.number().int().nonnegative()
|
|
979
1296
|
});
|
|
980
|
-
var StreamSummarySchema =
|
|
981
|
-
container:
|
|
982
|
-
durationMs:
|
|
983
|
-
video:
|
|
984
|
-
codec:
|
|
985
|
-
width:
|
|
986
|
-
height:
|
|
987
|
-
pixelFormat:
|
|
988
|
-
fps:
|
|
989
|
-
frames:
|
|
1297
|
+
var StreamSummarySchema = z8.object({
|
|
1298
|
+
container: z8.enum(["mp4", "webm", "other"]),
|
|
1299
|
+
durationMs: z8.number().int().nonnegative(),
|
|
1300
|
+
video: z8.object({
|
|
1301
|
+
codec: z8.string(),
|
|
1302
|
+
width: z8.number().int(),
|
|
1303
|
+
height: z8.number().int(),
|
|
1304
|
+
pixelFormat: z8.string(),
|
|
1305
|
+
fps: z8.number().int(),
|
|
1306
|
+
frames: z8.number().int(),
|
|
990
1307
|
/**
|
|
991
1308
|
* The video stream's own duration, as distinct from the container's above. The
|
|
992
1309
|
* constant-rate check measures frames against this, because a padded narration track makes
|
|
993
1310
|
* the container's duration the audio's. Zero for a WebM, which carries no per-track
|
|
994
1311
|
* duration at all.
|
|
995
1312
|
*/
|
|
996
|
-
durationMs:
|
|
1313
|
+
durationMs: z8.number().int().nonnegative()
|
|
997
1314
|
}),
|
|
998
|
-
subtitles:
|
|
1315
|
+
subtitles: z8.array(z8.object({ codec: z8.string(), language: z8.string(), title: z8.string() })),
|
|
999
1316
|
/** Properties, not a count — a narration track muxed in stereo or at the wrong rate
|
|
1000
1317
|
* still counts as one stream. */
|
|
1001
|
-
audio:
|
|
1002
|
-
|
|
1003
|
-
codec:
|
|
1004
|
-
sampleRate:
|
|
1005
|
-
channels:
|
|
1318
|
+
audio: z8.array(
|
|
1319
|
+
z8.object({
|
|
1320
|
+
codec: z8.string(),
|
|
1321
|
+
sampleRate: z8.number().int().nonnegative(),
|
|
1322
|
+
channels: z8.number().int().nonnegative()
|
|
1006
1323
|
})
|
|
1007
1324
|
),
|
|
1008
1325
|
/** Titled chapters make FFmpeg's MP4 muxer add a `bin_data` track beside the `chpl` atom. */
|
|
1009
|
-
dataStreams:
|
|
1010
|
-
chapters:
|
|
1011
|
-
|
|
1012
|
-
startMs:
|
|
1013
|
-
endMs:
|
|
1014
|
-
title:
|
|
1326
|
+
dataStreams: z8.number().int().nonnegative(),
|
|
1327
|
+
chapters: z8.array(
|
|
1328
|
+
z8.object({
|
|
1329
|
+
startMs: z8.number().int().nonnegative(),
|
|
1330
|
+
endMs: z8.number().int().nonnegative(),
|
|
1331
|
+
title: z8.string()
|
|
1015
1332
|
})
|
|
1016
1333
|
)
|
|
1017
1334
|
});
|
|
1018
|
-
var ToolchainSchema =
|
|
1019
|
-
node:
|
|
1020
|
-
playwright:
|
|
1021
|
-
chromiumRevision:
|
|
1022
|
-
ffmpeg:
|
|
1335
|
+
var ToolchainSchema = z8.object({
|
|
1336
|
+
node: z8.string(),
|
|
1337
|
+
playwright: z8.string(),
|
|
1338
|
+
chromiumRevision: z8.string(),
|
|
1339
|
+
ffmpeg: z8.string(),
|
|
1023
1340
|
/** May be the literal "unknown" — never fabricated. */
|
|
1024
|
-
libass:
|
|
1025
|
-
fontSha256:
|
|
1341
|
+
libass: z8.string(),
|
|
1342
|
+
fontSha256: Sha2563
|
|
1026
1343
|
});
|
|
1027
|
-
var AssertionSchema =
|
|
1028
|
-
var StatusSchema =
|
|
1029
|
-
var VariantsSchema =
|
|
1030
|
-
var ValidationResultSchema =
|
|
1344
|
+
var AssertionSchema = z8.object({ id: z8.string().min(1), status: z8.enum(["passed", "failed"]) });
|
|
1345
|
+
var StatusSchema = z8.enum(["passed", "failed"]);
|
|
1346
|
+
var VariantsSchema = z8.enum(["soft", "hard"]);
|
|
1347
|
+
var ValidationResultSchema = z8.object({
|
|
1031
1348
|
...envelope("validate"),
|
|
1032
1349
|
/** Absent when the scenario could not be loaded at all. */
|
|
1033
|
-
scenario:
|
|
1034
|
-
id:
|
|
1035
|
-
title:
|
|
1036
|
-
language:
|
|
1350
|
+
scenario: z8.object({
|
|
1351
|
+
id: z8.string(),
|
|
1352
|
+
title: z8.string(),
|
|
1353
|
+
language: z8.string(),
|
|
1037
1354
|
/**
|
|
1038
1355
|
* Whether this scenario needs a person, and in which phase. Reported so an author
|
|
1039
1356
|
* — or an agent reading `--json` — learns that a scenario cannot run unattended
|
|
1040
1357
|
* without first trying to run it. Defaulted, so a result written before this parses.
|
|
1041
1358
|
*/
|
|
1042
|
-
handoff:
|
|
1043
|
-
sha256:
|
|
1359
|
+
handoff: z8.enum(["none", "preflight", "session"]).default("none"),
|
|
1360
|
+
sha256: Sha2563,
|
|
1361
|
+
/**
|
|
1362
|
+
* The terminal the scenario records instead of a web page, with its defaults applied, or
|
|
1363
|
+
* null for a browser scenario. Defaulted like `handoff`, so a result written before
|
|
1364
|
+
* terminals existed parses.
|
|
1365
|
+
*/
|
|
1366
|
+
terminal: z8.object({
|
|
1367
|
+
cols: z8.number().int(),
|
|
1368
|
+
rows: z8.number().int(),
|
|
1369
|
+
shell: z8.string(),
|
|
1370
|
+
command: z8.array(z8.string()).optional(),
|
|
1371
|
+
/** Present (true) only when the scenario declares `terminal.browser`. */
|
|
1372
|
+
browser: z8.literal(true).optional()
|
|
1373
|
+
}).nullable().default(null)
|
|
1044
1374
|
}).optional()
|
|
1045
1375
|
});
|
|
1046
|
-
var RunCommandResultSchema =
|
|
1376
|
+
var RunCommandResultSchema = z8.object({
|
|
1047
1377
|
...envelope("run"),
|
|
1048
1378
|
bundleDir: DISPLAY_PATH,
|
|
1049
1379
|
status: StatusSchema,
|
|
1050
|
-
cueCount:
|
|
1380
|
+
cueCount: z8.number().int().nonnegative(),
|
|
1051
1381
|
/** Zero on the Free Tier, and zero for a scenario with no `chapter` calls. */
|
|
1052
|
-
chapterCount:
|
|
1382
|
+
chapterCount: z8.number().int().nonnegative(),
|
|
1053
1383
|
/**
|
|
1054
1384
|
* How many lines the video speaks. Zero for a silent run, which is the default.
|
|
1055
1385
|
*
|
|
@@ -1058,119 +1388,130 @@ var RunCommandResultSchema = z7.object({
|
|
|
1058
1388
|
* scenario whose steps carry no `subtitle`, which produces a perfectly valid silent video and
|
|
1059
1389
|
* would look like the feature being broken rather than like nothing having been written to say.
|
|
1060
1390
|
*/
|
|
1061
|
-
narratedCount:
|
|
1062
|
-
diagnostics:
|
|
1063
|
-
assertions:
|
|
1064
|
-
outputs:
|
|
1391
|
+
narratedCount: z8.number().int().nonnegative(),
|
|
1392
|
+
diagnostics: z8.array(z8.object({ code: z8.string(), cueId: z8.string(), detail: z8.string() })),
|
|
1393
|
+
assertions: z8.array(AssertionSchema),
|
|
1394
|
+
outputs: z8.array(ArtifactRefSchema),
|
|
1065
1395
|
/**
|
|
1066
1396
|
* The previous bundle this run replaced, archived as a `.<timestamp>` sibling rather
|
|
1067
1397
|
* than deleted. Absent when nothing occupied the output directory — the common first
|
|
1068
1398
|
* run — or when `--no-archive` asked for the old replace semantics. Optional and
|
|
1069
1399
|
* additive, so every result written before archives existed still parses.
|
|
1070
1400
|
*/
|
|
1071
|
-
archivedPath: DISPLAY_PATH.optional()
|
|
1401
|
+
archivedPath: DISPLAY_PATH.optional(),
|
|
1402
|
+
/**
|
|
1403
|
+
* The capture-drift verdict against `<name>.baseline.json` beside the scenario. Absent when
|
|
1404
|
+
* the recording failed (a failed recording is not evidence of drift) and in every result
|
|
1405
|
+
* written before baselines existed, so it is optional and additive.
|
|
1406
|
+
*/
|
|
1407
|
+
baseline: BaselineReportSchema.optional()
|
|
1072
1408
|
});
|
|
1073
|
-
var CheckAssertionSchema = AssertionSchema.extend({ message:
|
|
1074
|
-
var CheckCommandResultSchema =
|
|
1409
|
+
var CheckAssertionSchema = AssertionSchema.extend({ message: z8.string().optional() });
|
|
1410
|
+
var CheckCommandResultSchema = z8.object({
|
|
1075
1411
|
...envelope("check"),
|
|
1076
1412
|
bundleDir: DISPLAY_PATH,
|
|
1077
1413
|
status: StatusSchema,
|
|
1078
|
-
assertions:
|
|
1414
|
+
assertions: z8.array(CheckAssertionSchema),
|
|
1079
1415
|
/**
|
|
1080
1416
|
* Handoff notes, plus a `speech.close` note when closing the narrator failed after
|
|
1081
1417
|
* capture. Cue, chapter, cursor and camera diagnostics are all derived from a render plan
|
|
1082
1418
|
* that `check` never builds — fabricating them would report on capabilities this command
|
|
1083
1419
|
* cannot see.
|
|
1084
1420
|
*/
|
|
1085
|
-
diagnostics:
|
|
1421
|
+
diagnostics: z8.array(z8.object({ code: z8.string(), cueId: z8.string(), detail: z8.string() })),
|
|
1422
|
+
/**
|
|
1423
|
+
* The capture-drift verdict against `<name>.baseline.json` beside the scenario. Absent when
|
|
1424
|
+
* the recording failed (a failed recording is not evidence of drift) and in every result
|
|
1425
|
+
* written before baselines existed, so it is optional and additive.
|
|
1426
|
+
*/
|
|
1427
|
+
baseline: BaselineReportSchema.optional()
|
|
1086
1428
|
});
|
|
1087
|
-
var RenderCommandResultSchema =
|
|
1429
|
+
var RenderCommandResultSchema = z8.object({
|
|
1088
1430
|
...envelope("render"),
|
|
1089
1431
|
bundleDir: DISPLAY_PATH,
|
|
1090
1432
|
variants: VariantsSchema,
|
|
1091
|
-
streams:
|
|
1433
|
+
streams: z8.object({
|
|
1092
1434
|
base: StreamSummarySchema.optional(),
|
|
1093
1435
|
soft: StreamSummarySchema.optional(),
|
|
1094
1436
|
hard: StreamSummarySchema.optional()
|
|
1095
1437
|
}),
|
|
1096
|
-
outputs:
|
|
1438
|
+
outputs: z8.array(ArtifactRefSchema)
|
|
1097
1439
|
});
|
|
1098
|
-
var VerificationReportSchema =
|
|
1440
|
+
var VerificationReportSchema = z8.object({
|
|
1099
1441
|
...envelope("verify"),
|
|
1100
1442
|
bundleDir: DISPLAY_PATH,
|
|
1101
|
-
artifactCount:
|
|
1443
|
+
artifactCount: z8.number().int().nonnegative()
|
|
1102
1444
|
});
|
|
1103
|
-
var WarmCommandResultSchema =
|
|
1445
|
+
var WarmCommandResultSchema = z8.object({
|
|
1104
1446
|
...envelope("warm"),
|
|
1105
1447
|
bundleDir: DISPLAY_PATH,
|
|
1106
|
-
clipCount:
|
|
1107
|
-
synthesizedCount:
|
|
1108
|
-
cachedCount:
|
|
1448
|
+
clipCount: z8.number().int().nonnegative(),
|
|
1449
|
+
synthesizedCount: z8.number().int().nonnegative(),
|
|
1450
|
+
cachedCount: z8.number().int().nonnegative()
|
|
1109
1451
|
});
|
|
1110
|
-
var
|
|
1111
|
-
var DiffCommandResultSchema = z7.object({
|
|
1452
|
+
var DiffCommandResultSchema = z8.object({
|
|
1112
1453
|
...envelope("diff"),
|
|
1113
1454
|
bundleA: DISPLAY_PATH,
|
|
1114
1455
|
bundleB: DISPLAY_PATH,
|
|
1115
|
-
identical:
|
|
1116
|
-
differences:
|
|
1456
|
+
identical: z8.boolean(),
|
|
1457
|
+
differences: z8.array(DifferenceSchema)
|
|
1117
1458
|
});
|
|
1118
|
-
var CompareCommandResultSchema =
|
|
1459
|
+
var CompareCommandResultSchema = z8.object({
|
|
1119
1460
|
...envelope("compare"),
|
|
1120
1461
|
bundleA: DISPLAY_PATH,
|
|
1121
1462
|
bundleB: DISPLAY_PATH,
|
|
1122
|
-
checks:
|
|
1463
|
+
checks: z8.array(z8.object({ name: z8.string(), ok: z8.boolean(), detail: z8.string() }))
|
|
1123
1464
|
});
|
|
1124
|
-
var InitResultSchema =
|
|
1465
|
+
var InitResultSchema = z8.object({
|
|
1125
1466
|
...envelope("init"),
|
|
1126
|
-
files:
|
|
1127
|
-
validates:
|
|
1467
|
+
files: z8.array(DISPLAY_PATH),
|
|
1468
|
+
validates: z8.boolean()
|
|
1128
1469
|
});
|
|
1129
|
-
var PruneCandidateSchema =
|
|
1470
|
+
var PruneCandidateSchema = z8.object({
|
|
1130
1471
|
path: DISPLAY_PATH,
|
|
1131
|
-
bytes:
|
|
1132
|
-
scenarioId:
|
|
1472
|
+
bytes: z8.number().int().nonnegative(),
|
|
1473
|
+
scenarioId: z8.string().optional()
|
|
1133
1474
|
});
|
|
1134
|
-
var PruneCommandResultSchema =
|
|
1475
|
+
var PruneCommandResultSchema = z8.object({
|
|
1135
1476
|
...envelope("prune"),
|
|
1136
|
-
dryRun:
|
|
1477
|
+
dryRun: z8.boolean(),
|
|
1137
1478
|
/** What was removed (confirmed) or would be (dry run) — never includes a `failed` entry. */
|
|
1138
|
-
candidates:
|
|
1479
|
+
candidates: z8.array(PruneCandidateSchema),
|
|
1139
1480
|
/** A discovered bundle that was never attempted, and why — never silently dropped. */
|
|
1140
|
-
skipped:
|
|
1481
|
+
skipped: z8.array(z8.object({ path: DISPLAY_PATH, reason: z8.string() })),
|
|
1141
1482
|
/** A selected candidate `rm` was attempted on and could not remove, and why. Always empty
|
|
1142
1483
|
* on a dry run, since nothing is attempted until `--yes`. */
|
|
1143
|
-
failed:
|
|
1484
|
+
failed: z8.array(z8.object({ path: DISPLAY_PATH, reason: z8.string() })),
|
|
1144
1485
|
/** What `candidates` account for: bytes actually reclaimed if `dryRun` is false, or would
|
|
1145
1486
|
* be reclaimed if every candidate succeeded. */
|
|
1146
|
-
bytesReclaimed:
|
|
1487
|
+
bytesReclaimed: z8.number().int().nonnegative()
|
|
1147
1488
|
});
|
|
1148
|
-
var ImportCommandResultSchema =
|
|
1489
|
+
var ImportCommandResultSchema = z8.object({
|
|
1149
1490
|
...envelope("import"),
|
|
1150
1491
|
tracePath: DISPLAY_PATH,
|
|
1151
1492
|
outputPath: DISPLAY_PATH,
|
|
1152
1493
|
/** How many `demo.step` calls the draft carries. Zero only on the failure path. */
|
|
1153
|
-
stepCount:
|
|
1494
|
+
stepCount: z8.number().int().nonnegative(),
|
|
1154
1495
|
/** Whether the written draft already passes `plaintake validate`. Best-effort: a draft
|
|
1155
1496
|
* that fails is still written, still a success, and its problems are warnings here. */
|
|
1156
|
-
validates:
|
|
1497
|
+
validates: z8.boolean(),
|
|
1157
1498
|
/** Author-facing advice: multi-page traces, redactions, unimported trace calls, and the
|
|
1158
1499
|
* standing review-for-secrets line. Never empty on success. */
|
|
1159
|
-
warnings:
|
|
1500
|
+
warnings: z8.array(z8.string())
|
|
1160
1501
|
});
|
|
1161
|
-
var InspectResultSchema =
|
|
1502
|
+
var InspectResultSchema = z8.object({
|
|
1162
1503
|
...envelope("inspect"),
|
|
1163
1504
|
bundleDir: DISPLAY_PATH,
|
|
1164
1505
|
status: StatusSchema,
|
|
1165
|
-
scenario:
|
|
1506
|
+
scenario: z8.object({ id: z8.string(), sourceSha256: Sha2563 }),
|
|
1166
1507
|
toolchain: ToolchainSchema,
|
|
1167
|
-
video:
|
|
1508
|
+
video: z8.object({
|
|
1168
1509
|
/** The whole output: the cards, if any, plus the content between them. */
|
|
1169
|
-
durationMs:
|
|
1170
|
-
frames:
|
|
1171
|
-
width:
|
|
1172
|
-
height:
|
|
1173
|
-
fps:
|
|
1510
|
+
durationMs: z8.number().int(),
|
|
1511
|
+
frames: z8.number().int(),
|
|
1512
|
+
width: z8.number().int(),
|
|
1513
|
+
height: z8.number().int(),
|
|
1514
|
+
fps: z8.number().int(),
|
|
1174
1515
|
/**
|
|
1175
1516
|
* Split out because `durationMs` alone is misleading once a card exists: 12,034ms of
|
|
1176
1517
|
* output can be 9,034ms of recording. The parts are reported separately rather than
|
|
@@ -1185,20 +1526,20 @@ var InspectResultSchema = z7.object({
|
|
|
1185
1526
|
* the word means, and is worth stating because the plan's own `video.durationMs` is a
|
|
1186
1527
|
* different number that includes the opening card.
|
|
1187
1528
|
*/
|
|
1188
|
-
contentDurationMs:
|
|
1189
|
-
introDurationMs:
|
|
1190
|
-
outroDurationMs:
|
|
1529
|
+
contentDurationMs: z8.number().int().nonnegative(),
|
|
1530
|
+
introDurationMs: z8.number().int().nonnegative().default(0),
|
|
1531
|
+
outroDurationMs: z8.number().int().nonnegative()
|
|
1191
1532
|
}),
|
|
1192
|
-
captions:
|
|
1193
|
-
language:
|
|
1194
|
-
cueCount:
|
|
1195
|
-
characterCount:
|
|
1196
|
-
medianCueMs:
|
|
1197
|
-
shortestCueMs:
|
|
1198
|
-
longestCueMs:
|
|
1533
|
+
captions: z8.object({
|
|
1534
|
+
language: z8.string(),
|
|
1535
|
+
cueCount: z8.number().int().nonnegative(),
|
|
1536
|
+
characterCount: z8.number().int().nonnegative(),
|
|
1537
|
+
medianCueMs: z8.number().int().nonnegative(),
|
|
1538
|
+
shortestCueMs: z8.number().int().nonnegative(),
|
|
1539
|
+
longestCueMs: z8.number().int().nonnegative()
|
|
1199
1540
|
}),
|
|
1200
1541
|
/** Measured from the rendered MP4 where possible, declared from the plan otherwise. */
|
|
1201
|
-
chapters:
|
|
1542
|
+
chapters: z8.array(z8.object({ startMs: z8.number().int().nonnegative(), title: z8.string() })),
|
|
1202
1543
|
/**
|
|
1203
1544
|
* The narration this bundle carries, or absent for a silent one.
|
|
1204
1545
|
*
|
|
@@ -1209,13 +1550,13 @@ var InspectResultSchema = z7.object({
|
|
|
1209
1550
|
* model behind it, and inventing a voice id for it would make the one field that answers "why
|
|
1210
1551
|
* does this sound different" answer wrongly.
|
|
1211
1552
|
*/
|
|
1212
|
-
narration:
|
|
1213
|
-
clipCount:
|
|
1553
|
+
narration: z8.object({
|
|
1554
|
+
clipCount: z8.number().int().positive(),
|
|
1214
1555
|
/** Total speech, excluding the silence between clips. Not the video's length. */
|
|
1215
|
-
spokenMs:
|
|
1556
|
+
spokenMs: z8.number().int().nonnegative(),
|
|
1216
1557
|
/** How many clips the author supplied rather than the model producing. */
|
|
1217
|
-
suppliedCount:
|
|
1218
|
-
voice:
|
|
1558
|
+
suppliedCount: z8.number().int().nonnegative(),
|
|
1559
|
+
voice: z8.string().optional()
|
|
1219
1560
|
}).optional(),
|
|
1220
1561
|
/**
|
|
1221
1562
|
* The multi-actor demo's cast, or absent for a single-actor bundle — every bundle
|
|
@@ -1232,19 +1573,19 @@ var InspectResultSchema = z7.object({
|
|
|
1232
1573
|
* the field a reader of this JSON would already trust as the timeline, not from a second
|
|
1233
1574
|
* field that only agrees with it by a schema-level invariant this result does not surface.
|
|
1234
1575
|
*/
|
|
1235
|
-
actors:
|
|
1576
|
+
actors: z8.object({
|
|
1236
1577
|
/** `plan.actors.length`. Reported alongside `labels` rather than left for a caller to
|
|
1237
1578
|
* compute, matching `narration.clipCount` sitting beside `narration` above for the
|
|
1238
1579
|
* same reason: the count is the fact most callers want first, before the list. */
|
|
1239
|
-
count:
|
|
1580
|
+
count: z8.number().int().min(2),
|
|
1240
1581
|
/** In `plan.actors` order — the cast list a badge or transition card would draw, not
|
|
1241
1582
|
* deduplicated or sorted. */
|
|
1242
|
-
labels:
|
|
1583
|
+
labels: z8.array(z8.string().min(1)).min(2),
|
|
1243
1584
|
/** Hand-offs, not participants: one fewer than `plan.segments.length`, since a
|
|
1244
1585
|
* segment is a window one actor already holds and a turn is the cut between two of
|
|
1245
1586
|
* them — the same count `plan.transitionCards.length` carries, arrived at
|
|
1246
1587
|
* independently from `segments` for the reason above. */
|
|
1247
|
-
turnCount:
|
|
1588
|
+
turnCount: z8.number().int().nonnegative()
|
|
1248
1589
|
}).refine((value) => value.count === value.labels.length, "actors.count must equal actors.labels.length").optional(),
|
|
1249
1590
|
/**
|
|
1250
1591
|
* The explain scenes this bundle cut away to, in composite order, or absent for a bundle
|
|
@@ -1256,20 +1597,20 @@ var InspectResultSchema = z7.object({
|
|
|
1256
1597
|
* `actors` is sourced from the plan rather than re-read from `demo.explain()` calls: a
|
|
1257
1598
|
* bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
1258
1599
|
*/
|
|
1259
|
-
explainScenes:
|
|
1260
|
-
|
|
1261
|
-
id:
|
|
1262
|
-
durationMs:
|
|
1600
|
+
explainScenes: z8.array(
|
|
1601
|
+
z8.object({
|
|
1602
|
+
id: z8.string().min(1),
|
|
1603
|
+
durationMs: z8.number().int().positive()
|
|
1263
1604
|
})
|
|
1264
1605
|
).min(1).optional(),
|
|
1265
1606
|
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
1266
|
-
outputs:
|
|
1267
|
-
assertions:
|
|
1268
|
-
artifactCount:
|
|
1607
|
+
outputs: z8.array(ArtifactRefSchema),
|
|
1608
|
+
assertions: z8.array(AssertionSchema),
|
|
1609
|
+
artifactCount: z8.number().int().nonnegative(),
|
|
1269
1610
|
/** Everything the manifest accounts for. What "delete this bundle" would reclaim. */
|
|
1270
|
-
bundleBytes:
|
|
1611
|
+
bundleBytes: z8.number().int().nonnegative()
|
|
1271
1612
|
});
|
|
1272
|
-
var DoctorResultSchema =
|
|
1613
|
+
var DoctorResultSchema = z8.object({
|
|
1273
1614
|
...envelope("doctor"),
|
|
1274
1615
|
/**
|
|
1275
1616
|
* The PlainTake build this is. Here rather than in `ToolchainSchema` on purpose:
|
|
@@ -1277,12 +1618,12 @@ var DoctorResultSchema = z7.object({
|
|
|
1277
1618
|
* manifest on every release and break the byte-for-byte golden bundle. `doctor` is the
|
|
1278
1619
|
* one command a bug report is asked for, which makes it the right place.
|
|
1279
1620
|
*/
|
|
1280
|
-
version:
|
|
1281
|
-
ffmpeg:
|
|
1282
|
-
ffprobe:
|
|
1283
|
-
libass:
|
|
1284
|
-
hasLibass:
|
|
1285
|
-
hasX264:
|
|
1621
|
+
version: z8.string(),
|
|
1622
|
+
ffmpeg: z8.string(),
|
|
1623
|
+
ffprobe: z8.string(),
|
|
1624
|
+
libass: z8.string(),
|
|
1625
|
+
hasLibass: z8.boolean(),
|
|
1626
|
+
hasX264: z8.boolean(),
|
|
1286
1627
|
/**
|
|
1287
1628
|
* Whether this FFmpeg can encode AAC, which is what a narrated bundle is muxed with.
|
|
1288
1629
|
*
|
|
@@ -1292,8 +1633,8 @@ var DoctorResultSchema = z7.object({
|
|
|
1292
1633
|
* that actually needs it, with a fix string. Failing `doctor` here would be telling someone
|
|
1293
1634
|
* their installation is broken for a capability they may never turn on.
|
|
1294
1635
|
*/
|
|
1295
|
-
hasAac:
|
|
1296
|
-
filters:
|
|
1636
|
+
hasAac: z8.boolean(),
|
|
1637
|
+
filters: z8.array(z8.string()),
|
|
1297
1638
|
/**
|
|
1298
1639
|
* Whether spoken narration could be produced right now, and what is missing if not.
|
|
1299
1640
|
*
|
|
@@ -1302,32 +1643,27 @@ var DoctorResultSchema = z7.object({
|
|
|
1302
1643
|
* `openNarrator` asks before a run, answered by the same function (`speechAssetProblems`), so
|
|
1303
1644
|
* `doctor` cannot say speech is fine and then a run refuse it.
|
|
1304
1645
|
*/
|
|
1305
|
-
speech:
|
|
1306
|
-
ready:
|
|
1646
|
+
speech: z8.object({
|
|
1647
|
+
ready: z8.boolean(),
|
|
1307
1648
|
/** Installed voices, in catalogue order. Empty means the model cannot speak yet. */
|
|
1308
|
-
voices:
|
|
1649
|
+
voices: z8.array(z8.string()),
|
|
1309
1650
|
/** What is missing, in the same words a refused run would use. Empty when ready. */
|
|
1310
|
-
notes:
|
|
1651
|
+
notes: z8.array(z8.string())
|
|
1311
1652
|
}),
|
|
1312
1653
|
/**
|
|
1313
|
-
* Whether
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1317
|
-
* most runs will never invoke. `assertPlainmotion` (`recorder-playwright`) is the same
|
|
1318
|
-
* probe a run's own first `demo.explain()` pays, so `doctor` cannot say plainmotion is fine
|
|
1319
|
-
* and then a run refuse it.
|
|
1654
|
+
* Whether a terminal scenario could be recorded right now: the vendored node-pty loads, and
|
|
1655
|
+
* xterm.js and the terminal font are in place. Never folded into `problems` — the same stance as
|
|
1656
|
+
* `speech` — because only a scenario declaring `terminal` needs any of it. Optional so a result
|
|
1657
|
+
* written before terminals existed parses.
|
|
1320
1658
|
*/
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
/**
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
note: z7.string().optional()
|
|
1327
|
-
})
|
|
1659
|
+
terminal: z8.object({
|
|
1660
|
+
ready: z8.boolean(),
|
|
1661
|
+
/** What is missing, in the words a refused run would use. Empty when ready. */
|
|
1662
|
+
notes: z8.array(z8.string())
|
|
1663
|
+
}).optional()
|
|
1328
1664
|
});
|
|
1329
1665
|
var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
|
|
1330
|
-
var ActivateReasonSchema =
|
|
1666
|
+
var ActivateReasonSchema = z8.enum([
|
|
1331
1667
|
"empty-key",
|
|
1332
1668
|
"not-found",
|
|
1333
1669
|
"revoked",
|
|
@@ -1335,16 +1671,16 @@ var ActivateReasonSchema = z7.enum([
|
|
|
1335
1671
|
"unexpected-response",
|
|
1336
1672
|
"not-configured"
|
|
1337
1673
|
]);
|
|
1338
|
-
var ActivateResultSchema =
|
|
1674
|
+
var ActivateResultSchema = z8.object({
|
|
1339
1675
|
...envelope("activate"),
|
|
1340
1676
|
/** Present only when ok is false, so a machine reader can distinguish the classes. */
|
|
1341
1677
|
reason: ActivateReasonSchema.optional(),
|
|
1342
1678
|
/** Present only on success — a failed activation has no record. The key itself is
|
|
1343
1679
|
* deliberately absent: the caller already has it, and stdout may be logged. */
|
|
1344
|
-
licence:
|
|
1345
|
-
subject:
|
|
1346
|
-
activatedAt:
|
|
1347
|
-
uses:
|
|
1680
|
+
licence: z8.object({
|
|
1681
|
+
subject: z8.string().min(1),
|
|
1682
|
+
activatedAt: z8.string().min(1),
|
|
1683
|
+
uses: z8.number().int().nonnegative()
|
|
1348
1684
|
}).optional()
|
|
1349
1685
|
}).refine(okMatchesProblems, "ok must be true iff problems is empty").refine(
|
|
1350
1686
|
// Making the "present only when" comments above literally true, so a reader can
|
|
@@ -1352,25 +1688,25 @@ var ActivateResultSchema = z7.object({
|
|
|
1352
1688
|
(value) => value.reason !== void 0 === !value.ok && (value.licence === void 0 || value.ok),
|
|
1353
1689
|
"reason appears iff ok is false, and licence only on success"
|
|
1354
1690
|
);
|
|
1355
|
-
var LicenceResultSchema =
|
|
1356
|
-
|
|
1691
|
+
var LicenceResultSchema = z8.discriminatedUnion("tier", [
|
|
1692
|
+
z8.object({
|
|
1357
1693
|
...envelope("licence"),
|
|
1358
|
-
tier:
|
|
1694
|
+
tier: z8.literal("free"),
|
|
1359
1695
|
/**
|
|
1360
1696
|
* Why a present-but-unusable record degraded to free, in readLicense's own words.
|
|
1361
1697
|
* Must never quote a filesystem path: assertNoAbsolutePaths exempts only `problems`.
|
|
1362
1698
|
*/
|
|
1363
|
-
problem:
|
|
1699
|
+
problem: z8.string().optional()
|
|
1364
1700
|
}),
|
|
1365
|
-
|
|
1701
|
+
z8.object({
|
|
1366
1702
|
...envelope("licence"),
|
|
1367
|
-
tier:
|
|
1368
|
-
subject:
|
|
1369
|
-
activatedAt:
|
|
1370
|
-
uses:
|
|
1703
|
+
tier: z8.literal("pro"),
|
|
1704
|
+
subject: z8.string().min(1),
|
|
1705
|
+
activatedAt: z8.string().min(1),
|
|
1706
|
+
uses: z8.number().int().nonnegative()
|
|
1371
1707
|
})
|
|
1372
1708
|
]).refine(okMatchesProblems, "ok must be true iff problems is empty");
|
|
1373
|
-
var PublishReasonSchema =
|
|
1709
|
+
var PublishReasonSchema = z8.enum([
|
|
1374
1710
|
"no-endpoint",
|
|
1375
1711
|
"no-key",
|
|
1376
1712
|
"invalid-store",
|
|
@@ -1380,39 +1716,82 @@ var PublishReasonSchema = z7.enum([
|
|
|
1380
1716
|
"hash-mismatch",
|
|
1381
1717
|
"length-mismatch",
|
|
1382
1718
|
"conflict",
|
|
1719
|
+
"taken-down",
|
|
1383
1720
|
"unexpected-response"
|
|
1384
1721
|
]);
|
|
1385
|
-
var PublishResultSchema =
|
|
1386
|
-
|
|
1722
|
+
var PublishResultSchema = z8.discriminatedUnion("ok", [
|
|
1723
|
+
z8.object({
|
|
1387
1724
|
...envelope("publish"),
|
|
1388
|
-
ok:
|
|
1725
|
+
ok: z8.literal(false),
|
|
1389
1726
|
reason: PublishReasonSchema
|
|
1390
1727
|
}),
|
|
1391
|
-
|
|
1728
|
+
z8.object({
|
|
1392
1729
|
...envelope("publish"),
|
|
1393
|
-
ok:
|
|
1394
|
-
videoId:
|
|
1730
|
+
ok: z8.literal(true),
|
|
1731
|
+
videoId: z8.string().regex(/^[a-z2-7]{26}$/, "must be a 26-char base32 share id"),
|
|
1395
1732
|
/** The page viewers open — the endpoint plus the id. */
|
|
1396
|
-
url:
|
|
1733
|
+
url: z8.string().regex(/^https?:\/\/\S+$/, "must be an http or https URL"),
|
|
1397
1734
|
/** False when the video was already on the service — nothing was uploaded. */
|
|
1398
|
-
uploaded:
|
|
1735
|
+
uploaded: z8.boolean(),
|
|
1736
|
+
/**
|
|
1737
|
+
* True when the share had been unpublished (or had expired) and this publish brought
|
|
1738
|
+
* it back — `unpublish`'s undo. Defaults to false so a result written before the
|
|
1739
|
+
* field existed still parses.
|
|
1740
|
+
*/
|
|
1741
|
+
restored: z8.boolean().default(false),
|
|
1399
1742
|
/**
|
|
1400
1743
|
* Present only when `--remember` was asked: true when the key was stored beside
|
|
1401
1744
|
* license.json, false when the key file could not be written. The service's
|
|
1402
1745
|
* proof of the key (an accepted PUT, or the identity check on an already-shared
|
|
1403
1746
|
* run) is part of every success, so it is never the reason.
|
|
1404
1747
|
*/
|
|
1405
|
-
remembered:
|
|
1748
|
+
remembered: z8.boolean().optional(),
|
|
1406
1749
|
/**
|
|
1407
1750
|
* The ISO-8601 instant the share link now dies at — present only when an expiry
|
|
1408
1751
|
* policy (`--expiry` or publish.json's `expiry`) was set. Absent means never
|
|
1409
1752
|
* expires, exactly like a publish before the feature existed.
|
|
1410
1753
|
*/
|
|
1411
|
-
expiresAt:
|
|
1412
|
-
captions:
|
|
1413
|
-
chapters:
|
|
1754
|
+
expiresAt: z8.string().min(1).optional(),
|
|
1755
|
+
captions: z8.boolean(),
|
|
1756
|
+
chapters: z8.boolean(),
|
|
1414
1757
|
/** False is a state, not a fault: no FFmpeg on PATH means no poster frame. */
|
|
1415
|
-
poster:
|
|
1758
|
+
poster: z8.boolean()
|
|
1759
|
+
})
|
|
1760
|
+
]).refine(okMatchesProblems, "ok must be true iff problems is empty");
|
|
1761
|
+
var UnpublishReasonSchema = z8.enum([
|
|
1762
|
+
"no-endpoint",
|
|
1763
|
+
"no-key",
|
|
1764
|
+
"invalid-store",
|
|
1765
|
+
"not-a-bundle",
|
|
1766
|
+
"invalid-target",
|
|
1767
|
+
"endpoint-mismatch",
|
|
1768
|
+
"unauthorized",
|
|
1769
|
+
"forbidden",
|
|
1770
|
+
"not-found",
|
|
1771
|
+
"unreachable",
|
|
1772
|
+
"unexpected-response"
|
|
1773
|
+
]);
|
|
1774
|
+
var UnpublishResultSchema = z8.discriminatedUnion("ok", [
|
|
1775
|
+
z8.object({
|
|
1776
|
+
...envelope("unpublish"),
|
|
1777
|
+
ok: z8.literal(false),
|
|
1778
|
+
reason: UnpublishReasonSchema
|
|
1779
|
+
}),
|
|
1780
|
+
z8.object({
|
|
1781
|
+
...envelope("unpublish"),
|
|
1782
|
+
ok: z8.literal(true),
|
|
1783
|
+
videoId: z8.string().regex(/^[a-z2-7]{26}$/, "must be a 26-char base32 share id"),
|
|
1784
|
+
/** The page that now answers 410. */
|
|
1785
|
+
url: z8.string().regex(/^https?:\/\/\S+$/, "must be an http or https URL"),
|
|
1786
|
+
/** When the share went dark; repeating an unpublish keeps the first instant. */
|
|
1787
|
+
deletedAt: z8.string().min(1),
|
|
1788
|
+
/**
|
|
1789
|
+
* Who holds the tombstone. `admin` means the service's operator had already taken
|
|
1790
|
+
* the share down — still a success (it is dark), but only the operator can undo it.
|
|
1791
|
+
*/
|
|
1792
|
+
deletedBy: z8.enum(["producer", "admin", "expired"]),
|
|
1793
|
+
/** When retention may delete the bytes for good; a republish before then restores. */
|
|
1794
|
+
purgeAfter: z8.string().min(1)
|
|
1416
1795
|
})
|
|
1417
1796
|
]).refine(okMatchesProblems, "ok must be true iff problems is empty");
|
|
1418
1797
|
|