demowright 2.0.6 → 2.0.8
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 +1 -1
- package/dist/config.mjs +68 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{setup-B6UqVosV.mjs → setup-BwdzChHS.mjs} +228 -15
- package/dist/{setup-D-Ut4FYK.d.mts → setup-Dwx0Dt54.d.mts} +9 -0
- package/dist/setup.d.mts +1 -1
- package/dist/setup.mjs +1 -1
- package/dist/video-script.mjs +3 -1
- package/package.json +1 -1
package/dist/config.d.mts
CHANGED
package/dist/config.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { n as defaultOptions } from "./setup-
|
|
1
|
+
import { n as defaultOptions } from "./setup-BwdzChHS.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
//#region src/config.ts
|
|
4
7
|
/**
|
|
5
8
|
* Approach 3: Config helper (one line in playwright.config.ts).
|
|
@@ -30,7 +33,71 @@ function withDemowright(config, options) {
|
|
|
30
33
|
const flag = `--require ${registerPath}`;
|
|
31
34
|
const existing = process.env.NODE_OPTIONS || "";
|
|
32
35
|
if (!existing.includes(flag)) process.env.NODE_OPTIONS = existing ? `${existing} ${flag}` : flag;
|
|
36
|
+
if (opts.audio) {
|
|
37
|
+
const display = findLocalDisplay();
|
|
38
|
+
if (display) {
|
|
39
|
+
process.env.DISPLAY = display;
|
|
40
|
+
const use = config.use ??= {};
|
|
41
|
+
const launch = use.launchOptions ??= {};
|
|
42
|
+
if (launch.headless === void 0) launch.headless = false;
|
|
43
|
+
}
|
|
44
|
+
const pipePath = setupPulsePipeSink(opts.outputDir);
|
|
45
|
+
if (pipePath) process.env.__DEMOWRIGHT_PULSE_PIPE = pipePath;
|
|
46
|
+
}
|
|
33
47
|
return config;
|
|
34
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a PulseAudio pipe-sink in the main process.
|
|
51
|
+
* Returns the FIFO path, or undefined if PulseAudio is unavailable.
|
|
52
|
+
*/
|
|
53
|
+
function setupPulsePipeSink(outputDir) {
|
|
54
|
+
try {
|
|
55
|
+
execSync("pactl info", { stdio: "pipe" });
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const tmpDir = join(process.cwd(), outputDir, "tmp");
|
|
60
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
61
|
+
const pipePath = join(tmpDir, `pulse-pipe-${Date.now()}.raw`);
|
|
62
|
+
try {
|
|
63
|
+
const modules = execSync("pactl list modules short", { encoding: "utf-8" });
|
|
64
|
+
for (const line of modules.split("\n")) if (line.includes("demowright_sink")) {
|
|
65
|
+
const modId = line.split(" ")[0];
|
|
66
|
+
try {
|
|
67
|
+
execSync(`pactl unload-module ${modId}`, { stdio: "pipe" });
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
try {
|
|
72
|
+
execSync(`pactl load-module module-pipe-sink sink_name=demowright_sink file="${pipePath}" rate=44100 channels=2 format=s16le sink_properties=device.description="Demowright_Audio_Capture"`, { stdio: "pipe" });
|
|
73
|
+
execSync("pactl set-default-sink demowright_sink", { stdio: "pipe" });
|
|
74
|
+
return pipePath;
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Find a local X display (Xvfb) suitable for headed browser.
|
|
81
|
+
* Prefers :99, then :98, then any local display.
|
|
82
|
+
*/
|
|
83
|
+
function findLocalDisplay() {
|
|
84
|
+
try {
|
|
85
|
+
const displays = readdirSync("/tmp/.X11-unix").filter((s) => s.startsWith("X")).map((s) => `:${s.slice(1)}`).sort((a, b) => {
|
|
86
|
+
const na = parseInt(a.slice(1)), nb = parseInt(b.slice(1));
|
|
87
|
+
if (na >= 98) return nb >= 98 ? na - nb : -1;
|
|
88
|
+
if (nb >= 98) return 1;
|
|
89
|
+
return na - nb;
|
|
90
|
+
});
|
|
91
|
+
for (const d of displays) try {
|
|
92
|
+
execSync(`DISPLAY=${d} xdpyinfo`, {
|
|
93
|
+
stdio: "pipe",
|
|
94
|
+
timeout: 2e3
|
|
95
|
+
});
|
|
96
|
+
return d;
|
|
97
|
+
} catch {}
|
|
98
|
+
} catch {}
|
|
99
|
+
const cur = process.env.DISPLAY;
|
|
100
|
+
if (cur && !cur.includes("localhost") && !cur.includes(":10")) return cur;
|
|
101
|
+
}
|
|
35
102
|
//#endregion
|
|
36
103
|
export { withDemowright };
|
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-
|
|
2
|
+
import { a as AudioWriter, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-Dwx0Dt54.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-
|
|
1
|
+
import { r as AudioWriter, t as applyHud } from "./setup-BwdzChHS.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";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { r as __toCommonJS } from "./chunk-C0p4GxOx.mjs";
|
|
2
2
|
import { a as getRenderJob, l as registerHudPage, n as getCurrentSpec, s as init_hud_registry, t as getAudioSegments, u as setGlobalOutputDir } from "./hud-registry-Wfd4b4Nu.mjs";
|
|
3
3
|
import { n as video_script_exports, t as init_video_script } from "./video-script.mjs";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
//#region src/hud-overlay.ts
|
|
8
8
|
/**
|
|
@@ -253,6 +253,7 @@ function audioCaptureMain() {
|
|
|
253
253
|
origConnect.call(gain, processor);
|
|
254
254
|
origConnect.call(processor, dest);
|
|
255
255
|
interceptors.set(dest, gain);
|
|
256
|
+
if (ctx.state === "suspended") ctx.resume?.();
|
|
256
257
|
return gain;
|
|
257
258
|
}
|
|
258
259
|
AudioNode.prototype.connect = function(dest, output, input) {
|
|
@@ -277,6 +278,7 @@ function audioCaptureMain() {
|
|
|
277
278
|
try {
|
|
278
279
|
const ctx = new AudioContext();
|
|
279
280
|
ctx.createMediaElementSource(this).connect(ctx.destination);
|
|
281
|
+
if (ctx.state === "suspended") ctx.resume();
|
|
280
282
|
} catch {}
|
|
281
283
|
}
|
|
282
284
|
return origPlay.call(this);
|
|
@@ -294,14 +296,24 @@ var AudioWriter = class {
|
|
|
294
296
|
chunks = [];
|
|
295
297
|
sampleRate = 44100;
|
|
296
298
|
channels = 2;
|
|
299
|
+
startMs = 0;
|
|
297
300
|
/**
|
|
298
301
|
* Called from the browser via page.exposeFunction.
|
|
299
302
|
* Receives interleaved stereo float32 samples.
|
|
300
303
|
*/
|
|
301
304
|
addChunk(samples, sampleRate) {
|
|
305
|
+
if (this.chunks.length === 0) this.startMs = Date.now();
|
|
302
306
|
this.sampleRate = sampleRate;
|
|
303
307
|
this.chunks.push(new Float32Array(samples));
|
|
304
308
|
}
|
|
309
|
+
/** Wall-clock time when first chunk arrived */
|
|
310
|
+
get captureStartMs() {
|
|
311
|
+
return this.startMs;
|
|
312
|
+
}
|
|
313
|
+
/** Sample rate of captured audio */
|
|
314
|
+
get rate() {
|
|
315
|
+
return this.sampleRate;
|
|
316
|
+
}
|
|
305
317
|
/** Total samples collected (interleaved, so / channels for per-channel) */
|
|
306
318
|
get totalSamples() {
|
|
307
319
|
return this.chunks.reduce((sum, c) => sum + c.length, 0);
|
|
@@ -339,6 +351,20 @@ var AudioWriter = class {
|
|
|
339
351
|
Buffer.from(int16.buffer).copy(buffer, 44);
|
|
340
352
|
writeFileSync(filePath, buffer);
|
|
341
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Return all chunks concatenated as interleaved stereo float32.
|
|
356
|
+
*/
|
|
357
|
+
toFloat32() {
|
|
358
|
+
const total = this.totalSamples;
|
|
359
|
+
if (total === 0) return new Float32Array(0);
|
|
360
|
+
const out = new Float32Array(total);
|
|
361
|
+
let off = 0;
|
|
362
|
+
for (const chunk of this.chunks) {
|
|
363
|
+
out.set(chunk, off);
|
|
364
|
+
off += chunk.length;
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
342
368
|
/** Reset for reuse */
|
|
343
369
|
clear() {
|
|
344
370
|
this.chunks = [];
|
|
@@ -374,9 +400,11 @@ async function applyHud(context, options) {
|
|
|
374
400
|
const contextStartMs = Date.now();
|
|
375
401
|
await context.addInitScript(generateListenerScript());
|
|
376
402
|
let audioWriter;
|
|
403
|
+
let pulseCapture;
|
|
377
404
|
if (opts.audio) {
|
|
378
405
|
audioWriter = new AudioWriter();
|
|
379
406
|
await context.addInitScript(generateAudioCaptureScript());
|
|
407
|
+
pulseCapture = startPulseCapture(opts.outputDir);
|
|
380
408
|
}
|
|
381
409
|
const hudOpts = {
|
|
382
410
|
cursor: opts.cursor,
|
|
@@ -405,10 +433,13 @@ async function applyHud(context, options) {
|
|
|
405
433
|
const outDir = join(process.cwd(), opts.outputDir);
|
|
406
434
|
const tmpDir = join(outDir, "tmp");
|
|
407
435
|
mkdirSync(tmpDir, { recursive: true });
|
|
436
|
+
const gitignorePath = join(outDir, ".gitignore");
|
|
437
|
+
if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, "*\n");
|
|
408
438
|
const audioPath = typeof opts.audio === "string" ? opts.audio : join(tmpDir, `demowright-audio-${Date.now()}.wav`);
|
|
409
439
|
const allPages = [...context.pages()];
|
|
410
440
|
context.on("page", (pg) => allPages.push(pg));
|
|
411
441
|
context.on("close", () => {
|
|
442
|
+
const pulseWavPath = pulseCapture?.stop();
|
|
412
443
|
for (const pg of allPages) {
|
|
413
444
|
const job = getRenderJob(pg);
|
|
414
445
|
if (job) {
|
|
@@ -418,19 +449,23 @@ async function applyHud(context, options) {
|
|
|
418
449
|
}
|
|
419
450
|
const segments = [];
|
|
420
451
|
for (const pg of allPages) segments.push(...getAudioSegments(pg));
|
|
452
|
+
const hasTts = segments.length > 0;
|
|
453
|
+
const hasBrowserAudio = audioWriter.totalSamples > 0;
|
|
454
|
+
const hasPulseAudio = pulseWavPath && existsSync(pulseWavPath);
|
|
455
|
+
if (!hasTts && !hasBrowserAudio && !hasPulseAudio) return;
|
|
421
456
|
let audioOffsetMs = 0;
|
|
422
|
-
if (
|
|
423
|
-
const firstSegMs = segments[0].timestampMs;
|
|
457
|
+
if (hasTts || hasPulseAudio) {
|
|
458
|
+
const firstSegMs = hasTts ? segments[0].timestampMs : contextStartMs;
|
|
424
459
|
audioOffsetMs = firstSegMs - contextStartMs;
|
|
425
|
-
buildAndSaveAudioTrack(segments, audioPath, firstSegMs);
|
|
426
|
-
} else
|
|
427
|
-
|
|
460
|
+
buildAndSaveAudioTrack(segments, audioPath, firstSegMs, hasBrowserAudio ? audioWriter : void 0, contextStartMs, hasPulseAudio ? pulseWavPath : void 0);
|
|
461
|
+
} else audioWriter.save(audioPath);
|
|
462
|
+
if (hasPulseAudio) console.log(`[demowright] Pulse audio captured: ${pulseWavPath}`);
|
|
428
463
|
const mp4Path = join(outDir, `${getCurrentSpec() ?? pageNames[0] ?? `demowright-${Date.now()}`}.mp4`);
|
|
429
464
|
const trimSec = (audioOffsetMs / 1e3).toFixed(3);
|
|
430
465
|
let muxed = false;
|
|
431
466
|
for (const videoPath of videoPaths) try {
|
|
432
467
|
if (!existsSync(videoPath)) continue;
|
|
433
|
-
execSync(`ffmpeg -y -ss ${trimSec} -i "${videoPath}" -i "${audioPath}" -c:v libx264 -preset fast -c:a aac -shortest "${mp4Path}"`, { stdio: "pipe" });
|
|
468
|
+
execSync(`ffmpeg -y -ss ${trimSec} -i "${videoPath}" -i "${audioPath}" -c:v libx264 -preset fast -crf 28 -c:a aac -b:a 64k -shortest "${mp4Path}"`, { stdio: "pipe" });
|
|
434
469
|
muxed = true;
|
|
435
470
|
try {
|
|
436
471
|
unlinkSync(audioPath);
|
|
@@ -439,7 +474,7 @@ async function applyHud(context, options) {
|
|
|
439
474
|
} catch {}
|
|
440
475
|
if (!muxed) {
|
|
441
476
|
console.log(`[demowright] Audio saved: ${audioPath}`);
|
|
442
|
-
console.log(`[demowright] Mux: ffmpeg -y -ss ${trimSec} -i <video.webm> -i "${audioPath}" -c:v libx264 -preset fast -c:a aac -shortest "${mp4Path}"`);
|
|
477
|
+
console.log(`[demowright] Mux: ffmpeg -y -ss ${trimSec} -i <video.webm> -i "${audioPath}" -c:v libx264 -preset fast -crf 28 -c:a aac -b:a 64k -shortest "${mp4Path}"`);
|
|
443
478
|
}
|
|
444
479
|
});
|
|
445
480
|
}
|
|
@@ -548,11 +583,12 @@ async function setupAudioCapture(page, writer) {
|
|
|
548
583
|
* wall-clock timestamps. Silence fills gaps between segments.
|
|
549
584
|
* This eliminates drift caused by page.evaluate overhead.
|
|
550
585
|
*/
|
|
551
|
-
function buildAndSaveAudioTrack(segments, outputPath, contextStartMs) {
|
|
552
|
-
if (segments.length === 0) return;
|
|
553
|
-
const firstBuf = segments[0]
|
|
554
|
-
|
|
555
|
-
|
|
586
|
+
function buildAndSaveAudioTrack(segments, outputPath, contextStartMs, browserAudio, contextCreationMs, pulseWavPath) {
|
|
587
|
+
if (segments.length === 0 && !browserAudio && !pulseWavPath) return;
|
|
588
|
+
const firstBuf = segments[0]?.wavBuf;
|
|
589
|
+
const dataOffset0 = firstBuf ? firstBuf.indexOf("data") + 8 : -1;
|
|
590
|
+
if (segments.length > 0 && dataOffset0 < 8) return;
|
|
591
|
+
const sampleRate = firstBuf ? firstBuf.readUInt32LE(24) : browserAudio?.rate ?? 44100;
|
|
556
592
|
const channels = 2;
|
|
557
593
|
const baseMs = contextStartMs;
|
|
558
594
|
let totalMs = 0;
|
|
@@ -565,6 +601,22 @@ function buildAndSaveAudioTrack(segments, outputPath, contextStartMs) {
|
|
|
565
601
|
const endMs = seg.timestampMs - baseMs + segDur;
|
|
566
602
|
if (endMs > totalMs) totalMs = endMs;
|
|
567
603
|
}
|
|
604
|
+
if (browserAudio && browserAudio.totalSamples > 0) {
|
|
605
|
+
const browserStartMs = browserAudio.captureStartMs;
|
|
606
|
+
const browserDurMs = browserAudio.duration * 1e3;
|
|
607
|
+
const browserEndMs = browserStartMs - baseMs + browserDurMs;
|
|
608
|
+
if (browserEndMs > totalMs) totalMs = browserEndMs;
|
|
609
|
+
}
|
|
610
|
+
if (pulseWavPath && existsSync(pulseWavPath)) try {
|
|
611
|
+
const pBuf = readFileSync(pulseWavPath);
|
|
612
|
+
const pDoff = pBuf.indexOf("data");
|
|
613
|
+
if (pDoff >= 0) {
|
|
614
|
+
const pSr = pBuf.readUInt32LE(24);
|
|
615
|
+
const pCh = pBuf.readUInt16LE(22);
|
|
616
|
+
const pDurMs = pBuf.readUInt32LE(pDoff + 4) / 2 / pCh / pSr * 1e3;
|
|
617
|
+
if (pDurMs > totalMs) totalMs = pDurMs;
|
|
618
|
+
}
|
|
619
|
+
} catch {}
|
|
568
620
|
const totalSamples = Math.ceil(totalMs / 1e3 * sampleRate * channels);
|
|
569
621
|
const trackBuffer = new Float32Array(totalSamples);
|
|
570
622
|
for (const seg of segments) {
|
|
@@ -588,6 +640,58 @@ function buildAndSaveAudioTrack(segments, outputPath, contextStartMs) {
|
|
|
588
640
|
const offsetSamples = Math.floor(offsetMs / 1e3 * sampleRate) * channels;
|
|
589
641
|
for (let i = 0; i < stereo.length && offsetSamples + i < trackBuffer.length; i++) trackBuffer[offsetSamples + i] += stereo[i];
|
|
590
642
|
}
|
|
643
|
+
if (browserAudio && browserAudio.totalSamples > 0) {
|
|
644
|
+
const browserPcm = browserAudio.toFloat32();
|
|
645
|
+
const browserOffsetMs = browserAudio.captureStartMs - baseMs;
|
|
646
|
+
const browserOffsetSamples = Math.max(0, Math.floor(browserOffsetMs / 1e3 * sampleRate) * channels);
|
|
647
|
+
if (browserAudio.rate === sampleRate) for (let i = 0; i < browserPcm.length && browserOffsetSamples + i < trackBuffer.length; i++) trackBuffer[browserOffsetSamples + i] += browserPcm[i];
|
|
648
|
+
else {
|
|
649
|
+
const ratio = browserAudio.rate / sampleRate;
|
|
650
|
+
const outLen = Math.floor(browserPcm.length / ratio);
|
|
651
|
+
for (let i = 0; i < outLen && browserOffsetSamples + i < trackBuffer.length; i++) {
|
|
652
|
+
const srcIdx = i * ratio;
|
|
653
|
+
const lo = Math.floor(srcIdx);
|
|
654
|
+
const hi = Math.min(lo + 1, browserPcm.length - 1);
|
|
655
|
+
const frac = srcIdx - lo;
|
|
656
|
+
trackBuffer[browserOffsetSamples + i] += browserPcm[lo] * (1 - frac) + browserPcm[hi] * frac;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (pulseWavPath && existsSync(pulseWavPath)) try {
|
|
661
|
+
const pulseBuf = readFileSync(pulseWavPath);
|
|
662
|
+
const pDoff = pulseBuf.indexOf("data");
|
|
663
|
+
if (pDoff >= 0) {
|
|
664
|
+
const pSr = pulseBuf.readUInt32LE(24);
|
|
665
|
+
const pCh = pulseBuf.readUInt16LE(22);
|
|
666
|
+
const pBps = pulseBuf.readUInt16LE(34);
|
|
667
|
+
const pcmData = pulseBuf.subarray(pDoff + 8);
|
|
668
|
+
const bytesPerSample = pBps / 8;
|
|
669
|
+
const sampleCount = Math.floor(pcmData.length / bytesPerSample);
|
|
670
|
+
const float32 = new Float32Array(sampleCount);
|
|
671
|
+
for (let i = 0; i < sampleCount; i++) if (pBps === 16) float32[i] = pcmData.readInt16LE(i * 2) / 32768;
|
|
672
|
+
else if (pBps === 32) float32[i] = pcmData.readFloatLE(i * 4);
|
|
673
|
+
const stereo = pCh === 1 ? (() => {
|
|
674
|
+
const s = new Float32Array(sampleCount * 2);
|
|
675
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
676
|
+
s[i * 2] = float32[i];
|
|
677
|
+
s[i * 2 + 1] = float32[i];
|
|
678
|
+
}
|
|
679
|
+
return s;
|
|
680
|
+
})() : float32;
|
|
681
|
+
if (pSr === sampleRate) for (let i = 0; i < stereo.length && i < trackBuffer.length; i++) trackBuffer[i] += stereo[i];
|
|
682
|
+
else {
|
|
683
|
+
const ratio = pSr / sampleRate;
|
|
684
|
+
const outLen = Math.min(Math.floor(stereo.length / ratio), trackBuffer.length);
|
|
685
|
+
for (let i = 0; i < outLen; i++) {
|
|
686
|
+
const srcIdx = i * ratio;
|
|
687
|
+
const lo = Math.floor(srcIdx);
|
|
688
|
+
const hi = Math.min(lo + 1, stereo.length - 1);
|
|
689
|
+
const frac = srcIdx - lo;
|
|
690
|
+
trackBuffer[i] += stereo[lo] * (1 - frac) + stereo[hi] * frac;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch {}
|
|
591
695
|
const int16 = new Int16Array(trackBuffer.length);
|
|
592
696
|
for (let i = 0; i < trackBuffer.length; i++) {
|
|
593
697
|
const s = Math.max(-1, Math.min(1, trackBuffer[i]));
|
|
@@ -639,7 +743,7 @@ function finalizeRenderJob(job, videoPaths) {
|
|
|
639
743
|
`-i "${job.wavPath}"`,
|
|
640
744
|
chapterArgs,
|
|
641
745
|
vf,
|
|
642
|
-
`-c:v libx264 -preset fast`,
|
|
746
|
+
`-c:v libx264 -preset fast -crf 28`,
|
|
643
747
|
`-c:a aac`,
|
|
644
748
|
`-shortest`,
|
|
645
749
|
`"${job.mp4Path}"`
|
|
@@ -664,5 +768,114 @@ function finalizeRenderJob(job, videoPaths) {
|
|
|
664
768
|
}
|
|
665
769
|
}
|
|
666
770
|
}
|
|
771
|
+
function startPulseCapture(outputDir) {
|
|
772
|
+
const g = globalThis;
|
|
773
|
+
if (g.__qaHudPulseCapture) return g.__qaHudPulseCapture;
|
|
774
|
+
const preCreatedPipe = process.env.__DEMOWRIGHT_PULSE_PIPE;
|
|
775
|
+
if (!preCreatedPipe) return startPulseCaptureDirectly(outputDir);
|
|
776
|
+
const tmpDir = join(process.cwd(), outputDir, "tmp");
|
|
777
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
778
|
+
const wavPath = join(tmpDir, `pulse-capture-${Date.now()}.wav`);
|
|
779
|
+
const pipePath = preCreatedPipe;
|
|
780
|
+
const MAX_BYTES = 44100 * 2 * 2 * 300;
|
|
781
|
+
let ringBuffer = Buffer.alloc(0);
|
|
782
|
+
let totalBytesReceived = 0;
|
|
783
|
+
let readerProc;
|
|
784
|
+
try {
|
|
785
|
+
readerProc = spawn("cat", [pipePath], { stdio: [
|
|
786
|
+
"ignore",
|
|
787
|
+
"pipe",
|
|
788
|
+
"ignore"
|
|
789
|
+
] });
|
|
790
|
+
readerProc.stdout.on("data", (chunk) => {
|
|
791
|
+
totalBytesReceived += chunk.length;
|
|
792
|
+
ringBuffer = Buffer.concat([ringBuffer, chunk]);
|
|
793
|
+
if (ringBuffer.length > MAX_BYTES) {
|
|
794
|
+
const excess = ringBuffer.length - MAX_BYTES;
|
|
795
|
+
const alignedExcess = excess - excess % 4;
|
|
796
|
+
ringBuffer = ringBuffer.subarray(alignedExcess);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
console.log(`[demowright] Pulse pipe-sink capture started: pipe=${pipePath}, PID=${readerProc.pid}`);
|
|
800
|
+
} catch (e) {
|
|
801
|
+
console.log(`[demowright] Pulse pipe reader failed: ${e.message}`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const handle = { stop() {
|
|
805
|
+
g.__qaHudPulseCapture = void 0;
|
|
806
|
+
try {
|
|
807
|
+
readerProc?.kill("SIGTERM");
|
|
808
|
+
} catch {}
|
|
809
|
+
try {
|
|
810
|
+
const modules = execSync("pactl list modules short", { encoding: "utf-8" });
|
|
811
|
+
for (const line of modules.split("\n")) if (line.includes("demowright_sink")) {
|
|
812
|
+
const modId = line.split(" ")[0];
|
|
813
|
+
try {
|
|
814
|
+
execSync(`pactl unload-module ${modId}`, { stdio: "pipe" });
|
|
815
|
+
} catch {}
|
|
816
|
+
}
|
|
817
|
+
} catch {}
|
|
818
|
+
const raw = ringBuffer;
|
|
819
|
+
const durSec = raw.length / 44100 / 4;
|
|
820
|
+
console.log(`[demowright] Pulse audio: ${(totalBytesReceived / 1024 / 1024).toFixed(1)}MB received, ${raw.length} bytes kept, ${durSec.toFixed(1)}s`);
|
|
821
|
+
try {
|
|
822
|
+
unlinkSync(pipePath);
|
|
823
|
+
} catch {}
|
|
824
|
+
if (raw.length === 0) return void 0;
|
|
825
|
+
const maxWavData = 4294967259;
|
|
826
|
+
const pcmData = raw.length > maxWavData ? raw.subarray(raw.length - maxWavData) : raw;
|
|
827
|
+
const hdr = Buffer.alloc(44);
|
|
828
|
+
hdr.write("RIFF", 0);
|
|
829
|
+
hdr.writeUInt32LE(36 + pcmData.length, 4);
|
|
830
|
+
hdr.write("WAVE", 8);
|
|
831
|
+
hdr.write("fmt ", 12);
|
|
832
|
+
hdr.writeUInt32LE(16, 16);
|
|
833
|
+
hdr.writeUInt16LE(1, 20);
|
|
834
|
+
hdr.writeUInt16LE(2, 22);
|
|
835
|
+
hdr.writeUInt32LE(44100, 24);
|
|
836
|
+
hdr.writeUInt32LE(44100 * 2 * 2, 28);
|
|
837
|
+
hdr.writeUInt16LE(4, 32);
|
|
838
|
+
hdr.writeUInt16LE(16, 34);
|
|
839
|
+
hdr.write("data", 36);
|
|
840
|
+
hdr.writeUInt32LE(pcmData.length, 40);
|
|
841
|
+
writeFileSync(wavPath, Buffer.concat([hdr, pcmData]));
|
|
842
|
+
return wavPath;
|
|
843
|
+
} };
|
|
844
|
+
g.__qaHudPulseCapture = handle;
|
|
845
|
+
return handle;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Fallback: create pipe-sink directly (for programmatic applyHud without config.ts).
|
|
849
|
+
*/
|
|
850
|
+
function startPulseCaptureDirectly(outputDir) {
|
|
851
|
+
try {
|
|
852
|
+
execSync("pactl info", { stdio: "pipe" });
|
|
853
|
+
} catch {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const tmpDir = join(process.cwd(), outputDir, "tmp");
|
|
857
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
858
|
+
const pipePath = join(tmpDir, `pulse-pipe-${Date.now()}.raw`);
|
|
859
|
+
try {
|
|
860
|
+
const modules = execSync("pactl list modules short", { encoding: "utf-8" });
|
|
861
|
+
for (const line of modules.split("\n")) if (line.includes("demowright_sink")) {
|
|
862
|
+
const modId = line.split(" ")[0];
|
|
863
|
+
try {
|
|
864
|
+
execSync(`pactl unload-module ${modId}`, { stdio: "pipe" });
|
|
865
|
+
} catch {}
|
|
866
|
+
}
|
|
867
|
+
} catch {}
|
|
868
|
+
try {
|
|
869
|
+
execSync(`pactl load-module module-pipe-sink sink_name=demowright_sink file="${pipePath}" rate=44100 channels=2 format=s16le sink_properties=device.description="Demowright_Audio_Capture"`, {
|
|
870
|
+
stdio: "pipe",
|
|
871
|
+
encoding: "utf-8"
|
|
872
|
+
}).trim();
|
|
873
|
+
execSync("pactl set-default-sink demowright_sink", { stdio: "pipe" });
|
|
874
|
+
} catch {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
process.env.__DEMOWRIGHT_PULSE_PIPE = pipePath;
|
|
878
|
+
return startPulseCapture(outputDir);
|
|
879
|
+
}
|
|
667
880
|
//#endregion
|
|
668
881
|
export { defaultOptions as n, AudioWriter as r, applyHud as t };
|
|
@@ -20,11 +20,16 @@ declare class AudioWriter {
|
|
|
20
20
|
private chunks;
|
|
21
21
|
private sampleRate;
|
|
22
22
|
private channels;
|
|
23
|
+
private startMs;
|
|
23
24
|
/**
|
|
24
25
|
* Called from the browser via page.exposeFunction.
|
|
25
26
|
* Receives interleaved stereo float32 samples.
|
|
26
27
|
*/
|
|
27
28
|
addChunk(samples: number[], sampleRate: number): void;
|
|
29
|
+
/** Wall-clock time when first chunk arrived */
|
|
30
|
+
get captureStartMs(): number;
|
|
31
|
+
/** Sample rate of captured audio */
|
|
32
|
+
get rate(): number;
|
|
28
33
|
/** Total samples collected (interleaved, so / channels for per-channel) */
|
|
29
34
|
get totalSamples(): number;
|
|
30
35
|
get duration(): number;
|
|
@@ -32,6 +37,10 @@ declare class AudioWriter {
|
|
|
32
37
|
* Write collected audio to a WAV file.
|
|
33
38
|
*/
|
|
34
39
|
save(filePath: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Return all chunks concatenated as interleaved stereo float32.
|
|
42
|
+
*/
|
|
43
|
+
toFloat32(): Float32Array;
|
|
35
44
|
/** Reset for reuse */
|
|
36
45
|
clear(): void;
|
|
37
46
|
}
|
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-
|
|
1
|
+
import { i as defaultOptions, n as TtsProvider, r as applyHud, t as QaHudOptions } from "./setup-Dwx0Dt54.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-
|
|
1
|
+
import { n as defaultOptions, t as applyHud } from "./setup-BwdzChHS.mjs";
|
|
2
2
|
export { applyHud, defaultOptions };
|
package/dist/video-script.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __exportAll, t as __esmMin } from "./chunk-C0p4GxOx.mjs";
|
|
2
2
|
import { c as isHudActive, d as storeAudioSegment, f as storeRenderJob, i as getGlobalTtsProvider, o as getTtsProvider, r as getGlobalOutputDir, s as init_hud_registry } from "./hud-registry-Wfd4b4Nu.mjs";
|
|
3
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
//#region src/video-script.ts
|
|
6
6
|
/**
|
|
@@ -388,6 +388,8 @@ var init_video_script = __esmMin((() => {
|
|
|
388
388
|
const baseName = opts?.baseName ?? `demowright-video-${Date.now()}`;
|
|
389
389
|
mkdirSync(outputDir, { recursive: true });
|
|
390
390
|
mkdirSync(tmpDir, { recursive: true });
|
|
391
|
+
const gitignorePath = join(outputDir, ".gitignore");
|
|
392
|
+
if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, "*\n");
|
|
391
393
|
const wavPath = join(tmpDir, `${baseName}.wav`);
|
|
392
394
|
const srtPath = join(tmpDir, `${baseName}.srt`);
|
|
393
395
|
const chaptersPath = join(tmpDir, `${baseName}-chapters.txt`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demowright",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
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": {
|