demowright 2.1.0 → 2.3.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/config.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as QaHudOptions } from "./setup-Dwx0Dt54.mjs";
1
+ import { t as QaHudOptions } from "./setup-8X-3F3M7.mjs";
2
2
  import { PlaywrightTestConfig } from "@playwright/test";
3
3
 
4
4
  //#region src/config.d.ts
package/dist/config.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as defaultOptions } from "./setup-Ce4X1N16.mjs";
1
+ import { n as defaultOptions } from "./setup-c_UpOQ7a.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { execSync } from "node:child_process";
4
4
  import { mkdirSync, readdirSync } from "node:fs";
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { installAutoAnnotate } from "./auto-annotate.mjs";
2
- import { a as AudioWriter, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-Dwx0Dt54.mjs";
2
+ import { a as AudioWriter, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-8X-3F3M7.mjs";
3
3
  import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
4
4
  import { OutroOptions, PaceFn, RenderOptions, TimelineEntry, TitleOptions, VideoScriptResult, buildFfmpegCommand, createVideoScript } from "./video-script.mjs";
5
5
  import { Page as Page$1, expect } from "@playwright/test";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as AudioWriter, t as applyHud } from "./setup-Ce4X1N16.mjs";
1
+ import { r as AudioWriter, t as applyHud } from "./setup-c_UpOQ7a.mjs";
2
2
  import { i as getGlobalTtsProvider, s as init_hud_registry } from "./hud-registry-Wfd4b4Nu.mjs";
3
3
  import { buildFfmpegCommand, createVideoScript, t as init_video_script } from "./video-script.mjs";
4
4
  import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
@@ -24,6 +24,8 @@ declare class AudioWriter {
24
24
  /**
25
25
  * Called from the browser via page.exposeFunction.
26
26
  * Receives interleaved stereo float32 samples.
27
+ * Each chunk is timestamped with wall-clock time so silence gaps
28
+ * (e.g. during video pause) are preserved in the output.
27
29
  */
28
30
  addChunk(samples: number[], sampleRate: number): void;
29
31
  /** Wall-clock time when first chunk arrived */
@@ -32,13 +34,15 @@ declare class AudioWriter {
32
34
  get rate(): number;
33
35
  /** Total samples collected (interleaved, so / channels for per-channel) */
34
36
  get totalSamples(): number;
37
+ /** Total duration including silence gaps (wall-clock based) */
35
38
  get duration(): number;
36
39
  /**
37
40
  * Write collected audio to a WAV file.
38
41
  */
39
42
  save(filePath: string): void;
40
43
  /**
41
- * Return all chunks concatenated as interleaved stereo float32.
44
+ * Return all audio as interleaved stereo float32, preserving silence gaps
45
+ * between chunks based on their wall-clock timestamps.
42
46
  */
43
47
  toFloat32(): Float32Array;
44
48
  /** Reset for reuse */
@@ -300,11 +300,17 @@ var AudioWriter = class {
300
300
  /**
301
301
  * Called from the browser via page.exposeFunction.
302
302
  * Receives interleaved stereo float32 samples.
303
+ * Each chunk is timestamped with wall-clock time so silence gaps
304
+ * (e.g. during video pause) are preserved in the output.
303
305
  */
304
306
  addChunk(samples, sampleRate) {
305
- if (this.chunks.length === 0) this.startMs = Date.now();
307
+ const now = Date.now();
308
+ if (this.chunks.length === 0) this.startMs = now;
306
309
  this.sampleRate = sampleRate;
307
- this.chunks.push(new Float32Array(samples));
310
+ this.chunks.push({
311
+ samples: new Float32Array(samples),
312
+ timestampMs: now
313
+ });
308
314
  }
309
315
  /** Wall-clock time when first chunk arrived */
310
316
  get captureStartMs() {
@@ -316,22 +322,25 @@ var AudioWriter = class {
316
322
  }
317
323
  /** Total samples collected (interleaved, so / channels for per-channel) */
318
324
  get totalSamples() {
319
- return this.chunks.reduce((sum, c) => sum + c.length, 0);
325
+ return this.chunks.reduce((sum, c) => sum + c.samples.length, 0);
320
326
  }
327
+ /** Total duration including silence gaps (wall-clock based) */
321
328
  get duration() {
322
- return this.totalSamples / this.channels / this.sampleRate;
329
+ if (this.chunks.length === 0) return 0;
330
+ const last = this.chunks[this.chunks.length - 1];
331
+ const lastDurMs = last.samples.length / this.channels / this.sampleRate * 1e3;
332
+ return (last.timestampMs + lastDurMs - this.startMs) / 1e3;
323
333
  }
324
334
  /**
325
335
  * Write collected audio to a WAV file.
326
336
  */
327
337
  save(filePath) {
328
- const totalSamples = this.totalSamples;
329
- if (totalSamples === 0) return;
330
- const int16 = new Int16Array(totalSamples);
331
- let offset = 0;
332
- for (const chunk of this.chunks) for (let i = 0; i < chunk.length; i++) {
333
- const s = Math.max(-1, Math.min(1, chunk[i]));
334
- int16[offset++] = s < 0 ? s * 32768 : s * 32767;
338
+ const float32 = this.toFloat32();
339
+ if (float32.length === 0) return;
340
+ const int16 = new Int16Array(float32.length);
341
+ for (let i = 0; i < float32.length; i++) {
342
+ const s = Math.max(-1, Math.min(1, float32[i]));
343
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
335
344
  }
336
345
  const dataBytes = int16.length * 2;
337
346
  const buffer = Buffer.alloc(44 + dataBytes);
@@ -352,16 +361,20 @@ var AudioWriter = class {
352
361
  writeFileSync(filePath, buffer);
353
362
  }
354
363
  /**
355
- * Return all chunks concatenated as interleaved stereo float32.
364
+ * Return all audio as interleaved stereo float32, preserving silence gaps
365
+ * between chunks based on their wall-clock timestamps.
356
366
  */
357
367
  toFloat32() {
358
- const total = this.totalSamples;
359
- if (total === 0) return new Float32Array(0);
360
- const out = new Float32Array(total);
361
- let off = 0;
368
+ if (this.chunks.length === 0) return new Float32Array(0);
369
+ const last = this.chunks[this.chunks.length - 1];
370
+ const lastDurMs = last.samples.length / this.channels / this.sampleRate * 1e3;
371
+ const totalMs = last.timestampMs + lastDurMs - this.startMs;
372
+ const totalSamples = Math.ceil(totalMs / 1e3 * this.sampleRate) * this.channels;
373
+ const out = new Float32Array(totalSamples);
362
374
  for (const chunk of this.chunks) {
363
- out.set(chunk, off);
364
- off += chunk.length;
375
+ const offsetMs = chunk.timestampMs - this.startMs;
376
+ const offsetSamples = Math.floor(offsetMs / 1e3 * this.sampleRate) * this.channels;
377
+ for (let i = 0; i < chunk.samples.length && offsetSamples + i < out.length; i++) out[offsetSamples + i] += chunk.samples[i];
365
378
  }
366
379
  return out;
367
380
  }
package/dist/setup.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as defaultOptions, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-Dwx0Dt54.mjs";
1
+ import { i as defaultOptions, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-8X-3F3M7.mjs";
2
2
  export { QaHudOptions, TtsProvider, applyHud, defaultOptions };
package/dist/setup.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as defaultOptions, t as applyHud } from "./setup-Ce4X1N16.mjs";
1
+ import { n as defaultOptions, t as applyHud } from "./setup-c_UpOQ7a.mjs";
2
2
  export { applyHud, defaultOptions };
@@ -10,6 +10,9 @@ interface TitleStep {
10
10
  kind: "title";
11
11
  text: string;
12
12
  subtitle?: string;
13
+ /** Optional TTS narration read aloud over the card. When set, card
14
+ * duration becomes max(durationMs, narration audio length). */
15
+ narration?: string;
13
16
  durationMs: number;
14
17
  background?: string;
15
18
  }
@@ -27,6 +30,9 @@ interface OutroStep {
27
30
  kind: "outro";
28
31
  text: string;
29
32
  subtitle?: string;
33
+ /** Optional TTS narration read aloud over the card. When set, card
34
+ * duration becomes max(durationMs, narration audio length). */
35
+ narration?: string;
30
36
  durationMs: number;
31
37
  background?: string;
32
38
  }
@@ -53,6 +59,8 @@ interface VideoScriptResult {
53
59
  }
54
60
  interface TitleOptions {
55
61
  subtitle?: string;
62
+ /** TTS narration read aloud over the card overlay. Duration auto-extends to fit. */
63
+ narration?: string;
56
64
  durationMs?: number;
57
65
  /** CSS background — default: radial gradient */
58
66
  background?: string;
@@ -60,6 +68,8 @@ interface TitleOptions {
60
68
  interface OutroOptions {
61
69
  text?: string;
62
70
  subtitle?: string;
71
+ /** TTS narration read aloud over the card overlay. Duration auto-extends to fit. */
72
+ narration?: string;
63
73
  durationMs?: number;
64
74
  background?: string;
65
75
  }
@@ -89,7 +99,7 @@ declare class VideoScriptImpl {
89
99
  private static ttsCache;
90
100
  /**
91
101
  * Add a title card — full-screen overlay with text + optional subtitle.
92
- * No TTS narration by default (silent title card).
102
+ * Pass `narration` in opts to have TTS voiceover during the card.
93
103
  */
94
104
  title(text: string, opts?: TitleOptions): this;
95
105
  /**
@@ -104,6 +114,7 @@ declare class VideoScriptImpl {
104
114
  transition(type?: TransitionType, durationMs?: number): this;
105
115
  /**
106
116
  * Add an outro card — full-screen overlay, similar to title.
117
+ * Pass `narration` in opts to have TTS voiceover during the card.
107
118
  */
108
119
  outro(opts?: OutroOptions): this;
109
120
  /**
@@ -267,13 +267,14 @@ var init_video_script = __esmMin((() => {
267
267
  static ttsCache = /* @__PURE__ */ new Map();
268
268
  /**
269
269
  * Add a title card — full-screen overlay with text + optional subtitle.
270
- * No TTS narration by default (silent title card).
270
+ * Pass `narration` in opts to have TTS voiceover during the card.
271
271
  */
272
272
  title(text, opts) {
273
273
  this.steps.push({
274
274
  kind: "title",
275
275
  text,
276
276
  subtitle: opts?.subtitle,
277
+ narration: opts?.narration,
277
278
  durationMs: opts?.durationMs ?? 4e3,
278
279
  background: opts?.background
279
280
  });
@@ -305,12 +306,14 @@ var init_video_script = __esmMin((() => {
305
306
  }
306
307
  /**
307
308
  * Add an outro card — full-screen overlay, similar to title.
309
+ * Pass `narration` in opts to have TTS voiceover during the card.
308
310
  */
309
311
  outro(opts) {
310
312
  this.steps.push({
311
313
  kind: "outro",
312
314
  text: opts?.text ?? "Thanks for watching!",
313
315
  subtitle: opts?.subtitle,
316
+ narration: opts?.narration,
314
317
  durationMs: opts?.durationMs ?? 4e3,
315
318
  background: opts?.background
316
319
  });
@@ -326,11 +329,20 @@ var init_video_script = __esmMin((() => {
326
329
  else if (typeof pageOrProvider === "function" || typeof pageOrProvider === "string") provider = pageOrProvider;
327
330
  else provider = getTtsProvider(pageOrProvider);
328
331
  if (!provider) return;
329
- const segmentSteps = this.steps.filter((s) => s.kind === "segment").map((s, i) => ({
330
- ...s,
331
- id: `step-${i}`
332
- }));
333
- const results = await Promise.allSettled(segmentSteps.map(async (step) => {
332
+ const ttsSteps = [];
333
+ let segIdx2 = 0;
334
+ for (let i = 0; i < this.steps.length; i++) {
335
+ const s = this.steps[i];
336
+ if (s.kind === "segment") ttsSteps.push({
337
+ id: `step-${segIdx2++}`,
338
+ text: s.text
339
+ });
340
+ else if ((s.kind === "title" || s.kind === "outro") && s.narration) ttsSteps.push({
341
+ id: `card-${i}`,
342
+ text: s.narration
343
+ });
344
+ }
345
+ const results = await Promise.allSettled(ttsSteps.map(async (step) => {
334
346
  const cached = VideoScriptImpl.ttsCache.get(step.text);
335
347
  if (cached) return {
336
348
  id: step.id,
@@ -352,7 +364,7 @@ var init_video_script = __esmMin((() => {
352
364
  durationMs: r.value.durationMs
353
365
  };
354
366
  this.prepared.set(r.value.id, seg);
355
- const step = segmentSteps[idx];
367
+ const step = ttsSteps[idx];
356
368
  if (step) VideoScriptImpl.ttsCache.set(step.text, seg);
357
369
  }
358
370
  idx++;
@@ -450,14 +462,28 @@ var init_video_script = __esmMin((() => {
450
462
  const stepStartMs = Date.now();
451
463
  if (step.kind === "title" || step.kind === "outro") {
452
464
  const bg = step.background ?? DEFAULT_BG;
453
- if (active) showCard(page, step.text, step.subtitle, step.durationMs, bg).catch(() => {});
454
- await page.waitForTimeout(step.durationMs);
465
+ const cardTts = step.narration ? this.prepared.get(`card-${i}`) : void 0;
466
+ const effectiveDuration = cardTts ? Math.max(step.durationMs, cardTts.durationMs + 500) : step.durationMs;
467
+ if (active) showCard(page, step.text, step.subtitle, effectiveDuration, bg).catch(() => {});
468
+ if (active && cardTts) {
469
+ const offsetMs = Date.now() - planStartMs;
470
+ audioSegments.push({
471
+ offsetMs,
472
+ wavBuf: cardTts.wavBuf
473
+ });
474
+ storeAudioSegment(page, {
475
+ timestampMs: Date.now(),
476
+ wavBuf: cardTts.wavBuf
477
+ });
478
+ showCaption(page, step.narration, cardTts.durationMs).catch(() => {});
479
+ }
480
+ await page.waitForTimeout(effectiveDuration);
455
481
  timeline.push({
456
482
  id: stepIds[i],
457
483
  kind: step.kind,
458
- text: step.text,
484
+ text: step.narration ?? step.text,
459
485
  startMs: stepStartMs - planStartMs,
460
- durationMs: step.durationMs,
486
+ durationMs: effectiveDuration,
461
487
  actionMs: 0,
462
488
  overrunMs: 0
463
489
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demowright",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Playwright video production plugin — cursor overlay, keystroke badges, TTS narration, and narration-driven video scripts for test recordings",
5
5
  "license": "MIT",
6
6
  "repository": {