@plaintake/scenario 1.7.0 → 1.10.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 +214 -26
- package/dist/schema/event.d.ts +2 -0
- package/dist/schema/manifest.d.ts +1 -0
- package/dist/schema/render-plan.d.ts +102 -3
- package/dist/schema/results.d.ts +42 -1
- package/dist/schema/scenario.d.ts +10 -13
- package/dist/types.d.ts +178 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -51,7 +51,18 @@ var DemoEventTypeSchema = z.enum([
|
|
|
51
51
|
* `step.start`/`step.finish` exactly as before; the `actor` field below records which
|
|
52
52
|
* actor performed the step.
|
|
53
53
|
*/
|
|
54
|
-
"turn"
|
|
54
|
+
"turn",
|
|
55
|
+
/*
|
|
56
|
+
* A full-frame motion-graphics cut-away: `demo.explain(...)` stops the screencast, splices
|
|
57
|
+
* in a PlainMotion-rendered scene at this point on the timeline, and resumes the recording
|
|
58
|
+
* on the same page, same actor. A member of its own for the same reason `turn` is one: a
|
|
59
|
+
* cut-away is not a step — no target, no verb, no hold — and its `payload` carries the
|
|
60
|
+
* whole authored request (id, narration, scene props, cues) because the scene is compiled
|
|
61
|
+
* from that payload *after* capture ends, never drawn live. The event marks the boundary
|
|
62
|
+
* the way a `turn` marks its hand-off, so a run that dies mid-explain leaves a log that
|
|
63
|
+
* says where the cut was going to fall.
|
|
64
|
+
*/
|
|
65
|
+
"explain"
|
|
55
66
|
]);
|
|
56
67
|
var TargetSchema = z.object({
|
|
57
68
|
role: z.string().optional(),
|
|
@@ -97,7 +108,34 @@ var ScenarioCameraSchema = z2.object({
|
|
|
97
108
|
minDwellMs: z2.number().int().min(0, "camera.minDwellMs must be at least 0").max(5e3, "camera.minDwellMs must be at most 5000ms").optional()
|
|
98
109
|
});
|
|
99
110
|
var ScenarioSpeechSchema = z2.object({
|
|
100
|
-
speed: z2.number().min(0.5, "speech.speed must be at least 0.5 (half speed)").max(2, "speech.speed must be at most 2.0 (double speed) \u2014 an unmeasured, conservative ceiling").optional()
|
|
111
|
+
speed: z2.number().min(0.5, "speech.speed must be at least 0.5 (half speed)").max(2, "speech.speed must be at most 2.0 (double speed) \u2014 an unmeasured, conservative ceiling").optional(),
|
|
112
|
+
/*
|
|
113
|
+
* The voices this scenario may switch to mid-run, beyond the run-wide default `--voice`
|
|
114
|
+
* picks. A step names one with `demo.step({ voice })` or an actor with `demo.actor(id,
|
|
115
|
+
* { voice })`; a name that is not the default and not in this list is refused at record
|
|
116
|
+
* time with the fix in the message.
|
|
117
|
+
*
|
|
118
|
+
* Declared up front rather than discovered as steps run, for the same reason `handoff`
|
|
119
|
+
* is: the worker is configured with its voice list once, before Chromium starts, and an
|
|
120
|
+
* unknown voice found mid-recording would be minutes of footage thrown away over a list
|
|
121
|
+
* the scenario could have stated in its metadata. Declaring them also lets `openNarrator`
|
|
122
|
+
* preflight every voice's `.bin` eagerly, so a missing download is a refusal before the
|
|
123
|
+
* browser opens, not a dead step 40 seconds into a capture.
|
|
124
|
+
*
|
|
125
|
+
* Memory is why the list is the author's to keep short: the engine pre-loads every voice
|
|
126
|
+
* in this list the moment it opens (the first synthesised step) and holds them until the
|
|
127
|
+
* narrator closes. One voice measures ~500 MB resident, a second loaded alongside it
|
|
128
|
+
* ~750 MB — roughly +250 MB per additional voice, not 500 MB times the count. A scenario
|
|
129
|
+
* that declares voices but supplies a WAV for every step never opens the engine at all and
|
|
130
|
+
* pays none of this. `min(1)` because an empty list can only be a mistake — it is
|
|
131
|
+
* indistinguishable from declaring nothing, which is `undefined`'s job.
|
|
132
|
+
*
|
|
133
|
+
* One language per run: every voice named here, the default included, must map to the same
|
|
134
|
+
* language, derived from the voice's prefix (`speechLanguageOfRun` in `@plaintake/speech`).
|
|
135
|
+
* A mix is refused at record time with both sides named — the run's pronunciation
|
|
136
|
+
* dictionary is configured once, for the one language it speaks.
|
|
137
|
+
*/
|
|
138
|
+
voices: z2.array(z2.string().min(1, "speech.voices entries must be non-empty voice names")).min(1, "speech.voices must name at least one voice \u2014 an empty list declares nothing, so omit it").optional()
|
|
101
139
|
});
|
|
102
140
|
var ScenarioMetaSchema = z2.object({
|
|
103
141
|
schema: z2.literal("agent-demo.scenario/v1"),
|
|
@@ -207,7 +245,14 @@ var BundleManifestSchema = z4.object({
|
|
|
207
245
|
/** May be the literal "unknown" — never a fabricated version. */
|
|
208
246
|
libass: z4.string(),
|
|
209
247
|
fontSha256: Sha256,
|
|
210
|
-
containerImage: z4.string().optional()
|
|
248
|
+
containerImage: z4.string().optional(),
|
|
249
|
+
/**
|
|
250
|
+
* The plainmotion CLI's own `--version` string, present exactly when a scenario called
|
|
251
|
+
* `demo.explain()` at least once — a run that never cut away never spawned it, and
|
|
252
|
+
* `containerImage`'s own optionality is the precedent for saying so with an absent field
|
|
253
|
+
* rather than a fabricated one.
|
|
254
|
+
*/
|
|
255
|
+
plainmotion: z4.string().optional()
|
|
211
256
|
}),
|
|
212
257
|
environment: z4.object({
|
|
213
258
|
os: z4.string(),
|
|
@@ -385,6 +430,11 @@ var TransitionCardSchema = z5.object({
|
|
|
385
430
|
textColor: HEX_COLOUR,
|
|
386
431
|
assPath: z5.string().regex(/^captions\/turn-[0-9]+\.ass$/)
|
|
387
432
|
});
|
|
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()
|
|
437
|
+
});
|
|
388
438
|
var HighlightRectSchema = z5.object({
|
|
389
439
|
id: z5.string().min(1),
|
|
390
440
|
x: z5.number().int().min(0),
|
|
@@ -432,7 +482,23 @@ var SpeechClipSchema = z5.object({
|
|
|
432
482
|
path: z5.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
|
|
433
483
|
atMs: z5.number().int().nonnegative(),
|
|
434
484
|
durationMs: z5.number().int().positive(),
|
|
435
|
-
source: z5.enum(["synth", "file"])
|
|
485
|
+
source: z5.enum(["synth", "file", "explain"]),
|
|
486
|
+
/*
|
|
487
|
+
* Which voice synthesised this clip, when it was not the plan-wide default that
|
|
488
|
+
* `engine.voice` names. Absent on every clip of a one-voice recording and on every
|
|
489
|
+
* author-supplied clip — a `file` clip has no synthesiser, so there is no voice to record,
|
|
490
|
+
* and inventing the engine's would be the same lie `SpeechEngineSchema`'s comment refuses
|
|
491
|
+
* to tell about `modelSha256`.
|
|
492
|
+
*
|
|
493
|
+
* Evidence only, like the whole `engine` block: nothing reads it to make a decision. It is
|
|
494
|
+
* the per-clip answer to "this clip sounds different from its neighbours, why", which a
|
|
495
|
+
* mixed-voice bundle could not otherwise give — the engine block names one voice, and the
|
|
496
|
+
* WAV cannot be asked.
|
|
497
|
+
*
|
|
498
|
+
* Optional and additive: every bundle frozen before per-step voices existed carries no
|
|
499
|
+
* `voice` on any clip and must keep verifying, which is what `optional()` buys.
|
|
500
|
+
*/
|
|
501
|
+
voice: z5.string().min(1).optional()
|
|
436
502
|
});
|
|
437
503
|
var SpeechEngineSchema = z5.object({
|
|
438
504
|
name: z5.string().min(1),
|
|
@@ -468,13 +534,16 @@ var SpeechSchema = z5.object({
|
|
|
468
534
|
message: "clips must have unique ids and be laid out in order without overlapping",
|
|
469
535
|
path: ["clips"]
|
|
470
536
|
}
|
|
471
|
-
).refine(({ clips, engine }) => engine !== void 0 || clips.every((clip) => clip.source
|
|
537
|
+
).refine(({ clips, engine }) => engine !== void 0 || clips.every((clip) => clip.source !== "synth"), {
|
|
472
538
|
// A synthesised clip with no record of what synthesised it is the one state this block
|
|
473
539
|
// exists to prevent. The converse is fine: an engine recorded on an all-file track is
|
|
474
|
-
// merely redundant, not misleading.
|
|
540
|
+
// merely redundant, not misleading. An `explain` clip is exempt rather than overlooked:
|
|
541
|
+
// it was synthesised by plainmotion, whose identity the manifest's toolchain block
|
|
542
|
+
// carries — the `engine` here would name a synthesiser that never touched it.
|
|
475
543
|
message: "engine must be recorded whenever any clip was synthesised",
|
|
476
544
|
path: ["engine"]
|
|
477
545
|
});
|
|
546
|
+
var ThemeSchema = z5.object({ accentColor: HEX_COLOUR });
|
|
478
547
|
var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
|
|
479
548
|
var RenderPlanSchema = z5.object({
|
|
480
549
|
schema: z5.literal(RENDER_PLAN_SCHEMA),
|
|
@@ -663,49 +732,123 @@ var RenderPlanSchema = z5.object({
|
|
|
663
732
|
highlight: HighlightSchema.optional(),
|
|
664
733
|
speech: SpeechSchema.optional(),
|
|
665
734
|
/**
|
|
666
|
-
* The
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
735
|
+
* The branding decision, resolved once from the licence and configuration at run time
|
|
736
|
+
* and frozen whole — the accent the cursor's fill, its click-ripple ring and the
|
|
737
|
+
* highlight label's text all draw. See `ThemeSchema`.
|
|
738
|
+
*
|
|
739
|
+
* Optional and **never defaulted**. The plan's own JSON is a weaker claim than the
|
|
740
|
+
* render — the schema doc above records how a Zod `.default()` materialises on parse,
|
|
741
|
+
* so `stableStringify` writes the field into `render/render-plan.json` and diverges the
|
|
742
|
+
* committed golden plan, which is what `style`'s defaults already did once. A default
|
|
743
|
+
* here would do it again on every parse; absent means off, and the object appears only
|
|
744
|
+
* when a theme was actually resolved.
|
|
745
|
+
*
|
|
746
|
+
* Top-level — a sibling of `intro`/`outro`/`cursor`/`highlight`, not a key inside one
|
|
747
|
+
* of them — so `recutPlan`'s `{...plan}` spread carries it through `--aspect` re-cuts
|
|
748
|
+
* unchanged; a nested key would need explicit handling there.
|
|
749
|
+
*
|
|
750
|
+
* The plan is not `.strict()`, so an older binary strips a `theme` it has never heard
|
|
751
|
+
* of — but a plain re-render is unaffected by that: `renderBundle` executes the frozen
|
|
752
|
+
* themed `cursor.ass`/`highlight.ass` verbatim and never regenerates them, exactly as
|
|
753
|
+
* an older build ignoring `speech` still muxes the frozen WAV. The hardcoded colours
|
|
754
|
+
* return only where derived files are *redrawn* — a fresh freeze, or a `render
|
|
755
|
+
* --aspect` re-cut, whose `writeCaptions(recut)` regenerates the tracks from a plan the
|
|
756
|
+
* older binary's own re-parse has stripped `theme` out of.
|
|
757
|
+
*/
|
|
758
|
+
theme: ThemeSchema.optional(),
|
|
759
|
+
/**
|
|
760
|
+
* The capture timeline cut into windows — of a genuine multi-actor cast, of an implicit
|
|
761
|
+
* single actor pausing for explain cut-aways, or both at once. `segments` is the one
|
|
762
|
+
* field of the five below that a segmented capture *always* carries; the other four are
|
|
763
|
+
* two independent, narrower capabilities layered on top of it:
|
|
764
|
+
*
|
|
765
|
+
* - `actors`/`transitionCards`/`badgesAssPath` — the multi-actor demo's cast, the cards
|
|
766
|
+
* drawn at each hand-off, and the badge track naming the active actor throughout. Three
|
|
767
|
+
* fields that carry one capability and so, like every other optional block above, are
|
|
768
|
+
* present or absent together (the first refinement below) — and never without
|
|
769
|
+
* `segments`, which they reference by `actorId` (the second). Absent means no genuine
|
|
770
|
+
* cast: every explain-only demo, and every plan before multi-actor demos existed.
|
|
771
|
+
* - `explainSegments` — the frozen scenes cut into the gaps between `segments`, present
|
|
772
|
+
* whenever a scenario called `demo.explain()` at all, with or without a cast alongside
|
|
773
|
+
* it — never without `segments` either (the third refinement), for the same reason.
|
|
774
|
+
*
|
|
775
|
+
* Every gap between adjacent `segments` is claimed by exactly one occupant, a card or an
|
|
776
|
+
* explain scene, never both and never neither (the fifth refinement) — which is also why
|
|
777
|
+
* `segments` can appear with `transitionCards` and `explainSegments` both absent only when
|
|
778
|
+
* `segments.length === 1` never occurs (`SegmentSchema`'s own `.min(2)` on the array rules
|
|
779
|
+
* it out): a lone, unsegmented capture has no gap to claim in the first place, and needs
|
|
780
|
+
* none of these five fields at all.
|
|
671
781
|
*/
|
|
672
782
|
actors: z5.array(ActorSchema).min(2).optional(),
|
|
673
783
|
segments: z5.array(SegmentSchema).min(2).optional(),
|
|
674
784
|
transitionCards: z5.array(TransitionCardSchema).min(1).optional(),
|
|
675
|
-
badgesAssPath: z5.literal("captions/badges.ass").optional()
|
|
785
|
+
badgesAssPath: z5.literal("captions/badges.ass").optional(),
|
|
786
|
+
explainSegments: z5.array(ExplainSegmentPlanSchema).min(1).optional()
|
|
676
787
|
}).refine(
|
|
677
788
|
(plan) => {
|
|
678
|
-
const
|
|
679
|
-
return
|
|
789
|
+
const castFields = [plan.actors, plan.transitionCards, plan.badgesAssPath];
|
|
790
|
+
return castFields.every((field) => field !== void 0) || castFields.every((field) => field === void 0);
|
|
680
791
|
},
|
|
681
792
|
{
|
|
682
793
|
// The same "carries its capability" discipline every optional block above already
|
|
683
794
|
// follows: a plan naming actors with nowhere to draw their badges (or vice versa)
|
|
684
795
|
// cannot render, so it is refused at the parse rather than discovered mid-encode.
|
|
685
|
-
message: "actors,
|
|
796
|
+
message: "actors, transitionCards and badgesAssPath must be present together or absent together",
|
|
686
797
|
path: ["actors"]
|
|
687
798
|
}
|
|
799
|
+
).refine((plan) => plan.actors === void 0 || plan.segments !== void 0, {
|
|
800
|
+
// A cast with nowhere to draw its own windows — `segment.actorId` is what a badge or a
|
|
801
|
+
// transition card is drawn against — cannot render.
|
|
802
|
+
message: "actors requires segments \u2014 a cast with no capture windows of its own cannot render",
|
|
803
|
+
path: ["actors"]
|
|
804
|
+
}).refine((plan) => plan.explainSegments === void 0 || plan.segments !== void 0, {
|
|
805
|
+
// An explain scene occupies a gap between two segments; with no segments there is no
|
|
806
|
+
// gap for it to occupy.
|
|
807
|
+
message: "explainSegments requires segments \u2014 an explain scene with no gap to occupy cannot render",
|
|
808
|
+
path: ["explainSegments"]
|
|
809
|
+
}).refine(
|
|
810
|
+
(plan) => {
|
|
811
|
+
if (plan.segments === void 0) return true;
|
|
812
|
+
const { segments } = plan;
|
|
813
|
+
return segments[0]?.startMs === 0 && segments.every(
|
|
814
|
+
(segment, index) => segment.endMs > segment.startMs && (index === 0 || segments[index - 1]?.endMs === segment.startMs)
|
|
815
|
+
);
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
// Mirrors `ChaptersSchema`'s own tiling refine: segments must start at 0 and tile the
|
|
819
|
+
// source contiguously, the same reason a gap or overlap in chapter marks is refused
|
|
820
|
+
// rather than tolerated — true of every segmented capture, cast or castless alike.
|
|
821
|
+
message: "segments must tile raw/session.webm contiguously from 0",
|
|
822
|
+
path: ["segments"]
|
|
823
|
+
}
|
|
688
824
|
).refine(
|
|
689
825
|
(plan) => {
|
|
690
|
-
if (plan.
|
|
826
|
+
if (plan.segments === void 0) return true;
|
|
827
|
+
const gapCount = (plan.transitionCards?.length ?? 0) + (plan.explainSegments?.length ?? 0);
|
|
828
|
+
return gapCount === plan.segments.length - 1;
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
// The count a hand-off or an explain between each pair of adjacent segments implies —
|
|
832
|
+
// every gap claimed by exactly one occupant, restated here in one refine because
|
|
833
|
+
// `transitionCards` and `explainSegments` share the gaps between the very same
|
|
834
|
+
// `segments` array (see `ExplainSegmentPlanSchema`'s own doc on how the renderer tells
|
|
835
|
+
// the two apart without either array carrying an explicit gap index).
|
|
836
|
+
message: "transitionCards.length plus explainSegments.length must equal segments.length - 1 \u2014 every gap between segments must be claimed by exactly one card or explain scene",
|
|
837
|
+
path: ["segments"]
|
|
838
|
+
}
|
|
839
|
+
).refine(
|
|
840
|
+
(plan) => {
|
|
841
|
+
if (plan.actors === void 0 || plan.segments === void 0 || plan.transitionCards === void 0) {
|
|
691
842
|
return true;
|
|
692
843
|
}
|
|
693
844
|
const { actors, segments, transitionCards } = plan;
|
|
694
845
|
const namesDeclaredActor = (actorId) => actors.some((actor) => actor.id === actorId);
|
|
695
|
-
return
|
|
696
|
-
(segment, index) => segment.endMs > segment.startMs && (index === 0 || segments[index - 1]?.endMs === segment.startMs) && namesDeclaredActor(segment.actorId)
|
|
697
|
-
) && transitionCards.every(
|
|
846
|
+
return segments.every((segment) => namesDeclaredActor(segment.actorId)) && transitionCards.every(
|
|
698
847
|
(card) => namesDeclaredActor(card.fromActorId) && namesDeclaredActor(card.toActorId)
|
|
699
848
|
);
|
|
700
849
|
},
|
|
701
850
|
{
|
|
702
|
-
|
|
703
|
-
// source contiguously, the same reason a gap or overlap in chapter marks is refused
|
|
704
|
-
// rather than tolerated. `transitionCards.length === segments.length - 1` is the
|
|
705
|
-
// count a hand-off between each pair of adjacent segments implies, and every
|
|
706
|
-
// `segment.actorId`/`transitionCard.fromActorId`/`transitionCard.toActorId` must name
|
|
707
|
-
// a declared actor or there is nobody for the recorder to have handed the context to.
|
|
708
|
-
message: "segments must tile raw/session.webm contiguously from 0, transitionCards.length must equal segments.length - 1, and every segment.actorId, transitionCard.fromActorId and transitionCard.toActorId must name a declared actor",
|
|
851
|
+
message: "every segment.actorId, transitionCard.fromActorId and transitionCard.toActorId must name a declared actor",
|
|
709
852
|
path: ["segments"]
|
|
710
853
|
}
|
|
711
854
|
);
|
|
@@ -869,6 +1012,19 @@ var PruneCommandResultSchema = z6.object({
|
|
|
869
1012
|
* be reclaimed if every candidate succeeded. */
|
|
870
1013
|
bytesReclaimed: z6.number().int().nonnegative()
|
|
871
1014
|
});
|
|
1015
|
+
var ImportCommandResultSchema = z6.object({
|
|
1016
|
+
...envelope("import"),
|
|
1017
|
+
tracePath: DISPLAY_PATH,
|
|
1018
|
+
outputPath: DISPLAY_PATH,
|
|
1019
|
+
/** How many `demo.step` calls the draft carries. Zero only on the failure path. */
|
|
1020
|
+
stepCount: z6.number().int().nonnegative(),
|
|
1021
|
+
/** Whether the written draft already passes `plaintake validate`. Best-effort: a draft
|
|
1022
|
+
* that fails is still written, still a success, and its problems are warnings here. */
|
|
1023
|
+
validates: z6.boolean(),
|
|
1024
|
+
/** Author-facing advice: multi-page traces, redactions, unimported trace calls, and the
|
|
1025
|
+
* standing review-for-secrets line. Never empty on success. */
|
|
1026
|
+
warnings: z6.array(z6.string())
|
|
1027
|
+
});
|
|
872
1028
|
var InspectResultSchema = z6.object({
|
|
873
1029
|
...envelope("inspect"),
|
|
874
1030
|
bundleDir: DISPLAY_PATH,
|
|
@@ -957,6 +1113,22 @@ var InspectResultSchema = z6.object({
|
|
|
957
1113
|
* independently from `segments` for the reason above. */
|
|
958
1114
|
turnCount: z6.number().int().nonnegative()
|
|
959
1115
|
}).refine((value) => value.count === value.labels.length, "actors.count must equal actors.labels.length").optional(),
|
|
1116
|
+
/**
|
|
1117
|
+
* The explain scenes this bundle cut away to, in composite order, or absent for a bundle
|
|
1118
|
+
* with none — every bundle recorded before `demo.explain()` existed, and still the default
|
|
1119
|
+
* after it, so this stays absent rather than an empty array, the same distinction `actors`
|
|
1120
|
+
* above and `narration` further up both draw.
|
|
1121
|
+
*
|
|
1122
|
+
* Sourced from `plan.explainSegments` alone, never the scenario file, for the same reason
|
|
1123
|
+
* `actors` is sourced from the plan rather than re-read from `demo.explain()` calls: a
|
|
1124
|
+
* bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
1125
|
+
*/
|
|
1126
|
+
explainScenes: z6.array(
|
|
1127
|
+
z6.object({
|
|
1128
|
+
id: z6.string().min(1),
|
|
1129
|
+
durationMs: z6.number().int().positive()
|
|
1130
|
+
})
|
|
1131
|
+
).min(1).optional(),
|
|
960
1132
|
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
961
1133
|
outputs: z6.array(ArtifactRefSchema),
|
|
962
1134
|
assertions: z6.array(AssertionSchema),
|
|
@@ -1003,6 +1175,22 @@ var DoctorResultSchema = z6.object({
|
|
|
1003
1175
|
voices: z6.array(z6.string()),
|
|
1004
1176
|
/** What is missing, in the same words a refused run would use. Empty when ready. */
|
|
1005
1177
|
notes: z6.array(z6.string())
|
|
1178
|
+
}),
|
|
1179
|
+
/**
|
|
1180
|
+
* Whether the plainmotion CLI is on `PATH`, for a scenario that wants to call
|
|
1181
|
+
* `demo.explain()`. The same stance `hasAac`/`speech` above already take: absence is a
|
|
1182
|
+
* *state*, not a fault — the overwhelming majority of scenarios never cut away to an
|
|
1183
|
+
* explain scene, so `doctor` must not fail an otherwise-healthy installation over a binary
|
|
1184
|
+
* most runs will never invoke. `assertPlainmotion` (`recorder-playwright`) is the same
|
|
1185
|
+
* probe a run's own first `demo.explain()` pays, so `doctor` cannot say plainmotion is fine
|
|
1186
|
+
* and then a run refuse it.
|
|
1187
|
+
*/
|
|
1188
|
+
plainmotion: z6.object({
|
|
1189
|
+
installed: z6.boolean(),
|
|
1190
|
+
/** plainmotion's own `--version` output. Absent when not installed. */
|
|
1191
|
+
version: z6.string().optional(),
|
|
1192
|
+
/** Why the probe failed, in the same words a run's own refusal would use. Absent when installed. */
|
|
1193
|
+
note: z6.string().optional()
|
|
1006
1194
|
})
|
|
1007
1195
|
});
|
|
1008
1196
|
var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
|
package/dist/schema/event.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const DemoEventTypeSchema: z.ZodEnum<{
|
|
|
10
10
|
"privacy.mask": "privacy.mask";
|
|
11
11
|
"human.wait": "human.wait";
|
|
12
12
|
turn: "turn";
|
|
13
|
+
explain: "explain";
|
|
13
14
|
}>;
|
|
14
15
|
export declare const TargetSchema: z.ZodObject<{
|
|
15
16
|
role: z.ZodOptional<z.ZodString>;
|
|
@@ -38,6 +39,7 @@ export declare const DemoEventSchema: z.ZodObject<{
|
|
|
38
39
|
"privacy.mask": "privacy.mask";
|
|
39
40
|
"human.wait": "human.wait";
|
|
40
41
|
turn: "turn";
|
|
42
|
+
explain: "explain";
|
|
41
43
|
}>;
|
|
42
44
|
stableId: z.ZodOptional<z.ZodString>;
|
|
43
45
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -17,6 +17,7 @@ export declare const BundleManifestSchema: z.ZodObject<{
|
|
|
17
17
|
libass: z.ZodString;
|
|
18
18
|
fontSha256: z.ZodString;
|
|
19
19
|
containerImage: z.ZodOptional<z.ZodString>;
|
|
20
|
+
plainmotion: z.ZodOptional<z.ZodString>;
|
|
20
21
|
}, z.core.$strip>;
|
|
21
22
|
environment: z.ZodObject<{
|
|
22
23
|
os: z.ZodString;
|
|
@@ -238,6 +238,33 @@ export declare const TransitionCardSchema: z.ZodObject<{
|
|
|
238
238
|
assPath: z.ZodString;
|
|
239
239
|
}, z.core.$strip>;
|
|
240
240
|
export type TransitionCard = z.infer<typeof TransitionCardSchema>;
|
|
241
|
+
/**
|
|
242
|
+
* One frozen explain scene's real render file, occupying one of the gaps between `segments`.
|
|
243
|
+
*
|
|
244
|
+
* **Never carries an explicit gap index, for the same reason `TransitionCardSchema` never
|
|
245
|
+
* has: both arrays are purely positional among their own homogeneous class of gap, in
|
|
246
|
+
* left-to-right gap-encounter order — the same discipline `transitionCards` already keeps.**
|
|
247
|
+
* Which of the `segments.length - 1` gaps is a card and which is an explain scene follows
|
|
248
|
+
* deterministically from whether the two segments either side of it share an `actorId`: a
|
|
249
|
+
* `demo.turn()` boundary always changes who is filming (`runtime.ts` refuses a turn to the
|
|
250
|
+
* actor already holding the browser), and a `demo.explain()` boundary never does (the same
|
|
251
|
+
* actor's capture merely pauses for the cut-away) — so an "actor changed" gap is always a
|
|
252
|
+
* card, and an "actor unchanged" gap is always an explain, and the renderer recovers the
|
|
253
|
+
* interleaving from that invariant rather than from a stored index. `segments` may also
|
|
254
|
+
* carry no `actorId` at all — the no-cast, explain-only case — in which every gap is,
|
|
255
|
+
* necessarily, an explain (there is no hand-off to draw a card for).
|
|
256
|
+
*
|
|
257
|
+
* `segmentPath` is the one thing a card's own `assPath` is not: a real file to decode as an
|
|
258
|
+
* extra input, not a generated `color=` source — `durationMs` is carried alongside for
|
|
259
|
+
* validation and tooling (`plaintake inspect`), but the file itself is the render's
|
|
260
|
+
* authority on its own frame count.
|
|
261
|
+
*/
|
|
262
|
+
export declare const ExplainSegmentPlanSchema: z.ZodObject<{
|
|
263
|
+
id: z.ZodString;
|
|
264
|
+
segmentPath: z.ZodString;
|
|
265
|
+
durationMs: z.ZodNumber;
|
|
266
|
+
}, z.core.$strip>;
|
|
267
|
+
export type ExplainSegmentPlan = z.infer<typeof ExplainSegmentPlanSchema>;
|
|
241
268
|
/**
|
|
242
269
|
* How long a `demo.turn(actor, { card })` transition card is on screen when its own
|
|
243
270
|
* `durationMs` was omitted.
|
|
@@ -269,9 +296,11 @@ export declare const TRANSITION_CARD_DEFAULT_MS = 3000;
|
|
|
269
296
|
* to be a pure function of the plan, so the rect a step measured has to survive plan freeze
|
|
270
297
|
* as a rect, not merely as a decision to draw one.
|
|
271
298
|
*
|
|
272
|
-
* `startMs`/`endMs`
|
|
273
|
-
*
|
|
274
|
-
*
|
|
299
|
+
* `startMs`/`endMs` carry a window the derivation already retimed, the way a cursor point's
|
|
300
|
+
* do: a click's brackets the press — opening when the pointer parks and closing as the click
|
|
301
|
+
* ripple ends — while a continuous action's spans its whole step. The times are frozen as
|
|
302
|
+
* computed, so unlike `CursorPointSchema` (whose `rippleMs` the emitter re-derives fades
|
|
303
|
+
* around) there is no further choreography left for the render to do.
|
|
275
304
|
*
|
|
276
305
|
* `label` is the optional callout text a scenario may attach (`highlight: { label: '...' }`).
|
|
277
306
|
* Absent means the spotlight alone, exactly what `highlight: true` (no object) asks for.
|
|
@@ -318,6 +347,14 @@ export type HighlightRect = z.infer<typeof HighlightRectSchema>;
|
|
|
318
347
|
*
|
|
319
348
|
* `source` distinguishes a synthesised clip from an author-supplied WAV, because they are the
|
|
320
349
|
* same bytes by the time they reach here and the distinction is not otherwise recoverable.
|
|
350
|
+
*
|
|
351
|
+
* `explain` is the third provenance: a cut-away scene's narration, synthesised by *plainmotion*
|
|
352
|
+
* at record time rather than by this project's own narrator. It never requires the `engine`
|
|
353
|
+
* block below, because that block describes one synthesiser — the recorder's kokoro worker —
|
|
354
|
+
* and these clips were made by the other one; plainmotion's identity is recorded where the
|
|
355
|
+
* rest of its contribution is, the manifest's toolchain block. The rule the refine below
|
|
356
|
+
* actually states is "every clip must be accounted for": `synth` is accounted for by `engine`,
|
|
357
|
+
* `file` by the author who supplied it, `explain` by the toolchain.
|
|
321
358
|
*/
|
|
322
359
|
export declare const SpeechClipSchema: z.ZodObject<{
|
|
323
360
|
id: z.ZodString;
|
|
@@ -325,9 +362,11 @@ export declare const SpeechClipSchema: z.ZodObject<{
|
|
|
325
362
|
atMs: z.ZodNumber;
|
|
326
363
|
durationMs: z.ZodNumber;
|
|
327
364
|
source: z.ZodEnum<{
|
|
365
|
+
explain: "explain";
|
|
328
366
|
file: "file";
|
|
329
367
|
synth: "synth";
|
|
330
368
|
}>;
|
|
369
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
331
370
|
}, z.core.$strip>;
|
|
332
371
|
/**
|
|
333
372
|
* What produced the synthesised clips. Evidence only — nothing reads it to make a decision,
|
|
@@ -369,9 +408,11 @@ export declare const SpeechSchema: z.ZodObject<{
|
|
|
369
408
|
atMs: z.ZodNumber;
|
|
370
409
|
durationMs: z.ZodNumber;
|
|
371
410
|
source: z.ZodEnum<{
|
|
411
|
+
explain: "explain";
|
|
372
412
|
file: "file";
|
|
373
413
|
synth: "synth";
|
|
374
414
|
}>;
|
|
415
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
375
416
|
}, z.core.$strip>>;
|
|
376
417
|
engine: z.ZodOptional<z.ZodObject<{
|
|
377
418
|
name: z.ZodString;
|
|
@@ -418,6 +459,54 @@ export type IntroInput = Omit<Intro, 'assPath'>;
|
|
|
418
459
|
* position in the array against the schema's own `captions/turn-[0-9]+\.ass` family.
|
|
419
460
|
*/
|
|
420
461
|
export type TransitionCardInput = Omit<TransitionCard, 'assPath'>;
|
|
462
|
+
/**
|
|
463
|
+
* The brand accent: one colour shared by the cursor's fill, its click-ripple ring and the
|
|
464
|
+
* highlight label's text. One field rather than one per consumer, because the cursor and
|
|
465
|
+
* the highlight should never disagree about the brand — the same "one block, one decision"
|
|
466
|
+
* shape as every other optional capability on the plan.
|
|
467
|
+
*
|
|
468
|
+
* Present only when a theme actually resolved — Pro tier, branding enabled, at least one
|
|
469
|
+
* colour named. Absent means every hardcoded colour the emitters already draw, so
|
|
470
|
+
* an unbranded run's `cursor.ass`/`highlight.ass` are exactly what they always were.
|
|
471
|
+
* Caption colours deliberately do not live here: `RenderPlanSchema.style` already carries
|
|
472
|
+
* `textColor`/`outlineColor`/`boxColor` as the single source of `captions/captions.ass`,
|
|
473
|
+
* and theming captions is a change to what gets frozen into those, not a second place a
|
|
474
|
+
* colour can hide.
|
|
475
|
+
*
|
|
476
|
+
* `accentColor` is `HEX_COLOUR` for the reason every plan colour is: the value reaches
|
|
477
|
+
* `assColour` and from there an ASS override tag, and `#RRGGBB` admits no ASS
|
|
478
|
+
* metacharacter. It is stored in the form the author wrote and converted to `&HAABBGGRR`
|
|
479
|
+
* only at emission, so the bundle shows the colour, not its encoding.
|
|
480
|
+
*
|
|
481
|
+
* Like the inputs above, this is the decision as the application layer supplies it — but
|
|
482
|
+
* it is the narrow end of a wider input: `buildRenderPlan` takes the four-field
|
|
483
|
+
* `ThemeInput` below and freezes only the accent here, because the caption colours have
|
|
484
|
+
* somewhere else to go.
|
|
485
|
+
*/
|
|
486
|
+
export declare const ThemeSchema: z.ZodObject<{
|
|
487
|
+
accentColor: z.ZodString;
|
|
488
|
+
}, z.core.$strip>;
|
|
489
|
+
export type Theme = z.infer<typeof ThemeSchema>;
|
|
490
|
+
/**
|
|
491
|
+
* The theme decision as the application layer supplies it — all four colours a licence can
|
|
492
|
+
* resolve, in the same shape `@plaintake/license`'s `resolveTheme` returns, exactly as
|
|
493
|
+
* `OutroInput`/`IntroInput` are the shapes its other resolvers return. Only the fields the
|
|
494
|
+
* user actually configured appear; each absent field keeps the hardcoded default at the
|
|
495
|
+
* freeze, so recolouring `accentColor` alone is a complete theme.
|
|
496
|
+
*
|
|
497
|
+
* Wider than `ThemeSchema` on purpose. The plan-level block carries only `accentColor`
|
|
498
|
+
* because the three caption colours freeze into `RenderPlanSchema.style` — the single
|
|
499
|
+
* source `captions.ass` already reads — while the accent has no style field to freeze
|
|
500
|
+
* into and needs the plan key. A theme with no accent therefore freezes no `theme` key at
|
|
501
|
+
* all: the schema requires one, and defaulting it would recolour cursor surfaces the
|
|
502
|
+
* author never asked about.
|
|
503
|
+
*/
|
|
504
|
+
export type ThemeInput = {
|
|
505
|
+
accentColor?: string | undefined;
|
|
506
|
+
captionTextColor?: string | undefined;
|
|
507
|
+
captionOutlineColor?: string | undefined;
|
|
508
|
+
captionBoxColor?: string | undefined;
|
|
509
|
+
};
|
|
421
510
|
/**
|
|
422
511
|
* The one value a plan's `schema` field ever holds.
|
|
423
512
|
*
|
|
@@ -611,9 +700,11 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
611
700
|
atMs: z.ZodNumber;
|
|
612
701
|
durationMs: z.ZodNumber;
|
|
613
702
|
source: z.ZodEnum<{
|
|
703
|
+
explain: "explain";
|
|
614
704
|
file: "file";
|
|
615
705
|
synth: "synth";
|
|
616
706
|
}>;
|
|
707
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
617
708
|
}, z.core.$strip>>;
|
|
618
709
|
engine: z.ZodOptional<z.ZodObject<{
|
|
619
710
|
name: z.ZodString;
|
|
@@ -622,6 +713,9 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
622
713
|
voice: z.ZodString;
|
|
623
714
|
}, z.core.$strip>>;
|
|
624
715
|
}, z.core.$strip>>;
|
|
716
|
+
theme: z.ZodOptional<z.ZodObject<{
|
|
717
|
+
accentColor: z.ZodString;
|
|
718
|
+
}, z.core.$strip>>;
|
|
625
719
|
actors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
626
720
|
id: z.ZodString;
|
|
627
721
|
label: z.ZodString;
|
|
@@ -642,6 +736,11 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
642
736
|
assPath: z.ZodString;
|
|
643
737
|
}, z.core.$strip>>>;
|
|
644
738
|
badgesAssPath: z.ZodOptional<z.ZodLiteral<"captions/badges.ass">>;
|
|
739
|
+
explainSegments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
740
|
+
id: z.ZodString;
|
|
741
|
+
segmentPath: z.ZodString;
|
|
742
|
+
durationMs: z.ZodNumber;
|
|
743
|
+
}, z.core.$strip>>>;
|
|
645
744
|
}, z.core.$strip>;
|
|
646
745
|
export type RenderPlan = z.infer<typeof RenderPlanSchema>;
|
|
647
746
|
export type Cue = z.infer<typeof CueSchema>;
|
package/dist/schema/results.d.ts
CHANGED
|
@@ -281,6 +281,37 @@ export declare const PruneCommandResultSchema: z.ZodObject<{
|
|
|
281
281
|
ok: z.ZodBoolean;
|
|
282
282
|
problems: z.ZodArray<z.ZodString>;
|
|
283
283
|
}, z.core.$strip>;
|
|
284
|
+
/**
|
|
285
|
+
* `import <trace.zip> --output <draft.demo.ts>`'s result. The trace is a Playwright
|
|
286
|
+
* recording from any suite, not necessarily PlainTake's own; the output is a *draft*
|
|
287
|
+
* scenario the author is expected to edit, and every field here exists to say how far
|
|
288
|
+
* short of finished it is.
|
|
289
|
+
*
|
|
290
|
+
* **`ok`/`problems` mean "a draft was written", nothing more.** A draft that does not
|
|
291
|
+
* parse yet is still a successful import — `validates: false` carries that verdict, the
|
|
292
|
+
* same way `diff`'s `ok` is deliberately untied from `identical`. The two honest failure
|
|
293
|
+
* classes are an unreadable or corrupt zip (exit 6, `diff`'s code for "artifacts could
|
|
294
|
+
* not be read back out") and a readable trace with no importable action in it (exit 1 —
|
|
295
|
+
* the content failed, not the container).
|
|
296
|
+
*
|
|
297
|
+
* `warnings` is deliberately not `problems`: multi-page traces, redacted values,
|
|
298
|
+
* unimported methods and the standing review-for-secrets note are things the author must
|
|
299
|
+
* know to act on, not faults that made the command lie about having run. Every result
|
|
300
|
+
* carries at least one warning — "the trace records everything typed, review before
|
|
301
|
+
* committing" — because a draft that silently embedded a typed password would be worse
|
|
302
|
+
* than one that loudly asks to be read.
|
|
303
|
+
*/
|
|
304
|
+
export declare const ImportCommandResultSchema: z.ZodObject<{
|
|
305
|
+
tracePath: z.ZodString;
|
|
306
|
+
outputPath: z.ZodString;
|
|
307
|
+
stepCount: z.ZodNumber;
|
|
308
|
+
validates: z.ZodBoolean;
|
|
309
|
+
warnings: z.ZodArray<z.ZodString>;
|
|
310
|
+
schema: z.ZodLiteral<"agent-demo.result/v1">;
|
|
311
|
+
kind: z.ZodLiteral<"import">;
|
|
312
|
+
ok: z.ZodBoolean;
|
|
313
|
+
problems: z.ZodArray<z.ZodString>;
|
|
314
|
+
}, z.core.$strip>;
|
|
284
315
|
export declare const InspectResultSchema: z.ZodObject<{
|
|
285
316
|
bundleDir: z.ZodString;
|
|
286
317
|
status: z.ZodEnum<{
|
|
@@ -332,6 +363,10 @@ export declare const InspectResultSchema: z.ZodObject<{
|
|
|
332
363
|
labels: z.ZodArray<z.ZodString>;
|
|
333
364
|
turnCount: z.ZodNumber;
|
|
334
365
|
}, z.core.$strip>>;
|
|
366
|
+
explainScenes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
367
|
+
id: z.ZodString;
|
|
368
|
+
durationMs: z.ZodNumber;
|
|
369
|
+
}, z.core.$strip>>>;
|
|
335
370
|
outputs: z.ZodArray<z.ZodObject<{
|
|
336
371
|
path: z.ZodString;
|
|
337
372
|
sha256: z.ZodString;
|
|
@@ -365,6 +400,11 @@ export declare const DoctorResultSchema: z.ZodObject<{
|
|
|
365
400
|
voices: z.ZodArray<z.ZodString>;
|
|
366
401
|
notes: z.ZodArray<z.ZodString>;
|
|
367
402
|
}, z.core.$strip>;
|
|
403
|
+
plainmotion: z.ZodObject<{
|
|
404
|
+
installed: z.ZodBoolean;
|
|
405
|
+
version: z.ZodOptional<z.ZodString>;
|
|
406
|
+
note: z.ZodOptional<z.ZodString>;
|
|
407
|
+
}, z.core.$strip>;
|
|
368
408
|
schema: z.ZodLiteral<"agent-demo.result/v1">;
|
|
369
409
|
kind: z.ZodLiteral<"doctor">;
|
|
370
410
|
ok: z.ZodBoolean;
|
|
@@ -418,11 +458,12 @@ export type RenderCommandResult = z.infer<typeof RenderCommandResultSchema>;
|
|
|
418
458
|
export type VerificationReport = z.infer<typeof VerificationReportSchema>;
|
|
419
459
|
export type DiffCommandResult = z.infer<typeof DiffCommandResultSchema>;
|
|
420
460
|
export type PruneCommandResult = z.infer<typeof PruneCommandResultSchema>;
|
|
461
|
+
export type ImportCommandResult = z.infer<typeof ImportCommandResultSchema>;
|
|
421
462
|
export type InspectResult = z.infer<typeof InspectResultSchema>;
|
|
422
463
|
export type DoctorResult = z.infer<typeof DoctorResultSchema>;
|
|
423
464
|
export type ActivateResult = z.infer<typeof ActivateResultSchema>;
|
|
424
465
|
export type LicenceResult = z.infer<typeof LicenceResultSchema>;
|
|
425
|
-
export type DemoResult = ValidationResult | RunCommandResult | CheckCommandResult | RenderCommandResult | VerificationReport | DiffCommandResult | PruneCommandResult | InspectResult | DoctorResult | ActivateResult | LicenceResult;
|
|
466
|
+
export type DemoResult = ValidationResult | RunCommandResult | CheckCommandResult | RenderCommandResult | VerificationReport | DiffCommandResult | PruneCommandResult | ImportCommandResult | InspectResult | DoctorResult | ActivateResult | LicenceResult;
|
|
426
467
|
/**
|
|
427
468
|
* Renders a filesystem location for display in a result, relative to `root` when it
|
|
428
469
|
* lies inside it.
|
|
@@ -52,20 +52,15 @@ export declare const ScenarioCameraSchema: z.ZodObject<{
|
|
|
52
52
|
}, z.core.$strip>;
|
|
53
53
|
export type ScenarioCamera = z.infer<typeof ScenarioCameraSchema>;
|
|
54
54
|
/**
|
|
55
|
-
* Scenario-level narration
|
|
56
|
-
*
|
|
57
|
-
* `docs/release-checklist.md`'s "no speed, no pitch, no
|
|
58
|
-
* and no SSML" note, of which this closes exactly the first item and nothing else. A per-step
|
|
59
|
-
* override would mean preloading more than one voice at once; `engine.ts` measured a single
|
|
60
|
-
* voice at ~500 MB resident, rising to ~750 MB with a second loaded alongside it — roughly
|
|
61
|
-
* +250 MB per *additional* voice, not 500 MB multiplied by the voice count — a real
|
|
62
|
-
* architecture change this is deliberately not taking on.
|
|
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.
|
|
63
58
|
*
|
|
64
|
-
* Nested under `speech` rather than
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
59
|
+
* Nested under `speech` rather than bare top-level fields: it names what is being tuned (the
|
|
60
|
+
* narration) apart from the DSL's other top-level concerns, matching how `camera` groups
|
|
61
|
+
* framing rather than adding `maxZoom` directly to the metadata object, and it leaves room to
|
|
62
|
+
* grow if narration ever gains another scenario-level knob without another unrelated top-level
|
|
63
|
+
* field appearing beside it.
|
|
69
64
|
*
|
|
70
65
|
* `speed` has no measured bound anywhere in this codebase — grepped before picking one: only a
|
|
71
66
|
* hardcoded `speed: 1` default and one `speed: 1.1` in a cache test existed before this field
|
|
@@ -77,6 +72,7 @@ export type ScenarioCamera = z.infer<typeof ScenarioCameraSchema>;
|
|
|
77
72
|
*/
|
|
78
73
|
export declare const ScenarioSpeechSchema: z.ZodObject<{
|
|
79
74
|
speed: z.ZodOptional<z.ZodNumber>;
|
|
75
|
+
voices: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
80
76
|
}, z.core.$strip>;
|
|
81
77
|
export type ScenarioSpeech = z.infer<typeof ScenarioSpeechSchema>;
|
|
82
78
|
export declare const ScenarioMetaSchema: z.ZodObject<{
|
|
@@ -113,6 +109,7 @@ export declare const ScenarioMetaSchema: z.ZodObject<{
|
|
|
113
109
|
}, z.core.$strip>>;
|
|
114
110
|
speech: z.ZodOptional<z.ZodObject<{
|
|
115
111
|
speed: z.ZodOptional<z.ZodNumber>;
|
|
112
|
+
voices: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
116
113
|
}, z.core.$strip>>;
|
|
117
114
|
pronunciations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
118
115
|
}, z.core.$strip>;
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ export type DemoStep = {
|
|
|
33
33
|
highlight?: boolean | {
|
|
34
34
|
label?: string;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Speaks this step's subtitle with a different voice than the run-wide default `--voice`
|
|
38
|
+
* picks. The voice must be one this run declared: either the default, or an entry in the
|
|
39
|
+
* scenario metadata's `speech.voices` list — anything else is refused at record time with
|
|
40
|
+
* the declaration named as the fix, rather than surfacing as an unsupported voice minutes
|
|
41
|
+
* into a recording.
|
|
42
|
+
*
|
|
43
|
+
* Precedence is step over actor over default: `demo.actor(id, { voice })` sets the voice
|
|
44
|
+
* for that actor's every step, and this field overrides it for one step alone. Applies
|
|
45
|
+
* only to synthesised narration (`--speech on`): an author-supplied WAV (`--speech file`,
|
|
46
|
+
* or a file in `narration/` that shadows this step) is already recorded audio, and no voice
|
|
47
|
+
* changes it.
|
|
48
|
+
*/
|
|
49
|
+
voice?: string;
|
|
36
50
|
/** Extra presentation hold after the action completes, in whole milliseconds. */
|
|
37
51
|
holdMs?: number;
|
|
38
52
|
run: () => Promise<unknown>;
|
|
@@ -125,6 +139,142 @@ export type DemoHandoff = PreflightHandoff & {
|
|
|
125
139
|
*/
|
|
126
140
|
mask?: string;
|
|
127
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* A node in a `process` explain scene: a step in the flow being drawn. The `id` is the
|
|
144
|
+
* entity a cue's `target` names, so it is authored and stable — never generated.
|
|
145
|
+
*/
|
|
146
|
+
export type ExplainNode = {
|
|
147
|
+
id: string;
|
|
148
|
+
label: string;
|
|
149
|
+
};
|
|
150
|
+
/** A directed edge between two `process` nodes, drawn as the flow it names. */
|
|
151
|
+
export type ExplainConnection = {
|
|
152
|
+
id: string;
|
|
153
|
+
from: string;
|
|
154
|
+
to: string;
|
|
155
|
+
label?: string;
|
|
156
|
+
};
|
|
157
|
+
/** One side of a `comparison` explain scene: what this alternative is called, and its points. */
|
|
158
|
+
export type ExplainColumn = {
|
|
159
|
+
heading: string;
|
|
160
|
+
points: string[];
|
|
161
|
+
};
|
|
162
|
+
/** A named group of imported svg parts a `diagram` scene's cue can target as one thing. */
|
|
163
|
+
export type ExplainRegion = {
|
|
164
|
+
id: string;
|
|
165
|
+
of: string[];
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* The five motion-graphics scenes an explain cut-away can be, mirroring PlainMotion's scene
|
|
169
|
+
* registry prop-for-prop (`title`/`process`/`comparison`/`diagram`/`recap`). PlainTake does
|
|
170
|
+
* not draw these — it compiles each one through the `plainmotion` CLI and splices the frozen
|
|
171
|
+
* result into the recording — so this union is a structural mirror of PlainMotion's own
|
|
172
|
+
* schemas, and the deep validation (character caps, list bounds, phrase resolution) belongs
|
|
173
|
+
* to PlainMotion at run time, not to this package's types. What `validate` checks without a
|
|
174
|
+
* browser is the shape around the scene: id, narration, a known `type`.
|
|
175
|
+
*
|
|
176
|
+
* Geometry is deliberately absent. Where a pixel appears is the scene layout's decision,
|
|
177
|
+
* exactly as it is in a `plainmotion.yaml`; an author says what exists and what the
|
|
178
|
+
* narration points at, and nothing about inches.
|
|
179
|
+
*/
|
|
180
|
+
export type ExplainScene = {
|
|
181
|
+
type: 'title';
|
|
182
|
+
headline: string;
|
|
183
|
+
subtitle?: string;
|
|
184
|
+
} | {
|
|
185
|
+
type: 'process';
|
|
186
|
+
/** Defaults to `horizontal`. */
|
|
187
|
+
direction?: 'horizontal' | 'vertical';
|
|
188
|
+
/** Two to five nodes. */
|
|
189
|
+
nodes: ExplainNode[];
|
|
190
|
+
connections?: ExplainConnection[];
|
|
191
|
+
/** A line above the row, naming the flow. */
|
|
192
|
+
title?: string;
|
|
193
|
+
/** A supporting line beneath the row. */
|
|
194
|
+
caption?: string;
|
|
195
|
+
} | {
|
|
196
|
+
type: 'comparison';
|
|
197
|
+
title?: string;
|
|
198
|
+
left: ExplainColumn;
|
|
199
|
+
right: ExplainColumn;
|
|
200
|
+
} | {
|
|
201
|
+
type: 'diagram';
|
|
202
|
+
/** Path to an svg, relative to the scenario file. Imported as-drawn; only annotated here. */
|
|
203
|
+
src: string;
|
|
204
|
+
title?: string;
|
|
205
|
+
caption?: string;
|
|
206
|
+
regions?: ExplainRegion[];
|
|
207
|
+
} | {
|
|
208
|
+
type: 'recap';
|
|
209
|
+
title?: string;
|
|
210
|
+
items: string[];
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* What a cue does to the viewer's attention. The same vocabulary PlainMotion defines —
|
|
214
|
+
* deliberately small, and deliberately not easing curves or tween internals: an author says
|
|
215
|
+
* what should happen; how that is expressed is the renderer's job.
|
|
216
|
+
*/
|
|
217
|
+
export type ExplainCueAction = 'reveal' | 'hide' | 'emphasize' | 'deemphasize' | 'draw' | 'focus';
|
|
218
|
+
/**
|
|
219
|
+
* When a cue fires: bound to words in the narration, resolved against the timings the speech
|
|
220
|
+
* model measures. Rewriting a sentence moves the animation with it instead of silently
|
|
221
|
+
* desynchronising — which is why there is no numeric `at` offset here the way PlainMotion's
|
|
222
|
+
* own cue schema allows one. An explain scene always speaks (its narration is the scene's
|
|
223
|
+
* clock), so a phrase is the only anchor that survives a rewording, and an offset that could
|
|
224
|
+
* not is not offered.
|
|
225
|
+
*
|
|
226
|
+
* A phrase that does not occur in the narration is a validation error at run time, never a
|
|
227
|
+
* no-op.
|
|
228
|
+
*/
|
|
229
|
+
export type ExplainCue = {
|
|
230
|
+
atPhrase: string;
|
|
231
|
+
action: ExplainCueAction;
|
|
232
|
+
target: string;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* A full-frame motion-graphics cut-away in the middle of a browser demo — "explain while
|
|
236
|
+
* demoing". `demo.explain()` stops the screencast, splices in a PlainMotion-rendered scene
|
|
237
|
+
* (`title`/`process`/`comparison`/`diagram`/`recap`), and resumes the recording on the same
|
|
238
|
+
* page: the cut-away is a boundary on the timeline, the same mechanism a multi-actor `turn`
|
|
239
|
+
* uses, so nothing about the recording's determinism or re-renderability changes. The scene
|
|
240
|
+
* is compiled and rendered after capture finishes, from a generated one-scene project — the
|
|
241
|
+
* only inputs that matter are the ones right here.
|
|
242
|
+
*
|
|
243
|
+
* Free on every tier, like every authoring verb. Requires the `plainmotion` CLI on `PATH`
|
|
244
|
+
* at record time (`plaintake doctor` reports whether it is there); a bundle that carries an
|
|
245
|
+
* explain segment re-renders without it, the same way it re-renders without a browser.
|
|
246
|
+
*/
|
|
247
|
+
export type DemoExplain = {
|
|
248
|
+
/**
|
|
249
|
+
* Stable, authored — the same rule every other id here follows. It becomes the directory
|
|
250
|
+
* the frozen artifacts live in (`explain/<id>/segment.mp4` and its plainmotion plan) and
|
|
251
|
+
* the generated scene's own id, so it must read as plainmotion ids do: lower-case
|
|
252
|
+
* alphanumeric and hyphens, starting alphanumeric. Checked at run time, with the offending
|
|
253
|
+
* id named.
|
|
254
|
+
*/
|
|
255
|
+
id: string;
|
|
256
|
+
/** Human label for the cut-away, shown in `inspect` and diagnostics. Not drawn in the video. */
|
|
257
|
+
title: string;
|
|
258
|
+
/**
|
|
259
|
+
* What the scene says. The source of truth for both the audio and the caption — there is
|
|
260
|
+
* no separate caption field, for the same reason PlainMotion has none: two strings that
|
|
261
|
+
* are supposed to say the same thing will eventually disagree, and the one the viewer
|
|
262
|
+
* hears is the one that matters. The measured speech is also the scene's duration: the
|
|
263
|
+
* cut-away lasts exactly as long as it takes to say this, plus its lead-in and lead-out.
|
|
264
|
+
*/
|
|
265
|
+
narration: string;
|
|
266
|
+
/** Which motion-graphics scene to draw, and its content. See `ExplainScene`. */
|
|
267
|
+
scene: ExplainScene;
|
|
268
|
+
/** Optional animation cues, bound to phrases of `narration`. See `ExplainCue`. */
|
|
269
|
+
cues?: ExplainCue[];
|
|
270
|
+
/**
|
|
271
|
+
* Speaks this scene's narration with a different voice than the run-wide default `--voice`
|
|
272
|
+
* picks — the same contract as `DemoStep.voice`: the voice must be one this run declared
|
|
273
|
+
* (the default, or an entry in `speech.voices`), or the run is refused with the fix named.
|
|
274
|
+
* Applies only when the engine is doing the speaking (`--speech on`).
|
|
275
|
+
*/
|
|
276
|
+
voice?: string;
|
|
277
|
+
};
|
|
128
278
|
/**
|
|
129
279
|
* Presentation fields shared by every `Actor` verb below — the same subset of `DemoStep`
|
|
130
280
|
* that is still the caller's to supply once `target` and `action` no longer are: a verb's
|
|
@@ -154,6 +304,8 @@ export type ActorStepMeta = {
|
|
|
154
304
|
highlight?: boolean | {
|
|
155
305
|
label?: string;
|
|
156
306
|
};
|
|
307
|
+
/** See `DemoStep.voice` — one verb-call override, ahead of this actor's own `voice`. */
|
|
308
|
+
voice?: string;
|
|
157
309
|
};
|
|
158
310
|
/**
|
|
159
311
|
* A named participant in a multi-actor demo: its own Playwright `BrowserContext`, its own
|
|
@@ -287,10 +439,17 @@ export interface DemoContext extends PreflightContext {
|
|
|
287
439
|
* special-casing the first one: a single-actor scenario written before actors existed keeps
|
|
288
440
|
* working unchanged, and turning it into a two-actor one only costs the second
|
|
289
441
|
* `demo.actor()` call, not a rewrite of the first actor's steps.
|
|
442
|
+
*
|
|
443
|
+
* `voice` is the narration half of giving an actor an identity: every step this actor's
|
|
444
|
+
* verbs record speaks with it, unless the step itself names a different one (see
|
|
445
|
+
* `DemoStep.voice` for precedence and the `speech.voices` declaration requirement). Like
|
|
446
|
+
* the first call's other options, it is the one that counts — a second `demo.actor` with
|
|
447
|
+
* the same id returns the existing actor unchanged.
|
|
290
448
|
*/
|
|
291
449
|
actor(id: string, opts?: {
|
|
292
450
|
label?: string;
|
|
293
451
|
contextOptions?: BrowserContextOptions;
|
|
452
|
+
voice?: string;
|
|
294
453
|
}): Promise<Actor>;
|
|
295
454
|
/**
|
|
296
455
|
* Marks a hand-off from whichever actor currently holds the browser to `actor`, parking the
|
|
@@ -315,6 +474,25 @@ export interface DemoContext extends PreflightContext {
|
|
|
315
474
|
durationMs?: number;
|
|
316
475
|
};
|
|
317
476
|
}): Promise<void>;
|
|
477
|
+
/**
|
|
478
|
+
* Cuts away from the browser recording to a full-frame motion-graphics scene — a diagram
|
|
479
|
+
* of where the key lives, a process flow, a recap — then back to the same page, same
|
|
480
|
+
* actor, same state. The screencast stops at the boundary and resumes after it, exactly as
|
|
481
|
+
* a `turn` does minus the actor switch, so the cut-away occupies its own slot on the
|
|
482
|
+
* timeline and nothing it covers is ever captured.
|
|
483
|
+
*
|
|
484
|
+
* The scene is not drawn live: it is compiled and rendered by the `plainmotion` CLI after
|
|
485
|
+
* capture finishes, from the request's own fields, and spliced in at render time as a
|
|
486
|
+
* frozen 1920×1080@30 segment — which is what keeps the bundle re-renderable without
|
|
487
|
+
* `plainmotion`, without a browser, and byte-identically. Its narration is spoken by the
|
|
488
|
+
* same engine as the rest of the recording (`--speech on`), lands in the same caption
|
|
489
|
+
* track, and is the scene's clock: the cut-away lasts exactly as long as it takes to say.
|
|
490
|
+
*
|
|
491
|
+
* Like a `turn`, an explain is a boundary, never an excision — nothing around it is cut —
|
|
492
|
+
* and it does not create a chapter; call `demo.chapter()` where you want one. Free on
|
|
493
|
+
* every tier.
|
|
494
|
+
*/
|
|
495
|
+
explain(request: DemoExplain): Promise<void>;
|
|
318
496
|
}
|
|
319
497
|
export type DemoRunArgs = {
|
|
320
498
|
page: Page;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plaintake/scenario",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|