@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/index.mjs
CHANGED
|
@@ -434,8 +434,13 @@ async function composeVideo(opts) {
|
|
|
434
434
|
musicPath,
|
|
435
435
|
musicVolume = 0.3,
|
|
436
436
|
musicTargetLufs = MUSIC_BED_LUFS,
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
// Sized for the outro window: the song has 5.5s, so a 2s fade at each end
|
|
438
|
+
// left 1.5s of music anyone could actually hear. Up fast, hold, out slow.
|
|
439
|
+
musicFadeIn = 1,
|
|
440
|
+
musicFadeOut = 1.5,
|
|
441
|
+
tempo = 1,
|
|
442
|
+
musicStartSecs = 0,
|
|
443
|
+
musicDurationSecs = 0,
|
|
439
444
|
width,
|
|
440
445
|
height,
|
|
441
446
|
fps,
|
|
@@ -496,7 +501,19 @@ async function composeVideo(opts) {
|
|
|
496
501
|
const clipAudioLabel = opts.hasClipAudio ? "[0:a]" : "";
|
|
497
502
|
const hasVoiceover = !!voiceoverPath;
|
|
498
503
|
const hasMusic = !!musicPath;
|
|
499
|
-
const
|
|
504
|
+
const bedStart = Math.max(0, Math.min(musicStartSecs, Math.max(0, effectiveDuration - 0.5)));
|
|
505
|
+
const bedLength = Math.max(0.5, effectiveDuration - bedStart);
|
|
506
|
+
const bedDelay = bedStart > 0 ? `adelay=${Math.round(bedStart * 1e3)}|${Math.round(bedStart * 1e3)},` : "";
|
|
507
|
+
const bedTempo = tempo > 1 ? 1 / tempo : 1;
|
|
508
|
+
const bedSlow = bedTempo === 1 ? "" : `atempo=${bedTempo.toFixed(6)},`;
|
|
509
|
+
const bedSource = bedLength * bedTempo;
|
|
510
|
+
const bedSeek = musicDurationSecs > bedSource + 0.05 ? musicDurationSecs - bedSource : 0;
|
|
511
|
+
const bedTrimIn = bedSeek > 0 ? `atrim=start=${bedSeek.toFixed(3)},asetpts=PTS-STARTPTS,` : "";
|
|
512
|
+
const bedFadeOut = bedSeek > 0 ? 0 : musicFadeOut * tempo;
|
|
513
|
+
const bedLoop = bedSeek > 0 ? "" : "aloop=loop=-1:size=2e+09,";
|
|
514
|
+
const bedFadeIn = musicFadeIn * tempo;
|
|
515
|
+
const musicFadeOutStart = Math.max(0, bedLength - bedFadeOut);
|
|
516
|
+
const bedFadeOutFilter = bedFadeOut > 0 ? `afade=t=out:st=${musicFadeOutStart}:d=${bedFadeOut},` : "";
|
|
500
517
|
const vfStr = [...vf, ...encoder.filter ? [encoder.filter] : []].join(",");
|
|
501
518
|
if (hasVoiceover && hasMusic) {
|
|
502
519
|
const mixInputs = clipAudioLabel ? 3 : 2;
|
|
@@ -512,7 +529,7 @@ async function composeVideo(opts) {
|
|
|
512
529
|
// card, which is the one place the bed is the only thing playing.
|
|
513
530
|
`[voicekey0]apad=whole_dur=${effectiveDuration}[voicekey]`,
|
|
514
531
|
// Music: normalised to a known level, trimmed, then faded.
|
|
515
|
-
`[2:a]
|
|
532
|
+
`[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]`,
|
|
516
533
|
`[bed][voicekey]${DUCK}[music]`,
|
|
517
534
|
// normalize=0, because amix's default divides every input by the number
|
|
518
535
|
// of them: adding the bed used to quietly take 6 dB off the narration,
|
|
@@ -536,7 +553,7 @@ async function composeVideo(opts) {
|
|
|
536
553
|
args.push(...encoder.output);
|
|
537
554
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
538
555
|
} else if (hasMusic) {
|
|
539
|
-
const musicFilter =
|
|
556
|
+
const musicFilter = `${bedTrimIn}${bedLoop}${bedSlow}atrim=0:${bedLength},volume=${musicVolume},afade=t=in:st=0:d=${bedFadeIn},${bedFadeOutFilter}${bedDelay}apad=whole_dur=${effectiveDuration},${MASTER_CHAIN}`;
|
|
540
557
|
args.push("-vf", vfStr);
|
|
541
558
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
542
559
|
args.push(...encoder.output);
|
|
@@ -1846,6 +1863,108 @@ var init_timing = __esm({
|
|
|
1846
1863
|
}
|
|
1847
1864
|
});
|
|
1848
1865
|
|
|
1866
|
+
// src/lib/json-repair.ts
|
|
1867
|
+
function repairUnicodeEscapes(content) {
|
|
1868
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1869
|
+
const code = Number.parseInt(hex, 16);
|
|
1870
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1871
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1872
|
+
}
|
|
1873
|
+
var MAX_SALVAGE_ATTEMPTS = 500;
|
|
1874
|
+
var CLOSERS = { "{": "}", "[": "]" };
|
|
1875
|
+
function salvageTruncatedJson(content) {
|
|
1876
|
+
const stack = [];
|
|
1877
|
+
const cuts = [];
|
|
1878
|
+
let inString = false;
|
|
1879
|
+
let escaped = false;
|
|
1880
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1881
|
+
for (let i = 0; i < content.length; i++) {
|
|
1882
|
+
const ch = content[i];
|
|
1883
|
+
if (inString) {
|
|
1884
|
+
if (escaped) escaped = false;
|
|
1885
|
+
else if (ch === "\\") escaped = true;
|
|
1886
|
+
else if (ch === '"') {
|
|
1887
|
+
inString = false;
|
|
1888
|
+
record(i + 1, false);
|
|
1889
|
+
}
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (ch === '"') {
|
|
1893
|
+
inString = true;
|
|
1894
|
+
} else if (ch === "{" || ch === "[") {
|
|
1895
|
+
stack.push(ch);
|
|
1896
|
+
} else if (ch === "}" || ch === "]") {
|
|
1897
|
+
stack.pop();
|
|
1898
|
+
record(i + 1, true);
|
|
1899
|
+
} else if (ch === ",") {
|
|
1900
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const tryCut = (cut) => {
|
|
1904
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1905
|
+
if (head.endsWith(":")) return null;
|
|
1906
|
+
let closed = head;
|
|
1907
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1908
|
+
closed += CLOSERS[cut.open[d]];
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
JSON.parse(closed);
|
|
1912
|
+
return closed;
|
|
1913
|
+
} catch {
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1918
|
+
for (const containerOnly of [true, false]) {
|
|
1919
|
+
for (let n = 0; n < attempts; n++) {
|
|
1920
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1921
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1922
|
+
const salvaged = tryCut(cut);
|
|
1923
|
+
if (salvaged !== null) return salvaged;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
function extractJsonCandidates(content) {
|
|
1929
|
+
let text = content.trim();
|
|
1930
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1931
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1932
|
+
const start = text.search(/[{[]/);
|
|
1933
|
+
if (start === -1) return [];
|
|
1934
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1935
|
+
const end = text.lastIndexOf(closer);
|
|
1936
|
+
const candidates = [text.slice(start)];
|
|
1937
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1938
|
+
return candidates;
|
|
1939
|
+
}
|
|
1940
|
+
function parseAIJson(content) {
|
|
1941
|
+
const candidates = extractJsonCandidates(content);
|
|
1942
|
+
if (candidates.length === 0) return null;
|
|
1943
|
+
for (const text of candidates) {
|
|
1944
|
+
try {
|
|
1945
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
for (const text of candidates) {
|
|
1950
|
+
try {
|
|
1951
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
for (const text of candidates) {
|
|
1956
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1957
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1958
|
+
if (salvaged === null) continue;
|
|
1959
|
+
try {
|
|
1960
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1849
1968
|
// src/lib/api-client.ts
|
|
1850
1969
|
init_utils();
|
|
1851
1970
|
var PLAN_MAX_TOKENS = 1500;
|
|
@@ -2056,24 +2175,26 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", op
|
|
|
2056
2175
|
}
|
|
2057
2176
|
}
|
|
2058
2177
|
function parseAIPlanResponse(content) {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
if (codeBlockMatch?.[1]) {
|
|
2063
|
-
processedContent = codeBlockMatch[1].trim();
|
|
2064
|
-
}
|
|
2065
|
-
const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
|
|
2066
|
-
if (!jsonMatch) {
|
|
2067
|
-
return { success: false, error: "No valid JSON found in AI response" };
|
|
2068
|
-
}
|
|
2069
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
2070
|
-
return { success: true, plan: parsed };
|
|
2071
|
-
} catch (err) {
|
|
2072
|
-
return {
|
|
2073
|
-
success: false,
|
|
2074
|
-
error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
|
|
2075
|
-
};
|
|
2178
|
+
const parsed = parseAIJson(content);
|
|
2179
|
+
if (!parsed) {
|
|
2180
|
+
return { success: false, error: "No valid JSON found in AI response" };
|
|
2076
2181
|
}
|
|
2182
|
+
if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
|
|
2183
|
+
return { success: false, error: "AI response has no steps" };
|
|
2184
|
+
}
|
|
2185
|
+
return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
|
|
2186
|
+
}
|
|
2187
|
+
async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
2188
|
+
let lastParseError = "No valid JSON found in AI response";
|
|
2189
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
2190
|
+
const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
|
|
2191
|
+
if (!aiResult.success) return aiResult;
|
|
2192
|
+
const parsed = parseAIPlanResponse(aiResult.content);
|
|
2193
|
+
if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
|
|
2194
|
+
lastParseError = parsed.error;
|
|
2195
|
+
console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
|
|
2196
|
+
}
|
|
2197
|
+
return { success: false, error: lastParseError, isParseError: true };
|
|
2077
2198
|
}
|
|
2078
2199
|
function normalizeUrl(input) {
|
|
2079
2200
|
let url = input.trim();
|
|
@@ -2152,15 +2273,12 @@ function pricingUrl(opts) {
|
|
|
2152
2273
|
}
|
|
2153
2274
|
|
|
2154
2275
|
// src/lib/billing.ts
|
|
2155
|
-
function shouldWatermark(tier, videoDurationSeconds) {
|
|
2156
|
-
return tier === "free" && videoDurationSeconds > 20;
|
|
2157
|
-
}
|
|
2158
2276
|
function getUpgradeUrl(userEmail) {
|
|
2159
2277
|
return pricingUrl(userEmail ? { email: userEmail } : void 0);
|
|
2160
2278
|
}
|
|
2161
2279
|
|
|
2162
2280
|
// src/lib/config.ts
|
|
2163
|
-
var VERSION = "1.2.
|
|
2281
|
+
var VERSION = "1.2.6";
|
|
2164
2282
|
var CONFIG_FILE = "quickpeek.config.json";
|
|
2165
2283
|
var SIZE_PRESETS = {
|
|
2166
2284
|
wide: { width: 1920, height: 1080, zoom: 1 },
|
|
@@ -2187,11 +2305,12 @@ var DEFAULT_CONFIG = {
|
|
|
2187
2305
|
// ms
|
|
2188
2306
|
transitions: "none",
|
|
2189
2307
|
contrast: 0.25,
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
tempo
|
|
2308
|
+
// The recording plays at the pace it was filmed at. A tempo above 1 is
|
|
2309
|
+
// multiplied by the narration speed in VIDEO_PROFILES and by the fit
|
|
2310
|
+
// pass's squeeze, and the three together read as frantic rather than
|
|
2311
|
+
// brisk. Speed belongs in one place, so it lives in audioSpeed; set a
|
|
2312
|
+
// tempo in quickpeek.config.json to push the picture on top of that.
|
|
2313
|
+
tempo: 1,
|
|
2195
2314
|
stepDelay: 200
|
|
2196
2315
|
// ms delay between steps
|
|
2197
2316
|
},
|
|
@@ -2205,13 +2324,18 @@ var VIDEO_PROFILES = {
|
|
|
2205
2324
|
// these bitrates - the encode is pure wall-clock tax on every run.
|
|
2206
2325
|
wide: {
|
|
2207
2326
|
video: { width: 1920, height: 1080, zoom: 1.5, fitLimitSecs: 0, maxSpeedup: 1, preset: "veryfast" },
|
|
2208
|
-
|
|
2327
|
+
// A tutorial is watched to learn something, so the narrator is allowed to
|
|
2328
|
+
// be brisk but never hurried.
|
|
2329
|
+
audioSpeed: 1.15
|
|
2209
2330
|
},
|
|
2210
2331
|
short: {
|
|
2211
2332
|
// fadeOut 0: the video ends on a card the viewer is meant to read,
|
|
2212
2333
|
// and a fade takes it away mid-sentence.
|
|
2213
2334
|
video: { width: 1080, height: 1920, zoom: 3, fitLimitSecs: 60, maxSpeedup: 1.7, preset: "veryfast", fadeOut: 0 },
|
|
2214
|
-
|
|
2335
|
+
// 1.8 was unwatchable: stacked on tempo 1.2 and the outro speedup it ran
|
|
2336
|
+
// the voice at better than twice its natural rate. A short wants energy,
|
|
2337
|
+
// which is the voice and the cutting, not the tape running fast.
|
|
2338
|
+
audioSpeed: 1,
|
|
2215
2339
|
// A short lives or dies on energy. Ava is the warmest, most animated of
|
|
2216
2340
|
// the free neural voices; Sonia reads a short like the shipping forecast.
|
|
2217
2341
|
voice: "en-US-AvaMultilingualNeural"
|
|
@@ -2227,11 +2351,15 @@ function applyProfile(config, profile) {
|
|
|
2227
2351
|
format: "mp3",
|
|
2228
2352
|
bitrate: "192k",
|
|
2229
2353
|
...config.audio,
|
|
2230
|
-
|
|
2354
|
+
// outroOnly is on unless turned off, matching the video phase. The
|
|
2355
|
+
// speedup buys the song its window, so it applies to the profile's own
|
|
2356
|
+
// pace only: a speed somebody set by hand is the speed they wanted.
|
|
2357
|
+
speed: config.audio?.speed ?? preset.audioSpeed * (config.music?.outroOnly !== false ? OUTRO_SONG_SPEEDUP : 1),
|
|
2231
2358
|
...config.audio?.voice || !preset.voice ? {} : { voice: preset.voice }
|
|
2232
2359
|
}
|
|
2233
2360
|
};
|
|
2234
2361
|
}
|
|
2362
|
+
var OUTRO_SONG_SPEEDUP = 1.13;
|
|
2235
2363
|
var DEFAULT_CAPTIONS = {
|
|
2236
2364
|
font: "Arial",
|
|
2237
2365
|
outline: "#000000",
|
|
@@ -2287,8 +2415,24 @@ function defaultCaptionsFor(width, height) {
|
|
|
2287
2415
|
};
|
|
2288
2416
|
}
|
|
2289
2417
|
|
|
2290
|
-
// src/
|
|
2418
|
+
// src/browser/headless.ts
|
|
2291
2419
|
import { chromium } from "playwright";
|
|
2420
|
+
var 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";
|
|
2421
|
+
async function launchHeadlessChromium(options = {}) {
|
|
2422
|
+
const opts = { ...options, headless: true };
|
|
2423
|
+
try {
|
|
2424
|
+
return await chromium.launch(opts);
|
|
2425
|
+
} catch (bundledError) {
|
|
2426
|
+
try {
|
|
2427
|
+
return await chromium.launch({ ...opts, channel: "chrome" });
|
|
2428
|
+
} catch {
|
|
2429
|
+
const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
|
|
2430
|
+
throw new Error(`${NO_BROWSER_MESSAGE}
|
|
2431
|
+
|
|
2432
|
+
Playwright reported: ${detail}`);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2292
2436
|
|
|
2293
2437
|
// src/lib/scraping.ts
|
|
2294
2438
|
var EXCLUDED_LINK_PATTERNS = [
|
|
@@ -2409,6 +2553,7 @@ async function waitForDomStable(page, cap = SETTLE_CAP_MS) {
|
|
|
2409
2553
|
var PLACEHOLDER_POLL_MS = 250;
|
|
2410
2554
|
var PLACEHOLDER_CAP_MS = 8e3;
|
|
2411
2555
|
var PLACEHOLDER_STATIC_POLLS = 4;
|
|
2556
|
+
var PLACEHOLDER_CLEAR_POLLS = 3;
|
|
2412
2557
|
var PLACEHOLDER_SELECTOR = [
|
|
2413
2558
|
'[aria-busy="true"]',
|
|
2414
2559
|
'[role="progressbar"]',
|
|
@@ -2421,6 +2566,7 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
2421
2566
|
const deadline = Date.now() + cap;
|
|
2422
2567
|
let previous = -1;
|
|
2423
2568
|
let unchanged = 0;
|
|
2569
|
+
let clear = 0;
|
|
2424
2570
|
while (Date.now() < deadline) {
|
|
2425
2571
|
const count = await page.evaluate((selector) => {
|
|
2426
2572
|
let visible = 0;
|
|
@@ -2430,7 +2576,12 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
2430
2576
|
}
|
|
2431
2577
|
return visible;
|
|
2432
2578
|
}, PLACEHOLDER_SELECTOR).catch(() => 0);
|
|
2433
|
-
if (count === 0)
|
|
2579
|
+
if (count === 0) {
|
|
2580
|
+
if (++clear >= PLACEHOLDER_CLEAR_POLLS) return;
|
|
2581
|
+
await page.waitForTimeout(PLACEHOLDER_POLL_MS);
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
clear = 0;
|
|
2434
2585
|
if (count === previous) {
|
|
2435
2586
|
if (++unchanged >= PLACEHOLDER_STATIC_POLLS) return;
|
|
2436
2587
|
} else {
|
|
@@ -2519,7 +2670,13 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
|
2519
2670
|
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
2520
2671
|
}
|
|
2521
2672
|
const browserTask = task("Launching browser");
|
|
2522
|
-
|
|
2673
|
+
let browser;
|
|
2674
|
+
try {
|
|
2675
|
+
browser = await launchHeadlessChromium();
|
|
2676
|
+
} catch (err) {
|
|
2677
|
+
browserTask.succeed("Could not launch a browser");
|
|
2678
|
+
throw err;
|
|
2679
|
+
}
|
|
2523
2680
|
try {
|
|
2524
2681
|
const context = await browser.newContext({
|
|
2525
2682
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
@@ -2702,108 +2859,6 @@ var CURSOR_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsBAMAAAB0
|
|
|
2702
2859
|
// src/lib/index.ts
|
|
2703
2860
|
init_ffmpeg2();
|
|
2704
2861
|
|
|
2705
|
-
// src/lib/json-repair.ts
|
|
2706
|
-
function repairUnicodeEscapes(content) {
|
|
2707
|
-
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
2708
|
-
const code = Number.parseInt(hex, 16);
|
|
2709
|
-
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
2710
|
-
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
2711
|
-
}
|
|
2712
|
-
var MAX_SALVAGE_ATTEMPTS = 500;
|
|
2713
|
-
var CLOSERS = { "{": "}", "[": "]" };
|
|
2714
|
-
function salvageTruncatedJson(content) {
|
|
2715
|
-
const stack = [];
|
|
2716
|
-
const cuts = [];
|
|
2717
|
-
let inString = false;
|
|
2718
|
-
let escaped = false;
|
|
2719
|
-
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
2720
|
-
for (let i = 0; i < content.length; i++) {
|
|
2721
|
-
const ch = content[i];
|
|
2722
|
-
if (inString) {
|
|
2723
|
-
if (escaped) escaped = false;
|
|
2724
|
-
else if (ch === "\\") escaped = true;
|
|
2725
|
-
else if (ch === '"') {
|
|
2726
|
-
inString = false;
|
|
2727
|
-
record(i + 1, false);
|
|
2728
|
-
}
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
|
-
if (ch === '"') {
|
|
2732
|
-
inString = true;
|
|
2733
|
-
} else if (ch === "{" || ch === "[") {
|
|
2734
|
-
stack.push(ch);
|
|
2735
|
-
} else if (ch === "}" || ch === "]") {
|
|
2736
|
-
stack.pop();
|
|
2737
|
-
record(i + 1, true);
|
|
2738
|
-
} else if (ch === ",") {
|
|
2739
|
-
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
const tryCut = (cut) => {
|
|
2743
|
-
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
2744
|
-
if (head.endsWith(":")) return null;
|
|
2745
|
-
let closed = head;
|
|
2746
|
-
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
2747
|
-
closed += CLOSERS[cut.open[d]];
|
|
2748
|
-
}
|
|
2749
|
-
try {
|
|
2750
|
-
JSON.parse(closed);
|
|
2751
|
-
return closed;
|
|
2752
|
-
} catch {
|
|
2753
|
-
return null;
|
|
2754
|
-
}
|
|
2755
|
-
};
|
|
2756
|
-
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
2757
|
-
for (const containerOnly of [true, false]) {
|
|
2758
|
-
for (let n = 0; n < attempts; n++) {
|
|
2759
|
-
const cut = cuts[cuts.length - 1 - n];
|
|
2760
|
-
if (containerOnly && !cut.closesContainer) continue;
|
|
2761
|
-
const salvaged = tryCut(cut);
|
|
2762
|
-
if (salvaged !== null) return salvaged;
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
return null;
|
|
2766
|
-
}
|
|
2767
|
-
function extractJsonCandidates(content) {
|
|
2768
|
-
let text = content.trim();
|
|
2769
|
-
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
2770
|
-
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
2771
|
-
const start = text.search(/[{[]/);
|
|
2772
|
-
if (start === -1) return [];
|
|
2773
|
-
const closer = text[start] === "{" ? "}" : "]";
|
|
2774
|
-
const end = text.lastIndexOf(closer);
|
|
2775
|
-
const candidates = [text.slice(start)];
|
|
2776
|
-
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
2777
|
-
return candidates;
|
|
2778
|
-
}
|
|
2779
|
-
function parseAIJson(content) {
|
|
2780
|
-
const candidates = extractJsonCandidates(content);
|
|
2781
|
-
if (candidates.length === 0) return null;
|
|
2782
|
-
for (const text of candidates) {
|
|
2783
|
-
try {
|
|
2784
|
-
return { value: JSON.parse(text), repaired: false };
|
|
2785
|
-
} catch {
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
for (const text of candidates) {
|
|
2789
|
-
try {
|
|
2790
|
-
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
2791
|
-
} catch {
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
for (const text of candidates) {
|
|
2795
|
-
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
2796
|
-
const salvaged = salvageTruncatedJson(variant);
|
|
2797
|
-
if (salvaged === null) continue;
|
|
2798
|
-
try {
|
|
2799
|
-
return { value: JSON.parse(salvaged), repaired: true };
|
|
2800
|
-
} catch {
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
return null;
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
2862
|
// src/lib/prompts.ts
|
|
2808
2863
|
init_utils();
|
|
2809
2864
|
function buildSystemPrompt(maxSteps, profile) {
|
|
@@ -3162,6 +3217,9 @@ function renderHormoziAss(ctx) {
|
|
|
3162
3217
|
return `${header}${lines.join("\n")}
|
|
3163
3218
|
`;
|
|
3164
3219
|
}
|
|
3220
|
+
function looksLikeAddress(token) {
|
|
3221
|
+
return /^(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s]*)?[.,!?]?$/i.test(token);
|
|
3222
|
+
}
|
|
3165
3223
|
function renderShortsAss(ctx) {
|
|
3166
3224
|
const { captions, width, height } = ctx;
|
|
3167
3225
|
const font = resolveCaptionFont(captions.font === "Arial" ? "Arial Black" : captions.font);
|
|
@@ -3172,7 +3230,7 @@ function renderShortsAss(ctx) {
|
|
|
3172
3230
|
const alignment = positionToAlignment(captions.position);
|
|
3173
3231
|
const outline = Math.max(2, Math.round(fontSize / 22));
|
|
3174
3232
|
const upper = true;
|
|
3175
|
-
const caseOf = (t) => toCaptionCase(t, upper);
|
|
3233
|
+
const caseOf = (t) => looksLikeAddress(t) ? t : toCaptionCase(t, upper);
|
|
3176
3234
|
const maxChars = fittingMaxChars(width, fontSize, SIDE_MARGIN, captions.maxChars, upper);
|
|
3177
3235
|
const header = `[Script Info]
|
|
3178
3236
|
Title: QuickPeek Demo
|
|
@@ -3472,6 +3530,8 @@ asyncio.run(main())
|
|
|
3472
3530
|
await writeFile2(tempScriptPath, pythonScript);
|
|
3473
3531
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
3474
3532
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
3533
|
+
const written = await stat(outputPath).catch(() => null);
|
|
3534
|
+
if (!written?.size) throw new Error("edge-tts produced no audio");
|
|
3475
3535
|
return await readWordsSidecar(outputPath);
|
|
3476
3536
|
} finally {
|
|
3477
3537
|
await unlink3(tempScriptPath).catch(() => {
|
|
@@ -3695,12 +3755,21 @@ function spellAcronyms(text) {
|
|
|
3695
3755
|
(m) => m.split("").map((c) => LETTER_SOUNDS[c] ?? c).join(" ")
|
|
3696
3756
|
);
|
|
3697
3757
|
}
|
|
3758
|
+
var SPOKEN_TLD = {
|
|
3759
|
+
ai: "ay-eye"
|
|
3760
|
+
};
|
|
3698
3761
|
function bindDomains(text) {
|
|
3699
|
-
return text.replace(/\b([a-z0-9][a-z0-9
|
|
3762
|
+
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(
|
|
3763
|
+
/\b([a-z0-9][a-z0-9-]*)\.([a-z]{2,24})\b/gi,
|
|
3764
|
+
(_m, host, tld) => `${host}-dot-${SPOKEN_TLD[tld.toLowerCase()] ?? tld}`
|
|
3765
|
+
);
|
|
3700
3766
|
}
|
|
3701
3767
|
function speakSymbols(text) {
|
|
3702
3768
|
return text.replace(/\s*&\s*/g, " and ").replace(/[<>]/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
3703
3769
|
}
|
|
3770
|
+
function spokenForm(caption) {
|
|
3771
|
+
return speakNumbers(spellAcronyms(toSpokenForm(bindDomains(speakSymbols(caption)))));
|
|
3772
|
+
}
|
|
3704
3773
|
async function generateVoiceover(steps, config, workDir, outputDir, logError, task, info) {
|
|
3705
3774
|
resetVoiceEngine();
|
|
3706
3775
|
const voice = resolveVoice2(config);
|
|
@@ -3747,7 +3816,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError, ta
|
|
|
3747
3816
|
}
|
|
3748
3817
|
const audioPath = join6(audioDir, `step-${stepNum}.mp3`);
|
|
3749
3818
|
try {
|
|
3750
|
-
const spokenText =
|
|
3819
|
+
const spokenText = spokenForm(step.caption);
|
|
3751
3820
|
const key = clipKey(spokenText, voice, ratePercent ?? "");
|
|
3752
3821
|
const cacheDir = join6(outputDir, ".tts-clips");
|
|
3753
3822
|
const cached = await reuseClip(cacheDir, key, audioPath);
|
|
@@ -3855,6 +3924,7 @@ export {
|
|
|
3855
3924
|
NO_CLIP_OUTRO,
|
|
3856
3925
|
PLAN_MAX_TOKENS,
|
|
3857
3926
|
RATE_FACTOR,
|
|
3927
|
+
RELAY_BASE,
|
|
3858
3928
|
SERVER_BACKOFF_MS,
|
|
3859
3929
|
SIZE_PRESETS,
|
|
3860
3930
|
TRIM_EDGE_SILENCE,
|
|
@@ -3879,6 +3949,7 @@ export {
|
|
|
3879
3949
|
buildUserPrompt,
|
|
3880
3950
|
buildWatermarkFilters,
|
|
3881
3951
|
callAI,
|
|
3952
|
+
callAIForPlan,
|
|
3882
3953
|
callAIViaRelay,
|
|
3883
3954
|
canReuseVoiceover,
|
|
3884
3955
|
captionCanvas,
|
|
@@ -3906,6 +3977,7 @@ export {
|
|
|
3906
3977
|
getErrorMessage,
|
|
3907
3978
|
getLanguageName,
|
|
3908
3979
|
getMediaDuration,
|
|
3980
|
+
getOrRefreshRelayToken,
|
|
3909
3981
|
getStatus,
|
|
3910
3982
|
getStatusCached,
|
|
3911
3983
|
getTierByEmail,
|
|
@@ -3949,12 +4021,12 @@ export {
|
|
|
3949
4021
|
runPooled,
|
|
3950
4022
|
salvageTruncatedJson,
|
|
3951
4023
|
shouldSkipLink,
|
|
3952
|
-
shouldWatermark,
|
|
3953
4024
|
showPaywall,
|
|
3954
4025
|
speakSymbols,
|
|
3955
4026
|
speedUpMiddle,
|
|
3956
4027
|
speedUpVideo,
|
|
3957
4028
|
spliceVideos,
|
|
4029
|
+
spokenForm,
|
|
3958
4030
|
spokenWordTimings,
|
|
3959
4031
|
stripLongDashes,
|
|
3960
4032
|
synthesizeSpeech,
|