@tandryukha/aidemo 0.8.0 → 0.14.1
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/.claude/skills/record-demo/SKILL.md +5 -2
- package/README.md +16 -4
- package/docs/AUTHORING.md +609 -45
- package/package.json +3 -2
- package/src/anchors.ts +128 -0
- package/src/attention.ts +365 -0
- package/src/caption-render.ts +17 -8
- package/src/captions.ts +70 -6
- package/src/capture.ts +113 -0
- package/src/cards.ts +15 -6
- package/src/cli.ts +580 -22
- package/src/compose.ts +1006 -93
- package/src/cursor-overlay.ts +7 -113
- package/src/cursor.ts +9 -3
- package/src/distribute.ts +37 -4
- package/src/expr.ts +130 -0
- package/src/ffmpeg.ts +156 -0
- package/src/frame.ts +205 -0
- package/src/frames.ts +159 -0
- package/src/golden.ts +27 -0
- package/src/guide.ts +102 -0
- package/src/i18n.ts +7 -4
- package/src/import-trace.ts +629 -0
- package/src/inspect.ts +307 -0
- package/src/lint.ts +601 -0
- package/src/login.ts +143 -0
- package/src/mcp/jobs.ts +4 -1
- package/src/mcp/server.ts +403 -26
- package/src/player.ts +975 -32
- package/src/profile.ts +128 -0
- package/src/project.ts +12 -2
- package/src/recorder.ts +252 -15
- package/src/setup.ts +248 -0
- package/src/starter.ts +244 -0
- package/src/stills.ts +15 -4
- package/src/types.ts +643 -5
- package/src/variants.ts +1 -1
- package/src/voice-local.ts +7 -1
- package/src/voice.ts +60 -1
- package/src/walkthrough.ts +250 -0
- package/src/zoom.ts +85 -39
package/src/types.ts
CHANGED
|
@@ -42,7 +42,35 @@ const TargetSchema = z.object({
|
|
|
42
42
|
});
|
|
43
43
|
export type Target = z.infer<typeof TargetSchema>;
|
|
44
44
|
|
|
45
|
-
const BaseAction = {
|
|
45
|
+
const BaseAction = {
|
|
46
|
+
comment: z.string().optional(),
|
|
47
|
+
/**
|
|
48
|
+
* Best-effort action: when its target never appears or the interaction
|
|
49
|
+
* fails, log and continue instead of failing the take. For state-dependent
|
|
50
|
+
* one-time UI — e.g. collapsing the ChatGPT sidebar persists in the Chrome
|
|
51
|
+
* profile, so a re-run finds the close button absent and a required click
|
|
52
|
+
* would kill the run. Interaction ops (click/type/hover/scrollTo/focus)
|
|
53
|
+
* probe briefly for the target instead of stalling out the full action
|
|
54
|
+
* timeout; wait ops honor their own timeoutMs and then continue. Don't mark
|
|
55
|
+
* actions the demo's visuals depend on — a skipped optional action is only
|
|
56
|
+
* a log line.
|
|
57
|
+
*/
|
|
58
|
+
optional: z.boolean().optional(),
|
|
59
|
+
/**
|
|
60
|
+
* Re-attempt an interaction (click/type/hover/scrollTo/focus/moveTo/assert)
|
|
61
|
+
* up to this many extra times when it throws — a beat apart — before the
|
|
62
|
+
* take fails. For UI that re-renders under the cursor (a list that
|
|
63
|
+
* reorders on load, a button that mounts twice). Retries land in
|
|
64
|
+
* timeline.json `actions[].retries`. Optional actions skip instead.
|
|
65
|
+
*/
|
|
66
|
+
retry: z.number().int().min(0).max(5).optional(),
|
|
67
|
+
/**
|
|
68
|
+
* Narration anchor: land this action on the word marked `{{@name}}` in the
|
|
69
|
+
* scene's narration. Compose retimes the scene piecewise so the beat plays
|
|
70
|
+
* as the word is spoken. See docs/AUTHORING.md "Narration-anchored beats".
|
|
71
|
+
*/
|
|
72
|
+
anchor: z.string().regex(/^[\w.-]+$/).optional(),
|
|
73
|
+
};
|
|
46
74
|
|
|
47
75
|
/**
|
|
48
76
|
* Scroll easing presets. All are eased wheel scrolls; they differ in feel:
|
|
@@ -52,12 +80,39 @@ const BaseAction = { comment: z.string().optional() };
|
|
|
52
80
|
export const EasingPresetSchema = z.enum(["smooth", "snappy", "glide", "linear"]);
|
|
53
81
|
export type EasingPreset = z.infer<typeof EasingPresetSchema>;
|
|
54
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Element state a wait/scroll resolves on. "visible" (default) is Playwright's
|
|
85
|
+
* non-empty-box visibility; "attached" only requires the element to be in the
|
|
86
|
+
* DOM — the escape hatch for zero-height lazy mount points.
|
|
87
|
+
*/
|
|
88
|
+
export const WaitStateSchema = z.enum(["visible", "attached"]);
|
|
89
|
+
export type WaitState = z.infer<typeof WaitStateSchema>;
|
|
90
|
+
|
|
91
|
+
/** Look of an attention overlay (highlight ring, spotlight edge, callout pill). */
|
|
92
|
+
export const AttentionStyleSchema = z.object({
|
|
93
|
+
/** CSS color. Default: the storyboard `attention.color`, else #ff5a5f. */
|
|
94
|
+
color: z.string().optional(),
|
|
95
|
+
/** Outline thickness, px. Default 3. */
|
|
96
|
+
thickness: z.number().min(1).max(12).optional(),
|
|
97
|
+
/** Room between the element's box and the outline, px. Default 6. */
|
|
98
|
+
padding: z.number().min(0).max(60).optional(),
|
|
99
|
+
/** "box" (rounded rectangle, default) or "ring" (pill/circle). */
|
|
100
|
+
shape: z.enum(["box", "ring"]).optional(),
|
|
101
|
+
});
|
|
102
|
+
export type AttentionStyle = z.infer<typeof AttentionStyleSchema>;
|
|
103
|
+
|
|
55
104
|
export const ActionSchema = z.discriminatedUnion("op", [
|
|
56
105
|
z.object({ ...BaseAction, op: z.literal("goto"), url: z.string() }),
|
|
57
106
|
z.object({
|
|
58
107
|
...BaseAction,
|
|
59
108
|
op: z.literal("click"),
|
|
60
109
|
target: TargetSchema,
|
|
110
|
+
/**
|
|
111
|
+
* When the click opens a NEW TAB (target=_blank, window.open), close it and
|
|
112
|
+
* continue in the recorded tab at that URL — the take is one window, a
|
|
113
|
+
* second tab is never in the video. Default false (a popup is left alone).
|
|
114
|
+
*/
|
|
115
|
+
followPopup: z.boolean().optional(),
|
|
61
116
|
}),
|
|
62
117
|
z.object({
|
|
63
118
|
...BaseAction,
|
|
@@ -67,7 +122,13 @@ export const ActionSchema = z.discriminatedUnion("op", [
|
|
|
67
122
|
/** Per-keystroke jitter for a human cadence. Default true. */
|
|
68
123
|
humanize: z.boolean().optional(),
|
|
69
124
|
}),
|
|
70
|
-
z.object({
|
|
125
|
+
z.object({
|
|
126
|
+
...BaseAction,
|
|
127
|
+
op: z.literal("press"),
|
|
128
|
+
key: z.string(),
|
|
129
|
+
/** Show a keystroke chip for this press (overrides the top-level `keystrokes`). */
|
|
130
|
+
keystrokes: z.boolean().optional(),
|
|
131
|
+
}),
|
|
71
132
|
z.object({
|
|
72
133
|
...BaseAction,
|
|
73
134
|
op: z.literal("hover"),
|
|
@@ -81,22 +142,51 @@ export const ActionSchema = z.discriminatedUnion("op", [
|
|
|
81
142
|
easing: EasingPresetSchema.optional(),
|
|
82
143
|
/** Override the preset's duration, ms. */
|
|
83
144
|
durationMs: z.number().optional(),
|
|
145
|
+
/**
|
|
146
|
+
* Which element state to wait for before scrolling. Default "visible".
|
|
147
|
+
* "attached" accepts a zero-size mount point (a lazily-filled empty <div>
|
|
148
|
+
* that only renders once scrolled into view) — such an element is in the
|
|
149
|
+
* DOM but never "visible", so the default would time out (issue #44).
|
|
150
|
+
*/
|
|
151
|
+
state: WaitStateSchema.optional(),
|
|
152
|
+
/**
|
|
153
|
+
* After the scroll, wait until the scroll position has been still for this
|
|
154
|
+
* many ms (bounded at 3 s) — an honest "scroll finished" instead of a fixed
|
|
155
|
+
* pause against smooth-scroll settle races. Omit for the old fixed settle.
|
|
156
|
+
*/
|
|
157
|
+
settleMs: z.number().optional(),
|
|
84
158
|
}),
|
|
85
159
|
z.object({
|
|
86
160
|
...BaseAction,
|
|
87
161
|
op: z.literal("scrollBy"),
|
|
88
162
|
/** CSS pixels to scroll; positive = down. */
|
|
89
163
|
dy: z.number(),
|
|
164
|
+
/**
|
|
165
|
+
* Scope the scroll to this element's own scroller: the wheel is dispatched
|
|
166
|
+
* over the element, and `dy` is clamped to the room its nearest scrollable
|
|
167
|
+
* ancestor actually has — so a bottomed-out inner panel never chains the
|
|
168
|
+
* wheel to the page and slides the whole app off-screen (issue #43). Omit
|
|
169
|
+
* to scroll whatever is under the cursor (the page, usually).
|
|
170
|
+
*/
|
|
171
|
+
target: TargetSchema.optional(),
|
|
90
172
|
/** Scroll feel preset. Default "smooth". */
|
|
91
173
|
easing: EasingPresetSchema.optional(),
|
|
92
174
|
/** Override the preset's duration, ms. */
|
|
93
175
|
durationMs: z.number().optional(),
|
|
176
|
+
/** Wait until the scroll position has been still for this many ms (see scrollTo). */
|
|
177
|
+
settleMs: z.number().optional(),
|
|
94
178
|
}),
|
|
95
179
|
z.object({
|
|
96
180
|
...BaseAction,
|
|
97
181
|
op: z.literal("waitFor"),
|
|
98
182
|
target: TargetSchema,
|
|
99
183
|
timeoutMs: z.number().optional(),
|
|
184
|
+
/**
|
|
185
|
+
* Element state to wait for. Default "visible". "attached" = present in
|
|
186
|
+
* the DOM even if zero-size/hidden — for lazily-filled mount points that
|
|
187
|
+
* only become visible once something renders into them (issue #44).
|
|
188
|
+
*/
|
|
189
|
+
state: WaitStateSchema.optional(),
|
|
100
190
|
}),
|
|
101
191
|
/**
|
|
102
192
|
* Wait for an IN-PLACE mutation of an existing element — the gap that neither
|
|
@@ -183,6 +273,109 @@ export const ActionSchema = z.discriminatedUnion("op", [
|
|
|
183
273
|
* across the storyboard are a hard error at extraction.
|
|
184
274
|
*/
|
|
185
275
|
z.object({ ...BaseAction, op: z.literal("still"), name: z.string() }),
|
|
276
|
+
/**
|
|
277
|
+
* Attention beats — compose-time overlays drawn from a box the player
|
|
278
|
+
* measures (like `focus`, they are timeline markers; the drawing happens at
|
|
279
|
+
* compose, so restyling is a recompose). Each dwells briefly at record time
|
|
280
|
+
* (min(holdMs, 800) ms) so the eye registers it before the next action.
|
|
281
|
+
*/
|
|
282
|
+
/** Outline `target` (box or ring) for `holdMs`. */
|
|
283
|
+
z.object({
|
|
284
|
+
...BaseAction,
|
|
285
|
+
op: z.literal("highlight"),
|
|
286
|
+
target: TargetSchema,
|
|
287
|
+
/** How long the outline stays, ms. Default 1600. */
|
|
288
|
+
holdMs: z.number().min(100).max(15000).optional(),
|
|
289
|
+
style: AttentionStyleSchema.optional(),
|
|
290
|
+
}),
|
|
291
|
+
/** Dim everything except `target` for `holdMs`. */
|
|
292
|
+
z.object({
|
|
293
|
+
...BaseAction,
|
|
294
|
+
op: z.literal("spotlight"),
|
|
295
|
+
target: TargetSchema,
|
|
296
|
+
holdMs: z.number().min(100).max(15000).optional(),
|
|
297
|
+
/** Opacity of the dim layer, 0–0.9. Default 0.55. */
|
|
298
|
+
dimTo: z.number().min(0).max(0.9).optional(),
|
|
299
|
+
/** Extra room around the element, px. Default 10. */
|
|
300
|
+
padding: z.number().min(0).max(80).optional(),
|
|
301
|
+
style: AttentionStyleSchema.optional(),
|
|
302
|
+
}),
|
|
303
|
+
/** Pin a short label to `target` for `holdMs`. */
|
|
304
|
+
z.object({
|
|
305
|
+
...BaseAction,
|
|
306
|
+
op: z.literal("callout"),
|
|
307
|
+
target: TargetSchema,
|
|
308
|
+
text: z.string().min(1).max(80),
|
|
309
|
+
/** Where the label sits relative to the element. Default "auto". */
|
|
310
|
+
placement: z.enum(["auto", "top", "bottom", "left", "right"]).optional(),
|
|
311
|
+
holdMs: z.number().min(100).max(15000).optional(),
|
|
312
|
+
style: AttentionStyleSchema.optional(),
|
|
313
|
+
}),
|
|
314
|
+
/**
|
|
315
|
+
* Park the cursor deliberately: glide it to `target` (element center) or an
|
|
316
|
+
* absolute viewport point (`x`,`y` CSS px) without clicking. Use it to move
|
|
317
|
+
* the cursor out of the way before a reveal, or to point at what the
|
|
318
|
+
* narration is talking about without an interaction.
|
|
319
|
+
*/
|
|
320
|
+
z.object({
|
|
321
|
+
...BaseAction,
|
|
322
|
+
op: z.literal("moveTo"),
|
|
323
|
+
target: TargetSchema.optional(),
|
|
324
|
+
x: z.number().optional(),
|
|
325
|
+
y: z.number().optional(),
|
|
326
|
+
}),
|
|
327
|
+
/**
|
|
328
|
+
* Prove the payoff happened. Fails the take with a named error unless
|
|
329
|
+
* `target` is visible (and, when set, its text matches `textMatches`) and/or
|
|
330
|
+
* the page URL matches `url` — polled until `timeoutMs` (default 5000). A
|
|
331
|
+
* demo whose confirmation never rendered should fail loudly at record time,
|
|
332
|
+
* not ship a video of a spinner.
|
|
333
|
+
*/
|
|
334
|
+
z.object({
|
|
335
|
+
...BaseAction,
|
|
336
|
+
op: z.literal("assert"),
|
|
337
|
+
target: TargetSchema.optional(),
|
|
338
|
+
/** Regex the target's text must match (JS syntax, e.g. "Order #\\d+"). */
|
|
339
|
+
textMatches: z.string().optional(),
|
|
340
|
+
/** Regex the page URL must match. */
|
|
341
|
+
url: z.string().optional(),
|
|
342
|
+
timeoutMs: z.number().optional(),
|
|
343
|
+
}),
|
|
344
|
+
/** Choose an option in a native <select> by value or visible label (the cursor clicks it first). */
|
|
345
|
+
z.object({
|
|
346
|
+
...BaseAction,
|
|
347
|
+
op: z.literal("select"),
|
|
348
|
+
target: TargetSchema,
|
|
349
|
+
value: z.string().optional(),
|
|
350
|
+
label: z.string().optional(),
|
|
351
|
+
}),
|
|
352
|
+
/**
|
|
353
|
+
* Drag `target` to `to` (another target, or absolute viewport `x`,`y`):
|
|
354
|
+
* press, glide with the cursor, release — the drag shows in the take.
|
|
355
|
+
*/
|
|
356
|
+
z.object({
|
|
357
|
+
...BaseAction,
|
|
358
|
+
op: z.literal("drag"),
|
|
359
|
+
target: TargetSchema,
|
|
360
|
+
to: z.object({
|
|
361
|
+
target: TargetSchema.optional(),
|
|
362
|
+
x: z.number().optional(),
|
|
363
|
+
y: z.number().optional(),
|
|
364
|
+
}),
|
|
365
|
+
}),
|
|
366
|
+
/**
|
|
367
|
+
* Attach files: `target` is a file input (set directly) or the button that
|
|
368
|
+
* opens the OS file chooser (clicked; the chooser is answered headlessly —
|
|
369
|
+
* no dialog appears in the take). Paths resolve against the demo dir.
|
|
370
|
+
*/
|
|
371
|
+
z.object({
|
|
372
|
+
...BaseAction,
|
|
373
|
+
op: z.literal("upload"),
|
|
374
|
+
target: TargetSchema,
|
|
375
|
+
files: z.array(z.string()).min(1),
|
|
376
|
+
}),
|
|
377
|
+
/** Browser back (history), with the same readiness wait as `goto`. */
|
|
378
|
+
z.object({ ...BaseAction, op: z.literal("back") }),
|
|
186
379
|
]);
|
|
187
380
|
export type Action = z.infer<typeof ActionSchema>;
|
|
188
381
|
|
|
@@ -201,6 +394,13 @@ export const VoicePlanSchema = z.object({
|
|
|
201
394
|
instructions: z.string().optional(),
|
|
202
395
|
/** Playback speed hint passed to the provider when supported. Default 1.0. */
|
|
203
396
|
speed: z.number().min(0.5).max(2).optional(),
|
|
397
|
+
/**
|
|
398
|
+
* TTS-only substitutions, whole-word, case-sensitive: `{"aidemo":"A.I. demo",
|
|
399
|
+
* "SQL":"sequel"}`. Applied to the text sent to the voice provider; the
|
|
400
|
+
* narration (and therefore the captions) keeps the written form. Merged
|
|
401
|
+
* storyboard → scene (scene entries win).
|
|
402
|
+
*/
|
|
403
|
+
pronounce: z.record(z.string(), z.string()).optional(),
|
|
204
404
|
});
|
|
205
405
|
export type VoicePlan = z.infer<typeof VoicePlanSchema>;
|
|
206
406
|
|
|
@@ -217,8 +417,127 @@ export const MusicCueSchema = z.object({
|
|
|
217
417
|
// Scene + Storyboard
|
|
218
418
|
// ---------------------------------------------------------------------------
|
|
219
419
|
|
|
420
|
+
/**
|
|
421
|
+
* A region to blur at compose time. The player re-measures the selector after
|
|
422
|
+
* every action (all matches, up to 8) and logs the spans; compose crops, blurs
|
|
423
|
+
* and overlays each span back — so a price, an email, a token never ships,
|
|
424
|
+
* and the strength is a recompose. Frame-relative selectors use `frame`.
|
|
425
|
+
*/
|
|
426
|
+
export const RedactSchema = z.object({
|
|
427
|
+
selector: z.string(),
|
|
428
|
+
frame: z.string().optional(),
|
|
429
|
+
/** boxblur radius, px at the logical size. Default 14. */
|
|
430
|
+
blur: z.number().min(2).max(60).optional(),
|
|
431
|
+
});
|
|
432
|
+
export type Redact = z.infer<typeof RedactSchema>;
|
|
433
|
+
|
|
434
|
+
/** Caption strip placement + look. */
|
|
435
|
+
export const CaptionsConfigSchema = z.object({
|
|
436
|
+
/**
|
|
437
|
+
* "bottom" (default) or "top". With "auto-flip" (the default behaviour when
|
|
438
|
+
* omitted at scene level) compose moves the strip to the top while a
|
|
439
|
+
* highlight/spotlight/callout box or a keystroke chip would collide with it.
|
|
440
|
+
*/
|
|
441
|
+
position: z.enum(["bottom", "top"]).optional(),
|
|
442
|
+
/** "pill" (default, centered rounded box), "bar" (full-width band), "none" (no captions burned). */
|
|
443
|
+
style: z.enum(["pill", "bar", "none"]).optional(),
|
|
444
|
+
/** CSS font-family. Default: the brand font, else the system sans. */
|
|
445
|
+
font: z.string().optional(),
|
|
446
|
+
/** Font size, px at the logical width. Default 30. */
|
|
447
|
+
size: z.number().min(14).max(72).optional(),
|
|
448
|
+
/** Text color. Default #fff. */
|
|
449
|
+
color: z.string().optional(),
|
|
450
|
+
/** Pill/bar background (CSS). Default rgba(12,14,22,.66). */
|
|
451
|
+
background: z.string().optional(),
|
|
452
|
+
});
|
|
453
|
+
export type CaptionsConfig = z.infer<typeof CaptionsConfigSchema>;
|
|
454
|
+
|
|
455
|
+
/** Storyboard-wide defaults for the attention overlays. */
|
|
456
|
+
export const AttentionConfigSchema = z.object({
|
|
457
|
+
/** Default outline / pill accent color. Default #ff5a5f. */
|
|
458
|
+
color: z.string().optional(),
|
|
459
|
+
/** Draw a click ring where every click lands (compose-time cursor only). */
|
|
460
|
+
clicks: z.boolean().optional(),
|
|
461
|
+
});
|
|
462
|
+
export type AttentionConfig = z.infer<typeof AttentionConfigSchema>;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Produced-look frame around the content: padding, background, rounded
|
|
466
|
+
* corners, drop shadow, optional browser chrome. Compose-time; captions and
|
|
467
|
+
* cards render at the framed canvas size. Omit for the raw recording frame.
|
|
468
|
+
*/
|
|
469
|
+
export const FrameSchema = z.object({
|
|
470
|
+
/** Padding around the video, px at the logical size. Default 48. */
|
|
471
|
+
padding: z.number().min(0).max(400).optional(),
|
|
472
|
+
/** CSS background (color or gradient). Default: dark gradient tinted by brand.accent. */
|
|
473
|
+
background: z.string().optional(),
|
|
474
|
+
/** Corner radius of the video window, px. Default 14. */
|
|
475
|
+
radius: z.number().min(0).max(80).optional(),
|
|
476
|
+
/** Drop shadow under the window. Default true. */
|
|
477
|
+
shadow: z.boolean().optional(),
|
|
478
|
+
/**
|
|
479
|
+
* Window chrome: "none" (default), "browser" / "mac" (a bar with dots or
|
|
480
|
+
* traffic lights above the video), or a device bezel for phone-sized
|
|
481
|
+
* viewports — "iphone" (rounded bezel + dynamic island) / "android" (bezel +
|
|
482
|
+
* punch-hole camera). Device chrome adds no bar; `radius` defaults to 40.
|
|
483
|
+
*/
|
|
484
|
+
chrome: z.enum(["none", "browser", "mac", "iphone", "android"]).optional(),
|
|
485
|
+
/**
|
|
486
|
+
* Device chrome only: reserve a status-bar strip of bezel ABOVE the video
|
|
487
|
+
* (px at the logical size) instead of drawing the island / punch-hole over
|
|
488
|
+
* the recording's top pixels. `true` = 46 px (an iPhone status bar). Use it
|
|
489
|
+
* when the app paints something at the very top that must stay visible.
|
|
490
|
+
*/
|
|
491
|
+
safeTop: z.union([z.boolean(), z.number().min(0).max(120)]).optional(),
|
|
492
|
+
/** Text in the chrome's address pill (url wins over title). */
|
|
493
|
+
title: z.string().optional(),
|
|
494
|
+
url: z.string().optional(),
|
|
495
|
+
});
|
|
496
|
+
export type Frame = z.infer<typeof FrameSchema>;
|
|
497
|
+
|
|
498
|
+
/** Brand kit applied across cards, frame, captions, callouts and a watermark. */
|
|
499
|
+
export const BrandSchema = z.object({
|
|
500
|
+
/** Logo image path (png/svg/jpg), relative to the demo dir or absolute. Used as the watermark. */
|
|
501
|
+
logo: z.string().optional(),
|
|
502
|
+
/** Accent color: card rule, frame tint, default attention color. */
|
|
503
|
+
accent: z.string().optional(),
|
|
504
|
+
/** CSS font-family for cards, captions, callouts, chrome. */
|
|
505
|
+
font: z.string().optional(),
|
|
506
|
+
/** Watermark placement for `logo`. */
|
|
507
|
+
watermark: z
|
|
508
|
+
.object({
|
|
509
|
+
position: z
|
|
510
|
+
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
511
|
+
.optional(),
|
|
512
|
+
/** 0–1. Default 0.85. */
|
|
513
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
514
|
+
/** Logo width as a fraction of the frame width. Default 0.11. */
|
|
515
|
+
scale: z.number().min(0.03).max(0.5).optional(),
|
|
516
|
+
/** Set false to keep the logo out of the video (cards only). */
|
|
517
|
+
enabled: z.boolean().optional(),
|
|
518
|
+
})
|
|
519
|
+
.optional(),
|
|
520
|
+
});
|
|
521
|
+
export type Brand = z.infer<typeof BrandSchema>;
|
|
522
|
+
|
|
523
|
+
/** Tuning for compose-time idle detection (`autoIdle`). */
|
|
524
|
+
export const AutoIdleSchema = z.object({
|
|
525
|
+
enabled: z.boolean().optional(),
|
|
526
|
+
/** Shortest motionless span that counts as idle, ms. Default 1500. */
|
|
527
|
+
minMs: z.number().min(400).max(20000).optional(),
|
|
528
|
+
/**
|
|
529
|
+
* Per-pixel noise floor below which two frames count as identical (ffmpeg
|
|
530
|
+
* `freezedetect` noise, 0..1). Default 0.003 — tolerant of encoder dither,
|
|
531
|
+
* tight enough that a blinking caret still reads as motion.
|
|
532
|
+
*/
|
|
533
|
+
noise: z.number().min(0).max(0.2).optional(),
|
|
534
|
+
});
|
|
535
|
+
export type AutoIdle = z.infer<typeof AutoIdleSchema>;
|
|
536
|
+
|
|
220
537
|
export const SceneSchema = z.object({
|
|
221
538
|
id: z.string(),
|
|
539
|
+
/** Chapter title for this beat (`output.chapters`); defaults to the scene id. */
|
|
540
|
+
title: z.string().optional(),
|
|
222
541
|
/** Spoken narration for this beat. Also the caption source of truth. */
|
|
223
542
|
narration: z.string(),
|
|
224
543
|
/**
|
|
@@ -230,10 +549,26 @@ export const SceneSchema = z.object({
|
|
|
230
549
|
* LLM to translate. A code missing here falls back to `narration`.
|
|
231
550
|
*/
|
|
232
551
|
narrations: z.record(z.string(), z.string()).optional(),
|
|
552
|
+
/**
|
|
553
|
+
* Engine-populated at parse time from `{{@name}}` markers in `narration`:
|
|
554
|
+
* name → 0-based index of the word the beat lands on. Don't author this —
|
|
555
|
+
* write the marker in the narration instead.
|
|
556
|
+
*/
|
|
557
|
+
anchors: z.record(z.string(), z.number().int().min(0)).optional(),
|
|
558
|
+
/** Same, per `narrations` language code. */
|
|
559
|
+
narrationAnchors: z.record(z.string(), z.record(z.string(), z.number().int().min(0))).optional(),
|
|
233
560
|
voice: VoicePlanSchema.optional(),
|
|
234
561
|
music: MusicCueSchema.optional(),
|
|
235
562
|
/** Set false to suppress auto-zoom for this scene's clicks/typing. */
|
|
236
563
|
zoom: z.boolean().optional(),
|
|
564
|
+
/** Override the top-level `autoIdle` for this scene (true enables, false disables). */
|
|
565
|
+
autoIdle: z.boolean().optional(),
|
|
566
|
+
/** Caption placement for this scene (overrides top-level `captions.position`). */
|
|
567
|
+
captions: CaptionsConfigSchema.optional(),
|
|
568
|
+
/** Blur these regions while this scene records (adds to top-level `redact`). */
|
|
569
|
+
redact: z.array(RedactSchema).optional(),
|
|
570
|
+
/** Hide these selectors for this scene (adds to top-level `hide`). */
|
|
571
|
+
hide: z.array(z.string()).optional(),
|
|
237
572
|
actions: z.array(ActionSchema).default([]),
|
|
238
573
|
});
|
|
239
574
|
export type Scene = z.infer<typeof SceneSchema>;
|
|
@@ -317,6 +652,10 @@ export const CursorConfigSchema = z.object({
|
|
|
317
652
|
hideScenes: z.array(z.string()).optional(),
|
|
318
653
|
/** Cursor size multiplier (1 = the 24px baseline arrow). */
|
|
319
654
|
scale: z.number().min(0.5).max(4).optional(),
|
|
655
|
+
/** Pointer look: classic "arrow" (default) or a soft "dot" (screen-recorder style). */
|
|
656
|
+
style: z.enum(["arrow", "dot"]).optional(),
|
|
657
|
+
/** Dot style only: fill color. Default rgba(255,90,95,.85). */
|
|
658
|
+
color: z.string().optional(),
|
|
320
659
|
});
|
|
321
660
|
export type CursorConfig = z.infer<typeof CursorConfigSchema>;
|
|
322
661
|
|
|
@@ -389,8 +728,33 @@ export type Loudness = z.infer<typeof LoudnessSchema>;
|
|
|
389
728
|
* `loudness` (independent of resize) tunes the master level; omit it entirely
|
|
390
729
|
* for the automatic default (loudnorm when there's music, none otherwise).
|
|
391
730
|
*/
|
|
731
|
+
/** Named output presets — fill width/height/fit when they are not set explicitly. */
|
|
732
|
+
export const OutputPresetSchema = z.enum(["youtube", "short", "readme-gif", "x"]);
|
|
733
|
+
export type OutputPreset = z.infer<typeof OutputPresetSchema>;
|
|
734
|
+
export const OUTPUT_PRESETS: Record<
|
|
735
|
+
OutputPreset,
|
|
736
|
+
{ width: number; height: number; fit: "contain" | "cover" }
|
|
737
|
+
> = {
|
|
738
|
+
youtube: { width: 1920, height: 1080, fit: "contain" },
|
|
739
|
+
short: { width: 1080, height: 1920, fit: "contain" },
|
|
740
|
+
"readme-gif": { width: 960, height: 540, fit: "contain" },
|
|
741
|
+
x: { width: 1280, height: 720, fit: "contain" },
|
|
742
|
+
};
|
|
743
|
+
|
|
392
744
|
export const OutputSchema = z
|
|
393
745
|
.object({
|
|
746
|
+
/** Preset sizing (see OUTPUT_PRESETS); explicit width/height/fit win. */
|
|
747
|
+
preset: OutputPresetSchema.optional(),
|
|
748
|
+
/** Write MP4 chapter markers, one per scene (scene `title` or id). Default false. */
|
|
749
|
+
chapters: z.boolean().optional(),
|
|
750
|
+
/** Also extract output/poster.png (first content frame after the intro). Default false. */
|
|
751
|
+
poster: z.boolean().optional(),
|
|
752
|
+
/**
|
|
753
|
+
* Also export output/walkthrough/ (index.html + guide.md + per-scene
|
|
754
|
+
* frames + captions) after every render. Default false; `aidemo
|
|
755
|
+
* walkthrough <dir>` / the `walkthrough` job export on demand.
|
|
756
|
+
*/
|
|
757
|
+
walkthrough: z.boolean().optional(),
|
|
394
758
|
width: z.number().optional(),
|
|
395
759
|
height: z.number().optional(),
|
|
396
760
|
/**
|
|
@@ -415,6 +779,85 @@ export const OutputSchema = z
|
|
|
415
779
|
});
|
|
416
780
|
export type Output = z.infer<typeof OutputSchema>;
|
|
417
781
|
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* What compose does when a scene's narration outlasts its recorded action
|
|
785
|
+
* (after the ≤1.6x slow-down): the remainder is a HOLD on the last frame.
|
|
786
|
+
* Default "freeze" clones that frame (byte-for-byte the pre-`hold` behavior).
|
|
787
|
+
* "drift" instead pushes in very slowly on it (a Ken-Burns-style creep of
|
|
788
|
+
* `driftScale` over the hold), so a 10 s hold reads as a deliberate dwell
|
|
789
|
+
* rather than a stalled video — the fix for narration-heavy demos where half
|
|
790
|
+
* the runtime was a frozen frame (issue #40). `backoffMs` takes the held frame
|
|
791
|
+
* from slightly BEFORE the segment's end, dodging a half-painted transition or
|
|
792
|
+
* a hover state caught mid-flight. Opt-in: omit the key for the old behavior.
|
|
793
|
+
*/
|
|
794
|
+
export const HoldSchema = z.object({
|
|
795
|
+
mode: z.enum(["freeze", "drift"]).default("freeze"),
|
|
796
|
+
/** Drift mode: zoom level reached at the end of the hold. Default 1.06. */
|
|
797
|
+
driftScale: z.number().min(1).max(1.5).default(1.06),
|
|
798
|
+
/** Take the held frame this many ms before the segment end. Default 0. */
|
|
799
|
+
backoffMs: z.number().min(0).max(2000).default(0),
|
|
800
|
+
});
|
|
801
|
+
export type HoldConfig = z.infer<typeof HoldSchema>;
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* One cookie to seed into the recording profile before the take. Mirrors
|
|
805
|
+
* Playwright's cookie shape; `domain` + `path` (or `url`) place it.
|
|
806
|
+
*/
|
|
807
|
+
export const SeedCookieSchema = z.object({
|
|
808
|
+
name: z.string(),
|
|
809
|
+
value: z.string(),
|
|
810
|
+
/** Cookie domain, e.g. ".example.com" or "app.example.com". */
|
|
811
|
+
domain: z.string().optional(),
|
|
812
|
+
/** Cookie path. Default "/". */
|
|
813
|
+
path: z.string().optional(),
|
|
814
|
+
/** Alternative to domain+path: the URL the cookie belongs to. */
|
|
815
|
+
url: z.string().optional(),
|
|
816
|
+
/** Unix time in seconds; omit for a session cookie. */
|
|
817
|
+
expires: z.number().optional(),
|
|
818
|
+
httpOnly: z.boolean().optional(),
|
|
819
|
+
secure: z.boolean().optional(),
|
|
820
|
+
sameSite: z.enum(["Strict", "Lax", "None"]).optional(),
|
|
821
|
+
});
|
|
822
|
+
export type SeedCookie = z.infer<typeof SeedCookieSchema>;
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* How the take is prepared BEFORE the first action — the answer to "the
|
|
826
|
+
* feature is behind a cookie gate / a preview token / a rotating fixture"
|
|
827
|
+
* without a hand-written Playwright seed script (issue #44). Everything here
|
|
828
|
+
* is opt-in; a storyboard without `setup` records exactly as before.
|
|
829
|
+
*/
|
|
830
|
+
export const SetupSchema = z.object({
|
|
831
|
+
/**
|
|
832
|
+
* Path (relative to the demo dir, or absolute) to a Playwright storageState
|
|
833
|
+
* JSON (`{cookies:[…], origins:[{origin, localStorage:[{name,value}]}]}`),
|
|
834
|
+
* e.g. one saved by `context.storageState({path})`. Its cookies are added
|
|
835
|
+
* to the recording profile and each origin's localStorage entries are set
|
|
836
|
+
* before the storyboard runs. CLI: `--storage-state <file>`.
|
|
837
|
+
*/
|
|
838
|
+
storageState: z.string().optional(),
|
|
839
|
+
/** Cookies to add before the take. CLI: `--cookie name=value;domain=host` (repeatable). */
|
|
840
|
+
cookies: z.array(SeedCookieSchema).optional(),
|
|
841
|
+
/**
|
|
842
|
+
* Shell command run from the demo dir before EVERY take (record/probe/render),
|
|
843
|
+
* after the profile is resolved and before Chrome launches — e.g. a script
|
|
844
|
+
* that re-seeds a fixture, or patches the storyboard for whichever variant
|
|
845
|
+
* the app will serve today. Env: AIDEMO_DEMO_DIR, AIDEMO_STORYBOARD (the
|
|
846
|
+
* storyboard path — re-read after the hook, so edits are honored),
|
|
847
|
+
* AIDEMO_PROFILE (the Chrome user-data dir the take will use). A non-zero
|
|
848
|
+
* exit aborts the take.
|
|
849
|
+
*/
|
|
850
|
+
preflight: z.string().optional(),
|
|
851
|
+
/**
|
|
852
|
+
* Acknowledge that the recording profile is deliberately seeded (a login,
|
|
853
|
+
* a cookie gate) and silence the carried-over-state warning for this
|
|
854
|
+
* storyboard. Implied when storageState/cookies are given. CLI:
|
|
855
|
+
* `--profile-seeded`.
|
|
856
|
+
*/
|
|
857
|
+
expectState: z.boolean().optional(),
|
|
858
|
+
});
|
|
859
|
+
export type Setup = z.infer<typeof SetupSchema>;
|
|
860
|
+
|
|
418
861
|
export const StoryboardSchema = z.object({
|
|
419
862
|
title: z.string(),
|
|
420
863
|
/**
|
|
@@ -429,6 +872,15 @@ export const StoryboardSchema = z.object({
|
|
|
429
872
|
* fallback for non-English narration. Omit for English / to auto-detect.
|
|
430
873
|
*/
|
|
431
874
|
language: z.string().optional(),
|
|
875
|
+
/**
|
|
876
|
+
* Product names, brands and jargon that Whisper reliably mis-hears — the
|
|
877
|
+
* transcript is what gets burned in, so "fitness.ee" coming back as
|
|
878
|
+
* "fitness topam" ships as a caption (issue #38). Listed here they are sent
|
|
879
|
+
* at the FRONT of the STT prompt, inside the window Whisper actually biases
|
|
880
|
+
* on, even when the script itself is long. Cheaper than `captions --offline`,
|
|
881
|
+
* which fixes spelling but gives up real word timings.
|
|
882
|
+
*/
|
|
883
|
+
knownTerms: z.array(z.string()).optional(),
|
|
432
884
|
/**
|
|
433
885
|
* Declared template parameters: name → default value. Enables `{{name}}`
|
|
434
886
|
* placeholders in any storyboard string (narration, action url, `type` text,
|
|
@@ -461,6 +913,32 @@ export const StoryboardSchema = z.object({
|
|
|
461
913
|
motionBlur: MotionBlurSchema.optional(),
|
|
462
914
|
/** Compose-time cursor control (hide/resize post-hoc). Omit to bake the cursor. */
|
|
463
915
|
cursor: CursorConfigSchema.optional(),
|
|
916
|
+
/** Pre-take preparation: cookies / storageState seeding, a preflight hook. Opt-in. */
|
|
917
|
+
setup: SetupSchema.optional(),
|
|
918
|
+
/** How compose fills narration that outlasts a scene's action (freeze | drift). Opt-in. */
|
|
919
|
+
hold: HoldSchema.optional(),
|
|
920
|
+
/**
|
|
921
|
+
* Trim un-annotated dead air: compose scans the take for spans where nothing
|
|
922
|
+
* on screen moves and treats them as idle (capped like `waitFor*` idle is),
|
|
923
|
+
* so a slow XHR or a long `pause` stops inflating the scene. Opt-in — omit
|
|
924
|
+
* for the old behavior, where only annotated waits count as idle. Pass an
|
|
925
|
+
* object to tune the threshold; per-scene `autoIdle` overrides it.
|
|
926
|
+
*/
|
|
927
|
+
autoIdle: z.union([z.boolean(), AutoIdleSchema]).optional(),
|
|
928
|
+
/** Defaults for highlight/spotlight/callout overlays + click rings. Opt-in. */
|
|
929
|
+
attention: AttentionConfigSchema.optional(),
|
|
930
|
+
/** Produced-look frame: padding/background/radius/shadow/chrome. Opt-in. */
|
|
931
|
+
frame: FrameSchema.optional(),
|
|
932
|
+
/** Brand kit: logo watermark, accent, font. Opt-in. */
|
|
933
|
+
brand: BrandSchema.optional(),
|
|
934
|
+
/** Show a keystroke chip on every `press` (per-action `keystrokes` overrides). Opt-in. */
|
|
935
|
+
keystrokes: z.boolean().optional(),
|
|
936
|
+
/** Caption strip placement (bottom | top). Opt-in; per-scene `captions` overrides. */
|
|
937
|
+
captions: CaptionsConfigSchema.optional(),
|
|
938
|
+
/** Regions blurred at compose time throughout the demo. Opt-in. */
|
|
939
|
+
redact: z.array(RedactSchema).optional(),
|
|
940
|
+
/** Selectors hidden (visibility:hidden) at record time throughout the demo. Opt-in. */
|
|
941
|
+
hide: z.array(z.string()).optional(),
|
|
464
942
|
scenes: z.array(SceneSchema).min(1),
|
|
465
943
|
});
|
|
466
944
|
export type Storyboard = z.infer<typeof StoryboardSchema>;
|
|
@@ -517,6 +995,63 @@ export const CursorSampleSchema = z.object({
|
|
|
517
995
|
});
|
|
518
996
|
export type CursorSample = z.infer<typeof CursorSampleSchema>;
|
|
519
997
|
|
|
998
|
+
/**
|
|
999
|
+
* What one storyboard action actually did during the take: wall-clock window,
|
|
1000
|
+
* outcome, retries, and anything worth a second look (failed requests seen in
|
|
1001
|
+
* its window, an optional skip). The per-action twin of the compose report —
|
|
1002
|
+
* an agent reads this to see which action ate the time or was skipped without
|
|
1003
|
+
* regex-mining the log. Default [] keeps older timelines valid.
|
|
1004
|
+
*/
|
|
1005
|
+
export const TimelineActionSchema = z.object({
|
|
1006
|
+
/** 0-based index in the scene's actions[]. */
|
|
1007
|
+
index: z.number(),
|
|
1008
|
+
op: z.string(),
|
|
1009
|
+
/** Resolved target (named→selector, frame-prefixed) / goto URL, when any. */
|
|
1010
|
+
target: z.string().optional(),
|
|
1011
|
+
startMs: z.number(),
|
|
1012
|
+
endMs: z.number(),
|
|
1013
|
+
ok: z.boolean(),
|
|
1014
|
+
/** An `optional` action whose target never appeared / that failed. */
|
|
1015
|
+
skipped: z.boolean().optional(),
|
|
1016
|
+
/** Extra attempts consumed before success (from `retry`). */
|
|
1017
|
+
retries: z.number().optional(),
|
|
1018
|
+
warnings: z.array(z.string()).optional(),
|
|
1019
|
+
});
|
|
1020
|
+
export type TimelineAction = z.infer<typeof TimelineActionSchema>;
|
|
1021
|
+
|
|
1022
|
+
/** A measured attention beat (highlight/spotlight/callout) — viewport CSS px. */
|
|
1023
|
+
export const AttentionEventSchema = z.object({
|
|
1024
|
+
tMs: z.number(),
|
|
1025
|
+
kind: z.enum(["highlight", "spotlight", "callout"]),
|
|
1026
|
+
x: z.number(),
|
|
1027
|
+
y: z.number(),
|
|
1028
|
+
w: z.number(),
|
|
1029
|
+
h: z.number(),
|
|
1030
|
+
holdMs: z.number(),
|
|
1031
|
+
text: z.string().optional(),
|
|
1032
|
+
placement: z.string().optional(),
|
|
1033
|
+
dimTo: z.number().optional(),
|
|
1034
|
+
padding: z.number().optional(),
|
|
1035
|
+
style: AttentionStyleSchema.optional(),
|
|
1036
|
+
});
|
|
1037
|
+
export type AttentionEvent = z.infer<typeof AttentionEventSchema>;
|
|
1038
|
+
|
|
1039
|
+
/** A key press to show as a chip (already prettified, e.g. "⌘ K", "Enter"). */
|
|
1040
|
+
export const KeyEventSchema = z.object({ tMs: z.number(), keys: z.string() });
|
|
1041
|
+
export type KeyEvent = z.infer<typeof KeyEventSchema>;
|
|
1042
|
+
|
|
1043
|
+
/** A region blurred at compose time over [startMs, endMs] — viewport CSS px. */
|
|
1044
|
+
export const RedactSpanSchema = z.object({
|
|
1045
|
+
startMs: z.number(),
|
|
1046
|
+
endMs: z.number(),
|
|
1047
|
+
x: z.number(),
|
|
1048
|
+
y: z.number(),
|
|
1049
|
+
w: z.number(),
|
|
1050
|
+
h: z.number(),
|
|
1051
|
+
blur: z.number(),
|
|
1052
|
+
});
|
|
1053
|
+
export type RedactSpan = z.infer<typeof RedactSpanSchema>;
|
|
1054
|
+
|
|
520
1055
|
export const TimelineSceneSchema = z.object({
|
|
521
1056
|
id: z.string(),
|
|
522
1057
|
startMs: z.number(),
|
|
@@ -525,6 +1060,21 @@ export const TimelineSceneSchema = z.object({
|
|
|
525
1060
|
focusEvents: z.array(FocusEventSchema).default([]),
|
|
526
1061
|
stillEvents: z.array(StillEventSchema).default([]),
|
|
527
1062
|
cursorSamples: z.array(CursorSampleSchema).default([]),
|
|
1063
|
+
actions: z.array(TimelineActionSchema).default([]),
|
|
1064
|
+
attentionEvents: z.array(AttentionEventSchema).default([]),
|
|
1065
|
+
keyEvents: z.array(KeyEventSchema).default([]),
|
|
1066
|
+
redactSpans: z.array(RedactSpanSchema).default([]),
|
|
1067
|
+
/** Anchored actions: when the beat happened in the raw take (scene-relative like tMs elsewhere). */
|
|
1068
|
+
anchorEvents: z.array(z.object({ name: z.string(), tMs: z.number(), action: z.number() })).default([]),
|
|
1069
|
+
/**
|
|
1070
|
+
* Resume (`record --from-scene`): scenes reused from an earlier take name the
|
|
1071
|
+
* raw file they live in (relative to the demo dir) and that take's lead-in;
|
|
1072
|
+
* absent = the current take (`recordings/raw.*` + the timeline's leadInMs).
|
|
1073
|
+
*/
|
|
1074
|
+
source: z.string().optional(),
|
|
1075
|
+
leadInMs: z.number().optional(),
|
|
1076
|
+
/** Identity of the scene's action-spec at record time — resume reuses a scene only when it matches. */
|
|
1077
|
+
hash: z.string().optional(),
|
|
528
1078
|
});
|
|
529
1079
|
export type TimelineScene = z.infer<typeof TimelineSceneSchema>;
|
|
530
1080
|
|
|
@@ -539,6 +1089,86 @@ export const TimelineSchema = z.object({
|
|
|
539
1089
|
});
|
|
540
1090
|
export type Timeline = z.infer<typeof TimelineSchema>;
|
|
541
1091
|
|
|
1092
|
+
|
|
1093
|
+
// ---------------------------------------------------------------------------
|
|
1094
|
+
// Compose report — written to output/report.json by every compose. The
|
|
1095
|
+
// structured twin of the compose log: per-scene retime facts (how much was
|
|
1096
|
+
// stretched, held, trimmed) plus machine-readable warnings, so an agent can
|
|
1097
|
+
// judge a take without regex-mining logTail (issue #40's 28-scene log
|
|
1098
|
+
// overflowed the 40-line tail; the freeze-hold dominance was invisible).
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
|
|
1101
|
+
export const ComposeWarningSchema = z.object({
|
|
1102
|
+
/** Stable code: scene-freeze | overrun-trim | blank-tail | stale-captions |
|
|
1103
|
+
* focus-dropped | cursor-missing | scene-no-narration */
|
|
1104
|
+
code: z.string(),
|
|
1105
|
+
scene: z.string().optional(),
|
|
1106
|
+
message: z.string(),
|
|
1107
|
+
});
|
|
1108
|
+
export type ComposeWarning = z.infer<typeof ComposeWarningSchema>;
|
|
1109
|
+
|
|
1110
|
+
export const ComposeSceneReportSchema = z.object({
|
|
1111
|
+
id: z.string(),
|
|
1112
|
+
/** Recorded (kept, non-idle) ms feeding this scene. */
|
|
1113
|
+
srcMs: z.number(),
|
|
1114
|
+
/** Narration + gap ms the scene had to fill. */
|
|
1115
|
+
targetMs: z.number(),
|
|
1116
|
+
/** Time-stretch factor applied (0.5 … 1.6). */
|
|
1117
|
+
factor: z.number(),
|
|
1118
|
+
/** Ms of hold (frozen / drifting last frame) appended after the stretch. */
|
|
1119
|
+
holdMs: z.number(),
|
|
1120
|
+
/** holdMs / targetMs — the share of the scene that is a held frame. */
|
|
1121
|
+
holdPct: z.number(),
|
|
1122
|
+
/** Ms of recorded action cut from the tail because it overran the narration. */
|
|
1123
|
+
tailTrimMs: z.number(),
|
|
1124
|
+
/** Ms of solid-color tail dropped before holding (white pre-paint). */
|
|
1125
|
+
blankTrimMs: z.number(),
|
|
1126
|
+
/** Ms of motionless footage `autoIdle` marked trimmable in this scene (0 when off). */
|
|
1127
|
+
autoIdleMs: z.number().default(0),
|
|
1128
|
+
/** Kept spans the scene was cut from (idle spans trimmed between them). */
|
|
1129
|
+
spans: z.number(),
|
|
1130
|
+
/** Focus points (zoom) this scene contributed. */
|
|
1131
|
+
focusEvents: z.number(),
|
|
1132
|
+
/** Narration anchors: where the beat was asked to land vs where it did (content ms, scene-relative). */
|
|
1133
|
+
anchors: z
|
|
1134
|
+
.array(
|
|
1135
|
+
z.object({
|
|
1136
|
+
name: z.string(),
|
|
1137
|
+
targetMs: z.number(),
|
|
1138
|
+
landedMs: z.number(),
|
|
1139
|
+
/** landedMs − targetMs; |offMs| > 150 raises `anchor-unreachable`. */
|
|
1140
|
+
offMs: z.number(),
|
|
1141
|
+
})
|
|
1142
|
+
)
|
|
1143
|
+
.optional(),
|
|
1144
|
+
});
|
|
1145
|
+
export type ComposeSceneReport = z.infer<typeof ComposeSceneReportSchema>;
|
|
1146
|
+
|
|
1147
|
+
export const ComposeReportSchema = z.object({
|
|
1148
|
+
output: z.string(),
|
|
1149
|
+
/** Final video length, ms. */
|
|
1150
|
+
durationMs: z.number(),
|
|
1151
|
+
scenes: z.array(ComposeSceneReportSchema),
|
|
1152
|
+
zoom: z.object({ focusTotal: z.number(), focusDropped: z.number() }),
|
|
1153
|
+
cursorPoints: z.number(),
|
|
1154
|
+
captions: z.object({ cues: z.number(), passes: z.number() }),
|
|
1155
|
+
/** Attention overlays drawn (highlight/spotlight/callout), key chips, click rings, redact spans. */
|
|
1156
|
+
attention: z
|
|
1157
|
+
.object({
|
|
1158
|
+
events: z.number(),
|
|
1159
|
+
keys: z.number(),
|
|
1160
|
+
clicks: z.number(),
|
|
1161
|
+
redactSpans: z.number(),
|
|
1162
|
+
captionsFlipped: z.number(),
|
|
1163
|
+
})
|
|
1164
|
+
.optional(),
|
|
1165
|
+
hold: z.string(),
|
|
1166
|
+
/** output/poster.png when `output.poster` is set. */
|
|
1167
|
+
poster: z.string().optional(),
|
|
1168
|
+
warnings: z.array(ComposeWarningSchema),
|
|
1169
|
+
});
|
|
1170
|
+
export type ComposeReport = z.infer<typeof ComposeReportSchema>;
|
|
1171
|
+
|
|
542
1172
|
// ---------------------------------------------------------------------------
|
|
543
1173
|
// Voice manifest — emitted by the voice step. compose uses per-scene durations
|
|
544
1174
|
// as the target length for each scene's video segment.
|
|
@@ -600,9 +1230,10 @@ export const CaptionsManifestSchema = z.object({
|
|
|
600
1230
|
maxCueMs: z.number(),
|
|
601
1231
|
}),
|
|
602
1232
|
/**
|
|
603
|
-
* Per-scene inputs + reusable scene-relative word timings
|
|
604
|
-
*
|
|
605
|
-
*
|
|
1233
|
+
* Per-scene inputs + reusable scene-relative word timings. Offline path:
|
|
1234
|
+
* enables per-scene reuse (only a changed scene is re-derived). STT path:
|
|
1235
|
+
* the transcript split per scene, so compose can place narration anchors
|
|
1236
|
+
* on the word that was actually spoken.
|
|
606
1237
|
*/
|
|
607
1238
|
scenes: z
|
|
608
1239
|
.array(
|
|
@@ -642,6 +1273,13 @@ export const ProbeActionOutcomeSchema = z.object({
|
|
|
642
1273
|
ms: z.number().optional(),
|
|
643
1274
|
dy: z.number().optional(),
|
|
644
1275
|
label: z.string().optional(),
|
|
1276
|
+
/**
|
|
1277
|
+
* The action is marked `optional` in the storyboard. Optional actions always
|
|
1278
|
+
* record ok:true and no `found`: their real outcome is environment-dependent
|
|
1279
|
+
* by design (that's what optional is for), so it's normalized out of the
|
|
1280
|
+
* golden projection to keep it deterministic across runs.
|
|
1281
|
+
*/
|
|
1282
|
+
optional: z.boolean().optional(),
|
|
645
1283
|
});
|
|
646
1284
|
export type ProbeActionOutcome = z.infer<typeof ProbeActionOutcomeSchema>;
|
|
647
1285
|
|