@spark-apps/quickpeek 1.2.4 → 1.2.5
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/README.md +62 -13
- package/dist/assets/music/manifest.json +1 -1
- package/dist/index.d.mts +49 -10
- package/dist/index.mjs +69 -22
- package/dist/mcp-tools.mjs +771 -270
- package/dist/qp.js +817 -312
- package/dist/{voices-CPpnWn39.d.mts → voices-ufga0oOn.d.mts} +37 -3
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +8 -1
- package/package.json +12 -2
package/dist/qp.js
CHANGED
|
@@ -105,6 +105,48 @@ var init_lifecycle = __esm({
|
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// src/lib/image-kind.ts
|
|
109
|
+
function mimeForKind(kind) {
|
|
110
|
+
return MIME[kind];
|
|
111
|
+
}
|
|
112
|
+
function sniffImageKind(buf) {
|
|
113
|
+
if (buf.length < 12) return null;
|
|
114
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "png";
|
|
115
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
|
|
116
|
+
if (buf.subarray(0, 3).toString("latin1") === "GIF") return "gif";
|
|
117
|
+
if (buf.subarray(0, 4).toString("latin1") === "RIFF" && buf.subarray(8, 12).toString("latin1") === "WEBP") {
|
|
118
|
+
return "webp";
|
|
119
|
+
}
|
|
120
|
+
const head = buf.subarray(0, 1024).toString("utf8").replace(/^/, "").trimStart();
|
|
121
|
+
if (/^<(\?xml|!--|!doctype svg|svg)[\s>]/i.test(head) && /<svg[\s>]/i.test(head)) return "svg";
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function imageDataUri(path) {
|
|
125
|
+
try {
|
|
126
|
+
const buf = (0, import_node_fs2.readFileSync)(path);
|
|
127
|
+
const kind = sniffImageKind(buf);
|
|
128
|
+
if (!kind) return null;
|
|
129
|
+
return `data:${mimeForKind(kind)};base64,${buf.toString("base64")}`;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var import_node_fs2, MIME, MIN_ICON_PX;
|
|
135
|
+
var init_image_kind = __esm({
|
|
136
|
+
"src/lib/image-kind.ts"() {
|
|
137
|
+
"use strict";
|
|
138
|
+
import_node_fs2 = require("fs");
|
|
139
|
+
MIME = {
|
|
140
|
+
png: "image/png",
|
|
141
|
+
jpeg: "image/jpeg",
|
|
142
|
+
gif: "image/gif",
|
|
143
|
+
webp: "image/webp",
|
|
144
|
+
svg: "image/svg+xml"
|
|
145
|
+
};
|
|
146
|
+
MIN_ICON_PX = 48;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
108
150
|
// src/lib/utils.ts
|
|
109
151
|
function getLanguageName(lang) {
|
|
110
152
|
if (!lang || lang === "en") return null;
|
|
@@ -344,7 +386,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier,
|
|
|
344
386
|
...detail ? { detail } : {},
|
|
345
387
|
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
346
388
|
};
|
|
347
|
-
await new Promise((
|
|
389
|
+
await new Promise((resolve11) => setTimeout(resolve11, Math.min(delayMs, 3e4)));
|
|
348
390
|
continue;
|
|
349
391
|
}
|
|
350
392
|
const retryAfter = response.headers.get("Retry-After");
|
|
@@ -384,7 +426,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier,
|
|
|
384
426
|
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
385
427
|
}
|
|
386
428
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
387
|
-
await new Promise((
|
|
429
|
+
await new Promise((resolve11) => setTimeout(resolve11, delayMs));
|
|
388
430
|
}
|
|
389
431
|
}
|
|
390
432
|
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
@@ -546,9 +588,6 @@ var init_SparkStripe_feature_gating = __esm({
|
|
|
546
588
|
});
|
|
547
589
|
|
|
548
590
|
// src/lib/billing.ts
|
|
549
|
-
function shouldWatermark(tier, videoDurationSeconds) {
|
|
550
|
-
return tier === "free" && videoDurationSeconds > 20;
|
|
551
|
-
}
|
|
552
591
|
function getUpgradeUrl(userEmail) {
|
|
553
592
|
return pricingUrl(userEmail ? { email: userEmail } : void 0);
|
|
554
593
|
}
|
|
@@ -583,7 +622,10 @@ function applyProfile(config, profile) {
|
|
|
583
622
|
format: "mp3",
|
|
584
623
|
bitrate: "192k",
|
|
585
624
|
...config.audio,
|
|
586
|
-
|
|
625
|
+
// outroOnly is on unless turned off, matching the video phase. The
|
|
626
|
+
// speedup buys the song its window, so it applies to the profile's own
|
|
627
|
+
// pace only: a speed somebody set by hand is the speed they wanted.
|
|
628
|
+
speed: config.audio?.speed ?? preset.audioSpeed * (config.music?.outroOnly !== false ? OUTRO_SONG_SPEEDUP : 1),
|
|
587
629
|
...config.audio?.voice || !preset.voice ? {} : { voice: preset.voice }
|
|
588
630
|
}
|
|
589
631
|
};
|
|
@@ -620,11 +662,11 @@ function defaultCaptionsFor(width, height) {
|
|
|
620
662
|
maxChars: 20
|
|
621
663
|
};
|
|
622
664
|
}
|
|
623
|
-
var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONFIG, VIDEO_PROFILES, DEFAULT_CAPTIONS;
|
|
665
|
+
var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONFIG, VIDEO_PROFILES, OUTRO_SONG_SPEEDUP, DEFAULT_CAPTIONS;
|
|
624
666
|
var init_config = __esm({
|
|
625
667
|
"src/lib/config.ts"() {
|
|
626
668
|
"use strict";
|
|
627
|
-
VERSION = "1.2.
|
|
669
|
+
VERSION = "1.2.5";
|
|
628
670
|
CONFIG_FILE = "quickpeek.config.json";
|
|
629
671
|
SIZE_PRESETS = {
|
|
630
672
|
wide: { width: 1920, height: 1080, zoom: 1 },
|
|
@@ -651,11 +693,12 @@ var init_config = __esm({
|
|
|
651
693
|
// ms
|
|
652
694
|
transitions: "none",
|
|
653
695
|
contrast: 0.25,
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
tempo
|
|
696
|
+
// The recording plays at the pace it was filmed at. A tempo above 1 is
|
|
697
|
+
// multiplied by the narration speed in VIDEO_PROFILES and by the fit
|
|
698
|
+
// pass's squeeze, and the three together read as frantic rather than
|
|
699
|
+
// brisk. Speed belongs in one place, so it lives in audioSpeed; set a
|
|
700
|
+
// tempo in quickpeek.config.json to push the picture on top of that.
|
|
701
|
+
tempo: 1,
|
|
659
702
|
stepDelay: 200
|
|
660
703
|
// ms delay between steps
|
|
661
704
|
},
|
|
@@ -669,18 +712,24 @@ var init_config = __esm({
|
|
|
669
712
|
// these bitrates - the encode is pure wall-clock tax on every run.
|
|
670
713
|
wide: {
|
|
671
714
|
video: { width: 1920, height: 1080, zoom: 1.5, fitLimitSecs: 0, maxSpeedup: 1, preset: "veryfast" },
|
|
672
|
-
|
|
715
|
+
// A tutorial is watched to learn something, so the narrator is allowed to
|
|
716
|
+
// be brisk but never hurried.
|
|
717
|
+
audioSpeed: 1.15
|
|
673
718
|
},
|
|
674
719
|
short: {
|
|
675
720
|
// fadeOut 0: the video ends on a card the viewer is meant to read,
|
|
676
721
|
// and a fade takes it away mid-sentence.
|
|
677
722
|
video: { width: 1080, height: 1920, zoom: 3, fitLimitSecs: 60, maxSpeedup: 1.7, preset: "veryfast", fadeOut: 0 },
|
|
678
|
-
|
|
723
|
+
// 1.8 was unwatchable: stacked on tempo 1.2 and the outro speedup it ran
|
|
724
|
+
// the voice at better than twice its natural rate. A short wants energy,
|
|
725
|
+
// which is the voice and the cutting, not the tape running fast.
|
|
726
|
+
audioSpeed: 1,
|
|
679
727
|
// A short lives or dies on energy. Ava is the warmest, most animated of
|
|
680
728
|
// the free neural voices; Sonia reads a short like the shipping forecast.
|
|
681
729
|
voice: "en-US-AvaMultilingualNeural"
|
|
682
730
|
}
|
|
683
731
|
};
|
|
732
|
+
OUTRO_SONG_SPEEDUP = 1.13;
|
|
684
733
|
DEFAULT_CAPTIONS = {
|
|
685
734
|
font: "Arial",
|
|
686
735
|
outline: "#000000",
|
|
@@ -829,6 +878,7 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
829
878
|
const deadline = Date.now() + cap;
|
|
830
879
|
let previous = -1;
|
|
831
880
|
let unchanged = 0;
|
|
881
|
+
let clear = 0;
|
|
832
882
|
while (Date.now() < deadline) {
|
|
833
883
|
const count = await page.evaluate((selector) => {
|
|
834
884
|
let visible = 0;
|
|
@@ -838,7 +888,12 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
838
888
|
}
|
|
839
889
|
return visible;
|
|
840
890
|
}, PLACEHOLDER_SELECTOR).catch(() => 0);
|
|
841
|
-
if (count === 0)
|
|
891
|
+
if (count === 0) {
|
|
892
|
+
if (++clear >= PLACEHOLDER_CLEAR_POLLS) return;
|
|
893
|
+
await page.waitForTimeout(PLACEHOLDER_POLL_MS);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
clear = 0;
|
|
842
897
|
if (count === previous) {
|
|
843
898
|
if (++unchanged >= PLACEHOLDER_STATIC_POLLS) return;
|
|
844
899
|
} else {
|
|
@@ -1098,7 +1153,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
1098
1153
|
});
|
|
1099
1154
|
}
|
|
1100
1155
|
}
|
|
1101
|
-
var import_playwright4, SETTLE_POLL_MS, SETTLE_STABLE_POLLS, SETTLE_CAP_MS, PLACEHOLDER_POLL_MS, PLACEHOLDER_CAP_MS, PLACEHOLDER_STATIC_POLLS, PLACEHOLDER_SELECTOR, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
1156
|
+
var import_playwright4, SETTLE_POLL_MS, SETTLE_STABLE_POLLS, SETTLE_CAP_MS, PLACEHOLDER_POLL_MS, PLACEHOLDER_CAP_MS, PLACEHOLDER_STATIC_POLLS, PLACEHOLDER_CLEAR_POLLS, PLACEHOLDER_SELECTOR, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
1102
1157
|
var init_crawl = __esm({
|
|
1103
1158
|
"src/lib/crawl.ts"() {
|
|
1104
1159
|
"use strict";
|
|
@@ -1110,6 +1165,7 @@ var init_crawl = __esm({
|
|
|
1110
1165
|
PLACEHOLDER_POLL_MS = 250;
|
|
1111
1166
|
PLACEHOLDER_CAP_MS = 8e3;
|
|
1112
1167
|
PLACEHOLDER_STATIC_POLLS = 4;
|
|
1168
|
+
PLACEHOLDER_CLEAR_POLLS = 3;
|
|
1113
1169
|
PLACEHOLDER_SELECTOR = [
|
|
1114
1170
|
'[aria-busy="true"]',
|
|
1115
1171
|
'[role="progressbar"]',
|
|
@@ -1259,13 +1315,13 @@ function canEncode(encoder) {
|
|
|
1259
1315
|
]);
|
|
1260
1316
|
}
|
|
1261
1317
|
function run(args) {
|
|
1262
|
-
return new Promise((
|
|
1318
|
+
return new Promise((resolve11) => {
|
|
1263
1319
|
let settled = false;
|
|
1264
1320
|
const done = (ok) => {
|
|
1265
1321
|
if (settled) return;
|
|
1266
1322
|
settled = true;
|
|
1267
1323
|
clearTimeout(timer);
|
|
1268
|
-
|
|
1324
|
+
resolve11(ok);
|
|
1269
1325
|
};
|
|
1270
1326
|
const proc = trackChild((0, import_node_child_process2.spawn)("ffmpeg", args, { stdio: "ignore" }));
|
|
1271
1327
|
const timer = setTimeout(() => {
|
|
@@ -1332,7 +1388,7 @@ function spawnFFmpeg(cmd2, args) {
|
|
|
1332
1388
|
return trackChild((0, import_node_child_process3.spawn)(cmd2, args));
|
|
1333
1389
|
}
|
|
1334
1390
|
function runFFmpeg(args, errorLabel) {
|
|
1335
|
-
return new Promise((
|
|
1391
|
+
return new Promise((resolve11, reject) => {
|
|
1336
1392
|
const proc = spawnFFmpeg("ffmpeg", args);
|
|
1337
1393
|
let stderr = "";
|
|
1338
1394
|
proc.stderr.on("data", (d) => {
|
|
@@ -1340,7 +1396,7 @@ function runFFmpeg(args, errorLabel) {
|
|
|
1340
1396
|
});
|
|
1341
1397
|
proc.on("close", (code) => {
|
|
1342
1398
|
if (code !== 0) reject(new Error(`${errorLabel}: ${stderr.slice(-500)}`));
|
|
1343
|
-
else
|
|
1399
|
+
else resolve11();
|
|
1344
1400
|
});
|
|
1345
1401
|
proc.on("error", reject);
|
|
1346
1402
|
});
|
|
@@ -1367,8 +1423,13 @@ async function composeVideo(opts) {
|
|
|
1367
1423
|
musicPath,
|
|
1368
1424
|
musicVolume = 0.3,
|
|
1369
1425
|
musicTargetLufs = MUSIC_BED_LUFS,
|
|
1370
|
-
|
|
1371
|
-
|
|
1426
|
+
// Sized for the outro window: the song has 5.5s, so a 2s fade at each end
|
|
1427
|
+
// left 1.5s of music anyone could actually hear. Up fast, hold, out slow.
|
|
1428
|
+
musicFadeIn = 1,
|
|
1429
|
+
musicFadeOut = 1.5,
|
|
1430
|
+
tempo = 1,
|
|
1431
|
+
musicStartSecs = 0,
|
|
1432
|
+
musicDurationSecs = 0,
|
|
1372
1433
|
width,
|
|
1373
1434
|
height,
|
|
1374
1435
|
fps,
|
|
@@ -1429,7 +1490,19 @@ async function composeVideo(opts) {
|
|
|
1429
1490
|
const clipAudioLabel = opts.hasClipAudio ? "[0:a]" : "";
|
|
1430
1491
|
const hasVoiceover = !!voiceoverPath;
|
|
1431
1492
|
const hasMusic = !!musicPath;
|
|
1432
|
-
const
|
|
1493
|
+
const bedStart = Math.max(0, Math.min(musicStartSecs, Math.max(0, effectiveDuration - 0.5)));
|
|
1494
|
+
const bedLength = Math.max(0.5, effectiveDuration - bedStart);
|
|
1495
|
+
const bedDelay = bedStart > 0 ? `adelay=${Math.round(bedStart * 1e3)}|${Math.round(bedStart * 1e3)},` : "";
|
|
1496
|
+
const bedTempo = tempo > 1 ? 1 / tempo : 1;
|
|
1497
|
+
const bedSlow = bedTempo === 1 ? "" : `atempo=${bedTempo.toFixed(6)},`;
|
|
1498
|
+
const bedSource = bedLength * bedTempo;
|
|
1499
|
+
const bedSeek = musicDurationSecs > bedSource + 0.05 ? musicDurationSecs - bedSource : 0;
|
|
1500
|
+
const bedTrimIn = bedSeek > 0 ? `atrim=start=${bedSeek.toFixed(3)},asetpts=PTS-STARTPTS,` : "";
|
|
1501
|
+
const bedFadeOut = bedSeek > 0 ? 0 : musicFadeOut * tempo;
|
|
1502
|
+
const bedLoop = bedSeek > 0 ? "" : "aloop=loop=-1:size=2e+09,";
|
|
1503
|
+
const bedFadeIn = musicFadeIn * tempo;
|
|
1504
|
+
const musicFadeOutStart = Math.max(0, bedLength - bedFadeOut);
|
|
1505
|
+
const bedFadeOutFilter = bedFadeOut > 0 ? `afade=t=out:st=${musicFadeOutStart}:d=${bedFadeOut},` : "";
|
|
1433
1506
|
const vfStr = [...vf, ...encoder.filter ? [encoder.filter] : []].join(",");
|
|
1434
1507
|
if (hasVoiceover && hasMusic) {
|
|
1435
1508
|
const mixInputs = clipAudioLabel ? 3 : 2;
|
|
@@ -1445,7 +1518,7 @@ async function composeVideo(opts) {
|
|
|
1445
1518
|
// card, which is the one place the bed is the only thing playing.
|
|
1446
1519
|
`[voicekey0]apad=whole_dur=${effectiveDuration}[voicekey]`,
|
|
1447
1520
|
// Music: normalised to a known level, trimmed, then faded.
|
|
1448
|
-
`[2:a]
|
|
1521
|
+
`[2:a]${bedTrimIn}${bedLoop}${bedSlow}atrim=0:${bedLength},loudnorm=I=${musicTargetLufs}:TP=-9:LRA=11,volume=${musicVolume},afade=t=in:st=0:d=${bedFadeIn},${bedFadeOutFilter}${bedDelay}apad=whole_dur=${effectiveDuration}[bed]`,
|
|
1449
1522
|
`[bed][voicekey]${DUCK}[music]`,
|
|
1450
1523
|
// normalize=0, because amix's default divides every input by the number
|
|
1451
1524
|
// of them: adding the bed used to quietly take 6 dB off the narration,
|
|
@@ -1469,7 +1542,7 @@ async function composeVideo(opts) {
|
|
|
1469
1542
|
args.push(...encoder.output);
|
|
1470
1543
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
1471
1544
|
} else if (hasMusic) {
|
|
1472
|
-
const musicFilter =
|
|
1545
|
+
const musicFilter = `${bedTrimIn}${bedLoop}${bedSlow}atrim=0:${bedLength},volume=${musicVolume},afade=t=in:st=0:d=${bedFadeIn},${bedFadeOutFilter}${bedDelay}apad=whole_dur=${effectiveDuration},${import_video_kit3.MASTER_CHAIN}`;
|
|
1473
1546
|
args.push("-vf", vfStr);
|
|
1474
1547
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
1475
1548
|
args.push(...encoder.output);
|
|
@@ -1594,14 +1667,14 @@ var init_gif = __esm({
|
|
|
1594
1667
|
// src/lib/ffmpeg/media.ts
|
|
1595
1668
|
function cacheKey(filePath) {
|
|
1596
1669
|
try {
|
|
1597
|
-
const { size, mtimeMs } = (0,
|
|
1670
|
+
const { size, mtimeMs } = (0, import_node_fs5.statSync)(filePath);
|
|
1598
1671
|
return `${filePath}:${size}:${mtimeMs}`;
|
|
1599
1672
|
} catch {
|
|
1600
1673
|
return null;
|
|
1601
1674
|
}
|
|
1602
1675
|
}
|
|
1603
1676
|
function getVideoDimensions(filePath) {
|
|
1604
|
-
return new Promise((
|
|
1677
|
+
return new Promise((resolve11, reject) => {
|
|
1605
1678
|
const args = [
|
|
1606
1679
|
"-v",
|
|
1607
1680
|
"error",
|
|
@@ -1632,12 +1705,12 @@ function getVideoDimensions(filePath) {
|
|
|
1632
1705
|
reject(new Error(`ffprobe returned no dimensions for ${filePath}`));
|
|
1633
1706
|
return;
|
|
1634
1707
|
}
|
|
1635
|
-
|
|
1708
|
+
resolve11({ width: w, height: h });
|
|
1636
1709
|
});
|
|
1637
1710
|
});
|
|
1638
1711
|
}
|
|
1639
1712
|
function hasAudioStream(filePath) {
|
|
1640
|
-
return new Promise((
|
|
1713
|
+
return new Promise((resolve11) => {
|
|
1641
1714
|
const proc = spawnFFmpeg("ffprobe", [
|
|
1642
1715
|
"-v",
|
|
1643
1716
|
"error",
|
|
@@ -1653,15 +1726,15 @@ function hasAudioStream(filePath) {
|
|
|
1653
1726
|
proc.stdout.on("data", (d) => {
|
|
1654
1727
|
out += d;
|
|
1655
1728
|
});
|
|
1656
|
-
proc.on("close", () =>
|
|
1657
|
-
proc.on("error", () =>
|
|
1729
|
+
proc.on("close", () => resolve11(out.trim().length > 0));
|
|
1730
|
+
proc.on("error", () => resolve11(false));
|
|
1658
1731
|
});
|
|
1659
1732
|
}
|
|
1660
1733
|
function getMediaDuration(filePath) {
|
|
1661
1734
|
const key = cacheKey(filePath);
|
|
1662
1735
|
const cached = key === null ? void 0 : durationCache.get(key);
|
|
1663
1736
|
if (cached !== void 0) return Promise.resolve(cached);
|
|
1664
|
-
return new Promise((
|
|
1737
|
+
return new Promise((resolve11, reject) => {
|
|
1665
1738
|
const args = [
|
|
1666
1739
|
"-v",
|
|
1667
1740
|
"error",
|
|
@@ -1688,13 +1761,13 @@ function getMediaDuration(filePath) {
|
|
|
1688
1761
|
const ms = Math.ceil(parseFloat(stdout.trim()) * 1e3);
|
|
1689
1762
|
const freshKey = cacheKey(filePath);
|
|
1690
1763
|
if (freshKey !== null && Number.isFinite(ms)) durationCache.set(freshKey, ms);
|
|
1691
|
-
|
|
1764
|
+
resolve11(ms);
|
|
1692
1765
|
});
|
|
1693
1766
|
proc.on("error", reject);
|
|
1694
1767
|
});
|
|
1695
1768
|
}
|
|
1696
1769
|
function getAudioCodec(filePath) {
|
|
1697
|
-
return new Promise((
|
|
1770
|
+
return new Promise((resolve11) => {
|
|
1698
1771
|
const args = [
|
|
1699
1772
|
"-v",
|
|
1700
1773
|
"error",
|
|
@@ -1711,8 +1784,8 @@ function getAudioCodec(filePath) {
|
|
|
1711
1784
|
proc.stdout.on("data", (data) => {
|
|
1712
1785
|
stdout += data;
|
|
1713
1786
|
});
|
|
1714
|
-
proc.on("close", (code) =>
|
|
1715
|
-
proc.on("error", () =>
|
|
1787
|
+
proc.on("close", (code) => resolve11(code === 0 ? stdout.trim() : ""));
|
|
1788
|
+
proc.on("error", () => resolve11(""));
|
|
1716
1789
|
});
|
|
1717
1790
|
}
|
|
1718
1791
|
async function isHonestWav(filePath) {
|
|
@@ -1854,11 +1927,11 @@ function concatVideoWithFullFrameOutro(opts) {
|
|
|
1854
1927
|
"ffmpeg full-frame outro concat failed"
|
|
1855
1928
|
);
|
|
1856
1929
|
}
|
|
1857
|
-
var
|
|
1930
|
+
var import_node_fs5, durationCache, TRIM_EDGE_SILENCE;
|
|
1858
1931
|
var init_media = __esm({
|
|
1859
1932
|
"src/lib/ffmpeg/media.ts"() {
|
|
1860
1933
|
"use strict";
|
|
1861
|
-
|
|
1934
|
+
import_node_fs5 = require("fs");
|
|
1862
1935
|
init_spawn();
|
|
1863
1936
|
durationCache = /* @__PURE__ */ new Map();
|
|
1864
1937
|
TRIM_EDGE_SILENCE = "silenceremove=start_periods=1:start_silence=0:start_threshold=-45dB:detection=peak,areverse,silenceremove=start_periods=1:start_silence=0:start_threshold=-45dB:detection=peak,areverse";
|
|
@@ -1867,7 +1940,7 @@ var init_media = __esm({
|
|
|
1867
1940
|
|
|
1868
1941
|
// src/lib/ffmpeg/silence.ts
|
|
1869
1942
|
function getVideoStreamDuration(inputPath) {
|
|
1870
|
-
return new Promise((
|
|
1943
|
+
return new Promise((resolve11) => {
|
|
1871
1944
|
const proc = spawnFFmpeg("ffprobe", [
|
|
1872
1945
|
"-v",
|
|
1873
1946
|
"error",
|
|
@@ -1884,12 +1957,12 @@ function getVideoStreamDuration(inputPath) {
|
|
|
1884
1957
|
proc.stdout.on("data", (d) => {
|
|
1885
1958
|
out += d;
|
|
1886
1959
|
});
|
|
1887
|
-
proc.on("error", () =>
|
|
1888
|
-
proc.on("close", () =>
|
|
1960
|
+
proc.on("error", () => resolve11(0));
|
|
1961
|
+
proc.on("close", () => resolve11(Number.parseFloat(out.trim()) || 0));
|
|
1889
1962
|
});
|
|
1890
1963
|
}
|
|
1891
1964
|
function detectSilences(inputPath, minDurSecs) {
|
|
1892
|
-
return new Promise((
|
|
1965
|
+
return new Promise((resolve11) => {
|
|
1893
1966
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1894
1967
|
"-hide_banner",
|
|
1895
1968
|
"-nostats",
|
|
@@ -1908,7 +1981,7 @@ function detectSilences(inputPath, minDurSecs) {
|
|
|
1908
1981
|
proc.stderr.on("data", (d) => {
|
|
1909
1982
|
err += d;
|
|
1910
1983
|
});
|
|
1911
|
-
proc.on("error", () =>
|
|
1984
|
+
proc.on("error", () => resolve11([]));
|
|
1912
1985
|
proc.on("close", () => {
|
|
1913
1986
|
const spans = [];
|
|
1914
1987
|
let start = null;
|
|
@@ -1923,12 +1996,12 @@ function detectSilences(inputPath, minDurSecs) {
|
|
|
1923
1996
|
start = null;
|
|
1924
1997
|
}
|
|
1925
1998
|
}
|
|
1926
|
-
|
|
1999
|
+
resolve11(spans);
|
|
1927
2000
|
});
|
|
1928
2001
|
});
|
|
1929
2002
|
}
|
|
1930
2003
|
function detectFrozenSpans(inputPath, minDurSecs) {
|
|
1931
|
-
return new Promise((
|
|
2004
|
+
return new Promise((resolve11) => {
|
|
1932
2005
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1933
2006
|
"-hide_banner",
|
|
1934
2007
|
"-nostats",
|
|
@@ -1949,7 +2022,7 @@ function detectFrozenSpans(inputPath, minDurSecs) {
|
|
|
1949
2022
|
proc.stderr.on("data", (d) => {
|
|
1950
2023
|
err += d;
|
|
1951
2024
|
});
|
|
1952
|
-
proc.on("error", () =>
|
|
2025
|
+
proc.on("error", () => resolve11([]));
|
|
1953
2026
|
proc.on("close", () => {
|
|
1954
2027
|
const spans = [];
|
|
1955
2028
|
let start = null;
|
|
@@ -1962,12 +2035,12 @@ function detectFrozenSpans(inputPath, minDurSecs) {
|
|
|
1962
2035
|
start = null;
|
|
1963
2036
|
}
|
|
1964
2037
|
}
|
|
1965
|
-
|
|
2038
|
+
resolve11(spans);
|
|
1966
2039
|
});
|
|
1967
2040
|
});
|
|
1968
2041
|
}
|
|
1969
2042
|
function detectBlackSpans(inputPath, minDurSecs) {
|
|
1970
|
-
return new Promise((
|
|
2043
|
+
return new Promise((resolve11) => {
|
|
1971
2044
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1972
2045
|
"-hide_banner",
|
|
1973
2046
|
"-nostats",
|
|
@@ -1990,19 +2063,19 @@ function detectBlackSpans(inputPath, minDurSecs) {
|
|
|
1990
2063
|
proc.stderr.on("data", (d) => {
|
|
1991
2064
|
err += d;
|
|
1992
2065
|
});
|
|
1993
|
-
proc.on("error", () =>
|
|
2066
|
+
proc.on("error", () => resolve11([]));
|
|
1994
2067
|
proc.on("close", () => {
|
|
1995
2068
|
const spans = [];
|
|
1996
2069
|
for (const line of err.split("\n")) {
|
|
1997
2070
|
const m = line.match(/black_start:\s*([\d.]+)\s+black_end:\s*([\d.]+)/);
|
|
1998
2071
|
if (m?.[1] && m[2]) spans.push({ start: Number.parseFloat(m[1]), end: Number.parseFloat(m[2]) });
|
|
1999
2072
|
}
|
|
2000
|
-
|
|
2073
|
+
resolve11(spans);
|
|
2001
2074
|
});
|
|
2002
2075
|
});
|
|
2003
2076
|
}
|
|
2004
2077
|
function detectWhiteSpans(inputPath, minDurSecs) {
|
|
2005
|
-
return new Promise((
|
|
2078
|
+
return new Promise((resolve11) => {
|
|
2006
2079
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2007
2080
|
"-hide_banner",
|
|
2008
2081
|
"-nostats",
|
|
@@ -2020,14 +2093,14 @@ function detectWhiteSpans(inputPath, minDurSecs) {
|
|
|
2020
2093
|
proc.stderr.on("data", (d) => {
|
|
2021
2094
|
err += d;
|
|
2022
2095
|
});
|
|
2023
|
-
proc.on("error", () =>
|
|
2096
|
+
proc.on("error", () => resolve11([]));
|
|
2024
2097
|
proc.on("close", () => {
|
|
2025
2098
|
const spans = [];
|
|
2026
2099
|
for (const line of err.split("\n")) {
|
|
2027
2100
|
const m = line.match(/black_start:\s*([\d.]+)\s+black_end:\s*([\d.]+)/);
|
|
2028
2101
|
if (m?.[1] && m[2]) spans.push({ start: Number.parseFloat(m[1]), end: Number.parseFloat(m[2]) });
|
|
2029
2102
|
}
|
|
2030
|
-
|
|
2103
|
+
resolve11(spans);
|
|
2031
2104
|
});
|
|
2032
2105
|
});
|
|
2033
2106
|
}
|
|
@@ -2117,7 +2190,7 @@ async function cutSpansFromVideo(inputPath, outputPath, spans, preset) {
|
|
|
2117
2190
|
const drop = keep.map((s) => `between(t,${s.start.toFixed(3)},${s.end.toFixed(3)})`).join("+");
|
|
2118
2191
|
const filter = `[0:v]select='not(${drop})',setpts=N/FRAME_RATE/TB[outv]`;
|
|
2119
2192
|
const enc = await passEncoder(preset);
|
|
2120
|
-
return new Promise((
|
|
2193
|
+
return new Promise((resolve11) => {
|
|
2121
2194
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2122
2195
|
"-y",
|
|
2123
2196
|
...filterThreadArgs(),
|
|
@@ -2135,8 +2208,8 @@ async function cutSpansFromVideo(inputPath, outputPath, spans, preset) {
|
|
|
2135
2208
|
outputPath
|
|
2136
2209
|
]);
|
|
2137
2210
|
proc.stderr.resume();
|
|
2138
|
-
proc.on("error", () =>
|
|
2139
|
-
proc.on("close", (code) =>
|
|
2211
|
+
proc.on("error", () => resolve11(false));
|
|
2212
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2140
2213
|
});
|
|
2141
2214
|
}
|
|
2142
2215
|
function buildRetimeFilter(spans, stepBounds, speed, fps) {
|
|
@@ -2218,7 +2291,7 @@ async function retimeSpansInVideo(inputPath, outputPath, spans, stepBounds, spee
|
|
|
2218
2291
|
const filter = buildRetimeFilter(spans, stepBounds, speed, fps);
|
|
2219
2292
|
if (!filter) return false;
|
|
2220
2293
|
const enc = await passEncoder(preset);
|
|
2221
|
-
return new Promise((
|
|
2294
|
+
return new Promise((resolve11) => {
|
|
2222
2295
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2223
2296
|
"-y",
|
|
2224
2297
|
...filterThreadArgs(),
|
|
@@ -2236,8 +2309,8 @@ async function retimeSpansInVideo(inputPath, outputPath, spans, stepBounds, spee
|
|
|
2236
2309
|
outputPath
|
|
2237
2310
|
]);
|
|
2238
2311
|
proc.stderr.resume();
|
|
2239
|
-
proc.on("error", () =>
|
|
2240
|
-
proc.on("close", (code) =>
|
|
2312
|
+
proc.on("error", () => resolve11(false));
|
|
2313
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2241
2314
|
});
|
|
2242
2315
|
}
|
|
2243
2316
|
async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, preset, protectAfterSecs, narrationPath, protectBeforeSecs, leadSpans, keepSpans) {
|
|
@@ -2263,7 +2336,7 @@ async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, pr
|
|
|
2263
2336
|
const filter = buildCompressionFilter(spans, totalSecs, maxSecs, protectAfterSecs, protectBeforeSecs);
|
|
2264
2337
|
if (!filter) return false;
|
|
2265
2338
|
const enc = await passEncoder(preset);
|
|
2266
|
-
return new Promise((
|
|
2339
|
+
return new Promise((resolve11) => {
|
|
2267
2340
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2268
2341
|
"-y",
|
|
2269
2342
|
...filterThreadArgs(),
|
|
@@ -2289,8 +2362,8 @@ async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, pr
|
|
|
2289
2362
|
outputPath
|
|
2290
2363
|
]);
|
|
2291
2364
|
proc.stderr.resume();
|
|
2292
|
-
proc.on("error", () =>
|
|
2293
|
-
proc.on("close", (code) =>
|
|
2365
|
+
proc.on("error", () => resolve11(false));
|
|
2366
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2294
2367
|
});
|
|
2295
2368
|
}
|
|
2296
2369
|
async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, introSecs, outroSecs, maxFactor, preset) {
|
|
@@ -2314,7 +2387,7 @@ async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, intro
|
|
|
2314
2387
|
"[v0][a0][v1][a1][v2][a2]concat=n=3:v=1:a=1[outv][outa]"
|
|
2315
2388
|
].join(";");
|
|
2316
2389
|
const enc = await passEncoder(preset);
|
|
2317
|
-
const ok = await new Promise((
|
|
2390
|
+
const ok = await new Promise((resolve11) => {
|
|
2318
2391
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2319
2392
|
"-y",
|
|
2320
2393
|
...filterThreadArgs(),
|
|
@@ -2335,8 +2408,8 @@ async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, intro
|
|
|
2335
2408
|
outputPath
|
|
2336
2409
|
]);
|
|
2337
2410
|
proc.stderr.resume();
|
|
2338
|
-
proc.on("error", () =>
|
|
2339
|
-
proc.on("close", (code) =>
|
|
2411
|
+
proc.on("error", () => resolve11(false));
|
|
2412
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2340
2413
|
});
|
|
2341
2414
|
return { ok, factor, reachedTarget: needed <= maxFactor };
|
|
2342
2415
|
}
|
|
@@ -2372,7 +2445,7 @@ async function spliceVideos(opts) {
|
|
|
2372
2445
|
const audioPresent = await Promise.all(
|
|
2373
2446
|
overlays.map((o) => o.still ? Promise.resolve(false) : hasAudioStream(o.path))
|
|
2374
2447
|
);
|
|
2375
|
-
return new Promise((
|
|
2448
|
+
return new Promise((resolve11, reject) => {
|
|
2376
2449
|
const args = ["-y", "-i", inputPath];
|
|
2377
2450
|
for (const overlay of overlays) {
|
|
2378
2451
|
if (overlay.still) {
|
|
@@ -2435,7 +2508,7 @@ async function spliceVideos(opts) {
|
|
|
2435
2508
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", "18", "-pix_fmt", "yuv420p");
|
|
2436
2509
|
if (clipAudio.length > 0) args.push("-c:a", "aac");
|
|
2437
2510
|
args.push(outputPath);
|
|
2438
|
-
runFFmpeg(args, "ffmpeg splice failed").then(
|
|
2511
|
+
runFFmpeg(args, "ffmpeg splice failed").then(resolve11, reject);
|
|
2439
2512
|
});
|
|
2440
2513
|
}
|
|
2441
2514
|
var CLIP_AUDIO_GAIN, STILL_FRAMERATE, COVER_WASH_W, COVER_WASH_H, COVER_BLUR_SIGMA;
|
|
@@ -2471,10 +2544,10 @@ function buildStepTransitions(steps, stepDurations, defaultTransition) {
|
|
|
2471
2544
|
return transitions;
|
|
2472
2545
|
}
|
|
2473
2546
|
function applyTransitions(opts) {
|
|
2474
|
-
return new Promise((
|
|
2547
|
+
return new Promise((resolve11, reject) => {
|
|
2475
2548
|
const { inputVideo, outputVideo, transitions, videoDuration, preset = "fast" } = opts;
|
|
2476
2549
|
if (transitions.length === 0) {
|
|
2477
|
-
runFFmpeg(["-y", "-i", inputVideo, "-c", "copy", outputVideo], "FFmpeg copy failed").then(
|
|
2550
|
+
runFFmpeg(["-y", "-i", inputVideo, "-c", "copy", outputVideo], "FFmpeg copy failed").then(resolve11, reject);
|
|
2478
2551
|
return;
|
|
2479
2552
|
}
|
|
2480
2553
|
const sorted = [...transitions].sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -2522,7 +2595,7 @@ function applyTransitions(opts) {
|
|
|
2522
2595
|
"yuv420p",
|
|
2523
2596
|
outputVideo
|
|
2524
2597
|
];
|
|
2525
|
-
runFFmpeg(args, "FFmpeg transitions failed").then(
|
|
2598
|
+
runFFmpeg(args, "FFmpeg transitions failed").then(resolve11, reject);
|
|
2526
2599
|
});
|
|
2527
2600
|
}
|
|
2528
2601
|
var init_transitions = __esm({
|
|
@@ -2985,6 +3058,9 @@ function renderHormoziAss(ctx) {
|
|
|
2985
3058
|
return `${header2}${lines.join("\n")}
|
|
2986
3059
|
`;
|
|
2987
3060
|
}
|
|
3061
|
+
function looksLikeAddress(token) {
|
|
3062
|
+
return /^(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s]*)?[.,!?]?$/i.test(token);
|
|
3063
|
+
}
|
|
2988
3064
|
function renderShortsAss(ctx) {
|
|
2989
3065
|
const { captions, width, height } = ctx;
|
|
2990
3066
|
const font = resolveCaptionFont(captions.font === "Arial" ? "Arial Black" : captions.font);
|
|
@@ -2995,7 +3071,7 @@ function renderShortsAss(ctx) {
|
|
|
2995
3071
|
const alignment = positionToAlignment(captions.position);
|
|
2996
3072
|
const outline = Math.max(2, Math.round(fontSize / 22));
|
|
2997
3073
|
const upper = true;
|
|
2998
|
-
const caseOf = (t) => (0, import_video_kit4.toCaptionCase)(t, upper);
|
|
3074
|
+
const caseOf = (t) => looksLikeAddress(t) ? t : (0, import_video_kit4.toCaptionCase)(t, upper);
|
|
2999
3075
|
const maxChars = (0, import_video_kit4.fittingMaxChars)(width, fontSize, SIDE_MARGIN, captions.maxChars, upper);
|
|
3000
3076
|
const header2 = `[Script Info]
|
|
3001
3077
|
Title: QuickPeek Demo
|
|
@@ -3226,7 +3302,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
3226
3302
|
return 0;
|
|
3227
3303
|
}
|
|
3228
3304
|
const mediaPath = (0, import_node_path4.isAbsolute)(step.value) ? step.value : (0, import_node_path4.resolve)(baseDir, step.value);
|
|
3229
|
-
if (!(0,
|
|
3305
|
+
if (!(0, import_node_fs6.existsSync)(mediaPath)) {
|
|
3230
3306
|
return 0;
|
|
3231
3307
|
}
|
|
3232
3308
|
try {
|
|
@@ -3248,7 +3324,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
3248
3324
|
async function loadVoiceoverMeta(outputDir) {
|
|
3249
3325
|
const metaPath = (0, import_node_path4.join)(outputDir, "voiceover.json");
|
|
3250
3326
|
try {
|
|
3251
|
-
if (!(0,
|
|
3327
|
+
if (!(0, import_node_fs6.existsSync)(metaPath)) return null;
|
|
3252
3328
|
const content = await (0, import_promises4.readFile)(metaPath, "utf-8");
|
|
3253
3329
|
return JSON.parse(content);
|
|
3254
3330
|
} catch {
|
|
@@ -3271,11 +3347,11 @@ async function concatenateAudio(inputs, output, workDir) {
|
|
|
3271
3347
|
await (0, import_promises4.unlink)(listPath).catch(() => {
|
|
3272
3348
|
});
|
|
3273
3349
|
}
|
|
3274
|
-
var
|
|
3350
|
+
var import_node_fs6, import_promises4, import_node_path4, isWindows, NARRATION_LEAD_IN_MS, LEAD_IN_CLIP;
|
|
3275
3351
|
var init_helpers = __esm({
|
|
3276
3352
|
"src/lib/tts/helpers.ts"() {
|
|
3277
3353
|
"use strict";
|
|
3278
|
-
|
|
3354
|
+
import_node_fs6 = require("fs");
|
|
3279
3355
|
import_promises4 = require("fs/promises");
|
|
3280
3356
|
import_node_path4 = require("path");
|
|
3281
3357
|
init_ffmpeg2();
|
|
@@ -3351,7 +3427,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3351
3427
|
const segments = [];
|
|
3352
3428
|
const alignedStepAudio = /* @__PURE__ */ new Map();
|
|
3353
3429
|
const leadInPath = (0, import_node_path5.join)(audioDir, LEAD_IN_CLIP);
|
|
3354
|
-
const leadIn = (0,
|
|
3430
|
+
const leadIn = (0, import_node_fs7.existsSync)(leadInPath) ? NARRATION_LEAD_IN_MS : 0;
|
|
3355
3431
|
if (leadIn) segments.push(leadInPath);
|
|
3356
3432
|
for (let i = 0; i < actualRecordingDurations.length; i++) {
|
|
3357
3433
|
const stepNum = i + 1;
|
|
@@ -3362,9 +3438,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3362
3438
|
const audioPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}.mp3`);
|
|
3363
3439
|
const originalPaddingPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
3364
3440
|
const silencePath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
3365
|
-
if ((0,
|
|
3441
|
+
if ((0, import_node_fs7.existsSync)(audioPath)) {
|
|
3366
3442
|
segments.push(audioPath);
|
|
3367
|
-
if ((0,
|
|
3443
|
+
if ((0, import_node_fs7.existsSync)(originalPaddingPath)) {
|
|
3368
3444
|
segments.push(originalPaddingPath);
|
|
3369
3445
|
}
|
|
3370
3446
|
if (recordingDuration > voiceDuration) {
|
|
@@ -3372,7 +3448,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3372
3448
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
3373
3449
|
segments.push(alignPaddingPath);
|
|
3374
3450
|
}
|
|
3375
|
-
} else if ((0,
|
|
3451
|
+
} else if ((0, import_node_fs7.existsSync)(silencePath)) {
|
|
3376
3452
|
const own = targetDuration - (stepNum === 1 ? leadIn : 0);
|
|
3377
3453
|
if (targetDuration !== voiceDuration && own > 0) {
|
|
3378
3454
|
await generateSilence(silencePath, own);
|
|
@@ -3401,11 +3477,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3401
3477
|
}
|
|
3402
3478
|
return alignedStepAudio;
|
|
3403
3479
|
}
|
|
3404
|
-
var
|
|
3480
|
+
var import_node_fs7, import_node_path5;
|
|
3405
3481
|
var init_timing = __esm({
|
|
3406
3482
|
"src/lib/tts/timing.ts"() {
|
|
3407
3483
|
"use strict";
|
|
3408
|
-
|
|
3484
|
+
import_node_fs7 = require("fs");
|
|
3409
3485
|
import_node_path5 = require("path");
|
|
3410
3486
|
init_ffmpeg2();
|
|
3411
3487
|
init_helpers();
|
|
@@ -3554,6 +3630,8 @@ asyncio.run(main())
|
|
|
3554
3630
|
await (0, import_promises5.writeFile)(tempScriptPath, pythonScript);
|
|
3555
3631
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
3556
3632
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
3633
|
+
const written = await (0, import_promises5.stat)(outputPath).catch(() => null);
|
|
3634
|
+
if (!written?.size) throw new Error("edge-tts produced no audio");
|
|
3557
3635
|
return await readWordsSidecar(outputPath);
|
|
3558
3636
|
} finally {
|
|
3559
3637
|
await (0, import_promises5.unlink)(tempScriptPath).catch(() => {
|
|
@@ -3581,7 +3659,7 @@ async function synthesizeWithNode(text, outputPath, voice, pythonMsg, rate, maxR
|
|
|
3581
3659
|
const { audioStream, metadataStream } = tts.toStream(text, rate ? { rate } : void 0);
|
|
3582
3660
|
const metadata = [];
|
|
3583
3661
|
metadataStream?.on("data", (chunk) => metadata.push(chunk.toString()));
|
|
3584
|
-
await (0, import_promises6.pipeline)(audioStream, (0,
|
|
3662
|
+
await (0, import_promises6.pipeline)(audioStream, (0, import_node_fs8.createWriteStream)(outputPath));
|
|
3585
3663
|
await new Promise((r) => setTimeout(r, 250));
|
|
3586
3664
|
const { size } = await (0, import_promises5.stat)(outputPath);
|
|
3587
3665
|
if (size === 0) throw new Error("No audio data received");
|
|
@@ -3653,12 +3731,12 @@ function splitJsonObjects(raw) {
|
|
|
3653
3731
|
}
|
|
3654
3732
|
return out;
|
|
3655
3733
|
}
|
|
3656
|
-
var import_node_child_process4,
|
|
3734
|
+
var import_node_child_process4, import_node_fs8, import_promises5, import_node_path7, import_promises6, import_node_util, execAsync, PYTHON_ATTEMPTS, TICKS_PER_MS, wordsSidecar, pythonEdgeTts, lockedEngine;
|
|
3657
3735
|
var init_synthesize = __esm({
|
|
3658
3736
|
"src/lib/tts/synthesize.ts"() {
|
|
3659
3737
|
"use strict";
|
|
3660
3738
|
import_node_child_process4 = require("child_process");
|
|
3661
|
-
|
|
3739
|
+
import_node_fs8 = require("fs");
|
|
3662
3740
|
import_promises5 = require("fs/promises");
|
|
3663
3741
|
import_node_path7 = require("path");
|
|
3664
3742
|
import_promises6 = require("stream/promises");
|
|
@@ -3680,7 +3758,7 @@ function clipKey(spokenText, voice, rate) {
|
|
|
3680
3758
|
async function reuseClip(cacheDir, key, audioPath) {
|
|
3681
3759
|
const mp3 = (0, import_node_path8.join)(cacheDir, `${key}.mp3`);
|
|
3682
3760
|
const meta = (0, import_node_path8.join)(cacheDir, `${key}.json`);
|
|
3683
|
-
if (!(0,
|
|
3761
|
+
if (!(0, import_node_fs9.existsSync)(mp3) || !(0, import_node_fs9.existsSync)(meta)) return null;
|
|
3684
3762
|
try {
|
|
3685
3763
|
const boundaries = JSON.parse(await (0, import_promises7.readFile)(meta, "utf-8"));
|
|
3686
3764
|
await (0, import_promises7.copyFile)(mp3, audioPath);
|
|
@@ -3742,11 +3820,17 @@ function spellAcronyms(text) {
|
|
|
3742
3820
|
);
|
|
3743
3821
|
}
|
|
3744
3822
|
function bindDomains(text) {
|
|
3745
|
-
return text.replace(/\b([a-z0-9][a-z0-9
|
|
3823
|
+
return text.replace(/\b([a-z0-9][a-z0-9._+-]*)@(?=[a-z0-9][a-z0-9-]*\.[a-z]{2,24}\b)/gi, "$1-at-").replace(
|
|
3824
|
+
/\b([a-z0-9][a-z0-9-]*)\.([a-z]{2,24})\b/gi,
|
|
3825
|
+
(_m, host, tld) => `${host}-dot-${SPOKEN_TLD[tld.toLowerCase()] ?? tld}`
|
|
3826
|
+
);
|
|
3746
3827
|
}
|
|
3747
3828
|
function speakSymbols(text) {
|
|
3748
3829
|
return text.replace(/\s*&\s*/g, " and ").replace(/[<>]/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
3749
3830
|
}
|
|
3831
|
+
function spokenForm(caption) {
|
|
3832
|
+
return speakNumbers(spellAcronyms((0, import_video_kit7.toSpokenForm)(bindDomains(speakSymbols(caption)))));
|
|
3833
|
+
}
|
|
3750
3834
|
async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
|
|
3751
3835
|
resetVoiceEngine();
|
|
3752
3836
|
const voice = resolveVoice2(config);
|
|
@@ -3793,7 +3877,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
3793
3877
|
}
|
|
3794
3878
|
const audioPath = (0, import_node_path8.join)(audioDir, `step-${stepNum}.mp3`);
|
|
3795
3879
|
try {
|
|
3796
|
-
const spokenText =
|
|
3880
|
+
const spokenText = spokenForm(step.caption);
|
|
3797
3881
|
const key = clipKey(spokenText, voice, ratePercent ?? "");
|
|
3798
3882
|
const cacheDir = (0, import_node_path8.join)(outputDir, ".tts-clips");
|
|
3799
3883
|
const cached = await reuseClip(cacheDir, key, audioPath);
|
|
@@ -3874,12 +3958,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
3874
3958
|
await saveVoiceoverMeta(outputDir, meta);
|
|
3875
3959
|
return { voiceoverPath, stepAudio };
|
|
3876
3960
|
}
|
|
3877
|
-
var import_node_crypto,
|
|
3961
|
+
var import_node_crypto, import_node_fs9, import_promises7, import_node_path8, import_video_kit7, TTS_POOL, ONES, TENS, SPELLED_OUT, LETTER_SOUNDS, SPOKEN_TLD;
|
|
3878
3962
|
var init_generate = __esm({
|
|
3879
3963
|
"src/lib/tts/generate.ts"() {
|
|
3880
3964
|
"use strict";
|
|
3881
3965
|
import_node_crypto = require("crypto");
|
|
3882
|
-
|
|
3966
|
+
import_node_fs9 = require("fs");
|
|
3883
3967
|
import_promises7 = require("fs/promises");
|
|
3884
3968
|
import_node_path8 = require("path");
|
|
3885
3969
|
import_video_kit7 = require("@spark-apps/video-kit");
|
|
@@ -3944,6 +4028,9 @@ var init_generate = __esm({
|
|
|
3944
4028
|
Y: "why",
|
|
3945
4029
|
Z: "zed"
|
|
3946
4030
|
};
|
|
4031
|
+
SPOKEN_TLD = {
|
|
4032
|
+
ai: "ay-eye"
|
|
4033
|
+
};
|
|
3947
4034
|
}
|
|
3948
4035
|
});
|
|
3949
4036
|
|
|
@@ -4003,6 +4090,7 @@ __export(lib_exports, {
|
|
|
4003
4090
|
NO_CLIP_OUTRO: () => NO_CLIP_OUTRO,
|
|
4004
4091
|
PLAN_MAX_TOKENS: () => PLAN_MAX_TOKENS,
|
|
4005
4092
|
RATE_FACTOR: () => RATE_FACTOR,
|
|
4093
|
+
RELAY_BASE: () => RELAY_BASE,
|
|
4006
4094
|
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
4007
4095
|
SIZE_PRESETS: () => SIZE_PRESETS,
|
|
4008
4096
|
TRIM_EDGE_SILENCE: () => TRIM_EDGE_SILENCE,
|
|
@@ -4054,6 +4142,7 @@ __export(lib_exports, {
|
|
|
4054
4142
|
getErrorMessage: () => getErrorMessage,
|
|
4055
4143
|
getLanguageName: () => getLanguageName,
|
|
4056
4144
|
getMediaDuration: () => getMediaDuration,
|
|
4145
|
+
getOrRefreshRelayToken: () => getOrRefreshRelayToken,
|
|
4057
4146
|
getStatus: () => getStatus,
|
|
4058
4147
|
getStatusCached: () => getStatusCached,
|
|
4059
4148
|
getTierByEmail: () => getTierByEmail,
|
|
@@ -4097,12 +4186,12 @@ __export(lib_exports, {
|
|
|
4097
4186
|
runPooled: () => runPooled,
|
|
4098
4187
|
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
4099
4188
|
shouldSkipLink: () => shouldSkipLink,
|
|
4100
|
-
shouldWatermark: () => shouldWatermark,
|
|
4101
4189
|
showPaywall: () => showPaywall,
|
|
4102
4190
|
speakSymbols: () => speakSymbols,
|
|
4103
4191
|
speedUpMiddle: () => speedUpMiddle,
|
|
4104
4192
|
speedUpVideo: () => speedUpVideo,
|
|
4105
4193
|
spliceVideos: () => spliceVideos,
|
|
4194
|
+
spokenForm: () => spokenForm,
|
|
4106
4195
|
spokenWordTimings: () => spokenWordTimings,
|
|
4107
4196
|
stripLongDashes: () => stripLongDashes,
|
|
4108
4197
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
@@ -4241,54 +4330,16 @@ var init_ambience = __esm({
|
|
|
4241
4330
|
}
|
|
4242
4331
|
});
|
|
4243
4332
|
|
|
4244
|
-
// src/lib/image-kind.ts
|
|
4245
|
-
function mimeForKind(kind) {
|
|
4246
|
-
return MIME[kind];
|
|
4247
|
-
}
|
|
4248
|
-
function sniffImageKind(buf) {
|
|
4249
|
-
if (buf.length < 12) return null;
|
|
4250
|
-
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "png";
|
|
4251
|
-
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
|
|
4252
|
-
if (buf.subarray(0, 3).toString("latin1") === "GIF") return "gif";
|
|
4253
|
-
if (buf.subarray(0, 4).toString("latin1") === "RIFF" && buf.subarray(8, 12).toString("latin1") === "WEBP") {
|
|
4254
|
-
return "webp";
|
|
4255
|
-
}
|
|
4256
|
-
const head = buf.subarray(0, 1024).toString("utf8").replace(/^/, "").trimStart();
|
|
4257
|
-
if (/^<(\?xml|!--|!doctype svg|svg)[\s>]/i.test(head) && /<svg[\s>]/i.test(head)) return "svg";
|
|
4258
|
-
return null;
|
|
4259
|
-
}
|
|
4260
|
-
var MIME, MIN_ICON_PX;
|
|
4261
|
-
var init_image_kind = __esm({
|
|
4262
|
-
"src/lib/image-kind.ts"() {
|
|
4263
|
-
"use strict";
|
|
4264
|
-
MIME = {
|
|
4265
|
-
png: "image/png",
|
|
4266
|
-
jpeg: "image/jpeg",
|
|
4267
|
-
gif: "image/gif",
|
|
4268
|
-
webp: "image/webp",
|
|
4269
|
-
svg: "image/svg+xml"
|
|
4270
|
-
};
|
|
4271
|
-
MIN_ICON_PX = 48;
|
|
4272
|
-
}
|
|
4273
|
-
});
|
|
4274
|
-
|
|
4275
4333
|
// src/lib/outro-card.ts
|
|
4276
|
-
function dataUri(path) {
|
|
4277
|
-
try {
|
|
4278
|
-
const buf = (0, import_node_fs17.readFileSync)(path);
|
|
4279
|
-
const kind = sniffImageKind(buf);
|
|
4280
|
-
if (!kind) return null;
|
|
4281
|
-
return `data:${mimeForKind(kind)};base64,${buf.toString("base64")}`;
|
|
4282
|
-
} catch {
|
|
4283
|
-
return null;
|
|
4284
|
-
}
|
|
4285
|
-
}
|
|
4286
4334
|
function escapeHtml(text) {
|
|
4287
4335
|
return text.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4288
4336
|
}
|
|
4289
4337
|
function buildOutroCardHtml(options) {
|
|
4290
4338
|
const { subject, credits = [], promo, width, height } = options;
|
|
4291
4339
|
const unit = Math.min(width, height);
|
|
4340
|
+
const markFraction = width > height ? 0.28 : 0.46;
|
|
4341
|
+
const markSize = unit * markFraction;
|
|
4342
|
+
const padTop = width > height ? unit * 0.08 : unit * 0.23;
|
|
4292
4343
|
const creditRows = credits.map((credit) => {
|
|
4293
4344
|
const icon = credit.iconPath ? dataUri(credit.iconPath) : null;
|
|
4294
4345
|
return `<div class="credit">
|
|
@@ -4297,7 +4348,8 @@ function buildOutroCardHtml(options) {
|
|
|
4297
4348
|
${icon ? `<img class="cicon" src="${icon}" alt="" />` : ""}
|
|
4298
4349
|
</div>`;
|
|
4299
4350
|
}).join("");
|
|
4300
|
-
const
|
|
4351
|
+
const animated = subject.animatedMark;
|
|
4352
|
+
const subjectIcon = animated ? null : subject.iconPath ? dataUri(subject.iconPath) : null;
|
|
4301
4353
|
return `<!doctype html>
|
|
4302
4354
|
<html><head><meta charset="utf-8" /><style>
|
|
4303
4355
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -4315,7 +4367,7 @@ function buildOutroCardHtml(options) {
|
|
|
4315
4367
|
position: relative;
|
|
4316
4368
|
display: flex; flex-direction: column;
|
|
4317
4369
|
align-items: center; justify-content: flex-start;
|
|
4318
|
-
padding-top: ${
|
|
4370
|
+
padding-top: ${padTop}px;
|
|
4319
4371
|
gap: ${unit * 0.028}px;
|
|
4320
4372
|
}
|
|
4321
4373
|
/* Toolchain credits: top right, deliberately small. */
|
|
@@ -4333,7 +4385,7 @@ function buildOutroCardHtml(options) {
|
|
|
4333
4385
|
/* The app the video is about. */
|
|
4334
4386
|
/* The app's mark leads the card, and it is meant to be seen: at 46% of
|
|
4335
4387
|
the short edge it reads at a glance on a phone. */
|
|
4336
|
-
.mark { width: ${
|
|
4388
|
+
.mark { width: ${markSize}px; height: ${markSize}px; object-fit: contain; border-radius: ${unit * 0.09}px; }
|
|
4337
4389
|
/* A dark mark on a dark card is invisible, which looks exactly like a
|
|
4338
4390
|
failed image. Sites that publish a black-on-transparent logo are common
|
|
4339
4391
|
enough to be worth catching, and a light plate is what their own site
|
|
@@ -4355,8 +4407,12 @@ function buildOutroCardHtml(options) {
|
|
|
4355
4407
|
}
|
|
4356
4408
|
|
|
4357
4409
|
/* Today's offer, along the very bottom edge. */
|
|
4410
|
+
/* In the flow, not pinned to the bottom edge. Pinned, it was laid out
|
|
4411
|
+
without reference to the text above it, so on a short canvas it simply
|
|
4412
|
+
covered whatever had grown into its space. margin-top pushes it clear of
|
|
4413
|
+
the domain instead. */
|
|
4358
4414
|
.promo {
|
|
4359
|
-
|
|
4415
|
+
margin-top: ${unit * 0.05}px;
|
|
4360
4416
|
display: flex; align-items: baseline; gap: ${unit * 0.022}px;
|
|
4361
4417
|
padding: ${unit * 0.026}px ${unit * 0.055}px;
|
|
4362
4418
|
border: 2px solid rgba(255, 209, 64, .85); border-radius: ${unit * 0.024}px;
|
|
@@ -4364,9 +4420,60 @@ function buildOutroCardHtml(options) {
|
|
|
4364
4420
|
}
|
|
4365
4421
|
.ppct { font-size: ${unit * 0.056}px; font-weight: 700; color: #ffd140; }
|
|
4366
4422
|
.pcode { font-size: ${unit * 0.048}px; font-weight: 700; letter-spacing: .05em; }
|
|
4423
|
+
|
|
4424
|
+
/* The animated mark, sized to the same 46% slot the still one occupies so
|
|
4425
|
+
the rest of the card's rhythm is unchanged. Ported from VidLet's
|
|
4426
|
+
src/gui/css/loader.css: the amber tip flies off and snaps back into the
|
|
4427
|
+
cut - a jump cut, on a loop. Pure CSS, because the renderer steps these
|
|
4428
|
+
animations frame by frame and a JS-driven one could not be stepped.
|
|
4429
|
+
|
|
4430
|
+
No prefers-reduced-motion fallback here, unlike the original: this runs
|
|
4431
|
+
in a headless Chromium rendering a video, where there is no viewer whose
|
|
4432
|
+
preference could be read, and honouring the default would silently flatten
|
|
4433
|
+
the animation to a fade in CI. */
|
|
4434
|
+
.amark { width: ${markSize}px; height: ${markSize}px; display: block; }
|
|
4435
|
+
.amark path {
|
|
4436
|
+
/* Without fill-box the origin is the whole SVG viewport, which swings the
|
|
4437
|
+
tip off its own centre on the scale steps. */
|
|
4438
|
+
transform-box: fill-box;
|
|
4439
|
+
transform-origin: center;
|
|
4440
|
+
}
|
|
4441
|
+
.amark .vl-body { fill: #fff6e5; stroke: #fff6e5; animation: vl-nudge 1.4s cubic-bezier(.4,0,.2,1) infinite; }
|
|
4442
|
+
.amark .vl-tip { fill: #ffc53d; stroke: #ffc53d; animation: vl-cut 1.4s cubic-bezier(.4,0,.2,1) infinite; }
|
|
4443
|
+
.amark .vl-ghost { opacity: .35; }
|
|
4444
|
+
.amark .vl-ghost .vl-tip { animation-delay: -.1s; }
|
|
4445
|
+
|
|
4446
|
+
/* Translations are in viewBox units, so the motion scales with the mark. */
|
|
4447
|
+
@keyframes vl-cut {
|
|
4448
|
+
0%, 30% { transform: translateX(0) scale(1); opacity: 1; }
|
|
4449
|
+
48% { transform: translateX(62px) scale(.94); opacity: 1; }
|
|
4450
|
+
60% { transform: translateX(88px) scale(.86); opacity: 0; }
|
|
4451
|
+
61%, 74% { transform: translateX(-58px) scale(.86); opacity: 0; }
|
|
4452
|
+
86% { transform: translateX(6px) scale(1.06); opacity: 1; }
|
|
4453
|
+
100% { transform: translateX(0) scale(1); opacity: 1; }
|
|
4454
|
+
}
|
|
4455
|
+
/* The body takes the recoil, so the cut lands rather than just sliding. */
|
|
4456
|
+
@keyframes vl-nudge {
|
|
4457
|
+
0%, 30% { transform: scaleX(1); }
|
|
4458
|
+
55% { transform: scaleX(1.04); }
|
|
4459
|
+
86% { transform: scaleX(.97); }
|
|
4460
|
+
100% { transform: scaleX(1); }
|
|
4461
|
+
}
|
|
4462
|
+
${/* The card normally reads its palette off the mark's pixels, and an SVG
|
|
4463
|
+
drawn by this stylesheet has none to read - the measuring script finds
|
|
4464
|
+
no <img class="mark"> and leaves the defaults, which would put VidLet's
|
|
4465
|
+
amber on a teal card. Stated instead of measured, last so it wins. */
|
|
4466
|
+
animated === "vidlet" ? ":root { --bg-in: #1f1a10; --bg-out: #07060a; --accent: #ffc53d; }" : ""}
|
|
4367
4467
|
</style></head>
|
|
4368
4468
|
<body>
|
|
4369
4469
|
${credits.length ? `<div class="credits">${creditRows}</div>` : ""}
|
|
4470
|
+
${animated === "vidlet" ? `<svg class="amark" viewBox="0 0 512 512" aria-hidden="true">
|
|
4471
|
+
<g stroke-linejoin="round" stroke-width="20">
|
|
4472
|
+
<g class="vl-ghost"><path class="vl-tip" d="M318 246 L410 302 L318 358 Z"/></g>
|
|
4473
|
+
<path class="vl-body" d="M176 124 L300 200 L300 312 L176 388 Z"/>
|
|
4474
|
+
<path class="vl-tip" d="M318 246 L410 302 L318 358 Z"/>
|
|
4475
|
+
</g>
|
|
4476
|
+
</svg>` : ""}
|
|
4370
4477
|
${subjectIcon ? `<img class="mark" src="${subjectIcon}" alt="" />` : ""}
|
|
4371
4478
|
<div class="name">${escapeHtml(subject.name)}</div>
|
|
4372
4479
|
${subject.tagline ? `<div class="tagline">${escapeHtml(subject.tagline)}</div>` : ""}
|
|
@@ -4469,12 +4576,18 @@ function buildOutroCardHtml(options) {
|
|
|
4469
4576
|
</script>
|
|
4470
4577
|
</body></html>`;
|
|
4471
4578
|
}
|
|
4472
|
-
var
|
|
4579
|
+
var ANIMATED_MARK_LOOP_SECONDS, dataUri;
|
|
4473
4580
|
var init_outro_card = __esm({
|
|
4474
4581
|
"src/lib/outro-card.ts"() {
|
|
4475
4582
|
"use strict";
|
|
4476
|
-
import_node_fs17 = require("fs");
|
|
4477
4583
|
init_image_kind();
|
|
4584
|
+
ANIMATED_MARK_LOOP_SECONDS = {
|
|
4585
|
+
// Matches `vl-cut` / `vl-nudge` in VidLet's src/gui/css/loader.css. Changing
|
|
4586
|
+
// it there without changing it here makes the encoded loop stutter at the
|
|
4587
|
+
// seam, because the clip is cut to exactly this length and then repeated.
|
|
4588
|
+
vidlet: 1.4
|
|
4589
|
+
};
|
|
4590
|
+
dataUri = imageDataUri;
|
|
4478
4591
|
}
|
|
4479
4592
|
});
|
|
4480
4593
|
|
|
@@ -4505,6 +4618,9 @@ async function renderOutroClip(options) {
|
|
|
4505
4618
|
const htmlPath = (0, import_node_path22.join)(workDir, "outro-card.html");
|
|
4506
4619
|
const pngPath = (0, import_node_path22.join)(workDir, "outro-card.png");
|
|
4507
4620
|
const clipPath = (0, import_node_path22.join)(workDir, "outro-card.mp4");
|
|
4621
|
+
if (options.subject.animatedMark) {
|
|
4622
|
+
return renderAnimatedOutroClip(options, { htmlPath, clipPath, seconds, fps });
|
|
4623
|
+
}
|
|
4508
4624
|
await (0, import_promises16.writeFile)(htmlPath, buildOutroCardHtml(options));
|
|
4509
4625
|
const browser = await import_playwright7.chromium.launch({ headless: true });
|
|
4510
4626
|
try {
|
|
@@ -4547,6 +4663,70 @@ async function renderOutroClip(options) {
|
|
|
4547
4663
|
);
|
|
4548
4664
|
return clipPath;
|
|
4549
4665
|
}
|
|
4666
|
+
async function renderAnimatedOutroClip(options, paths) {
|
|
4667
|
+
const { htmlPath, clipPath, seconds, fps } = paths;
|
|
4668
|
+
const { workDir, width, height } = options;
|
|
4669
|
+
const loopSeconds = ANIMATED_MARK_LOOP_SECONDS[options.subject.animatedMark];
|
|
4670
|
+
const frameCount = Math.max(1, Math.round(loopSeconds * fps));
|
|
4671
|
+
const framesDir = (0, import_node_path22.join)(workDir, "outro-frames");
|
|
4672
|
+
await (0, import_promises16.rm)(framesDir, { recursive: true, force: true });
|
|
4673
|
+
await (0, import_promises16.mkdir)(framesDir, { recursive: true });
|
|
4674
|
+
await (0, import_promises16.writeFile)(htmlPath, buildOutroCardHtml(options));
|
|
4675
|
+
const browser = await import_playwright7.chromium.launch({ headless: true });
|
|
4676
|
+
try {
|
|
4677
|
+
const page = await browser.newPage({ viewport: { width, height } });
|
|
4678
|
+
await page.goto((0, import_node_url2.pathToFileURL)((0, import_node_path22.resolve)(htmlPath)).href, { waitUntil: "load" });
|
|
4679
|
+
await page.waitForSelector('body[data-cards-ready="1"]', { timeout: 3e3 }).catch(() => {
|
|
4680
|
+
});
|
|
4681
|
+
await page.waitForTimeout(300);
|
|
4682
|
+
await page.evaluate(() => {
|
|
4683
|
+
for (const animation of document.getAnimations()) animation.pause();
|
|
4684
|
+
});
|
|
4685
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
4686
|
+
const elapsedMs = frame / frameCount * loopSeconds * 1e3;
|
|
4687
|
+
await page.evaluate((ms) => {
|
|
4688
|
+
for (const animation of document.getAnimations()) animation.currentTime = ms;
|
|
4689
|
+
}, elapsedMs);
|
|
4690
|
+
await page.screenshot({ path: (0, import_node_path22.join)(framesDir, `f-${String(frame).padStart(4, "0")}.png`) });
|
|
4691
|
+
}
|
|
4692
|
+
} finally {
|
|
4693
|
+
await browser.close();
|
|
4694
|
+
}
|
|
4695
|
+
await runFFmpeg(
|
|
4696
|
+
[
|
|
4697
|
+
"-y",
|
|
4698
|
+
// -stream_loop applies to the input that FOLLOWS it, so it has to sit
|
|
4699
|
+
// before -framerate/-i, not after.
|
|
4700
|
+
"-stream_loop",
|
|
4701
|
+
"-1",
|
|
4702
|
+
"-framerate",
|
|
4703
|
+
String(fps),
|
|
4704
|
+
"-i",
|
|
4705
|
+
(0, import_node_path22.join)(framesDir, "f-%04d.png"),
|
|
4706
|
+
"-f",
|
|
4707
|
+
"lavfi",
|
|
4708
|
+
"-i",
|
|
4709
|
+
"anullsrc=channel_layout=stereo:sample_rate=44100",
|
|
4710
|
+
"-t",
|
|
4711
|
+
String(seconds),
|
|
4712
|
+
"-r",
|
|
4713
|
+
String(fps),
|
|
4714
|
+
"-pix_fmt",
|
|
4715
|
+
"yuv420p",
|
|
4716
|
+
"-c:v",
|
|
4717
|
+
"libx264",
|
|
4718
|
+
"-preset",
|
|
4719
|
+
"veryfast",
|
|
4720
|
+
"-c:a",
|
|
4721
|
+
"aac",
|
|
4722
|
+
"-shortest",
|
|
4723
|
+
clipPath
|
|
4724
|
+
],
|
|
4725
|
+
"ffmpeg animated outro card render failed"
|
|
4726
|
+
);
|
|
4727
|
+
await (0, import_promises16.rm)(framesDir, { recursive: true, force: true });
|
|
4728
|
+
return clipPath;
|
|
4729
|
+
}
|
|
4550
4730
|
var import_promises16, import_node_path22, import_node_url2, import_playwright7;
|
|
4551
4731
|
var init_render_outro = __esm({
|
|
4552
4732
|
"src/browser/render-outro.ts"() {
|
|
@@ -4860,9 +5040,100 @@ var import_promises2 = require("fs/promises");
|
|
|
4860
5040
|
var import_playwright2 = require("playwright");
|
|
4861
5041
|
init_lifecycle();
|
|
4862
5042
|
|
|
5043
|
+
// src/browser/brand-watermark.ts
|
|
5044
|
+
init_image_kind();
|
|
5045
|
+
function watermarkInitScript(payload) {
|
|
5046
|
+
return `(() => {
|
|
5047
|
+
var ID = '__quickpeek_brand_watermark';
|
|
5048
|
+
var P = ${embedJson(payload)};
|
|
5049
|
+
var vertical = P.position.indexOf('top') === 0 ? 'top:22px' : 'bottom:22px';
|
|
5050
|
+
var horizontal = P.position.indexOf('right') > -1 ? 'right:24px' : 'left:24px';
|
|
5051
|
+
var SHELL = [
|
|
5052
|
+
'position:fixed',
|
|
5053
|
+
vertical,
|
|
5054
|
+
horizontal,
|
|
5055
|
+
'z-index:2147483646',
|
|
5056
|
+
'display:flex',
|
|
5057
|
+
'align-items:center',
|
|
5058
|
+
'gap:10px',
|
|
5059
|
+
'padding:8px 16px 8px 10px',
|
|
5060
|
+
'border-radius:999px',
|
|
5061
|
+
// A hairline and a blur rather than a solid plate: the mark sits on top of
|
|
5062
|
+
// whatever the app is showing, and a hard box reads as part of the UI.
|
|
5063
|
+
// The alpha is high enough to stay dark over a LIGHT app: at 0.55 the blur
|
|
5064
|
+
// averaged a pale page up into grey and the white label sat on it at about
|
|
5065
|
+
// 2:1, which photographs as a smudge.
|
|
5066
|
+
'background:rgba(12,12,18,0.78)',
|
|
5067
|
+
'border:1px solid rgba(255,255,255,0.14)',
|
|
5068
|
+
'backdrop-filter:blur(14px) saturate(1.3)',
|
|
5069
|
+
'-webkit-backdrop-filter:blur(14px) saturate(1.3)',
|
|
5070
|
+
'box-shadow:0 6px 24px rgba(0,0,0,0.30)',
|
|
5071
|
+
'opacity:' + P.opacity,
|
|
5072
|
+
'pointer-events:none',
|
|
5073
|
+
'user-select:none'
|
|
5074
|
+
].join(';');
|
|
5075
|
+
var ICON = 'width:26px;height:26px;border-radius:7px;object-fit:contain;display:block';
|
|
5076
|
+
var NAME = [
|
|
5077
|
+
'font:600 15px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
|
|
5078
|
+
'letter-spacing:0.015em',
|
|
5079
|
+
'color:#ffffff',
|
|
5080
|
+
'white-space:nowrap'
|
|
5081
|
+
].join(';');
|
|
5082
|
+
|
|
5083
|
+
function mount() {
|
|
5084
|
+
try {
|
|
5085
|
+
if (!document.body || document.getElementById(ID)) return;
|
|
5086
|
+
var el = document.createElement('div');
|
|
5087
|
+
el.id = ID;
|
|
5088
|
+
el.setAttribute('style', SHELL);
|
|
5089
|
+
if (P.icon) {
|
|
5090
|
+
var img = document.createElement('img');
|
|
5091
|
+
img.setAttribute('style', ICON);
|
|
5092
|
+
img.src = P.icon;
|
|
5093
|
+
img.alt = '';
|
|
5094
|
+
el.appendChild(img);
|
|
5095
|
+
}
|
|
5096
|
+
var label = document.createElement('span');
|
|
5097
|
+
label.setAttribute('style', NAME);
|
|
5098
|
+
label.textContent = P.name;
|
|
5099
|
+
el.appendChild(label);
|
|
5100
|
+
document.body.appendChild(el);
|
|
5101
|
+
} catch (e) {
|
|
5102
|
+
// A page that refuses the node still records; the mark is cosmetic.
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
if (document.body) mount();
|
|
5107
|
+
else document.addEventListener('DOMContentLoaded', mount, { once: true });
|
|
5108
|
+
setInterval(mount, 400);
|
|
5109
|
+
})();`;
|
|
5110
|
+
}
|
|
5111
|
+
async function applyBrandWatermark(context, watermark) {
|
|
5112
|
+
if (!watermark?.name) return false;
|
|
5113
|
+
const payload = {
|
|
5114
|
+
name: watermark.name,
|
|
5115
|
+
icon: watermark.iconPath ? imageDataUri(watermark.iconPath) : null,
|
|
5116
|
+
position: watermark.position ?? "top-right",
|
|
5117
|
+
opacity: clampOpacity(watermark.opacity)
|
|
5118
|
+
};
|
|
5119
|
+
try {
|
|
5120
|
+
await context.addInitScript(watermarkInitScript(payload));
|
|
5121
|
+
return true;
|
|
5122
|
+
} catch {
|
|
5123
|
+
return false;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function embedJson(value) {
|
|
5127
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
5128
|
+
}
|
|
5129
|
+
function clampOpacity(value) {
|
|
5130
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0.92;
|
|
5131
|
+
return Math.min(1, Math.max(0.35, value));
|
|
5132
|
+
}
|
|
5133
|
+
|
|
4863
5134
|
// src/browser/wsl.ts
|
|
4864
5135
|
var import_node_child_process = require("child_process");
|
|
4865
|
-
var
|
|
5136
|
+
var import_node_fs3 = require("fs");
|
|
4866
5137
|
var import_node_http = require("http");
|
|
4867
5138
|
var import_playwright = require("playwright");
|
|
4868
5139
|
function isWSL() {
|
|
@@ -4875,7 +5146,7 @@ function isWSL() {
|
|
|
4875
5146
|
}
|
|
4876
5147
|
}
|
|
4877
5148
|
function tryEnableWSLInterop() {
|
|
4878
|
-
return (0,
|
|
5149
|
+
return (0, import_node_fs3.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
4879
5150
|
}
|
|
4880
5151
|
function wslToWindowsPath(wslPath) {
|
|
4881
5152
|
if (!wslPath) return wslPath;
|
|
@@ -4884,7 +5155,7 @@ function wslToWindowsPath(wslPath) {
|
|
|
4884
5155
|
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
4885
5156
|
}
|
|
4886
5157
|
function findExe(whereExeQuery, ...hardcoded) {
|
|
4887
|
-
for (const p of hardcoded) if ((0,
|
|
5158
|
+
for (const p of hardcoded) if ((0, import_node_fs3.existsSync)(p)) return p;
|
|
4888
5159
|
try {
|
|
4889
5160
|
const line = (0, import_node_child_process.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
4890
5161
|
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
@@ -4907,7 +5178,7 @@ function getWindowsHostIP() {
|
|
|
4907
5178
|
} catch {
|
|
4908
5179
|
}
|
|
4909
5180
|
try {
|
|
4910
|
-
const m = (0,
|
|
5181
|
+
const m = (0, import_node_fs3.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
4911
5182
|
if (m?.[1]) return m[1];
|
|
4912
5183
|
} catch {
|
|
4913
5184
|
}
|
|
@@ -4915,7 +5186,7 @@ function getWindowsHostIP() {
|
|
|
4915
5186
|
}
|
|
4916
5187
|
function startRelay(nodePath, windowsHost, targetPort) {
|
|
4917
5188
|
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
4918
|
-
return new Promise((
|
|
5189
|
+
return new Promise((resolve11, reject) => {
|
|
4919
5190
|
const proc = (0, import_node_child_process.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
4920
5191
|
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
4921
5192
|
let buf = "";
|
|
@@ -4924,7 +5195,7 @@ function startRelay(nodePath, windowsHost, targetPort) {
|
|
|
4924
5195
|
const m = buf.match(/PORT:(\d+)/);
|
|
4925
5196
|
if (m) {
|
|
4926
5197
|
clearTimeout(timer);
|
|
4927
|
-
|
|
5198
|
+
resolve11({ proc, port: +m[1] });
|
|
4928
5199
|
}
|
|
4929
5200
|
});
|
|
4930
5201
|
proc.on("error", (e) => {
|
|
@@ -4934,7 +5205,7 @@ function startRelay(nodePath, windowsHost, targetPort) {
|
|
|
4934
5205
|
});
|
|
4935
5206
|
}
|
|
4936
5207
|
function waitForChrome(host, port) {
|
|
4937
|
-
return new Promise((
|
|
5208
|
+
return new Promise((resolve11, reject) => {
|
|
4938
5209
|
const deadline = Date.now() + 2e4;
|
|
4939
5210
|
const poll = () => {
|
|
4940
5211
|
if (Date.now() > deadline) {
|
|
@@ -4943,7 +5214,7 @@ function waitForChrome(host, port) {
|
|
|
4943
5214
|
}
|
|
4944
5215
|
const req = (0, import_node_http.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
4945
5216
|
res.resume();
|
|
4946
|
-
|
|
5217
|
+
resolve11();
|
|
4947
5218
|
});
|
|
4948
5219
|
req.on("error", () => setTimeout(poll, 600));
|
|
4949
5220
|
req.setTimeout(1e3, () => {
|
|
@@ -5086,6 +5357,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
5086
5357
|
});
|
|
5087
5358
|
}
|
|
5088
5359
|
await hideDevOverlays(context);
|
|
5360
|
+
await applyBrandWatermark(context, config.video.watermark);
|
|
5089
5361
|
const session = { context, browser, chromeSession };
|
|
5090
5362
|
session.release = onCleanup(() => closeBrowserSession(session));
|
|
5091
5363
|
return session;
|
|
@@ -5178,7 +5450,7 @@ async function cancelSignIn() {
|
|
|
5178
5450
|
}
|
|
5179
5451
|
|
|
5180
5452
|
// src/browser/chrome-import.ts
|
|
5181
|
-
var
|
|
5453
|
+
var import_node_fs4 = require("fs");
|
|
5182
5454
|
var import_node_os2 = require("os");
|
|
5183
5455
|
var import_node_path2 = require("path");
|
|
5184
5456
|
var import_playwright3 = require("playwright");
|
|
@@ -5215,7 +5487,7 @@ var COOKIE_SIBLINGS = ["", "-wal", "-shm", "-journal"];
|
|
|
5215
5487
|
function findCookieDb(userDataDir, profile) {
|
|
5216
5488
|
for (const rel of [(0, import_node_path2.join)(profile, "Network", "Cookies"), (0, import_node_path2.join)(profile, "Cookies")]) {
|
|
5217
5489
|
const abs = (0, import_node_path2.join)(userDataDir, rel);
|
|
5218
|
-
if ((0,
|
|
5490
|
+
if ((0, import_node_fs4.existsSync)(abs)) return { abs, rel };
|
|
5219
5491
|
}
|
|
5220
5492
|
return null;
|
|
5221
5493
|
}
|
|
@@ -5223,7 +5495,7 @@ function detectImportSources() {
|
|
|
5223
5495
|
const found = [];
|
|
5224
5496
|
for (const browser of BROWSERS) {
|
|
5225
5497
|
for (const userDataDir of userDataRoots(browser)) {
|
|
5226
|
-
if (!(0,
|
|
5498
|
+
if (!(0, import_node_fs4.existsSync)(userDataDir)) continue;
|
|
5227
5499
|
const db = findCookieDb(userDataDir, "Default");
|
|
5228
5500
|
if (db) found.push({ browser, userDataDir, profile: "Default", cookiesPath: db.abs, cookiesRel: db.rel });
|
|
5229
5501
|
}
|
|
@@ -5252,16 +5524,16 @@ function passwordStores() {
|
|
|
5252
5524
|
async function importFromSource(url, source) {
|
|
5253
5525
|
const host = hostOf2(url);
|
|
5254
5526
|
if (!host) throw new Error(`Not a valid URL: ${url}`);
|
|
5255
|
-
const scratch = (0,
|
|
5527
|
+
const scratch = (0, import_node_fs4.mkdtempSync)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "qp-import-"));
|
|
5256
5528
|
try {
|
|
5257
5529
|
const localState = (0, import_node_path2.join)(source.userDataDir, "Local State");
|
|
5258
|
-
if ((0,
|
|
5530
|
+
if ((0, import_node_fs4.existsSync)(localState)) (0, import_node_fs4.copyFileSync)(localState, (0, import_node_path2.join)(scratch, "Local State"));
|
|
5259
5531
|
for (const suffix of COOKIE_SIBLINGS) {
|
|
5260
5532
|
const from = source.cookiesPath + suffix;
|
|
5261
|
-
if (!(0,
|
|
5533
|
+
if (!(0, import_node_fs4.existsSync)(from)) continue;
|
|
5262
5534
|
const to = (0, import_node_path2.join)(scratch, source.cookiesRel + suffix);
|
|
5263
|
-
(0,
|
|
5264
|
-
(0,
|
|
5535
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path2.dirname)(to), { recursive: true });
|
|
5536
|
+
(0, import_node_fs4.copyFileSync)(from, to);
|
|
5265
5537
|
}
|
|
5266
5538
|
for (const store2 of passwordStores()) {
|
|
5267
5539
|
const state = await readState(scratch, source.browser.channel, store2);
|
|
@@ -5277,7 +5549,7 @@ async function importFromSource(url, source) {
|
|
|
5277
5549
|
}
|
|
5278
5550
|
return null;
|
|
5279
5551
|
} finally {
|
|
5280
|
-
(0,
|
|
5552
|
+
(0, import_node_fs4.rmSync)(scratch, { recursive: true, force: true });
|
|
5281
5553
|
}
|
|
5282
5554
|
}
|
|
5283
5555
|
async function readState(profileDir, channel, store2) {
|
|
@@ -5334,7 +5606,7 @@ function renderLogo(config = {}) {
|
|
|
5334
5606
|
}
|
|
5335
5607
|
|
|
5336
5608
|
// src/utils/config-loader.ts
|
|
5337
|
-
var
|
|
5609
|
+
var import_node_fs10 = require("fs");
|
|
5338
5610
|
var import_promises8 = require("fs/promises");
|
|
5339
5611
|
var import_node_os5 = require("os");
|
|
5340
5612
|
var import_node_path9 = require("path");
|
|
@@ -5351,7 +5623,7 @@ function runRoot() {
|
|
|
5351
5623
|
async function resolveRunFile(name) {
|
|
5352
5624
|
const target = (0, import_node_path9.join)(runRoot(), name);
|
|
5353
5625
|
const source = (0, import_node_path9.join)(process.cwd(), name);
|
|
5354
|
-
if (target !== source && !(0,
|
|
5626
|
+
if (target !== source && !(0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(source)) {
|
|
5355
5627
|
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
|
|
5356
5628
|
await (0, import_promises8.copyFile)(source, target);
|
|
5357
5629
|
}
|
|
@@ -5359,7 +5631,7 @@ async function resolveRunFile(name) {
|
|
|
5359
5631
|
}
|
|
5360
5632
|
async function loadConfig(_url, langOverride) {
|
|
5361
5633
|
const configPath = await resolveRunFile(CONFIG_FILE);
|
|
5362
|
-
if ((0,
|
|
5634
|
+
if ((0, import_node_fs10.existsSync)(configPath)) {
|
|
5363
5635
|
try {
|
|
5364
5636
|
const content = await (0, import_promises8.readFile)(configPath, "utf-8");
|
|
5365
5637
|
const userConfig = JSON.parse(content);
|
|
@@ -5420,7 +5692,7 @@ async function prepareWorkDir(scope = "run") {
|
|
|
5420
5692
|
const safe = scope.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "run";
|
|
5421
5693
|
const root = (0, import_node_path9.join)(runRoot(), WORK_DIR);
|
|
5422
5694
|
const workDir = (0, import_node_path9.join)(root, `${safe}-${process.pid}`);
|
|
5423
|
-
if ((0,
|
|
5695
|
+
if ((0, import_node_fs10.existsSync)(workDir)) {
|
|
5424
5696
|
await (0, import_promises8.rm)(workDir, { recursive: true });
|
|
5425
5697
|
}
|
|
5426
5698
|
await (0, import_promises8.mkdir)(workDir, { recursive: true });
|
|
@@ -5447,7 +5719,7 @@ var USER_DATA_DIR = (0, import_node_path9.join)((0, import_node_os5.homedir)(),
|
|
|
5447
5719
|
var USER_DATA_FILE = (0, import_node_path9.join)(USER_DATA_DIR, "user.json");
|
|
5448
5720
|
async function loadUserData() {
|
|
5449
5721
|
try {
|
|
5450
|
-
if ((0,
|
|
5722
|
+
if ((0, import_node_fs10.existsSync)(USER_DATA_FILE)) {
|
|
5451
5723
|
return JSON.parse(await (0, import_promises8.readFile)(USER_DATA_FILE, "utf-8"));
|
|
5452
5724
|
}
|
|
5453
5725
|
} catch {
|
|
@@ -5455,7 +5727,7 @@ async function loadUserData() {
|
|
|
5455
5727
|
return {};
|
|
5456
5728
|
}
|
|
5457
5729
|
async function saveUserData(userData) {
|
|
5458
|
-
if (!(0,
|
|
5730
|
+
if (!(0, import_node_fs10.existsSync)(USER_DATA_DIR)) {
|
|
5459
5731
|
await (0, import_promises8.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
5460
5732
|
}
|
|
5461
5733
|
const existing = await loadUserData();
|
|
@@ -5524,7 +5796,7 @@ function parseCaptionCSS(css) {
|
|
|
5524
5796
|
}
|
|
5525
5797
|
async function loadCaptions() {
|
|
5526
5798
|
const captionsPath = await resolveRunFile("captions.css");
|
|
5527
|
-
if (!(0,
|
|
5799
|
+
if (!(0, import_node_fs10.existsSync)(captionsPath)) {
|
|
5528
5800
|
await (0, import_promises8.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
5529
5801
|
return DEFAULT_CAPTIONS;
|
|
5530
5802
|
}
|
|
@@ -5538,7 +5810,10 @@ async function loadCaptions() {
|
|
|
5538
5810
|
async function savePlan(plan, planPath) {
|
|
5539
5811
|
const cleanPlan = {
|
|
5540
5812
|
...plan,
|
|
5541
|
-
steps: plan.steps.map(({ duration, ...step }) =>
|
|
5813
|
+
steps: plan.steps.map(({ duration, ...step }) => ({
|
|
5814
|
+
...step,
|
|
5815
|
+
...typeof duration === "number" && duration > 0 ? { duration: Math.min(duration, 30) } : {}
|
|
5816
|
+
}))
|
|
5542
5817
|
};
|
|
5543
5818
|
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(planPath), { recursive: true });
|
|
5544
5819
|
await (0, import_promises8.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
@@ -5647,6 +5922,11 @@ async function showHelp() {
|
|
|
5647
5922
|
`-s, --script <file>`,
|
|
5648
5923
|
"Narration script (one caption per line)"
|
|
5649
5924
|
],
|
|
5925
|
+
[
|
|
5926
|
+
`${opt("--draft")}, ${opt("-dr")}`,
|
|
5927
|
+
`--draft, -dr`,
|
|
5928
|
+
"Rough cut first: small, silent, quick. Re-run without it for the real one"
|
|
5929
|
+
],
|
|
5650
5930
|
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
5651
5931
|
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
5652
5932
|
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"],
|
|
@@ -5744,7 +6024,7 @@ function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
|
|
|
5744
6024
|
)
|
|
5745
6025
|
);
|
|
5746
6026
|
}
|
|
5747
|
-
return new Promise((
|
|
6027
|
+
return new Promise((resolve11, reject) => {
|
|
5748
6028
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
5749
6029
|
let done = false;
|
|
5750
6030
|
rl.on("close", () => {
|
|
@@ -5755,7 +6035,7 @@ function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
|
|
|
5755
6035
|
rl.question(prompt2, () => {
|
|
5756
6036
|
done = true;
|
|
5757
6037
|
rl.close();
|
|
5758
|
-
|
|
6038
|
+
resolve11();
|
|
5759
6039
|
});
|
|
5760
6040
|
});
|
|
5761
6041
|
}
|
|
@@ -5897,8 +6177,8 @@ function editorBaseUrl() {
|
|
|
5897
6177
|
function openInBrowser(url) {
|
|
5898
6178
|
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
5899
6179
|
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
5900
|
-
return new Promise((
|
|
5901
|
-
(0, import_node_child_process5.exec)(command, () =>
|
|
6180
|
+
return new Promise((resolve11) => {
|
|
6181
|
+
(0, import_node_child_process5.exec)(command, () => resolve11());
|
|
5902
6182
|
});
|
|
5903
6183
|
}
|
|
5904
6184
|
|
|
@@ -5906,13 +6186,13 @@ function openInBrowser(url) {
|
|
|
5906
6186
|
init_SparkStripe_feature_gating();
|
|
5907
6187
|
var TIMEOUT_MS = 12e4;
|
|
5908
6188
|
async function browserLogin() {
|
|
5909
|
-
return new Promise((
|
|
6189
|
+
return new Promise((resolve11) => {
|
|
5910
6190
|
let server;
|
|
5911
6191
|
let timer;
|
|
5912
6192
|
const cleanup = (email) => {
|
|
5913
6193
|
clearTimeout(timer);
|
|
5914
6194
|
server?.close();
|
|
5915
|
-
|
|
6195
|
+
resolve11(email);
|
|
5916
6196
|
};
|
|
5917
6197
|
server = (0, import_node_http2.createServer)((req, res) => {
|
|
5918
6198
|
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
@@ -6063,10 +6343,10 @@ function getLastRateLimitInfo() {
|
|
|
6063
6343
|
}
|
|
6064
6344
|
function prompt(question) {
|
|
6065
6345
|
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
6066
|
-
return new Promise((
|
|
6346
|
+
return new Promise((resolve11) => {
|
|
6067
6347
|
rl.question(question, (answer) => {
|
|
6068
6348
|
rl.close();
|
|
6069
|
-
|
|
6349
|
+
resolve11(answer.trim());
|
|
6070
6350
|
});
|
|
6071
6351
|
});
|
|
6072
6352
|
}
|
|
@@ -6075,8 +6355,10 @@ async function setupUserEmail() {
|
|
|
6075
6355
|
console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
|
|
6076
6356
|
console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
|
|
6077
6357
|
console.log();
|
|
6078
|
-
console.log(import_chalk5.default.white("
|
|
6079
|
-
console.log(import_chalk5.default.white("
|
|
6358
|
+
console.log(import_chalk5.default.white(" The AI that plans your demo runs on our servers, so it"));
|
|
6359
|
+
console.log(import_chalk5.default.white(" needs an address to meter. No card, and no verification"));
|
|
6360
|
+
console.log(import_chalk5.default.white(" needed for your first 5 videos."));
|
|
6361
|
+
console.log(import_chalk5.default.dim(" After that, signing in gets you 10 videos a day."));
|
|
6080
6362
|
const browserEmail = await browserLogin();
|
|
6081
6363
|
if (browserEmail) {
|
|
6082
6364
|
console.log();
|
|
@@ -6085,11 +6367,15 @@ async function setupUserEmail() {
|
|
|
6085
6367
|
return browserEmail;
|
|
6086
6368
|
}
|
|
6087
6369
|
console.log();
|
|
6088
|
-
console.log(import_chalk5.default.dim(" Or enter your email
|
|
6370
|
+
console.log(import_chalk5.default.dim(" Or enter your email (Enter to skip):"));
|
|
6089
6371
|
const email = await prompt(import_chalk5.default.white(" Email: "));
|
|
6090
6372
|
if (!email) {
|
|
6091
6373
|
console.log();
|
|
6092
|
-
console.log(import_chalk5.default.
|
|
6374
|
+
console.log(import_chalk5.default.yellow(" No email given, so the AI planner cannot be reached."));
|
|
6375
|
+
console.log(import_chalk5.default.dim(" Nothing has been recorded. Run qp again when you are ready."));
|
|
6376
|
+
console.log(import_chalk5.default.dim(" To record with no account at all, use the MCP server:"));
|
|
6377
|
+
console.log(import_chalk5.default.dim(" https://quickpeek.co/mcp"));
|
|
6378
|
+
console.log();
|
|
6093
6379
|
process.exit(1);
|
|
6094
6380
|
}
|
|
6095
6381
|
if (!email.includes("@") || !email.includes(".")) {
|
|
@@ -6097,6 +6383,9 @@ async function setupUserEmail() {
|
|
|
6097
6383
|
console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
6098
6384
|
const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
|
|
6099
6385
|
if (proceed.toLowerCase() !== "y") {
|
|
6386
|
+
console.log();
|
|
6387
|
+
console.log(import_chalk5.default.dim(" Nothing has been recorded. Run qp again when you are ready."));
|
|
6388
|
+
console.log();
|
|
6100
6389
|
process.exit(1);
|
|
6101
6390
|
}
|
|
6102
6391
|
}
|
|
@@ -6570,15 +6859,15 @@ async function launchCaptureChrome() {
|
|
|
6570
6859
|
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
6571
6860
|
const windowsHost = getWindowsHostIP();
|
|
6572
6861
|
const probe = await startRelay(nodePath, windowsHost, debugPort);
|
|
6573
|
-
const alreadyUp = await new Promise((
|
|
6862
|
+
const alreadyUp = await new Promise((resolve11) => {
|
|
6574
6863
|
const req = (0, import_node_http3.request)({ host: windowsHost, port: probe.port, path: "/json/version", method: "GET" }, (res) => {
|
|
6575
6864
|
res.resume();
|
|
6576
|
-
|
|
6865
|
+
resolve11(true);
|
|
6577
6866
|
});
|
|
6578
|
-
req.on("error", () =>
|
|
6867
|
+
req.on("error", () => resolve11(false));
|
|
6579
6868
|
req.setTimeout(2e3, () => {
|
|
6580
6869
|
req.destroy();
|
|
6581
|
-
|
|
6870
|
+
resolve11(false);
|
|
6582
6871
|
});
|
|
6583
6872
|
req.end();
|
|
6584
6873
|
});
|
|
@@ -6784,7 +7073,7 @@ async function hasHeadroom() {
|
|
|
6784
7073
|
async function withRenderSlot(render, onWait) {
|
|
6785
7074
|
if (active >= MAX_CONCURRENT_RENDERS) {
|
|
6786
7075
|
onWait?.(`waiting for a render slot (${active}/${MAX_CONCURRENT_RENDERS} busy)`);
|
|
6787
|
-
await new Promise((
|
|
7076
|
+
await new Promise((resolve11) => waiting.push(resolve11));
|
|
6788
7077
|
}
|
|
6789
7078
|
active++;
|
|
6790
7079
|
try {
|
|
@@ -6813,19 +7102,19 @@ init_lib();
|
|
|
6813
7102
|
init_helpers();
|
|
6814
7103
|
|
|
6815
7104
|
// src/record.ts
|
|
6816
|
-
var
|
|
7105
|
+
var import_node_fs15 = require("fs");
|
|
6817
7106
|
var import_promises13 = require("fs/promises");
|
|
6818
7107
|
var import_node_path17 = require("path");
|
|
6819
7108
|
var import_chalk6 = __toESM(require("chalk"));
|
|
6820
7109
|
|
|
6821
7110
|
// src/browser/actions.ts
|
|
6822
|
-
var
|
|
7111
|
+
var import_node_fs12 = require("fs");
|
|
6823
7112
|
var import_promises11 = require("fs/promises");
|
|
6824
7113
|
var import_node_path13 = require("path");
|
|
6825
7114
|
init_lib();
|
|
6826
7115
|
|
|
6827
7116
|
// src/browser/effects.ts
|
|
6828
|
-
var
|
|
7117
|
+
var import_node_fs11 = require("fs");
|
|
6829
7118
|
var import_node_path12 = require("path");
|
|
6830
7119
|
init_lib();
|
|
6831
7120
|
|
|
@@ -6846,7 +7135,7 @@ function loadHighlightCSS() {
|
|
|
6846
7135
|
// src/browser/ → src/
|
|
6847
7136
|
];
|
|
6848
7137
|
for (const p of candidates) {
|
|
6849
|
-
if ((0,
|
|
7138
|
+
if ((0, import_node_fs11.existsSync)(p)) return (0, import_node_fs11.readFileSync)(p, "utf-8");
|
|
6850
7139
|
}
|
|
6851
7140
|
return "";
|
|
6852
7141
|
}
|
|
@@ -7059,6 +7348,7 @@ function locatorFor(page, keyword) {
|
|
|
7059
7348
|
var WAIT_MS = 450;
|
|
7060
7349
|
var HANDLE_MS = 3e3;
|
|
7061
7350
|
var RESOLVE_BUDGET_MS2 = 2e4;
|
|
7351
|
+
var DOM_POLL_MS = 150;
|
|
7062
7352
|
function remaining(deadline) {
|
|
7063
7353
|
return Math.max(1, deadline - Date.now());
|
|
7064
7354
|
}
|
|
@@ -7169,7 +7459,7 @@ async function resolveElementSettled(page, target, deadlineMs = RESOLVE_DEADLINE
|
|
|
7169
7459
|
}
|
|
7170
7460
|
}
|
|
7171
7461
|
async function resolvePlainText(page, text, deadline) {
|
|
7172
|
-
|
|
7462
|
+
let scanned = await domTextMatch(page, text);
|
|
7173
7463
|
const stages = plainTextStages(page, text);
|
|
7174
7464
|
while (Date.now() < deadline) {
|
|
7175
7465
|
for (const { locator, state } of stages) {
|
|
@@ -7180,6 +7470,12 @@ async function resolvePlainText(page, text, deadline) {
|
|
|
7180
7470
|
}
|
|
7181
7471
|
}
|
|
7182
7472
|
if (scanned) return scanned;
|
|
7473
|
+
while (Date.now() < deadline) {
|
|
7474
|
+
await page.waitForTimeout(Math.min(DOM_POLL_MS, remaining(deadline)));
|
|
7475
|
+
scanned = await domTextMatch(page, text);
|
|
7476
|
+
if (scanned) break;
|
|
7477
|
+
}
|
|
7478
|
+
if (!scanned) return null;
|
|
7183
7479
|
}
|
|
7184
7480
|
return scanned ?? null;
|
|
7185
7481
|
}
|
|
@@ -7605,7 +7901,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7605
7901
|
if (!isPlaceholder) {
|
|
7606
7902
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
7607
7903
|
const filePath = (0, import_node_path13.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path13.resolve)(baseDir, normalizedPath);
|
|
7608
|
-
if ((0,
|
|
7904
|
+
if ((0, import_node_fs12.existsSync)(filePath)) {
|
|
7609
7905
|
await fileInput.setInputFiles(filePath);
|
|
7610
7906
|
await page.waitForTimeout(2e3);
|
|
7611
7907
|
break;
|
|
@@ -7722,7 +8018,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7722
8018
|
break;
|
|
7723
8019
|
case "image": {
|
|
7724
8020
|
const imagePath = step.value ? (0, import_node_path13.isAbsolute)(step.value) ? step.value : (0, import_node_path13.resolve)(baseDir, step.value) : null;
|
|
7725
|
-
if (imagePath && (0,
|
|
8021
|
+
if (imagePath && (0, import_node_fs12.existsSync)(imagePath)) {
|
|
7726
8022
|
const imgStyle = step.target === "stretch" ? "width: 100vw; height: 100vh; object-fit: cover;" : "max-width: 90%; max-height: 90%;";
|
|
7727
8023
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected, false)) injected = true;
|
|
7728
8024
|
}
|
|
@@ -7751,7 +8047,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7751
8047
|
await pendingDownload;
|
|
7752
8048
|
pendingDownload = null;
|
|
7753
8049
|
}
|
|
7754
|
-
if (lastDownloadPath && (0,
|
|
8050
|
+
if (lastDownloadPath && (0, import_node_fs12.existsSync)(lastDownloadPath)) {
|
|
7755
8051
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
7756
8052
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
7757
8053
|
}
|
|
@@ -7857,7 +8153,7 @@ async function getPageElementSelectors(page, query, limit, keyword = "") {
|
|
|
7857
8153
|
}
|
|
7858
8154
|
|
|
7859
8155
|
// src/browser/step-validator.ts
|
|
7860
|
-
var
|
|
8156
|
+
var import_node_fs13 = require("fs");
|
|
7861
8157
|
var import_node_path14 = require("path");
|
|
7862
8158
|
init_crawl();
|
|
7863
8159
|
init_utils();
|
|
@@ -7865,9 +8161,9 @@ function findMediaFile(filename, baseDir) {
|
|
|
7865
8161
|
const name = (0, import_node_path14.basename)(filename);
|
|
7866
8162
|
const searchDirs = [baseDir, (0, import_node_path14.join)(baseDir, "res"), (0, import_node_path14.join)(baseDir, "assets"), (0, import_node_path14.join)(baseDir, "media")];
|
|
7867
8163
|
for (const dir of searchDirs) {
|
|
7868
|
-
if (!(0,
|
|
8164
|
+
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
7869
8165
|
try {
|
|
7870
|
-
const files = (0,
|
|
8166
|
+
const files = (0, import_node_fs13.readdirSync)(dir);
|
|
7871
8167
|
if (files.includes(name)) return (0, import_node_path14.join)(dir, name);
|
|
7872
8168
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
7873
8169
|
if (match) return (0, import_node_path14.join)(dir, match);
|
|
@@ -7919,7 +8215,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
7919
8215
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
7920
8216
|
}
|
|
7921
8217
|
const specifiedPath = (0, import_node_path14.isAbsolute)(step.value) ? step.value : (0, import_node_path14.resolve)(baseDir, step.value);
|
|
7922
|
-
if ((0,
|
|
8218
|
+
if ((0, import_node_fs13.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
7923
8219
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
7924
8220
|
if (foundPath) {
|
|
7925
8221
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -8048,7 +8344,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
8048
8344
|
if (!isPlaceholder) {
|
|
8049
8345
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
8050
8346
|
let filePath = (0, import_node_path14.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path14.resolve)(baseDir, normalizedPath);
|
|
8051
|
-
if (!(0,
|
|
8347
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) {
|
|
8052
8348
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
8053
8349
|
if (foundPath) {
|
|
8054
8350
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -8190,7 +8486,7 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
8190
8486
|
}
|
|
8191
8487
|
|
|
8192
8488
|
// src/browser/meta.ts
|
|
8193
|
-
var
|
|
8489
|
+
var import_node_fs14 = require("fs");
|
|
8194
8490
|
var import_promises12 = require("fs/promises");
|
|
8195
8491
|
var import_node_path16 = require("path");
|
|
8196
8492
|
var RECORDING_FILE = "recording.mp4";
|
|
@@ -8200,7 +8496,7 @@ function getRecordingPath(outputDir) {
|
|
|
8200
8496
|
}
|
|
8201
8497
|
async function loadRecordingMeta(outputDir) {
|
|
8202
8498
|
const metaPath = (0, import_node_path16.join)(outputDir, RECORDING_META_FILE);
|
|
8203
|
-
if (!(0,
|
|
8499
|
+
if (!(0, import_node_fs14.existsSync)(metaPath)) return null;
|
|
8204
8500
|
try {
|
|
8205
8501
|
const content = await (0, import_promises12.readFile)(metaPath, "utf-8");
|
|
8206
8502
|
return JSON.parse(content);
|
|
@@ -8318,11 +8614,33 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8318
8614
|
const authState = findAuthState(plan.url);
|
|
8319
8615
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
8320
8616
|
try {
|
|
8617
|
+
const isExtensionPage = plan.url.startsWith("chrome-extension://");
|
|
8618
|
+
if (!isExtensionPage) {
|
|
8619
|
+
const warm = await context.newPage();
|
|
8620
|
+
const warmVideo = warm.video();
|
|
8621
|
+
const warmUrls = [plan.url];
|
|
8622
|
+
for (const step of plan.steps) {
|
|
8623
|
+
if (step.action !== "navigate" || !step.target) continue;
|
|
8624
|
+
if (!warmUrls.includes(step.target)) warmUrls.push(step.target);
|
|
8625
|
+
}
|
|
8626
|
+
try {
|
|
8627
|
+
for (const url of warmUrls) {
|
|
8628
|
+
await gotoSettled(warm, url).catch(() => {
|
|
8629
|
+
});
|
|
8630
|
+
}
|
|
8631
|
+
} catch {
|
|
8632
|
+
} finally {
|
|
8633
|
+
await warm.close().catch(() => {
|
|
8634
|
+
});
|
|
8635
|
+
const stray = await warmVideo?.path().catch(() => void 0);
|
|
8636
|
+
if (stray) await (0, import_promises13.rm)(stray, { force: true }).catch(() => {
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
}
|
|
8321
8640
|
const page = context.pages()[0] || await context.newPage();
|
|
8322
8641
|
setupDownloadTracking(page);
|
|
8323
8642
|
context.on("page", (p) => setupDownloadTracking(p));
|
|
8324
8643
|
const firstStepIsMedia = ["media", "image", "video"].includes(plan.steps[0]?.action ?? "");
|
|
8325
|
-
const isExtensionPage = plan.url.startsWith("chrome-extension://");
|
|
8326
8644
|
let signInChecked = false;
|
|
8327
8645
|
const guardSignedIn = async () => {
|
|
8328
8646
|
if (signInChecked || opts.allowLoggedOut || isExtensionPage) return;
|
|
@@ -8399,7 +8717,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8399
8717
|
if (!targetEl) {
|
|
8400
8718
|
console.log(
|
|
8401
8719
|
`
|
|
8402
|
-
\u26A0 target "${step.target}" not found on ${page.url()}
|
|
8720
|
+
\u26A0 target "${step.target}" not found on ${page.url()}; step ${stepNum} recorded without the action`
|
|
8403
8721
|
);
|
|
8404
8722
|
}
|
|
8405
8723
|
}
|
|
@@ -8538,7 +8856,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8538
8856
|
if (!pageClosedEarly) await page.close();
|
|
8539
8857
|
const videoPath = await video.path();
|
|
8540
8858
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
8541
|
-
if (!videoPath || !(0,
|
|
8859
|
+
if (!videoPath || !(0, import_node_fs15.existsSync)(videoPath)) {
|
|
8542
8860
|
throw new Error("Video file was not created");
|
|
8543
8861
|
}
|
|
8544
8862
|
saveTask.succeed("Video saved");
|
|
@@ -8549,14 +8867,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8549
8867
|
}
|
|
8550
8868
|
|
|
8551
8869
|
// src/utils/video-pipeline.ts
|
|
8552
|
-
var
|
|
8870
|
+
var import_node_fs17 = require("fs");
|
|
8553
8871
|
var import_promises15 = require("fs/promises");
|
|
8554
8872
|
var import_node_path21 = require("path");
|
|
8555
8873
|
var import_chalk9 = __toESM(require("chalk"));
|
|
8556
8874
|
init_lib();
|
|
8557
8875
|
|
|
8558
8876
|
// src/utils/music.ts
|
|
8559
|
-
var
|
|
8877
|
+
var import_node_fs16 = require("fs");
|
|
8560
8878
|
var import_node_path20 = require("path");
|
|
8561
8879
|
|
|
8562
8880
|
// src/commands/utils/record-helpers.ts
|
|
@@ -8573,6 +8891,17 @@ var import_playwright6 = require("playwright");
|
|
|
8573
8891
|
init_mcp_context();
|
|
8574
8892
|
init_lib();
|
|
8575
8893
|
init_json_repair();
|
|
8894
|
+
var WRITTEN_ADDRESS = /\b(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s]*)?/gi;
|
|
8895
|
+
var SPELLED_OUT_ACRONYM = /\b[A-Z](?:\s+[A-Z])+\b/g;
|
|
8896
|
+
function spokenCaption(caption) {
|
|
8897
|
+
const held = [];
|
|
8898
|
+
const masked = caption.replace(SPELLED_OUT_ACRONYM, (run2) => run2.replace(/\s+/g, "")).replace(WRITTEN_ADDRESS, (address) => {
|
|
8899
|
+
held.push(address);
|
|
8900
|
+
return `qpaddr${held.length - 1}qp`;
|
|
8901
|
+
});
|
|
8902
|
+
const shaped = toTitleCase((0, import_video_kit8.toSpokenForm)(masked));
|
|
8903
|
+
return shaped.replace(/qpaddr(\d+)qp/gi, (_match, index) => held[Number(index)] ?? "");
|
|
8904
|
+
}
|
|
8576
8905
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
8577
8906
|
const browserTask = task2("Launching browser with extension");
|
|
8578
8907
|
const userDataDir = (0, import_node_path18.join)(workDir, ".browser-data");
|
|
@@ -8701,8 +9030,18 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
8701
9030
|
// The spoken form is what the voice will actually say, so it is what
|
|
8702
9031
|
// the caption shows: burning the written form instead left the words on
|
|
8703
9032
|
// screen out of step with the words in the ear wherever the two differ
|
|
8704
|
-
// (a
|
|
8705
|
-
|
|
9033
|
+
// (a number read as words).
|
|
9034
|
+
//
|
|
9035
|
+
// Addresses are the exception. Expanding one here turned "quickpeek.co"
|
|
9036
|
+
// into three caption words, "Quickpeek Dot Co", which the line packer
|
|
9037
|
+
// is then free to break across lines: the call to action arrived on
|
|
9038
|
+
// screen as "DOT C O" with the name on the row above. An address is the
|
|
9039
|
+
// one thing in a demo a viewer has to be able to read and type, so it
|
|
9040
|
+
// is held back verbatim from both passes and rejoins as written. The
|
|
9041
|
+
// voice is unaffected: generate.ts derives its own spoken form from
|
|
9042
|
+
// this caption at synthesis time, and word timings fall back to an even
|
|
9043
|
+
// spread across the speech envelope when the counts no longer line up.
|
|
9044
|
+
caption: s.caption ? spokenCaption(s.caption) : s.caption
|
|
8706
9045
|
}));
|
|
8707
9046
|
aiTask.succeed(`Plan: ${plan.steps.length} steps`);
|
|
8708
9047
|
return plan;
|
|
@@ -8874,12 +9213,12 @@ function getOutputDirFromUrl(url) {
|
|
|
8874
9213
|
}
|
|
8875
9214
|
|
|
8876
9215
|
// src/utils/music.ts
|
|
8877
|
-
var DEFAULT_MOOD = "
|
|
9216
|
+
var DEFAULT_MOOD = "corporate";
|
|
8878
9217
|
function musicDir() {
|
|
8879
9218
|
let dir = getScriptDir();
|
|
8880
9219
|
for (let i = 0; i < 6; i++) {
|
|
8881
9220
|
const candidate = (0, import_node_path20.join)(dir, "assets", "music");
|
|
8882
|
-
if ((0,
|
|
9221
|
+
if ((0, import_node_fs16.existsSync)((0, import_node_path20.join)(candidate, "manifest.json"))) return candidate;
|
|
8883
9222
|
const parent = (0, import_node_path20.dirname)(dir);
|
|
8884
9223
|
if (parent === dir) break;
|
|
8885
9224
|
dir = parent;
|
|
@@ -8890,9 +9229,9 @@ function listBundledMusic() {
|
|
|
8890
9229
|
const dir = musicDir();
|
|
8891
9230
|
if (!dir) return [];
|
|
8892
9231
|
try {
|
|
8893
|
-
const parsed = JSON.parse((0,
|
|
9232
|
+
const parsed = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path20.join)(dir, "manifest.json"), "utf8"));
|
|
8894
9233
|
const tracks = Array.isArray(parsed.tracks) ? parsed.tracks : [];
|
|
8895
|
-
return tracks.filter((t) => typeof t?.file === "string").map((t) => ({ ...t, path: (0, import_node_path20.join)(dir, t.file) })).filter((t) => (0,
|
|
9234
|
+
return tracks.filter((t) => typeof t?.file === "string").map((t) => ({ ...t, path: (0, import_node_path20.join)(dir, t.file) })).filter((t) => (0, import_node_fs16.existsSync)(t.path));
|
|
8896
9235
|
} catch {
|
|
8897
9236
|
return [];
|
|
8898
9237
|
}
|
|
@@ -8905,6 +9244,16 @@ function resolveBundledBed(mood = DEFAULT_MOOD) {
|
|
|
8905
9244
|
|
|
8906
9245
|
// src/utils/video-pipeline.ts
|
|
8907
9246
|
var INDENT3 = " ";
|
|
9247
|
+
function captionWordTimings(caption, spoken, duration) {
|
|
9248
|
+
const captionWords = caption.split(/\s+/).filter(Boolean);
|
|
9249
|
+
if (spoken.length === 0 || captionWords.length === 0) {
|
|
9250
|
+
return estimateWordTimings(caption, duration);
|
|
9251
|
+
}
|
|
9252
|
+
if (spoken.length === captionWords.length) {
|
|
9253
|
+
return spoken.map((w, i) => ({ ...w, word: captionWords[i] ?? w.word }));
|
|
9254
|
+
}
|
|
9255
|
+
return estimateWordTimings(caption, duration);
|
|
9256
|
+
}
|
|
8908
9257
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
8909
9258
|
const cues = [];
|
|
8910
9259
|
let currentTime = VOICE_LEAD_MS;
|
|
@@ -8915,7 +9264,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
8915
9264
|
currentTime += config.timing.minStepDuration;
|
|
8916
9265
|
continue;
|
|
8917
9266
|
}
|
|
8918
|
-
const words =
|
|
9267
|
+
const words = captionWordTimings(step.caption || "", audio.words, audio.duration);
|
|
8919
9268
|
if (words.length > 0 && !step.spokenOnly) {
|
|
8920
9269
|
const offset = currentTime;
|
|
8921
9270
|
cues.push({
|
|
@@ -8935,7 +9284,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
8935
9284
|
return assPath;
|
|
8936
9285
|
}
|
|
8937
9286
|
var STILL_EXTENSIONS = ["png", "jpg", "jpeg", "webp"];
|
|
8938
|
-
function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
9287
|
+
function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0, totalSecs, blackSpans) {
|
|
8939
9288
|
const overlays = [];
|
|
8940
9289
|
let cumulativeTime = leadInSecs;
|
|
8941
9290
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -8949,12 +9298,19 @@ function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
|
8949
9298
|
const still = isCard && STILL_EXTENSIONS.includes(ext);
|
|
8950
9299
|
if (still || isClip && ["mp4", "webm", "mov"].includes(ext)) {
|
|
8951
9300
|
const mediaPath = (0, import_node_path21.isAbsolute)(step.value) ? step.value : (0, import_node_path21.resolve)(baseDir, step.value);
|
|
8952
|
-
if ((0,
|
|
9301
|
+
if ((0, import_node_fs17.existsSync)(mediaPath) && durationSec > 0) {
|
|
8953
9302
|
const partial = parsePartial(step.partial);
|
|
9303
|
+
const runsToEnd = still && i === steps.length - 1 && totalSecs !== void 0;
|
|
9304
|
+
let duration = runsToEnd ? Math.max(durationSec, totalSecs - cumulativeTime) : durationSec;
|
|
9305
|
+
if (still && !runsToEnd && blackSpans) {
|
|
9306
|
+
const endsAt = cumulativeTime + duration;
|
|
9307
|
+
const following = blackSpans.find((sp) => sp.start <= endsAt + 0.25 && sp.end > endsAt);
|
|
9308
|
+
if (following) duration = following.end - cumulativeTime;
|
|
9309
|
+
}
|
|
8954
9310
|
overlays.push({
|
|
8955
9311
|
path: mediaPath,
|
|
8956
9312
|
startTime: cumulativeTime,
|
|
8957
|
-
duration
|
|
9313
|
+
duration,
|
|
8958
9314
|
mode: step.target === "stretch" ? "stretch" : step.target === "cover" ? "cover" : "center",
|
|
8959
9315
|
...still ? { still: true } : {},
|
|
8960
9316
|
...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
|
|
@@ -8967,7 +9323,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
|
8967
9323
|
}
|
|
8968
9324
|
return overlays;
|
|
8969
9325
|
}
|
|
8970
|
-
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, hasClipAudio = false, endsOnCreditsCard = false, _stepSync) {
|
|
9326
|
+
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, hasClipAudio = false, endsOnCreditsCard = false, _stepSync, musicStartSecs = 0) {
|
|
8971
9327
|
header("Compose");
|
|
8972
9328
|
const configPath = (0, import_node_path21.join)(runRoot(), CONFIG_FILE);
|
|
8973
9329
|
console.log(`${INDENT3}${import_chalk9.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
@@ -8983,9 +9339,9 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
8983
9339
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
8984
9340
|
const finalPath = (0, import_node_path21.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
8985
9341
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
8986
|
-
const hasVoiceover = audioPath && (0,
|
|
9342
|
+
const hasVoiceover = audioPath && (0, import_node_fs17.existsSync)(audioPath);
|
|
8987
9343
|
const musicPath = config.music?.path ? (0, import_node_path21.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path21.resolve)(config.output.dir, config.music.path) : "";
|
|
8988
|
-
let hasMusic = musicPath && (0,
|
|
9344
|
+
let hasMusic = musicPath && (0, import_node_fs17.existsSync)(musicPath);
|
|
8989
9345
|
let bedPath = "";
|
|
8990
9346
|
let bedCredit = "";
|
|
8991
9347
|
if (!hasMusic && config.audio?.ambience !== false) {
|
|
@@ -9037,7 +9393,14 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
9037
9393
|
hasClipAudio,
|
|
9038
9394
|
mainContentDuration,
|
|
9039
9395
|
stepTransitions,
|
|
9040
|
-
hwaccel: config.video.hwaccel
|
|
9396
|
+
hwaccel: config.video.hwaccel,
|
|
9397
|
+
// The tempo pass runs after this and speeds the whole file up, music
|
|
9398
|
+
// included. Told about it, compose lays the bed down pre-slowed so the
|
|
9399
|
+
// song comes out at its own speed.
|
|
9400
|
+
tempo: config.video.tempo,
|
|
9401
|
+
musicStartSecs,
|
|
9402
|
+
// So the bed can be landed with its last note on the last frame.
|
|
9403
|
+
musicDurationSecs: hasMusic ? await getMediaDuration(backingPath).then((ms) => ms / 1e3).catch(() => 0) : 0
|
|
9041
9404
|
});
|
|
9042
9405
|
composeTask.succeed("Video composed");
|
|
9043
9406
|
return finalPath;
|
|
@@ -9110,7 +9473,7 @@ function resolveOutro(config, userTier, userEmail) {
|
|
|
9110
9473
|
const fallback = (0, import_node_path23.join)(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
9111
9474
|
return (0, import_node_fs18.existsSync)(fallback) ? fallback : void 0;
|
|
9112
9475
|
}
|
|
9113
|
-
var EMPTY_MIN_SECS = 0.
|
|
9476
|
+
var EMPTY_MIN_SECS = 0.4;
|
|
9114
9477
|
var OUTRO_TAIL_MS = 900;
|
|
9115
9478
|
var FIT_SECOND_PASS_CUSHION_SECS = 1;
|
|
9116
9479
|
var FIT_SECOND_PASS_MAX_SPEEDUP = 1.45;
|
|
@@ -9140,6 +9503,7 @@ async function renderVideoPhase(params) {
|
|
|
9140
9503
|
const firstStep = plan.steps[0];
|
|
9141
9504
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
9142
9505
|
let trimStart = 0;
|
|
9506
|
+
let recordedStepLengths = null;
|
|
9143
9507
|
const doRecord = async () => {
|
|
9144
9508
|
header("Record");
|
|
9145
9509
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError, {
|
|
@@ -9151,6 +9515,7 @@ async function renderVideoPhase(params) {
|
|
|
9151
9515
|
]);
|
|
9152
9516
|
const detectedEmpty = mergeSpans([...result.leadSpans ?? [], ...emptyBlack, ...emptyWhite]);
|
|
9153
9517
|
let stepLengths = result.actualStepDurations;
|
|
9518
|
+
let zeroAnchored = false;
|
|
9154
9519
|
if (detectedEmpty.length > 0) {
|
|
9155
9520
|
const bounds = [];
|
|
9156
9521
|
let at = 0;
|
|
@@ -9175,6 +9540,7 @@ async function renderVideoPhase(params) {
|
|
|
9175
9540
|
const sum = result.actualStepDurations.reduce((a, b2) => a + b2, 0);
|
|
9176
9541
|
const factor = sum > 0 && realSecs > 0 ? realSecs * 1e3 / sum : 1;
|
|
9177
9542
|
stepLengths = result.actualStepDurations.map((ms) => ms * factor);
|
|
9543
|
+
zeroAnchored = true;
|
|
9178
9544
|
info(`Sped up ${emptySecs.toFixed(1)}s of loading screens at ${EMPTY_SPAN_SPEED}x`);
|
|
9179
9545
|
} else {
|
|
9180
9546
|
await discardPartial(retimed);
|
|
@@ -9191,16 +9557,19 @@ async function renderVideoPhase(params) {
|
|
|
9191
9557
|
const meta = buildRecordingMeta(plan, config, stepDurations);
|
|
9192
9558
|
meta.actualStepDurations = stepLengths;
|
|
9193
9559
|
meta.pageLoadTime = result.pageLoadTime;
|
|
9560
|
+
meta.zeroAnchored = zeroAnchored;
|
|
9194
9561
|
await saveRecordingMeta(config.output.dir, meta);
|
|
9195
9562
|
videoPath = recordingPath;
|
|
9196
|
-
|
|
9563
|
+
recordedStepLengths = stepLengths;
|
|
9564
|
+
trimStart = firstStepIsVideo || zeroAnchored ? 0 : result.pageLoadTime;
|
|
9197
9565
|
};
|
|
9198
9566
|
if (hasExistingRecording) {
|
|
9199
9567
|
const reuseCheck = canReuseRecording(plan, config, stepDurations, existingRecordingMeta);
|
|
9200
9568
|
if (reuseCheck.canReuse) {
|
|
9201
9569
|
info("Reusing existing recording");
|
|
9202
9570
|
videoPath = recordingPath;
|
|
9203
|
-
|
|
9571
|
+
recordedStepLengths = existingRecordingMeta.actualStepDurations ?? existingRecordingMeta.stepDurations;
|
|
9572
|
+
trimStart = firstStepIsVideo || existingRecordingMeta.zeroAnchored ? 0 : existingRecordingMeta?.pageLoadTime ?? 0.5;
|
|
9204
9573
|
stepAudio = await alignVoiceoverToRecording(
|
|
9205
9574
|
stepAudio,
|
|
9206
9575
|
existingRecordingMeta.actualStepDurations ?? existingRecordingMeta.stepDurations,
|
|
@@ -9214,9 +9583,23 @@ async function renderVideoPhase(params) {
|
|
|
9214
9583
|
} else {
|
|
9215
9584
|
await doRecord();
|
|
9216
9585
|
}
|
|
9217
|
-
const alignedStepDurations = plan.steps.map(
|
|
9586
|
+
const alignedStepDurations = plan.steps.map(
|
|
9587
|
+
(step, i) => Math.max(
|
|
9588
|
+
stepAudio.get(i + 1)?.duration ?? recordedStepLengths?.[i] ?? stepDurations[i] ?? 0,
|
|
9589
|
+
step.duration ? step.duration * 1e3 : 0
|
|
9590
|
+
)
|
|
9591
|
+
);
|
|
9218
9592
|
let splicedClipAudio = false;
|
|
9219
|
-
const
|
|
9593
|
+
const recordingSecs = await getMediaDuration(videoPath).then((ms) => ms / 1e3).catch(() => void 0);
|
|
9594
|
+
const blackSpans = await detectBlackSpans(videoPath, 0.3).catch(() => []);
|
|
9595
|
+
const videoOverlays = buildVideoOverlays(
|
|
9596
|
+
plan.steps,
|
|
9597
|
+
alignedStepDurations,
|
|
9598
|
+
config.output.dir,
|
|
9599
|
+
trimStart,
|
|
9600
|
+
recordingSecs,
|
|
9601
|
+
blackSpans
|
|
9602
|
+
);
|
|
9220
9603
|
if (videoOverlays.length > 0) {
|
|
9221
9604
|
const spliceTask = task("Painting cards and clips into recording");
|
|
9222
9605
|
try {
|
|
@@ -9270,11 +9653,13 @@ async function renderVideoPhase(params) {
|
|
|
9270
9653
|
const cardStepSeconds = (index) => {
|
|
9271
9654
|
const action = plan.steps[index]?.action;
|
|
9272
9655
|
if (action !== "image" && action !== "media") return 0;
|
|
9273
|
-
|
|
9656
|
+
const audioSecs = (stepAudio.get(index + 1)?.duration ?? 0) / 1e3;
|
|
9657
|
+
const tail = index === plan.steps.length - 1 && action === "image" ? OUTRO_TAIL_MS / 1e3 : 0;
|
|
9658
|
+
return Math.max(audioSecs, plan.steps[index]?.duration ?? 0) + tail;
|
|
9274
9659
|
};
|
|
9275
9660
|
const introSecs = cardStepSeconds(0);
|
|
9276
9661
|
const outroStepSecs = cardStepSeconds(plan.steps.length - 1);
|
|
9277
|
-
const outroSeconds = config.video.outroCard?.seconds ??
|
|
9662
|
+
const outroSeconds = Math.max(config.video.outroCard?.seconds ?? 3.5, outroStepSecs);
|
|
9278
9663
|
const card = config.video.outroCard;
|
|
9279
9664
|
if (card?.subject?.name) {
|
|
9280
9665
|
const cardTask = task("Rendering credits card");
|
|
@@ -9318,6 +9703,20 @@ async function renderVideoPhase(params) {
|
|
|
9318
9703
|
mainContentDuration = void 0;
|
|
9319
9704
|
}
|
|
9320
9705
|
}
|
|
9706
|
+
const outroOnly = config.music?.outroOnly !== false;
|
|
9707
|
+
let musicStartSecs = 0;
|
|
9708
|
+
if (outroOnly) {
|
|
9709
|
+
let trailingCards = 0;
|
|
9710
|
+
for (let i = plan.steps.length - 1; i >= 0; i--) {
|
|
9711
|
+
const action = plan.steps[i]?.action;
|
|
9712
|
+
if (action !== "image" && action !== "media") break;
|
|
9713
|
+
trailingCards++;
|
|
9714
|
+
}
|
|
9715
|
+
if (trailingCards > 0 && trailingCards < plan.steps.length) {
|
|
9716
|
+
const upTo = plan.steps.length - trailingCards;
|
|
9717
|
+
musicStartSecs = alignedStepDurations.slice(0, upTo).reduce((a, b) => a + b, 0) / 1e3;
|
|
9718
|
+
}
|
|
9719
|
+
}
|
|
9321
9720
|
let finalPath = await composeAndFinish(
|
|
9322
9721
|
videoToCompose,
|
|
9323
9722
|
voiceoverPath,
|
|
@@ -9329,7 +9728,9 @@ async function renderVideoPhase(params) {
|
|
|
9329
9728
|
stepTransitions,
|
|
9330
9729
|
// The splice writes a clip-audio track only when a spliced clip had one.
|
|
9331
9730
|
splicedClipAudio,
|
|
9332
|
-
endsOnCreditsCard
|
|
9731
|
+
endsOnCreditsCard,
|
|
9732
|
+
void 0,
|
|
9733
|
+
musicStartSecs
|
|
9333
9734
|
);
|
|
9334
9735
|
const maxSilence = config.video.maxSilenceSecs ?? 0.6;
|
|
9335
9736
|
if (maxSilence > 0) {
|
|
@@ -9685,8 +10086,8 @@ async function captureCommand(url, lang) {
|
|
|
9685
10086
|
}
|
|
9686
10087
|
|
|
9687
10088
|
// src/commands/edit-cmd.ts
|
|
9688
|
-
var
|
|
9689
|
-
var
|
|
10089
|
+
var import_node_fs22 = require("fs");
|
|
10090
|
+
var import_node_path28 = require("path");
|
|
9690
10091
|
var import_chalk12 = __toESM(require("chalk"));
|
|
9691
10092
|
|
|
9692
10093
|
// src/lib/plan-validation.ts
|
|
@@ -9844,17 +10245,81 @@ function planEditorUrl(baseUrl, plan) {
|
|
|
9844
10245
|
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
9845
10246
|
}
|
|
9846
10247
|
|
|
10248
|
+
// src/lib/plan-thumbs.ts
|
|
10249
|
+
var import_node_fs21 = require("fs");
|
|
10250
|
+
var import_node_path27 = require("path");
|
|
10251
|
+
init_spawn();
|
|
10252
|
+
var MEDIA_ACTIONS = ["image", "media", "video"];
|
|
10253
|
+
var THUMB_WIDTH = 160;
|
|
10254
|
+
var MAX_THUMB_CHARS = 1e4;
|
|
10255
|
+
var MAX_TOTAL_THUMB_CHARS = 16e3;
|
|
10256
|
+
var THUMB_TIMEOUT_MS = 3e3;
|
|
10257
|
+
function thumbnail(path) {
|
|
10258
|
+
return new Promise((done) => {
|
|
10259
|
+
const proc = spawnFFmpeg("ffmpeg", [
|
|
10260
|
+
"-v",
|
|
10261
|
+
"error",
|
|
10262
|
+
"-i",
|
|
10263
|
+
path,
|
|
10264
|
+
"-frames:v",
|
|
10265
|
+
"1",
|
|
10266
|
+
"-vf",
|
|
10267
|
+
`scale=${THUMB_WIDTH}:-2`,
|
|
10268
|
+
"-q:v",
|
|
10269
|
+
"14",
|
|
10270
|
+
"-f",
|
|
10271
|
+
"mjpeg",
|
|
10272
|
+
"-"
|
|
10273
|
+
]);
|
|
10274
|
+
const chunks = [];
|
|
10275
|
+
const timer = setTimeout(() => {
|
|
10276
|
+
proc.kill();
|
|
10277
|
+
done(null);
|
|
10278
|
+
}, THUMB_TIMEOUT_MS);
|
|
10279
|
+
const finish = (result) => {
|
|
10280
|
+
clearTimeout(timer);
|
|
10281
|
+
done(result);
|
|
10282
|
+
};
|
|
10283
|
+
proc.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
10284
|
+
proc.on("error", () => finish(null));
|
|
10285
|
+
proc.on("close", (code) => {
|
|
10286
|
+
const bytes = Buffer.concat(chunks);
|
|
10287
|
+
if (code !== 0 || bytes.length === 0) return finish(null);
|
|
10288
|
+
const base64 = bytes.toString("base64");
|
|
10289
|
+
finish(base64.length > MAX_THUMB_CHARS ? null : `data:image/jpeg;base64,${base64}`);
|
|
10290
|
+
});
|
|
10291
|
+
});
|
|
10292
|
+
}
|
|
10293
|
+
async function withEditorThumbs(plan, baseDir) {
|
|
10294
|
+
const steps = [...plan.steps];
|
|
10295
|
+
let budget = MAX_TOTAL_THUMB_CHARS;
|
|
10296
|
+
for (const [index, step] of steps.entries()) {
|
|
10297
|
+
if (!step.value || !MEDIA_ACTIONS.includes(step.action)) continue;
|
|
10298
|
+
const path = (0, import_node_path27.isAbsolute)(step.value) ? step.value : (0, import_node_path27.resolve)(baseDir, step.value);
|
|
10299
|
+
if (!(0, import_node_fs21.existsSync)(path)) continue;
|
|
10300
|
+
const thumb = await thumbnail(path);
|
|
10301
|
+
if (!thumb || thumb.length > budget) continue;
|
|
10302
|
+
budget -= thumb.length;
|
|
10303
|
+
steps[index] = { ...step, thumb };
|
|
10304
|
+
}
|
|
10305
|
+
return { ...plan, steps };
|
|
10306
|
+
}
|
|
10307
|
+
async function editorUrlWithThumbs(baseUrl, plan, baseDir) {
|
|
10308
|
+
const withThumbs = await withEditorThumbs(plan, baseDir);
|
|
10309
|
+
return planEditorUrl(baseUrl, withThumbs) ?? planEditorUrl(baseUrl, plan);
|
|
10310
|
+
}
|
|
10311
|
+
|
|
9847
10312
|
// src/commands/edit-cmd.ts
|
|
9848
10313
|
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
9849
10314
|
function findPlanFile(dir, lang) {
|
|
9850
10315
|
if (lang) {
|
|
9851
|
-
const exact = (0,
|
|
9852
|
-
return (0,
|
|
10316
|
+
const exact = (0, import_node_path28.join)(dir, `plan-${lang}.json`);
|
|
10317
|
+
return (0, import_node_fs22.existsSync)(exact) ? exact : null;
|
|
9853
10318
|
}
|
|
9854
|
-
if (!(0,
|
|
9855
|
-
const plans = (0,
|
|
10319
|
+
if (!(0, import_node_fs22.existsSync)(dir)) return null;
|
|
10320
|
+
const plans = (0, import_node_fs22.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
|
|
9856
10321
|
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
9857
|
-
return preferred ? (0,
|
|
10322
|
+
return preferred ? (0, import_node_path28.join)(dir, preferred) : null;
|
|
9858
10323
|
}
|
|
9859
10324
|
async function editCommand(url, lang) {
|
|
9860
10325
|
console.log();
|
|
@@ -9867,14 +10332,14 @@ async function editCommand(url, lang) {
|
|
|
9867
10332
|
console.log();
|
|
9868
10333
|
return;
|
|
9869
10334
|
}
|
|
9870
|
-
const parsed = parsePlanText((0,
|
|
10335
|
+
const parsed = parsePlanText((0, import_node_fs22.readFileSync)(planPath, "utf8"));
|
|
9871
10336
|
if (!parsed.ok) {
|
|
9872
10337
|
console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
|
|
9873
10338
|
console.log();
|
|
9874
10339
|
return;
|
|
9875
10340
|
}
|
|
9876
10341
|
const base = editorBaseUrl();
|
|
9877
|
-
const editorUrl =
|
|
10342
|
+
const editorUrl = await editorUrlWithThumbs(base, parsed.plan, dir);
|
|
9878
10343
|
if (!editorUrl) {
|
|
9879
10344
|
console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
|
|
9880
10345
|
console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
|
|
@@ -9890,14 +10355,14 @@ async function editCommand(url, lang) {
|
|
|
9890
10355
|
}
|
|
9891
10356
|
|
|
9892
10357
|
// src/commands/init-cmd.ts
|
|
9893
|
-
var
|
|
10358
|
+
var import_node_fs23 = require("fs");
|
|
9894
10359
|
var import_promises18 = require("fs/promises");
|
|
9895
|
-
var
|
|
10360
|
+
var import_node_path29 = require("path");
|
|
9896
10361
|
var import_chalk13 = __toESM(require("chalk"));
|
|
9897
10362
|
init_lib();
|
|
9898
10363
|
async function initCommand() {
|
|
9899
|
-
const configPath = (0,
|
|
9900
|
-
if ((0,
|
|
10364
|
+
const configPath = (0, import_node_path29.join)(process.cwd(), CONFIG_FILE);
|
|
10365
|
+
if ((0, import_node_fs23.existsSync)(configPath)) {
|
|
9901
10366
|
console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
9902
10367
|
return;
|
|
9903
10368
|
}
|
|
@@ -9928,9 +10393,9 @@ async function initCommand() {
|
|
|
9928
10393
|
console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
9929
10394
|
}
|
|
9930
10395
|
async function configCommand(key, value) {
|
|
9931
|
-
const configPath = (0,
|
|
10396
|
+
const configPath = (0, import_node_path29.join)(process.cwd(), CONFIG_FILE);
|
|
9932
10397
|
if (!key) {
|
|
9933
|
-
if (!(0,
|
|
10398
|
+
if (!(0, import_node_fs23.existsSync)(configPath)) {
|
|
9934
10399
|
console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
9935
10400
|
return;
|
|
9936
10401
|
}
|
|
@@ -9945,7 +10410,7 @@ async function configCommand(key, value) {
|
|
|
9945
10410
|
return;
|
|
9946
10411
|
}
|
|
9947
10412
|
let config = {};
|
|
9948
|
-
if ((0,
|
|
10413
|
+
if ((0, import_node_fs23.existsSync)(configPath)) {
|
|
9949
10414
|
try {
|
|
9950
10415
|
config = JSON.parse(await (0, import_promises18.readFile)(configPath, "utf-8"));
|
|
9951
10416
|
} catch {
|
|
@@ -10034,11 +10499,36 @@ async function logoutCommand() {
|
|
|
10034
10499
|
}
|
|
10035
10500
|
|
|
10036
10501
|
// src/commands/record-cmd.ts
|
|
10037
|
-
var
|
|
10502
|
+
var import_node_fs29 = require("fs");
|
|
10038
10503
|
var import_promises23 = require("fs/promises");
|
|
10039
|
-
var
|
|
10504
|
+
var import_node_path35 = require("path");
|
|
10040
10505
|
var import_chalk21 = __toESM(require("chalk"));
|
|
10041
10506
|
init_mcp_context();
|
|
10507
|
+
|
|
10508
|
+
// src/lib/demo-registry.ts
|
|
10509
|
+
init_api_client();
|
|
10510
|
+
var TIMEOUT_MS2 = 4e3;
|
|
10511
|
+
async function registerDemoRun(email, run2) {
|
|
10512
|
+
if (!email || email === "anonymous") return false;
|
|
10513
|
+
const controller = new AbortController();
|
|
10514
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
|
|
10515
|
+
try {
|
|
10516
|
+
const token = await getOrRefreshRelayToken(email);
|
|
10517
|
+
const response = await fetch(`${RELAY_BASE}/api/cli/demos`, {
|
|
10518
|
+
method: "POST",
|
|
10519
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
10520
|
+
body: JSON.stringify(run2),
|
|
10521
|
+
signal: controller.signal
|
|
10522
|
+
});
|
|
10523
|
+
return response.ok;
|
|
10524
|
+
} catch {
|
|
10525
|
+
return false;
|
|
10526
|
+
} finally {
|
|
10527
|
+
clearTimeout(timer);
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
|
|
10531
|
+
// src/commands/record-cmd.ts
|
|
10042
10532
|
init_lib();
|
|
10043
10533
|
|
|
10044
10534
|
// src/commands/phases/auth-phase.ts
|
|
@@ -10055,7 +10545,7 @@ async function handleRegistrationGate() {
|
|
|
10055
10545
|
console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
|
|
10056
10546
|
console.log();
|
|
10057
10547
|
console.log(import_chalk15.default.dim(" Benefits of signing in:"));
|
|
10058
|
-
console.log(import_chalk15.default.dim(" \u2022
|
|
10548
|
+
console.log(import_chalk15.default.dim(" \u2022 10 videos a day instead of 5 in total"));
|
|
10059
10549
|
console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
|
|
10060
10550
|
console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
10061
10551
|
let email = await browserLogin();
|
|
@@ -10106,9 +10596,12 @@ async function runAuthPhase(input) {
|
|
|
10106
10596
|
const { isVerified: isVerified2 } = input;
|
|
10107
10597
|
const mcpTool = currentMcpTool();
|
|
10108
10598
|
if (mcpTool && !(userEmail && isVerified2)) {
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10599
|
+
if (localVideoCount >= FREE_VIDEO_LIMIT) {
|
|
10600
|
+
throw new Error(
|
|
10601
|
+
`"${mcpTool}" has used all ${FREE_VIDEO_LIMIT} free videos on this machine and needs a verified QuickPeek account to continue${userEmail ? ` (${userEmail} is registered but unverified)` : ""}. Sign in once from a terminal with \`qp login\`, a browser flow that cannot run inside an MCP call, then retry. Use check_account to confirm.`
|
|
10602
|
+
);
|
|
10603
|
+
}
|
|
10604
|
+
return { userEmail: "anonymous", userTier: "free", localVideoCount, isVerified: false };
|
|
10112
10605
|
}
|
|
10113
10606
|
if (userEmail && isVerified2) {
|
|
10114
10607
|
const tierTask = task("Checking subscription");
|
|
@@ -10199,7 +10692,7 @@ async function runAuthPhase(input) {
|
|
|
10199
10692
|
const remaining2 = FREE_VIDEO_LIMIT - localVideoCount;
|
|
10200
10693
|
console.log();
|
|
10201
10694
|
console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
10202
|
-
console.log(import_chalk16.default.dim(` ${remaining2 - 1} more after this \u2022 Sign up for
|
|
10695
|
+
console.log(import_chalk16.default.dim(` ${remaining2 - 1} more after this \u2022 Sign up for 10 a day`));
|
|
10203
10696
|
userEmail = await setupUserEmail();
|
|
10204
10697
|
let registeredVerified = false;
|
|
10205
10698
|
try {
|
|
@@ -10218,9 +10711,9 @@ async function runAuthPhase(input) {
|
|
|
10218
10711
|
}
|
|
10219
10712
|
|
|
10220
10713
|
// src/commands/phases/plan-phase.ts
|
|
10221
|
-
var
|
|
10714
|
+
var import_node_fs26 = require("fs");
|
|
10222
10715
|
var import_promises20 = require("fs/promises");
|
|
10223
|
-
var
|
|
10716
|
+
var import_node_path32 = require("path");
|
|
10224
10717
|
var import_chalk18 = __toESM(require("chalk"));
|
|
10225
10718
|
|
|
10226
10719
|
// src/ai/fix.ts
|
|
@@ -10376,15 +10869,15 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
10376
10869
|
}
|
|
10377
10870
|
|
|
10378
10871
|
// src/commands/phases/plan-cards.ts
|
|
10379
|
-
var
|
|
10380
|
-
var
|
|
10872
|
+
var import_node_fs25 = require("fs");
|
|
10873
|
+
var import_node_path31 = require("path");
|
|
10381
10874
|
var import_chalk17 = __toESM(require("chalk"));
|
|
10382
10875
|
init_render_outro();
|
|
10383
10876
|
|
|
10384
10877
|
// src/lib/og-card.ts
|
|
10385
|
-
var
|
|
10878
|
+
var import_node_fs24 = require("fs");
|
|
10386
10879
|
var import_promises19 = require("fs/promises");
|
|
10387
|
-
var
|
|
10880
|
+
var import_node_path30 = require("path");
|
|
10388
10881
|
init_image_kind();
|
|
10389
10882
|
var PORTRAIT_PATHS = ["/opengraph-image/portrait", "/opengraph-image-portrait", "/og/portrait"];
|
|
10390
10883
|
async function fetchImage(url) {
|
|
@@ -10464,7 +10957,7 @@ async function fetchOgCard(siteUrl, dir, preferPortrait) {
|
|
|
10464
10957
|
for (const candidate of candidates) {
|
|
10465
10958
|
const buf = await fetchImage(candidate);
|
|
10466
10959
|
if (buf) {
|
|
10467
|
-
const path = (0,
|
|
10960
|
+
const path = (0, import_node_path30.join)(dir, "og-card.png");
|
|
10468
10961
|
await (0, import_promises19.writeFile)(path, buf);
|
|
10469
10962
|
return path;
|
|
10470
10963
|
}
|
|
@@ -10537,7 +11030,7 @@ async function saveFirstUsableIcon(hrefs, siteUrl, dir) {
|
|
|
10537
11030
|
if (!buf) continue;
|
|
10538
11031
|
const kind = sniffImageKind(buf);
|
|
10539
11032
|
if (!kind) continue;
|
|
10540
|
-
const path = (0,
|
|
11033
|
+
const path = (0, import_node_path30.join)(dir, `app-icon.${kind === "jpeg" ? "jpg" : kind}`);
|
|
10541
11034
|
await (0, import_promises19.writeFile)(path, buf);
|
|
10542
11035
|
if (kind === "png") {
|
|
10543
11036
|
const size = pngSize(path);
|
|
@@ -10551,10 +11044,10 @@ async function saveFirstUsableIcon(hrefs, siteUrl, dir) {
|
|
|
10551
11044
|
}
|
|
10552
11045
|
function existingIcon(dir) {
|
|
10553
11046
|
for (const ext of ["png", "svg", "jpg", "webp", "gif"]) {
|
|
10554
|
-
const path = (0,
|
|
11047
|
+
const path = (0, import_node_path30.join)(dir, `app-icon.${ext}`);
|
|
10555
11048
|
try {
|
|
10556
|
-
if (!(0,
|
|
10557
|
-
if (!sniffImageKind((0,
|
|
11049
|
+
if (!(0, import_node_fs24.existsSync)(path)) continue;
|
|
11050
|
+
if (!sniffImageKind((0, import_node_fs24.readFileSync)(path))) continue;
|
|
10558
11051
|
return path;
|
|
10559
11052
|
} catch {
|
|
10560
11053
|
}
|
|
@@ -10563,7 +11056,7 @@ function existingIcon(dir) {
|
|
|
10563
11056
|
}
|
|
10564
11057
|
function pngSize(path) {
|
|
10565
11058
|
try {
|
|
10566
|
-
const buf = (0,
|
|
11059
|
+
const buf = (0, import_node_fs24.readFileSync)(path);
|
|
10567
11060
|
const head = buf.subarray(0, 33);
|
|
10568
11061
|
if (head.length >= 24 && head.readUInt32BE(12) === 1229472850) {
|
|
10569
11062
|
return { width: head.readUInt32BE(16), height: head.readUInt32BE(20) };
|
|
@@ -10642,8 +11135,8 @@ async function useOgCardAsIntro(plan, url, config, fresh = false) {
|
|
|
10642
11135
|
}
|
|
10643
11136
|
const rewritable = first.action === "wait" && !first.target;
|
|
10644
11137
|
const portrait = (config.video.height ?? 0) > (config.video.width ?? 0);
|
|
10645
|
-
const existing = (0,
|
|
10646
|
-
const fetched = !fresh && (0,
|
|
11138
|
+
const existing = (0, import_node_path31.join)(config.output.dir, "og-card.png");
|
|
11139
|
+
const fetched = !fresh && (0, import_node_fs25.existsSync)(existing) ? existing : await fetchOgCard(url, config.output.dir, portrait);
|
|
10647
11140
|
const ogPath = fetched && coversFrame(fetched, config) ? fetched : null;
|
|
10648
11141
|
const cardPath = ogPath ?? await renderGeneratedIntroCard(url, config);
|
|
10649
11142
|
if (!cardPath) {
|
|
@@ -10733,30 +11226,20 @@ function fitFor(cardPath, config) {
|
|
|
10733
11226
|
return Math.abs(size.width / size.height - frame) < 0.06 ? "stretch" : "cover";
|
|
10734
11227
|
}
|
|
10735
11228
|
function resolveCardPath(value, config) {
|
|
10736
|
-
return (0,
|
|
11229
|
+
return (0, import_node_path31.isAbsolute)(value) ? value : (0, import_node_path31.join)(config.output.dir, value.replace(/^\.\//, ""));
|
|
10737
11230
|
}
|
|
10738
11231
|
var DEFAULT_INTRO_CARD_SECONDS = 2.5;
|
|
10739
11232
|
function packageAsset(relative) {
|
|
10740
11233
|
let dir = getScriptDir();
|
|
10741
11234
|
for (let up = 0; up < 6; up++) {
|
|
10742
|
-
const candidate = (0,
|
|
10743
|
-
if ((0,
|
|
10744
|
-
dir = (0,
|
|
10745
|
-
}
|
|
10746
|
-
return void 0;
|
|
10747
|
-
}
|
|
10748
|
-
function siblingAsset(relative) {
|
|
10749
|
-
let dir = (0, import_node_path30.join)(getScriptDir(), "..");
|
|
10750
|
-
for (let up = 0; up < 7; up++) {
|
|
10751
|
-
const candidate = (0, import_node_path30.join)(dir, relative);
|
|
10752
|
-
if ((0, import_node_fs24.existsSync)(candidate)) return candidate;
|
|
10753
|
-
dir = (0, import_node_path30.join)(dir, "..");
|
|
11235
|
+
const candidate = (0, import_node_path31.join)(dir, relative);
|
|
11236
|
+
if ((0, import_node_fs25.existsSync)(candidate)) return candidate;
|
|
11237
|
+
dir = (0, import_node_path31.join)(dir, "..");
|
|
10754
11238
|
}
|
|
10755
11239
|
return void 0;
|
|
10756
11240
|
}
|
|
10757
11241
|
var CREDIT_TOOLS = [
|
|
10758
|
-
{ name: "QuickPeek", role: "Video generated by", icon: () => packageAsset("imgs/qp.png") }
|
|
10759
|
-
{ name: "Vidlet", role: "Video edited by", icon: () => siblingAsset("VidLet/res/icons/vidlet-512.png") }
|
|
11242
|
+
{ name: "QuickPeek", role: "Video generated by", icon: () => packageAsset("imgs/qp.png") }
|
|
10760
11243
|
];
|
|
10761
11244
|
function oneTagline(text) {
|
|
10762
11245
|
if (!text) return void 0;
|
|
@@ -10848,7 +11331,7 @@ async function runPlanPhase(params) {
|
|
|
10848
11331
|
const extensionPath = config.browser?.extension;
|
|
10849
11332
|
let existingPlan = null;
|
|
10850
11333
|
let existingPlanPath = null;
|
|
10851
|
-
if ((0,
|
|
11334
|
+
if ((0, import_node_fs26.existsSync)(planPath)) {
|
|
10852
11335
|
try {
|
|
10853
11336
|
const parsed = JSON.parse(await (0, import_promises20.readFile)(planPath, "utf-8"));
|
|
10854
11337
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -10860,10 +11343,10 @@ async function runPlanPhase(params) {
|
|
|
10860
11343
|
}
|
|
10861
11344
|
if (!existingPlan) {
|
|
10862
11345
|
try {
|
|
10863
|
-
const files = (0,
|
|
10864
|
-
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0,
|
|
11346
|
+
const files = (0, import_node_fs26.readdirSync)(config.output.dir);
|
|
11347
|
+
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path32.basename)(planPath));
|
|
10865
11348
|
for (const file of planFiles) {
|
|
10866
|
-
const filePath = (0,
|
|
11349
|
+
const filePath = (0, import_node_path32.join)(config.output.dir, file);
|
|
10867
11350
|
try {
|
|
10868
11351
|
const parsed = JSON.parse(await (0, import_promises20.readFile)(filePath, "utf-8"));
|
|
10869
11352
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -10878,7 +11361,22 @@ async function runPlanPhase(params) {
|
|
|
10878
11361
|
}
|
|
10879
11362
|
}
|
|
10880
11363
|
const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
10881
|
-
const
|
|
11364
|
+
const isBareOrigin = (u) => {
|
|
11365
|
+
try {
|
|
11366
|
+
const parsed = new URL(u);
|
|
11367
|
+
return parsed.pathname === "/" && !parsed.search && !parsed.hash;
|
|
11368
|
+
} catch {
|
|
11369
|
+
return false;
|
|
11370
|
+
}
|
|
11371
|
+
};
|
|
11372
|
+
const sameOrigin = (a, b) => {
|
|
11373
|
+
try {
|
|
11374
|
+
return new URL(a).origin === new URL(b).origin;
|
|
11375
|
+
} catch {
|
|
11376
|
+
return false;
|
|
11377
|
+
}
|
|
11378
|
+
};
|
|
11379
|
+
const urlMatches = existingPlan && (normalizeUrl4(existingPlan.url) === normalizeUrl4(url) || isBareOrigin(existingPlan.url) && sameOrigin(existingPlan.url, url));
|
|
10882
11380
|
if (existingPlan && !urlMatches && isAutoYes()) {
|
|
10883
11381
|
console.log();
|
|
10884
11382
|
console.log(import_chalk18.default.dim(` Existing plan is for ${existingPlan.url}, not ${url}, regenerating.`));
|
|
@@ -11022,7 +11520,7 @@ async function runPlanPhase(params) {
|
|
|
11022
11520
|
const planLang = plan.lang || "en";
|
|
11023
11521
|
if (planLang !== targetLang) {
|
|
11024
11522
|
let useExistingTranslation = false;
|
|
11025
|
-
if ((0,
|
|
11523
|
+
if ((0, import_node_fs26.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
11026
11524
|
try {
|
|
11027
11525
|
const translatedPlan = JSON.parse(await (0, import_promises20.readFile)(planPath, "utf-8"));
|
|
11028
11526
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
@@ -11242,31 +11740,31 @@ async function ensureCliSignIn(url, opts) {
|
|
|
11242
11740
|
}
|
|
11243
11741
|
|
|
11244
11742
|
// src/commands/utils/chrome-extension.ts
|
|
11245
|
-
var
|
|
11743
|
+
var import_node_fs27 = require("fs");
|
|
11246
11744
|
var import_node_os7 = require("os");
|
|
11247
|
-
var
|
|
11745
|
+
var import_node_path33 = require("path");
|
|
11248
11746
|
function resolveChromeExtension(extensionId) {
|
|
11249
11747
|
const platform = process.platform;
|
|
11250
11748
|
const possibleDirs = [];
|
|
11251
11749
|
const getChromeExtDirs = (userDataPath) => {
|
|
11252
11750
|
const dirs = [];
|
|
11253
11751
|
try {
|
|
11254
|
-
const profiles = (0,
|
|
11752
|
+
const profiles = (0, import_node_fs27.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
11255
11753
|
for (const profile of profiles) {
|
|
11256
|
-
dirs.push((0,
|
|
11754
|
+
dirs.push((0, import_node_path33.join)(userDataPath, profile, "Extensions"));
|
|
11257
11755
|
}
|
|
11258
11756
|
} catch {
|
|
11259
|
-
dirs.push((0,
|
|
11757
|
+
dirs.push((0, import_node_path33.join)(userDataPath, "Default", "Extensions"));
|
|
11260
11758
|
}
|
|
11261
11759
|
return dirs;
|
|
11262
11760
|
};
|
|
11263
11761
|
if (platform === "win32") {
|
|
11264
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11762
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
11265
11763
|
} else if (platform === "darwin") {
|
|
11266
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11764
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
11267
11765
|
} else {
|
|
11268
11766
|
try {
|
|
11269
|
-
const windowsUsers = (0,
|
|
11767
|
+
const windowsUsers = (0, import_node_fs27.readdirSync)("/mnt/c/Users").filter(
|
|
11270
11768
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
11271
11769
|
);
|
|
11272
11770
|
for (const user of windowsUsers) {
|
|
@@ -11274,22 +11772,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
11274
11772
|
}
|
|
11275
11773
|
} catch {
|
|
11276
11774
|
}
|
|
11277
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11775
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), ".config", "google-chrome")));
|
|
11278
11776
|
}
|
|
11279
11777
|
for (const chromeExtDir of possibleDirs) {
|
|
11280
|
-
const extDir = (0,
|
|
11281
|
-
if (!(0,
|
|
11778
|
+
const extDir = (0, import_node_path33.join)(chromeExtDir, extensionId);
|
|
11779
|
+
if (!(0, import_node_fs27.existsSync)(extDir)) continue;
|
|
11282
11780
|
try {
|
|
11283
|
-
const versions = (0,
|
|
11781
|
+
const versions = (0, import_node_fs27.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
11284
11782
|
if (versions.length === 0) continue;
|
|
11285
11783
|
const latestVersion = versions.sort().pop();
|
|
11286
|
-
const extPath = (0,
|
|
11784
|
+
const extPath = (0, import_node_path33.join)(extDir, latestVersion);
|
|
11287
11785
|
let name = extensionId;
|
|
11288
11786
|
let popupPath = "popup.html";
|
|
11289
|
-
const manifestPath = (0,
|
|
11290
|
-
if ((0,
|
|
11787
|
+
const manifestPath = (0, import_node_path33.join)(extPath, "manifest.json");
|
|
11788
|
+
if ((0, import_node_fs27.existsSync)(manifestPath)) {
|
|
11291
11789
|
try {
|
|
11292
|
-
const manifest = JSON.parse((0,
|
|
11790
|
+
const manifest = JSON.parse((0, import_node_fs27.readFileSync)(manifestPath, "utf-8"));
|
|
11293
11791
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
11294
11792
|
name = manifest.name;
|
|
11295
11793
|
}
|
|
@@ -11310,9 +11808,9 @@ function resolveChromeExtension(extensionId) {
|
|
|
11310
11808
|
|
|
11311
11809
|
// src/commands/utils/plan-review.ts
|
|
11312
11810
|
var import_node_crypto2 = require("crypto");
|
|
11313
|
-
var
|
|
11811
|
+
var import_node_fs28 = require("fs");
|
|
11314
11812
|
var import_promises22 = require("fs/promises");
|
|
11315
|
-
var
|
|
11813
|
+
var import_node_path34 = require("path");
|
|
11316
11814
|
var REVIEW_FILE = "plan-review.json";
|
|
11317
11815
|
async function hashPlanFile(planPath) {
|
|
11318
11816
|
try {
|
|
@@ -11322,8 +11820,8 @@ async function hashPlanFile(planPath) {
|
|
|
11322
11820
|
}
|
|
11323
11821
|
}
|
|
11324
11822
|
async function loadPlanReview(outputDir) {
|
|
11325
|
-
const path = (0,
|
|
11326
|
-
if (!(0,
|
|
11823
|
+
const path = (0, import_node_path34.join)(outputDir, REVIEW_FILE);
|
|
11824
|
+
if (!(0, import_node_fs28.existsSync)(path)) return {};
|
|
11327
11825
|
try {
|
|
11328
11826
|
return JSON.parse(await (0, import_promises22.readFile)(path, "utf-8"));
|
|
11329
11827
|
} catch {
|
|
@@ -11331,7 +11829,7 @@ async function loadPlanReview(outputDir) {
|
|
|
11331
11829
|
}
|
|
11332
11830
|
}
|
|
11333
11831
|
async function savePlanReview(outputDir, review) {
|
|
11334
|
-
await (0, import_promises22.writeFile)((0,
|
|
11832
|
+
await (0, import_promises22.writeFile)((0, import_node_path34.join)(outputDir, REVIEW_FILE), JSON.stringify(review, null, 2));
|
|
11335
11833
|
}
|
|
11336
11834
|
|
|
11337
11835
|
// src/commands/record-cmd.ts
|
|
@@ -11393,7 +11891,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11393
11891
|
url = startUrl;
|
|
11394
11892
|
}
|
|
11395
11893
|
}
|
|
11396
|
-
config.output.dir = chromeExtension ? (0,
|
|
11894
|
+
config.output.dir = chromeExtension ? (0, import_node_path35.join)(runRoot(), `${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo`) : getOutputDirFromUrl(url);
|
|
11397
11895
|
const userData = await loadUserData();
|
|
11398
11896
|
const authResult = await runAuthPhase({
|
|
11399
11897
|
userEmail: userData.email,
|
|
@@ -11404,7 +11902,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11404
11902
|
if (!authResult) return;
|
|
11405
11903
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
11406
11904
|
if (configCreated) {
|
|
11407
|
-
const configPath = (0,
|
|
11905
|
+
const configPath = (0, import_node_path35.join)(runRoot(), CONFIG_FILE);
|
|
11408
11906
|
console.log();
|
|
11409
11907
|
console.log(import_chalk21.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
11410
11908
|
console.log(import_chalk21.default.dim(` ${configPath}`));
|
|
@@ -11425,17 +11923,17 @@ async function recordCommand(url, options = {}) {
|
|
|
11425
11923
|
}
|
|
11426
11924
|
if (langOverride) config.lang = langOverride;
|
|
11427
11925
|
if (config.music?.path) {
|
|
11428
|
-
const musicPath = (0,
|
|
11429
|
-
if (!(0,
|
|
11926
|
+
const musicPath = (0, import_node_path35.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path35.resolve)(config.output.dir, config.music.path);
|
|
11927
|
+
if (!(0, import_node_fs29.existsSync)(musicPath)) {
|
|
11430
11928
|
console.log();
|
|
11431
11929
|
console.log(import_chalk21.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
11432
|
-
console.log(import_chalk21.default.dim(` Config: ${(0,
|
|
11930
|
+
console.log(import_chalk21.default.dim(` Config: ${(0, import_node_path35.join)(runRoot(), CONFIG_FILE)}`));
|
|
11433
11931
|
console.log();
|
|
11434
11932
|
const proceed = await confirm(`${INDENT2}Continue without background music?`);
|
|
11435
11933
|
if (!proceed) {
|
|
11436
11934
|
console.log(import_chalk21.default.yellow(`
|
|
11437
11935
|
Edit music.path in config and run again.`));
|
|
11438
|
-
console.log(import_chalk21.default.dim(` ${(0,
|
|
11936
|
+
console.log(import_chalk21.default.dim(` ${(0, import_node_path35.join)(runRoot(), CONFIG_FILE)}
|
|
11439
11937
|
`));
|
|
11440
11938
|
process.exit(0);
|
|
11441
11939
|
}
|
|
@@ -11443,9 +11941,9 @@ async function recordCommand(url, options = {}) {
|
|
|
11443
11941
|
}
|
|
11444
11942
|
}
|
|
11445
11943
|
const captions = resolveCaptions(await loadCaptions(), config.video);
|
|
11446
|
-
const workDir = await prepareWorkDir((0,
|
|
11944
|
+
const workDir = await prepareWorkDir((0, import_node_path35.basename)(config.output.dir));
|
|
11447
11945
|
const targetLang = config.lang || "en";
|
|
11448
|
-
const planPath = (0,
|
|
11946
|
+
const planPath = (0, import_node_path35.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
11449
11947
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
11450
11948
|
await (0, import_promises23.mkdir)(config.output.dir, { recursive: true });
|
|
11451
11949
|
await setLogFilePath(config.output.dir);
|
|
@@ -11536,6 +12034,13 @@ async function recordCommand(url, options = {}) {
|
|
|
11536
12034
|
userTier,
|
|
11537
12035
|
userEmail
|
|
11538
12036
|
});
|
|
12037
|
+
await registerDemoRun(userEmail, {
|
|
12038
|
+
source: currentMcpTool2() ? "mcp" : "cli",
|
|
12039
|
+
targetUrl: validatedPlan.url || url,
|
|
12040
|
+
localPath: finalPath,
|
|
12041
|
+
title: validatedPlan.title,
|
|
12042
|
+
...config.lang ? { language: config.lang } : {}
|
|
12043
|
+
});
|
|
11539
12044
|
if (!userEmail || userEmail === "anonymous") {
|
|
11540
12045
|
const newCount = localVideoCount + 1;
|
|
11541
12046
|
await saveUserData({ localVideoCount: newCount });
|
|
@@ -11562,7 +12067,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11562
12067
|
|
|
11563
12068
|
// src/commands/script-cmd.ts
|
|
11564
12069
|
var import_promises24 = require("fs/promises");
|
|
11565
|
-
var
|
|
12070
|
+
var import_node_path36 = require("path");
|
|
11566
12071
|
var import_chalk22 = __toESM(require("chalk"));
|
|
11567
12072
|
init_lib();
|
|
11568
12073
|
async function reusablePlan(planPath, lines) {
|
|
@@ -11694,7 +12199,7 @@ async function scriptCommand(url, scriptPath, options = {}) {
|
|
|
11694
12199
|
);
|
|
11695
12200
|
console.log(import_chalk22.default.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
|
|
11696
12201
|
}
|
|
11697
|
-
const planPath = (0,
|
|
12202
|
+
const planPath = (0, import_node_path36.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
11698
12203
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
11699
12204
|
let plan = await reusablePlan(planPath, lines);
|
|
11700
12205
|
if (plan) {
|
|
@@ -11904,7 +12409,7 @@ function parseArgs(args) {
|
|
|
11904
12409
|
i++;
|
|
11905
12410
|
} else if (arg2 === "--skip-voice" || arg2 === "-sv") {
|
|
11906
12411
|
skipVoice = true;
|
|
11907
|
-
} else if (arg2 === "--draft" || arg2 === "-
|
|
12412
|
+
} else if (arg2 === "--draft" || arg2 === "-dr") {
|
|
11908
12413
|
draft = true;
|
|
11909
12414
|
} else if (arg2 === "--yes" || arg2 === "-y") {
|
|
11910
12415
|
yes = true;
|