demowright 0.1.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.
@@ -0,0 +1,299 @@
1
+ import { a as getTtsProvider, o as init_hud_registry, s as isHudActive, u as storeAudioSegment } from "./hud-registry-CptkyV32.mjs";
2
+ //#region src/helpers.ts
3
+ init_hud_registry();
4
+ /**
5
+ * Wait for `ms` milliseconds, but only when demowright is active.
6
+ * Use this instead of `page.waitForTimeout()` for recording-only pauses.
7
+ */
8
+ async function hudWait(page, ms) {
9
+ if (!await isHudActive(page)) return;
10
+ await page.waitForTimeout(ms);
11
+ }
12
+ /**
13
+ * Smoothly move the HUD cursor to (x, y) over `steps` frames.
14
+ * No-op when HUD is inactive.
15
+ */
16
+ async function moveTo(page, x, y, steps = 10) {
17
+ if (!await isHudActive(page)) return;
18
+ const start = await page.evaluate(() => ({
19
+ x: window.__qaHud?.cx ?? 0,
20
+ y: window.__qaHud?.cy ?? 0
21
+ }));
22
+ for (let i = 1; i <= steps; i++) {
23
+ const t = i / steps;
24
+ await page.evaluate(([mx, my]) => document.dispatchEvent(new MouseEvent("mousemove", {
25
+ clientX: mx,
26
+ clientY: my,
27
+ bubbles: true
28
+ })), [start.x + (x - start.x) * t, start.y + (y - start.y) * t]);
29
+ await page.waitForTimeout(20);
30
+ }
31
+ }
32
+ /**
33
+ * Smoothly move the HUD cursor to the center of `selector`.
34
+ * Returns the element center coordinates.
35
+ * When HUD is inactive, resolves coordinates but skips the animation.
36
+ */
37
+ async function moveToEl(page, selector) {
38
+ const center = await page.evaluate((s) => {
39
+ const r = document.querySelector(s).getBoundingClientRect();
40
+ return {
41
+ x: r.x + r.width / 2,
42
+ y: r.y + r.height / 2
43
+ };
44
+ }, selector);
45
+ if (await isHudActive(page)) await moveTo(page, center.x, center.y);
46
+ return center;
47
+ }
48
+ /**
49
+ * Animated click on `selector` — moves cursor, fires mousedown/mouseup
50
+ * ripple, then performs the actual DOM click.
51
+ * When HUD is inactive, performs only the DOM click (no animation/delays).
52
+ */
53
+ async function clickEl(page, selector) {
54
+ const active = await isHudActive(page);
55
+ if (active) {
56
+ const c = await moveToEl(page, selector);
57
+ await page.waitForTimeout(150);
58
+ await page.evaluate(([x, y]) => {
59
+ document.dispatchEvent(new MouseEvent("mousedown", {
60
+ clientX: x,
61
+ clientY: y,
62
+ bubbles: true
63
+ }));
64
+ setTimeout(() => document.dispatchEvent(new MouseEvent("mouseup", {
65
+ clientX: x,
66
+ clientY: y,
67
+ bubbles: true
68
+ })), 60);
69
+ }, [c.x, c.y]);
70
+ }
71
+ await page.evaluate((s) => document.querySelector(s)?.click(), selector);
72
+ if (active) await page.waitForTimeout(100);
73
+ }
74
+ /**
75
+ * Type `text` character-by-character with visible key badges.
76
+ * When HUD is inactive, sets the input value directly.
77
+ *
78
+ * @param inputSelector — optional selector for the input element to update.
79
+ * If omitted, uses `document.activeElement`.
80
+ */
81
+ async function typeKeys(page, text, delay = 65, inputSelector) {
82
+ if (!await isHudActive(page)) {
83
+ await page.evaluate(([t, sel]) => {
84
+ const el = sel ? document.querySelector(sel) : document.activeElement;
85
+ if (el && "value" in el) {
86
+ el.value = t;
87
+ el.dispatchEvent(new Event("input", { bubbles: true }));
88
+ el.dispatchEvent(new Event("change", { bubbles: true }));
89
+ }
90
+ }, [text, inputSelector]);
91
+ return;
92
+ }
93
+ for (let i = 0; i < text.length; i++) {
94
+ await page.evaluate(([k, sel, partial]) => {
95
+ document.dispatchEvent(new KeyboardEvent("keydown", {
96
+ key: k,
97
+ bubbles: true
98
+ }));
99
+ const el = sel ? document.querySelector(sel) : document.activeElement;
100
+ if (el && "value" in el) el.value = partial;
101
+ }, [
102
+ text[i],
103
+ inputSelector,
104
+ text.slice(0, i + 1)
105
+ ]);
106
+ await page.waitForTimeout(delay);
107
+ }
108
+ }
109
+ /** Fetch audio from a TTS provider. Returns a WAV Buffer. */
110
+ async function fetchTtsAudio(text, provider) {
111
+ if (typeof provider === "string") {
112
+ const url = provider.replace(/%s/g, encodeURIComponent(text));
113
+ const res = await fetch(url);
114
+ if (!res.ok) throw new Error(`TTS fetch ${res.status}`);
115
+ return Buffer.from(await res.arrayBuffer());
116
+ }
117
+ const result = await provider(text);
118
+ return Buffer.isBuffer(result) ? result : Buffer.from(result);
119
+ }
120
+ /** Parse WAV and return { float32, sampleRate, channels, durationMs }. */
121
+ function parseWav(wavBuf) {
122
+ const dataOffset = wavBuf.indexOf("data") + 8;
123
+ if (dataOffset < 8) throw new Error("Invalid WAV");
124
+ const sampleRate = wavBuf.readUInt32LE(24);
125
+ const channels = wavBuf.readUInt16LE(22);
126
+ const pcmData = wavBuf.subarray(dataOffset);
127
+ const sampleCount = pcmData.length / 2;
128
+ const float32 = new Float32Array(sampleCount);
129
+ for (let i = 0; i < sampleCount; i++) float32[i] = pcmData.readInt16LE(i * 2) / 32768;
130
+ return {
131
+ float32,
132
+ sampleRate,
133
+ channels,
134
+ sampleCount,
135
+ durationMs: sampleCount / channels / sampleRate * 1e3
136
+ };
137
+ }
138
+ /**
139
+ * Store TTS audio segment with its timestamp for deferred track building.
140
+ * The complete audio track is assembled at context close using actual
141
+ * wall-clock timestamps, eliminating drift from page.evaluate overhead.
142
+ */
143
+ async function playTtsAudio(page, wavBuf) {
144
+ const { durationMs } = parseWav(wavBuf);
145
+ storeAudioSegment(page, {
146
+ timestampMs: Date.now(),
147
+ wavBuf
148
+ });
149
+ await page.waitForTimeout(durationMs);
150
+ }
151
+ /**
152
+ * Speak `text` via the configured TTS provider, or fall back to
153
+ * the browser's speechSynthesis API.
154
+ *
155
+ * When called with a callback, pre-fetches the TTS audio, then runs
156
+ * the callback actions in parallel with audio playback — waiting for
157
+ * whichever takes longer. This keeps narration and actions in sync.
158
+ *
159
+ * ```ts
160
+ * // TTS only
161
+ * await narrate(page, "Processing complete");
162
+ *
163
+ * // TTS + actions timed together
164
+ * await narrate(page, "Now let's fill the form", async () => {
165
+ * await clickEl(page, "#name");
166
+ * await typeKeys(page, "Alice");
167
+ * });
168
+ * ```
169
+ *
170
+ * When HUD is inactive, only the callback runs (instantly, no TTS).
171
+ */
172
+ async function narrate(page, text, callbackOrOptions, callback) {
173
+ const cb = typeof callbackOrOptions === "function" ? callbackOrOptions : callback;
174
+ const options = typeof callbackOrOptions === "object" ? callbackOrOptions : void 0;
175
+ if (!await isHudActive(page)) {
176
+ await cb?.();
177
+ return;
178
+ }
179
+ const provider = getTtsProvider(page);
180
+ if (provider) try {
181
+ const wavBuf = await fetchTtsAudio(text, provider);
182
+ if (cb) await Promise.all([playTtsAudio(page, wavBuf), cb()]);
183
+ else await playTtsAudio(page, wavBuf);
184
+ return;
185
+ } catch {}
186
+ const opts = {
187
+ rate: options?.rate ?? 1,
188
+ pitch: options?.pitch ?? 1,
189
+ volume: options?.volume ?? 1,
190
+ voice: options?.voice
191
+ };
192
+ const speechPromise = page.evaluate(([t, o]) => {
193
+ return new Promise((resolve) => {
194
+ const synth = window.speechSynthesis;
195
+ if (!synth) {
196
+ resolve();
197
+ return;
198
+ }
199
+ try {
200
+ const utter = new SpeechSynthesisUtterance(t);
201
+ utter.rate = o.rate;
202
+ utter.pitch = o.pitch;
203
+ utter.volume = o.volume;
204
+ if (o.voice) {
205
+ const match = synth.getVoices().find((v) => v.name === o.voice || v.lang === o.voice);
206
+ if (match) utter.voice = match;
207
+ }
208
+ utter.onend = () => resolve();
209
+ utter.onerror = () => resolve();
210
+ const timeout = Math.min(5e3, Math.max(1e3, t.length * 80));
211
+ setTimeout(resolve, timeout);
212
+ synth.speak(utter);
213
+ } catch {
214
+ resolve();
215
+ }
216
+ });
217
+ }, [text, opts]);
218
+ if (cb) await Promise.all([speechPromise, cb()]);
219
+ else await speechPromise;
220
+ }
221
+ /**
222
+ * Show a caption text overlay on the page for `durationMs` milliseconds.
223
+ * Useful as a visual annotation in recordings. No-op when HUD is inactive.
224
+ */
225
+ async function caption(page, text, durationMs = 3e3) {
226
+ if (!await isHudActive(page)) return;
227
+ await page.evaluate(([t, d]) => {
228
+ const el = document.createElement("div");
229
+ el.textContent = t;
230
+ el.style.cssText = [
231
+ "position:fixed",
232
+ "bottom:60px",
233
+ "left:50%",
234
+ "transform:translateX(-50%)",
235
+ "background:rgba(0,0,0,0.8)",
236
+ "color:#fff",
237
+ "font-family:system-ui,sans-serif",
238
+ "font-size:18px",
239
+ "padding:10px 24px",
240
+ "border-radius:8px",
241
+ "z-index:2147483647",
242
+ "pointer-events:none",
243
+ "white-space:nowrap",
244
+ "max-width:90vw",
245
+ "text-align:center",
246
+ `animation:qa-sub-fade ${d}ms ease-out forwards`
247
+ ].join(";");
248
+ if (!document.querySelector("#qa-sub-style")) {
249
+ const style = document.createElement("style");
250
+ style.id = "qa-sub-style";
251
+ style.textContent = `
252
+ @keyframes qa-sub-fade {
253
+ 0% { opacity: 0; transform: translateX(-50%) translateY(10px); }
254
+ 8% { opacity: 1; transform: translateX(-50%) translateY(0); }
255
+ 85% { opacity: 1; transform: translateX(-50%) translateY(0); }
256
+ 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
257
+ }
258
+ `;
259
+ document.head.appendChild(style);
260
+ }
261
+ document.body.appendChild(el);
262
+ setTimeout(() => el.remove(), d);
263
+ }, [text, durationMs]);
264
+ }
265
+ /** @deprecated Use `caption()` instead. */
266
+ const subtitle = caption;
267
+ /**
268
+ * Show a caption + speak it via TTS simultaneously.
269
+ * When called with a callback, runs the actions in parallel with
270
+ * the narration — waiting for whichever takes longer.
271
+ *
272
+ * ```ts
273
+ * // Caption + TTS
274
+ * await annotate(page, "Welcome to the tour");
275
+ *
276
+ * // Caption + TTS + actions
277
+ * await annotate(page, "Let's fill the form", async () => {
278
+ * await clickEl(page, "#name");
279
+ * await typeKeys(page, "Alice");
280
+ * });
281
+ * ```
282
+ *
283
+ * No-op when HUD is inactive (callback still runs).
284
+ */
285
+ async function annotate(page, text, callbackOrOptions, callback) {
286
+ const cb = typeof callbackOrOptions === "function" ? callbackOrOptions : callback;
287
+ const options = typeof callbackOrOptions === "object" ? callbackOrOptions : void 0;
288
+ if (!await isHudActive(page)) {
289
+ await cb?.();
290
+ return;
291
+ }
292
+ const durationMs = options?.durationMs ?? 4e3;
293
+ await Promise.all([caption(page, text, durationMs), narrate(page, text, {
294
+ rate: options?.rate,
295
+ voice: options?.voice
296
+ }, cb)]);
297
+ }
298
+ //#endregion
299
+ export { annotate, caption, clickEl, hudWait, moveTo, moveToEl, narrate, subtitle, typeKeys };
@@ -0,0 +1,90 @@
1
+ import { t as __esmMin } from "./chunk-C0p4GxOx.mjs";
2
+ //#region src/hud-registry.ts
3
+ function registerHudPage(page, config) {
4
+ hudPages.set(page, config ?? { tts: false });
5
+ if (config?.tts) g.__qaHudGlobal.tts = config.tts;
6
+ }
7
+ /**
8
+ * Check if demowright is active on this page.
9
+ * Checks the Node-side registry first (fast), falls back to querying
10
+ * the browser for window.__qaHud (covers the config/register approach).
11
+ */
12
+ async function isHudActive(page) {
13
+ if (hudPages.has(page)) return true;
14
+ try {
15
+ const active = await page.evaluate(() => !!window.__qaHud);
16
+ if (active) hudPages.set(page, { tts: g.__qaHudGlobal.tts || false });
17
+ return active;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Get TTS provider configured for this page (if any).
24
+ */
25
+ function getTtsProvider(page) {
26
+ return hudPages.get(page)?.tts || g.__qaHudGlobal.tts || false;
27
+ }
28
+ /**
29
+ * Get the global TTS provider (set by register.cjs or applyHud).
30
+ * Useful for pre-generating audio before a page/context exists.
31
+ */
32
+ function getGlobalTtsProvider() {
33
+ return g.__qaHudGlobal.tts || false;
34
+ }
35
+ function setGlobalOutputDir(dir) {
36
+ g.__qaHudGlobal.outputDir = dir;
37
+ }
38
+ function getGlobalOutputDir() {
39
+ return g.__qaHudGlobal.outputDir || ".demowright";
40
+ }
41
+ /**
42
+ * Store a TTS audio segment with its wall-clock timestamp.
43
+ * Used for deferred audio track building at context close.
44
+ */
45
+ function storeAudioSegment(page, segment) {
46
+ let segs = audioSegments.get(page);
47
+ if (!segs) {
48
+ segs = [];
49
+ audioSegments.set(page, segs);
50
+ }
51
+ segs.push(segment);
52
+ const pageId = page._guid || String(Date.now());
53
+ if (!g.__qaHudGlobal.audioSegments.has(pageId)) g.__qaHudGlobal.audioSegments.set(pageId, segs);
54
+ }
55
+ /**
56
+ * Get all stored audio segments for a page.
57
+ */
58
+ function getAudioSegments(page) {
59
+ const local = audioSegments.get(page);
60
+ if (local && local.length > 0) return local;
61
+ const pageId = page._guid;
62
+ if (pageId) return g.__qaHudGlobal.audioSegments.get(pageId) ?? [];
63
+ return [];
64
+ }
65
+ function storeRenderJob(page, job) {
66
+ renderJobs.set(page, job);
67
+ const pageId = page._guid || String(Date.now());
68
+ if (!g.__qaHudGlobal.renderJobs) g.__qaHudGlobal.renderJobs = /* @__PURE__ */ new Map();
69
+ g.__qaHudGlobal.renderJobs.set(pageId, job);
70
+ }
71
+ function getRenderJob(page) {
72
+ const local = renderJobs.get(page);
73
+ if (local) return local;
74
+ const pageId = page._guid;
75
+ if (pageId) return g.__qaHudGlobal.renderJobs?.get(pageId);
76
+ }
77
+ var hudPages, audioSegments, g, renderJobs;
78
+ var init_hud_registry = __esmMin((() => {
79
+ hudPages = /* @__PURE__ */ new WeakMap();
80
+ audioSegments = /* @__PURE__ */ new WeakMap();
81
+ g = globalThis;
82
+ if (!g.__qaHudGlobal) g.__qaHudGlobal = {
83
+ tts: false,
84
+ audioSegments: /* @__PURE__ */ new Map(),
85
+ outputDir: ".demowright"
86
+ };
87
+ renderJobs = /* @__PURE__ */ new WeakMap();
88
+ }));
89
+ //#endregion
90
+ export { getTtsProvider as a, registerHudPage as c, storeRenderJob as d, getRenderJob as i, setGlobalOutputDir as l, getGlobalOutputDir as n, init_hud_registry as o, getGlobalTtsProvider as r, isHudActive as s, getAudioSegments as t, storeAudioSegment as u };