@spark-apps/quickpeek 1.2.1 → 1.2.2
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 +204 -204
- package/dist/highlight.css +15 -15
- package/dist/index.d.mts +79 -3
- package/dist/index.mjs +253 -51
- package/dist/mcp-tools.mjs +1294 -602
- package/dist/qp.js +1271 -728
- package/dist/{scraping-CHN4ClyT.d.mts → scraping-BgepklP3.d.mts} +44 -3
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +64 -24
- package/mcp.mjs +5 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -347,8 +347,39 @@ function getMediaDuration(filePath) {
|
|
|
347
347
|
proc.on("error", reject);
|
|
348
348
|
});
|
|
349
349
|
}
|
|
350
|
+
function getAudioCodec(filePath) {
|
|
351
|
+
return new Promise((resolve3) => {
|
|
352
|
+
const args = [
|
|
353
|
+
"-v",
|
|
354
|
+
"error",
|
|
355
|
+
"-select_streams",
|
|
356
|
+
"a:0",
|
|
357
|
+
"-show_entries",
|
|
358
|
+
"stream=codec_name",
|
|
359
|
+
"-of",
|
|
360
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
361
|
+
filePath
|
|
362
|
+
];
|
|
363
|
+
const proc = spawnFFmpeg("ffprobe", args);
|
|
364
|
+
let stdout = "";
|
|
365
|
+
proc.stdout.on("data", (data) => {
|
|
366
|
+
stdout += data;
|
|
367
|
+
});
|
|
368
|
+
proc.on("close", (code) => resolve3(code === 0 ? stdout.trim() : ""));
|
|
369
|
+
proc.on("error", () => resolve3(""));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
async function isHonestWav(filePath) {
|
|
373
|
+
const codec = await getAudioCodec(filePath);
|
|
374
|
+
return codec.startsWith("pcm_");
|
|
375
|
+
}
|
|
376
|
+
function buildConcatArgs(listPath, output) {
|
|
377
|
+
const isWav = output.toLowerCase().endsWith(".wav");
|
|
378
|
+
const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
|
|
379
|
+
return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
|
|
380
|
+
}
|
|
350
381
|
function concatMedia(listPath, output) {
|
|
351
|
-
return runFFmpeg(
|
|
382
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
352
383
|
}
|
|
353
384
|
function generateSilence(outputPath, durationMs) {
|
|
354
385
|
return runFFmpeg(
|
|
@@ -775,6 +806,8 @@ var init_timing = __esm({
|
|
|
775
806
|
|
|
776
807
|
// src/lib/api-client.ts
|
|
777
808
|
init_utils();
|
|
809
|
+
var SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
810
|
+
var CLI_BACKOFF_MS = [1e4, 3e4];
|
|
778
811
|
var SPARK_AI_BASE = "https://sparkbrain.app";
|
|
779
812
|
var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
780
813
|
var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
@@ -784,6 +817,20 @@ var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
|
784
817
|
function apiKey() {
|
|
785
818
|
return process.env.SPARK_AI_KEY || "";
|
|
786
819
|
}
|
|
820
|
+
function hostOf(url) {
|
|
821
|
+
try {
|
|
822
|
+
return new URL(url).host;
|
|
823
|
+
} catch {
|
|
824
|
+
return "AI service";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function firstLine(text) {
|
|
828
|
+
return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
|
|
829
|
+
}
|
|
830
|
+
function truncate(text, max) {
|
|
831
|
+
const trimmed = (text ?? "").trim();
|
|
832
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
|
|
833
|
+
}
|
|
787
834
|
var cachedDirectToken = null;
|
|
788
835
|
var cachedRelayToken = null;
|
|
789
836
|
async function getOrRefreshDirectToken() {
|
|
@@ -837,8 +884,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
837
884
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
838
885
|
}
|
|
839
886
|
}
|
|
840
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
841
|
-
|
|
887
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
|
|
888
|
+
const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
|
|
889
|
+
let lastRetriableError = null;
|
|
890
|
+
const host = hostOf(chatUrl);
|
|
842
891
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
843
892
|
try {
|
|
844
893
|
const response = await fetch(chatUrl, {
|
|
@@ -854,36 +903,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
854
903
|
]
|
|
855
904
|
})
|
|
856
905
|
});
|
|
906
|
+
const bodyText = await response.text().catch(() => "");
|
|
857
907
|
let data = {};
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
908
|
+
try {
|
|
909
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
910
|
+
} catch {
|
|
861
911
|
}
|
|
862
912
|
if (!response.ok) {
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
913
|
+
const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
|
|
914
|
+
const error = `${host} ${response.status}: ${upstream}`;
|
|
915
|
+
const detail = truncate(bodyText, 800);
|
|
916
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
917
|
+
if (isRetriable && attempt < 3) {
|
|
918
|
+
const retryAfter2 = response.headers.get("Retry-After");
|
|
919
|
+
const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
|
|
920
|
+
onRetry?.({ attempt, status: response.status, delayMs, host });
|
|
921
|
+
lastRetriableError = {
|
|
868
922
|
success: false,
|
|
869
923
|
error,
|
|
870
|
-
isRateLimit:
|
|
871
|
-
|
|
924
|
+
isRateLimit: response.status === 429,
|
|
925
|
+
status: response.status,
|
|
926
|
+
host,
|
|
927
|
+
...detail ? { detail } : {},
|
|
928
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
872
929
|
};
|
|
873
930
|
await new Promise((resolve3) => setTimeout(resolve3, Math.min(delayMs, 3e4)));
|
|
874
931
|
continue;
|
|
875
932
|
}
|
|
933
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
934
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
876
935
|
return {
|
|
877
936
|
success: false,
|
|
878
937
|
error,
|
|
879
938
|
isRateLimit: response.status === 429,
|
|
880
|
-
isAuthError: response.status === 401 || response.status === 403
|
|
939
|
+
isAuthError: response.status === 401 || response.status === 403,
|
|
940
|
+
status: response.status,
|
|
941
|
+
host,
|
|
942
|
+
...detail ? { detail } : {},
|
|
943
|
+
...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
|
|
881
944
|
};
|
|
882
945
|
}
|
|
883
946
|
if (!data.success || !data.response) {
|
|
884
947
|
return {
|
|
885
948
|
success: false,
|
|
886
|
-
error: data.error || "
|
|
949
|
+
error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
|
|
950
|
+
status: response.status,
|
|
951
|
+
host,
|
|
952
|
+
...bodyText ? { detail: truncate(bodyText, 800) } : {}
|
|
887
953
|
};
|
|
888
954
|
}
|
|
889
955
|
return {
|
|
@@ -897,14 +963,16 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
897
963
|
}
|
|
898
964
|
};
|
|
899
965
|
} catch (err) {
|
|
900
|
-
if (attempt === 3)
|
|
966
|
+
if (attempt === 3) {
|
|
967
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
968
|
+
}
|
|
901
969
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
902
970
|
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
903
971
|
}
|
|
904
972
|
}
|
|
905
|
-
return
|
|
973
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
906
974
|
}
|
|
907
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
975
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
908
976
|
if (!apiKey()) {
|
|
909
977
|
return {
|
|
910
978
|
success: false,
|
|
@@ -914,12 +982,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
914
982
|
}
|
|
915
983
|
try {
|
|
916
984
|
const token = await getOrRefreshDirectToken();
|
|
917
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
985
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
918
986
|
} catch (err) {
|
|
919
987
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
920
988
|
}
|
|
921
989
|
}
|
|
922
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
990
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
923
991
|
if (!email) {
|
|
924
992
|
return {
|
|
925
993
|
success: false,
|
|
@@ -929,11 +997,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
929
997
|
}
|
|
930
998
|
try {
|
|
931
999
|
let token = await getOrRefreshRelayToken(email);
|
|
932
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
1000
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
933
1001
|
if (!result.success && result.isAuthError) {
|
|
934
1002
|
cachedRelayToken = null;
|
|
935
1003
|
token = await getOrRefreshRelayToken(email);
|
|
936
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
1004
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
937
1005
|
}
|
|
938
1006
|
return result;
|
|
939
1007
|
} catch (err) {
|
|
@@ -968,7 +1036,8 @@ function normalizeUrl(input) {
|
|
|
968
1036
|
|
|
969
1037
|
// src/lib/SparkStripe-feature-gating.ts
|
|
970
1038
|
var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
971
|
-
var
|
|
1039
|
+
var OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
1040
|
+
var BASE = "https://sparkpay.dev";
|
|
972
1041
|
var TIERS = {
|
|
973
1042
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
974
1043
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -1011,7 +1080,7 @@ async function getStatusCached(email) {
|
|
|
1011
1080
|
function getTier(d) {
|
|
1012
1081
|
if (!d.subscription || !d.registered) return "free";
|
|
1013
1082
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
1014
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
1083
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
1015
1084
|
}
|
|
1016
1085
|
var isPaid = (d) => getTier(d) !== "free";
|
|
1017
1086
|
var hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
|
|
@@ -1033,7 +1102,7 @@ async function isVerified(email) {
|
|
|
1033
1102
|
}
|
|
1034
1103
|
}
|
|
1035
1104
|
function pricingUrl(opts) {
|
|
1036
|
-
const u = new URL(`${BASE}
|
|
1105
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
1037
1106
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
1038
1107
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
1039
1108
|
return u.toString();
|
|
@@ -1193,6 +1262,9 @@ function shouldSkipLink(text, href, currentPath) {
|
|
|
1193
1262
|
}
|
|
1194
1263
|
|
|
1195
1264
|
// src/lib/crawl.ts
|
|
1265
|
+
function idSelector(id) {
|
|
1266
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
1267
|
+
}
|
|
1196
1268
|
function getActionForType(type) {
|
|
1197
1269
|
switch (type) {
|
|
1198
1270
|
case "button":
|
|
@@ -1327,9 +1399,9 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
|
1327
1399
|
} else if (el.isFile || el.type === "file") {
|
|
1328
1400
|
selector = 'input[type="file"]';
|
|
1329
1401
|
} else if (el.type === "range") {
|
|
1330
|
-
selector = el.id ?
|
|
1402
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
1331
1403
|
} else if (el.type === "checkbox") {
|
|
1332
|
-
selector = el.id ?
|
|
1404
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
1333
1405
|
} else if (el.type === "button") {
|
|
1334
1406
|
if (btnTexts[el.text] === 1) {
|
|
1335
1407
|
selector = `text="${el.text}"`;
|
|
@@ -1339,7 +1411,7 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
|
1339
1411
|
} else if (el.type === "link") {
|
|
1340
1412
|
selector = `text="${el.text}"`;
|
|
1341
1413
|
} else if (el.id) {
|
|
1342
|
-
selector =
|
|
1414
|
+
selector = idSelector(el.id);
|
|
1343
1415
|
} else if (el.name) {
|
|
1344
1416
|
selector = `[name="${el.name}"]`;
|
|
1345
1417
|
} else if (el.placeholder) {
|
|
@@ -1381,6 +1453,108 @@ var CURSOR_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsBAMAAAB0
|
|
|
1381
1453
|
// src/lib/index.ts
|
|
1382
1454
|
init_ffmpeg2();
|
|
1383
1455
|
|
|
1456
|
+
// src/lib/json-repair.ts
|
|
1457
|
+
function repairUnicodeEscapes(content) {
|
|
1458
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1459
|
+
const code = Number.parseInt(hex, 16);
|
|
1460
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1461
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1462
|
+
}
|
|
1463
|
+
var MAX_SALVAGE_ATTEMPTS = 500;
|
|
1464
|
+
var CLOSERS = { "{": "}", "[": "]" };
|
|
1465
|
+
function salvageTruncatedJson(content) {
|
|
1466
|
+
const stack = [];
|
|
1467
|
+
const cuts = [];
|
|
1468
|
+
let inString = false;
|
|
1469
|
+
let escaped = false;
|
|
1470
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1471
|
+
for (let i = 0; i < content.length; i++) {
|
|
1472
|
+
const ch = content[i];
|
|
1473
|
+
if (inString) {
|
|
1474
|
+
if (escaped) escaped = false;
|
|
1475
|
+
else if (ch === "\\") escaped = true;
|
|
1476
|
+
else if (ch === '"') {
|
|
1477
|
+
inString = false;
|
|
1478
|
+
record(i + 1, false);
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (ch === '"') {
|
|
1483
|
+
inString = true;
|
|
1484
|
+
} else if (ch === "{" || ch === "[") {
|
|
1485
|
+
stack.push(ch);
|
|
1486
|
+
} else if (ch === "}" || ch === "]") {
|
|
1487
|
+
stack.pop();
|
|
1488
|
+
record(i + 1, true);
|
|
1489
|
+
} else if (ch === ",") {
|
|
1490
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const tryCut = (cut) => {
|
|
1494
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1495
|
+
if (head.endsWith(":")) return null;
|
|
1496
|
+
let closed = head;
|
|
1497
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1498
|
+
closed += CLOSERS[cut.open[d]];
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
JSON.parse(closed);
|
|
1502
|
+
return closed;
|
|
1503
|
+
} catch {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1508
|
+
for (const containerOnly of [true, false]) {
|
|
1509
|
+
for (let n = 0; n < attempts; n++) {
|
|
1510
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1511
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1512
|
+
const salvaged = tryCut(cut);
|
|
1513
|
+
if (salvaged !== null) return salvaged;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
function extractJsonCandidates(content) {
|
|
1519
|
+
let text = content.trim();
|
|
1520
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1521
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1522
|
+
const start = text.search(/[{[]/);
|
|
1523
|
+
if (start === -1) return [];
|
|
1524
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1525
|
+
const end = text.lastIndexOf(closer);
|
|
1526
|
+
const candidates = [text.slice(start)];
|
|
1527
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1528
|
+
return candidates;
|
|
1529
|
+
}
|
|
1530
|
+
function parseAIJson(content) {
|
|
1531
|
+
const candidates = extractJsonCandidates(content);
|
|
1532
|
+
if (candidates.length === 0) return null;
|
|
1533
|
+
for (const text of candidates) {
|
|
1534
|
+
try {
|
|
1535
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const text of candidates) {
|
|
1540
|
+
try {
|
|
1541
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const text of candidates) {
|
|
1546
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1547
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1548
|
+
if (salvaged === null) continue;
|
|
1549
|
+
try {
|
|
1550
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1384
1558
|
// src/lib/prompts.ts
|
|
1385
1559
|
init_utils();
|
|
1386
1560
|
function buildSystemPrompt(maxSteps) {
|
|
@@ -1569,7 +1743,7 @@ function generateASSHeader(width, height, font, size, primaryColor, secondaryCol
|
|
|
1569
1743
|
async function registerFreeUser(email) {
|
|
1570
1744
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1571
1745
|
try {
|
|
1572
|
-
const response = await fetch(`https://
|
|
1746
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1573
1747
|
method: "POST",
|
|
1574
1748
|
headers: { "Content-Type": "application/json" },
|
|
1575
1749
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1653,11 +1827,27 @@ init_helpers();
|
|
|
1653
1827
|
// src/lib/tts/synthesize.ts
|
|
1654
1828
|
init_helpers();
|
|
1655
1829
|
import { exec } from "child_process";
|
|
1656
|
-
import {
|
|
1830
|
+
import { createWriteStream } from "fs";
|
|
1831
|
+
import { stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
1657
1832
|
import { join as join4, resolve as resolve2 } from "path";
|
|
1833
|
+
import { pipeline } from "stream/promises";
|
|
1658
1834
|
import { promisify } from "util";
|
|
1659
1835
|
var execAsync = promisify(exec);
|
|
1836
|
+
var PYTHON_ATTEMPTS = 2;
|
|
1660
1837
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1838
|
+
let pythonMsg = "";
|
|
1839
|
+
for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
|
|
1840
|
+
try {
|
|
1841
|
+
await synthesizeWithPython(text, outputPath, voice);
|
|
1842
|
+
return;
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
pythonMsg = err instanceof Error ? err.message : String(err);
|
|
1845
|
+
if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
await synthesizeWithNode(text, outputPath, voice, pythonMsg);
|
|
1849
|
+
}
|
|
1850
|
+
async function synthesizeWithPython(text, outputPath, voice) {
|
|
1661
1851
|
const tempScriptPath = join4(resolve2(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1662
1852
|
const wslScriptPath = isWindows ? toWSLPathForTTS(resolve2(tempScriptPath)) : tempScriptPath;
|
|
1663
1853
|
const wslOutputPath = isWindows ? toWSLPathForTTS(resolve2(outputPath)) : outputPath;
|
|
@@ -1678,28 +1868,30 @@ asyncio.run(main())
|
|
|
1678
1868
|
await writeFile2(tempScriptPath, pythonScript);
|
|
1679
1869
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1680
1870
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1871
|
+
} finally {
|
|
1872
|
+
await unlink2(tempScriptPath).catch(() => {
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
|
|
1877
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1878
|
+
try {
|
|
1879
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
1880
|
+
const tts = new MsEdgeTTS();
|
|
1881
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
1882
|
+
const { audioStream } = tts.toStream(text);
|
|
1883
|
+
await pipeline(audioStream, createWriteStream(outputPath));
|
|
1884
|
+
const { size } = await stat(outputPath);
|
|
1885
|
+
if (size === 0) throw new Error("No audio data received");
|
|
1886
|
+
return;
|
|
1887
|
+
} catch (nodeErr) {
|
|
1888
|
+
await unlink2(outputPath).catch(() => {
|
|
1889
|
+
});
|
|
1890
|
+
if (attempt === maxRetries) {
|
|
1891
|
+
const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
1892
|
+
throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
|
|
1702
1893
|
}
|
|
1894
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1703
1895
|
}
|
|
1704
1896
|
}
|
|
1705
1897
|
}
|
|
@@ -1811,6 +2003,7 @@ init_timing();
|
|
|
1811
2003
|
init_utils();
|
|
1812
2004
|
init_watermark();
|
|
1813
2005
|
export {
|
|
2006
|
+
CLI_BACKOFF_MS,
|
|
1814
2007
|
CONFIG_FILE,
|
|
1815
2008
|
CURSOR_CSS,
|
|
1816
2009
|
CURSOR_PNG,
|
|
@@ -1821,9 +2014,11 @@ export {
|
|
|
1821
2014
|
FEATURES,
|
|
1822
2015
|
INTERACTIVE_SELECTORS,
|
|
1823
2016
|
WIDE_VOICES as LANGUAGE_VOICES,
|
|
2017
|
+
SERVER_BACKOFF_MS,
|
|
1824
2018
|
VERSION,
|
|
1825
2019
|
alignVoiceoverToRecording,
|
|
1826
2020
|
applyTransitions,
|
|
2021
|
+
buildConcatArgs,
|
|
1827
2022
|
buildKeepSegments,
|
|
1828
2023
|
buildStepTransitions,
|
|
1829
2024
|
buildSystemPrompt,
|
|
@@ -1837,9 +2032,11 @@ export {
|
|
|
1837
2032
|
concatVideoWithOutro,
|
|
1838
2033
|
crawlPage,
|
|
1839
2034
|
estimateWordTimings,
|
|
2035
|
+
extractJsonCandidates,
|
|
1840
2036
|
generateASSHeader,
|
|
1841
2037
|
generateSilence,
|
|
1842
2038
|
generateVoiceover,
|
|
2039
|
+
getAudioCodec,
|
|
1843
2040
|
getErrorMessage,
|
|
1844
2041
|
getLanguageName,
|
|
1845
2042
|
getMediaDuration,
|
|
@@ -1850,20 +2047,25 @@ export {
|
|
|
1850
2047
|
hasAccess,
|
|
1851
2048
|
hasTrialRemaining,
|
|
1852
2049
|
hexToAss as hexToASS,
|
|
2050
|
+
idSelector,
|
|
1853
2051
|
isCanceling,
|
|
1854
2052
|
isExcludedLink,
|
|
2053
|
+
isHonestWav,
|
|
1855
2054
|
isPaid,
|
|
1856
2055
|
isVerified,
|
|
1857
2056
|
loadExistingVoiceover,
|
|
1858
2057
|
loadVoiceoverMeta,
|
|
1859
2058
|
mapTimestampsAfterCompression,
|
|
1860
2059
|
normalizeUrl,
|
|
2060
|
+
parseAIJson,
|
|
1861
2061
|
parseAIPlanResponse,
|
|
1862
2062
|
parsePartial,
|
|
1863
2063
|
pricingUrl,
|
|
1864
2064
|
registerFreeUser,
|
|
2065
|
+
repairUnicodeEscapes,
|
|
1865
2066
|
resolveVoice,
|
|
1866
2067
|
rgbaToHex,
|
|
2068
|
+
salvageTruncatedJson,
|
|
1867
2069
|
shouldSkipLink,
|
|
1868
2070
|
shouldWatermark,
|
|
1869
2071
|
showPaywall,
|