demowright 2.2.0 → 2.4.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";
@@ -34,6 +34,23 @@ declare function clickEl(page: Page, selector: string): Promise<void>;
34
34
  * If omitted, uses `document.activeElement`.
35
35
  */
36
36
  declare function typeKeys(page: Page, text: string, delay?: number, inputSelector?: string): Promise<void>;
37
+ /**
38
+ * Pre-fetch TTS audio for all narration texts in parallel.
39
+ * Call this at the start of a test to eliminate per-annotate wait times.
40
+ *
41
+ * ```ts
42
+ * const narrations = [
43
+ * "Welcome to the demo",
44
+ * "Now clicking the button",
45
+ * "That wraps up the tour",
46
+ * ];
47
+ * await prefetchTts(page, narrations);
48
+ *
49
+ * // All subsequent annotate/narrate calls hit the cache instantly
50
+ * await annotate(page, narrations[0], async () => { ... });
51
+ * ```
52
+ */
53
+ declare function prefetchTts(page: Page, texts: string[]): Promise<void>;
37
54
  /**
38
55
  * Speak `text` via the configured TTS provider, or fall back to
39
56
  * the browser's speechSynthesis API.
@@ -92,4 +109,4 @@ declare function annotate(page: Page, text: string, callbackOrOptions?: (() => P
92
109
  voice?: string;
93
110
  }, callback?: () => Promise<void>): Promise<void>;
94
111
  //#endregion
95
- export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys };
112
+ export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys };
package/dist/helpers.mjs CHANGED
@@ -129,16 +129,48 @@ async function typeKeys(page, text, delay = 65, inputSelector) {
129
129
  await page.waitForTimeout(delay);
130
130
  }
131
131
  }
132
- /** Fetch audio from a TTS provider. Returns a WAV Buffer. */
132
+ const ttsCache = /* @__PURE__ */ new Map();
133
+ /** Fetch audio from a TTS provider. Returns a WAV Buffer (cached). */
133
134
  async function fetchTtsAudio(text, provider) {
135
+ const cached = ttsCache.get(text);
136
+ if (cached) return cached;
137
+ let buf;
134
138
  if (typeof provider === "string") {
135
139
  const url = provider.replace(/%s/g, encodeURIComponent(text));
136
140
  const res = await fetch(url);
137
141
  if (!res.ok) throw new Error(`TTS fetch ${res.status}`);
138
- return Buffer.from(await res.arrayBuffer());
142
+ buf = Buffer.from(await res.arrayBuffer());
143
+ } else {
144
+ const result = await provider(text);
145
+ buf = Buffer.isBuffer(result) ? result : Buffer.from(result);
139
146
  }
140
- const result = await provider(text);
141
- return Buffer.isBuffer(result) ? result : Buffer.from(result);
147
+ ttsCache.set(text, buf);
148
+ return buf;
149
+ }
150
+ /**
151
+ * Pre-fetch TTS audio for all narration texts in parallel.
152
+ * Call this at the start of a test to eliminate per-annotate wait times.
153
+ *
154
+ * ```ts
155
+ * const narrations = [
156
+ * "Welcome to the demo",
157
+ * "Now clicking the button",
158
+ * "That wraps up the tour",
159
+ * ];
160
+ * await prefetchTts(page, narrations);
161
+ *
162
+ * // All subsequent annotate/narrate calls hit the cache instantly
163
+ * await annotate(page, narrations[0], async () => { ... });
164
+ * ```
165
+ */
166
+ async function prefetchTts(page, texts) {
167
+ const provider = getTtsProvider(page);
168
+ if (!provider) return;
169
+ const uncached = texts.filter((t) => !ttsCache.has(t));
170
+ if (uncached.length === 0) return;
171
+ console.log(`[demowright] Prefetching ${uncached.length} TTS segments...`);
172
+ const failed = (await Promise.allSettled(uncached.map((text) => fetchTtsAudio(text, provider)))).filter((r) => r.status === "rejected").length;
173
+ if (failed > 0) console.warn(`[demowright] ${failed}/${uncached.length} TTS prefetch failed`);
142
174
  }
143
175
  /** Parse WAV and return { float32, sampleRate, channels, durationMs }. */
144
176
  function parseWav(wavBuf) {
@@ -318,4 +350,4 @@ async function annotate(page, text, callbackOrOptions, callback) {
318
350
  }, cb)]);
319
351
  }
320
352
  //#endregion
321
- export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys };
353
+ export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
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";
3
- import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
2
+ import { a as AudioWriter, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-8X-3F3M7.mjs";
3
+ import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, 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";
6
6
  import * as _$electron from "electron";
@@ -54041,4 +54041,4 @@ declare const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions & {
54041
54041
  */
54042
54042
  declare function getGlobalTtsProvider(): TtsProvider;
54043
54043
  //#endregion
54044
- export { AudioWriter, type OutroOptions, type PaceFn, type QaHudOptions, type RenderOptions, type TimelineEntry, type TitleOptions, type TtsProvider, type VideoScriptResult, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, subtitle, test, typeKeys };
54044
+ export { AudioWriter, type OutroOptions, type PaceFn, type QaHudOptions, type RenderOptions, type TimelineEntry, type TitleOptions, type TtsProvider, type VideoScriptResult, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, prefetchTts, subtitle, test, typeKeys };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
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
- import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys } from "./helpers.mjs";
4
+ import { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, prefetchTts, subtitle, typeKeys } from "./helpers.mjs";
5
5
  import { installAutoAnnotate } from "./auto-annotate.mjs";
6
6
  import { expect, test as test$1 } from "@playwright/test";
7
7
  //#region src/fixture.ts
@@ -26,4 +26,4 @@ const test = test$1.extend({
26
26
  init_video_script();
27
27
  init_hud_registry();
28
28
  //#endregion
29
- export { AudioWriter, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, subtitle, test, typeKeys };
29
+ export { AudioWriter, annotate, applyHud, buildFfmpegCommand, caption, clickEl, createVideoScript, expect, getGlobalTtsProvider, hudWait, installAutoAnnotate, moveTo, moveToEl, narrate, prefetchTts, subtitle, test, typeKeys };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demowright",
3
- "version": "2.2.0",
3
+ "version": "2.4.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": {