@plaintake/scenario 1.13.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 CHANGED
@@ -274,37 +274,73 @@ var BundleManifestSchema = z4.object({
274
274
  )
275
275
  });
276
276
 
277
- // ../schema/src/render-plan.ts
277
+ // ../schema/src/narration-index.ts
278
278
  import { z as z5 } from "zod";
279
- var CueSchema = z5.object({
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
- startMs: z5.number().int().nonnegative(),
282
- endMs: z5.number().int().positive(),
283
- lines: z5.array(z5.string()).min(1).max(2)
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 HEX_COLOUR = z5.string().regex(/^#[0-9A-Fa-f]{6}$/, "must be a #RRGGBB colour");
286
- var OutroSchema = z5.object({
287
- durationMs: z5.number().int().positive().max(15e3),
288
- lines: z5.array(z5.string().min(1)).min(1).max(2),
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: z5.literal("captions/outro.ass")
327
+ assPath: z6.literal("captions/outro.ass")
292
328
  });
293
- var IntroSchema = z5.object({
294
- durationMs: z5.number().int().positive().max(15e3),
295
- lines: z5.array(z5.string().min(1)).min(1).max(2),
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: z5.literal("captions/intro.ass")
334
+ assPath: z6.literal("captions/intro.ass")
299
335
  });
300
- var ChapterMarkSchema = z5.object({
301
- startMs: z5.number().int().nonnegative(),
302
- endMs: z5.number().int().positive(),
303
- title: z5.string().min(1)
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 = z5.object({
306
- metadataPath: z5.literal("render/chapters.ffmetadata"),
307
- marks: z5.array(ChapterMarkSchema).min(1)
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 = z5.object({
321
- id: z5.string().min(1),
322
- x: z5.number().int().min(0).max(1920),
323
- y: z5.number().int().min(0).max(1080),
324
- arriveMs: z5.number().int().nonnegative(),
325
- departMs: z5.number().int().nonnegative(),
326
- action: z5.enum(["click", "type", "point"]),
327
- rippleMs: z5.number().int().nonnegative().optional(),
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: z5.number().int().min(1).max(400).optional()
394
+ scale: z6.number().int().min(1).max(400).optional()
359
395
  });
360
- var CursorSchema = z5.object({
361
- assPath: z5.literal("captions/cursor.ass"),
362
- points: z5.array(CursorPointSchema).min(1)
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 = z5.object({
376
- id: z5.string().min(1),
377
- enterMs: z5.number().int().nonnegative(),
378
- holdFromMs: z5.number().int().nonnegative(),
379
- x: z5.number().int().min(0),
380
- y: z5.number().int().min(0),
381
- w: z5.number().int().positive(),
382
- h: z5.number().int().positive()
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 = z5.object({
385
- commandPath: z5.literal("render/camera.cmd"),
386
- shots: z5.array(CameraShotSchema).min(1)
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 = z5.object({
415
- id: z5.string().min(1),
416
- label: z5.string().min(1)
450
+ var ActorSchema = z6.object({
451
+ id: z6.string().min(1),
452
+ label: z6.string().min(1)
417
453
  });
418
- var SegmentSchema = z5.object({
419
- id: z5.string().min(1),
420
- actorId: z5.string().min(1),
421
- startMs: z5.number().int().nonnegative(),
422
- endMs: z5.number().int().positive()
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 = z5.object({
425
- fromActorId: z5.string().min(1),
426
- toActorId: z5.string().min(1),
427
- durationMs: z5.number().int().positive().max(15e3),
428
- lines: z5.array(z5.string().min(1)).min(1).max(2),
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: z5.string().regex(/^captions\/turn-[0-9]+\.ass$/)
467
+ assPath: z6.string().regex(/^captions\/turn-[0-9]+\.ass$/)
432
468
  });
433
- var ExplainSegmentPlanSchema = z5.object({
434
- id: z5.string().min(1),
435
- segmentPath: z5.string().regex(/^explain\/[a-z0-9][a-z0-9-]*\/segment\.mp4$/),
436
- durationMs: z5.number().int().positive()
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 = z5.object({
439
- id: z5.string().min(1),
440
- x: z5.number().int().min(0),
441
- y: z5.number().int().min(0),
442
- width: z5.number().int().positive(),
443
- height: z5.number().int().positive(),
444
- startMs: z5.number().int().nonnegative(),
445
- endMs: z5.number().int().positive(),
446
- label: z5.string().min(1).optional(),
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: z5.object({
459
- x: z5.number().int().min(0),
460
- y: z5.number().int().min(0),
461
- width: z5.number().int().positive(),
462
- height: z5.number().int().positive()
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: z5.number().int().positive().optional()
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 = z5.object({
502
- assPath: z5.literal("captions/highlight.ass"),
503
- rects: z5.array(HighlightRectSchema).min(1)
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 = z5.object({
514
- id: z5.string().min(1),
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: z5.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
529
- atMs: z5.number().int().nonnegative(),
530
- durationMs: z5.number().int().positive(),
531
- source: z5.enum(["synth", "file", "explain"]),
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: z5.string().min(1).optional()
583
+ voice: z6.string().min(1).optional()
548
584
  });
549
- var SpeechEngineSchema = z5.object({
550
- name: z5.string().min(1),
551
- version: z5.string().min(1),
552
- modelSha256: z5.string().regex(/^[0-9a-f]{64}$/),
553
- voice: z5.string().min(1)
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 = z5.object({
556
- trackPath: z5.literal("speech/narration.wav"),
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: z5.literal(24e3),
565
- channels: z5.literal(1),
566
- clips: z5.array(SpeechClipSchema).min(1),
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 = z5.object({ accentColor: HEX_COLOUR });
628
+ var ThemeSchema = z6.object({ accentColor: HEX_COLOUR });
593
629
  var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
594
- var RenderPlanSchema = z5.object({
595
- schema: z5.literal(RENDER_PLAN_SCHEMA),
596
- source: z5.object({
597
- path: z5.literal("raw/session.webm"),
598
- sha256: z5.string().regex(/^[0-9a-f]{64}$/),
599
- durationMs: z5.number().int().nonnegative()
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: z5.object({
662
- width: z5.number().int().positive().multipleOf(2),
663
- height: z5.number().int().positive().multipleOf(2),
664
- fps: z5.literal(30),
665
- pixelFormat: z5.literal("yuv420p"),
666
- capture: z5.object({ width: z5.literal(1920), height: z5.literal(1080) }).default({ width: 1920, height: 1080 }),
667
- letterbox: z5.object({
668
- width: z5.number().int().positive().multipleOf(2),
669
- height: z5.number().int().positive().multipleOf(2),
670
- x: z5.number().int().nonnegative().multipleOf(2),
671
- y: z5.number().int().nonnegative().multipleOf(2),
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: z5.number().int().positive(),
695
- tailPadMs: z5.number().int().nonnegative()
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: z5.object({
710
- language: z5.string().min(2),
711
- srtPath: z5.literal("captions/captions.srt"),
712
- vttPath: z5.literal("captions/captions.vtt"),
713
- assPath: z5.literal("captions/captions.ass"),
714
- cues: z5.array(CueSchema)
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: z5.object({
729
- fontFile: z5.literal("assets/fonts/NotoSans-Regular.ttf"),
730
- fontName: z5.literal("Noto Sans"),
731
- fontSize: z5.number().int().positive(),
732
- textColor: z5.string(),
733
- outlineColor: z5.string(),
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: z5.number().int().nonnegative(),
736
- marginBottom: z5.number().int().nonnegative(),
737
- marginSide: z5.number().int().nonnegative().default(80),
738
- borderStyle: z5.union([z5.literal(1), z5.literal(4)]).default(1),
739
- outlineOpacity: z5.number().min(0).max(1).default(1),
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: z5.number().min(0).max(1).default(1),
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: z5.union([z5.literal(2), z5.literal(8)]).default(2)
802
+ alignment: z6.union([z6.literal(2), z6.literal(8)]).default(2)
767
803
  }),
768
- ffmpeg: z5.object({
769
- base: z5.array(z5.string()),
770
- soft: z5.array(z5.string()),
771
- hard: z5.array(z5.string())
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: z5.array(ActorSchema).min(2).optional(),
829
- segments: z5.array(SegmentSchema).min(2).optional(),
830
- transitionCards: z5.array(TransitionCardSchema).min(1).optional(),
831
- badgesAssPath: z5.literal("captions/badges.ass").optional(),
832
- explainSegments: z5.array(ExplainSegmentPlanSchema).min(1).optional()
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 z6 } from "zod";
904
- var RELATIVE_POSIX = z6.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");
905
- var Sha2562 = z6.string().regex(/^[0-9a-f]{64}$/);
906
- var DISPLAY_PATH = z6.string().min(1);
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: z6.literal("agent-demo.result/v1"),
909
- kind: z6.literal(kind),
910
- ok: z6.boolean(),
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: z6.array(z6.string())
948
+ problems: z7.array(z7.string())
913
949
  });
914
- var ArtifactRefSchema = z6.object({
950
+ var ArtifactRefSchema = z7.object({
915
951
  path: RELATIVE_POSIX,
916
952
  sha256: Sha2562,
917
- bytes: z6.number().int().nonnegative()
953
+ bytes: z7.number().int().nonnegative()
918
954
  });
919
- var StreamSummarySchema = z6.object({
920
- container: z6.enum(["mp4", "webm", "other"]),
921
- durationMs: z6.number().int().nonnegative(),
922
- video: z6.object({
923
- codec: z6.string(),
924
- width: z6.number().int(),
925
- height: z6.number().int(),
926
- pixelFormat: z6.string(),
927
- fps: z6.number().int(),
928
- frames: z6.number().int(),
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: z6.number().int().nonnegative()
971
+ durationMs: z7.number().int().nonnegative()
936
972
  }),
937
- subtitles: z6.array(z6.object({ codec: z6.string(), language: z6.string(), title: z6.string() })),
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: z6.array(
941
- z6.object({
942
- codec: z6.string(),
943
- sampleRate: z6.number().int().nonnegative(),
944
- channels: z6.number().int().nonnegative()
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: z6.number().int().nonnegative(),
949
- chapters: z6.array(
950
- z6.object({
951
- startMs: z6.number().int().nonnegative(),
952
- endMs: z6.number().int().nonnegative(),
953
- title: z6.string()
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 = z6.object({
958
- node: z6.string(),
959
- playwright: z6.string(),
960
- chromiumRevision: z6.string(),
961
- ffmpeg: z6.string(),
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: z6.string(),
999
+ libass: z7.string(),
964
1000
  fontSha256: Sha2562
965
1001
  });
966
- var AssertionSchema = z6.object({ id: z6.string().min(1), status: z6.enum(["passed", "failed"]) });
967
- var StatusSchema = z6.enum(["passed", "failed"]);
968
- var VariantsSchema = z6.enum(["soft", "hard"]);
969
- var ValidationResultSchema = z6.object({
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: z6.object({
973
- id: z6.string(),
974
- title: z6.string(),
975
- language: z6.string(),
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: z6.enum(["none", "preflight", "session"]).default("none"),
1017
+ handoff: z7.enum(["none", "preflight", "session"]).default("none"),
982
1018
  sha256: Sha2562
983
1019
  }).optional()
984
1020
  });
985
- var RunCommandResultSchema = z6.object({
1021
+ var RunCommandResultSchema = z7.object({
986
1022
  ...envelope("run"),
987
1023
  bundleDir: DISPLAY_PATH,
988
1024
  status: StatusSchema,
989
- cueCount: z6.number().int().nonnegative(),
1025
+ cueCount: z7.number().int().nonnegative(),
990
1026
  /** Zero on the Free Tier, and zero for a scenario with no `chapter` calls. */
991
- chapterCount: z6.number().int().nonnegative(),
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: z6.number().int().nonnegative(),
1001
- diagnostics: z6.array(z6.object({ code: z6.string(), cueId: z6.string(), detail: z6.string() })),
1002
- assertions: z6.array(AssertionSchema),
1003
- outputs: z6.array(ArtifactRefSchema)
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: z6.string().optional() });
1006
- var CheckCommandResultSchema = z6.object({
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: z6.array(CheckAssertionSchema),
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: z6.array(z6.object({ code: z6.string(), cueId: z6.string(), detail: z6.string() }))
1052
+ diagnostics: z7.array(z7.object({ code: z7.string(), cueId: z7.string(), detail: z7.string() }))
1017
1053
  });
1018
- var RenderCommandResultSchema = z6.object({
1054
+ var RenderCommandResultSchema = z7.object({
1019
1055
  ...envelope("render"),
1020
1056
  bundleDir: DISPLAY_PATH,
1021
1057
  variants: VariantsSchema,
1022
- streams: z6.object({
1058
+ streams: z7.object({
1023
1059
  base: StreamSummarySchema.optional(),
1024
1060
  soft: StreamSummarySchema.optional(),
1025
1061
  hard: StreamSummarySchema.optional()
1026
1062
  }),
1027
- outputs: z6.array(ArtifactRefSchema)
1063
+ outputs: z7.array(ArtifactRefSchema)
1028
1064
  });
1029
- var VerificationReportSchema = z6.object({
1065
+ var VerificationReportSchema = z7.object({
1030
1066
  ...envelope("verify"),
1031
1067
  bundleDir: DISPLAY_PATH,
1032
- artifactCount: z6.number().int().nonnegative()
1068
+ artifactCount: z7.number().int().nonnegative()
1033
1069
  });
1034
- var DifferenceCategorySchema = z6.enum(["step", "assertion", "target", "timing", "caption", "actor"]);
1035
- var DiffCommandResultSchema = z6.object({
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: z6.boolean(),
1040
- differences: z6.array(z6.object({ category: DifferenceCategorySchema, detail: z6.string() }))
1082
+ identical: z7.boolean(),
1083
+ differences: z7.array(z7.object({ category: DifferenceCategorySchema, detail: z7.string() }))
1041
1084
  });
1042
- var PruneCandidateSchema = z6.object({
1085
+ var PruneCandidateSchema = z7.object({
1043
1086
  path: DISPLAY_PATH,
1044
- bytes: z6.number().int().nonnegative(),
1045
- scenarioId: z6.string().optional()
1087
+ bytes: z7.number().int().nonnegative(),
1088
+ scenarioId: z7.string().optional()
1046
1089
  });
1047
- var PruneCommandResultSchema = z6.object({
1090
+ var PruneCommandResultSchema = z7.object({
1048
1091
  ...envelope("prune"),
1049
- dryRun: z6.boolean(),
1092
+ dryRun: z7.boolean(),
1050
1093
  /** What was removed (confirmed) or would be (dry run) — never includes a `failed` entry. */
1051
- candidates: z6.array(PruneCandidateSchema),
1094
+ candidates: z7.array(PruneCandidateSchema),
1052
1095
  /** A discovered bundle that was never attempted, and why — never silently dropped. */
1053
- skipped: z6.array(z6.object({ path: DISPLAY_PATH, reason: z6.string() })),
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: z6.array(z6.object({ path: DISPLAY_PATH, reason: z6.string() })),
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: z6.number().int().nonnegative()
1102
+ bytesReclaimed: z7.number().int().nonnegative()
1060
1103
  });
1061
- var ImportCommandResultSchema = z6.object({
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: z6.number().int().nonnegative(),
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: z6.boolean(),
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: z6.array(z6.string())
1115
+ warnings: z7.array(z7.string())
1073
1116
  });
1074
- var InspectResultSchema = z6.object({
1117
+ var InspectResultSchema = z7.object({
1075
1118
  ...envelope("inspect"),
1076
1119
  bundleDir: DISPLAY_PATH,
1077
1120
  status: StatusSchema,
1078
- scenario: z6.object({ id: z6.string(), sourceSha256: Sha2562 }),
1121
+ scenario: z7.object({ id: z7.string(), sourceSha256: Sha2562 }),
1079
1122
  toolchain: ToolchainSchema,
1080
- video: z6.object({
1123
+ video: z7.object({
1081
1124
  /** The whole output: the cards, if any, plus the content between them. */
1082
- durationMs: z6.number().int(),
1083
- frames: z6.number().int(),
1084
- width: z6.number().int(),
1085
- height: z6.number().int(),
1086
- fps: z6.number().int(),
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: z6.number().int().nonnegative(),
1102
- introDurationMs: z6.number().int().nonnegative().default(0),
1103
- outroDurationMs: z6.number().int().nonnegative()
1144
+ contentDurationMs: z7.number().int().nonnegative(),
1145
+ introDurationMs: z7.number().int().nonnegative().default(0),
1146
+ outroDurationMs: z7.number().int().nonnegative()
1104
1147
  }),
1105
- captions: z6.object({
1106
- language: z6.string(),
1107
- cueCount: z6.number().int().nonnegative(),
1108
- characterCount: z6.number().int().nonnegative(),
1109
- medianCueMs: z6.number().int().nonnegative(),
1110
- shortestCueMs: z6.number().int().nonnegative(),
1111
- longestCueMs: z6.number().int().nonnegative()
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: z6.array(z6.object({ startMs: z6.number().int().nonnegative(), title: z6.string() })),
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: z6.object({
1126
- clipCount: z6.number().int().positive(),
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: z6.number().int().nonnegative(),
1171
+ spokenMs: z7.number().int().nonnegative(),
1129
1172
  /** How many clips the author supplied rather than the model producing. */
1130
- suppliedCount: z6.number().int().nonnegative(),
1131
- voice: z6.string().optional()
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: z6.object({
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: z6.number().int().min(2),
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: z6.array(z6.string().min(1)).min(2),
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: z6.number().int().nonnegative()
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: z6.array(
1173
- z6.object({
1174
- id: z6.string().min(1),
1175
- durationMs: z6.number().int().positive()
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: z6.array(ArtifactRefSchema),
1180
- assertions: z6.array(AssertionSchema),
1181
- artifactCount: z6.number().int().nonnegative(),
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: z6.number().int().nonnegative()
1226
+ bundleBytes: z7.number().int().nonnegative()
1184
1227
  });
1185
- var DoctorResultSchema = z6.object({
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: z6.string(),
1194
- ffmpeg: z6.string(),
1195
- ffprobe: z6.string(),
1196
- libass: z6.string(),
1197
- hasLibass: z6.boolean(),
1198
- hasX264: z6.boolean(),
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: z6.boolean(),
1209
- filters: z6.array(z6.string()),
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: z6.object({
1219
- ready: z6.boolean(),
1261
+ speech: z7.object({
1262
+ ready: z7.boolean(),
1220
1263
  /** Installed voices, in catalogue order. Empty means the model cannot speak yet. */
1221
- voices: z6.array(z6.string()),
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: z6.array(z6.string())
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: z6.object({
1235
- installed: z6.boolean(),
1277
+ plainmotion: z7.object({
1278
+ installed: z7.boolean(),
1236
1279
  /** plainmotion's own `--version` output. Absent when not installed. */
1237
- version: z6.string().optional(),
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: z6.string().optional()
1282
+ note: z7.string().optional()
1240
1283
  })
1241
1284
  });
1242
1285
  var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
1243
- var ActivateReasonSchema = z6.enum([
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 = z6.object({
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: z6.object({
1258
- subject: z6.string().min(1),
1259
- activatedAt: z6.string().min(1),
1260
- uses: z6.number().int().nonnegative()
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 = z6.discriminatedUnion("tier", [
1269
- z6.object({
1311
+ var LicenceResultSchema = z7.discriminatedUnion("tier", [
1312
+ z7.object({
1270
1313
  ...envelope("licence"),
1271
- tier: z6.literal("free"),
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: z6.string().optional()
1319
+ problem: z7.string().optional()
1277
1320
  }),
1278
- z6.object({
1321
+ z7.object({
1279
1322
  ...envelope("licence"),
1280
- tier: z6.literal("pro"),
1281
- subject: z6.string().min(1),
1282
- activatedAt: z6.string().min(1),
1283
- uses: z6.number().int().nonnegative()
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
 
@@ -2,6 +2,7 @@ export * from './errors.js';
2
2
  export * from './event.js';
3
3
  export * from './json-schema.js';
4
4
  export * from './manifest.js';
5
+ export * from './narration-index.js';
5
6
  export * from './render-plan.js';
6
7
  export * from './results.js';
7
8
  export * from './scenario.js';
@@ -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>;
@@ -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.13.0",
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",