@tandryukha/aidemo 0.14.1 → 0.15.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/docs/AUTHORING.md +142 -20
- package/package.json +1 -1
- package/src/captions.ts +174 -30
- package/src/cli.ts +134 -7
- package/src/compose.ts +340 -36
- package/src/cursor.ts +13 -4
- package/src/ffmpeg.ts +68 -0
- package/src/frames.ts +54 -5
- package/src/import-trace.ts +20 -9
- package/src/inspect.ts +9 -0
- package/src/lint.ts +75 -3
- package/src/mcp/jobs.ts +1 -0
- package/src/mcp/server.ts +67 -3
- package/src/player.ts +317 -15
- package/src/qa.ts +295 -0
- package/src/recorder.ts +65 -7
- package/src/safe-regex.ts +36 -0
- package/src/types.ts +155 -19
package/src/types.ts
CHANGED
|
@@ -133,6 +133,15 @@ export const ActionSchema = z.discriminatedUnion("op", [
|
|
|
133
133
|
...BaseAction,
|
|
134
134
|
op: z.literal("hover"),
|
|
135
135
|
target: TargetSchema,
|
|
136
|
+
/**
|
|
137
|
+
* Where the pointer parks relative to the element. Default "center" — but
|
|
138
|
+
* on a small chip or a number the narration is about, the cursor then
|
|
139
|
+
* COVERS the thing it points at (issue #50). "edge" (or a side) parks it
|
|
140
|
+
* just outside the element's box, still close enough to read as pointing.
|
|
141
|
+
*/
|
|
142
|
+
anchor: z.enum(["center", "edge", "top", "bottom", "left", "right"]).optional(),
|
|
143
|
+
/** Extra nudge from the anchor point, CSS px. */
|
|
144
|
+
offset: z.object({ x: z.number(), y: z.number() }).optional(),
|
|
136
145
|
}),
|
|
137
146
|
z.object({
|
|
138
147
|
...BaseAction,
|
|
@@ -376,6 +385,35 @@ export const ActionSchema = z.discriminatedUnion("op", [
|
|
|
376
385
|
}),
|
|
377
386
|
/** Browser back (history), with the same readiness wait as `goto`. */
|
|
378
387
|
z.object({ ...BaseAction, op: z.literal("back") }),
|
|
388
|
+
/**
|
|
389
|
+
* Arm a one-shot handler for the NEXT native dialog (`alert`, `confirm`,
|
|
390
|
+
* `prompt`, `beforeunload`) so the action that triggers it actually goes
|
|
391
|
+
* through (issue #54). Playwright auto-DISMISSES dialogs when nothing is
|
|
392
|
+
* listening, so without this a filmed click on an "Archive this?" button
|
|
393
|
+
* looks like it worked while the handler never ran — a false outcome on
|
|
394
|
+
* camera. Put the `dialog` action immediately BEFORE the click that opens it.
|
|
395
|
+
*
|
|
396
|
+
* The dialog itself is chrome, not page content, so it is not visible in the
|
|
397
|
+
* take; `holdMs` only paces the beat before answering.
|
|
398
|
+
*/
|
|
399
|
+
z.object({
|
|
400
|
+
...BaseAction,
|
|
401
|
+
op: z.literal("dialog"),
|
|
402
|
+
/** Answer the dialog with OK (default) or Cancel. */
|
|
403
|
+
action: z.enum(["accept", "dismiss"]).default("accept"),
|
|
404
|
+
/** Text typed into a `prompt()` before accepting. */
|
|
405
|
+
promptText: z.string().optional(),
|
|
406
|
+
/** Wait this long before answering, ms. Default 0. */
|
|
407
|
+
holdMs: z.number().min(0).max(5000).optional(),
|
|
408
|
+
/** How long to wait for the dialog to appear before warning, ms. Default 5000. */
|
|
409
|
+
timeoutMs: z.number().optional(),
|
|
410
|
+
/**
|
|
411
|
+
* Set false when the dialog is not guaranteed to open — the take then only
|
|
412
|
+
* notes that none came. Default true: a `dialog` whose dialog never fired
|
|
413
|
+
* fails the take, because the filmed action silently did nothing.
|
|
414
|
+
*/
|
|
415
|
+
required: z.boolean().optional(),
|
|
416
|
+
}),
|
|
379
417
|
]);
|
|
380
418
|
export type Action = z.infer<typeof ActionSchema>;
|
|
381
419
|
|
|
@@ -449,6 +487,22 @@ export const CaptionsConfigSchema = z.object({
|
|
|
449
487
|
color: z.string().optional(),
|
|
450
488
|
/** Pill/bar background (CSS). Default rgba(12,14,22,.66). */
|
|
451
489
|
background: z.string().optional(),
|
|
490
|
+
/**
|
|
491
|
+
* Cue segmentation (storyboard level only — per-scene overrides just move
|
|
492
|
+
* the strip). Defaults follow broadcast practice: up to 2 lines of ~38
|
|
493
|
+
* characters, 1–6 s per cue, broken at punctuation and conjunctions rather
|
|
494
|
+
* than at a hard word count (issue #48).
|
|
495
|
+
*/
|
|
496
|
+
/** Max words per cue. Default 12 (≈2 lines). */
|
|
497
|
+
maxWords: z.number().int().min(2).max(30).optional(),
|
|
498
|
+
/** Max cue length, ms. Default 6000. */
|
|
499
|
+
maxCueMs: z.number().min(500).max(12000).optional(),
|
|
500
|
+
/** Cues shorter than this are merged into a neighbour where they fit, ms. Default 1000. */
|
|
501
|
+
minCueMs: z.number().min(0).max(4000).optional(),
|
|
502
|
+
/** Lines a cue may occupy. Default 2. */
|
|
503
|
+
maxLines: z.number().int().min(1).max(3).optional(),
|
|
504
|
+
/** Characters per line used to size a cue. Default 38. */
|
|
505
|
+
maxCharsPerLine: z.number().int().min(16).max(80).optional(),
|
|
452
506
|
});
|
|
453
507
|
export type CaptionsConfig = z.infer<typeof CaptionsConfigSchema>;
|
|
454
508
|
|
|
@@ -458,6 +512,12 @@ export const AttentionConfigSchema = z.object({
|
|
|
458
512
|
color: z.string().optional(),
|
|
459
513
|
/** Draw a click ring where every click lands (compose-time cursor only). */
|
|
460
514
|
clicks: z.boolean().optional(),
|
|
515
|
+
/**
|
|
516
|
+
* Move the cursor out of the way before a highlight/spotlight/callout/focus
|
|
517
|
+
* beat, so the pointer never sits on top of the value being showcased
|
|
518
|
+
* (issue #50). Opt-in; the cursor glides to just outside the target's box.
|
|
519
|
+
*/
|
|
520
|
+
cursorClear: z.boolean().optional(),
|
|
461
521
|
});
|
|
462
522
|
export type AttentionConfig = z.infer<typeof AttentionConfigSchema>;
|
|
463
523
|
|
|
@@ -491,6 +551,12 @@ export const FrameSchema = z.object({
|
|
|
491
551
|
safeTop: z.union([z.boolean(), z.number().min(0).max(120)]).optional(),
|
|
492
552
|
/** Text in the chrome's address pill (url wins over title). */
|
|
493
553
|
title: z.string().optional(),
|
|
554
|
+
/**
|
|
555
|
+
* Address-pill text. `"auto"` tracks the page: each scene's pill shows the
|
|
556
|
+
* host of the URL that scene is actually on (from the take's `goto`s), so a
|
|
557
|
+
* demo that moves between hosts stops printing one fixed address over all of
|
|
558
|
+
* them (issue #52). Any other string is used verbatim for the whole video.
|
|
559
|
+
*/
|
|
494
560
|
url: z.string().optional(),
|
|
495
561
|
});
|
|
496
562
|
export type Frame = z.infer<typeof FrameSchema>;
|
|
@@ -534,6 +600,20 @@ export const AutoIdleSchema = z.object({
|
|
|
534
600
|
});
|
|
535
601
|
export type AutoIdle = z.infer<typeof AutoIdleSchema>;
|
|
536
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Scene-to-scene transition (opt-in). When present, compose crossfades the
|
|
605
|
+
* video across every scene boundary instead of hard-cutting. The overlap is
|
|
606
|
+
* stolen from each scene's own frozen tail so the timeline does NOT shrink —
|
|
607
|
+
* total duration and per-scene narration alignment are preserved exactly.
|
|
608
|
+
* Omit the key for the default hard cuts (unchanged stream-copy concat).
|
|
609
|
+
*/
|
|
610
|
+
export const TransitionSchema = z.object({
|
|
611
|
+
type: z.literal("crossfade"),
|
|
612
|
+
/** Crossfade length, ms. Default 400. */
|
|
613
|
+
durationMs: z.number().default(400),
|
|
614
|
+
});
|
|
615
|
+
export type Transition = z.infer<typeof TransitionSchema>;
|
|
616
|
+
|
|
537
617
|
export const SceneSchema = z.object({
|
|
538
618
|
id: z.string(),
|
|
539
619
|
/** Chapter title for this beat (`output.chapters`); defaults to the scene id. */
|
|
@@ -565,6 +645,14 @@ export const SceneSchema = z.object({
|
|
|
565
645
|
autoIdle: z.boolean().optional(),
|
|
566
646
|
/** Caption placement for this scene (overrides top-level `captions.position`). */
|
|
567
647
|
captions: CaptionsConfigSchema.optional(),
|
|
648
|
+
/**
|
|
649
|
+
* Override the storyboard `transition` at the boundary ENTERING this scene:
|
|
650
|
+
* `false` hard-cuts into it, an object changes the crossfade. Use it when a
|
|
651
|
+
* dissolve would ghost — e.g. this scene opens on a new page still painting
|
|
652
|
+
* (issue #49). Boundaries that cross a navigation hard-cut automatically;
|
|
653
|
+
* this is the manual override.
|
|
654
|
+
*/
|
|
655
|
+
transition: z.union([z.literal(false), TransitionSchema]).optional(),
|
|
568
656
|
/** Blur these regions while this scene records (adds to top-level `redact`). */
|
|
569
657
|
redact: z.array(RedactSchema).optional(),
|
|
570
658
|
/** Hide these selectors for this scene (adds to top-level `hide`). */
|
|
@@ -656,6 +744,16 @@ export const CursorConfigSchema = z.object({
|
|
|
656
744
|
style: z.enum(["arrow", "dot"]).optional(),
|
|
657
745
|
/** Dot style only: fill color. Default rgba(255,90,95,.85). */
|
|
658
746
|
color: z.string().optional(),
|
|
747
|
+
/**
|
|
748
|
+
* Outline drawn around the pointer so it stays findable on brand-coloured
|
|
749
|
+
* UI (a gold dot on a gold chip disappears — issue #50). CSS color; default
|
|
750
|
+
* rgba(255,255,255,.9) for "dot", the arrow's dark stroke otherwise.
|
|
751
|
+
*/
|
|
752
|
+
outline: z.string().optional(),
|
|
753
|
+
/** Outline thickness in cursor units (the pointer is 24 wide). Default 2. */
|
|
754
|
+
outlineWidth: z.number().min(0).max(6).optional(),
|
|
755
|
+
/** Soft contrasting halo behind the pointer — findable on any background. */
|
|
756
|
+
halo: z.boolean().optional(),
|
|
659
757
|
});
|
|
660
758
|
export type CursorConfig = z.infer<typeof CursorConfigSchema>;
|
|
661
759
|
|
|
@@ -686,19 +784,6 @@ export const CardSchema = z.object({
|
|
|
686
784
|
});
|
|
687
785
|
export type Card = z.infer<typeof CardSchema>;
|
|
688
786
|
|
|
689
|
-
/**
|
|
690
|
-
* Scene-to-scene transition (opt-in). When present, compose crossfades the
|
|
691
|
-
* video across every scene boundary instead of hard-cutting. The overlap is
|
|
692
|
-
* stolen from each scene's own frozen tail so the timeline does NOT shrink —
|
|
693
|
-
* total duration and per-scene narration alignment are preserved exactly.
|
|
694
|
-
* Omit the key for the default hard cuts (unchanged stream-copy concat).
|
|
695
|
-
*/
|
|
696
|
-
export const TransitionSchema = z.object({
|
|
697
|
-
type: z.literal("crossfade"),
|
|
698
|
-
/** Crossfade length, ms. Default 400. */
|
|
699
|
-
durationMs: z.number().default(400),
|
|
700
|
-
});
|
|
701
|
-
export type Transition = z.infer<typeof TransitionSchema>;
|
|
702
787
|
|
|
703
788
|
/**
|
|
704
789
|
* Master loudness target for the final muxed audio (ffmpeg `loudnorm`). Lands
|
|
@@ -747,8 +832,22 @@ export const OutputSchema = z
|
|
|
747
832
|
preset: OutputPresetSchema.optional(),
|
|
748
833
|
/** Write MP4 chapter markers, one per scene (scene `title` or id). Default false. */
|
|
749
834
|
chapters: z.boolean().optional(),
|
|
750
|
-
/**
|
|
751
|
-
|
|
835
|
+
/**
|
|
836
|
+
* Also extract output/poster.png. `true` picks the first frame after the
|
|
837
|
+
* intro card that actually shows CONTENT (a blank white load frame is
|
|
838
|
+
* skipped — issue #47); a number picks the frame at that ms of the final
|
|
839
|
+
* video instead. Default false.
|
|
840
|
+
*/
|
|
841
|
+
poster: z.union([z.boolean(), z.number().min(0)]).optional(),
|
|
842
|
+
/**
|
|
843
|
+
* Drop the browser's blank pre-paint at the very start of the video, so
|
|
844
|
+
* the demo opens on the product instead of on ~0.5 s of white under the
|
|
845
|
+
* first caption (issue #47). At most `blankOpenCapMs` of the leading flat
|
|
846
|
+
* span is kept. Default false (unchanged opening).
|
|
847
|
+
*/
|
|
848
|
+
trimLeadingBlank: z.boolean().optional(),
|
|
849
|
+
/** With `trimLeadingBlank`: leading blank kept, ms. Default 100. */
|
|
850
|
+
blankOpenCapMs: z.number().min(0).max(2000).optional(),
|
|
752
851
|
/**
|
|
753
852
|
* Also export output/walkthrough/ (index.html + guide.md + per-scene
|
|
754
853
|
* frames + captions) after every render. Default false; `aidemo
|
|
@@ -759,9 +858,12 @@ export const OutputSchema = z
|
|
|
759
858
|
height: z.number().optional(),
|
|
760
859
|
/**
|
|
761
860
|
* "contain" (default) = scale to fit + pad the remainder (letterbox bars).
|
|
762
|
-
* "cover" = scale to fill + center-crop the overflow (no bars)
|
|
861
|
+
* "cover" = scale to fill + center-crop the overflow (no bars) — the
|
|
862
|
+
* edge-to-edge answer when the capture's aspect differs from the
|
|
863
|
+
* preset's (issue #58). Set explicitly it also wins over a `preset`'s
|
|
864
|
+
* own fit; leave it out to take the preset's.
|
|
763
865
|
*/
|
|
764
|
-
fit: z.enum(["contain", "cover"]).
|
|
866
|
+
fit: z.enum(["contain", "cover"]).optional(),
|
|
765
867
|
/** Pad color for "contain" (ffmpeg color syntax, e.g. black, 0x1a1a1a). Default black. */
|
|
766
868
|
background: z.string().optional(),
|
|
767
869
|
/**
|
|
@@ -797,6 +899,20 @@ export const HoldSchema = z.object({
|
|
|
797
899
|
driftScale: z.number().min(1).max(1.5).default(1.06),
|
|
798
900
|
/** Take the held frame this many ms before the segment end. Default 0. */
|
|
799
901
|
backoffMs: z.number().min(0).max(2000).default(0),
|
|
902
|
+
/**
|
|
903
|
+
* Shortest hold that still drifts, ms. Default 2000: a 6-px push over a
|
|
904
|
+
* second reads as a bob — a jitter bug — not as a dwell, so short holds
|
|
905
|
+
* freeze instead (issue #57).
|
|
906
|
+
*/
|
|
907
|
+
minDriftMs: z.number().min(0).max(20000).default(2000),
|
|
908
|
+
/**
|
|
909
|
+
* Drift even when a window `frame.chrome` is composited around the video.
|
|
910
|
+
* Default false: the drift zooms the captured PAGE while the drawn browser
|
|
911
|
+
* chrome stays put, so the eye — locked on the static window edge and the
|
|
912
|
+
* sticky header — sees the page sliding inside its frame, i.e. a scroll
|
|
913
|
+
* glitch. Such scenes freeze instead unless you set this (issue #57).
|
|
914
|
+
*/
|
|
915
|
+
driftInFrame: z.boolean().default(false),
|
|
800
916
|
});
|
|
801
917
|
export type HoldConfig = z.infer<typeof HoldSchema>;
|
|
802
918
|
|
|
@@ -891,9 +1007,27 @@ export const StoryboardSchema = z.object({
|
|
|
891
1007
|
*/
|
|
892
1008
|
params: z.record(z.string(), z.string()).optional(),
|
|
893
1009
|
targetLengthSeconds: z.number().optional(),
|
|
894
|
-
/**
|
|
1010
|
+
/**
|
|
1011
|
+
* Recording viewport (CSS px) and, with `deviceScaleFactor`, the raw video
|
|
1012
|
+
* size. Default 1280x720.
|
|
1013
|
+
*/
|
|
895
1014
|
video: z
|
|
896
|
-
.object({
|
|
1015
|
+
.object({
|
|
1016
|
+
width: z.number(),
|
|
1017
|
+
height: z.number(),
|
|
1018
|
+
/**
|
|
1019
|
+
* Record the video at `width*n x height*n` pixels while the PAGE still
|
|
1020
|
+
* lays out at `width x height` CSS px — the standard fix for "the
|
|
1021
|
+
* product is unreadable" (issue #51). A desktop app recorded at a
|
|
1022
|
+
* 1920-px viewport renders its 1200-px content column tiny; record at
|
|
1023
|
+
* 1280–1440 CSS px with `deviceScaleFactor: 2` instead and let
|
|
1024
|
+
* `output.preset` scale the 2x raw down to 1080p: the UI is a third
|
|
1025
|
+
* bigger on screen and stays crisp. Default 1 (unchanged output).
|
|
1026
|
+
* Built-in (Playwright) capture only — native/OBS capture records real
|
|
1027
|
+
* screen pixels.
|
|
1028
|
+
*/
|
|
1029
|
+
deviceScaleFactor: z.number().min(1).max(3).optional(),
|
|
1030
|
+
})
|
|
897
1031
|
.default({ width: 1280, height: 720 }),
|
|
898
1032
|
/** Named frames used by actions, mapping name -> iframe selector. */
|
|
899
1033
|
frames: z.record(z.string(), z.string()).default({}),
|
|
@@ -1165,6 +1299,8 @@ export const ComposeReportSchema = z.object({
|
|
|
1165
1299
|
hold: z.string(),
|
|
1166
1300
|
/** output/poster.png when `output.poster` is set. */
|
|
1167
1301
|
poster: z.string().optional(),
|
|
1302
|
+
/** Ms of blank (contentless) video at the very start of the final cut. */
|
|
1303
|
+
blankOpenMs: z.number().optional(),
|
|
1168
1304
|
warnings: z.array(ComposeWarningSchema),
|
|
1169
1305
|
});
|
|
1170
1306
|
export type ComposeReport = z.infer<typeof ComposeReportSchema>;
|