@plaintake/scenario 1.12.0 → 1.14.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/dist/index.js +337 -294
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/narration-index.d.ts +60 -0
- package/dist/schema/results.d.ts +19 -1
- package/dist/schema/scenario.d.ts +5 -2
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -274,37 +274,73 @@ var BundleManifestSchema = z4.object({
|
|
|
274
274
|
)
|
|
275
275
|
});
|
|
276
276
|
|
|
277
|
-
// ../schema/src/
|
|
277
|
+
// ../schema/src/narration-index.ts
|
|
278
278
|
import { z as z5 } from "zod";
|
|
279
|
-
var
|
|
279
|
+
var NARRATION_INDEX_SCHEMA = "agent-demo.narration-index/v1";
|
|
280
|
+
var NarrationIndexClipSchema = z5.object({
|
|
281
|
+
/** The step id, which is also the clip id and the cache filename stem. */
|
|
280
282
|
id: z5.string().min(1),
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
/**
|
|
284
|
+
* The marker-free, normalised line — `clipFor`'s `text`. Never empty: the runtime does not call
|
|
285
|
+
* the narrator for a step that normalises to nothing, so an empty line here is a corrupt index.
|
|
286
|
+
*/
|
|
287
|
+
text: z5.string().min(1),
|
|
288
|
+
/**
|
|
289
|
+
* The author's `[pause]` segments, present only when a marker actually split the line — which is
|
|
290
|
+
* two or more pieces, never one. When present it is what the cache key is built from (the pieces
|
|
291
|
+
* join by NUL), so it must round-trip exactly or a warmed marked line lands under a different key
|
|
292
|
+
* from the one the recording will look for.
|
|
293
|
+
*/
|
|
294
|
+
breaks: z5.array(z5.string().min(1)).min(2).optional(),
|
|
295
|
+
/**
|
|
296
|
+
* The resolved voice, present only when it was not the run's default (`speech.voice` below).
|
|
297
|
+
* Absent means "spoken by the default", exactly as it does on the frozen plan's per-clip voice.
|
|
298
|
+
*/
|
|
299
|
+
voice: z5.string().min(1).optional()
|
|
284
300
|
});
|
|
285
|
-
var
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
301
|
+
var NarrationIndexSchema = z5.object({
|
|
302
|
+
schema: z5.literal(NARRATION_INDEX_SCHEMA),
|
|
303
|
+
speech: z5.object({
|
|
304
|
+
voice: z5.string().min(1),
|
|
305
|
+
voices: z5.array(z5.string().min(1)).optional(),
|
|
306
|
+
speed: z5.number().positive(),
|
|
307
|
+
dtype: z5.string().min(1),
|
|
308
|
+
pronunciations: z5.record(z5.string().min(1), z5.string()).optional()
|
|
309
|
+
}),
|
|
310
|
+
clips: z5.array(NarrationIndexClipSchema).min(1)
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ../schema/src/render-plan.ts
|
|
314
|
+
import { z as z6 } from "zod";
|
|
315
|
+
var CueSchema = z6.object({
|
|
316
|
+
id: z6.string().min(1),
|
|
317
|
+
startMs: z6.number().int().nonnegative(),
|
|
318
|
+
endMs: z6.number().int().positive(),
|
|
319
|
+
lines: z6.array(z6.string()).min(1).max(2)
|
|
320
|
+
});
|
|
321
|
+
var HEX_COLOUR = z6.string().regex(/^#[0-9A-Fa-f]{6}$/, "must be a #RRGGBB colour");
|
|
322
|
+
var OutroSchema = z6.object({
|
|
323
|
+
durationMs: z6.number().int().positive().max(15e3),
|
|
324
|
+
lines: z6.array(z6.string().min(1)).min(1).max(2),
|
|
289
325
|
backgroundColor: HEX_COLOUR,
|
|
290
326
|
textColor: HEX_COLOUR,
|
|
291
|
-
assPath:
|
|
327
|
+
assPath: z6.literal("captions/outro.ass")
|
|
292
328
|
});
|
|
293
|
-
var IntroSchema =
|
|
294
|
-
durationMs:
|
|
295
|
-
lines:
|
|
329
|
+
var IntroSchema = z6.object({
|
|
330
|
+
durationMs: z6.number().int().positive().max(15e3),
|
|
331
|
+
lines: z6.array(z6.string().min(1)).min(1).max(2),
|
|
296
332
|
backgroundColor: HEX_COLOUR,
|
|
297
333
|
textColor: HEX_COLOUR,
|
|
298
|
-
assPath:
|
|
334
|
+
assPath: z6.literal("captions/intro.ass")
|
|
299
335
|
});
|
|
300
|
-
var ChapterMarkSchema =
|
|
301
|
-
startMs:
|
|
302
|
-
endMs:
|
|
303
|
-
title:
|
|
336
|
+
var ChapterMarkSchema = z6.object({
|
|
337
|
+
startMs: z6.number().int().nonnegative(),
|
|
338
|
+
endMs: z6.number().int().positive(),
|
|
339
|
+
title: z6.string().min(1)
|
|
304
340
|
});
|
|
305
|
-
var ChaptersSchema =
|
|
306
|
-
metadataPath:
|
|
307
|
-
marks:
|
|
341
|
+
var ChaptersSchema = z6.object({
|
|
342
|
+
metadataPath: z6.literal("render/chapters.ffmetadata"),
|
|
343
|
+
marks: z6.array(ChapterMarkSchema).min(1)
|
|
308
344
|
}).refine(
|
|
309
345
|
({ marks }) => marks[0]?.startMs === 0 && marks.every(
|
|
310
346
|
(mark, index) => mark.endMs > mark.startMs && (index === 0 || marks[index - 1]?.endMs === mark.startMs)
|
|
@@ -317,14 +353,14 @@ var ChaptersSchema = z5.object({
|
|
|
317
353
|
path: ["marks"]
|
|
318
354
|
}
|
|
319
355
|
);
|
|
320
|
-
var CursorPointSchema =
|
|
321
|
-
id:
|
|
322
|
-
x:
|
|
323
|
-
y:
|
|
324
|
-
arriveMs:
|
|
325
|
-
departMs:
|
|
326
|
-
action:
|
|
327
|
-
rippleMs:
|
|
356
|
+
var CursorPointSchema = z6.object({
|
|
357
|
+
id: z6.string().min(1),
|
|
358
|
+
x: z6.number().int().min(0).max(1920),
|
|
359
|
+
y: z6.number().int().min(0).max(1080),
|
|
360
|
+
arriveMs: z6.number().int().nonnegative(),
|
|
361
|
+
departMs: z6.number().int().nonnegative(),
|
|
362
|
+
action: z6.enum(["click", "type", "point"]),
|
|
363
|
+
rippleMs: z6.number().int().nonnegative().optional(),
|
|
328
364
|
/**
|
|
329
365
|
* Size-correction percent, applied by the emitter with `\fscx`/`\fscy`, so the arrow
|
|
330
366
|
* stays one size on screen whatever the frame does to it between the cursor filter and
|
|
@@ -355,11 +391,11 @@ var CursorPointSchema = z5.object({
|
|
|
355
391
|
* always did — so a cursor bundle from before the camera, or from before the letterbox,
|
|
356
392
|
* still parses and regenerates byte-for-byte.
|
|
357
393
|
*/
|
|
358
|
-
scale:
|
|
394
|
+
scale: z6.number().int().min(1).max(400).optional()
|
|
359
395
|
});
|
|
360
|
-
var CursorSchema =
|
|
361
|
-
assPath:
|
|
362
|
-
points:
|
|
396
|
+
var CursorSchema = z6.object({
|
|
397
|
+
assPath: z6.literal("captions/cursor.ass"),
|
|
398
|
+
points: z6.array(CursorPointSchema).min(1)
|
|
363
399
|
}).refine(
|
|
364
400
|
({ points }) => points.every(
|
|
365
401
|
(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)
|
|
@@ -372,18 +408,18 @@ var CursorSchema = z5.object({
|
|
|
372
408
|
path: ["points"]
|
|
373
409
|
}
|
|
374
410
|
);
|
|
375
|
-
var CameraShotSchema =
|
|
376
|
-
id:
|
|
377
|
-
enterMs:
|
|
378
|
-
holdFromMs:
|
|
379
|
-
x:
|
|
380
|
-
y:
|
|
381
|
-
w:
|
|
382
|
-
h:
|
|
411
|
+
var CameraShotSchema = z6.object({
|
|
412
|
+
id: z6.string().min(1),
|
|
413
|
+
enterMs: z6.number().int().nonnegative(),
|
|
414
|
+
holdFromMs: z6.number().int().nonnegative(),
|
|
415
|
+
x: z6.number().int().min(0),
|
|
416
|
+
y: z6.number().int().min(0),
|
|
417
|
+
w: z6.number().int().positive(),
|
|
418
|
+
h: z6.number().int().positive()
|
|
383
419
|
});
|
|
384
|
-
var CameraSchema =
|
|
385
|
-
commandPath:
|
|
386
|
-
shots:
|
|
420
|
+
var CameraSchema = z6.object({
|
|
421
|
+
commandPath: z6.literal("render/camera.cmd"),
|
|
422
|
+
shots: z6.array(CameraShotSchema).min(1)
|
|
387
423
|
}).refine(
|
|
388
424
|
({ shots }) => shots.every(
|
|
389
425
|
(shot) => shot.x % 2 === 0 && shot.y % 2 === 0 && // Implied by the %32 and 9w/16 rules below; kept so a future relaxation of
|
|
@@ -411,39 +447,39 @@ var CameraSchema = z5.object({
|
|
|
411
447
|
path: ["shots"]
|
|
412
448
|
}
|
|
413
449
|
);
|
|
414
|
-
var ActorSchema =
|
|
415
|
-
id:
|
|
416
|
-
label:
|
|
450
|
+
var ActorSchema = z6.object({
|
|
451
|
+
id: z6.string().min(1),
|
|
452
|
+
label: z6.string().min(1)
|
|
417
453
|
});
|
|
418
|
-
var SegmentSchema =
|
|
419
|
-
id:
|
|
420
|
-
actorId:
|
|
421
|
-
startMs:
|
|
422
|
-
endMs:
|
|
454
|
+
var SegmentSchema = z6.object({
|
|
455
|
+
id: z6.string().min(1),
|
|
456
|
+
actorId: z6.string().min(1),
|
|
457
|
+
startMs: z6.number().int().nonnegative(),
|
|
458
|
+
endMs: z6.number().int().positive()
|
|
423
459
|
});
|
|
424
|
-
var TransitionCardSchema =
|
|
425
|
-
fromActorId:
|
|
426
|
-
toActorId:
|
|
427
|
-
durationMs:
|
|
428
|
-
lines:
|
|
460
|
+
var TransitionCardSchema = z6.object({
|
|
461
|
+
fromActorId: z6.string().min(1),
|
|
462
|
+
toActorId: z6.string().min(1),
|
|
463
|
+
durationMs: z6.number().int().positive().max(15e3),
|
|
464
|
+
lines: z6.array(z6.string().min(1)).min(1).max(2),
|
|
429
465
|
backgroundColor: HEX_COLOUR,
|
|
430
466
|
textColor: HEX_COLOUR,
|
|
431
|
-
assPath:
|
|
467
|
+
assPath: z6.string().regex(/^captions\/turn-[0-9]+\.ass$/)
|
|
432
468
|
});
|
|
433
|
-
var ExplainSegmentPlanSchema =
|
|
434
|
-
id:
|
|
435
|
-
segmentPath:
|
|
436
|
-
durationMs:
|
|
469
|
+
var ExplainSegmentPlanSchema = z6.object({
|
|
470
|
+
id: z6.string().min(1),
|
|
471
|
+
segmentPath: z6.string().regex(/^explain\/[a-z0-9][a-z0-9-]*\/segment\.mp4$/),
|
|
472
|
+
durationMs: z6.number().int().positive()
|
|
437
473
|
});
|
|
438
|
-
var HighlightRectSchema =
|
|
439
|
-
id:
|
|
440
|
-
x:
|
|
441
|
-
y:
|
|
442
|
-
width:
|
|
443
|
-
height:
|
|
444
|
-
startMs:
|
|
445
|
-
endMs:
|
|
446
|
-
label:
|
|
474
|
+
var HighlightRectSchema = z6.object({
|
|
475
|
+
id: z6.string().min(1),
|
|
476
|
+
x: z6.number().int().min(0),
|
|
477
|
+
y: z6.number().int().min(0),
|
|
478
|
+
width: z6.number().int().positive(),
|
|
479
|
+
height: z6.number().int().positive(),
|
|
480
|
+
startMs: z6.number().int().nonnegative(),
|
|
481
|
+
endMs: z6.number().int().positive(),
|
|
482
|
+
label: z6.string().min(1).optional(),
|
|
447
483
|
/**
|
|
448
484
|
* The rect the spotlight glides *from*, when this window's cutout should animate rather
|
|
449
485
|
* than cut straight to the held `x`/`y`/`width`/`height` — the pre-reveal position a
|
|
@@ -455,11 +491,11 @@ var HighlightRectSchema = z5.object({
|
|
|
455
491
|
* whole span — so every plan frozen before this field existed, and every window a future
|
|
456
492
|
* derivation decides not to glide, is unaffected by construction.
|
|
457
493
|
*/
|
|
458
|
-
from:
|
|
459
|
-
x:
|
|
460
|
-
y:
|
|
461
|
-
width:
|
|
462
|
-
height:
|
|
494
|
+
from: z6.object({
|
|
495
|
+
x: z6.number().int().min(0),
|
|
496
|
+
y: z6.number().int().min(0),
|
|
497
|
+
width: z6.number().int().positive(),
|
|
498
|
+
height: z6.number().int().positive()
|
|
463
499
|
}).optional(),
|
|
464
500
|
/**
|
|
465
501
|
* The absolute video-ms at which the glide from `from` completes and the spotlight
|
|
@@ -468,7 +504,7 @@ var HighlightRectSchema = z5.object({
|
|
|
468
504
|
* has to be real time left to glide across) and no later than `endMs` (the window
|
|
469
505
|
* cannot settle after it has already closed).
|
|
470
506
|
*/
|
|
471
|
-
settleMs:
|
|
507
|
+
settleMs: z6.number().int().positive().optional()
|
|
472
508
|
}).refine(({ x, y, width, height }) => x + width <= 1920 && y + height <= 1080, {
|
|
473
509
|
// Unlike CameraShotSchema's crop window, a highlight rect is drawn by libass, not fed to
|
|
474
510
|
// `crop` — there is no yuv420p even-pixel rule to enforce — but it still has to fit the
|
|
@@ -498,9 +534,9 @@ var HighlightRectSchema = z5.object({
|
|
|
498
534
|
message: "settleMs must be strictly after startMs and no later than endMs",
|
|
499
535
|
path: ["settleMs"]
|
|
500
536
|
});
|
|
501
|
-
var HighlightSchema =
|
|
502
|
-
assPath:
|
|
503
|
-
rects:
|
|
537
|
+
var HighlightSchema = z6.object({
|
|
538
|
+
assPath: z6.literal("captions/highlight.ass"),
|
|
539
|
+
rects: z6.array(HighlightRectSchema).min(1)
|
|
504
540
|
}).refine(
|
|
505
541
|
({ rects }) => rects.every(
|
|
506
542
|
(rect, index) => rect.endMs > rect.startMs && (index === 0 || rects[index - 1].endMs <= rect.startMs)
|
|
@@ -510,8 +546,8 @@ var HighlightSchema = z5.object({
|
|
|
510
546
|
path: ["rects"]
|
|
511
547
|
}
|
|
512
548
|
);
|
|
513
|
-
var SpeechClipSchema =
|
|
514
|
-
id:
|
|
549
|
+
var SpeechClipSchema = z6.object({
|
|
550
|
+
id: z6.string().min(1),
|
|
515
551
|
/*
|
|
516
552
|
* Bundle-relative and unescapable by construction: no slash, no leading dot, so `..` and
|
|
517
553
|
* an absolute path are both unrepresentable. The renderer's `assertBundleRelative` says
|
|
@@ -525,10 +561,10 @@ var SpeechClipSchema = z5.object({
|
|
|
525
561
|
* character the recorder could have replaced. `narration.test.ts` asserts the two agree on a
|
|
526
562
|
* table of candidates, because a comment cannot.
|
|
527
563
|
*/
|
|
528
|
-
path:
|
|
529
|
-
atMs:
|
|
530
|
-
durationMs:
|
|
531
|
-
source:
|
|
564
|
+
path: z6.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
|
|
565
|
+
atMs: z6.number().int().nonnegative(),
|
|
566
|
+
durationMs: z6.number().int().positive(),
|
|
567
|
+
source: z6.enum(["synth", "file", "explain"]),
|
|
532
568
|
/*
|
|
533
569
|
* Which voice synthesised this clip, when it was not the plan-wide default that
|
|
534
570
|
* `engine.voice` names. Absent on every clip of a one-voice recording and on every
|
|
@@ -544,16 +580,16 @@ var SpeechClipSchema = z5.object({
|
|
|
544
580
|
* Optional and additive: every bundle frozen before per-step voices existed carries no
|
|
545
581
|
* `voice` on any clip and must keep verifying, which is what `optional()` buys.
|
|
546
582
|
*/
|
|
547
|
-
voice:
|
|
583
|
+
voice: z6.string().min(1).optional()
|
|
548
584
|
});
|
|
549
|
-
var SpeechEngineSchema =
|
|
550
|
-
name:
|
|
551
|
-
version:
|
|
552
|
-
modelSha256:
|
|
553
|
-
voice:
|
|
585
|
+
var SpeechEngineSchema = z6.object({
|
|
586
|
+
name: z6.string().min(1),
|
|
587
|
+
version: z6.string().min(1),
|
|
588
|
+
modelSha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
589
|
+
voice: z6.string().min(1)
|
|
554
590
|
});
|
|
555
|
-
var SpeechSchema =
|
|
556
|
-
trackPath:
|
|
591
|
+
var SpeechSchema = z6.object({
|
|
592
|
+
trackPath: z6.literal("speech/narration.wav"),
|
|
557
593
|
/*
|
|
558
594
|
* 24 kHz mono, because that is what Kokoro emits and nothing on this path resamples.
|
|
559
595
|
* Written as literals rather than imported from `@plaintake/audio`, which owns the
|
|
@@ -561,9 +597,9 @@ var SpeechSchema = z5.object({
|
|
|
561
597
|
* be the cycle `no-circular` refuses. `packages/audio/src/wav.test.ts` asserts the two
|
|
562
598
|
* agree, since a comment cannot.
|
|
563
599
|
*/
|
|
564
|
-
sampleRate:
|
|
565
|
-
channels:
|
|
566
|
-
clips:
|
|
600
|
+
sampleRate: z6.literal(24e3),
|
|
601
|
+
channels: z6.literal(1),
|
|
602
|
+
clips: z6.array(SpeechClipSchema).min(1),
|
|
567
603
|
engine: SpeechEngineSchema.optional()
|
|
568
604
|
}).refine(
|
|
569
605
|
({ clips }) => new Set(clips.map((clip) => clip.id)).size === clips.length && clips.every(
|
|
@@ -589,14 +625,14 @@ var SpeechSchema = z5.object({
|
|
|
589
625
|
message: "engine must be recorded whenever any clip was synthesised",
|
|
590
626
|
path: ["engine"]
|
|
591
627
|
});
|
|
592
|
-
var ThemeSchema =
|
|
628
|
+
var ThemeSchema = z6.object({ accentColor: HEX_COLOUR });
|
|
593
629
|
var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
|
|
594
|
-
var RenderPlanSchema =
|
|
595
|
-
schema:
|
|
596
|
-
source:
|
|
597
|
-
path:
|
|
598
|
-
sha256:
|
|
599
|
-
durationMs:
|
|
630
|
+
var RenderPlanSchema = z6.object({
|
|
631
|
+
schema: z6.literal(RENDER_PLAN_SCHEMA),
|
|
632
|
+
source: z6.object({
|
|
633
|
+
path: z6.literal("raw/session.webm"),
|
|
634
|
+
sha256: z6.string().regex(/^[0-9a-f]{64}$/),
|
|
635
|
+
durationMs: z6.number().int().nonnegative()
|
|
600
636
|
}),
|
|
601
637
|
/**
|
|
602
638
|
* The frame. **Three geometries live here and they are not the same thing**, which is the
|
|
@@ -658,17 +694,17 @@ var RenderPlanSchema = z5.object({
|
|
|
658
694
|
* one would be backwards — a plan is frozen once and rendered later, so both belong at the
|
|
659
695
|
* parse, not at the encode.
|
|
660
696
|
*/
|
|
661
|
-
video:
|
|
662
|
-
width:
|
|
663
|
-
height:
|
|
664
|
-
fps:
|
|
665
|
-
pixelFormat:
|
|
666
|
-
capture:
|
|
667
|
-
letterbox:
|
|
668
|
-
width:
|
|
669
|
-
height:
|
|
670
|
-
x:
|
|
671
|
-
y:
|
|
697
|
+
video: z6.object({
|
|
698
|
+
width: z6.number().int().positive().multipleOf(2),
|
|
699
|
+
height: z6.number().int().positive().multipleOf(2),
|
|
700
|
+
fps: z6.literal(30),
|
|
701
|
+
pixelFormat: z6.literal("yuv420p"),
|
|
702
|
+
capture: z6.object({ width: z6.literal(1920), height: z6.literal(1080) }).default({ width: 1920, height: 1080 }),
|
|
703
|
+
letterbox: z6.object({
|
|
704
|
+
width: z6.number().int().positive().multipleOf(2),
|
|
705
|
+
height: z6.number().int().positive().multipleOf(2),
|
|
706
|
+
x: z6.number().int().nonnegative().multipleOf(2),
|
|
707
|
+
y: z6.number().int().nonnegative().multipleOf(2),
|
|
672
708
|
/**
|
|
673
709
|
* The fill for everything the box does not cover. `#RRGGBB` for the reason
|
|
674
710
|
* `OutroSchema`'s colours are: the value reaches an FFmpeg filtergraph, and that
|
|
@@ -691,8 +727,8 @@ var RenderPlanSchema = z5.object({
|
|
|
691
727
|
* With no intro it is exactly what it always was, which is what keeps a plan without
|
|
692
728
|
* one byte-identical to what earlier versions wrote.
|
|
693
729
|
*/
|
|
694
|
-
durationMs:
|
|
695
|
-
tailPadMs:
|
|
730
|
+
durationMs: z6.number().int().positive(),
|
|
731
|
+
tailPadMs: z6.number().int().nonnegative()
|
|
696
732
|
}).refine(
|
|
697
733
|
({ width, height, letterbox }) => letterbox === void 0 || letterbox.x + letterbox.width <= width && letterbox.y + letterbox.height <= height,
|
|
698
734
|
{
|
|
@@ -706,12 +742,12 @@ var RenderPlanSchema = z5.object({
|
|
|
706
742
|
path: ["letterbox"]
|
|
707
743
|
}
|
|
708
744
|
),
|
|
709
|
-
captions:
|
|
710
|
-
language:
|
|
711
|
-
srtPath:
|
|
712
|
-
vttPath:
|
|
713
|
-
assPath:
|
|
714
|
-
cues:
|
|
745
|
+
captions: z6.object({
|
|
746
|
+
language: z6.string().min(2),
|
|
747
|
+
srtPath: z6.literal("captions/captions.srt"),
|
|
748
|
+
vttPath: z6.literal("captions/captions.vtt"),
|
|
749
|
+
assPath: z6.literal("captions/captions.ass"),
|
|
750
|
+
cues: z6.array(CueSchema)
|
|
715
751
|
}),
|
|
716
752
|
/**
|
|
717
753
|
* How the burned-in captions were drawn. This is the **source** of
|
|
@@ -725,18 +761,18 @@ var RenderPlanSchema = z5.object({
|
|
|
725
761
|
* older reader ignoring fields it does not know still executes the same frozen arguments
|
|
726
762
|
* over the same frozen ASS. That observation generalises to the schema as a whole.
|
|
727
763
|
*/
|
|
728
|
-
style:
|
|
729
|
-
fontFile:
|
|
730
|
-
fontName:
|
|
731
|
-
fontSize:
|
|
732
|
-
textColor:
|
|
733
|
-
outlineColor:
|
|
764
|
+
style: z6.object({
|
|
765
|
+
fontFile: z6.literal("assets/fonts/NotoSans-Regular.ttf"),
|
|
766
|
+
fontName: z6.literal("Noto Sans"),
|
|
767
|
+
fontSize: z6.number().int().positive(),
|
|
768
|
+
textColor: z6.string(),
|
|
769
|
+
outlineColor: z6.string(),
|
|
734
770
|
/** ASS `Outline`: a stroke width under `BorderStyle 1`, box padding under `BorderStyle 4`. */
|
|
735
|
-
outlineWidth:
|
|
736
|
-
marginBottom:
|
|
737
|
-
marginSide:
|
|
738
|
-
borderStyle:
|
|
739
|
-
outlineOpacity:
|
|
771
|
+
outlineWidth: z6.number().int().nonnegative(),
|
|
772
|
+
marginBottom: z6.number().int().nonnegative(),
|
|
773
|
+
marginSide: z6.number().int().nonnegative().default(80),
|
|
774
|
+
borderStyle: z6.union([z6.literal(1), z6.literal(4)]).default(1),
|
|
775
|
+
outlineOpacity: z6.number().min(0).max(1).default(1),
|
|
740
776
|
boxColor: HEX_COLOUR.default("#000000"),
|
|
741
777
|
/*
|
|
742
778
|
* Opaque, which under the `BorderStyle 1` default draws nothing at all: `BackColour` is
|
|
@@ -744,7 +780,7 @@ var RenderPlanSchema = z5.object({
|
|
|
744
780
|
* and ASS's own default, so a plan frozen before this field existed regenerates its ASS
|
|
745
781
|
* byte-for-byte rather than byte-for-byte-except-one-field-nobody-can-see.
|
|
746
782
|
*/
|
|
747
|
-
boxOpacity:
|
|
783
|
+
boxOpacity: z6.number().min(0).max(1).default(1),
|
|
748
784
|
/**
|
|
749
785
|
* ASS `\an` alignment: which edge of the frame the caption block is anchored to, and
|
|
750
786
|
* therefore which margin `marginBottom` is measured from.
|
|
@@ -763,12 +799,12 @@ var RenderPlanSchema = z5.object({
|
|
|
763
799
|
* remaining code, middle-centre (5), would put the words back over the picture — which
|
|
764
800
|
* is the thing the letterbox exists to stop.
|
|
765
801
|
*/
|
|
766
|
-
alignment:
|
|
802
|
+
alignment: z6.union([z6.literal(2), z6.literal(8)]).default(2)
|
|
767
803
|
}),
|
|
768
|
-
ffmpeg:
|
|
769
|
-
base:
|
|
770
|
-
soft:
|
|
771
|
-
hard:
|
|
804
|
+
ffmpeg: z6.object({
|
|
805
|
+
base: z6.array(z6.string()),
|
|
806
|
+
soft: z6.array(z6.string()),
|
|
807
|
+
hard: z6.array(z6.string())
|
|
772
808
|
}),
|
|
773
809
|
intro: IntroSchema.optional(),
|
|
774
810
|
outro: OutroSchema.optional(),
|
|
@@ -825,11 +861,11 @@ var RenderPlanSchema = z5.object({
|
|
|
825
861
|
* it out): a lone, unsegmented capture has no gap to claim in the first place, and needs
|
|
826
862
|
* none of these five fields at all.
|
|
827
863
|
*/
|
|
828
|
-
actors:
|
|
829
|
-
segments:
|
|
830
|
-
transitionCards:
|
|
831
|
-
badgesAssPath:
|
|
832
|
-
explainSegments:
|
|
864
|
+
actors: z6.array(ActorSchema).min(2).optional(),
|
|
865
|
+
segments: z6.array(SegmentSchema).min(2).optional(),
|
|
866
|
+
transitionCards: z6.array(TransitionCardSchema).min(1).optional(),
|
|
867
|
+
badgesAssPath: z6.literal("captions/badges.ass").optional(),
|
|
868
|
+
explainSegments: z6.array(ExplainSegmentPlanSchema).min(1).optional()
|
|
833
869
|
}).refine(
|
|
834
870
|
(plan) => {
|
|
835
871
|
const castFields = [plan.actors, plan.transitionCards, plan.badgesAssPath];
|
|
@@ -900,95 +936,95 @@ var RenderPlanSchema = z5.object({
|
|
|
900
936
|
);
|
|
901
937
|
|
|
902
938
|
// ../schema/src/results.ts
|
|
903
|
-
import { z as
|
|
904
|
-
var RELATIVE_POSIX =
|
|
905
|
-
var Sha2562 =
|
|
906
|
-
var DISPLAY_PATH =
|
|
939
|
+
import { z as z7 } from "zod";
|
|
940
|
+
var RELATIVE_POSIX = z7.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");
|
|
941
|
+
var Sha2562 = z7.string().regex(/^[0-9a-f]{64}$/);
|
|
942
|
+
var DISPLAY_PATH = z7.string().min(1);
|
|
907
943
|
var envelope = (kind) => ({
|
|
908
|
-
schema:
|
|
909
|
-
kind:
|
|
910
|
-
ok:
|
|
944
|
+
schema: z7.literal("agent-demo.result/v1"),
|
|
945
|
+
kind: z7.literal(kind),
|
|
946
|
+
ok: z7.boolean(),
|
|
911
947
|
/** Human-readable failures. Empty iff ok. */
|
|
912
|
-
problems:
|
|
948
|
+
problems: z7.array(z7.string())
|
|
913
949
|
});
|
|
914
|
-
var ArtifactRefSchema =
|
|
950
|
+
var ArtifactRefSchema = z7.object({
|
|
915
951
|
path: RELATIVE_POSIX,
|
|
916
952
|
sha256: Sha2562,
|
|
917
|
-
bytes:
|
|
953
|
+
bytes: z7.number().int().nonnegative()
|
|
918
954
|
});
|
|
919
|
-
var StreamSummarySchema =
|
|
920
|
-
container:
|
|
921
|
-
durationMs:
|
|
922
|
-
video:
|
|
923
|
-
codec:
|
|
924
|
-
width:
|
|
925
|
-
height:
|
|
926
|
-
pixelFormat:
|
|
927
|
-
fps:
|
|
928
|
-
frames:
|
|
955
|
+
var StreamSummarySchema = z7.object({
|
|
956
|
+
container: z7.enum(["mp4", "webm", "other"]),
|
|
957
|
+
durationMs: z7.number().int().nonnegative(),
|
|
958
|
+
video: z7.object({
|
|
959
|
+
codec: z7.string(),
|
|
960
|
+
width: z7.number().int(),
|
|
961
|
+
height: z7.number().int(),
|
|
962
|
+
pixelFormat: z7.string(),
|
|
963
|
+
fps: z7.number().int(),
|
|
964
|
+
frames: z7.number().int(),
|
|
929
965
|
/**
|
|
930
966
|
* The video stream's own duration, as distinct from the container's above. The
|
|
931
967
|
* constant-rate check measures frames against this, because a padded narration track makes
|
|
932
968
|
* the container's duration the audio's. Zero for a WebM, which carries no per-track
|
|
933
969
|
* duration at all.
|
|
934
970
|
*/
|
|
935
|
-
durationMs:
|
|
971
|
+
durationMs: z7.number().int().nonnegative()
|
|
936
972
|
}),
|
|
937
|
-
subtitles:
|
|
973
|
+
subtitles: z7.array(z7.object({ codec: z7.string(), language: z7.string(), title: z7.string() })),
|
|
938
974
|
/** Properties, not a count — a narration track muxed in stereo or at the wrong rate
|
|
939
975
|
* still counts as one stream. */
|
|
940
|
-
audio:
|
|
941
|
-
|
|
942
|
-
codec:
|
|
943
|
-
sampleRate:
|
|
944
|
-
channels:
|
|
976
|
+
audio: z7.array(
|
|
977
|
+
z7.object({
|
|
978
|
+
codec: z7.string(),
|
|
979
|
+
sampleRate: z7.number().int().nonnegative(),
|
|
980
|
+
channels: z7.number().int().nonnegative()
|
|
945
981
|
})
|
|
946
982
|
),
|
|
947
983
|
/** Titled chapters make FFmpeg's MP4 muxer add a `bin_data` track beside the `chpl` atom. */
|
|
948
|
-
dataStreams:
|
|
949
|
-
chapters:
|
|
950
|
-
|
|
951
|
-
startMs:
|
|
952
|
-
endMs:
|
|
953
|
-
title:
|
|
984
|
+
dataStreams: z7.number().int().nonnegative(),
|
|
985
|
+
chapters: z7.array(
|
|
986
|
+
z7.object({
|
|
987
|
+
startMs: z7.number().int().nonnegative(),
|
|
988
|
+
endMs: z7.number().int().nonnegative(),
|
|
989
|
+
title: z7.string()
|
|
954
990
|
})
|
|
955
991
|
)
|
|
956
992
|
});
|
|
957
|
-
var ToolchainSchema =
|
|
958
|
-
node:
|
|
959
|
-
playwright:
|
|
960
|
-
chromiumRevision:
|
|
961
|
-
ffmpeg:
|
|
993
|
+
var ToolchainSchema = z7.object({
|
|
994
|
+
node: z7.string(),
|
|
995
|
+
playwright: z7.string(),
|
|
996
|
+
chromiumRevision: z7.string(),
|
|
997
|
+
ffmpeg: z7.string(),
|
|
962
998
|
/** May be the literal "unknown" — never fabricated. */
|
|
963
|
-
libass:
|
|
999
|
+
libass: z7.string(),
|
|
964
1000
|
fontSha256: Sha2562
|
|
965
1001
|
});
|
|
966
|
-
var AssertionSchema =
|
|
967
|
-
var StatusSchema =
|
|
968
|
-
var VariantsSchema =
|
|
969
|
-
var ValidationResultSchema =
|
|
1002
|
+
var AssertionSchema = z7.object({ id: z7.string().min(1), status: z7.enum(["passed", "failed"]) });
|
|
1003
|
+
var StatusSchema = z7.enum(["passed", "failed"]);
|
|
1004
|
+
var VariantsSchema = z7.enum(["soft", "hard"]);
|
|
1005
|
+
var ValidationResultSchema = z7.object({
|
|
970
1006
|
...envelope("validate"),
|
|
971
1007
|
/** Absent when the scenario could not be loaded at all. */
|
|
972
|
-
scenario:
|
|
973
|
-
id:
|
|
974
|
-
title:
|
|
975
|
-
language:
|
|
1008
|
+
scenario: z7.object({
|
|
1009
|
+
id: z7.string(),
|
|
1010
|
+
title: z7.string(),
|
|
1011
|
+
language: z7.string(),
|
|
976
1012
|
/**
|
|
977
1013
|
* Whether this scenario needs a person, and in which phase. Reported so an author
|
|
978
1014
|
* — or an agent reading `--json` — learns that a scenario cannot run unattended
|
|
979
1015
|
* without first trying to run it. Defaulted, so a result written before this parses.
|
|
980
1016
|
*/
|
|
981
|
-
handoff:
|
|
1017
|
+
handoff: z7.enum(["none", "preflight", "session"]).default("none"),
|
|
982
1018
|
sha256: Sha2562
|
|
983
1019
|
}).optional()
|
|
984
1020
|
});
|
|
985
|
-
var RunCommandResultSchema =
|
|
1021
|
+
var RunCommandResultSchema = z7.object({
|
|
986
1022
|
...envelope("run"),
|
|
987
1023
|
bundleDir: DISPLAY_PATH,
|
|
988
1024
|
status: StatusSchema,
|
|
989
|
-
cueCount:
|
|
1025
|
+
cueCount: z7.number().int().nonnegative(),
|
|
990
1026
|
/** Zero on the Free Tier, and zero for a scenario with no `chapter` calls. */
|
|
991
|
-
chapterCount:
|
|
1027
|
+
chapterCount: z7.number().int().nonnegative(),
|
|
992
1028
|
/**
|
|
993
1029
|
* How many lines the video speaks. Zero for a silent run, which is the default.
|
|
994
1030
|
*
|
|
@@ -997,93 +1033,100 @@ var RunCommandResultSchema = z6.object({
|
|
|
997
1033
|
* scenario whose steps carry no `subtitle`, which produces a perfectly valid silent video and
|
|
998
1034
|
* would look like the feature being broken rather than like nothing having been written to say.
|
|
999
1035
|
*/
|
|
1000
|
-
narratedCount:
|
|
1001
|
-
diagnostics:
|
|
1002
|
-
assertions:
|
|
1003
|
-
outputs:
|
|
1036
|
+
narratedCount: z7.number().int().nonnegative(),
|
|
1037
|
+
diagnostics: z7.array(z7.object({ code: z7.string(), cueId: z7.string(), detail: z7.string() })),
|
|
1038
|
+
assertions: z7.array(AssertionSchema),
|
|
1039
|
+
outputs: z7.array(ArtifactRefSchema)
|
|
1004
1040
|
});
|
|
1005
|
-
var CheckAssertionSchema = AssertionSchema.extend({ message:
|
|
1006
|
-
var CheckCommandResultSchema =
|
|
1041
|
+
var CheckAssertionSchema = AssertionSchema.extend({ message: z7.string().optional() });
|
|
1042
|
+
var CheckCommandResultSchema = z7.object({
|
|
1007
1043
|
...envelope("check"),
|
|
1008
1044
|
bundleDir: DISPLAY_PATH,
|
|
1009
1045
|
status: StatusSchema,
|
|
1010
|
-
assertions:
|
|
1046
|
+
assertions: z7.array(CheckAssertionSchema),
|
|
1011
1047
|
/**
|
|
1012
1048
|
* Handoff notes only. Cue, chapter, cursor and camera diagnostics are all derived from a
|
|
1013
1049
|
* render plan that `check` never builds — fabricating them would report on capabilities
|
|
1014
1050
|
* this command cannot see.
|
|
1015
1051
|
*/
|
|
1016
|
-
diagnostics:
|
|
1052
|
+
diagnostics: z7.array(z7.object({ code: z7.string(), cueId: z7.string(), detail: z7.string() }))
|
|
1017
1053
|
});
|
|
1018
|
-
var RenderCommandResultSchema =
|
|
1054
|
+
var RenderCommandResultSchema = z7.object({
|
|
1019
1055
|
...envelope("render"),
|
|
1020
1056
|
bundleDir: DISPLAY_PATH,
|
|
1021
1057
|
variants: VariantsSchema,
|
|
1022
|
-
streams:
|
|
1058
|
+
streams: z7.object({
|
|
1023
1059
|
base: StreamSummarySchema.optional(),
|
|
1024
1060
|
soft: StreamSummarySchema.optional(),
|
|
1025
1061
|
hard: StreamSummarySchema.optional()
|
|
1026
1062
|
}),
|
|
1027
|
-
outputs:
|
|
1063
|
+
outputs: z7.array(ArtifactRefSchema)
|
|
1028
1064
|
});
|
|
1029
|
-
var VerificationReportSchema =
|
|
1065
|
+
var VerificationReportSchema = z7.object({
|
|
1030
1066
|
...envelope("verify"),
|
|
1031
1067
|
bundleDir: DISPLAY_PATH,
|
|
1032
|
-
artifactCount:
|
|
1068
|
+
artifactCount: z7.number().int().nonnegative()
|
|
1033
1069
|
});
|
|
1034
|
-
var
|
|
1035
|
-
|
|
1070
|
+
var WarmCommandResultSchema = z7.object({
|
|
1071
|
+
...envelope("warm"),
|
|
1072
|
+
bundleDir: DISPLAY_PATH,
|
|
1073
|
+
clipCount: z7.number().int().nonnegative(),
|
|
1074
|
+
synthesizedCount: z7.number().int().nonnegative(),
|
|
1075
|
+
cachedCount: z7.number().int().nonnegative()
|
|
1076
|
+
});
|
|
1077
|
+
var DifferenceCategorySchema = z7.enum(["step", "assertion", "target", "timing", "caption", "actor"]);
|
|
1078
|
+
var DiffCommandResultSchema = z7.object({
|
|
1036
1079
|
...envelope("diff"),
|
|
1037
1080
|
bundleA: DISPLAY_PATH,
|
|
1038
1081
|
bundleB: DISPLAY_PATH,
|
|
1039
|
-
identical:
|
|
1040
|
-
differences:
|
|
1082
|
+
identical: z7.boolean(),
|
|
1083
|
+
differences: z7.array(z7.object({ category: DifferenceCategorySchema, detail: z7.string() }))
|
|
1041
1084
|
});
|
|
1042
|
-
var PruneCandidateSchema =
|
|
1085
|
+
var PruneCandidateSchema = z7.object({
|
|
1043
1086
|
path: DISPLAY_PATH,
|
|
1044
|
-
bytes:
|
|
1045
|
-
scenarioId:
|
|
1087
|
+
bytes: z7.number().int().nonnegative(),
|
|
1088
|
+
scenarioId: z7.string().optional()
|
|
1046
1089
|
});
|
|
1047
|
-
var PruneCommandResultSchema =
|
|
1090
|
+
var PruneCommandResultSchema = z7.object({
|
|
1048
1091
|
...envelope("prune"),
|
|
1049
|
-
dryRun:
|
|
1092
|
+
dryRun: z7.boolean(),
|
|
1050
1093
|
/** What was removed (confirmed) or would be (dry run) — never includes a `failed` entry. */
|
|
1051
|
-
candidates:
|
|
1094
|
+
candidates: z7.array(PruneCandidateSchema),
|
|
1052
1095
|
/** A discovered bundle that was never attempted, and why — never silently dropped. */
|
|
1053
|
-
skipped:
|
|
1096
|
+
skipped: z7.array(z7.object({ path: DISPLAY_PATH, reason: z7.string() })),
|
|
1054
1097
|
/** A selected candidate `rm` was attempted on and could not remove, and why. Always empty
|
|
1055
1098
|
* on a dry run, since nothing is attempted until `--yes`. */
|
|
1056
|
-
failed:
|
|
1099
|
+
failed: z7.array(z7.object({ path: DISPLAY_PATH, reason: z7.string() })),
|
|
1057
1100
|
/** What `candidates` account for: bytes actually reclaimed if `dryRun` is false, or would
|
|
1058
1101
|
* be reclaimed if every candidate succeeded. */
|
|
1059
|
-
bytesReclaimed:
|
|
1102
|
+
bytesReclaimed: z7.number().int().nonnegative()
|
|
1060
1103
|
});
|
|
1061
|
-
var ImportCommandResultSchema =
|
|
1104
|
+
var ImportCommandResultSchema = z7.object({
|
|
1062
1105
|
...envelope("import"),
|
|
1063
1106
|
tracePath: DISPLAY_PATH,
|
|
1064
1107
|
outputPath: DISPLAY_PATH,
|
|
1065
1108
|
/** How many `demo.step` calls the draft carries. Zero only on the failure path. */
|
|
1066
|
-
stepCount:
|
|
1109
|
+
stepCount: z7.number().int().nonnegative(),
|
|
1067
1110
|
/** Whether the written draft already passes `plaintake validate`. Best-effort: a draft
|
|
1068
1111
|
* that fails is still written, still a success, and its problems are warnings here. */
|
|
1069
|
-
validates:
|
|
1112
|
+
validates: z7.boolean(),
|
|
1070
1113
|
/** Author-facing advice: multi-page traces, redactions, unimported trace calls, and the
|
|
1071
1114
|
* standing review-for-secrets line. Never empty on success. */
|
|
1072
|
-
warnings:
|
|
1115
|
+
warnings: z7.array(z7.string())
|
|
1073
1116
|
});
|
|
1074
|
-
var InspectResultSchema =
|
|
1117
|
+
var InspectResultSchema = z7.object({
|
|
1075
1118
|
...envelope("inspect"),
|
|
1076
1119
|
bundleDir: DISPLAY_PATH,
|
|
1077
1120
|
status: StatusSchema,
|
|
1078
|
-
scenario:
|
|
1121
|
+
scenario: z7.object({ id: z7.string(), sourceSha256: Sha2562 }),
|
|
1079
1122
|
toolchain: ToolchainSchema,
|
|
1080
|
-
video:
|
|
1123
|
+
video: z7.object({
|
|
1081
1124
|
/** The whole output: the cards, if any, plus the content between them. */
|
|
1082
|
-
durationMs:
|
|
1083
|
-
frames:
|
|
1084
|
-
width:
|
|
1085
|
-
height:
|
|
1086
|
-
fps:
|
|
1125
|
+
durationMs: z7.number().int(),
|
|
1126
|
+
frames: z7.number().int(),
|
|
1127
|
+
width: z7.number().int(),
|
|
1128
|
+
height: z7.number().int(),
|
|
1129
|
+
fps: z7.number().int(),
|
|
1087
1130
|
/**
|
|
1088
1131
|
* Split out because `durationMs` alone is misleading once a card exists: 12,034ms of
|
|
1089
1132
|
* output can be 9,034ms of recording. The parts are reported separately rather than
|
|
@@ -1098,20 +1141,20 @@ var InspectResultSchema = z6.object({
|
|
|
1098
1141
|
* the word means, and is worth stating because the plan's own `video.durationMs` is a
|
|
1099
1142
|
* different number that includes the opening card.
|
|
1100
1143
|
*/
|
|
1101
|
-
contentDurationMs:
|
|
1102
|
-
introDurationMs:
|
|
1103
|
-
outroDurationMs:
|
|
1144
|
+
contentDurationMs: z7.number().int().nonnegative(),
|
|
1145
|
+
introDurationMs: z7.number().int().nonnegative().default(0),
|
|
1146
|
+
outroDurationMs: z7.number().int().nonnegative()
|
|
1104
1147
|
}),
|
|
1105
|
-
captions:
|
|
1106
|
-
language:
|
|
1107
|
-
cueCount:
|
|
1108
|
-
characterCount:
|
|
1109
|
-
medianCueMs:
|
|
1110
|
-
shortestCueMs:
|
|
1111
|
-
longestCueMs:
|
|
1148
|
+
captions: z7.object({
|
|
1149
|
+
language: z7.string(),
|
|
1150
|
+
cueCount: z7.number().int().nonnegative(),
|
|
1151
|
+
characterCount: z7.number().int().nonnegative(),
|
|
1152
|
+
medianCueMs: z7.number().int().nonnegative(),
|
|
1153
|
+
shortestCueMs: z7.number().int().nonnegative(),
|
|
1154
|
+
longestCueMs: z7.number().int().nonnegative()
|
|
1112
1155
|
}),
|
|
1113
1156
|
/** Measured from the rendered MP4 where possible, declared from the plan otherwise. */
|
|
1114
|
-
chapters:
|
|
1157
|
+
chapters: z7.array(z7.object({ startMs: z7.number().int().nonnegative(), title: z7.string() })),
|
|
1115
1158
|
/**
|
|
1116
1159
|
* The narration this bundle carries, or absent for a silent one.
|
|
1117
1160
|
*
|
|
@@ -1122,13 +1165,13 @@ var InspectResultSchema = z6.object({
|
|
|
1122
1165
|
* model behind it, and inventing a voice id for it would make the one field that answers "why
|
|
1123
1166
|
* does this sound different" answer wrongly.
|
|
1124
1167
|
*/
|
|
1125
|
-
narration:
|
|
1126
|
-
clipCount:
|
|
1168
|
+
narration: z7.object({
|
|
1169
|
+
clipCount: z7.number().int().positive(),
|
|
1127
1170
|
/** Total speech, excluding the silence between clips. Not the video's length. */
|
|
1128
|
-
spokenMs:
|
|
1171
|
+
spokenMs: z7.number().int().nonnegative(),
|
|
1129
1172
|
/** How many clips the author supplied rather than the model producing. */
|
|
1130
|
-
suppliedCount:
|
|
1131
|
-
voice:
|
|
1173
|
+
suppliedCount: z7.number().int().nonnegative(),
|
|
1174
|
+
voice: z7.string().optional()
|
|
1132
1175
|
}).optional(),
|
|
1133
1176
|
/**
|
|
1134
1177
|
* The multi-actor demo's cast, or absent for a single-actor bundle — every bundle
|
|
@@ -1145,19 +1188,19 @@ var InspectResultSchema = z6.object({
|
|
|
1145
1188
|
* the field a reader of this JSON would already trust as the timeline, not from a second
|
|
1146
1189
|
* field that only agrees with it by a schema-level invariant this result does not surface.
|
|
1147
1190
|
*/
|
|
1148
|
-
actors:
|
|
1191
|
+
actors: z7.object({
|
|
1149
1192
|
/** `plan.actors.length`. Reported alongside `labels` rather than left for a caller to
|
|
1150
1193
|
* compute, matching `narration.clipCount` sitting beside `narration` above for the
|
|
1151
1194
|
* same reason: the count is the fact most callers want first, before the list. */
|
|
1152
|
-
count:
|
|
1195
|
+
count: z7.number().int().min(2),
|
|
1153
1196
|
/** In `plan.actors` order — the cast list a badge or transition card would draw, not
|
|
1154
1197
|
* deduplicated or sorted. */
|
|
1155
|
-
labels:
|
|
1198
|
+
labels: z7.array(z7.string().min(1)).min(2),
|
|
1156
1199
|
/** Hand-offs, not participants: one fewer than `plan.segments.length`, since a
|
|
1157
1200
|
* segment is a window one actor already holds and a turn is the cut between two of
|
|
1158
1201
|
* them — the same count `plan.transitionCards.length` carries, arrived at
|
|
1159
1202
|
* independently from `segments` for the reason above. */
|
|
1160
|
-
turnCount:
|
|
1203
|
+
turnCount: z7.number().int().nonnegative()
|
|
1161
1204
|
}).refine((value) => value.count === value.labels.length, "actors.count must equal actors.labels.length").optional(),
|
|
1162
1205
|
/**
|
|
1163
1206
|
* The explain scenes this bundle cut away to, in composite order, or absent for a bundle
|
|
@@ -1169,20 +1212,20 @@ var InspectResultSchema = z6.object({
|
|
|
1169
1212
|
* `actors` is sourced from the plan rather than re-read from `demo.explain()` calls: a
|
|
1170
1213
|
* bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
1171
1214
|
*/
|
|
1172
|
-
explainScenes:
|
|
1173
|
-
|
|
1174
|
-
id:
|
|
1175
|
-
durationMs:
|
|
1215
|
+
explainScenes: z7.array(
|
|
1216
|
+
z7.object({
|
|
1217
|
+
id: z7.string().min(1),
|
|
1218
|
+
durationMs: z7.number().int().positive()
|
|
1176
1219
|
})
|
|
1177
1220
|
).min(1).optional(),
|
|
1178
1221
|
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
1179
|
-
outputs:
|
|
1180
|
-
assertions:
|
|
1181
|
-
artifactCount:
|
|
1222
|
+
outputs: z7.array(ArtifactRefSchema),
|
|
1223
|
+
assertions: z7.array(AssertionSchema),
|
|
1224
|
+
artifactCount: z7.number().int().nonnegative(),
|
|
1182
1225
|
/** Everything the manifest accounts for. What "delete this bundle" would reclaim. */
|
|
1183
|
-
bundleBytes:
|
|
1226
|
+
bundleBytes: z7.number().int().nonnegative()
|
|
1184
1227
|
});
|
|
1185
|
-
var DoctorResultSchema =
|
|
1228
|
+
var DoctorResultSchema = z7.object({
|
|
1186
1229
|
...envelope("doctor"),
|
|
1187
1230
|
/**
|
|
1188
1231
|
* The PlainTake build this is. Here rather than in `ToolchainSchema` on purpose:
|
|
@@ -1190,12 +1233,12 @@ var DoctorResultSchema = z6.object({
|
|
|
1190
1233
|
* manifest on every release and break the byte-for-byte golden bundle. `doctor` is the
|
|
1191
1234
|
* one command a bug report is asked for, which makes it the right place.
|
|
1192
1235
|
*/
|
|
1193
|
-
version:
|
|
1194
|
-
ffmpeg:
|
|
1195
|
-
ffprobe:
|
|
1196
|
-
libass:
|
|
1197
|
-
hasLibass:
|
|
1198
|
-
hasX264:
|
|
1236
|
+
version: z7.string(),
|
|
1237
|
+
ffmpeg: z7.string(),
|
|
1238
|
+
ffprobe: z7.string(),
|
|
1239
|
+
libass: z7.string(),
|
|
1240
|
+
hasLibass: z7.boolean(),
|
|
1241
|
+
hasX264: z7.boolean(),
|
|
1199
1242
|
/**
|
|
1200
1243
|
* Whether this FFmpeg can encode AAC, which is what a narrated bundle is muxed with.
|
|
1201
1244
|
*
|
|
@@ -1205,8 +1248,8 @@ var DoctorResultSchema = z6.object({
|
|
|
1205
1248
|
* that actually needs it, with a fix string. Failing `doctor` here would be telling someone
|
|
1206
1249
|
* their installation is broken for a capability they may never turn on.
|
|
1207
1250
|
*/
|
|
1208
|
-
hasAac:
|
|
1209
|
-
filters:
|
|
1251
|
+
hasAac: z7.boolean(),
|
|
1252
|
+
filters: z7.array(z7.string()),
|
|
1210
1253
|
/**
|
|
1211
1254
|
* Whether spoken narration could be produced right now, and what is missing if not.
|
|
1212
1255
|
*
|
|
@@ -1215,12 +1258,12 @@ var DoctorResultSchema = z6.object({
|
|
|
1215
1258
|
* `openNarrator` asks before a run, answered by the same function (`speechAssetProblems`), so
|
|
1216
1259
|
* `doctor` cannot say speech is fine and then a run refuse it.
|
|
1217
1260
|
*/
|
|
1218
|
-
speech:
|
|
1219
|
-
ready:
|
|
1261
|
+
speech: z7.object({
|
|
1262
|
+
ready: z7.boolean(),
|
|
1220
1263
|
/** Installed voices, in catalogue order. Empty means the model cannot speak yet. */
|
|
1221
|
-
voices:
|
|
1264
|
+
voices: z7.array(z7.string()),
|
|
1222
1265
|
/** What is missing, in the same words a refused run would use. Empty when ready. */
|
|
1223
|
-
notes:
|
|
1266
|
+
notes: z7.array(z7.string())
|
|
1224
1267
|
}),
|
|
1225
1268
|
/**
|
|
1226
1269
|
* Whether the plainmotion CLI is on `PATH`, for a scenario that wants to call
|
|
@@ -1231,16 +1274,16 @@ var DoctorResultSchema = z6.object({
|
|
|
1231
1274
|
* probe a run's own first `demo.explain()` pays, so `doctor` cannot say plainmotion is fine
|
|
1232
1275
|
* and then a run refuse it.
|
|
1233
1276
|
*/
|
|
1234
|
-
plainmotion:
|
|
1235
|
-
installed:
|
|
1277
|
+
plainmotion: z7.object({
|
|
1278
|
+
installed: z7.boolean(),
|
|
1236
1279
|
/** plainmotion's own `--version` output. Absent when not installed. */
|
|
1237
|
-
version:
|
|
1280
|
+
version: z7.string().optional(),
|
|
1238
1281
|
/** Why the probe failed, in the same words a run's own refusal would use. Absent when installed. */
|
|
1239
|
-
note:
|
|
1282
|
+
note: z7.string().optional()
|
|
1240
1283
|
})
|
|
1241
1284
|
});
|
|
1242
1285
|
var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
|
|
1243
|
-
var ActivateReasonSchema =
|
|
1286
|
+
var ActivateReasonSchema = z7.enum([
|
|
1244
1287
|
"empty-key",
|
|
1245
1288
|
"not-found",
|
|
1246
1289
|
"revoked",
|
|
@@ -1248,16 +1291,16 @@ var ActivateReasonSchema = z6.enum([
|
|
|
1248
1291
|
"unexpected-response",
|
|
1249
1292
|
"not-configured"
|
|
1250
1293
|
]);
|
|
1251
|
-
var ActivateResultSchema =
|
|
1294
|
+
var ActivateResultSchema = z7.object({
|
|
1252
1295
|
...envelope("activate"),
|
|
1253
1296
|
/** Present only when ok is false, so a machine reader can distinguish the classes. */
|
|
1254
1297
|
reason: ActivateReasonSchema.optional(),
|
|
1255
1298
|
/** Present only on success — a failed activation has no record. The key itself is
|
|
1256
1299
|
* deliberately absent: the caller already has it, and stdout may be logged. */
|
|
1257
|
-
licence:
|
|
1258
|
-
subject:
|
|
1259
|
-
activatedAt:
|
|
1260
|
-
uses:
|
|
1300
|
+
licence: z7.object({
|
|
1301
|
+
subject: z7.string().min(1),
|
|
1302
|
+
activatedAt: z7.string().min(1),
|
|
1303
|
+
uses: z7.number().int().nonnegative()
|
|
1261
1304
|
}).optional()
|
|
1262
1305
|
}).refine(okMatchesProblems, "ok must be true iff problems is empty").refine(
|
|
1263
1306
|
// Making the "present only when" comments above literally true, so a reader can
|
|
@@ -1265,22 +1308,22 @@ var ActivateResultSchema = z6.object({
|
|
|
1265
1308
|
(value) => value.reason !== void 0 === !value.ok && (value.licence === void 0 || value.ok),
|
|
1266
1309
|
"reason appears iff ok is false, and licence only on success"
|
|
1267
1310
|
);
|
|
1268
|
-
var LicenceResultSchema =
|
|
1269
|
-
|
|
1311
|
+
var LicenceResultSchema = z7.discriminatedUnion("tier", [
|
|
1312
|
+
z7.object({
|
|
1270
1313
|
...envelope("licence"),
|
|
1271
|
-
tier:
|
|
1314
|
+
tier: z7.literal("free"),
|
|
1272
1315
|
/**
|
|
1273
1316
|
* Why a present-but-unusable record degraded to free, in readLicense's own words.
|
|
1274
1317
|
* Must never quote a filesystem path: assertNoAbsolutePaths exempts only `problems`.
|
|
1275
1318
|
*/
|
|
1276
|
-
problem:
|
|
1319
|
+
problem: z7.string().optional()
|
|
1277
1320
|
}),
|
|
1278
|
-
|
|
1321
|
+
z7.object({
|
|
1279
1322
|
...envelope("licence"),
|
|
1280
|
-
tier:
|
|
1281
|
-
subject:
|
|
1282
|
-
activatedAt:
|
|
1283
|
-
uses:
|
|
1323
|
+
tier: z7.literal("pro"),
|
|
1324
|
+
subject: z7.string().min(1),
|
|
1325
|
+
activatedAt: z7.string().min(1),
|
|
1326
|
+
uses: z7.number().int().nonnegative()
|
|
1284
1327
|
})
|
|
1285
1328
|
]).refine(okMatchesProblems, "ok must be true iff problems is empty");
|
|
1286
1329
|
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The warm index: the ordered set of synthesis requests a narrated recording made, written into
|
|
4
|
+
* the bundle so the clips can be re-synthesised **before** a later re-recording rather than during
|
|
5
|
+
* it.
|
|
6
|
+
*
|
|
7
|
+
* It exists because synthesis sits on the capture's critical path — a clip is produced while the
|
|
8
|
+
* screencast rolls, and a cache-cold run (a fresh machine, CI, or the one after a
|
|
9
|
+
* `SPEECH_ENGINE_VERSION` bump) therefore records every synthesis stall into the video as extra
|
|
10
|
+
* silence. `plaintake warm` reads this file and drives the *same* narrator's `clipFor` for each
|
|
11
|
+
* entry with no browser and no target app, filling `~/.cache/plaintake/speech` so the next
|
|
12
|
+
* recording is all cache hits and its length is a function of the demo, not of the machine.
|
|
13
|
+
*
|
|
14
|
+
* **It is inputs, not audio.** Every field here is something the recorder knew *before* it
|
|
15
|
+
* synthesised — the line, the split, the voice, the run's speed and dtype — so warming combines it
|
|
16
|
+
* with whatever engine identity the code carries *now*. That is the whole point: after a version
|
|
17
|
+
* bump the text is unchanged, so warming the old index re-synthesises correctly under the new
|
|
18
|
+
* engine. What is deliberately absent is anything version-stamped (`modelSha256`, the engine
|
|
19
|
+
* version): those come from the current build, never from the file.
|
|
20
|
+
*
|
|
21
|
+
* Only `synth`-sourced clips appear. A `--speech file` clip is the author's own audio with nothing
|
|
22
|
+
* to synthesise, and an `explain` scene is plainmotion's, not this narrator's.
|
|
23
|
+
*/
|
|
24
|
+
export declare const NARRATION_INDEX_SCHEMA = "agent-demo.narration-index/v1";
|
|
25
|
+
/**
|
|
26
|
+
* One synthesis request, carrying exactly the fields `StepNarrator.clipFor` takes — so warming is a
|
|
27
|
+
* replay of the call the recording already made, not a second code path that has to be kept in step
|
|
28
|
+
* with how a line becomes a cache key.
|
|
29
|
+
*/
|
|
30
|
+
export declare const NarrationIndexClipSchema: z.ZodObject<{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
text: z.ZodString;
|
|
33
|
+
breaks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
34
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type NarrationIndexClip = z.infer<typeof NarrationIndexClipSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* The whole index: the run-wide voice controls, then one entry per synthesised clip. `speech` holds
|
|
39
|
+
* everything a narrator needs to reconstruct the same cache keys *except* the engine identity — the
|
|
40
|
+
* default voice, any extra declared voices, the speed and dtype the run resolved, and the
|
|
41
|
+
* scenario's own pronunciation dictionary (merged under the built-in defaults again at warm time,
|
|
42
|
+
* exactly as `openNarrator` merges it at record time).
|
|
43
|
+
*/
|
|
44
|
+
export declare const NarrationIndexSchema: z.ZodObject<{
|
|
45
|
+
schema: z.ZodLiteral<"agent-demo.narration-index/v1">;
|
|
46
|
+
speech: z.ZodObject<{
|
|
47
|
+
voice: z.ZodString;
|
|
48
|
+
voices: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
49
|
+
speed: z.ZodNumber;
|
|
50
|
+
dtype: z.ZodString;
|
|
51
|
+
pronunciations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
clips: z.ZodArray<z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
text: z.ZodString;
|
|
56
|
+
breaks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strip>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export type NarrationIndex = z.infer<typeof NarrationIndexSchema>;
|
package/dist/schema/results.d.ts
CHANGED
|
@@ -201,6 +201,23 @@ export declare const VerificationReportSchema: z.ZodObject<{
|
|
|
201
201
|
ok: z.ZodBoolean;
|
|
202
202
|
problems: z.ZodArray<z.ZodString>;
|
|
203
203
|
}, z.core.$strip>;
|
|
204
|
+
/**
|
|
205
|
+
* `warm <bundleDir>`'s result. Warming synthesises a bundle's narration into the shared clip cache
|
|
206
|
+
* with no browser and no target app, so the next recording is cache hits rather than synthesis
|
|
207
|
+
* stalls padding the video — hence the two counts: how many clips it had to synthesise now, and how
|
|
208
|
+
* many the cache already held. `ok` is a plain success (the cache was filled), unlike `verify` where
|
|
209
|
+
* it is a verdict; a failure — a missing index, an uninstalled model with work to do — throws.
|
|
210
|
+
*/
|
|
211
|
+
export declare const WarmCommandResultSchema: z.ZodObject<{
|
|
212
|
+
bundleDir: z.ZodString;
|
|
213
|
+
clipCount: z.ZodNumber;
|
|
214
|
+
synthesizedCount: z.ZodNumber;
|
|
215
|
+
cachedCount: z.ZodNumber;
|
|
216
|
+
schema: z.ZodLiteral<"agent-demo.result/v1">;
|
|
217
|
+
kind: z.ZodLiteral<"warm">;
|
|
218
|
+
ok: z.ZodBoolean;
|
|
219
|
+
problems: z.ZodArray<z.ZodString>;
|
|
220
|
+
}, z.core.$strip>;
|
|
204
221
|
/**
|
|
205
222
|
* `diff <bundleA> <bundleB>`'s result. Two `DISPLAY_PATH`s rather than `verify`'s and
|
|
206
223
|
* `inspect`'s single `bundleDir`, because this is the one command that compares two bundles
|
|
@@ -456,6 +473,7 @@ export type RunCommandResult = z.infer<typeof RunCommandResultSchema>;
|
|
|
456
473
|
export type CheckCommandResult = z.infer<typeof CheckCommandResultSchema>;
|
|
457
474
|
export type RenderCommandResult = z.infer<typeof RenderCommandResultSchema>;
|
|
458
475
|
export type VerificationReport = z.infer<typeof VerificationReportSchema>;
|
|
476
|
+
export type WarmCommandResult = z.infer<typeof WarmCommandResultSchema>;
|
|
459
477
|
export type DiffCommandResult = z.infer<typeof DiffCommandResultSchema>;
|
|
460
478
|
export type PruneCommandResult = z.infer<typeof PruneCommandResultSchema>;
|
|
461
479
|
export type ImportCommandResult = z.infer<typeof ImportCommandResultSchema>;
|
|
@@ -463,7 +481,7 @@ export type InspectResult = z.infer<typeof InspectResultSchema>;
|
|
|
463
481
|
export type DoctorResult = z.infer<typeof DoctorResultSchema>;
|
|
464
482
|
export type ActivateResult = z.infer<typeof ActivateResultSchema>;
|
|
465
483
|
export type LicenceResult = z.infer<typeof LicenceResultSchema>;
|
|
466
|
-
export type DemoResult = ValidationResult | RunCommandResult | CheckCommandResult | RenderCommandResult | VerificationReport | DiffCommandResult | PruneCommandResult | ImportCommandResult | InspectResult | DoctorResult | ActivateResult | LicenceResult;
|
|
484
|
+
export type DemoResult = ValidationResult | RunCommandResult | CheckCommandResult | RenderCommandResult | VerificationReport | WarmCommandResult | DiffCommandResult | PruneCommandResult | ImportCommandResult | InspectResult | DoctorResult | ActivateResult | LicenceResult;
|
|
467
485
|
/**
|
|
468
486
|
* Renders a filesystem location for display in a result, relative to `root` when it
|
|
469
487
|
* lies inside it.
|
|
@@ -53,8 +53,11 @@ export declare const ScenarioCameraSchema: z.ZodObject<{
|
|
|
53
53
|
export type ScenarioCamera = z.infer<typeof ScenarioCameraSchema>;
|
|
54
54
|
/**
|
|
55
55
|
* Scenario-level narration controls. Two dials, no more: the reading speed, and the set of
|
|
56
|
-
* voices the scenario is allowed to switch between mid-run. No pitch, no SSML — see
|
|
57
|
-
* `docs/release-checklist.md`'s "no speed, no pitch, no SSML" note.
|
|
56
|
+
* voices the scenario is allowed to switch between mid-run. No pitch, no general SSML — see
|
|
57
|
+
* `docs/release-checklist.md`'s "no speed, no pitch, no SSML" note. The one in-line exception is
|
|
58
|
+
* the `[pause]` break marker an author may write inside a `subtitle`/`intro.narration` (parsed
|
|
59
|
+
* in the recorder, not here): a single, bounded place-marker with no length, deliberately not a
|
|
60
|
+
* door to general markup.
|
|
58
61
|
*
|
|
59
62
|
* Nested under `speech` rather than bare top-level fields: it names what is being tuned (the
|
|
60
63
|
* narration) apart from the DSL's other top-level concerns, matching how `camera` groups
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type DemoAction = 'click' | 'type' | 'point' | 'scroll';
|
|
|
11
11
|
export type DemoStep = {
|
|
12
12
|
id: string;
|
|
13
13
|
title: string;
|
|
14
|
+
/**
|
|
15
|
+
* The on-screen caption and, under `--speech on`, the spoken line — one string for both. Write
|
|
16
|
+
* a `[pause]` inside it to force the voice to break at that point on a long line the model would
|
|
17
|
+
* otherwise mistime; the marker is stripped from the caption and marks a place, not a length.
|
|
18
|
+
* See `public/docs/scenarios.md` → Narration breaks.
|
|
19
|
+
*/
|
|
14
20
|
subtitle?: string;
|
|
15
21
|
/** Locator whose bounding box is recorded on step.start. Never used to perform the action. */
|
|
16
22
|
target?: Locator;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plaintake/scenario",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Authoring SDK for PlainTake demo scenarios: defineDemo and the scenario DSL types. Install for editor autocomplete; the PlainTake binary ships a runtime fallback.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|