@plaintake/scenario 1.6.0 → 1.9.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 +283 -10
- package/dist/schema/event.d.ts +5 -0
- package/dist/schema/manifest.d.ts +1 -0
- package/dist/schema/render-plan.d.ts +145 -0
- package/dist/schema/results.d.ts +48 -1
- package/dist/schema/scenario.d.ts +10 -13
- package/dist/types.d.ts +344 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,27 @@ var DemoEventTypeSchema = z.enum([
|
|
|
42
42
|
* Appended once, when the wait ends, so `tEndNs` is a real reading rather than a
|
|
43
43
|
* placeholder: the `assertion.pass` shape, not the `step.start`/`step.finish` pair.
|
|
44
44
|
*/
|
|
45
|
-
"human.wait"
|
|
45
|
+
"human.wait",
|
|
46
|
+
/*
|
|
47
|
+
* An actor hand-off: `demo.turn(actor, ...)` parking the outgoing actor's `BrowserContext`
|
|
48
|
+
* and activating the incoming one. A member of its own rather than folded into
|
|
49
|
+
* `step.start`/`step.finish`, because a hand-off is not a step — it has no `target` and
|
|
50
|
+
* narrates no action of its own. Verb steps taken during a turn keep using
|
|
51
|
+
* `step.start`/`step.finish` exactly as before; the `actor` field below records which
|
|
52
|
+
* actor performed the step.
|
|
53
|
+
*/
|
|
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"
|
|
46
66
|
]);
|
|
47
67
|
var TargetSchema = z.object({
|
|
48
68
|
role: z.string().optional(),
|
|
@@ -60,7 +80,15 @@ var DemoEventSchema = z.object({
|
|
|
60
80
|
title: z.string().optional(),
|
|
61
81
|
subtitle: z.string().optional(),
|
|
62
82
|
target: TargetSchema.optional(),
|
|
63
|
-
payload: z.record(z.string(), z.unknown()).optional()
|
|
83
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
84
|
+
/**
|
|
85
|
+
* Which actor performed this event, for a `turn` hand-off or a verb step taken during one.
|
|
86
|
+
* Absent means the scenario's default actor — a single-actor event log never sets this, so
|
|
87
|
+
* it stays byte-identical to every log recorded before actors existed. No `.default()`
|
|
88
|
+
* here: the default actor's id is an application-layer notion the recorder resolves, not a
|
|
89
|
+
* value this schema should invent and materialise on parse.
|
|
90
|
+
*/
|
|
91
|
+
actor: z.string().min(1).optional()
|
|
64
92
|
});
|
|
65
93
|
|
|
66
94
|
// ../schema/src/json-schema.ts
|
|
@@ -80,7 +108,34 @@ var ScenarioCameraSchema = z2.object({
|
|
|
80
108
|
minDwellMs: z2.number().int().min(0, "camera.minDwellMs must be at least 0").max(5e3, "camera.minDwellMs must be at most 5000ms").optional()
|
|
81
109
|
});
|
|
82
110
|
var ScenarioSpeechSchema = z2.object({
|
|
83
|
-
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()
|
|
84
139
|
});
|
|
85
140
|
var ScenarioMetaSchema = z2.object({
|
|
86
141
|
schema: z2.literal("agent-demo.scenario/v1"),
|
|
@@ -190,7 +245,14 @@ var BundleManifestSchema = z4.object({
|
|
|
190
245
|
/** May be the literal "unknown" — never a fabricated version. */
|
|
191
246
|
libass: z4.string(),
|
|
192
247
|
fontSha256: Sha256,
|
|
193
|
-
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()
|
|
194
256
|
}),
|
|
195
257
|
environment: z4.object({
|
|
196
258
|
os: z4.string(),
|
|
@@ -349,6 +411,30 @@ var CameraSchema = z5.object({
|
|
|
349
411
|
path: ["shots"]
|
|
350
412
|
}
|
|
351
413
|
);
|
|
414
|
+
var ActorSchema = z5.object({
|
|
415
|
+
id: z5.string().min(1),
|
|
416
|
+
label: z5.string().min(1)
|
|
417
|
+
});
|
|
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()
|
|
423
|
+
});
|
|
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),
|
|
429
|
+
backgroundColor: HEX_COLOUR,
|
|
430
|
+
textColor: HEX_COLOUR,
|
|
431
|
+
assPath: z5.string().regex(/^captions\/turn-[0-9]+\.ass$/)
|
|
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
|
+
});
|
|
352
438
|
var HighlightRectSchema = z5.object({
|
|
353
439
|
id: z5.string().min(1),
|
|
354
440
|
x: z5.number().int().min(0),
|
|
@@ -396,7 +482,23 @@ var SpeechClipSchema = z5.object({
|
|
|
396
482
|
path: z5.string().regex(/^speech\/clips\/[A-Za-z0-9][A-Za-z0-9._-]*\.wav$/),
|
|
397
483
|
atMs: z5.number().int().nonnegative(),
|
|
398
484
|
durationMs: z5.number().int().positive(),
|
|
399
|
-
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()
|
|
400
502
|
});
|
|
401
503
|
var SpeechEngineSchema = z5.object({
|
|
402
504
|
name: z5.string().min(1),
|
|
@@ -432,10 +534,12 @@ var SpeechSchema = z5.object({
|
|
|
432
534
|
message: "clips must have unique ids and be laid out in order without overlapping",
|
|
433
535
|
path: ["clips"]
|
|
434
536
|
}
|
|
435
|
-
).refine(({ clips, engine }) => engine !== void 0 || clips.every((clip) => clip.source
|
|
537
|
+
).refine(({ clips, engine }) => engine !== void 0 || clips.every((clip) => clip.source !== "synth"), {
|
|
436
538
|
// A synthesised clip with no record of what synthesised it is the one state this block
|
|
437
539
|
// exists to prevent. The converse is fine: an engine recorded on an all-file track is
|
|
438
|
-
// 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.
|
|
439
543
|
message: "engine must be recorded whenever any clip was synthesised",
|
|
440
544
|
path: ["engine"]
|
|
441
545
|
});
|
|
@@ -625,8 +729,103 @@ var RenderPlanSchema = z5.object({
|
|
|
625
729
|
cursor: CursorSchema.optional(),
|
|
626
730
|
camera: CameraSchema.optional(),
|
|
627
731
|
highlight: HighlightSchema.optional(),
|
|
628
|
-
speech: SpeechSchema.optional()
|
|
629
|
-
|
|
732
|
+
speech: SpeechSchema.optional(),
|
|
733
|
+
/**
|
|
734
|
+
* The capture timeline cut into windows — of a genuine multi-actor cast, of an implicit
|
|
735
|
+
* single actor pausing for explain cut-aways, or both at once. `segments` is the one
|
|
736
|
+
* field of the five below that a segmented capture *always* carries; the other four are
|
|
737
|
+
* two independent, narrower capabilities layered on top of it:
|
|
738
|
+
*
|
|
739
|
+
* - `actors`/`transitionCards`/`badgesAssPath` — the multi-actor demo's cast, the cards
|
|
740
|
+
* drawn at each hand-off, and the badge track naming the active actor throughout. Three
|
|
741
|
+
* fields that carry one capability and so, like every other optional block above, are
|
|
742
|
+
* present or absent together (the first refinement below) — and never without
|
|
743
|
+
* `segments`, which they reference by `actorId` (the second). Absent means no genuine
|
|
744
|
+
* cast: every explain-only demo, and every plan before multi-actor demos existed.
|
|
745
|
+
* - `explainSegments` — the frozen scenes cut into the gaps between `segments`, present
|
|
746
|
+
* whenever a scenario called `demo.explain()` at all, with or without a cast alongside
|
|
747
|
+
* it — never without `segments` either (the third refinement), for the same reason.
|
|
748
|
+
*
|
|
749
|
+
* Every gap between adjacent `segments` is claimed by exactly one occupant, a card or an
|
|
750
|
+
* explain scene, never both and never neither (the fifth refinement) — which is also why
|
|
751
|
+
* `segments` can appear with `transitionCards` and `explainSegments` both absent only when
|
|
752
|
+
* `segments.length === 1` never occurs (`SegmentSchema`'s own `.min(2)` on the array rules
|
|
753
|
+
* it out): a lone, unsegmented capture has no gap to claim in the first place, and needs
|
|
754
|
+
* none of these five fields at all.
|
|
755
|
+
*/
|
|
756
|
+
actors: z5.array(ActorSchema).min(2).optional(),
|
|
757
|
+
segments: z5.array(SegmentSchema).min(2).optional(),
|
|
758
|
+
transitionCards: z5.array(TransitionCardSchema).min(1).optional(),
|
|
759
|
+
badgesAssPath: z5.literal("captions/badges.ass").optional(),
|
|
760
|
+
explainSegments: z5.array(ExplainSegmentPlanSchema).min(1).optional()
|
|
761
|
+
}).refine(
|
|
762
|
+
(plan) => {
|
|
763
|
+
const castFields = [plan.actors, plan.transitionCards, plan.badgesAssPath];
|
|
764
|
+
return castFields.every((field) => field !== void 0) || castFields.every((field) => field === void 0);
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
// The same "carries its capability" discipline every optional block above already
|
|
768
|
+
// follows: a plan naming actors with nowhere to draw their badges (or vice versa)
|
|
769
|
+
// cannot render, so it is refused at the parse rather than discovered mid-encode.
|
|
770
|
+
message: "actors, transitionCards and badgesAssPath must be present together or absent together",
|
|
771
|
+
path: ["actors"]
|
|
772
|
+
}
|
|
773
|
+
).refine((plan) => plan.actors === void 0 || plan.segments !== void 0, {
|
|
774
|
+
// A cast with nowhere to draw its own windows — `segment.actorId` is what a badge or a
|
|
775
|
+
// transition card is drawn against — cannot render.
|
|
776
|
+
message: "actors requires segments \u2014 a cast with no capture windows of its own cannot render",
|
|
777
|
+
path: ["actors"]
|
|
778
|
+
}).refine((plan) => plan.explainSegments === void 0 || plan.segments !== void 0, {
|
|
779
|
+
// An explain scene occupies a gap between two segments; with no segments there is no
|
|
780
|
+
// gap for it to occupy.
|
|
781
|
+
message: "explainSegments requires segments \u2014 an explain scene with no gap to occupy cannot render",
|
|
782
|
+
path: ["explainSegments"]
|
|
783
|
+
}).refine(
|
|
784
|
+
(plan) => {
|
|
785
|
+
if (plan.segments === void 0) return true;
|
|
786
|
+
const { segments } = plan;
|
|
787
|
+
return segments[0]?.startMs === 0 && segments.every(
|
|
788
|
+
(segment, index) => segment.endMs > segment.startMs && (index === 0 || segments[index - 1]?.endMs === segment.startMs)
|
|
789
|
+
);
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
// Mirrors `ChaptersSchema`'s own tiling refine: segments must start at 0 and tile the
|
|
793
|
+
// source contiguously, the same reason a gap or overlap in chapter marks is refused
|
|
794
|
+
// rather than tolerated — true of every segmented capture, cast or castless alike.
|
|
795
|
+
message: "segments must tile raw/session.webm contiguously from 0",
|
|
796
|
+
path: ["segments"]
|
|
797
|
+
}
|
|
798
|
+
).refine(
|
|
799
|
+
(plan) => {
|
|
800
|
+
if (plan.segments === void 0) return true;
|
|
801
|
+
const gapCount = (plan.transitionCards?.length ?? 0) + (plan.explainSegments?.length ?? 0);
|
|
802
|
+
return gapCount === plan.segments.length - 1;
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
// The count a hand-off or an explain between each pair of adjacent segments implies —
|
|
806
|
+
// every gap claimed by exactly one occupant, restated here in one refine because
|
|
807
|
+
// `transitionCards` and `explainSegments` share the gaps between the very same
|
|
808
|
+
// `segments` array (see `ExplainSegmentPlanSchema`'s own doc on how the renderer tells
|
|
809
|
+
// the two apart without either array carrying an explicit gap index).
|
|
810
|
+
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",
|
|
811
|
+
path: ["segments"]
|
|
812
|
+
}
|
|
813
|
+
).refine(
|
|
814
|
+
(plan) => {
|
|
815
|
+
if (plan.actors === void 0 || plan.segments === void 0 || plan.transitionCards === void 0) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
const { actors, segments, transitionCards } = plan;
|
|
819
|
+
const namesDeclaredActor = (actorId) => actors.some((actor) => actor.id === actorId);
|
|
820
|
+
return segments.every((segment) => namesDeclaredActor(segment.actorId)) && transitionCards.every(
|
|
821
|
+
(card) => namesDeclaredActor(card.fromActorId) && namesDeclaredActor(card.toActorId)
|
|
822
|
+
);
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
message: "every segment.actorId, transitionCard.fromActorId and transitionCard.toActorId must name a declared actor",
|
|
826
|
+
path: ["segments"]
|
|
827
|
+
}
|
|
828
|
+
);
|
|
630
829
|
|
|
631
830
|
// ../schema/src/results.ts
|
|
632
831
|
import { z as z6 } from "zod";
|
|
@@ -760,7 +959,7 @@ var VerificationReportSchema = z6.object({
|
|
|
760
959
|
bundleDir: DISPLAY_PATH,
|
|
761
960
|
artifactCount: z6.number().int().nonnegative()
|
|
762
961
|
});
|
|
763
|
-
var DifferenceCategorySchema = z6.enum(["step", "assertion", "target", "timing", "caption"]);
|
|
962
|
+
var DifferenceCategorySchema = z6.enum(["step", "assertion", "target", "timing", "caption", "actor"]);
|
|
764
963
|
var DiffCommandResultSchema = z6.object({
|
|
765
964
|
...envelope("diff"),
|
|
766
965
|
bundleA: DISPLAY_PATH,
|
|
@@ -787,6 +986,19 @@ var PruneCommandResultSchema = z6.object({
|
|
|
787
986
|
* be reclaimed if every candidate succeeded. */
|
|
788
987
|
bytesReclaimed: z6.number().int().nonnegative()
|
|
789
988
|
});
|
|
989
|
+
var ImportCommandResultSchema = z6.object({
|
|
990
|
+
...envelope("import"),
|
|
991
|
+
tracePath: DISPLAY_PATH,
|
|
992
|
+
outputPath: DISPLAY_PATH,
|
|
993
|
+
/** How many `demo.step` calls the draft carries. Zero only on the failure path. */
|
|
994
|
+
stepCount: z6.number().int().nonnegative(),
|
|
995
|
+
/** Whether the written draft already passes `plaintake validate`. Best-effort: a draft
|
|
996
|
+
* that fails is still written, still a success, and its problems are warnings here. */
|
|
997
|
+
validates: z6.boolean(),
|
|
998
|
+
/** Author-facing advice: multi-page traces, redactions, unimported trace calls, and the
|
|
999
|
+
* standing review-for-secrets line. Never empty on success. */
|
|
1000
|
+
warnings: z6.array(z6.string())
|
|
1001
|
+
});
|
|
790
1002
|
var InspectResultSchema = z6.object({
|
|
791
1003
|
...envelope("inspect"),
|
|
792
1004
|
bundleDir: DISPLAY_PATH,
|
|
@@ -846,6 +1058,51 @@ var InspectResultSchema = z6.object({
|
|
|
846
1058
|
suppliedCount: z6.number().int().nonnegative(),
|
|
847
1059
|
voice: z6.string().optional()
|
|
848
1060
|
}).optional(),
|
|
1061
|
+
/**
|
|
1062
|
+
* The multi-actor demo's cast, or absent for a single-actor bundle — every bundle
|
|
1063
|
+
* recorded before actors existed, and still the default for most scenarios after, so
|
|
1064
|
+
* this stays absent rather than a zero-cast object, the same distinction `narration`
|
|
1065
|
+
* above draws between "silent" and "nothing to report".
|
|
1066
|
+
*
|
|
1067
|
+
* Sourced from the frozen plan's `actors`/`segments` fields, never the scenario file:
|
|
1068
|
+
* `inspectCommand` (`apps/cli/src/commands.ts`) reads only the render plan and manifest,
|
|
1069
|
+
* so a bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
1070
|
+
* Not sourced from `plan.transitionCards`, even though `transitionCards.length` and
|
|
1071
|
+
* `segments.length - 1` are provably equal by the render plan's own refinement: `segments`
|
|
1072
|
+
* is the more primal fact — the capture's own cut points — so `turnCount` is derived from
|
|
1073
|
+
* the field a reader of this JSON would already trust as the timeline, not from a second
|
|
1074
|
+
* field that only agrees with it by a schema-level invariant this result does not surface.
|
|
1075
|
+
*/
|
|
1076
|
+
actors: z6.object({
|
|
1077
|
+
/** `plan.actors.length`. Reported alongside `labels` rather than left for a caller to
|
|
1078
|
+
* compute, matching `narration.clipCount` sitting beside `narration` above for the
|
|
1079
|
+
* same reason: the count is the fact most callers want first, before the list. */
|
|
1080
|
+
count: z6.number().int().min(2),
|
|
1081
|
+
/** In `plan.actors` order — the cast list a badge or transition card would draw, not
|
|
1082
|
+
* deduplicated or sorted. */
|
|
1083
|
+
labels: z6.array(z6.string().min(1)).min(2),
|
|
1084
|
+
/** Hand-offs, not participants: one fewer than `plan.segments.length`, since a
|
|
1085
|
+
* segment is a window one actor already holds and a turn is the cut between two of
|
|
1086
|
+
* them — the same count `plan.transitionCards.length` carries, arrived at
|
|
1087
|
+
* independently from `segments` for the reason above. */
|
|
1088
|
+
turnCount: z6.number().int().nonnegative()
|
|
1089
|
+
}).refine((value) => value.count === value.labels.length, "actors.count must equal actors.labels.length").optional(),
|
|
1090
|
+
/**
|
|
1091
|
+
* The explain scenes this bundle cut away to, in composite order, or absent for a bundle
|
|
1092
|
+
* with none — every bundle recorded before `demo.explain()` existed, and still the default
|
|
1093
|
+
* after it, so this stays absent rather than an empty array, the same distinction `actors`
|
|
1094
|
+
* above and `narration` further up both draw.
|
|
1095
|
+
*
|
|
1096
|
+
* Sourced from `plan.explainSegments` alone, never the scenario file, for the same reason
|
|
1097
|
+
* `actors` is sourced from the plan rather than re-read from `demo.explain()` calls: a
|
|
1098
|
+
* bundle whose scenario has since been edited or deleted still inspects correctly.
|
|
1099
|
+
*/
|
|
1100
|
+
explainScenes: z6.array(
|
|
1101
|
+
z6.object({
|
|
1102
|
+
id: z6.string().min(1),
|
|
1103
|
+
durationMs: z6.number().int().positive()
|
|
1104
|
+
})
|
|
1105
|
+
).min(1).optional(),
|
|
849
1106
|
/** The rendered MP4s, from the manifest, so hashes are not recomputed. */
|
|
850
1107
|
outputs: z6.array(ArtifactRefSchema),
|
|
851
1108
|
assertions: z6.array(AssertionSchema),
|
|
@@ -892,6 +1149,22 @@ var DoctorResultSchema = z6.object({
|
|
|
892
1149
|
voices: z6.array(z6.string()),
|
|
893
1150
|
/** What is missing, in the same words a refused run would use. Empty when ready. */
|
|
894
1151
|
notes: z6.array(z6.string())
|
|
1152
|
+
}),
|
|
1153
|
+
/**
|
|
1154
|
+
* Whether the plainmotion CLI is on `PATH`, for a scenario that wants to call
|
|
1155
|
+
* `demo.explain()`. The same stance `hasAac`/`speech` above already take: absence is a
|
|
1156
|
+
* *state*, not a fault — the overwhelming majority of scenarios never cut away to an
|
|
1157
|
+
* explain scene, so `doctor` must not fail an otherwise-healthy installation over a binary
|
|
1158
|
+
* most runs will never invoke. `assertPlainmotion` (`recorder-playwright`) is the same
|
|
1159
|
+
* probe a run's own first `demo.explain()` pays, so `doctor` cannot say plainmotion is fine
|
|
1160
|
+
* and then a run refuse it.
|
|
1161
|
+
*/
|
|
1162
|
+
plainmotion: z6.object({
|
|
1163
|
+
installed: z6.boolean(),
|
|
1164
|
+
/** plainmotion's own `--version` output. Absent when not installed. */
|
|
1165
|
+
version: z6.string().optional(),
|
|
1166
|
+
/** Why the probe failed, in the same words a run's own refusal would use. Absent when installed. */
|
|
1167
|
+
note: z6.string().optional()
|
|
895
1168
|
})
|
|
896
1169
|
});
|
|
897
1170
|
var okMatchesProblems = (value) => value.ok === (value.problems.length === 0);
|
package/dist/schema/event.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const DemoEventTypeSchema: z.ZodEnum<{
|
|
|
9
9
|
"assertion.fail": "assertion.fail";
|
|
10
10
|
"privacy.mask": "privacy.mask";
|
|
11
11
|
"human.wait": "human.wait";
|
|
12
|
+
turn: "turn";
|
|
13
|
+
explain: "explain";
|
|
12
14
|
}>;
|
|
13
15
|
export declare const TargetSchema: z.ZodObject<{
|
|
14
16
|
role: z.ZodOptional<z.ZodString>;
|
|
@@ -36,6 +38,8 @@ export declare const DemoEventSchema: z.ZodObject<{
|
|
|
36
38
|
"assertion.fail": "assertion.fail";
|
|
37
39
|
"privacy.mask": "privacy.mask";
|
|
38
40
|
"human.wait": "human.wait";
|
|
41
|
+
turn: "turn";
|
|
42
|
+
explain: "explain";
|
|
39
43
|
}>;
|
|
40
44
|
stableId: z.ZodOptional<z.ZodString>;
|
|
41
45
|
title: z.ZodOptional<z.ZodString>;
|
|
@@ -52,6 +56,7 @@ export declare const DemoEventSchema: z.ZodObject<{
|
|
|
52
56
|
}, z.core.$strip>>;
|
|
53
57
|
}, z.core.$strip>>;
|
|
54
58
|
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
59
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
55
60
|
}, z.core.$strip>;
|
|
56
61
|
export type DemoEvent = z.infer<typeof DemoEventSchema>;
|
|
57
62
|
export type DemoEventType = z.infer<typeof DemoEventTypeSchema>;
|
|
@@ -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;
|
|
@@ -190,6 +190,104 @@ export declare const CameraSchema: z.ZodObject<{
|
|
|
190
190
|
}, z.core.$strip>;
|
|
191
191
|
export type Camera = z.infer<typeof CameraSchema>;
|
|
192
192
|
export type CameraShot = z.infer<typeof CameraShotSchema>;
|
|
193
|
+
/**
|
|
194
|
+
* One participant in a multi-actor demo, each with its own Playwright `BrowserContext` —
|
|
195
|
+
* `AGENTS.md` §1 constraint 3 guarantees at most one is ever active at a single instant.
|
|
196
|
+
* `id` is what `SegmentSchema.actorId` and `TransitionCardSchema.fromActorId`/`toActorId`
|
|
197
|
+
* reference; `label` is the human-readable name a badge or a transition card draws.
|
|
198
|
+
*/
|
|
199
|
+
export declare const ActorSchema: z.ZodObject<{
|
|
200
|
+
id: z.ZodString;
|
|
201
|
+
label: z.ZodString;
|
|
202
|
+
}, z.core.$strip>;
|
|
203
|
+
export type Actor = z.infer<typeof ActorSchema>;
|
|
204
|
+
/**
|
|
205
|
+
* One contiguous window of `raw/session.webm` during which a single actor held the (one,
|
|
206
|
+
* at most) active `BrowserContext`. `startMs`/`endMs` are source-relative — the same space
|
|
207
|
+
* `source.durationMs` and the `trim=duration=...` FFmpeg argument already operate in — not
|
|
208
|
+
* a file path: the concatenation of raw capture segments into the one `raw/session.webm`
|
|
209
|
+
* this plan's `source` names has already happened by the time this schema is populated, so
|
|
210
|
+
* there is deliberately no `rawFile`/path field here, unlike what an earlier design draft
|
|
211
|
+
* suggested. A segment describes how that single file is internally structured, never a
|
|
212
|
+
* second file to read.
|
|
213
|
+
*/
|
|
214
|
+
export declare const SegmentSchema: z.ZodObject<{
|
|
215
|
+
id: z.ZodString;
|
|
216
|
+
actorId: z.ZodString;
|
|
217
|
+
startMs: z.ZodNumber;
|
|
218
|
+
endMs: z.ZodNumber;
|
|
219
|
+
}, z.core.$strip>;
|
|
220
|
+
export type Segment = z.infer<typeof SegmentSchema>;
|
|
221
|
+
/**
|
|
222
|
+
* A full-frame card drawn between two actors' segments on a hand-off — structurally
|
|
223
|
+
* `Intro`/`Outro`'s sibling, down to the same `HEX_COLOUR` injection guard on its colours,
|
|
224
|
+
* plus which actor is handing off to which.
|
|
225
|
+
*
|
|
226
|
+
* Unlike the single well-known `captions/intro.ass`/`captions/outro.ass`, a demo can have
|
|
227
|
+
* many hand-offs, so `assPath` cannot be one literal. It is a regex-constrained family
|
|
228
|
+
* instead — `SpeechClipSchema.path`'s discipline applied to its own N-member family:
|
|
229
|
+
* constrained enough that no filtergraph metacharacter or path traversal is representable.
|
|
230
|
+
*/
|
|
231
|
+
export declare const TransitionCardSchema: z.ZodObject<{
|
|
232
|
+
fromActorId: z.ZodString;
|
|
233
|
+
toActorId: z.ZodString;
|
|
234
|
+
durationMs: z.ZodNumber;
|
|
235
|
+
lines: z.ZodArray<z.ZodString>;
|
|
236
|
+
backgroundColor: z.ZodString;
|
|
237
|
+
textColor: z.ZodString;
|
|
238
|
+
assPath: z.ZodString;
|
|
239
|
+
}, z.core.$strip>;
|
|
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>;
|
|
268
|
+
/**
|
|
269
|
+
* How long a `demo.turn(actor, { card })` transition card is on screen when its own
|
|
270
|
+
* `durationMs` was omitted.
|
|
271
|
+
*
|
|
272
|
+
* Lives here, beside `TransitionCardSchema`, rather than in either package that resolves it:
|
|
273
|
+
* `packages/recorder-playwright/src/pipeline.ts`'s `transitionCardDurationMs` reads it at
|
|
274
|
+
* derivation time, before `transitionCards` exists, to offset later segments; the renderer
|
|
275
|
+
* that eventually draws a frozen `RenderPlanSchema.transitionCards` entry with no
|
|
276
|
+
* `durationMs` of its own needs the same number. `AGENTS.md` §2 forbids `renderer-ffmpeg`
|
|
277
|
+
* from importing `recorder-playwright` (the reverse edge already exists, so importing back
|
|
278
|
+
* would be the cycle `no-circular` refuses), so a bare constant defined in either package
|
|
279
|
+
* would be unreachable from the other — both already depend on `@plaintake/schema`, which is
|
|
280
|
+
* why it is defined here instead.
|
|
281
|
+
*
|
|
282
|
+
* Matches `DEFAULT_INTRO_MS` and `FREE_TIER_OUTRO.durationMs`
|
|
283
|
+
* (`packages/license/src/branding.ts`), both already 3000ms for the same shape of decision —
|
|
284
|
+
* a full-frame card with a line or two to read, defaulted absent an author- or
|
|
285
|
+
* configuration-supplied length. Not imported from `@plaintake/license`: that package
|
|
286
|
+
* resolves *licensed* branding, and a transition card is never licence-gated (the camera is
|
|
287
|
+
* the one capability still Pro-gated; see `AGENTS.md`), so a shared import would be the wrong
|
|
288
|
+
* fix for a coincidence, not a rule the two packages share.
|
|
289
|
+
*/
|
|
290
|
+
export declare const TRANSITION_CARD_DEFAULT_MS = 3000;
|
|
193
291
|
/**
|
|
194
292
|
* One dimmed-frame-with-a-hole window: a step's measured target rect, frozen exactly as
|
|
195
293
|
* `CursorPointSchema` freezes a target's centre, and for the same reason. `renderer-ffmpeg`'s
|
|
@@ -247,6 +345,14 @@ export type HighlightRect = z.infer<typeof HighlightRectSchema>;
|
|
|
247
345
|
*
|
|
248
346
|
* `source` distinguishes a synthesised clip from an author-supplied WAV, because they are the
|
|
249
347
|
* same bytes by the time they reach here and the distinction is not otherwise recoverable.
|
|
348
|
+
*
|
|
349
|
+
* `explain` is the third provenance: a cut-away scene's narration, synthesised by *plainmotion*
|
|
350
|
+
* at record time rather than by this project's own narrator. It never requires the `engine`
|
|
351
|
+
* block below, because that block describes one synthesiser — the recorder's kokoro worker —
|
|
352
|
+
* and these clips were made by the other one; plainmotion's identity is recorded where the
|
|
353
|
+
* rest of its contribution is, the manifest's toolchain block. The rule the refine below
|
|
354
|
+
* actually states is "every clip must be accounted for": `synth` is accounted for by `engine`,
|
|
355
|
+
* `file` by the author who supplied it, `explain` by the toolchain.
|
|
250
356
|
*/
|
|
251
357
|
export declare const SpeechClipSchema: z.ZodObject<{
|
|
252
358
|
id: z.ZodString;
|
|
@@ -254,9 +360,11 @@ export declare const SpeechClipSchema: z.ZodObject<{
|
|
|
254
360
|
atMs: z.ZodNumber;
|
|
255
361
|
durationMs: z.ZodNumber;
|
|
256
362
|
source: z.ZodEnum<{
|
|
363
|
+
explain: "explain";
|
|
257
364
|
file: "file";
|
|
258
365
|
synth: "synth";
|
|
259
366
|
}>;
|
|
367
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
260
368
|
}, z.core.$strip>;
|
|
261
369
|
/**
|
|
262
370
|
* What produced the synthesised clips. Evidence only — nothing reads it to make a decision,
|
|
@@ -298,9 +406,11 @@ export declare const SpeechSchema: z.ZodObject<{
|
|
|
298
406
|
atMs: z.ZodNumber;
|
|
299
407
|
durationMs: z.ZodNumber;
|
|
300
408
|
source: z.ZodEnum<{
|
|
409
|
+
explain: "explain";
|
|
301
410
|
file: "file";
|
|
302
411
|
synth: "synth";
|
|
303
412
|
}>;
|
|
413
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
304
414
|
}, z.core.$strip>>;
|
|
305
415
|
engine: z.ZodOptional<z.ZodObject<{
|
|
306
416
|
name: z.ZodString;
|
|
@@ -339,6 +449,14 @@ export type HighlightInput = Omit<Highlight, 'assPath'>;
|
|
|
339
449
|
export type OutroInput = Omit<Outro, 'assPath'>;
|
|
340
450
|
/** The opening card as the application layer supplies it, exactly as `OutroInput` works. */
|
|
341
451
|
export type IntroInput = Omit<Intro, 'assPath'>;
|
|
452
|
+
/**
|
|
453
|
+
* The transition card decision as the application layer supplies it — `pipeline.ts`, in
|
|
454
|
+
* practice, which resolves a hand-off's concrete `durationMs` via `transitionCardDurationMs`
|
|
455
|
+
* before this is ever built, exactly as `intro`/`outro` durations are resolved before
|
|
456
|
+
* `buildRenderPlan` is called. `assPath` is filled in by `buildRenderPlan`, indexed by
|
|
457
|
+
* position in the array against the schema's own `captions/turn-[0-9]+\.ass` family.
|
|
458
|
+
*/
|
|
459
|
+
export type TransitionCardInput = Omit<TransitionCard, 'assPath'>;
|
|
342
460
|
/**
|
|
343
461
|
* The one value a plan's `schema` field ever holds.
|
|
344
462
|
*
|
|
@@ -532,9 +650,11 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
532
650
|
atMs: z.ZodNumber;
|
|
533
651
|
durationMs: z.ZodNumber;
|
|
534
652
|
source: z.ZodEnum<{
|
|
653
|
+
explain: "explain";
|
|
535
654
|
file: "file";
|
|
536
655
|
synth: "synth";
|
|
537
656
|
}>;
|
|
657
|
+
voice: z.ZodOptional<z.ZodString>;
|
|
538
658
|
}, z.core.$strip>>;
|
|
539
659
|
engine: z.ZodOptional<z.ZodObject<{
|
|
540
660
|
name: z.ZodString;
|
|
@@ -543,6 +663,31 @@ export declare const RenderPlanSchema: z.ZodObject<{
|
|
|
543
663
|
voice: z.ZodString;
|
|
544
664
|
}, z.core.$strip>>;
|
|
545
665
|
}, z.core.$strip>>;
|
|
666
|
+
actors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
667
|
+
id: z.ZodString;
|
|
668
|
+
label: z.ZodString;
|
|
669
|
+
}, z.core.$strip>>>;
|
|
670
|
+
segments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
671
|
+
id: z.ZodString;
|
|
672
|
+
actorId: z.ZodString;
|
|
673
|
+
startMs: z.ZodNumber;
|
|
674
|
+
endMs: z.ZodNumber;
|
|
675
|
+
}, z.core.$strip>>>;
|
|
676
|
+
transitionCards: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
677
|
+
fromActorId: z.ZodString;
|
|
678
|
+
toActorId: z.ZodString;
|
|
679
|
+
durationMs: z.ZodNumber;
|
|
680
|
+
lines: z.ZodArray<z.ZodString>;
|
|
681
|
+
backgroundColor: z.ZodString;
|
|
682
|
+
textColor: z.ZodString;
|
|
683
|
+
assPath: z.ZodString;
|
|
684
|
+
}, z.core.$strip>>>;
|
|
685
|
+
badgesAssPath: z.ZodOptional<z.ZodLiteral<"captions/badges.ass">>;
|
|
686
|
+
explainSegments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
687
|
+
id: z.ZodString;
|
|
688
|
+
segmentPath: z.ZodString;
|
|
689
|
+
durationMs: z.ZodNumber;
|
|
690
|
+
}, z.core.$strip>>>;
|
|
546
691
|
}, z.core.$strip>;
|
|
547
692
|
export type RenderPlan = z.infer<typeof RenderPlanSchema>;
|
|
548
693
|
export type Cue = z.infer<typeof CueSchema>;
|
package/dist/schema/results.d.ts
CHANGED
|
@@ -228,6 +228,7 @@ export declare const DiffCommandResultSchema: z.ZodObject<{
|
|
|
228
228
|
category: z.ZodEnum<{
|
|
229
229
|
assertion: "assertion";
|
|
230
230
|
target: "target";
|
|
231
|
+
actor: "actor";
|
|
231
232
|
step: "step";
|
|
232
233
|
timing: "timing";
|
|
233
234
|
caption: "caption";
|
|
@@ -280,6 +281,37 @@ export declare const PruneCommandResultSchema: z.ZodObject<{
|
|
|
280
281
|
ok: z.ZodBoolean;
|
|
281
282
|
problems: z.ZodArray<z.ZodString>;
|
|
282
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>;
|
|
283
315
|
export declare const InspectResultSchema: z.ZodObject<{
|
|
284
316
|
bundleDir: z.ZodString;
|
|
285
317
|
status: z.ZodEnum<{
|
|
@@ -326,6 +358,15 @@ export declare const InspectResultSchema: z.ZodObject<{
|
|
|
326
358
|
suppliedCount: z.ZodNumber;
|
|
327
359
|
voice: z.ZodOptional<z.ZodString>;
|
|
328
360
|
}, z.core.$strip>>;
|
|
361
|
+
actors: z.ZodOptional<z.ZodObject<{
|
|
362
|
+
count: z.ZodNumber;
|
|
363
|
+
labels: z.ZodArray<z.ZodString>;
|
|
364
|
+
turnCount: z.ZodNumber;
|
|
365
|
+
}, z.core.$strip>>;
|
|
366
|
+
explainScenes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
367
|
+
id: z.ZodString;
|
|
368
|
+
durationMs: z.ZodNumber;
|
|
369
|
+
}, z.core.$strip>>>;
|
|
329
370
|
outputs: z.ZodArray<z.ZodObject<{
|
|
330
371
|
path: z.ZodString;
|
|
331
372
|
sha256: z.ZodString;
|
|
@@ -359,6 +400,11 @@ export declare const DoctorResultSchema: z.ZodObject<{
|
|
|
359
400
|
voices: z.ZodArray<z.ZodString>;
|
|
360
401
|
notes: z.ZodArray<z.ZodString>;
|
|
361
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>;
|
|
362
408
|
schema: z.ZodLiteral<"agent-demo.result/v1">;
|
|
363
409
|
kind: z.ZodLiteral<"doctor">;
|
|
364
410
|
ok: z.ZodBoolean;
|
|
@@ -412,11 +458,12 @@ export type RenderCommandResult = z.infer<typeof RenderCommandResultSchema>;
|
|
|
412
458
|
export type VerificationReport = z.infer<typeof VerificationReportSchema>;
|
|
413
459
|
export type DiffCommandResult = z.infer<typeof DiffCommandResultSchema>;
|
|
414
460
|
export type PruneCommandResult = z.infer<typeof PruneCommandResultSchema>;
|
|
461
|
+
export type ImportCommandResult = z.infer<typeof ImportCommandResultSchema>;
|
|
415
462
|
export type InspectResult = z.infer<typeof InspectResultSchema>;
|
|
416
463
|
export type DoctorResult = z.infer<typeof DoctorResultSchema>;
|
|
417
464
|
export type ActivateResult = z.infer<typeof ActivateResultSchema>;
|
|
418
465
|
export type LicenceResult = z.infer<typeof LicenceResultSchema>;
|
|
419
|
-
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;
|
|
420
467
|
/**
|
|
421
468
|
* Renders a filesystem location for display in a result, relative to `root` when it
|
|
422
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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ScenarioMeta } from './schema/index.js';
|
|
2
|
-
import type { Locator, Page } from 'playwright-core';
|
|
2
|
+
import type { BrowserContextOptions, Locator, Page } from 'playwright-core';
|
|
3
3
|
/**
|
|
4
4
|
* What kind of interaction `run()` performs, declared rather than observed. Like
|
|
5
5
|
* `target`, this is metadata the recorder records and never performs — it tells the
|
|
6
|
-
* cursor track what to draw at the target (`click` a ripple, `type`/`point` a
|
|
7
|
-
*
|
|
6
|
+
* cursor track what to draw at the target (`click` a ripple, `type`/`point`/`scroll` a
|
|
7
|
+
* settle — none of the three pulse, only `click` does). Absent means `point`: the cursor
|
|
8
|
+
* still moves to the target, nothing pulses.
|
|
8
9
|
*/
|
|
9
|
-
export type DemoAction = 'click' | 'type' | 'point';
|
|
10
|
+
export type DemoAction = 'click' | 'type' | 'point' | 'scroll';
|
|
10
11
|
export type DemoStep = {
|
|
11
12
|
id: string;
|
|
12
13
|
title: string;
|
|
@@ -32,6 +33,20 @@ export type DemoStep = {
|
|
|
32
33
|
highlight?: boolean | {
|
|
33
34
|
label?: string;
|
|
34
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;
|
|
35
50
|
/** Extra presentation hold after the action completes, in whole milliseconds. */
|
|
36
51
|
holdMs?: number;
|
|
37
52
|
run: () => Promise<unknown>;
|
|
@@ -124,6 +139,262 @@ export type DemoHandoff = PreflightHandoff & {
|
|
|
124
139
|
*/
|
|
125
140
|
mask?: string;
|
|
126
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
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Presentation fields shared by every `Actor` verb below — the same subset of `DemoStep`
|
|
280
|
+
* that is still the caller's to supply once `target` and `action` no longer are: a verb's
|
|
281
|
+
* argument list already gives the target (when it has one), and the verb's own name already
|
|
282
|
+
* gives the action. Only how the step presents is left to ask for.
|
|
283
|
+
*
|
|
284
|
+
* `title` is optional here where `DemoStep.title` is required, on purpose: a verb call that
|
|
285
|
+
* narrates nothing — a background click with no caption — is meant to stay a one-liner,
|
|
286
|
+
* not force every single interaction through the same captioning `demo.step()` requires.
|
|
287
|
+
*/
|
|
288
|
+
export type ActorStepMeta = {
|
|
289
|
+
/**
|
|
290
|
+
* Stable id for the step this call records, the same role `DemoStep.id` plays and for the
|
|
291
|
+
* same reasons: it becomes the `stableId` on this step's `step.start`/`step.finish` events,
|
|
292
|
+
* the key a target-miss or an abort diagnostic names, and — via `clipFileName(id)` — the
|
|
293
|
+
* exact filename an author drops into `narrationDir` for a `--speech file` WAV. Absent
|
|
294
|
+
* means the runtime synthesizes one; supply your own when you need that identity to stay
|
|
295
|
+
* stable across runs — a hand-chosen id does not shift when an earlier call in the same
|
|
296
|
+
* turn is added, removed or reordered the way a synthesized one might.
|
|
297
|
+
*/
|
|
298
|
+
id?: string;
|
|
299
|
+
title?: string;
|
|
300
|
+
subtitle?: string;
|
|
301
|
+
/** See `DemoStep.holdMs`. */
|
|
302
|
+
holdMs?: number;
|
|
303
|
+
/** See `DemoStep.highlight`, including the "requires a measurable target" rule. */
|
|
304
|
+
highlight?: boolean | {
|
|
305
|
+
label?: string;
|
|
306
|
+
};
|
|
307
|
+
/** See `DemoStep.voice` — one verb-call override, ahead of this actor's own `voice`. */
|
|
308
|
+
voice?: string;
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* A named participant in a multi-actor demo: its own Playwright `BrowserContext`, its own
|
|
312
|
+
* page, and a handle of verbs that each perform a Playwright action *and* record the
|
|
313
|
+
* render-track annotation that action needs — the pairing `demo.step()` otherwise leaves an
|
|
314
|
+
* author to assemble by hand (a `target`, a declared `action`, a `run`). `admin.click(button)`
|
|
315
|
+
* does both in one call; there is no separate bookkeeping step the way there is today.
|
|
316
|
+
*
|
|
317
|
+
* `page` is the same escape hatch `DemoRunArgs.page` already is: anything the named verbs
|
|
318
|
+
* below don't model — a Playwright call with no render-track equivalent — is still reachable
|
|
319
|
+
* through it, at the same cost reaching past `demo.step()` into raw Playwright already has
|
|
320
|
+
* today: nothing about the call is recorded automatically.
|
|
321
|
+
*
|
|
322
|
+
* Every verb that takes a `Locator` records that locator's measured rect on `step.start`, the
|
|
323
|
+
* same rect `DemoStep.target` records; a verb with nothing to point at (`goto`, and `scroll`'s
|
|
324
|
+
* delta form) records none, exactly as a `DemoStep` with no `target` does.
|
|
325
|
+
*
|
|
326
|
+
* Same name, different thing from `@plaintake/schema`'s `Actor`: that one is the frozen
|
|
327
|
+
* `{ id, label }` render-plan record a *finished* run leaves behind; this one is the live,
|
|
328
|
+
* verb-bearing handle a scenario's `run()` calls while a session is still recording.
|
|
329
|
+
*/
|
|
330
|
+
export type Actor = {
|
|
331
|
+
/** Raw Playwright escape hatch. See this type's own doc comment. */
|
|
332
|
+
page: Page;
|
|
333
|
+
goto(url: string, meta?: ActorStepMeta): Promise<void>;
|
|
334
|
+
click(target: Locator, meta?: ActorStepMeta): Promise<void>;
|
|
335
|
+
/**
|
|
336
|
+
* Fills `target` with `text`. Masking a field's *value* once it exists is `mask`'s job,
|
|
337
|
+
* not this verb's — `type` only ever records that typing happened, never what was typed.
|
|
338
|
+
*/
|
|
339
|
+
type(target: Locator, text: string, meta?: ActorStepMeta): Promise<void>;
|
|
340
|
+
/**
|
|
341
|
+
* Presses `key` while `target` holds focus — Playwright's `locator.press`, not the
|
|
342
|
+
* page-global `page.keyboard.press`. A demo step points at something; a key press with
|
|
343
|
+
* nothing to point at is exactly what `actor.page.keyboard.press(key)` remains for, at
|
|
344
|
+
* the same recorded-nothing cost as any other reach past a named verb.
|
|
345
|
+
*/
|
|
346
|
+
press(target: Locator, key: string, meta?: ActorStepMeta): Promise<void>;
|
|
347
|
+
/**
|
|
348
|
+
* Scrolls the page or a specific element into view — two different Playwright calls,
|
|
349
|
+
* `locator.scrollIntoViewIfNeeded()` and `page.mouse.wheel(deltaX, deltaY)`, behind one
|
|
350
|
+
* verb, chosen by which shape `where` is. Named `where` rather than `target` because the
|
|
351
|
+
* delta form is not a target — nothing measurable sits behind a pixel offset — where every
|
|
352
|
+
* other verb's `target` names a `Locator` and nothing else. A `Locator` scrolls that element
|
|
353
|
+
* into view and records its rect exactly as `click`/`hover` do; a `{ deltaX?, deltaY? }`
|
|
354
|
+
* delta scrolls the viewport by that many pixels (either defaulting to 0) and records no
|
|
355
|
+
* target, exactly as `goto` does.
|
|
356
|
+
*/
|
|
357
|
+
scroll(where: Locator | {
|
|
358
|
+
deltaX?: number;
|
|
359
|
+
deltaY?: number;
|
|
360
|
+
}, meta?: ActorStepMeta): Promise<void>;
|
|
361
|
+
hover(target: Locator, meta?: ActorStepMeta): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Mirrors `locator.selectOption`'s common case: one value or several. Anything its fuller
|
|
364
|
+
* overload set covers that a plain value or value list cannot is `step`'s or `page`'s to
|
|
365
|
+
* reach instead.
|
|
366
|
+
*/
|
|
367
|
+
select(target: Locator, value: string | string[], meta?: ActorStepMeta): Promise<void>;
|
|
368
|
+
/**
|
|
369
|
+
* Reuses `DemoAssertion` verbatim. The only thing this adds over `DemoContext.assert` is
|
|
370
|
+
* which actor the recorded event is attributed to.
|
|
371
|
+
*/
|
|
372
|
+
assert(assertion: DemoAssertion): Promise<void>;
|
|
373
|
+
/**
|
|
374
|
+
* Escape hatch for an interaction none of the named verbs above model, bound to this actor
|
|
375
|
+
* exactly as `DemoContext.step` is bound to the default one.
|
|
376
|
+
*/
|
|
377
|
+
step(step: DemoStep): Promise<void>;
|
|
378
|
+
/**
|
|
379
|
+
* Reuses `DemoMask` verbatim. Actor-scoped: applies to this actor's own `BrowserContext`
|
|
380
|
+
* only. Each actor's context is its own page, its own DOM and its own selectors, so a mask
|
|
381
|
+
* registered here has nothing to do in a context it was never registered against.
|
|
382
|
+
*/
|
|
383
|
+
mask(mask: DemoMask): Promise<void>;
|
|
384
|
+
/**
|
|
385
|
+
* Another `Page` on this actor's own `BrowserContext` — the multi-tab counterpart to
|
|
386
|
+
* `demo.actor`'s multi-context one, still governed by the same one-active-page-at-a-time
|
|
387
|
+
* rule within this actor's own turn.
|
|
388
|
+
*/
|
|
389
|
+
newTab(): Promise<Page>;
|
|
390
|
+
/**
|
|
391
|
+
* Hands the real browser window to a person during this actor's turn. Reuses `DemoHandoff`
|
|
392
|
+
* verbatim; the only difference from `DemoContext.handoff` is that a handoff can originate
|
|
393
|
+
* from any actor's turn, not only the default one a scenario would fall back to if none
|
|
394
|
+
* were ever named — a person can be handed the browser whoever currently holds it.
|
|
395
|
+
*/
|
|
396
|
+
handoff(request: DemoHandoff): Promise<void>;
|
|
397
|
+
};
|
|
127
398
|
/**
|
|
128
399
|
* What a scenario may do before recording starts.
|
|
129
400
|
*
|
|
@@ -153,6 +424,75 @@ export interface DemoContext extends PreflightContext {
|
|
|
153
424
|
assert(assertion: DemoAssertion): Promise<void>;
|
|
154
425
|
mask(mask: DemoMask): Promise<void>;
|
|
155
426
|
pause(ms: number): Promise<void>;
|
|
427
|
+
/**
|
|
428
|
+
* Returns a handle for a named actor, standing up its `BrowserContext` and `Page` the first
|
|
429
|
+
* time `id` is seen. Async, not a synchronous handle: `browser.newContext()` and
|
|
430
|
+
* `context.newPage()` are themselves async Playwright calls, and there is no way to hand
|
|
431
|
+
* back an `Actor` whose `page` is already a real, working page without awaiting them first
|
|
432
|
+
* — so authors write `const admin = await demo.actor('admin')`. See `turn` for what makes
|
|
433
|
+
* an actor's context the active one.
|
|
434
|
+
*
|
|
435
|
+
* The first `id` a scenario ever calls this with reuses the implicit default context and
|
|
436
|
+
* page every scenario already runs on, rather than opening a second one nobody asked for,
|
|
437
|
+
* so in practice that call resolves immediately — there is no new `BrowserContext` to wait
|
|
438
|
+
* on. The signature stays `Promise<Actor>` for every actor regardless, rather than
|
|
439
|
+
* special-casing the first one: a single-actor scenario written before actors existed keeps
|
|
440
|
+
* working unchanged, and turning it into a two-actor one only costs the second
|
|
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.
|
|
448
|
+
*/
|
|
449
|
+
actor(id: string, opts?: {
|
|
450
|
+
label?: string;
|
|
451
|
+
contextOptions?: BrowserContextOptions;
|
|
452
|
+
voice?: string;
|
|
453
|
+
}): Promise<Actor>;
|
|
454
|
+
/**
|
|
455
|
+
* Marks a hand-off from whichever actor currently holds the browser to `actor`, parking the
|
|
456
|
+
* outgoing `BrowserContext` and activating the incoming one. Async because, like `chapter`
|
|
457
|
+
* and `pause`, it is itself an event on the render timeline, not just a bookkeeping call.
|
|
458
|
+
*
|
|
459
|
+
* `opts.card`, when given, plays a full-frame transition card between the two actors'
|
|
460
|
+
* segments — the `lines` it names, an optional spoken `narration`, held for `durationMs`.
|
|
461
|
+
*
|
|
462
|
+
* Omitting `opts.card` is allowed, but still draws a card: `RenderPlanSchema.transitionCards`
|
|
463
|
+
* requires exactly one entry per hand-off with non-empty `lines`, so a card-less turn's card
|
|
464
|
+
* is synthesised at render time as `Now: <label>`, naming the incoming actor by the `label`
|
|
465
|
+
* it was given to `demo.actor()`, held for the same default duration an explicit card with no
|
|
466
|
+
* `durationMs` of its own gets. The persistent corner badge names the active actor throughout
|
|
467
|
+
* either way; `opts.card` only ever changes whether that hand-off also gets a full-frame beat
|
|
468
|
+
* of its own, and what it says.
|
|
469
|
+
*/
|
|
470
|
+
turn(actor: Actor, opts?: {
|
|
471
|
+
card?: {
|
|
472
|
+
lines: string[];
|
|
473
|
+
narration?: string;
|
|
474
|
+
durationMs?: number;
|
|
475
|
+
};
|
|
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>;
|
|
156
496
|
}
|
|
157
497
|
export type DemoRunArgs = {
|
|
158
498
|
page: Page;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plaintake/scenario",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|