@spark-apps/quickpeek 1.2.4 → 1.2.6
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 +64 -22
- package/dist/assets/music/manifest.json +1 -1
- package/dist/index.d.mts +49 -10
- package/dist/index.mjs +215 -143
- package/dist/mcp-tools.mjs +968 -431
- package/dist/qp.js +1014 -473
- package/dist/{voices-CPpnWn39.d.mts → voices-5AFt1GDp.d.mts} +80 -4
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +154 -20
- package/package.json +9 -2
package/dist/qp.js
CHANGED
|
@@ -105,6 +105,181 @@ 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
|
+
|
|
150
|
+
// src/browser/headless.ts
|
|
151
|
+
async function launchHeadlessChromium(options = {}) {
|
|
152
|
+
const opts = { ...options, headless: true };
|
|
153
|
+
try {
|
|
154
|
+
return await import_playwright.chromium.launch(opts);
|
|
155
|
+
} catch (bundledError) {
|
|
156
|
+
try {
|
|
157
|
+
return await import_playwright.chromium.launch({ ...opts, channel: "chrome" });
|
|
158
|
+
} catch {
|
|
159
|
+
const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
|
|
160
|
+
throw new Error(`${NO_BROWSER_MESSAGE}
|
|
161
|
+
|
|
162
|
+
Playwright reported: ${detail}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
var import_playwright, NO_BROWSER_MESSAGE;
|
|
167
|
+
var init_headless = __esm({
|
|
168
|
+
"src/browser/headless.ts"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
import_playwright = require("playwright");
|
|
171
|
+
NO_BROWSER_MESSAGE = "QuickPeek needs a browser to record and could not find one.\n Run this once, then run QuickPeek again:\n npx playwright install chromium";
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/lib/json-repair.ts
|
|
176
|
+
function repairUnicodeEscapes(content) {
|
|
177
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
178
|
+
const code = Number.parseInt(hex, 16);
|
|
179
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
180
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
181
|
+
}
|
|
182
|
+
function salvageTruncatedJson(content) {
|
|
183
|
+
const stack = [];
|
|
184
|
+
const cuts = [];
|
|
185
|
+
let inString = false;
|
|
186
|
+
let escaped = false;
|
|
187
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
188
|
+
for (let i = 0; i < content.length; i++) {
|
|
189
|
+
const ch = content[i];
|
|
190
|
+
if (inString) {
|
|
191
|
+
if (escaped) escaped = false;
|
|
192
|
+
else if (ch === "\\") escaped = true;
|
|
193
|
+
else if (ch === '"') {
|
|
194
|
+
inString = false;
|
|
195
|
+
record(i + 1, false);
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (ch === '"') {
|
|
200
|
+
inString = true;
|
|
201
|
+
} else if (ch === "{" || ch === "[") {
|
|
202
|
+
stack.push(ch);
|
|
203
|
+
} else if (ch === "}" || ch === "]") {
|
|
204
|
+
stack.pop();
|
|
205
|
+
record(i + 1, true);
|
|
206
|
+
} else if (ch === ",") {
|
|
207
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const tryCut = (cut) => {
|
|
211
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
212
|
+
if (head.endsWith(":")) return null;
|
|
213
|
+
let closed = head;
|
|
214
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
215
|
+
closed += CLOSERS[cut.open[d]];
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
JSON.parse(closed);
|
|
219
|
+
return closed;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
225
|
+
for (const containerOnly of [true, false]) {
|
|
226
|
+
for (let n = 0; n < attempts; n++) {
|
|
227
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
228
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
229
|
+
const salvaged = tryCut(cut);
|
|
230
|
+
if (salvaged !== null) return salvaged;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function extractJsonCandidates(content) {
|
|
236
|
+
let text = content.trim();
|
|
237
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
238
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
239
|
+
const start = text.search(/[{[]/);
|
|
240
|
+
if (start === -1) return [];
|
|
241
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
242
|
+
const end = text.lastIndexOf(closer);
|
|
243
|
+
const candidates = [text.slice(start)];
|
|
244
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
245
|
+
return candidates;
|
|
246
|
+
}
|
|
247
|
+
function parseAIJson(content) {
|
|
248
|
+
const candidates = extractJsonCandidates(content);
|
|
249
|
+
if (candidates.length === 0) return null;
|
|
250
|
+
for (const text of candidates) {
|
|
251
|
+
try {
|
|
252
|
+
return { value: JSON.parse(text), repaired: false };
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const text of candidates) {
|
|
257
|
+
try {
|
|
258
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const text of candidates) {
|
|
263
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
264
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
265
|
+
if (salvaged === null) continue;
|
|
266
|
+
try {
|
|
267
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
var MAX_SALVAGE_ATTEMPTS, CLOSERS;
|
|
275
|
+
var init_json_repair = __esm({
|
|
276
|
+
"src/lib/json-repair.ts"() {
|
|
277
|
+
"use strict";
|
|
278
|
+
MAX_SALVAGE_ATTEMPTS = 500;
|
|
279
|
+
CLOSERS = { "{": "}", "[": "]" };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
108
283
|
// src/lib/utils.ts
|
|
109
284
|
function getLanguageName(lang) {
|
|
110
285
|
if (!lang || lang === "en") return null;
|
|
@@ -344,7 +519,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier,
|
|
|
344
519
|
...detail ? { detail } : {},
|
|
345
520
|
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
346
521
|
};
|
|
347
|
-
await new Promise((
|
|
522
|
+
await new Promise((resolve11) => setTimeout(resolve11, Math.min(delayMs, 3e4)));
|
|
348
523
|
continue;
|
|
349
524
|
}
|
|
350
525
|
const retryAfter = response.headers.get("Retry-After");
|
|
@@ -384,7 +559,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier,
|
|
|
384
559
|
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
385
560
|
}
|
|
386
561
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
387
|
-
await new Promise((
|
|
562
|
+
await new Promise((resolve11) => setTimeout(resolve11, delayMs));
|
|
388
563
|
}
|
|
389
564
|
}
|
|
390
565
|
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
@@ -426,24 +601,26 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", op
|
|
|
426
601
|
}
|
|
427
602
|
}
|
|
428
603
|
function parseAIPlanResponse(content) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (codeBlockMatch?.[1]) {
|
|
433
|
-
processedContent = codeBlockMatch[1].trim();
|
|
434
|
-
}
|
|
435
|
-
const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
|
|
436
|
-
if (!jsonMatch) {
|
|
437
|
-
return { success: false, error: "No valid JSON found in AI response" };
|
|
438
|
-
}
|
|
439
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
440
|
-
return { success: true, plan: parsed };
|
|
441
|
-
} catch (err) {
|
|
442
|
-
return {
|
|
443
|
-
success: false,
|
|
444
|
-
error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
|
|
445
|
-
};
|
|
604
|
+
const parsed = parseAIJson(content);
|
|
605
|
+
if (!parsed) {
|
|
606
|
+
return { success: false, error: "No valid JSON found in AI response" };
|
|
446
607
|
}
|
|
608
|
+
if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
|
|
609
|
+
return { success: false, error: "AI response has no steps" };
|
|
610
|
+
}
|
|
611
|
+
return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
|
|
612
|
+
}
|
|
613
|
+
async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
614
|
+
let lastParseError = "No valid JSON found in AI response";
|
|
615
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
616
|
+
const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
|
|
617
|
+
if (!aiResult.success) return aiResult;
|
|
618
|
+
const parsed = parseAIPlanResponse(aiResult.content);
|
|
619
|
+
if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
|
|
620
|
+
lastParseError = parsed.error;
|
|
621
|
+
console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
|
|
622
|
+
}
|
|
623
|
+
return { success: false, error: lastParseError, isParseError: true };
|
|
447
624
|
}
|
|
448
625
|
function normalizeUrl(input) {
|
|
449
626
|
let url = input.trim();
|
|
@@ -454,6 +631,7 @@ var PLAN_MAX_TOKENS, SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_
|
|
|
454
631
|
var init_api_client = __esm({
|
|
455
632
|
"src/lib/api-client.ts"() {
|
|
456
633
|
"use strict";
|
|
634
|
+
init_json_repair();
|
|
457
635
|
init_utils();
|
|
458
636
|
PLAN_MAX_TOKENS = 1500;
|
|
459
637
|
SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
@@ -546,9 +724,6 @@ var init_SparkStripe_feature_gating = __esm({
|
|
|
546
724
|
});
|
|
547
725
|
|
|
548
726
|
// src/lib/billing.ts
|
|
549
|
-
function shouldWatermark(tier, videoDurationSeconds) {
|
|
550
|
-
return tier === "free" && videoDurationSeconds > 20;
|
|
551
|
-
}
|
|
552
727
|
function getUpgradeUrl(userEmail) {
|
|
553
728
|
return pricingUrl(userEmail ? { email: userEmail } : void 0);
|
|
554
729
|
}
|
|
@@ -583,7 +758,10 @@ function applyProfile(config, profile) {
|
|
|
583
758
|
format: "mp3",
|
|
584
759
|
bitrate: "192k",
|
|
585
760
|
...config.audio,
|
|
586
|
-
|
|
761
|
+
// outroOnly is on unless turned off, matching the video phase. The
|
|
762
|
+
// speedup buys the song its window, so it applies to the profile's own
|
|
763
|
+
// pace only: a speed somebody set by hand is the speed they wanted.
|
|
764
|
+
speed: config.audio?.speed ?? preset.audioSpeed * (config.music?.outroOnly !== false ? OUTRO_SONG_SPEEDUP : 1),
|
|
587
765
|
...config.audio?.voice || !preset.voice ? {} : { voice: preset.voice }
|
|
588
766
|
}
|
|
589
767
|
};
|
|
@@ -620,11 +798,11 @@ function defaultCaptionsFor(width, height) {
|
|
|
620
798
|
maxChars: 20
|
|
621
799
|
};
|
|
622
800
|
}
|
|
623
|
-
var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONFIG, VIDEO_PROFILES, DEFAULT_CAPTIONS;
|
|
801
|
+
var VERSION, CONFIG_FILE, SIZE_PRESETS, RATE_FACTOR, NO_CLIP_OUTRO, DEFAULT_CONFIG, VIDEO_PROFILES, OUTRO_SONG_SPEEDUP, DEFAULT_CAPTIONS;
|
|
624
802
|
var init_config = __esm({
|
|
625
803
|
"src/lib/config.ts"() {
|
|
626
804
|
"use strict";
|
|
627
|
-
VERSION = "1.2.
|
|
805
|
+
VERSION = "1.2.6";
|
|
628
806
|
CONFIG_FILE = "quickpeek.config.json";
|
|
629
807
|
SIZE_PRESETS = {
|
|
630
808
|
wide: { width: 1920, height: 1080, zoom: 1 },
|
|
@@ -651,11 +829,12 @@ var init_config = __esm({
|
|
|
651
829
|
// ms
|
|
652
830
|
transitions: "none",
|
|
653
831
|
contrast: 0.25,
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
tempo
|
|
832
|
+
// The recording plays at the pace it was filmed at. A tempo above 1 is
|
|
833
|
+
// multiplied by the narration speed in VIDEO_PROFILES and by the fit
|
|
834
|
+
// pass's squeeze, and the three together read as frantic rather than
|
|
835
|
+
// brisk. Speed belongs in one place, so it lives in audioSpeed; set a
|
|
836
|
+
// tempo in quickpeek.config.json to push the picture on top of that.
|
|
837
|
+
tempo: 1,
|
|
659
838
|
stepDelay: 200
|
|
660
839
|
// ms delay between steps
|
|
661
840
|
},
|
|
@@ -669,18 +848,24 @@ var init_config = __esm({
|
|
|
669
848
|
// these bitrates - the encode is pure wall-clock tax on every run.
|
|
670
849
|
wide: {
|
|
671
850
|
video: { width: 1920, height: 1080, zoom: 1.5, fitLimitSecs: 0, maxSpeedup: 1, preset: "veryfast" },
|
|
672
|
-
|
|
851
|
+
// A tutorial is watched to learn something, so the narrator is allowed to
|
|
852
|
+
// be brisk but never hurried.
|
|
853
|
+
audioSpeed: 1.15
|
|
673
854
|
},
|
|
674
855
|
short: {
|
|
675
856
|
// fadeOut 0: the video ends on a card the viewer is meant to read,
|
|
676
857
|
// and a fade takes it away mid-sentence.
|
|
677
858
|
video: { width: 1080, height: 1920, zoom: 3, fitLimitSecs: 60, maxSpeedup: 1.7, preset: "veryfast", fadeOut: 0 },
|
|
678
|
-
|
|
859
|
+
// 1.8 was unwatchable: stacked on tempo 1.2 and the outro speedup it ran
|
|
860
|
+
// the voice at better than twice its natural rate. A short wants energy,
|
|
861
|
+
// which is the voice and the cutting, not the tape running fast.
|
|
862
|
+
audioSpeed: 1,
|
|
679
863
|
// A short lives or dies on energy. Ava is the warmest, most animated of
|
|
680
864
|
// the free neural voices; Sonia reads a short like the shipping forecast.
|
|
681
865
|
voice: "en-US-AvaMultilingualNeural"
|
|
682
866
|
}
|
|
683
867
|
};
|
|
868
|
+
OUTRO_SONG_SPEEDUP = 1.13;
|
|
684
869
|
DEFAULT_CAPTIONS = {
|
|
685
870
|
font: "Arial",
|
|
686
871
|
outline: "#000000",
|
|
@@ -829,6 +1014,7 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
829
1014
|
const deadline = Date.now() + cap;
|
|
830
1015
|
let previous = -1;
|
|
831
1016
|
let unchanged = 0;
|
|
1017
|
+
let clear = 0;
|
|
832
1018
|
while (Date.now() < deadline) {
|
|
833
1019
|
const count = await page.evaluate((selector) => {
|
|
834
1020
|
let visible = 0;
|
|
@@ -838,7 +1024,12 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
838
1024
|
}
|
|
839
1025
|
return visible;
|
|
840
1026
|
}, PLACEHOLDER_SELECTOR).catch(() => 0);
|
|
841
|
-
if (count === 0)
|
|
1027
|
+
if (count === 0) {
|
|
1028
|
+
if (++clear >= PLACEHOLDER_CLEAR_POLLS) return;
|
|
1029
|
+
await page.waitForTimeout(PLACEHOLDER_POLL_MS);
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
clear = 0;
|
|
842
1033
|
if (count === previous) {
|
|
843
1034
|
if (++unchanged >= PLACEHOLDER_STATIC_POLLS) return;
|
|
844
1035
|
} else {
|
|
@@ -922,7 +1113,13 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
922
1113
|
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
923
1114
|
}
|
|
924
1115
|
const browserTask = task2("Launching browser");
|
|
925
|
-
|
|
1116
|
+
let browser;
|
|
1117
|
+
try {
|
|
1118
|
+
browser = await launchHeadlessChromium();
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
browserTask.succeed("Could not launch a browser");
|
|
1121
|
+
throw err;
|
|
1122
|
+
}
|
|
926
1123
|
try {
|
|
927
1124
|
const context = await browser.newContext({
|
|
928
1125
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
@@ -1098,11 +1295,11 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
1098
1295
|
});
|
|
1099
1296
|
}
|
|
1100
1297
|
}
|
|
1101
|
-
var
|
|
1298
|
+
var 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
1299
|
var init_crawl = __esm({
|
|
1103
1300
|
"src/lib/crawl.ts"() {
|
|
1104
1301
|
"use strict";
|
|
1105
|
-
|
|
1302
|
+
init_headless();
|
|
1106
1303
|
init_scraping();
|
|
1107
1304
|
SETTLE_POLL_MS = 400;
|
|
1108
1305
|
SETTLE_STABLE_POLLS = 2;
|
|
@@ -1110,6 +1307,7 @@ var init_crawl = __esm({
|
|
|
1110
1307
|
PLACEHOLDER_POLL_MS = 250;
|
|
1111
1308
|
PLACEHOLDER_CAP_MS = 8e3;
|
|
1112
1309
|
PLACEHOLDER_STATIC_POLLS = 4;
|
|
1310
|
+
PLACEHOLDER_CLEAR_POLLS = 3;
|
|
1113
1311
|
PLACEHOLDER_SELECTOR = [
|
|
1114
1312
|
'[aria-busy="true"]',
|
|
1115
1313
|
'[role="progressbar"]',
|
|
@@ -1259,13 +1457,13 @@ function canEncode(encoder) {
|
|
|
1259
1457
|
]);
|
|
1260
1458
|
}
|
|
1261
1459
|
function run(args) {
|
|
1262
|
-
return new Promise((
|
|
1460
|
+
return new Promise((resolve11) => {
|
|
1263
1461
|
let settled = false;
|
|
1264
1462
|
const done = (ok) => {
|
|
1265
1463
|
if (settled) return;
|
|
1266
1464
|
settled = true;
|
|
1267
1465
|
clearTimeout(timer);
|
|
1268
|
-
|
|
1466
|
+
resolve11(ok);
|
|
1269
1467
|
};
|
|
1270
1468
|
const proc = trackChild((0, import_node_child_process2.spawn)("ffmpeg", args, { stdio: "ignore" }));
|
|
1271
1469
|
const timer = setTimeout(() => {
|
|
@@ -1332,7 +1530,7 @@ function spawnFFmpeg(cmd2, args) {
|
|
|
1332
1530
|
return trackChild((0, import_node_child_process3.spawn)(cmd2, args));
|
|
1333
1531
|
}
|
|
1334
1532
|
function runFFmpeg(args, errorLabel) {
|
|
1335
|
-
return new Promise((
|
|
1533
|
+
return new Promise((resolve11, reject) => {
|
|
1336
1534
|
const proc = spawnFFmpeg("ffmpeg", args);
|
|
1337
1535
|
let stderr = "";
|
|
1338
1536
|
proc.stderr.on("data", (d) => {
|
|
@@ -1340,7 +1538,7 @@ function runFFmpeg(args, errorLabel) {
|
|
|
1340
1538
|
});
|
|
1341
1539
|
proc.on("close", (code) => {
|
|
1342
1540
|
if (code !== 0) reject(new Error(`${errorLabel}: ${stderr.slice(-500)}`));
|
|
1343
|
-
else
|
|
1541
|
+
else resolve11();
|
|
1344
1542
|
});
|
|
1345
1543
|
proc.on("error", reject);
|
|
1346
1544
|
});
|
|
@@ -1367,8 +1565,13 @@ async function composeVideo(opts) {
|
|
|
1367
1565
|
musicPath,
|
|
1368
1566
|
musicVolume = 0.3,
|
|
1369
1567
|
musicTargetLufs = MUSIC_BED_LUFS,
|
|
1370
|
-
|
|
1371
|
-
|
|
1568
|
+
// Sized for the outro window: the song has 5.5s, so a 2s fade at each end
|
|
1569
|
+
// left 1.5s of music anyone could actually hear. Up fast, hold, out slow.
|
|
1570
|
+
musicFadeIn = 1,
|
|
1571
|
+
musicFadeOut = 1.5,
|
|
1572
|
+
tempo = 1,
|
|
1573
|
+
musicStartSecs = 0,
|
|
1574
|
+
musicDurationSecs = 0,
|
|
1372
1575
|
width,
|
|
1373
1576
|
height,
|
|
1374
1577
|
fps,
|
|
@@ -1429,7 +1632,19 @@ async function composeVideo(opts) {
|
|
|
1429
1632
|
const clipAudioLabel = opts.hasClipAudio ? "[0:a]" : "";
|
|
1430
1633
|
const hasVoiceover = !!voiceoverPath;
|
|
1431
1634
|
const hasMusic = !!musicPath;
|
|
1432
|
-
const
|
|
1635
|
+
const bedStart = Math.max(0, Math.min(musicStartSecs, Math.max(0, effectiveDuration - 0.5)));
|
|
1636
|
+
const bedLength = Math.max(0.5, effectiveDuration - bedStart);
|
|
1637
|
+
const bedDelay = bedStart > 0 ? `adelay=${Math.round(bedStart * 1e3)}|${Math.round(bedStart * 1e3)},` : "";
|
|
1638
|
+
const bedTempo = tempo > 1 ? 1 / tempo : 1;
|
|
1639
|
+
const bedSlow = bedTempo === 1 ? "" : `atempo=${bedTempo.toFixed(6)},`;
|
|
1640
|
+
const bedSource = bedLength * bedTempo;
|
|
1641
|
+
const bedSeek = musicDurationSecs > bedSource + 0.05 ? musicDurationSecs - bedSource : 0;
|
|
1642
|
+
const bedTrimIn = bedSeek > 0 ? `atrim=start=${bedSeek.toFixed(3)},asetpts=PTS-STARTPTS,` : "";
|
|
1643
|
+
const bedFadeOut = bedSeek > 0 ? 0 : musicFadeOut * tempo;
|
|
1644
|
+
const bedLoop = bedSeek > 0 ? "" : "aloop=loop=-1:size=2e+09,";
|
|
1645
|
+
const bedFadeIn = musicFadeIn * tempo;
|
|
1646
|
+
const musicFadeOutStart = Math.max(0, bedLength - bedFadeOut);
|
|
1647
|
+
const bedFadeOutFilter = bedFadeOut > 0 ? `afade=t=out:st=${musicFadeOutStart}:d=${bedFadeOut},` : "";
|
|
1433
1648
|
const vfStr = [...vf, ...encoder.filter ? [encoder.filter] : []].join(",");
|
|
1434
1649
|
if (hasVoiceover && hasMusic) {
|
|
1435
1650
|
const mixInputs = clipAudioLabel ? 3 : 2;
|
|
@@ -1445,7 +1660,7 @@ async function composeVideo(opts) {
|
|
|
1445
1660
|
// card, which is the one place the bed is the only thing playing.
|
|
1446
1661
|
`[voicekey0]apad=whole_dur=${effectiveDuration}[voicekey]`,
|
|
1447
1662
|
// Music: normalised to a known level, trimmed, then faded.
|
|
1448
|
-
`[2:a]
|
|
1663
|
+
`[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
1664
|
`[bed][voicekey]${DUCK}[music]`,
|
|
1450
1665
|
// normalize=0, because amix's default divides every input by the number
|
|
1451
1666
|
// of them: adding the bed used to quietly take 6 dB off the narration,
|
|
@@ -1469,7 +1684,7 @@ async function composeVideo(opts) {
|
|
|
1469
1684
|
args.push(...encoder.output);
|
|
1470
1685
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
1471
1686
|
} else if (hasMusic) {
|
|
1472
|
-
const musicFilter =
|
|
1687
|
+
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
1688
|
args.push("-vf", vfStr);
|
|
1474
1689
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
1475
1690
|
args.push(...encoder.output);
|
|
@@ -1594,14 +1809,14 @@ var init_gif = __esm({
|
|
|
1594
1809
|
// src/lib/ffmpeg/media.ts
|
|
1595
1810
|
function cacheKey(filePath) {
|
|
1596
1811
|
try {
|
|
1597
|
-
const { size, mtimeMs } = (0,
|
|
1812
|
+
const { size, mtimeMs } = (0, import_node_fs5.statSync)(filePath);
|
|
1598
1813
|
return `${filePath}:${size}:${mtimeMs}`;
|
|
1599
1814
|
} catch {
|
|
1600
1815
|
return null;
|
|
1601
1816
|
}
|
|
1602
1817
|
}
|
|
1603
1818
|
function getVideoDimensions(filePath) {
|
|
1604
|
-
return new Promise((
|
|
1819
|
+
return new Promise((resolve11, reject) => {
|
|
1605
1820
|
const args = [
|
|
1606
1821
|
"-v",
|
|
1607
1822
|
"error",
|
|
@@ -1632,12 +1847,12 @@ function getVideoDimensions(filePath) {
|
|
|
1632
1847
|
reject(new Error(`ffprobe returned no dimensions for ${filePath}`));
|
|
1633
1848
|
return;
|
|
1634
1849
|
}
|
|
1635
|
-
|
|
1850
|
+
resolve11({ width: w, height: h });
|
|
1636
1851
|
});
|
|
1637
1852
|
});
|
|
1638
1853
|
}
|
|
1639
1854
|
function hasAudioStream(filePath) {
|
|
1640
|
-
return new Promise((
|
|
1855
|
+
return new Promise((resolve11) => {
|
|
1641
1856
|
const proc = spawnFFmpeg("ffprobe", [
|
|
1642
1857
|
"-v",
|
|
1643
1858
|
"error",
|
|
@@ -1653,15 +1868,15 @@ function hasAudioStream(filePath) {
|
|
|
1653
1868
|
proc.stdout.on("data", (d) => {
|
|
1654
1869
|
out += d;
|
|
1655
1870
|
});
|
|
1656
|
-
proc.on("close", () =>
|
|
1657
|
-
proc.on("error", () =>
|
|
1871
|
+
proc.on("close", () => resolve11(out.trim().length > 0));
|
|
1872
|
+
proc.on("error", () => resolve11(false));
|
|
1658
1873
|
});
|
|
1659
1874
|
}
|
|
1660
1875
|
function getMediaDuration(filePath) {
|
|
1661
1876
|
const key = cacheKey(filePath);
|
|
1662
1877
|
const cached = key === null ? void 0 : durationCache.get(key);
|
|
1663
1878
|
if (cached !== void 0) return Promise.resolve(cached);
|
|
1664
|
-
return new Promise((
|
|
1879
|
+
return new Promise((resolve11, reject) => {
|
|
1665
1880
|
const args = [
|
|
1666
1881
|
"-v",
|
|
1667
1882
|
"error",
|
|
@@ -1688,13 +1903,13 @@ function getMediaDuration(filePath) {
|
|
|
1688
1903
|
const ms = Math.ceil(parseFloat(stdout.trim()) * 1e3);
|
|
1689
1904
|
const freshKey = cacheKey(filePath);
|
|
1690
1905
|
if (freshKey !== null && Number.isFinite(ms)) durationCache.set(freshKey, ms);
|
|
1691
|
-
|
|
1906
|
+
resolve11(ms);
|
|
1692
1907
|
});
|
|
1693
1908
|
proc.on("error", reject);
|
|
1694
1909
|
});
|
|
1695
1910
|
}
|
|
1696
1911
|
function getAudioCodec(filePath) {
|
|
1697
|
-
return new Promise((
|
|
1912
|
+
return new Promise((resolve11) => {
|
|
1698
1913
|
const args = [
|
|
1699
1914
|
"-v",
|
|
1700
1915
|
"error",
|
|
@@ -1711,8 +1926,8 @@ function getAudioCodec(filePath) {
|
|
|
1711
1926
|
proc.stdout.on("data", (data) => {
|
|
1712
1927
|
stdout += data;
|
|
1713
1928
|
});
|
|
1714
|
-
proc.on("close", (code) =>
|
|
1715
|
-
proc.on("error", () =>
|
|
1929
|
+
proc.on("close", (code) => resolve11(code === 0 ? stdout.trim() : ""));
|
|
1930
|
+
proc.on("error", () => resolve11(""));
|
|
1716
1931
|
});
|
|
1717
1932
|
}
|
|
1718
1933
|
async function isHonestWav(filePath) {
|
|
@@ -1854,11 +2069,11 @@ function concatVideoWithFullFrameOutro(opts) {
|
|
|
1854
2069
|
"ffmpeg full-frame outro concat failed"
|
|
1855
2070
|
);
|
|
1856
2071
|
}
|
|
1857
|
-
var
|
|
2072
|
+
var import_node_fs5, durationCache, TRIM_EDGE_SILENCE;
|
|
1858
2073
|
var init_media = __esm({
|
|
1859
2074
|
"src/lib/ffmpeg/media.ts"() {
|
|
1860
2075
|
"use strict";
|
|
1861
|
-
|
|
2076
|
+
import_node_fs5 = require("fs");
|
|
1862
2077
|
init_spawn();
|
|
1863
2078
|
durationCache = /* @__PURE__ */ new Map();
|
|
1864
2079
|
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 +2082,7 @@ var init_media = __esm({
|
|
|
1867
2082
|
|
|
1868
2083
|
// src/lib/ffmpeg/silence.ts
|
|
1869
2084
|
function getVideoStreamDuration(inputPath) {
|
|
1870
|
-
return new Promise((
|
|
2085
|
+
return new Promise((resolve11) => {
|
|
1871
2086
|
const proc = spawnFFmpeg("ffprobe", [
|
|
1872
2087
|
"-v",
|
|
1873
2088
|
"error",
|
|
@@ -1884,12 +2099,12 @@ function getVideoStreamDuration(inputPath) {
|
|
|
1884
2099
|
proc.stdout.on("data", (d) => {
|
|
1885
2100
|
out += d;
|
|
1886
2101
|
});
|
|
1887
|
-
proc.on("error", () =>
|
|
1888
|
-
proc.on("close", () =>
|
|
2102
|
+
proc.on("error", () => resolve11(0));
|
|
2103
|
+
proc.on("close", () => resolve11(Number.parseFloat(out.trim()) || 0));
|
|
1889
2104
|
});
|
|
1890
2105
|
}
|
|
1891
2106
|
function detectSilences(inputPath, minDurSecs) {
|
|
1892
|
-
return new Promise((
|
|
2107
|
+
return new Promise((resolve11) => {
|
|
1893
2108
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1894
2109
|
"-hide_banner",
|
|
1895
2110
|
"-nostats",
|
|
@@ -1908,7 +2123,7 @@ function detectSilences(inputPath, minDurSecs) {
|
|
|
1908
2123
|
proc.stderr.on("data", (d) => {
|
|
1909
2124
|
err += d;
|
|
1910
2125
|
});
|
|
1911
|
-
proc.on("error", () =>
|
|
2126
|
+
proc.on("error", () => resolve11([]));
|
|
1912
2127
|
proc.on("close", () => {
|
|
1913
2128
|
const spans = [];
|
|
1914
2129
|
let start = null;
|
|
@@ -1923,12 +2138,12 @@ function detectSilences(inputPath, minDurSecs) {
|
|
|
1923
2138
|
start = null;
|
|
1924
2139
|
}
|
|
1925
2140
|
}
|
|
1926
|
-
|
|
2141
|
+
resolve11(spans);
|
|
1927
2142
|
});
|
|
1928
2143
|
});
|
|
1929
2144
|
}
|
|
1930
2145
|
function detectFrozenSpans(inputPath, minDurSecs) {
|
|
1931
|
-
return new Promise((
|
|
2146
|
+
return new Promise((resolve11) => {
|
|
1932
2147
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1933
2148
|
"-hide_banner",
|
|
1934
2149
|
"-nostats",
|
|
@@ -1949,7 +2164,7 @@ function detectFrozenSpans(inputPath, minDurSecs) {
|
|
|
1949
2164
|
proc.stderr.on("data", (d) => {
|
|
1950
2165
|
err += d;
|
|
1951
2166
|
});
|
|
1952
|
-
proc.on("error", () =>
|
|
2167
|
+
proc.on("error", () => resolve11([]));
|
|
1953
2168
|
proc.on("close", () => {
|
|
1954
2169
|
const spans = [];
|
|
1955
2170
|
let start = null;
|
|
@@ -1962,12 +2177,12 @@ function detectFrozenSpans(inputPath, minDurSecs) {
|
|
|
1962
2177
|
start = null;
|
|
1963
2178
|
}
|
|
1964
2179
|
}
|
|
1965
|
-
|
|
2180
|
+
resolve11(spans);
|
|
1966
2181
|
});
|
|
1967
2182
|
});
|
|
1968
2183
|
}
|
|
1969
2184
|
function detectBlackSpans(inputPath, minDurSecs) {
|
|
1970
|
-
return new Promise((
|
|
2185
|
+
return new Promise((resolve11) => {
|
|
1971
2186
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
1972
2187
|
"-hide_banner",
|
|
1973
2188
|
"-nostats",
|
|
@@ -1990,19 +2205,19 @@ function detectBlackSpans(inputPath, minDurSecs) {
|
|
|
1990
2205
|
proc.stderr.on("data", (d) => {
|
|
1991
2206
|
err += d;
|
|
1992
2207
|
});
|
|
1993
|
-
proc.on("error", () =>
|
|
2208
|
+
proc.on("error", () => resolve11([]));
|
|
1994
2209
|
proc.on("close", () => {
|
|
1995
2210
|
const spans = [];
|
|
1996
2211
|
for (const line of err.split("\n")) {
|
|
1997
2212
|
const m = line.match(/black_start:\s*([\d.]+)\s+black_end:\s*([\d.]+)/);
|
|
1998
2213
|
if (m?.[1] && m[2]) spans.push({ start: Number.parseFloat(m[1]), end: Number.parseFloat(m[2]) });
|
|
1999
2214
|
}
|
|
2000
|
-
|
|
2215
|
+
resolve11(spans);
|
|
2001
2216
|
});
|
|
2002
2217
|
});
|
|
2003
2218
|
}
|
|
2004
2219
|
function detectWhiteSpans(inputPath, minDurSecs) {
|
|
2005
|
-
return new Promise((
|
|
2220
|
+
return new Promise((resolve11) => {
|
|
2006
2221
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2007
2222
|
"-hide_banner",
|
|
2008
2223
|
"-nostats",
|
|
@@ -2020,14 +2235,14 @@ function detectWhiteSpans(inputPath, minDurSecs) {
|
|
|
2020
2235
|
proc.stderr.on("data", (d) => {
|
|
2021
2236
|
err += d;
|
|
2022
2237
|
});
|
|
2023
|
-
proc.on("error", () =>
|
|
2238
|
+
proc.on("error", () => resolve11([]));
|
|
2024
2239
|
proc.on("close", () => {
|
|
2025
2240
|
const spans = [];
|
|
2026
2241
|
for (const line of err.split("\n")) {
|
|
2027
2242
|
const m = line.match(/black_start:\s*([\d.]+)\s+black_end:\s*([\d.]+)/);
|
|
2028
2243
|
if (m?.[1] && m[2]) spans.push({ start: Number.parseFloat(m[1]), end: Number.parseFloat(m[2]) });
|
|
2029
2244
|
}
|
|
2030
|
-
|
|
2245
|
+
resolve11(spans);
|
|
2031
2246
|
});
|
|
2032
2247
|
});
|
|
2033
2248
|
}
|
|
@@ -2117,7 +2332,7 @@ async function cutSpansFromVideo(inputPath, outputPath, spans, preset) {
|
|
|
2117
2332
|
const drop = keep.map((s) => `between(t,${s.start.toFixed(3)},${s.end.toFixed(3)})`).join("+");
|
|
2118
2333
|
const filter = `[0:v]select='not(${drop})',setpts=N/FRAME_RATE/TB[outv]`;
|
|
2119
2334
|
const enc = await passEncoder(preset);
|
|
2120
|
-
return new Promise((
|
|
2335
|
+
return new Promise((resolve11) => {
|
|
2121
2336
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2122
2337
|
"-y",
|
|
2123
2338
|
...filterThreadArgs(),
|
|
@@ -2135,8 +2350,8 @@ async function cutSpansFromVideo(inputPath, outputPath, spans, preset) {
|
|
|
2135
2350
|
outputPath
|
|
2136
2351
|
]);
|
|
2137
2352
|
proc.stderr.resume();
|
|
2138
|
-
proc.on("error", () =>
|
|
2139
|
-
proc.on("close", (code) =>
|
|
2353
|
+
proc.on("error", () => resolve11(false));
|
|
2354
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2140
2355
|
});
|
|
2141
2356
|
}
|
|
2142
2357
|
function buildRetimeFilter(spans, stepBounds, speed, fps) {
|
|
@@ -2218,7 +2433,7 @@ async function retimeSpansInVideo(inputPath, outputPath, spans, stepBounds, spee
|
|
|
2218
2433
|
const filter = buildRetimeFilter(spans, stepBounds, speed, fps);
|
|
2219
2434
|
if (!filter) return false;
|
|
2220
2435
|
const enc = await passEncoder(preset);
|
|
2221
|
-
return new Promise((
|
|
2436
|
+
return new Promise((resolve11) => {
|
|
2222
2437
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2223
2438
|
"-y",
|
|
2224
2439
|
...filterThreadArgs(),
|
|
@@ -2236,8 +2451,8 @@ async function retimeSpansInVideo(inputPath, outputPath, spans, stepBounds, spee
|
|
|
2236
2451
|
outputPath
|
|
2237
2452
|
]);
|
|
2238
2453
|
proc.stderr.resume();
|
|
2239
|
-
proc.on("error", () =>
|
|
2240
|
-
proc.on("close", (code) =>
|
|
2454
|
+
proc.on("error", () => resolve11(false));
|
|
2455
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2241
2456
|
});
|
|
2242
2457
|
}
|
|
2243
2458
|
async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, preset, protectAfterSecs, narrationPath, protectBeforeSecs, leadSpans, keepSpans) {
|
|
@@ -2263,7 +2478,7 @@ async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, pr
|
|
|
2263
2478
|
const filter = buildCompressionFilter(spans, totalSecs, maxSecs, protectAfterSecs, protectBeforeSecs);
|
|
2264
2479
|
if (!filter) return false;
|
|
2265
2480
|
const enc = await passEncoder(preset);
|
|
2266
|
-
return new Promise((
|
|
2481
|
+
return new Promise((resolve11) => {
|
|
2267
2482
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2268
2483
|
"-y",
|
|
2269
2484
|
...filterThreadArgs(),
|
|
@@ -2289,8 +2504,8 @@ async function compressSilentSpans(inputPath, outputPath, totalSecs, maxSecs, pr
|
|
|
2289
2504
|
outputPath
|
|
2290
2505
|
]);
|
|
2291
2506
|
proc.stderr.resume();
|
|
2292
|
-
proc.on("error", () =>
|
|
2293
|
-
proc.on("close", (code) =>
|
|
2507
|
+
proc.on("error", () => resolve11(false));
|
|
2508
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2294
2509
|
});
|
|
2295
2510
|
}
|
|
2296
2511
|
async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, introSecs, outroSecs, maxFactor, preset) {
|
|
@@ -2314,7 +2529,7 @@ async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, intro
|
|
|
2314
2529
|
"[v0][a0][v1][a1][v2][a2]concat=n=3:v=1:a=1[outv][outa]"
|
|
2315
2530
|
].join(";");
|
|
2316
2531
|
const enc = await passEncoder(preset);
|
|
2317
|
-
const ok = await new Promise((
|
|
2532
|
+
const ok = await new Promise((resolve11) => {
|
|
2318
2533
|
const proc = spawnFFmpeg("ffmpeg", [
|
|
2319
2534
|
"-y",
|
|
2320
2535
|
...filterThreadArgs(),
|
|
@@ -2335,8 +2550,8 @@ async function speedUpMiddle(inputPath, outputPath, totalSecs, targetSecs, intro
|
|
|
2335
2550
|
outputPath
|
|
2336
2551
|
]);
|
|
2337
2552
|
proc.stderr.resume();
|
|
2338
|
-
proc.on("error", () =>
|
|
2339
|
-
proc.on("close", (code) =>
|
|
2553
|
+
proc.on("error", () => resolve11(false));
|
|
2554
|
+
proc.on("close", (code) => resolve11(code === 0));
|
|
2340
2555
|
});
|
|
2341
2556
|
return { ok, factor, reachedTarget: needed <= maxFactor };
|
|
2342
2557
|
}
|
|
@@ -2372,7 +2587,7 @@ async function spliceVideos(opts) {
|
|
|
2372
2587
|
const audioPresent = await Promise.all(
|
|
2373
2588
|
overlays.map((o) => o.still ? Promise.resolve(false) : hasAudioStream(o.path))
|
|
2374
2589
|
);
|
|
2375
|
-
return new Promise((
|
|
2590
|
+
return new Promise((resolve11, reject) => {
|
|
2376
2591
|
const args = ["-y", "-i", inputPath];
|
|
2377
2592
|
for (const overlay of overlays) {
|
|
2378
2593
|
if (overlay.still) {
|
|
@@ -2435,7 +2650,7 @@ async function spliceVideos(opts) {
|
|
|
2435
2650
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", "18", "-pix_fmt", "yuv420p");
|
|
2436
2651
|
if (clipAudio.length > 0) args.push("-c:a", "aac");
|
|
2437
2652
|
args.push(outputPath);
|
|
2438
|
-
runFFmpeg(args, "ffmpeg splice failed").then(
|
|
2653
|
+
runFFmpeg(args, "ffmpeg splice failed").then(resolve11, reject);
|
|
2439
2654
|
});
|
|
2440
2655
|
}
|
|
2441
2656
|
var CLIP_AUDIO_GAIN, STILL_FRAMERATE, COVER_WASH_W, COVER_WASH_H, COVER_BLUR_SIGMA;
|
|
@@ -2471,10 +2686,10 @@ function buildStepTransitions(steps, stepDurations, defaultTransition) {
|
|
|
2471
2686
|
return transitions;
|
|
2472
2687
|
}
|
|
2473
2688
|
function applyTransitions(opts) {
|
|
2474
|
-
return new Promise((
|
|
2689
|
+
return new Promise((resolve11, reject) => {
|
|
2475
2690
|
const { inputVideo, outputVideo, transitions, videoDuration, preset = "fast" } = opts;
|
|
2476
2691
|
if (transitions.length === 0) {
|
|
2477
|
-
runFFmpeg(["-y", "-i", inputVideo, "-c", "copy", outputVideo], "FFmpeg copy failed").then(
|
|
2692
|
+
runFFmpeg(["-y", "-i", inputVideo, "-c", "copy", outputVideo], "FFmpeg copy failed").then(resolve11, reject);
|
|
2478
2693
|
return;
|
|
2479
2694
|
}
|
|
2480
2695
|
const sorted = [...transitions].sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -2522,7 +2737,7 @@ function applyTransitions(opts) {
|
|
|
2522
2737
|
"yuv420p",
|
|
2523
2738
|
outputVideo
|
|
2524
2739
|
];
|
|
2525
|
-
runFFmpeg(args, "FFmpeg transitions failed").then(
|
|
2740
|
+
runFFmpeg(args, "FFmpeg transitions failed").then(resolve11, reject);
|
|
2526
2741
|
});
|
|
2527
2742
|
}
|
|
2528
2743
|
var init_transitions = __esm({
|
|
@@ -2562,114 +2777,6 @@ var init_ffmpeg2 = __esm({
|
|
|
2562
2777
|
}
|
|
2563
2778
|
});
|
|
2564
2779
|
|
|
2565
|
-
// src/lib/json-repair.ts
|
|
2566
|
-
function repairUnicodeEscapes(content) {
|
|
2567
|
-
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
2568
|
-
const code = Number.parseInt(hex, 16);
|
|
2569
|
-
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
2570
|
-
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
2571
|
-
}
|
|
2572
|
-
function salvageTruncatedJson(content) {
|
|
2573
|
-
const stack = [];
|
|
2574
|
-
const cuts = [];
|
|
2575
|
-
let inString = false;
|
|
2576
|
-
let escaped = false;
|
|
2577
|
-
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
2578
|
-
for (let i = 0; i < content.length; i++) {
|
|
2579
|
-
const ch = content[i];
|
|
2580
|
-
if (inString) {
|
|
2581
|
-
if (escaped) escaped = false;
|
|
2582
|
-
else if (ch === "\\") escaped = true;
|
|
2583
|
-
else if (ch === '"') {
|
|
2584
|
-
inString = false;
|
|
2585
|
-
record(i + 1, false);
|
|
2586
|
-
}
|
|
2587
|
-
continue;
|
|
2588
|
-
}
|
|
2589
|
-
if (ch === '"') {
|
|
2590
|
-
inString = true;
|
|
2591
|
-
} else if (ch === "{" || ch === "[") {
|
|
2592
|
-
stack.push(ch);
|
|
2593
|
-
} else if (ch === "}" || ch === "]") {
|
|
2594
|
-
stack.pop();
|
|
2595
|
-
record(i + 1, true);
|
|
2596
|
-
} else if (ch === ",") {
|
|
2597
|
-
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
const tryCut = (cut) => {
|
|
2601
|
-
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
2602
|
-
if (head.endsWith(":")) return null;
|
|
2603
|
-
let closed = head;
|
|
2604
|
-
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
2605
|
-
closed += CLOSERS[cut.open[d]];
|
|
2606
|
-
}
|
|
2607
|
-
try {
|
|
2608
|
-
JSON.parse(closed);
|
|
2609
|
-
return closed;
|
|
2610
|
-
} catch {
|
|
2611
|
-
return null;
|
|
2612
|
-
}
|
|
2613
|
-
};
|
|
2614
|
-
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
2615
|
-
for (const containerOnly of [true, false]) {
|
|
2616
|
-
for (let n = 0; n < attempts; n++) {
|
|
2617
|
-
const cut = cuts[cuts.length - 1 - n];
|
|
2618
|
-
if (containerOnly && !cut.closesContainer) continue;
|
|
2619
|
-
const salvaged = tryCut(cut);
|
|
2620
|
-
if (salvaged !== null) return salvaged;
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
return null;
|
|
2624
|
-
}
|
|
2625
|
-
function extractJsonCandidates(content) {
|
|
2626
|
-
let text = content.trim();
|
|
2627
|
-
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
2628
|
-
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
2629
|
-
const start = text.search(/[{[]/);
|
|
2630
|
-
if (start === -1) return [];
|
|
2631
|
-
const closer = text[start] === "{" ? "}" : "]";
|
|
2632
|
-
const end = text.lastIndexOf(closer);
|
|
2633
|
-
const candidates = [text.slice(start)];
|
|
2634
|
-
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
2635
|
-
return candidates;
|
|
2636
|
-
}
|
|
2637
|
-
function parseAIJson(content) {
|
|
2638
|
-
const candidates = extractJsonCandidates(content);
|
|
2639
|
-
if (candidates.length === 0) return null;
|
|
2640
|
-
for (const text of candidates) {
|
|
2641
|
-
try {
|
|
2642
|
-
return { value: JSON.parse(text), repaired: false };
|
|
2643
|
-
} catch {
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
for (const text of candidates) {
|
|
2647
|
-
try {
|
|
2648
|
-
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
2649
|
-
} catch {
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
for (const text of candidates) {
|
|
2653
|
-
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
2654
|
-
const salvaged = salvageTruncatedJson(variant);
|
|
2655
|
-
if (salvaged === null) continue;
|
|
2656
|
-
try {
|
|
2657
|
-
return { value: JSON.parse(salvaged), repaired: true };
|
|
2658
|
-
} catch {
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
return null;
|
|
2663
|
-
}
|
|
2664
|
-
var MAX_SALVAGE_ATTEMPTS, CLOSERS;
|
|
2665
|
-
var init_json_repair = __esm({
|
|
2666
|
-
"src/lib/json-repair.ts"() {
|
|
2667
|
-
"use strict";
|
|
2668
|
-
MAX_SALVAGE_ATTEMPTS = 500;
|
|
2669
|
-
CLOSERS = { "{": "}", "[": "]" };
|
|
2670
|
-
}
|
|
2671
|
-
});
|
|
2672
|
-
|
|
2673
2780
|
// src/lib/prompts.ts
|
|
2674
2781
|
function buildSystemPrompt(maxSteps, profile) {
|
|
2675
2782
|
let scope;
|
|
@@ -2985,6 +3092,9 @@ function renderHormoziAss(ctx) {
|
|
|
2985
3092
|
return `${header2}${lines.join("\n")}
|
|
2986
3093
|
`;
|
|
2987
3094
|
}
|
|
3095
|
+
function looksLikeAddress(token) {
|
|
3096
|
+
return /^(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s]*)?[.,!?]?$/i.test(token);
|
|
3097
|
+
}
|
|
2988
3098
|
function renderShortsAss(ctx) {
|
|
2989
3099
|
const { captions, width, height } = ctx;
|
|
2990
3100
|
const font = resolveCaptionFont(captions.font === "Arial" ? "Arial Black" : captions.font);
|
|
@@ -2995,7 +3105,7 @@ function renderShortsAss(ctx) {
|
|
|
2995
3105
|
const alignment = positionToAlignment(captions.position);
|
|
2996
3106
|
const outline = Math.max(2, Math.round(fontSize / 22));
|
|
2997
3107
|
const upper = true;
|
|
2998
|
-
const caseOf = (t) => (0, import_video_kit4.toCaptionCase)(t, upper);
|
|
3108
|
+
const caseOf = (t) => looksLikeAddress(t) ? t : (0, import_video_kit4.toCaptionCase)(t, upper);
|
|
2999
3109
|
const maxChars = (0, import_video_kit4.fittingMaxChars)(width, fontSize, SIDE_MARGIN, captions.maxChars, upper);
|
|
3000
3110
|
const header2 = `[Script Info]
|
|
3001
3111
|
Title: QuickPeek Demo
|
|
@@ -3226,7 +3336,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
3226
3336
|
return 0;
|
|
3227
3337
|
}
|
|
3228
3338
|
const mediaPath = (0, import_node_path4.isAbsolute)(step.value) ? step.value : (0, import_node_path4.resolve)(baseDir, step.value);
|
|
3229
|
-
if (!(0,
|
|
3339
|
+
if (!(0, import_node_fs6.existsSync)(mediaPath)) {
|
|
3230
3340
|
return 0;
|
|
3231
3341
|
}
|
|
3232
3342
|
try {
|
|
@@ -3248,7 +3358,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
3248
3358
|
async function loadVoiceoverMeta(outputDir) {
|
|
3249
3359
|
const metaPath = (0, import_node_path4.join)(outputDir, "voiceover.json");
|
|
3250
3360
|
try {
|
|
3251
|
-
if (!(0,
|
|
3361
|
+
if (!(0, import_node_fs6.existsSync)(metaPath)) return null;
|
|
3252
3362
|
const content = await (0, import_promises4.readFile)(metaPath, "utf-8");
|
|
3253
3363
|
return JSON.parse(content);
|
|
3254
3364
|
} catch {
|
|
@@ -3271,11 +3381,11 @@ async function concatenateAudio(inputs, output, workDir) {
|
|
|
3271
3381
|
await (0, import_promises4.unlink)(listPath).catch(() => {
|
|
3272
3382
|
});
|
|
3273
3383
|
}
|
|
3274
|
-
var
|
|
3384
|
+
var import_node_fs6, import_promises4, import_node_path4, isWindows, NARRATION_LEAD_IN_MS, LEAD_IN_CLIP;
|
|
3275
3385
|
var init_helpers = __esm({
|
|
3276
3386
|
"src/lib/tts/helpers.ts"() {
|
|
3277
3387
|
"use strict";
|
|
3278
|
-
|
|
3388
|
+
import_node_fs6 = require("fs");
|
|
3279
3389
|
import_promises4 = require("fs/promises");
|
|
3280
3390
|
import_node_path4 = require("path");
|
|
3281
3391
|
init_ffmpeg2();
|
|
@@ -3351,7 +3461,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3351
3461
|
const segments = [];
|
|
3352
3462
|
const alignedStepAudio = /* @__PURE__ */ new Map();
|
|
3353
3463
|
const leadInPath = (0, import_node_path5.join)(audioDir, LEAD_IN_CLIP);
|
|
3354
|
-
const leadIn = (0,
|
|
3464
|
+
const leadIn = (0, import_node_fs7.existsSync)(leadInPath) ? NARRATION_LEAD_IN_MS : 0;
|
|
3355
3465
|
if (leadIn) segments.push(leadInPath);
|
|
3356
3466
|
for (let i = 0; i < actualRecordingDurations.length; i++) {
|
|
3357
3467
|
const stepNum = i + 1;
|
|
@@ -3362,9 +3472,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3362
3472
|
const audioPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}.mp3`);
|
|
3363
3473
|
const originalPaddingPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
3364
3474
|
const silencePath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
3365
|
-
if ((0,
|
|
3475
|
+
if ((0, import_node_fs7.existsSync)(audioPath)) {
|
|
3366
3476
|
segments.push(audioPath);
|
|
3367
|
-
if ((0,
|
|
3477
|
+
if ((0, import_node_fs7.existsSync)(originalPaddingPath)) {
|
|
3368
3478
|
segments.push(originalPaddingPath);
|
|
3369
3479
|
}
|
|
3370
3480
|
if (recordingDuration > voiceDuration) {
|
|
@@ -3372,7 +3482,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3372
3482
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
3373
3483
|
segments.push(alignPaddingPath);
|
|
3374
3484
|
}
|
|
3375
|
-
} else if ((0,
|
|
3485
|
+
} else if ((0, import_node_fs7.existsSync)(silencePath)) {
|
|
3376
3486
|
const own = targetDuration - (stepNum === 1 ? leadIn : 0);
|
|
3377
3487
|
if (targetDuration !== voiceDuration && own > 0) {
|
|
3378
3488
|
await generateSilence(silencePath, own);
|
|
@@ -3401,11 +3511,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
3401
3511
|
}
|
|
3402
3512
|
return alignedStepAudio;
|
|
3403
3513
|
}
|
|
3404
|
-
var
|
|
3514
|
+
var import_node_fs7, import_node_path5;
|
|
3405
3515
|
var init_timing = __esm({
|
|
3406
3516
|
"src/lib/tts/timing.ts"() {
|
|
3407
3517
|
"use strict";
|
|
3408
|
-
|
|
3518
|
+
import_node_fs7 = require("fs");
|
|
3409
3519
|
import_node_path5 = require("path");
|
|
3410
3520
|
init_ffmpeg2();
|
|
3411
3521
|
init_helpers();
|
|
@@ -3554,6 +3664,8 @@ asyncio.run(main())
|
|
|
3554
3664
|
await (0, import_promises5.writeFile)(tempScriptPath, pythonScript);
|
|
3555
3665
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
3556
3666
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
3667
|
+
const written = await (0, import_promises5.stat)(outputPath).catch(() => null);
|
|
3668
|
+
if (!written?.size) throw new Error("edge-tts produced no audio");
|
|
3557
3669
|
return await readWordsSidecar(outputPath);
|
|
3558
3670
|
} finally {
|
|
3559
3671
|
await (0, import_promises5.unlink)(tempScriptPath).catch(() => {
|
|
@@ -3581,7 +3693,7 @@ async function synthesizeWithNode(text, outputPath, voice, pythonMsg, rate, maxR
|
|
|
3581
3693
|
const { audioStream, metadataStream } = tts.toStream(text, rate ? { rate } : void 0);
|
|
3582
3694
|
const metadata = [];
|
|
3583
3695
|
metadataStream?.on("data", (chunk) => metadata.push(chunk.toString()));
|
|
3584
|
-
await (0, import_promises6.pipeline)(audioStream, (0,
|
|
3696
|
+
await (0, import_promises6.pipeline)(audioStream, (0, import_node_fs8.createWriteStream)(outputPath));
|
|
3585
3697
|
await new Promise((r) => setTimeout(r, 250));
|
|
3586
3698
|
const { size } = await (0, import_promises5.stat)(outputPath);
|
|
3587
3699
|
if (size === 0) throw new Error("No audio data received");
|
|
@@ -3653,12 +3765,12 @@ function splitJsonObjects(raw) {
|
|
|
3653
3765
|
}
|
|
3654
3766
|
return out;
|
|
3655
3767
|
}
|
|
3656
|
-
var import_node_child_process4,
|
|
3768
|
+
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
3769
|
var init_synthesize = __esm({
|
|
3658
3770
|
"src/lib/tts/synthesize.ts"() {
|
|
3659
3771
|
"use strict";
|
|
3660
3772
|
import_node_child_process4 = require("child_process");
|
|
3661
|
-
|
|
3773
|
+
import_node_fs8 = require("fs");
|
|
3662
3774
|
import_promises5 = require("fs/promises");
|
|
3663
3775
|
import_node_path7 = require("path");
|
|
3664
3776
|
import_promises6 = require("stream/promises");
|
|
@@ -3680,7 +3792,7 @@ function clipKey(spokenText, voice, rate) {
|
|
|
3680
3792
|
async function reuseClip(cacheDir, key, audioPath) {
|
|
3681
3793
|
const mp3 = (0, import_node_path8.join)(cacheDir, `${key}.mp3`);
|
|
3682
3794
|
const meta = (0, import_node_path8.join)(cacheDir, `${key}.json`);
|
|
3683
|
-
if (!(0,
|
|
3795
|
+
if (!(0, import_node_fs9.existsSync)(mp3) || !(0, import_node_fs9.existsSync)(meta)) return null;
|
|
3684
3796
|
try {
|
|
3685
3797
|
const boundaries = JSON.parse(await (0, import_promises7.readFile)(meta, "utf-8"));
|
|
3686
3798
|
await (0, import_promises7.copyFile)(mp3, audioPath);
|
|
@@ -3742,11 +3854,17 @@ function spellAcronyms(text) {
|
|
|
3742
3854
|
);
|
|
3743
3855
|
}
|
|
3744
3856
|
function bindDomains(text) {
|
|
3745
|
-
return text.replace(/\b([a-z0-9][a-z0-9
|
|
3857
|
+
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(
|
|
3858
|
+
/\b([a-z0-9][a-z0-9-]*)\.([a-z]{2,24})\b/gi,
|
|
3859
|
+
(_m, host, tld) => `${host}-dot-${SPOKEN_TLD[tld.toLowerCase()] ?? tld}`
|
|
3860
|
+
);
|
|
3746
3861
|
}
|
|
3747
3862
|
function speakSymbols(text) {
|
|
3748
3863
|
return text.replace(/\s*&\s*/g, " and ").replace(/[<>]/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
3749
3864
|
}
|
|
3865
|
+
function spokenForm(caption) {
|
|
3866
|
+
return speakNumbers(spellAcronyms((0, import_video_kit7.toSpokenForm)(bindDomains(speakSymbols(caption)))));
|
|
3867
|
+
}
|
|
3750
3868
|
async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
|
|
3751
3869
|
resetVoiceEngine();
|
|
3752
3870
|
const voice = resolveVoice2(config);
|
|
@@ -3793,7 +3911,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
3793
3911
|
}
|
|
3794
3912
|
const audioPath = (0, import_node_path8.join)(audioDir, `step-${stepNum}.mp3`);
|
|
3795
3913
|
try {
|
|
3796
|
-
const spokenText =
|
|
3914
|
+
const spokenText = spokenForm(step.caption);
|
|
3797
3915
|
const key = clipKey(spokenText, voice, ratePercent ?? "");
|
|
3798
3916
|
const cacheDir = (0, import_node_path8.join)(outputDir, ".tts-clips");
|
|
3799
3917
|
const cached = await reuseClip(cacheDir, key, audioPath);
|
|
@@ -3874,12 +3992,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
3874
3992
|
await saveVoiceoverMeta(outputDir, meta);
|
|
3875
3993
|
return { voiceoverPath, stepAudio };
|
|
3876
3994
|
}
|
|
3877
|
-
var import_node_crypto,
|
|
3995
|
+
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
3996
|
var init_generate = __esm({
|
|
3879
3997
|
"src/lib/tts/generate.ts"() {
|
|
3880
3998
|
"use strict";
|
|
3881
3999
|
import_node_crypto = require("crypto");
|
|
3882
|
-
|
|
4000
|
+
import_node_fs9 = require("fs");
|
|
3883
4001
|
import_promises7 = require("fs/promises");
|
|
3884
4002
|
import_node_path8 = require("path");
|
|
3885
4003
|
import_video_kit7 = require("@spark-apps/video-kit");
|
|
@@ -3944,6 +4062,9 @@ var init_generate = __esm({
|
|
|
3944
4062
|
Y: "why",
|
|
3945
4063
|
Z: "zed"
|
|
3946
4064
|
};
|
|
4065
|
+
SPOKEN_TLD = {
|
|
4066
|
+
ai: "ay-eye"
|
|
4067
|
+
};
|
|
3947
4068
|
}
|
|
3948
4069
|
});
|
|
3949
4070
|
|
|
@@ -4003,6 +4124,7 @@ __export(lib_exports, {
|
|
|
4003
4124
|
NO_CLIP_OUTRO: () => NO_CLIP_OUTRO,
|
|
4004
4125
|
PLAN_MAX_TOKENS: () => PLAN_MAX_TOKENS,
|
|
4005
4126
|
RATE_FACTOR: () => RATE_FACTOR,
|
|
4127
|
+
RELAY_BASE: () => RELAY_BASE,
|
|
4006
4128
|
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
4007
4129
|
SIZE_PRESETS: () => SIZE_PRESETS,
|
|
4008
4130
|
TRIM_EDGE_SILENCE: () => TRIM_EDGE_SILENCE,
|
|
@@ -4027,6 +4149,7 @@ __export(lib_exports, {
|
|
|
4027
4149
|
buildUserPrompt: () => buildUserPrompt,
|
|
4028
4150
|
buildWatermarkFilters: () => buildWatermarkFilters,
|
|
4029
4151
|
callAI: () => callAI,
|
|
4152
|
+
callAIForPlan: () => callAIForPlan,
|
|
4030
4153
|
callAIViaRelay: () => callAIViaRelay,
|
|
4031
4154
|
canReuseVoiceover: () => canReuseVoiceover,
|
|
4032
4155
|
captionCanvas: () => captionCanvas,
|
|
@@ -4054,6 +4177,7 @@ __export(lib_exports, {
|
|
|
4054
4177
|
getErrorMessage: () => getErrorMessage,
|
|
4055
4178
|
getLanguageName: () => getLanguageName,
|
|
4056
4179
|
getMediaDuration: () => getMediaDuration,
|
|
4180
|
+
getOrRefreshRelayToken: () => getOrRefreshRelayToken,
|
|
4057
4181
|
getStatus: () => getStatus,
|
|
4058
4182
|
getStatusCached: () => getStatusCached,
|
|
4059
4183
|
getTierByEmail: () => getTierByEmail,
|
|
@@ -4097,12 +4221,12 @@ __export(lib_exports, {
|
|
|
4097
4221
|
runPooled: () => runPooled,
|
|
4098
4222
|
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
4099
4223
|
shouldSkipLink: () => shouldSkipLink,
|
|
4100
|
-
shouldWatermark: () => shouldWatermark,
|
|
4101
4224
|
showPaywall: () => showPaywall,
|
|
4102
4225
|
speakSymbols: () => speakSymbols,
|
|
4103
4226
|
speedUpMiddle: () => speedUpMiddle,
|
|
4104
4227
|
speedUpVideo: () => speedUpVideo,
|
|
4105
4228
|
spliceVideos: () => spliceVideos,
|
|
4229
|
+
spokenForm: () => spokenForm,
|
|
4106
4230
|
spokenWordTimings: () => spokenWordTimings,
|
|
4107
4231
|
stripLongDashes: () => stripLongDashes,
|
|
4108
4232
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
@@ -4241,54 +4365,16 @@ var init_ambience = __esm({
|
|
|
4241
4365
|
}
|
|
4242
4366
|
});
|
|
4243
4367
|
|
|
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
4368
|
// 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
4369
|
function escapeHtml(text) {
|
|
4287
4370
|
return text.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4288
4371
|
}
|
|
4289
4372
|
function buildOutroCardHtml(options) {
|
|
4290
4373
|
const { subject, credits = [], promo, width, height } = options;
|
|
4291
4374
|
const unit = Math.min(width, height);
|
|
4375
|
+
const markFraction = width > height ? 0.28 : 0.46;
|
|
4376
|
+
const markSize = unit * markFraction;
|
|
4377
|
+
const padTop = width > height ? unit * 0.08 : unit * 0.23;
|
|
4292
4378
|
const creditRows = credits.map((credit) => {
|
|
4293
4379
|
const icon = credit.iconPath ? dataUri(credit.iconPath) : null;
|
|
4294
4380
|
return `<div class="credit">
|
|
@@ -4297,7 +4383,8 @@ function buildOutroCardHtml(options) {
|
|
|
4297
4383
|
${icon ? `<img class="cicon" src="${icon}" alt="" />` : ""}
|
|
4298
4384
|
</div>`;
|
|
4299
4385
|
}).join("");
|
|
4300
|
-
const
|
|
4386
|
+
const animated = subject.animatedMark;
|
|
4387
|
+
const subjectIcon = animated ? null : subject.iconPath ? dataUri(subject.iconPath) : null;
|
|
4301
4388
|
return `<!doctype html>
|
|
4302
4389
|
<html><head><meta charset="utf-8" /><style>
|
|
4303
4390
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -4315,7 +4402,7 @@ function buildOutroCardHtml(options) {
|
|
|
4315
4402
|
position: relative;
|
|
4316
4403
|
display: flex; flex-direction: column;
|
|
4317
4404
|
align-items: center; justify-content: flex-start;
|
|
4318
|
-
padding-top: ${
|
|
4405
|
+
padding-top: ${padTop}px;
|
|
4319
4406
|
gap: ${unit * 0.028}px;
|
|
4320
4407
|
}
|
|
4321
4408
|
/* Toolchain credits: top right, deliberately small. */
|
|
@@ -4333,7 +4420,7 @@ function buildOutroCardHtml(options) {
|
|
|
4333
4420
|
/* The app the video is about. */
|
|
4334
4421
|
/* The app's mark leads the card, and it is meant to be seen: at 46% of
|
|
4335
4422
|
the short edge it reads at a glance on a phone. */
|
|
4336
|
-
.mark { width: ${
|
|
4423
|
+
.mark { width: ${markSize}px; height: ${markSize}px; object-fit: contain; border-radius: ${unit * 0.09}px; }
|
|
4337
4424
|
/* A dark mark on a dark card is invisible, which looks exactly like a
|
|
4338
4425
|
failed image. Sites that publish a black-on-transparent logo are common
|
|
4339
4426
|
enough to be worth catching, and a light plate is what their own site
|
|
@@ -4355,8 +4442,12 @@ function buildOutroCardHtml(options) {
|
|
|
4355
4442
|
}
|
|
4356
4443
|
|
|
4357
4444
|
/* Today's offer, along the very bottom edge. */
|
|
4445
|
+
/* In the flow, not pinned to the bottom edge. Pinned, it was laid out
|
|
4446
|
+
without reference to the text above it, so on a short canvas it simply
|
|
4447
|
+
covered whatever had grown into its space. margin-top pushes it clear of
|
|
4448
|
+
the domain instead. */
|
|
4358
4449
|
.promo {
|
|
4359
|
-
|
|
4450
|
+
margin-top: ${unit * 0.05}px;
|
|
4360
4451
|
display: flex; align-items: baseline; gap: ${unit * 0.022}px;
|
|
4361
4452
|
padding: ${unit * 0.026}px ${unit * 0.055}px;
|
|
4362
4453
|
border: 2px solid rgba(255, 209, 64, .85); border-radius: ${unit * 0.024}px;
|
|
@@ -4364,9 +4455,60 @@ function buildOutroCardHtml(options) {
|
|
|
4364
4455
|
}
|
|
4365
4456
|
.ppct { font-size: ${unit * 0.056}px; font-weight: 700; color: #ffd140; }
|
|
4366
4457
|
.pcode { font-size: ${unit * 0.048}px; font-weight: 700; letter-spacing: .05em; }
|
|
4458
|
+
|
|
4459
|
+
/* The animated mark, sized to the same 46% slot the still one occupies so
|
|
4460
|
+
the rest of the card's rhythm is unchanged. Ported from VidLet's
|
|
4461
|
+
src/gui/css/loader.css: the amber tip flies off and snaps back into the
|
|
4462
|
+
cut - a jump cut, on a loop. Pure CSS, because the renderer steps these
|
|
4463
|
+
animations frame by frame and a JS-driven one could not be stepped.
|
|
4464
|
+
|
|
4465
|
+
No prefers-reduced-motion fallback here, unlike the original: this runs
|
|
4466
|
+
in a headless Chromium rendering a video, where there is no viewer whose
|
|
4467
|
+
preference could be read, and honouring the default would silently flatten
|
|
4468
|
+
the animation to a fade in CI. */
|
|
4469
|
+
.amark { width: ${markSize}px; height: ${markSize}px; display: block; }
|
|
4470
|
+
.amark path {
|
|
4471
|
+
/* Without fill-box the origin is the whole SVG viewport, which swings the
|
|
4472
|
+
tip off its own centre on the scale steps. */
|
|
4473
|
+
transform-box: fill-box;
|
|
4474
|
+
transform-origin: center;
|
|
4475
|
+
}
|
|
4476
|
+
.amark .vl-body { fill: #fff6e5; stroke: #fff6e5; animation: vl-nudge 1.4s cubic-bezier(.4,0,.2,1) infinite; }
|
|
4477
|
+
.amark .vl-tip { fill: #ffc53d; stroke: #ffc53d; animation: vl-cut 1.4s cubic-bezier(.4,0,.2,1) infinite; }
|
|
4478
|
+
.amark .vl-ghost { opacity: .35; }
|
|
4479
|
+
.amark .vl-ghost .vl-tip { animation-delay: -.1s; }
|
|
4480
|
+
|
|
4481
|
+
/* Translations are in viewBox units, so the motion scales with the mark. */
|
|
4482
|
+
@keyframes vl-cut {
|
|
4483
|
+
0%, 30% { transform: translateX(0) scale(1); opacity: 1; }
|
|
4484
|
+
48% { transform: translateX(62px) scale(.94); opacity: 1; }
|
|
4485
|
+
60% { transform: translateX(88px) scale(.86); opacity: 0; }
|
|
4486
|
+
61%, 74% { transform: translateX(-58px) scale(.86); opacity: 0; }
|
|
4487
|
+
86% { transform: translateX(6px) scale(1.06); opacity: 1; }
|
|
4488
|
+
100% { transform: translateX(0) scale(1); opacity: 1; }
|
|
4489
|
+
}
|
|
4490
|
+
/* The body takes the recoil, so the cut lands rather than just sliding. */
|
|
4491
|
+
@keyframes vl-nudge {
|
|
4492
|
+
0%, 30% { transform: scaleX(1); }
|
|
4493
|
+
55% { transform: scaleX(1.04); }
|
|
4494
|
+
86% { transform: scaleX(.97); }
|
|
4495
|
+
100% { transform: scaleX(1); }
|
|
4496
|
+
}
|
|
4497
|
+
${/* The card normally reads its palette off the mark's pixels, and an SVG
|
|
4498
|
+
drawn by this stylesheet has none to read - the measuring script finds
|
|
4499
|
+
no <img class="mark"> and leaves the defaults, which would put VidLet's
|
|
4500
|
+
amber on a teal card. Stated instead of measured, last so it wins. */
|
|
4501
|
+
animated === "vidlet" ? ":root { --bg-in: #1f1a10; --bg-out: #07060a; --accent: #ffc53d; }" : ""}
|
|
4367
4502
|
</style></head>
|
|
4368
4503
|
<body>
|
|
4369
4504
|
${credits.length ? `<div class="credits">${creditRows}</div>` : ""}
|
|
4505
|
+
${animated === "vidlet" ? `<svg class="amark" viewBox="0 0 512 512" aria-hidden="true">
|
|
4506
|
+
<g stroke-linejoin="round" stroke-width="20">
|
|
4507
|
+
<g class="vl-ghost"><path class="vl-tip" d="M318 246 L410 302 L318 358 Z"/></g>
|
|
4508
|
+
<path class="vl-body" d="M176 124 L300 200 L300 312 L176 388 Z"/>
|
|
4509
|
+
<path class="vl-tip" d="M318 246 L410 302 L318 358 Z"/>
|
|
4510
|
+
</g>
|
|
4511
|
+
</svg>` : ""}
|
|
4370
4512
|
${subjectIcon ? `<img class="mark" src="${subjectIcon}" alt="" />` : ""}
|
|
4371
4513
|
<div class="name">${escapeHtml(subject.name)}</div>
|
|
4372
4514
|
${subject.tagline ? `<div class="tagline">${escapeHtml(subject.tagline)}</div>` : ""}
|
|
@@ -4469,12 +4611,18 @@ function buildOutroCardHtml(options) {
|
|
|
4469
4611
|
</script>
|
|
4470
4612
|
</body></html>`;
|
|
4471
4613
|
}
|
|
4472
|
-
var
|
|
4614
|
+
var ANIMATED_MARK_LOOP_SECONDS, dataUri;
|
|
4473
4615
|
var init_outro_card = __esm({
|
|
4474
4616
|
"src/lib/outro-card.ts"() {
|
|
4475
4617
|
"use strict";
|
|
4476
|
-
import_node_fs17 = require("fs");
|
|
4477
4618
|
init_image_kind();
|
|
4619
|
+
ANIMATED_MARK_LOOP_SECONDS = {
|
|
4620
|
+
// Matches `vl-cut` / `vl-nudge` in VidLet's src/gui/css/loader.css. Changing
|
|
4621
|
+
// it there without changing it here makes the encoded loop stutter at the
|
|
4622
|
+
// seam, because the clip is cut to exactly this length and then repeated.
|
|
4623
|
+
vidlet: 1.4
|
|
4624
|
+
};
|
|
4625
|
+
dataUri = imageDataUri;
|
|
4478
4626
|
}
|
|
4479
4627
|
});
|
|
4480
4628
|
|
|
@@ -4505,6 +4653,9 @@ async function renderOutroClip(options) {
|
|
|
4505
4653
|
const htmlPath = (0, import_node_path22.join)(workDir, "outro-card.html");
|
|
4506
4654
|
const pngPath = (0, import_node_path22.join)(workDir, "outro-card.png");
|
|
4507
4655
|
const clipPath = (0, import_node_path22.join)(workDir, "outro-card.mp4");
|
|
4656
|
+
if (options.subject.animatedMark) {
|
|
4657
|
+
return renderAnimatedOutroClip(options, { htmlPath, clipPath, seconds, fps });
|
|
4658
|
+
}
|
|
4508
4659
|
await (0, import_promises16.writeFile)(htmlPath, buildOutroCardHtml(options));
|
|
4509
4660
|
const browser = await import_playwright7.chromium.launch({ headless: true });
|
|
4510
4661
|
try {
|
|
@@ -4547,6 +4698,70 @@ async function renderOutroClip(options) {
|
|
|
4547
4698
|
);
|
|
4548
4699
|
return clipPath;
|
|
4549
4700
|
}
|
|
4701
|
+
async function renderAnimatedOutroClip(options, paths) {
|
|
4702
|
+
const { htmlPath, clipPath, seconds, fps } = paths;
|
|
4703
|
+
const { workDir, width, height } = options;
|
|
4704
|
+
const loopSeconds = ANIMATED_MARK_LOOP_SECONDS[options.subject.animatedMark];
|
|
4705
|
+
const frameCount = Math.max(1, Math.round(loopSeconds * fps));
|
|
4706
|
+
const framesDir = (0, import_node_path22.join)(workDir, "outro-frames");
|
|
4707
|
+
await (0, import_promises16.rm)(framesDir, { recursive: true, force: true });
|
|
4708
|
+
await (0, import_promises16.mkdir)(framesDir, { recursive: true });
|
|
4709
|
+
await (0, import_promises16.writeFile)(htmlPath, buildOutroCardHtml(options));
|
|
4710
|
+
const browser = await import_playwright7.chromium.launch({ headless: true });
|
|
4711
|
+
try {
|
|
4712
|
+
const page = await browser.newPage({ viewport: { width, height } });
|
|
4713
|
+
await page.goto((0, import_node_url2.pathToFileURL)((0, import_node_path22.resolve)(htmlPath)).href, { waitUntil: "load" });
|
|
4714
|
+
await page.waitForSelector('body[data-cards-ready="1"]', { timeout: 3e3 }).catch(() => {
|
|
4715
|
+
});
|
|
4716
|
+
await page.waitForTimeout(300);
|
|
4717
|
+
await page.evaluate(() => {
|
|
4718
|
+
for (const animation of document.getAnimations()) animation.pause();
|
|
4719
|
+
});
|
|
4720
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
4721
|
+
const elapsedMs = frame / frameCount * loopSeconds * 1e3;
|
|
4722
|
+
await page.evaluate((ms) => {
|
|
4723
|
+
for (const animation of document.getAnimations()) animation.currentTime = ms;
|
|
4724
|
+
}, elapsedMs);
|
|
4725
|
+
await page.screenshot({ path: (0, import_node_path22.join)(framesDir, `f-${String(frame).padStart(4, "0")}.png`) });
|
|
4726
|
+
}
|
|
4727
|
+
} finally {
|
|
4728
|
+
await browser.close();
|
|
4729
|
+
}
|
|
4730
|
+
await runFFmpeg(
|
|
4731
|
+
[
|
|
4732
|
+
"-y",
|
|
4733
|
+
// -stream_loop applies to the input that FOLLOWS it, so it has to sit
|
|
4734
|
+
// before -framerate/-i, not after.
|
|
4735
|
+
"-stream_loop",
|
|
4736
|
+
"-1",
|
|
4737
|
+
"-framerate",
|
|
4738
|
+
String(fps),
|
|
4739
|
+
"-i",
|
|
4740
|
+
(0, import_node_path22.join)(framesDir, "f-%04d.png"),
|
|
4741
|
+
"-f",
|
|
4742
|
+
"lavfi",
|
|
4743
|
+
"-i",
|
|
4744
|
+
"anullsrc=channel_layout=stereo:sample_rate=44100",
|
|
4745
|
+
"-t",
|
|
4746
|
+
String(seconds),
|
|
4747
|
+
"-r",
|
|
4748
|
+
String(fps),
|
|
4749
|
+
"-pix_fmt",
|
|
4750
|
+
"yuv420p",
|
|
4751
|
+
"-c:v",
|
|
4752
|
+
"libx264",
|
|
4753
|
+
"-preset",
|
|
4754
|
+
"veryfast",
|
|
4755
|
+
"-c:a",
|
|
4756
|
+
"aac",
|
|
4757
|
+
"-shortest",
|
|
4758
|
+
clipPath
|
|
4759
|
+
],
|
|
4760
|
+
"ffmpeg animated outro card render failed"
|
|
4761
|
+
);
|
|
4762
|
+
await (0, import_promises16.rm)(framesDir, { recursive: true, force: true });
|
|
4763
|
+
return clipPath;
|
|
4764
|
+
}
|
|
4550
4765
|
var import_promises16, import_node_path22, import_node_url2, import_playwright7;
|
|
4551
4766
|
var init_render_outro = __esm({
|
|
4552
4767
|
"src/browser/render-outro.ts"() {
|
|
@@ -4857,14 +5072,108 @@ async function listAuthStates() {
|
|
|
4857
5072
|
|
|
4858
5073
|
// src/browser/launch.ts
|
|
4859
5074
|
var import_promises2 = require("fs/promises");
|
|
4860
|
-
var
|
|
5075
|
+
var import_playwright3 = require("playwright");
|
|
4861
5076
|
init_lifecycle();
|
|
4862
5077
|
|
|
5078
|
+
// src/browser/brand-watermark.ts
|
|
5079
|
+
init_image_kind();
|
|
5080
|
+
function watermarkInitScript(payload) {
|
|
5081
|
+
return `(() => {
|
|
5082
|
+
var ID = '__quickpeek_brand_watermark';
|
|
5083
|
+
var P = ${embedJson(payload)};
|
|
5084
|
+
var vertical = P.position.indexOf('top') === 0 ? 'top:22px' : 'bottom:22px';
|
|
5085
|
+
var horizontal = P.position.indexOf('right') > -1 ? 'right:24px' : 'left:24px';
|
|
5086
|
+
var SHELL = [
|
|
5087
|
+
'position:fixed',
|
|
5088
|
+
vertical,
|
|
5089
|
+
horizontal,
|
|
5090
|
+
'z-index:2147483646',
|
|
5091
|
+
'display:flex',
|
|
5092
|
+
'align-items:center',
|
|
5093
|
+
'gap:10px',
|
|
5094
|
+
'padding:8px 16px 8px 10px',
|
|
5095
|
+
'border-radius:999px',
|
|
5096
|
+
// A hairline and a blur rather than a solid plate: the mark sits on top of
|
|
5097
|
+
// whatever the app is showing, and a hard box reads as part of the UI.
|
|
5098
|
+
// The alpha is high enough to stay dark over a LIGHT app: at 0.55 the blur
|
|
5099
|
+
// averaged a pale page up into grey and the white label sat on it at about
|
|
5100
|
+
// 2:1, which photographs as a smudge.
|
|
5101
|
+
'background:rgba(12,12,18,0.78)',
|
|
5102
|
+
'border:1px solid rgba(255,255,255,0.14)',
|
|
5103
|
+
'backdrop-filter:blur(14px) saturate(1.3)',
|
|
5104
|
+
'-webkit-backdrop-filter:blur(14px) saturate(1.3)',
|
|
5105
|
+
'box-shadow:0 6px 24px rgba(0,0,0,0.30)',
|
|
5106
|
+
'opacity:' + P.opacity,
|
|
5107
|
+
'pointer-events:none',
|
|
5108
|
+
'user-select:none'
|
|
5109
|
+
].join(';');
|
|
5110
|
+
var ICON = 'width:26px;height:26px;border-radius:7px;object-fit:contain;display:block';
|
|
5111
|
+
var NAME = [
|
|
5112
|
+
'font:600 15px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
|
|
5113
|
+
'letter-spacing:0.015em',
|
|
5114
|
+
'color:#ffffff',
|
|
5115
|
+
'white-space:nowrap'
|
|
5116
|
+
].join(';');
|
|
5117
|
+
|
|
5118
|
+
function mount() {
|
|
5119
|
+
try {
|
|
5120
|
+
if (!document.body || document.getElementById(ID)) return;
|
|
5121
|
+
var el = document.createElement('div');
|
|
5122
|
+
el.id = ID;
|
|
5123
|
+
el.setAttribute('style', SHELL);
|
|
5124
|
+
if (P.icon) {
|
|
5125
|
+
var img = document.createElement('img');
|
|
5126
|
+
img.setAttribute('style', ICON);
|
|
5127
|
+
img.src = P.icon;
|
|
5128
|
+
img.alt = '';
|
|
5129
|
+
el.appendChild(img);
|
|
5130
|
+
}
|
|
5131
|
+
var label = document.createElement('span');
|
|
5132
|
+
label.setAttribute('style', NAME);
|
|
5133
|
+
label.textContent = P.name;
|
|
5134
|
+
el.appendChild(label);
|
|
5135
|
+
document.body.appendChild(el);
|
|
5136
|
+
} catch (e) {
|
|
5137
|
+
// A page that refuses the node still records; the mark is cosmetic.
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
if (document.body) mount();
|
|
5142
|
+
else document.addEventListener('DOMContentLoaded', mount, { once: true });
|
|
5143
|
+
setInterval(mount, 400);
|
|
5144
|
+
})();`;
|
|
5145
|
+
}
|
|
5146
|
+
async function applyBrandWatermark(context, watermark) {
|
|
5147
|
+
if (!watermark?.name) return false;
|
|
5148
|
+
const payload = {
|
|
5149
|
+
name: watermark.name,
|
|
5150
|
+
icon: watermark.iconPath ? imageDataUri(watermark.iconPath) : null,
|
|
5151
|
+
position: watermark.position ?? "top-right",
|
|
5152
|
+
opacity: clampOpacity(watermark.opacity)
|
|
5153
|
+
};
|
|
5154
|
+
try {
|
|
5155
|
+
await context.addInitScript(watermarkInitScript(payload));
|
|
5156
|
+
return true;
|
|
5157
|
+
} catch {
|
|
5158
|
+
return false;
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
function embedJson(value) {
|
|
5162
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
5163
|
+
}
|
|
5164
|
+
function clampOpacity(value) {
|
|
5165
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0.92;
|
|
5166
|
+
return Math.min(1, Math.max(0.35, value));
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
// src/browser/launch.ts
|
|
5170
|
+
init_headless();
|
|
5171
|
+
|
|
4863
5172
|
// src/browser/wsl.ts
|
|
4864
5173
|
var import_node_child_process = require("child_process");
|
|
4865
|
-
var
|
|
5174
|
+
var import_node_fs3 = require("fs");
|
|
4866
5175
|
var import_node_http = require("http");
|
|
4867
|
-
var
|
|
5176
|
+
var import_playwright2 = require("playwright");
|
|
4868
5177
|
function isWSL() {
|
|
4869
5178
|
if (process.platform !== "linux") return false;
|
|
4870
5179
|
try {
|
|
@@ -4875,7 +5184,7 @@ function isWSL() {
|
|
|
4875
5184
|
}
|
|
4876
5185
|
}
|
|
4877
5186
|
function tryEnableWSLInterop() {
|
|
4878
|
-
return (0,
|
|
5187
|
+
return (0, import_node_fs3.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
4879
5188
|
}
|
|
4880
5189
|
function wslToWindowsPath(wslPath) {
|
|
4881
5190
|
if (!wslPath) return wslPath;
|
|
@@ -4884,7 +5193,7 @@ function wslToWindowsPath(wslPath) {
|
|
|
4884
5193
|
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
4885
5194
|
}
|
|
4886
5195
|
function findExe(whereExeQuery, ...hardcoded) {
|
|
4887
|
-
for (const p of hardcoded) if ((0,
|
|
5196
|
+
for (const p of hardcoded) if ((0, import_node_fs3.existsSync)(p)) return p;
|
|
4888
5197
|
try {
|
|
4889
5198
|
const line = (0, import_node_child_process.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
4890
5199
|
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
@@ -4907,7 +5216,7 @@ function getWindowsHostIP() {
|
|
|
4907
5216
|
} catch {
|
|
4908
5217
|
}
|
|
4909
5218
|
try {
|
|
4910
|
-
const m = (0,
|
|
5219
|
+
const m = (0, import_node_fs3.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
4911
5220
|
if (m?.[1]) return m[1];
|
|
4912
5221
|
} catch {
|
|
4913
5222
|
}
|
|
@@ -4915,7 +5224,7 @@ function getWindowsHostIP() {
|
|
|
4915
5224
|
}
|
|
4916
5225
|
function startRelay(nodePath, windowsHost, targetPort) {
|
|
4917
5226
|
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((
|
|
5227
|
+
return new Promise((resolve11, reject) => {
|
|
4919
5228
|
const proc = (0, import_node_child_process.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
4920
5229
|
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
4921
5230
|
let buf = "";
|
|
@@ -4924,7 +5233,7 @@ function startRelay(nodePath, windowsHost, targetPort) {
|
|
|
4924
5233
|
const m = buf.match(/PORT:(\d+)/);
|
|
4925
5234
|
if (m) {
|
|
4926
5235
|
clearTimeout(timer);
|
|
4927
|
-
|
|
5236
|
+
resolve11({ proc, port: +m[1] });
|
|
4928
5237
|
}
|
|
4929
5238
|
});
|
|
4930
5239
|
proc.on("error", (e) => {
|
|
@@ -4934,7 +5243,7 @@ function startRelay(nodePath, windowsHost, targetPort) {
|
|
|
4934
5243
|
});
|
|
4935
5244
|
}
|
|
4936
5245
|
function waitForChrome(host, port) {
|
|
4937
|
-
return new Promise((
|
|
5246
|
+
return new Promise((resolve11, reject) => {
|
|
4938
5247
|
const deadline = Date.now() + 2e4;
|
|
4939
5248
|
const poll = () => {
|
|
4940
5249
|
if (Date.now() > deadline) {
|
|
@@ -4943,7 +5252,7 @@ function waitForChrome(host, port) {
|
|
|
4943
5252
|
}
|
|
4944
5253
|
const req = (0, import_node_http.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
4945
5254
|
res.resume();
|
|
4946
|
-
|
|
5255
|
+
resolve11();
|
|
4947
5256
|
});
|
|
4948
5257
|
req.on("error", () => setTimeout(poll, 600));
|
|
4949
5258
|
req.setTimeout(1e3, () => {
|
|
@@ -4983,7 +5292,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
4983
5292
|
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
4984
5293
|
try {
|
|
4985
5294
|
await waitForChrome(windowsHost, proxyPort);
|
|
4986
|
-
const browser = await
|
|
5295
|
+
const browser = await import_playwright2.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
4987
5296
|
return {
|
|
4988
5297
|
browser,
|
|
4989
5298
|
cleanup: async () => {
|
|
@@ -5040,7 +5349,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
5040
5349
|
acceptDownloads: true
|
|
5041
5350
|
});
|
|
5042
5351
|
} catch {
|
|
5043
|
-
context = await
|
|
5352
|
+
context = await import_playwright3.chromium.launchPersistentContext(userDataDir, {
|
|
5044
5353
|
headless: false,
|
|
5045
5354
|
args: persistentArgs,
|
|
5046
5355
|
viewport,
|
|
@@ -5051,7 +5360,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
5051
5360
|
});
|
|
5052
5361
|
}
|
|
5053
5362
|
} else {
|
|
5054
|
-
context = await
|
|
5363
|
+
context = await import_playwright3.chromium.launchPersistentContext(userDataDir, {
|
|
5055
5364
|
headless: false,
|
|
5056
5365
|
args: persistentArgs,
|
|
5057
5366
|
viewport,
|
|
@@ -5069,12 +5378,12 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
5069
5378
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
5070
5379
|
};
|
|
5071
5380
|
try {
|
|
5072
|
-
browser = await
|
|
5381
|
+
browser = await import_playwright3.chromium.launch({ ...headedOpts, channel: "chrome" });
|
|
5073
5382
|
} catch {
|
|
5074
|
-
browser = await
|
|
5383
|
+
browser = await import_playwright3.chromium.launch(headedOpts);
|
|
5075
5384
|
}
|
|
5076
5385
|
} else {
|
|
5077
|
-
browser = await
|
|
5386
|
+
browser = await launchHeadlessChromium();
|
|
5078
5387
|
}
|
|
5079
5388
|
context = await browser.newContext({
|
|
5080
5389
|
viewport,
|
|
@@ -5086,6 +5395,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
5086
5395
|
});
|
|
5087
5396
|
}
|
|
5088
5397
|
await hideDevOverlays(context);
|
|
5398
|
+
await applyBrandWatermark(context, config.video.watermark);
|
|
5089
5399
|
const session = { context, browser, chromeSession };
|
|
5090
5400
|
session.release = onCleanup(() => closeBrowserSession(session));
|
|
5091
5401
|
return session;
|
|
@@ -5110,9 +5420,9 @@ async function launchSignInBrowser() {
|
|
|
5110
5420
|
ignoreDefaultArgs: ["--enable-automation"]
|
|
5111
5421
|
};
|
|
5112
5422
|
try {
|
|
5113
|
-
return await
|
|
5423
|
+
return await import_playwright3.chromium.launch({ ...opts, channel: "chrome" });
|
|
5114
5424
|
} catch {
|
|
5115
|
-
return await
|
|
5425
|
+
return await import_playwright3.chromium.launch(opts);
|
|
5116
5426
|
}
|
|
5117
5427
|
}
|
|
5118
5428
|
async function cleanupBrowserSession(session) {
|
|
@@ -5178,10 +5488,10 @@ async function cancelSignIn() {
|
|
|
5178
5488
|
}
|
|
5179
5489
|
|
|
5180
5490
|
// src/browser/chrome-import.ts
|
|
5181
|
-
var
|
|
5491
|
+
var import_node_fs4 = require("fs");
|
|
5182
5492
|
var import_node_os2 = require("os");
|
|
5183
5493
|
var import_node_path2 = require("path");
|
|
5184
|
-
var
|
|
5494
|
+
var import_playwright4 = require("playwright");
|
|
5185
5495
|
var BROWSERS = [
|
|
5186
5496
|
{
|
|
5187
5497
|
key: "chrome",
|
|
@@ -5215,7 +5525,7 @@ var COOKIE_SIBLINGS = ["", "-wal", "-shm", "-journal"];
|
|
|
5215
5525
|
function findCookieDb(userDataDir, profile) {
|
|
5216
5526
|
for (const rel of [(0, import_node_path2.join)(profile, "Network", "Cookies"), (0, import_node_path2.join)(profile, "Cookies")]) {
|
|
5217
5527
|
const abs = (0, import_node_path2.join)(userDataDir, rel);
|
|
5218
|
-
if ((0,
|
|
5528
|
+
if ((0, import_node_fs4.existsSync)(abs)) return { abs, rel };
|
|
5219
5529
|
}
|
|
5220
5530
|
return null;
|
|
5221
5531
|
}
|
|
@@ -5223,7 +5533,7 @@ function detectImportSources() {
|
|
|
5223
5533
|
const found = [];
|
|
5224
5534
|
for (const browser of BROWSERS) {
|
|
5225
5535
|
for (const userDataDir of userDataRoots(browser)) {
|
|
5226
|
-
if (!(0,
|
|
5536
|
+
if (!(0, import_node_fs4.existsSync)(userDataDir)) continue;
|
|
5227
5537
|
const db = findCookieDb(userDataDir, "Default");
|
|
5228
5538
|
if (db) found.push({ browser, userDataDir, profile: "Default", cookiesPath: db.abs, cookiesRel: db.rel });
|
|
5229
5539
|
}
|
|
@@ -5252,16 +5562,16 @@ function passwordStores() {
|
|
|
5252
5562
|
async function importFromSource(url, source) {
|
|
5253
5563
|
const host = hostOf2(url);
|
|
5254
5564
|
if (!host) throw new Error(`Not a valid URL: ${url}`);
|
|
5255
|
-
const scratch = (0,
|
|
5565
|
+
const scratch = (0, import_node_fs4.mkdtempSync)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "qp-import-"));
|
|
5256
5566
|
try {
|
|
5257
5567
|
const localState = (0, import_node_path2.join)(source.userDataDir, "Local State");
|
|
5258
|
-
if ((0,
|
|
5568
|
+
if ((0, import_node_fs4.existsSync)(localState)) (0, import_node_fs4.copyFileSync)(localState, (0, import_node_path2.join)(scratch, "Local State"));
|
|
5259
5569
|
for (const suffix of COOKIE_SIBLINGS) {
|
|
5260
5570
|
const from = source.cookiesPath + suffix;
|
|
5261
|
-
if (!(0,
|
|
5571
|
+
if (!(0, import_node_fs4.existsSync)(from)) continue;
|
|
5262
5572
|
const to = (0, import_node_path2.join)(scratch, source.cookiesRel + suffix);
|
|
5263
|
-
(0,
|
|
5264
|
-
(0,
|
|
5573
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path2.dirname)(to), { recursive: true });
|
|
5574
|
+
(0, import_node_fs4.copyFileSync)(from, to);
|
|
5265
5575
|
}
|
|
5266
5576
|
for (const store2 of passwordStores()) {
|
|
5267
5577
|
const state = await readState(scratch, source.browser.channel, store2);
|
|
@@ -5277,7 +5587,7 @@ async function importFromSource(url, source) {
|
|
|
5277
5587
|
}
|
|
5278
5588
|
return null;
|
|
5279
5589
|
} finally {
|
|
5280
|
-
(0,
|
|
5590
|
+
(0, import_node_fs4.rmSync)(scratch, { recursive: true, force: true });
|
|
5281
5591
|
}
|
|
5282
5592
|
}
|
|
5283
5593
|
async function readState(profileDir, channel, store2) {
|
|
@@ -5291,7 +5601,7 @@ async function readState(profileDir, channel, store2) {
|
|
|
5291
5601
|
];
|
|
5292
5602
|
if (store2) args.push(`--password-store=${store2}`);
|
|
5293
5603
|
try {
|
|
5294
|
-
const context = await
|
|
5604
|
+
const context = await import_playwright4.chromium.launchPersistentContext(profileDir, { headless: true, channel, args });
|
|
5295
5605
|
try {
|
|
5296
5606
|
return await context.storageState();
|
|
5297
5607
|
} finally {
|
|
@@ -5334,7 +5644,7 @@ function renderLogo(config = {}) {
|
|
|
5334
5644
|
}
|
|
5335
5645
|
|
|
5336
5646
|
// src/utils/config-loader.ts
|
|
5337
|
-
var
|
|
5647
|
+
var import_node_fs10 = require("fs");
|
|
5338
5648
|
var import_promises8 = require("fs/promises");
|
|
5339
5649
|
var import_node_os5 = require("os");
|
|
5340
5650
|
var import_node_path9 = require("path");
|
|
@@ -5351,7 +5661,7 @@ function runRoot() {
|
|
|
5351
5661
|
async function resolveRunFile(name) {
|
|
5352
5662
|
const target = (0, import_node_path9.join)(runRoot(), name);
|
|
5353
5663
|
const source = (0, import_node_path9.join)(process.cwd(), name);
|
|
5354
|
-
if (target !== source && !(0,
|
|
5664
|
+
if (target !== source && !(0, import_node_fs10.existsSync)(target) && (0, import_node_fs10.existsSync)(source)) {
|
|
5355
5665
|
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
|
|
5356
5666
|
await (0, import_promises8.copyFile)(source, target);
|
|
5357
5667
|
}
|
|
@@ -5359,7 +5669,7 @@ async function resolveRunFile(name) {
|
|
|
5359
5669
|
}
|
|
5360
5670
|
async function loadConfig(_url, langOverride) {
|
|
5361
5671
|
const configPath = await resolveRunFile(CONFIG_FILE);
|
|
5362
|
-
if ((0,
|
|
5672
|
+
if ((0, import_node_fs10.existsSync)(configPath)) {
|
|
5363
5673
|
try {
|
|
5364
5674
|
const content = await (0, import_promises8.readFile)(configPath, "utf-8");
|
|
5365
5675
|
const userConfig = JSON.parse(content);
|
|
@@ -5420,7 +5730,7 @@ async function prepareWorkDir(scope = "run") {
|
|
|
5420
5730
|
const safe = scope.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "run";
|
|
5421
5731
|
const root = (0, import_node_path9.join)(runRoot(), WORK_DIR);
|
|
5422
5732
|
const workDir = (0, import_node_path9.join)(root, `${safe}-${process.pid}`);
|
|
5423
|
-
if ((0,
|
|
5733
|
+
if ((0, import_node_fs10.existsSync)(workDir)) {
|
|
5424
5734
|
await (0, import_promises8.rm)(workDir, { recursive: true });
|
|
5425
5735
|
}
|
|
5426
5736
|
await (0, import_promises8.mkdir)(workDir, { recursive: true });
|
|
@@ -5447,7 +5757,7 @@ var USER_DATA_DIR = (0, import_node_path9.join)((0, import_node_os5.homedir)(),
|
|
|
5447
5757
|
var USER_DATA_FILE = (0, import_node_path9.join)(USER_DATA_DIR, "user.json");
|
|
5448
5758
|
async function loadUserData() {
|
|
5449
5759
|
try {
|
|
5450
|
-
if ((0,
|
|
5760
|
+
if ((0, import_node_fs10.existsSync)(USER_DATA_FILE)) {
|
|
5451
5761
|
return JSON.parse(await (0, import_promises8.readFile)(USER_DATA_FILE, "utf-8"));
|
|
5452
5762
|
}
|
|
5453
5763
|
} catch {
|
|
@@ -5455,7 +5765,7 @@ async function loadUserData() {
|
|
|
5455
5765
|
return {};
|
|
5456
5766
|
}
|
|
5457
5767
|
async function saveUserData(userData) {
|
|
5458
|
-
if (!(0,
|
|
5768
|
+
if (!(0, import_node_fs10.existsSync)(USER_DATA_DIR)) {
|
|
5459
5769
|
await (0, import_promises8.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
5460
5770
|
}
|
|
5461
5771
|
const existing = await loadUserData();
|
|
@@ -5524,7 +5834,7 @@ function parseCaptionCSS(css) {
|
|
|
5524
5834
|
}
|
|
5525
5835
|
async function loadCaptions() {
|
|
5526
5836
|
const captionsPath = await resolveRunFile("captions.css");
|
|
5527
|
-
if (!(0,
|
|
5837
|
+
if (!(0, import_node_fs10.existsSync)(captionsPath)) {
|
|
5528
5838
|
await (0, import_promises8.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
5529
5839
|
return DEFAULT_CAPTIONS;
|
|
5530
5840
|
}
|
|
@@ -5538,7 +5848,10 @@ async function loadCaptions() {
|
|
|
5538
5848
|
async function savePlan(plan, planPath) {
|
|
5539
5849
|
const cleanPlan = {
|
|
5540
5850
|
...plan,
|
|
5541
|
-
steps: plan.steps.map(({ duration, ...step }) =>
|
|
5851
|
+
steps: plan.steps.map(({ duration, ...step }) => ({
|
|
5852
|
+
...step,
|
|
5853
|
+
...typeof duration === "number" && duration > 0 ? { duration: Math.min(duration, 30) } : {}
|
|
5854
|
+
}))
|
|
5542
5855
|
};
|
|
5543
5856
|
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(planPath), { recursive: true });
|
|
5544
5857
|
await (0, import_promises8.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
@@ -5647,6 +5960,11 @@ async function showHelp() {
|
|
|
5647
5960
|
`-s, --script <file>`,
|
|
5648
5961
|
"Narration script (one caption per line)"
|
|
5649
5962
|
],
|
|
5963
|
+
[
|
|
5964
|
+
`${opt("--draft")}, ${opt("-dr")}`,
|
|
5965
|
+
`--draft, -dr`,
|
|
5966
|
+
"Rough cut first: small, silent, quick. Re-run without it for the real one"
|
|
5967
|
+
],
|
|
5650
5968
|
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
5651
5969
|
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
5652
5970
|
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"],
|
|
@@ -5744,7 +6062,7 @@ function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
|
|
|
5744
6062
|
)
|
|
5745
6063
|
);
|
|
5746
6064
|
}
|
|
5747
|
-
return new Promise((
|
|
6065
|
+
return new Promise((resolve11, reject) => {
|
|
5748
6066
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
5749
6067
|
let done = false;
|
|
5750
6068
|
rl.on("close", () => {
|
|
@@ -5755,7 +6073,7 @@ function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
|
|
|
5755
6073
|
rl.question(prompt2, () => {
|
|
5756
6074
|
done = true;
|
|
5757
6075
|
rl.close();
|
|
5758
|
-
|
|
6076
|
+
resolve11();
|
|
5759
6077
|
});
|
|
5760
6078
|
});
|
|
5761
6079
|
}
|
|
@@ -5897,8 +6215,8 @@ function editorBaseUrl() {
|
|
|
5897
6215
|
function openInBrowser(url) {
|
|
5898
6216
|
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
5899
6217
|
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, () =>
|
|
6218
|
+
return new Promise((resolve11) => {
|
|
6219
|
+
(0, import_node_child_process5.exec)(command, () => resolve11());
|
|
5902
6220
|
});
|
|
5903
6221
|
}
|
|
5904
6222
|
|
|
@@ -5906,13 +6224,13 @@ function openInBrowser(url) {
|
|
|
5906
6224
|
init_SparkStripe_feature_gating();
|
|
5907
6225
|
var TIMEOUT_MS = 12e4;
|
|
5908
6226
|
async function browserLogin() {
|
|
5909
|
-
return new Promise((
|
|
6227
|
+
return new Promise((resolve11) => {
|
|
5910
6228
|
let server;
|
|
5911
6229
|
let timer;
|
|
5912
6230
|
const cleanup = (email) => {
|
|
5913
6231
|
clearTimeout(timer);
|
|
5914
6232
|
server?.close();
|
|
5915
|
-
|
|
6233
|
+
resolve11(email);
|
|
5916
6234
|
};
|
|
5917
6235
|
server = (0, import_node_http2.createServer)((req, res) => {
|
|
5918
6236
|
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
@@ -6063,10 +6381,10 @@ function getLastRateLimitInfo() {
|
|
|
6063
6381
|
}
|
|
6064
6382
|
function prompt(question) {
|
|
6065
6383
|
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
6066
|
-
return new Promise((
|
|
6384
|
+
return new Promise((resolve11) => {
|
|
6067
6385
|
rl.question(question, (answer) => {
|
|
6068
6386
|
rl.close();
|
|
6069
|
-
|
|
6387
|
+
resolve11(answer.trim());
|
|
6070
6388
|
});
|
|
6071
6389
|
});
|
|
6072
6390
|
}
|
|
@@ -6075,30 +6393,27 @@ async function setupUserEmail() {
|
|
|
6075
6393
|
console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
|
|
6076
6394
|
console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
|
|
6077
6395
|
console.log();
|
|
6078
|
-
console.log(import_chalk5.default.white("
|
|
6079
|
-
console.log(import_chalk5.default.white("
|
|
6080
|
-
|
|
6081
|
-
|
|
6396
|
+
console.log(import_chalk5.default.white(" The AI that plans your demo runs on our servers, so it"));
|
|
6397
|
+
console.log(import_chalk5.default.white(" needs an address to meter. No card, and no verification"));
|
|
6398
|
+
console.log(import_chalk5.default.white(" needed for your first 5 videos."));
|
|
6399
|
+
console.log(import_chalk5.default.dim(" After that, signing in gets you 10 videos a day."));
|
|
6400
|
+
console.log();
|
|
6401
|
+
console.log(import_chalk5.default.dim(" Type your email, or 'b' to sign in through a browser."));
|
|
6402
|
+
const email = await prompt(import_chalk5.default.white(" Email: "));
|
|
6403
|
+
if (!email) return null;
|
|
6404
|
+
if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
|
|
6405
|
+
const browserEmail = await browserLogin();
|
|
6406
|
+
if (!browserEmail) return null;
|
|
6082
6407
|
console.log();
|
|
6083
6408
|
console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
|
|
6084
6409
|
console.log();
|
|
6085
6410
|
return browserEmail;
|
|
6086
6411
|
}
|
|
6087
|
-
console.log();
|
|
6088
|
-
console.log(import_chalk5.default.dim(" Or enter your email manually:"));
|
|
6089
|
-
const email = await prompt(import_chalk5.default.white(" Email: "));
|
|
6090
|
-
if (!email) {
|
|
6091
|
-
console.log();
|
|
6092
|
-
console.log(import_chalk5.default.red(" No email provided. Exiting."));
|
|
6093
|
-
process.exit(1);
|
|
6094
|
-
}
|
|
6095
6412
|
if (!email.includes("@") || !email.includes(".")) {
|
|
6096
6413
|
console.log();
|
|
6097
6414
|
console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
6098
6415
|
const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
|
|
6099
|
-
if (proceed.toLowerCase() !== "y")
|
|
6100
|
-
process.exit(1);
|
|
6101
|
-
}
|
|
6416
|
+
if (proceed.toLowerCase() !== "y") return null;
|
|
6102
6417
|
}
|
|
6103
6418
|
console.log();
|
|
6104
6419
|
console.log(import_chalk5.default.green(" \u2713 Email registered"));
|
|
@@ -6570,15 +6885,15 @@ async function launchCaptureChrome() {
|
|
|
6570
6885
|
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
6571
6886
|
const windowsHost = getWindowsHostIP();
|
|
6572
6887
|
const probe = await startRelay(nodePath, windowsHost, debugPort);
|
|
6573
|
-
const alreadyUp = await new Promise((
|
|
6888
|
+
const alreadyUp = await new Promise((resolve11) => {
|
|
6574
6889
|
const req = (0, import_node_http3.request)({ host: windowsHost, port: probe.port, path: "/json/version", method: "GET" }, (res) => {
|
|
6575
6890
|
res.resume();
|
|
6576
|
-
|
|
6891
|
+
resolve11(true);
|
|
6577
6892
|
});
|
|
6578
|
-
req.on("error", () =>
|
|
6893
|
+
req.on("error", () => resolve11(false));
|
|
6579
6894
|
req.setTimeout(2e3, () => {
|
|
6580
6895
|
req.destroy();
|
|
6581
|
-
|
|
6896
|
+
resolve11(false);
|
|
6582
6897
|
});
|
|
6583
6898
|
req.end();
|
|
6584
6899
|
});
|
|
@@ -6784,7 +7099,7 @@ async function hasHeadroom() {
|
|
|
6784
7099
|
async function withRenderSlot(render, onWait) {
|
|
6785
7100
|
if (active >= MAX_CONCURRENT_RENDERS) {
|
|
6786
7101
|
onWait?.(`waiting for a render slot (${active}/${MAX_CONCURRENT_RENDERS} busy)`);
|
|
6787
|
-
await new Promise((
|
|
7102
|
+
await new Promise((resolve11) => waiting.push(resolve11));
|
|
6788
7103
|
}
|
|
6789
7104
|
active++;
|
|
6790
7105
|
try {
|
|
@@ -6813,19 +7128,19 @@ init_lib();
|
|
|
6813
7128
|
init_helpers();
|
|
6814
7129
|
|
|
6815
7130
|
// src/record.ts
|
|
6816
|
-
var
|
|
7131
|
+
var import_node_fs15 = require("fs");
|
|
6817
7132
|
var import_promises13 = require("fs/promises");
|
|
6818
7133
|
var import_node_path17 = require("path");
|
|
6819
7134
|
var import_chalk6 = __toESM(require("chalk"));
|
|
6820
7135
|
|
|
6821
7136
|
// src/browser/actions.ts
|
|
6822
|
-
var
|
|
7137
|
+
var import_node_fs12 = require("fs");
|
|
6823
7138
|
var import_promises11 = require("fs/promises");
|
|
6824
7139
|
var import_node_path13 = require("path");
|
|
6825
7140
|
init_lib();
|
|
6826
7141
|
|
|
6827
7142
|
// src/browser/effects.ts
|
|
6828
|
-
var
|
|
7143
|
+
var import_node_fs11 = require("fs");
|
|
6829
7144
|
var import_node_path12 = require("path");
|
|
6830
7145
|
init_lib();
|
|
6831
7146
|
|
|
@@ -6846,7 +7161,7 @@ function loadHighlightCSS() {
|
|
|
6846
7161
|
// src/browser/ → src/
|
|
6847
7162
|
];
|
|
6848
7163
|
for (const p of candidates) {
|
|
6849
|
-
if ((0,
|
|
7164
|
+
if ((0, import_node_fs11.existsSync)(p)) return (0, import_node_fs11.readFileSync)(p, "utf-8");
|
|
6850
7165
|
}
|
|
6851
7166
|
return "";
|
|
6852
7167
|
}
|
|
@@ -7059,6 +7374,7 @@ function locatorFor(page, keyword) {
|
|
|
7059
7374
|
var WAIT_MS = 450;
|
|
7060
7375
|
var HANDLE_MS = 3e3;
|
|
7061
7376
|
var RESOLVE_BUDGET_MS2 = 2e4;
|
|
7377
|
+
var DOM_POLL_MS = 150;
|
|
7062
7378
|
function remaining(deadline) {
|
|
7063
7379
|
return Math.max(1, deadline - Date.now());
|
|
7064
7380
|
}
|
|
@@ -7169,7 +7485,7 @@ async function resolveElementSettled(page, target, deadlineMs = RESOLVE_DEADLINE
|
|
|
7169
7485
|
}
|
|
7170
7486
|
}
|
|
7171
7487
|
async function resolvePlainText(page, text, deadline) {
|
|
7172
|
-
|
|
7488
|
+
let scanned = await domTextMatch(page, text);
|
|
7173
7489
|
const stages = plainTextStages(page, text);
|
|
7174
7490
|
while (Date.now() < deadline) {
|
|
7175
7491
|
for (const { locator, state } of stages) {
|
|
@@ -7180,6 +7496,12 @@ async function resolvePlainText(page, text, deadline) {
|
|
|
7180
7496
|
}
|
|
7181
7497
|
}
|
|
7182
7498
|
if (scanned) return scanned;
|
|
7499
|
+
while (Date.now() < deadline) {
|
|
7500
|
+
await page.waitForTimeout(Math.min(DOM_POLL_MS, remaining(deadline)));
|
|
7501
|
+
scanned = await domTextMatch(page, text);
|
|
7502
|
+
if (scanned) break;
|
|
7503
|
+
}
|
|
7504
|
+
if (!scanned) return null;
|
|
7183
7505
|
}
|
|
7184
7506
|
return scanned ?? null;
|
|
7185
7507
|
}
|
|
@@ -7605,7 +7927,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7605
7927
|
if (!isPlaceholder) {
|
|
7606
7928
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
7607
7929
|
const filePath = (0, import_node_path13.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path13.resolve)(baseDir, normalizedPath);
|
|
7608
|
-
if ((0,
|
|
7930
|
+
if ((0, import_node_fs12.existsSync)(filePath)) {
|
|
7609
7931
|
await fileInput.setInputFiles(filePath);
|
|
7610
7932
|
await page.waitForTimeout(2e3);
|
|
7611
7933
|
break;
|
|
@@ -7722,7 +8044,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7722
8044
|
break;
|
|
7723
8045
|
case "image": {
|
|
7724
8046
|
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,
|
|
8047
|
+
if (imagePath && (0, import_node_fs12.existsSync)(imagePath)) {
|
|
7726
8048
|
const imgStyle = step.target === "stretch" ? "width: 100vw; height: 100vh; object-fit: cover;" : "max-width: 90%; max-height: 90%;";
|
|
7727
8049
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected, false)) injected = true;
|
|
7728
8050
|
}
|
|
@@ -7751,7 +8073,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir, pr
|
|
|
7751
8073
|
await pendingDownload;
|
|
7752
8074
|
pendingDownload = null;
|
|
7753
8075
|
}
|
|
7754
|
-
if (lastDownloadPath && (0,
|
|
8076
|
+
if (lastDownloadPath && (0, import_node_fs12.existsSync)(lastDownloadPath)) {
|
|
7755
8077
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
7756
8078
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
7757
8079
|
}
|
|
@@ -7857,7 +8179,7 @@ async function getPageElementSelectors(page, query, limit, keyword = "") {
|
|
|
7857
8179
|
}
|
|
7858
8180
|
|
|
7859
8181
|
// src/browser/step-validator.ts
|
|
7860
|
-
var
|
|
8182
|
+
var import_node_fs13 = require("fs");
|
|
7861
8183
|
var import_node_path14 = require("path");
|
|
7862
8184
|
init_crawl();
|
|
7863
8185
|
init_utils();
|
|
@@ -7865,9 +8187,9 @@ function findMediaFile(filename, baseDir) {
|
|
|
7865
8187
|
const name = (0, import_node_path14.basename)(filename);
|
|
7866
8188
|
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
8189
|
for (const dir of searchDirs) {
|
|
7868
|
-
if (!(0,
|
|
8190
|
+
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
7869
8191
|
try {
|
|
7870
|
-
const files = (0,
|
|
8192
|
+
const files = (0, import_node_fs13.readdirSync)(dir);
|
|
7871
8193
|
if (files.includes(name)) return (0, import_node_path14.join)(dir, name);
|
|
7872
8194
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
7873
8195
|
if (match) return (0, import_node_path14.join)(dir, match);
|
|
@@ -7919,7 +8241,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
7919
8241
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
7920
8242
|
}
|
|
7921
8243
|
const specifiedPath = (0, import_node_path14.isAbsolute)(step.value) ? step.value : (0, import_node_path14.resolve)(baseDir, step.value);
|
|
7922
|
-
if ((0,
|
|
8244
|
+
if ((0, import_node_fs13.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
7923
8245
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
7924
8246
|
if (foundPath) {
|
|
7925
8247
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -8048,7 +8370,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
8048
8370
|
if (!isPlaceholder) {
|
|
8049
8371
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
8050
8372
|
let filePath = (0, import_node_path14.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path14.resolve)(baseDir, normalizedPath);
|
|
8051
|
-
if (!(0,
|
|
8373
|
+
if (!(0, import_node_fs13.existsSync)(filePath)) {
|
|
8052
8374
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
8053
8375
|
if (foundPath) {
|
|
8054
8376
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -8190,7 +8512,7 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
8190
8512
|
}
|
|
8191
8513
|
|
|
8192
8514
|
// src/browser/meta.ts
|
|
8193
|
-
var
|
|
8515
|
+
var import_node_fs14 = require("fs");
|
|
8194
8516
|
var import_promises12 = require("fs/promises");
|
|
8195
8517
|
var import_node_path16 = require("path");
|
|
8196
8518
|
var RECORDING_FILE = "recording.mp4";
|
|
@@ -8200,7 +8522,7 @@ function getRecordingPath(outputDir) {
|
|
|
8200
8522
|
}
|
|
8201
8523
|
async function loadRecordingMeta(outputDir) {
|
|
8202
8524
|
const metaPath = (0, import_node_path16.join)(outputDir, RECORDING_META_FILE);
|
|
8203
|
-
if (!(0,
|
|
8525
|
+
if (!(0, import_node_fs14.existsSync)(metaPath)) return null;
|
|
8204
8526
|
try {
|
|
8205
8527
|
const content = await (0, import_promises12.readFile)(metaPath, "utf-8");
|
|
8206
8528
|
return JSON.parse(content);
|
|
@@ -8318,11 +8640,33 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8318
8640
|
const authState = findAuthState(plan.url);
|
|
8319
8641
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
8320
8642
|
try {
|
|
8643
|
+
const isExtensionPage = plan.url.startsWith("chrome-extension://");
|
|
8644
|
+
if (!isExtensionPage) {
|
|
8645
|
+
const warm = await context.newPage();
|
|
8646
|
+
const warmVideo = warm.video();
|
|
8647
|
+
const warmUrls = [plan.url];
|
|
8648
|
+
for (const step of plan.steps) {
|
|
8649
|
+
if (step.action !== "navigate" || !step.target) continue;
|
|
8650
|
+
if (!warmUrls.includes(step.target)) warmUrls.push(step.target);
|
|
8651
|
+
}
|
|
8652
|
+
try {
|
|
8653
|
+
for (const url of warmUrls) {
|
|
8654
|
+
await gotoSettled(warm, url).catch(() => {
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
} catch {
|
|
8658
|
+
} finally {
|
|
8659
|
+
await warm.close().catch(() => {
|
|
8660
|
+
});
|
|
8661
|
+
const stray = await warmVideo?.path().catch(() => void 0);
|
|
8662
|
+
if (stray) await (0, import_promises13.rm)(stray, { force: true }).catch(() => {
|
|
8663
|
+
});
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8321
8666
|
const page = context.pages()[0] || await context.newPage();
|
|
8322
8667
|
setupDownloadTracking(page);
|
|
8323
8668
|
context.on("page", (p) => setupDownloadTracking(p));
|
|
8324
8669
|
const firstStepIsMedia = ["media", "image", "video"].includes(plan.steps[0]?.action ?? "");
|
|
8325
|
-
const isExtensionPage = plan.url.startsWith("chrome-extension://");
|
|
8326
8670
|
let signInChecked = false;
|
|
8327
8671
|
const guardSignedIn = async () => {
|
|
8328
8672
|
if (signInChecked || opts.allowLoggedOut || isExtensionPage) return;
|
|
@@ -8399,7 +8743,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8399
8743
|
if (!targetEl) {
|
|
8400
8744
|
console.log(
|
|
8401
8745
|
`
|
|
8402
|
-
\u26A0 target "${step.target}" not found on ${page.url()}
|
|
8746
|
+
\u26A0 target "${step.target}" not found on ${page.url()}; step ${stepNum} recorded without the action`
|
|
8403
8747
|
);
|
|
8404
8748
|
}
|
|
8405
8749
|
}
|
|
@@ -8538,7 +8882,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8538
8882
|
if (!pageClosedEarly) await page.close();
|
|
8539
8883
|
const videoPath = await video.path();
|
|
8540
8884
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
8541
|
-
if (!videoPath || !(0,
|
|
8885
|
+
if (!videoPath || !(0, import_node_fs15.existsSync)(videoPath)) {
|
|
8542
8886
|
throw new Error("Video file was not created");
|
|
8543
8887
|
}
|
|
8544
8888
|
saveTask.succeed("Video saved");
|
|
@@ -8549,14 +8893,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
8549
8893
|
}
|
|
8550
8894
|
|
|
8551
8895
|
// src/utils/video-pipeline.ts
|
|
8552
|
-
var
|
|
8896
|
+
var import_node_fs17 = require("fs");
|
|
8553
8897
|
var import_promises15 = require("fs/promises");
|
|
8554
8898
|
var import_node_path21 = require("path");
|
|
8555
8899
|
var import_chalk9 = __toESM(require("chalk"));
|
|
8556
8900
|
init_lib();
|
|
8557
8901
|
|
|
8558
8902
|
// src/utils/music.ts
|
|
8559
|
-
var
|
|
8903
|
+
var import_node_fs16 = require("fs");
|
|
8560
8904
|
var import_node_path20 = require("path");
|
|
8561
8905
|
|
|
8562
8906
|
// src/commands/utils/record-helpers.ts
|
|
@@ -8573,6 +8917,17 @@ var import_playwright6 = require("playwright");
|
|
|
8573
8917
|
init_mcp_context();
|
|
8574
8918
|
init_lib();
|
|
8575
8919
|
init_json_repair();
|
|
8920
|
+
var WRITTEN_ADDRESS = /\b(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s]*)?/gi;
|
|
8921
|
+
var SPELLED_OUT_ACRONYM = /\b[A-Z](?:\s+[A-Z])+\b/g;
|
|
8922
|
+
function spokenCaption(caption) {
|
|
8923
|
+
const held = [];
|
|
8924
|
+
const masked = caption.replace(SPELLED_OUT_ACRONYM, (run2) => run2.replace(/\s+/g, "")).replace(WRITTEN_ADDRESS, (address) => {
|
|
8925
|
+
held.push(address);
|
|
8926
|
+
return `qpaddr${held.length - 1}qp`;
|
|
8927
|
+
});
|
|
8928
|
+
const shaped = toTitleCase((0, import_video_kit8.toSpokenForm)(masked));
|
|
8929
|
+
return shaped.replace(/qpaddr(\d+)qp/gi, (_match, index) => held[Number(index)] ?? "");
|
|
8930
|
+
}
|
|
8576
8931
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
8577
8932
|
const browserTask = task2("Launching browser with extension");
|
|
8578
8933
|
const userDataDir = (0, import_node_path18.join)(workDir, ".browser-data");
|
|
@@ -8701,8 +9056,18 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
8701
9056
|
// The spoken form is what the voice will actually say, so it is what
|
|
8702
9057
|
// the caption shows: burning the written form instead left the words on
|
|
8703
9058
|
// screen out of step with the words in the ear wherever the two differ
|
|
8704
|
-
// (a
|
|
8705
|
-
|
|
9059
|
+
// (a number read as words).
|
|
9060
|
+
//
|
|
9061
|
+
// Addresses are the exception. Expanding one here turned "quickpeek.co"
|
|
9062
|
+
// into three caption words, "Quickpeek Dot Co", which the line packer
|
|
9063
|
+
// is then free to break across lines: the call to action arrived on
|
|
9064
|
+
// screen as "DOT C O" with the name on the row above. An address is the
|
|
9065
|
+
// one thing in a demo a viewer has to be able to read and type, so it
|
|
9066
|
+
// is held back verbatim from both passes and rejoins as written. The
|
|
9067
|
+
// voice is unaffected: generate.ts derives its own spoken form from
|
|
9068
|
+
// this caption at synthesis time, and word timings fall back to an even
|
|
9069
|
+
// spread across the speech envelope when the counts no longer line up.
|
|
9070
|
+
caption: s.caption ? spokenCaption(s.caption) : s.caption
|
|
8706
9071
|
}));
|
|
8707
9072
|
aiTask.succeed(`Plan: ${plan.steps.length} steps`);
|
|
8708
9073
|
return plan;
|
|
@@ -8874,12 +9239,12 @@ function getOutputDirFromUrl(url) {
|
|
|
8874
9239
|
}
|
|
8875
9240
|
|
|
8876
9241
|
// src/utils/music.ts
|
|
8877
|
-
var DEFAULT_MOOD = "
|
|
9242
|
+
var DEFAULT_MOOD = "corporate";
|
|
8878
9243
|
function musicDir() {
|
|
8879
9244
|
let dir = getScriptDir();
|
|
8880
9245
|
for (let i = 0; i < 6; i++) {
|
|
8881
9246
|
const candidate = (0, import_node_path20.join)(dir, "assets", "music");
|
|
8882
|
-
if ((0,
|
|
9247
|
+
if ((0, import_node_fs16.existsSync)((0, import_node_path20.join)(candidate, "manifest.json"))) return candidate;
|
|
8883
9248
|
const parent = (0, import_node_path20.dirname)(dir);
|
|
8884
9249
|
if (parent === dir) break;
|
|
8885
9250
|
dir = parent;
|
|
@@ -8890,9 +9255,9 @@ function listBundledMusic() {
|
|
|
8890
9255
|
const dir = musicDir();
|
|
8891
9256
|
if (!dir) return [];
|
|
8892
9257
|
try {
|
|
8893
|
-
const parsed = JSON.parse((0,
|
|
9258
|
+
const parsed = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path20.join)(dir, "manifest.json"), "utf8"));
|
|
8894
9259
|
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,
|
|
9260
|
+
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
9261
|
} catch {
|
|
8897
9262
|
return [];
|
|
8898
9263
|
}
|
|
@@ -8905,6 +9270,16 @@ function resolveBundledBed(mood = DEFAULT_MOOD) {
|
|
|
8905
9270
|
|
|
8906
9271
|
// src/utils/video-pipeline.ts
|
|
8907
9272
|
var INDENT3 = " ";
|
|
9273
|
+
function captionWordTimings(caption, spoken, duration) {
|
|
9274
|
+
const captionWords = caption.split(/\s+/).filter(Boolean);
|
|
9275
|
+
if (spoken.length === 0 || captionWords.length === 0) {
|
|
9276
|
+
return estimateWordTimings(caption, duration);
|
|
9277
|
+
}
|
|
9278
|
+
if (spoken.length === captionWords.length) {
|
|
9279
|
+
return spoken.map((w, i) => ({ ...w, word: captionWords[i] ?? w.word }));
|
|
9280
|
+
}
|
|
9281
|
+
return estimateWordTimings(caption, duration);
|
|
9282
|
+
}
|
|
8908
9283
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
8909
9284
|
const cues = [];
|
|
8910
9285
|
let currentTime = VOICE_LEAD_MS;
|
|
@@ -8915,7 +9290,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
8915
9290
|
currentTime += config.timing.minStepDuration;
|
|
8916
9291
|
continue;
|
|
8917
9292
|
}
|
|
8918
|
-
const words =
|
|
9293
|
+
const words = captionWordTimings(step.caption || "", audio.words, audio.duration);
|
|
8919
9294
|
if (words.length > 0 && !step.spokenOnly) {
|
|
8920
9295
|
const offset = currentTime;
|
|
8921
9296
|
cues.push({
|
|
@@ -8935,7 +9310,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
8935
9310
|
return assPath;
|
|
8936
9311
|
}
|
|
8937
9312
|
var STILL_EXTENSIONS = ["png", "jpg", "jpeg", "webp"];
|
|
8938
|
-
function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
9313
|
+
function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0, totalSecs, blackSpans) {
|
|
8939
9314
|
const overlays = [];
|
|
8940
9315
|
let cumulativeTime = leadInSecs;
|
|
8941
9316
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -8949,12 +9324,19 @@ function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
|
8949
9324
|
const still = isCard && STILL_EXTENSIONS.includes(ext);
|
|
8950
9325
|
if (still || isClip && ["mp4", "webm", "mov"].includes(ext)) {
|
|
8951
9326
|
const mediaPath = (0, import_node_path21.isAbsolute)(step.value) ? step.value : (0, import_node_path21.resolve)(baseDir, step.value);
|
|
8952
|
-
if ((0,
|
|
9327
|
+
if ((0, import_node_fs17.existsSync)(mediaPath) && durationSec > 0) {
|
|
8953
9328
|
const partial = parsePartial(step.partial);
|
|
9329
|
+
const runsToEnd = still && i === steps.length - 1 && totalSecs !== void 0;
|
|
9330
|
+
let duration = runsToEnd ? Math.max(durationSec, totalSecs - cumulativeTime) : durationSec;
|
|
9331
|
+
if (still && !runsToEnd && blackSpans) {
|
|
9332
|
+
const endsAt = cumulativeTime + duration;
|
|
9333
|
+
const following = blackSpans.find((sp) => sp.start <= endsAt + 0.25 && sp.end > endsAt);
|
|
9334
|
+
if (following) duration = following.end - cumulativeTime;
|
|
9335
|
+
}
|
|
8954
9336
|
overlays.push({
|
|
8955
9337
|
path: mediaPath,
|
|
8956
9338
|
startTime: cumulativeTime,
|
|
8957
|
-
duration
|
|
9339
|
+
duration,
|
|
8958
9340
|
mode: step.target === "stretch" ? "stretch" : step.target === "cover" ? "cover" : "center",
|
|
8959
9341
|
...still ? { still: true } : {},
|
|
8960
9342
|
...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
|
|
@@ -8967,7 +9349,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir, leadInSecs = 0) {
|
|
|
8967
9349
|
}
|
|
8968
9350
|
return overlays;
|
|
8969
9351
|
}
|
|
8970
|
-
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, hasClipAudio = false, endsOnCreditsCard = false, _stepSync) {
|
|
9352
|
+
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, hasClipAudio = false, endsOnCreditsCard = false, _stepSync, musicStartSecs = 0) {
|
|
8971
9353
|
header("Compose");
|
|
8972
9354
|
const configPath = (0, import_node_path21.join)(runRoot(), CONFIG_FILE);
|
|
8973
9355
|
console.log(`${INDENT3}${import_chalk9.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
@@ -8983,9 +9365,9 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
8983
9365
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
8984
9366
|
const finalPath = (0, import_node_path21.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
8985
9367
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
8986
|
-
const hasVoiceover = audioPath && (0,
|
|
9368
|
+
const hasVoiceover = audioPath && (0, import_node_fs17.existsSync)(audioPath);
|
|
8987
9369
|
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,
|
|
9370
|
+
let hasMusic = musicPath && (0, import_node_fs17.existsSync)(musicPath);
|
|
8989
9371
|
let bedPath = "";
|
|
8990
9372
|
let bedCredit = "";
|
|
8991
9373
|
if (!hasMusic && config.audio?.ambience !== false) {
|
|
@@ -9037,7 +9419,14 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
9037
9419
|
hasClipAudio,
|
|
9038
9420
|
mainContentDuration,
|
|
9039
9421
|
stepTransitions,
|
|
9040
|
-
hwaccel: config.video.hwaccel
|
|
9422
|
+
hwaccel: config.video.hwaccel,
|
|
9423
|
+
// The tempo pass runs after this and speeds the whole file up, music
|
|
9424
|
+
// included. Told about it, compose lays the bed down pre-slowed so the
|
|
9425
|
+
// song comes out at its own speed.
|
|
9426
|
+
tempo: config.video.tempo,
|
|
9427
|
+
musicStartSecs,
|
|
9428
|
+
// So the bed can be landed with its last note on the last frame.
|
|
9429
|
+
musicDurationSecs: hasMusic ? await getMediaDuration(backingPath).then((ms) => ms / 1e3).catch(() => 0) : 0
|
|
9041
9430
|
});
|
|
9042
9431
|
composeTask.succeed("Video composed");
|
|
9043
9432
|
return finalPath;
|
|
@@ -9110,7 +9499,7 @@ function resolveOutro(config, userTier, userEmail) {
|
|
|
9110
9499
|
const fallback = (0, import_node_path23.join)(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
9111
9500
|
return (0, import_node_fs18.existsSync)(fallback) ? fallback : void 0;
|
|
9112
9501
|
}
|
|
9113
|
-
var EMPTY_MIN_SECS = 0.
|
|
9502
|
+
var EMPTY_MIN_SECS = 0.4;
|
|
9114
9503
|
var OUTRO_TAIL_MS = 900;
|
|
9115
9504
|
var FIT_SECOND_PASS_CUSHION_SECS = 1;
|
|
9116
9505
|
var FIT_SECOND_PASS_MAX_SPEEDUP = 1.45;
|
|
@@ -9140,6 +9529,7 @@ async function renderVideoPhase(params) {
|
|
|
9140
9529
|
const firstStep = plan.steps[0];
|
|
9141
9530
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
9142
9531
|
let trimStart = 0;
|
|
9532
|
+
let recordedStepLengths = null;
|
|
9143
9533
|
const doRecord = async () => {
|
|
9144
9534
|
header("Record");
|
|
9145
9535
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError, {
|
|
@@ -9151,6 +9541,7 @@ async function renderVideoPhase(params) {
|
|
|
9151
9541
|
]);
|
|
9152
9542
|
const detectedEmpty = mergeSpans([...result.leadSpans ?? [], ...emptyBlack, ...emptyWhite]);
|
|
9153
9543
|
let stepLengths = result.actualStepDurations;
|
|
9544
|
+
let zeroAnchored = false;
|
|
9154
9545
|
if (detectedEmpty.length > 0) {
|
|
9155
9546
|
const bounds = [];
|
|
9156
9547
|
let at = 0;
|
|
@@ -9175,6 +9566,7 @@ async function renderVideoPhase(params) {
|
|
|
9175
9566
|
const sum = result.actualStepDurations.reduce((a, b2) => a + b2, 0);
|
|
9176
9567
|
const factor = sum > 0 && realSecs > 0 ? realSecs * 1e3 / sum : 1;
|
|
9177
9568
|
stepLengths = result.actualStepDurations.map((ms) => ms * factor);
|
|
9569
|
+
zeroAnchored = true;
|
|
9178
9570
|
info(`Sped up ${emptySecs.toFixed(1)}s of loading screens at ${EMPTY_SPAN_SPEED}x`);
|
|
9179
9571
|
} else {
|
|
9180
9572
|
await discardPartial(retimed);
|
|
@@ -9191,16 +9583,19 @@ async function renderVideoPhase(params) {
|
|
|
9191
9583
|
const meta = buildRecordingMeta(plan, config, stepDurations);
|
|
9192
9584
|
meta.actualStepDurations = stepLengths;
|
|
9193
9585
|
meta.pageLoadTime = result.pageLoadTime;
|
|
9586
|
+
meta.zeroAnchored = zeroAnchored;
|
|
9194
9587
|
await saveRecordingMeta(config.output.dir, meta);
|
|
9195
9588
|
videoPath = recordingPath;
|
|
9196
|
-
|
|
9589
|
+
recordedStepLengths = stepLengths;
|
|
9590
|
+
trimStart = firstStepIsVideo || zeroAnchored ? 0 : result.pageLoadTime;
|
|
9197
9591
|
};
|
|
9198
9592
|
if (hasExistingRecording) {
|
|
9199
9593
|
const reuseCheck = canReuseRecording(plan, config, stepDurations, existingRecordingMeta);
|
|
9200
9594
|
if (reuseCheck.canReuse) {
|
|
9201
9595
|
info("Reusing existing recording");
|
|
9202
9596
|
videoPath = recordingPath;
|
|
9203
|
-
|
|
9597
|
+
recordedStepLengths = existingRecordingMeta.actualStepDurations ?? existingRecordingMeta.stepDurations;
|
|
9598
|
+
trimStart = firstStepIsVideo || existingRecordingMeta.zeroAnchored ? 0 : existingRecordingMeta?.pageLoadTime ?? 0.5;
|
|
9204
9599
|
stepAudio = await alignVoiceoverToRecording(
|
|
9205
9600
|
stepAudio,
|
|
9206
9601
|
existingRecordingMeta.actualStepDurations ?? existingRecordingMeta.stepDurations,
|
|
@@ -9214,9 +9609,23 @@ async function renderVideoPhase(params) {
|
|
|
9214
9609
|
} else {
|
|
9215
9610
|
await doRecord();
|
|
9216
9611
|
}
|
|
9217
|
-
const alignedStepDurations = plan.steps.map(
|
|
9612
|
+
const alignedStepDurations = plan.steps.map(
|
|
9613
|
+
(step, i) => Math.max(
|
|
9614
|
+
stepAudio.get(i + 1)?.duration ?? recordedStepLengths?.[i] ?? stepDurations[i] ?? 0,
|
|
9615
|
+
step.duration ? step.duration * 1e3 : 0
|
|
9616
|
+
)
|
|
9617
|
+
);
|
|
9218
9618
|
let splicedClipAudio = false;
|
|
9219
|
-
const
|
|
9619
|
+
const recordingSecs = await getMediaDuration(videoPath).then((ms) => ms / 1e3).catch(() => void 0);
|
|
9620
|
+
const blackSpans = await detectBlackSpans(videoPath, 0.3).catch(() => []);
|
|
9621
|
+
const videoOverlays = buildVideoOverlays(
|
|
9622
|
+
plan.steps,
|
|
9623
|
+
alignedStepDurations,
|
|
9624
|
+
config.output.dir,
|
|
9625
|
+
trimStart,
|
|
9626
|
+
recordingSecs,
|
|
9627
|
+
blackSpans
|
|
9628
|
+
);
|
|
9220
9629
|
if (videoOverlays.length > 0) {
|
|
9221
9630
|
const spliceTask = task("Painting cards and clips into recording");
|
|
9222
9631
|
try {
|
|
@@ -9270,11 +9679,13 @@ async function renderVideoPhase(params) {
|
|
|
9270
9679
|
const cardStepSeconds = (index) => {
|
|
9271
9680
|
const action = plan.steps[index]?.action;
|
|
9272
9681
|
if (action !== "image" && action !== "media") return 0;
|
|
9273
|
-
|
|
9682
|
+
const audioSecs = (stepAudio.get(index + 1)?.duration ?? 0) / 1e3;
|
|
9683
|
+
const tail = index === plan.steps.length - 1 && action === "image" ? OUTRO_TAIL_MS / 1e3 : 0;
|
|
9684
|
+
return Math.max(audioSecs, plan.steps[index]?.duration ?? 0) + tail;
|
|
9274
9685
|
};
|
|
9275
9686
|
const introSecs = cardStepSeconds(0);
|
|
9276
9687
|
const outroStepSecs = cardStepSeconds(plan.steps.length - 1);
|
|
9277
|
-
const outroSeconds = config.video.outroCard?.seconds ??
|
|
9688
|
+
const outroSeconds = Math.max(config.video.outroCard?.seconds ?? 3.5, outroStepSecs);
|
|
9278
9689
|
const card = config.video.outroCard;
|
|
9279
9690
|
if (card?.subject?.name) {
|
|
9280
9691
|
const cardTask = task("Rendering credits card");
|
|
@@ -9318,6 +9729,20 @@ async function renderVideoPhase(params) {
|
|
|
9318
9729
|
mainContentDuration = void 0;
|
|
9319
9730
|
}
|
|
9320
9731
|
}
|
|
9732
|
+
const outroOnly = config.music?.outroOnly !== false;
|
|
9733
|
+
let musicStartSecs = 0;
|
|
9734
|
+
if (outroOnly) {
|
|
9735
|
+
let trailingCards = 0;
|
|
9736
|
+
for (let i = plan.steps.length - 1; i >= 0; i--) {
|
|
9737
|
+
const action = plan.steps[i]?.action;
|
|
9738
|
+
if (action !== "image" && action !== "media") break;
|
|
9739
|
+
trailingCards++;
|
|
9740
|
+
}
|
|
9741
|
+
if (trailingCards > 0 && trailingCards < plan.steps.length) {
|
|
9742
|
+
const upTo = plan.steps.length - trailingCards;
|
|
9743
|
+
musicStartSecs = alignedStepDurations.slice(0, upTo).reduce((a, b) => a + b, 0) / 1e3;
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9321
9746
|
let finalPath = await composeAndFinish(
|
|
9322
9747
|
videoToCompose,
|
|
9323
9748
|
voiceoverPath,
|
|
@@ -9329,7 +9754,9 @@ async function renderVideoPhase(params) {
|
|
|
9329
9754
|
stepTransitions,
|
|
9330
9755
|
// The splice writes a clip-audio track only when a spliced clip had one.
|
|
9331
9756
|
splicedClipAudio,
|
|
9332
|
-
endsOnCreditsCard
|
|
9757
|
+
endsOnCreditsCard,
|
|
9758
|
+
void 0,
|
|
9759
|
+
musicStartSecs
|
|
9333
9760
|
);
|
|
9334
9761
|
const maxSilence = config.video.maxSilenceSecs ?? 0.6;
|
|
9335
9762
|
if (maxSilence > 0) {
|
|
@@ -9685,8 +10112,8 @@ async function captureCommand(url, lang) {
|
|
|
9685
10112
|
}
|
|
9686
10113
|
|
|
9687
10114
|
// src/commands/edit-cmd.ts
|
|
9688
|
-
var
|
|
9689
|
-
var
|
|
10115
|
+
var import_node_fs22 = require("fs");
|
|
10116
|
+
var import_node_path28 = require("path");
|
|
9690
10117
|
var import_chalk12 = __toESM(require("chalk"));
|
|
9691
10118
|
|
|
9692
10119
|
// src/lib/plan-validation.ts
|
|
@@ -9844,17 +10271,81 @@ function planEditorUrl(baseUrl, plan) {
|
|
|
9844
10271
|
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
9845
10272
|
}
|
|
9846
10273
|
|
|
10274
|
+
// src/lib/plan-thumbs.ts
|
|
10275
|
+
var import_node_fs21 = require("fs");
|
|
10276
|
+
var import_node_path27 = require("path");
|
|
10277
|
+
init_spawn();
|
|
10278
|
+
var MEDIA_ACTIONS = ["image", "media", "video"];
|
|
10279
|
+
var THUMB_WIDTH = 160;
|
|
10280
|
+
var MAX_THUMB_CHARS = 1e4;
|
|
10281
|
+
var MAX_TOTAL_THUMB_CHARS = 16e3;
|
|
10282
|
+
var THUMB_TIMEOUT_MS = 3e3;
|
|
10283
|
+
function thumbnail(path) {
|
|
10284
|
+
return new Promise((done) => {
|
|
10285
|
+
const proc = spawnFFmpeg("ffmpeg", [
|
|
10286
|
+
"-v",
|
|
10287
|
+
"error",
|
|
10288
|
+
"-i",
|
|
10289
|
+
path,
|
|
10290
|
+
"-frames:v",
|
|
10291
|
+
"1",
|
|
10292
|
+
"-vf",
|
|
10293
|
+
`scale=${THUMB_WIDTH}:-2`,
|
|
10294
|
+
"-q:v",
|
|
10295
|
+
"14",
|
|
10296
|
+
"-f",
|
|
10297
|
+
"mjpeg",
|
|
10298
|
+
"-"
|
|
10299
|
+
]);
|
|
10300
|
+
const chunks = [];
|
|
10301
|
+
const timer = setTimeout(() => {
|
|
10302
|
+
proc.kill();
|
|
10303
|
+
done(null);
|
|
10304
|
+
}, THUMB_TIMEOUT_MS);
|
|
10305
|
+
const finish = (result) => {
|
|
10306
|
+
clearTimeout(timer);
|
|
10307
|
+
done(result);
|
|
10308
|
+
};
|
|
10309
|
+
proc.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
10310
|
+
proc.on("error", () => finish(null));
|
|
10311
|
+
proc.on("close", (code) => {
|
|
10312
|
+
const bytes = Buffer.concat(chunks);
|
|
10313
|
+
if (code !== 0 || bytes.length === 0) return finish(null);
|
|
10314
|
+
const base64 = bytes.toString("base64");
|
|
10315
|
+
finish(base64.length > MAX_THUMB_CHARS ? null : `data:image/jpeg;base64,${base64}`);
|
|
10316
|
+
});
|
|
10317
|
+
});
|
|
10318
|
+
}
|
|
10319
|
+
async function withEditorThumbs(plan, baseDir) {
|
|
10320
|
+
const steps = [...plan.steps];
|
|
10321
|
+
let budget = MAX_TOTAL_THUMB_CHARS;
|
|
10322
|
+
for (const [index, step] of steps.entries()) {
|
|
10323
|
+
if (!step.value || !MEDIA_ACTIONS.includes(step.action)) continue;
|
|
10324
|
+
const path = (0, import_node_path27.isAbsolute)(step.value) ? step.value : (0, import_node_path27.resolve)(baseDir, step.value);
|
|
10325
|
+
if (!(0, import_node_fs21.existsSync)(path)) continue;
|
|
10326
|
+
const thumb = await thumbnail(path);
|
|
10327
|
+
if (!thumb || thumb.length > budget) continue;
|
|
10328
|
+
budget -= thumb.length;
|
|
10329
|
+
steps[index] = { ...step, thumb };
|
|
10330
|
+
}
|
|
10331
|
+
return { ...plan, steps };
|
|
10332
|
+
}
|
|
10333
|
+
async function editorUrlWithThumbs(baseUrl, plan, baseDir) {
|
|
10334
|
+
const withThumbs = await withEditorThumbs(plan, baseDir);
|
|
10335
|
+
return planEditorUrl(baseUrl, withThumbs) ?? planEditorUrl(baseUrl, plan);
|
|
10336
|
+
}
|
|
10337
|
+
|
|
9847
10338
|
// src/commands/edit-cmd.ts
|
|
9848
10339
|
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
9849
10340
|
function findPlanFile(dir, lang) {
|
|
9850
10341
|
if (lang) {
|
|
9851
|
-
const exact = (0,
|
|
9852
|
-
return (0,
|
|
10342
|
+
const exact = (0, import_node_path28.join)(dir, `plan-${lang}.json`);
|
|
10343
|
+
return (0, import_node_fs22.existsSync)(exact) ? exact : null;
|
|
9853
10344
|
}
|
|
9854
|
-
if (!(0,
|
|
9855
|
-
const plans = (0,
|
|
10345
|
+
if (!(0, import_node_fs22.existsSync)(dir)) return null;
|
|
10346
|
+
const plans = (0, import_node_fs22.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
|
|
9856
10347
|
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
9857
|
-
return preferred ? (0,
|
|
10348
|
+
return preferred ? (0, import_node_path28.join)(dir, preferred) : null;
|
|
9858
10349
|
}
|
|
9859
10350
|
async function editCommand(url, lang) {
|
|
9860
10351
|
console.log();
|
|
@@ -9867,14 +10358,14 @@ async function editCommand(url, lang) {
|
|
|
9867
10358
|
console.log();
|
|
9868
10359
|
return;
|
|
9869
10360
|
}
|
|
9870
|
-
const parsed = parsePlanText((0,
|
|
10361
|
+
const parsed = parsePlanText((0, import_node_fs22.readFileSync)(planPath, "utf8"));
|
|
9871
10362
|
if (!parsed.ok) {
|
|
9872
10363
|
console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
|
|
9873
10364
|
console.log();
|
|
9874
10365
|
return;
|
|
9875
10366
|
}
|
|
9876
10367
|
const base = editorBaseUrl();
|
|
9877
|
-
const editorUrl =
|
|
10368
|
+
const editorUrl = await editorUrlWithThumbs(base, parsed.plan, dir);
|
|
9878
10369
|
if (!editorUrl) {
|
|
9879
10370
|
console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
|
|
9880
10371
|
console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
|
|
@@ -9890,14 +10381,14 @@ async function editCommand(url, lang) {
|
|
|
9890
10381
|
}
|
|
9891
10382
|
|
|
9892
10383
|
// src/commands/init-cmd.ts
|
|
9893
|
-
var
|
|
10384
|
+
var import_node_fs23 = require("fs");
|
|
9894
10385
|
var import_promises18 = require("fs/promises");
|
|
9895
|
-
var
|
|
10386
|
+
var import_node_path29 = require("path");
|
|
9896
10387
|
var import_chalk13 = __toESM(require("chalk"));
|
|
9897
10388
|
init_lib();
|
|
9898
10389
|
async function initCommand() {
|
|
9899
|
-
const configPath = (0,
|
|
9900
|
-
if ((0,
|
|
10390
|
+
const configPath = (0, import_node_path29.join)(process.cwd(), CONFIG_FILE);
|
|
10391
|
+
if ((0, import_node_fs23.existsSync)(configPath)) {
|
|
9901
10392
|
console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
9902
10393
|
return;
|
|
9903
10394
|
}
|
|
@@ -9928,9 +10419,9 @@ async function initCommand() {
|
|
|
9928
10419
|
console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
9929
10420
|
}
|
|
9930
10421
|
async function configCommand(key, value) {
|
|
9931
|
-
const configPath = (0,
|
|
10422
|
+
const configPath = (0, import_node_path29.join)(process.cwd(), CONFIG_FILE);
|
|
9932
10423
|
if (!key) {
|
|
9933
|
-
if (!(0,
|
|
10424
|
+
if (!(0, import_node_fs23.existsSync)(configPath)) {
|
|
9934
10425
|
console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
9935
10426
|
return;
|
|
9936
10427
|
}
|
|
@@ -9945,7 +10436,7 @@ async function configCommand(key, value) {
|
|
|
9945
10436
|
return;
|
|
9946
10437
|
}
|
|
9947
10438
|
let config = {};
|
|
9948
|
-
if ((0,
|
|
10439
|
+
if ((0, import_node_fs23.existsSync)(configPath)) {
|
|
9949
10440
|
try {
|
|
9950
10441
|
config = JSON.parse(await (0, import_promises18.readFile)(configPath, "utf-8"));
|
|
9951
10442
|
} catch {
|
|
@@ -10034,11 +10525,36 @@ async function logoutCommand() {
|
|
|
10034
10525
|
}
|
|
10035
10526
|
|
|
10036
10527
|
// src/commands/record-cmd.ts
|
|
10037
|
-
var
|
|
10528
|
+
var import_node_fs29 = require("fs");
|
|
10038
10529
|
var import_promises23 = require("fs/promises");
|
|
10039
|
-
var
|
|
10530
|
+
var import_node_path35 = require("path");
|
|
10040
10531
|
var import_chalk21 = __toESM(require("chalk"));
|
|
10041
10532
|
init_mcp_context();
|
|
10533
|
+
|
|
10534
|
+
// src/lib/demo-registry.ts
|
|
10535
|
+
init_api_client();
|
|
10536
|
+
var TIMEOUT_MS2 = 4e3;
|
|
10537
|
+
async function registerDemoRun(email, run2) {
|
|
10538
|
+
if (!email || email === "anonymous") return false;
|
|
10539
|
+
const controller = new AbortController();
|
|
10540
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
|
|
10541
|
+
try {
|
|
10542
|
+
const token = await getOrRefreshRelayToken(email);
|
|
10543
|
+
const response = await fetch(`${RELAY_BASE}/api/cli/demos`, {
|
|
10544
|
+
method: "POST",
|
|
10545
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
10546
|
+
body: JSON.stringify(run2),
|
|
10547
|
+
signal: controller.signal
|
|
10548
|
+
});
|
|
10549
|
+
return response.ok;
|
|
10550
|
+
} catch {
|
|
10551
|
+
return false;
|
|
10552
|
+
} finally {
|
|
10553
|
+
clearTimeout(timer);
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
|
|
10557
|
+
// src/commands/record-cmd.ts
|
|
10042
10558
|
init_lib();
|
|
10043
10559
|
|
|
10044
10560
|
// src/commands/phases/auth-phase.ts
|
|
@@ -10055,13 +10571,14 @@ async function handleRegistrationGate() {
|
|
|
10055
10571
|
console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
|
|
10056
10572
|
console.log();
|
|
10057
10573
|
console.log(import_chalk15.default.dim(" Benefits of signing in:"));
|
|
10058
|
-
console.log(import_chalk15.default.dim(" \u2022
|
|
10574
|
+
console.log(import_chalk15.default.dim(" \u2022 10 videos a day instead of 5 in total"));
|
|
10059
10575
|
console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
|
|
10060
10576
|
console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10577
|
+
console.log();
|
|
10578
|
+
console.log(import_chalk15.default.dim(" Type your email, or 'b' to sign in through a browser."));
|
|
10579
|
+
let email = await promptInput("Email: ");
|
|
10580
|
+
if (email.toLowerCase() === "b" || email.toLowerCase() === "browser") {
|
|
10581
|
+
email = await browserLogin() ?? "";
|
|
10065
10582
|
}
|
|
10066
10583
|
if (!email || !email.includes("@")) {
|
|
10067
10584
|
console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
|
|
@@ -10106,9 +10623,12 @@ async function runAuthPhase(input) {
|
|
|
10106
10623
|
const { isVerified: isVerified2 } = input;
|
|
10107
10624
|
const mcpTool = currentMcpTool();
|
|
10108
10625
|
if (mcpTool && !(userEmail && isVerified2)) {
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10626
|
+
if (localVideoCount >= FREE_VIDEO_LIMIT) {
|
|
10627
|
+
throw new Error(
|
|
10628
|
+
`"${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.`
|
|
10629
|
+
);
|
|
10630
|
+
}
|
|
10631
|
+
return { userEmail: "anonymous", userTier: "free", localVideoCount, isVerified: false };
|
|
10112
10632
|
}
|
|
10113
10633
|
if (userEmail && isVerified2) {
|
|
10114
10634
|
const tierTask = task("Checking subscription");
|
|
@@ -10133,7 +10653,7 @@ async function runAuthPhase(input) {
|
|
|
10133
10653
|
console.log();
|
|
10134
10654
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
10135
10655
|
if (input2.toLowerCase() === "exit") {
|
|
10136
|
-
gracefulExit();
|
|
10656
|
+
gracefulExit(1);
|
|
10137
10657
|
return null;
|
|
10138
10658
|
}
|
|
10139
10659
|
if (input2?.includes("@")) {
|
|
@@ -10142,7 +10662,7 @@ async function runAuthPhase(input) {
|
|
|
10142
10662
|
const result = await registerFreeUser(newEmail);
|
|
10143
10663
|
if (!result.success) {
|
|
10144
10664
|
registerTask.fail(result.error || "Registration failed");
|
|
10145
|
-
gracefulExit();
|
|
10665
|
+
gracefulExit(1);
|
|
10146
10666
|
return null;
|
|
10147
10667
|
}
|
|
10148
10668
|
if (result.alreadyVerified) {
|
|
@@ -10157,7 +10677,7 @@ async function runAuthPhase(input) {
|
|
|
10157
10677
|
console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
10158
10678
|
console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
|
|
10159
10679
|
console.log();
|
|
10160
|
-
gracefulExit();
|
|
10680
|
+
gracefulExit(1);
|
|
10161
10681
|
return null;
|
|
10162
10682
|
}
|
|
10163
10683
|
} else {
|
|
@@ -10173,7 +10693,7 @@ async function runAuthPhase(input) {
|
|
|
10173
10693
|
console.log();
|
|
10174
10694
|
console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
10175
10695
|
console.log();
|
|
10176
|
-
gracefulExit();
|
|
10696
|
+
gracefulExit(1);
|
|
10177
10697
|
return null;
|
|
10178
10698
|
}
|
|
10179
10699
|
}
|
|
@@ -10181,7 +10701,7 @@ async function runAuthPhase(input) {
|
|
|
10181
10701
|
} else if (localVideoCount >= FREE_VIDEO_LIMIT) {
|
|
10182
10702
|
const registration = await handleRegistrationGate();
|
|
10183
10703
|
if (!registration) {
|
|
10184
|
-
gracefulExit();
|
|
10704
|
+
gracefulExit(1);
|
|
10185
10705
|
return null;
|
|
10186
10706
|
}
|
|
10187
10707
|
userEmail = registration.email;
|
|
@@ -10191,7 +10711,7 @@ async function runAuthPhase(input) {
|
|
|
10191
10711
|
localVideoCount
|
|
10192
10712
|
});
|
|
10193
10713
|
if (!registration.verified) {
|
|
10194
|
-
gracefulExit();
|
|
10714
|
+
gracefulExit(1);
|
|
10195
10715
|
return null;
|
|
10196
10716
|
}
|
|
10197
10717
|
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
@@ -10199,8 +10719,17 @@ async function runAuthPhase(input) {
|
|
|
10199
10719
|
const remaining2 = FREE_VIDEO_LIMIT - localVideoCount;
|
|
10200
10720
|
console.log();
|
|
10201
10721
|
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
|
|
10203
|
-
|
|
10722
|
+
console.log(import_chalk16.default.dim(` ${remaining2 - 1} more after this \u2022 Sign up for 10 a day`));
|
|
10723
|
+
const enteredEmail = await setupUserEmail();
|
|
10724
|
+
if (!enteredEmail) {
|
|
10725
|
+
console.log();
|
|
10726
|
+
console.log(import_chalk16.default.yellow(" No email, so no video was recorded."));
|
|
10727
|
+
console.log(import_chalk16.default.white(" Next: run `qp login` to sign in, then run qp again."));
|
|
10728
|
+
console.log();
|
|
10729
|
+
gracefulExit(1);
|
|
10730
|
+
return null;
|
|
10731
|
+
}
|
|
10732
|
+
userEmail = enteredEmail;
|
|
10204
10733
|
let registeredVerified = false;
|
|
10205
10734
|
try {
|
|
10206
10735
|
const reg = await registerFreeUser(userEmail);
|
|
@@ -10218,9 +10747,9 @@ async function runAuthPhase(input) {
|
|
|
10218
10747
|
}
|
|
10219
10748
|
|
|
10220
10749
|
// src/commands/phases/plan-phase.ts
|
|
10221
|
-
var
|
|
10750
|
+
var import_node_fs26 = require("fs");
|
|
10222
10751
|
var import_promises20 = require("fs/promises");
|
|
10223
|
-
var
|
|
10752
|
+
var import_node_path32 = require("path");
|
|
10224
10753
|
var import_chalk18 = __toESM(require("chalk"));
|
|
10225
10754
|
|
|
10226
10755
|
// src/ai/fix.ts
|
|
@@ -10376,15 +10905,15 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
10376
10905
|
}
|
|
10377
10906
|
|
|
10378
10907
|
// src/commands/phases/plan-cards.ts
|
|
10379
|
-
var
|
|
10380
|
-
var
|
|
10908
|
+
var import_node_fs25 = require("fs");
|
|
10909
|
+
var import_node_path31 = require("path");
|
|
10381
10910
|
var import_chalk17 = __toESM(require("chalk"));
|
|
10382
10911
|
init_render_outro();
|
|
10383
10912
|
|
|
10384
10913
|
// src/lib/og-card.ts
|
|
10385
|
-
var
|
|
10914
|
+
var import_node_fs24 = require("fs");
|
|
10386
10915
|
var import_promises19 = require("fs/promises");
|
|
10387
|
-
var
|
|
10916
|
+
var import_node_path30 = require("path");
|
|
10388
10917
|
init_image_kind();
|
|
10389
10918
|
var PORTRAIT_PATHS = ["/opengraph-image/portrait", "/opengraph-image-portrait", "/og/portrait"];
|
|
10390
10919
|
async function fetchImage(url) {
|
|
@@ -10464,7 +10993,7 @@ async function fetchOgCard(siteUrl, dir, preferPortrait) {
|
|
|
10464
10993
|
for (const candidate of candidates) {
|
|
10465
10994
|
const buf = await fetchImage(candidate);
|
|
10466
10995
|
if (buf) {
|
|
10467
|
-
const path = (0,
|
|
10996
|
+
const path = (0, import_node_path30.join)(dir, "og-card.png");
|
|
10468
10997
|
await (0, import_promises19.writeFile)(path, buf);
|
|
10469
10998
|
return path;
|
|
10470
10999
|
}
|
|
@@ -10537,7 +11066,7 @@ async function saveFirstUsableIcon(hrefs, siteUrl, dir) {
|
|
|
10537
11066
|
if (!buf) continue;
|
|
10538
11067
|
const kind = sniffImageKind(buf);
|
|
10539
11068
|
if (!kind) continue;
|
|
10540
|
-
const path = (0,
|
|
11069
|
+
const path = (0, import_node_path30.join)(dir, `app-icon.${kind === "jpeg" ? "jpg" : kind}`);
|
|
10541
11070
|
await (0, import_promises19.writeFile)(path, buf);
|
|
10542
11071
|
if (kind === "png") {
|
|
10543
11072
|
const size = pngSize(path);
|
|
@@ -10551,10 +11080,10 @@ async function saveFirstUsableIcon(hrefs, siteUrl, dir) {
|
|
|
10551
11080
|
}
|
|
10552
11081
|
function existingIcon(dir) {
|
|
10553
11082
|
for (const ext of ["png", "svg", "jpg", "webp", "gif"]) {
|
|
10554
|
-
const path = (0,
|
|
11083
|
+
const path = (0, import_node_path30.join)(dir, `app-icon.${ext}`);
|
|
10555
11084
|
try {
|
|
10556
|
-
if (!(0,
|
|
10557
|
-
if (!sniffImageKind((0,
|
|
11085
|
+
if (!(0, import_node_fs24.existsSync)(path)) continue;
|
|
11086
|
+
if (!sniffImageKind((0, import_node_fs24.readFileSync)(path))) continue;
|
|
10558
11087
|
return path;
|
|
10559
11088
|
} catch {
|
|
10560
11089
|
}
|
|
@@ -10563,7 +11092,7 @@ function existingIcon(dir) {
|
|
|
10563
11092
|
}
|
|
10564
11093
|
function pngSize(path) {
|
|
10565
11094
|
try {
|
|
10566
|
-
const buf = (0,
|
|
11095
|
+
const buf = (0, import_node_fs24.readFileSync)(path);
|
|
10567
11096
|
const head = buf.subarray(0, 33);
|
|
10568
11097
|
if (head.length >= 24 && head.readUInt32BE(12) === 1229472850) {
|
|
10569
11098
|
return { width: head.readUInt32BE(16), height: head.readUInt32BE(20) };
|
|
@@ -10642,8 +11171,8 @@ async function useOgCardAsIntro(plan, url, config, fresh = false) {
|
|
|
10642
11171
|
}
|
|
10643
11172
|
const rewritable = first.action === "wait" && !first.target;
|
|
10644
11173
|
const portrait = (config.video.height ?? 0) > (config.video.width ?? 0);
|
|
10645
|
-
const existing = (0,
|
|
10646
|
-
const fetched = !fresh && (0,
|
|
11174
|
+
const existing = (0, import_node_path31.join)(config.output.dir, "og-card.png");
|
|
11175
|
+
const fetched = !fresh && (0, import_node_fs25.existsSync)(existing) ? existing : await fetchOgCard(url, config.output.dir, portrait);
|
|
10647
11176
|
const ogPath = fetched && coversFrame(fetched, config) ? fetched : null;
|
|
10648
11177
|
const cardPath = ogPath ?? await renderGeneratedIntroCard(url, config);
|
|
10649
11178
|
if (!cardPath) {
|
|
@@ -10733,30 +11262,20 @@ function fitFor(cardPath, config) {
|
|
|
10733
11262
|
return Math.abs(size.width / size.height - frame) < 0.06 ? "stretch" : "cover";
|
|
10734
11263
|
}
|
|
10735
11264
|
function resolveCardPath(value, config) {
|
|
10736
|
-
return (0,
|
|
11265
|
+
return (0, import_node_path31.isAbsolute)(value) ? value : (0, import_node_path31.join)(config.output.dir, value.replace(/^\.\//, ""));
|
|
10737
11266
|
}
|
|
10738
11267
|
var DEFAULT_INTRO_CARD_SECONDS = 2.5;
|
|
10739
11268
|
function packageAsset(relative) {
|
|
10740
11269
|
let dir = getScriptDir();
|
|
10741
11270
|
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, "..");
|
|
11271
|
+
const candidate = (0, import_node_path31.join)(dir, relative);
|
|
11272
|
+
if ((0, import_node_fs25.existsSync)(candidate)) return candidate;
|
|
11273
|
+
dir = (0, import_node_path31.join)(dir, "..");
|
|
10754
11274
|
}
|
|
10755
11275
|
return void 0;
|
|
10756
11276
|
}
|
|
10757
11277
|
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") }
|
|
11278
|
+
{ name: "QuickPeek", role: "Video generated by", icon: () => packageAsset("imgs/qp.png") }
|
|
10760
11279
|
];
|
|
10761
11280
|
function oneTagline(text) {
|
|
10762
11281
|
if (!text) return void 0;
|
|
@@ -10848,7 +11367,7 @@ async function runPlanPhase(params) {
|
|
|
10848
11367
|
const extensionPath = config.browser?.extension;
|
|
10849
11368
|
let existingPlan = null;
|
|
10850
11369
|
let existingPlanPath = null;
|
|
10851
|
-
if ((0,
|
|
11370
|
+
if ((0, import_node_fs26.existsSync)(planPath)) {
|
|
10852
11371
|
try {
|
|
10853
11372
|
const parsed = JSON.parse(await (0, import_promises20.readFile)(planPath, "utf-8"));
|
|
10854
11373
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -10860,10 +11379,10 @@ async function runPlanPhase(params) {
|
|
|
10860
11379
|
}
|
|
10861
11380
|
if (!existingPlan) {
|
|
10862
11381
|
try {
|
|
10863
|
-
const files = (0,
|
|
10864
|
-
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0,
|
|
11382
|
+
const files = (0, import_node_fs26.readdirSync)(config.output.dir);
|
|
11383
|
+
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path32.basename)(planPath));
|
|
10865
11384
|
for (const file of planFiles) {
|
|
10866
|
-
const filePath = (0,
|
|
11385
|
+
const filePath = (0, import_node_path32.join)(config.output.dir, file);
|
|
10867
11386
|
try {
|
|
10868
11387
|
const parsed = JSON.parse(await (0, import_promises20.readFile)(filePath, "utf-8"));
|
|
10869
11388
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -10878,7 +11397,22 @@ async function runPlanPhase(params) {
|
|
|
10878
11397
|
}
|
|
10879
11398
|
}
|
|
10880
11399
|
const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
10881
|
-
const
|
|
11400
|
+
const isBareOrigin = (u) => {
|
|
11401
|
+
try {
|
|
11402
|
+
const parsed = new URL(u);
|
|
11403
|
+
return parsed.pathname === "/" && !parsed.search && !parsed.hash;
|
|
11404
|
+
} catch {
|
|
11405
|
+
return false;
|
|
11406
|
+
}
|
|
11407
|
+
};
|
|
11408
|
+
const sameOrigin = (a, b) => {
|
|
11409
|
+
try {
|
|
11410
|
+
return new URL(a).origin === new URL(b).origin;
|
|
11411
|
+
} catch {
|
|
11412
|
+
return false;
|
|
11413
|
+
}
|
|
11414
|
+
};
|
|
11415
|
+
const urlMatches = existingPlan && (normalizeUrl4(existingPlan.url) === normalizeUrl4(url) || isBareOrigin(existingPlan.url) && sameOrigin(existingPlan.url, url));
|
|
10882
11416
|
if (existingPlan && !urlMatches && isAutoYes()) {
|
|
10883
11417
|
console.log();
|
|
10884
11418
|
console.log(import_chalk18.default.dim(` Existing plan is for ${existingPlan.url}, not ${url}, regenerating.`));
|
|
@@ -11022,7 +11556,7 @@ async function runPlanPhase(params) {
|
|
|
11022
11556
|
const planLang = plan.lang || "en";
|
|
11023
11557
|
if (planLang !== targetLang) {
|
|
11024
11558
|
let useExistingTranslation = false;
|
|
11025
|
-
if ((0,
|
|
11559
|
+
if ((0, import_node_fs26.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
11026
11560
|
try {
|
|
11027
11561
|
const translatedPlan = JSON.parse(await (0, import_promises20.readFile)(planPath, "utf-8"));
|
|
11028
11562
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
@@ -11242,31 +11776,31 @@ async function ensureCliSignIn(url, opts) {
|
|
|
11242
11776
|
}
|
|
11243
11777
|
|
|
11244
11778
|
// src/commands/utils/chrome-extension.ts
|
|
11245
|
-
var
|
|
11779
|
+
var import_node_fs27 = require("fs");
|
|
11246
11780
|
var import_node_os7 = require("os");
|
|
11247
|
-
var
|
|
11781
|
+
var import_node_path33 = require("path");
|
|
11248
11782
|
function resolveChromeExtension(extensionId) {
|
|
11249
11783
|
const platform = process.platform;
|
|
11250
11784
|
const possibleDirs = [];
|
|
11251
11785
|
const getChromeExtDirs = (userDataPath) => {
|
|
11252
11786
|
const dirs = [];
|
|
11253
11787
|
try {
|
|
11254
|
-
const profiles = (0,
|
|
11788
|
+
const profiles = (0, import_node_fs27.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
11255
11789
|
for (const profile of profiles) {
|
|
11256
|
-
dirs.push((0,
|
|
11790
|
+
dirs.push((0, import_node_path33.join)(userDataPath, profile, "Extensions"));
|
|
11257
11791
|
}
|
|
11258
11792
|
} catch {
|
|
11259
|
-
dirs.push((0,
|
|
11793
|
+
dirs.push((0, import_node_path33.join)(userDataPath, "Default", "Extensions"));
|
|
11260
11794
|
}
|
|
11261
11795
|
return dirs;
|
|
11262
11796
|
};
|
|
11263
11797
|
if (platform === "win32") {
|
|
11264
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11798
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
11265
11799
|
} else if (platform === "darwin") {
|
|
11266
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11800
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
11267
11801
|
} else {
|
|
11268
11802
|
try {
|
|
11269
|
-
const windowsUsers = (0,
|
|
11803
|
+
const windowsUsers = (0, import_node_fs27.readdirSync)("/mnt/c/Users").filter(
|
|
11270
11804
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
11271
11805
|
);
|
|
11272
11806
|
for (const user of windowsUsers) {
|
|
@@ -11274,22 +11808,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
11274
11808
|
}
|
|
11275
11809
|
} catch {
|
|
11276
11810
|
}
|
|
11277
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
11811
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path33.join)((0, import_node_os7.homedir)(), ".config", "google-chrome")));
|
|
11278
11812
|
}
|
|
11279
11813
|
for (const chromeExtDir of possibleDirs) {
|
|
11280
|
-
const extDir = (0,
|
|
11281
|
-
if (!(0,
|
|
11814
|
+
const extDir = (0, import_node_path33.join)(chromeExtDir, extensionId);
|
|
11815
|
+
if (!(0, import_node_fs27.existsSync)(extDir)) continue;
|
|
11282
11816
|
try {
|
|
11283
|
-
const versions = (0,
|
|
11817
|
+
const versions = (0, import_node_fs27.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
11284
11818
|
if (versions.length === 0) continue;
|
|
11285
11819
|
const latestVersion = versions.sort().pop();
|
|
11286
|
-
const extPath = (0,
|
|
11820
|
+
const extPath = (0, import_node_path33.join)(extDir, latestVersion);
|
|
11287
11821
|
let name = extensionId;
|
|
11288
11822
|
let popupPath = "popup.html";
|
|
11289
|
-
const manifestPath = (0,
|
|
11290
|
-
if ((0,
|
|
11823
|
+
const manifestPath = (0, import_node_path33.join)(extPath, "manifest.json");
|
|
11824
|
+
if ((0, import_node_fs27.existsSync)(manifestPath)) {
|
|
11291
11825
|
try {
|
|
11292
|
-
const manifest = JSON.parse((0,
|
|
11826
|
+
const manifest = JSON.parse((0, import_node_fs27.readFileSync)(manifestPath, "utf-8"));
|
|
11293
11827
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
11294
11828
|
name = manifest.name;
|
|
11295
11829
|
}
|
|
@@ -11310,9 +11844,9 @@ function resolveChromeExtension(extensionId) {
|
|
|
11310
11844
|
|
|
11311
11845
|
// src/commands/utils/plan-review.ts
|
|
11312
11846
|
var import_node_crypto2 = require("crypto");
|
|
11313
|
-
var
|
|
11847
|
+
var import_node_fs28 = require("fs");
|
|
11314
11848
|
var import_promises22 = require("fs/promises");
|
|
11315
|
-
var
|
|
11849
|
+
var import_node_path34 = require("path");
|
|
11316
11850
|
var REVIEW_FILE = "plan-review.json";
|
|
11317
11851
|
async function hashPlanFile(planPath) {
|
|
11318
11852
|
try {
|
|
@@ -11322,8 +11856,8 @@ async function hashPlanFile(planPath) {
|
|
|
11322
11856
|
}
|
|
11323
11857
|
}
|
|
11324
11858
|
async function loadPlanReview(outputDir) {
|
|
11325
|
-
const path = (0,
|
|
11326
|
-
if (!(0,
|
|
11859
|
+
const path = (0, import_node_path34.join)(outputDir, REVIEW_FILE);
|
|
11860
|
+
if (!(0, import_node_fs28.existsSync)(path)) return {};
|
|
11327
11861
|
try {
|
|
11328
11862
|
return JSON.parse(await (0, import_promises22.readFile)(path, "utf-8"));
|
|
11329
11863
|
} catch {
|
|
@@ -11331,7 +11865,7 @@ async function loadPlanReview(outputDir) {
|
|
|
11331
11865
|
}
|
|
11332
11866
|
}
|
|
11333
11867
|
async function savePlanReview(outputDir, review) {
|
|
11334
|
-
await (0, import_promises22.writeFile)((0,
|
|
11868
|
+
await (0, import_promises22.writeFile)((0, import_node_path34.join)(outputDir, REVIEW_FILE), JSON.stringify(review, null, 2));
|
|
11335
11869
|
}
|
|
11336
11870
|
|
|
11337
11871
|
// src/commands/record-cmd.ts
|
|
@@ -11393,7 +11927,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11393
11927
|
url = startUrl;
|
|
11394
11928
|
}
|
|
11395
11929
|
}
|
|
11396
|
-
config.output.dir = chromeExtension ? (0,
|
|
11930
|
+
config.output.dir = chromeExtension ? (0, import_node_path35.join)(runRoot(), `${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo`) : getOutputDirFromUrl(url);
|
|
11397
11931
|
const userData = await loadUserData();
|
|
11398
11932
|
const authResult = await runAuthPhase({
|
|
11399
11933
|
userEmail: userData.email,
|
|
@@ -11404,7 +11938,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11404
11938
|
if (!authResult) return;
|
|
11405
11939
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
11406
11940
|
if (configCreated) {
|
|
11407
|
-
const configPath = (0,
|
|
11941
|
+
const configPath = (0, import_node_path35.join)(runRoot(), CONFIG_FILE);
|
|
11408
11942
|
console.log();
|
|
11409
11943
|
console.log(import_chalk21.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
11410
11944
|
console.log(import_chalk21.default.dim(` ${configPath}`));
|
|
@@ -11425,17 +11959,17 @@ async function recordCommand(url, options = {}) {
|
|
|
11425
11959
|
}
|
|
11426
11960
|
if (langOverride) config.lang = langOverride;
|
|
11427
11961
|
if (config.music?.path) {
|
|
11428
|
-
const musicPath = (0,
|
|
11429
|
-
if (!(0,
|
|
11962
|
+
const musicPath = (0, import_node_path35.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path35.resolve)(config.output.dir, config.music.path);
|
|
11963
|
+
if (!(0, import_node_fs29.existsSync)(musicPath)) {
|
|
11430
11964
|
console.log();
|
|
11431
11965
|
console.log(import_chalk21.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
11432
|
-
console.log(import_chalk21.default.dim(` Config: ${(0,
|
|
11966
|
+
console.log(import_chalk21.default.dim(` Config: ${(0, import_node_path35.join)(runRoot(), CONFIG_FILE)}`));
|
|
11433
11967
|
console.log();
|
|
11434
11968
|
const proceed = await confirm(`${INDENT2}Continue without background music?`);
|
|
11435
11969
|
if (!proceed) {
|
|
11436
11970
|
console.log(import_chalk21.default.yellow(`
|
|
11437
11971
|
Edit music.path in config and run again.`));
|
|
11438
|
-
console.log(import_chalk21.default.dim(` ${(0,
|
|
11972
|
+
console.log(import_chalk21.default.dim(` ${(0, import_node_path35.join)(runRoot(), CONFIG_FILE)}
|
|
11439
11973
|
`));
|
|
11440
11974
|
process.exit(0);
|
|
11441
11975
|
}
|
|
@@ -11443,9 +11977,9 @@ async function recordCommand(url, options = {}) {
|
|
|
11443
11977
|
}
|
|
11444
11978
|
}
|
|
11445
11979
|
const captions = resolveCaptions(await loadCaptions(), config.video);
|
|
11446
|
-
const workDir = await prepareWorkDir((0,
|
|
11980
|
+
const workDir = await prepareWorkDir((0, import_node_path35.basename)(config.output.dir));
|
|
11447
11981
|
const targetLang = config.lang || "en";
|
|
11448
|
-
const planPath = (0,
|
|
11982
|
+
const planPath = (0, import_node_path35.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
11449
11983
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
11450
11984
|
await (0, import_promises23.mkdir)(config.output.dir, { recursive: true });
|
|
11451
11985
|
await setLogFilePath(config.output.dir);
|
|
@@ -11536,6 +12070,13 @@ async function recordCommand(url, options = {}) {
|
|
|
11536
12070
|
userTier,
|
|
11537
12071
|
userEmail
|
|
11538
12072
|
});
|
|
12073
|
+
await registerDemoRun(userEmail, {
|
|
12074
|
+
source: currentMcpTool2() ? "mcp" : "cli",
|
|
12075
|
+
targetUrl: validatedPlan.url || url,
|
|
12076
|
+
localPath: finalPath,
|
|
12077
|
+
title: validatedPlan.title,
|
|
12078
|
+
...config.lang ? { language: config.lang } : {}
|
|
12079
|
+
});
|
|
11539
12080
|
if (!userEmail || userEmail === "anonymous") {
|
|
11540
12081
|
const newCount = localVideoCount + 1;
|
|
11541
12082
|
await saveUserData({ localVideoCount: newCount });
|
|
@@ -11562,7 +12103,7 @@ async function recordCommand(url, options = {}) {
|
|
|
11562
12103
|
|
|
11563
12104
|
// src/commands/script-cmd.ts
|
|
11564
12105
|
var import_promises24 = require("fs/promises");
|
|
11565
|
-
var
|
|
12106
|
+
var import_node_path36 = require("path");
|
|
11566
12107
|
var import_chalk22 = __toESM(require("chalk"));
|
|
11567
12108
|
init_lib();
|
|
11568
12109
|
async function reusablePlan(planPath, lines) {
|
|
@@ -11694,7 +12235,7 @@ async function scriptCommand(url, scriptPath, options = {}) {
|
|
|
11694
12235
|
);
|
|
11695
12236
|
console.log(import_chalk22.default.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
|
|
11696
12237
|
}
|
|
11697
|
-
const planPath = (0,
|
|
12238
|
+
const planPath = (0, import_node_path36.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
11698
12239
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
11699
12240
|
let plan = await reusablePlan(planPath, lines);
|
|
11700
12241
|
if (plan) {
|
|
@@ -11904,7 +12445,7 @@ function parseArgs(args) {
|
|
|
11904
12445
|
i++;
|
|
11905
12446
|
} else if (arg2 === "--skip-voice" || arg2 === "-sv") {
|
|
11906
12447
|
skipVoice = true;
|
|
11907
|
-
} else if (arg2 === "--draft" || arg2 === "-
|
|
12448
|
+
} else if (arg2 === "--draft" || arg2 === "-dr") {
|
|
11908
12449
|
draft = true;
|
|
11909
12450
|
} else if (arg2 === "--yes" || arg2 === "-y") {
|
|
11910
12451
|
yes = true;
|