@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/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(["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", output], "ffmpeg concat failed");
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
- let lastRateLimitError = null;
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
- const contentType = response.headers.get("content-type") ?? "";
859
- if (contentType.includes("application/json")) {
860
- data = await response.json();
908
+ try {
909
+ data = bodyText ? JSON.parse(bodyText) : {};
910
+ } catch {
861
911
  }
862
912
  if (!response.ok) {
863
- const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
864
- if (response.status === 429 && attempt < 3) {
865
- const retryAfter = response.headers.get("Retry-After");
866
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
867
- lastRateLimitError = {
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: true,
871
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
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 || "No response from Spark AI"
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) throw err;
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 lastRateLimitError || { success: false, error: "Request failed after retries" };
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 BASE = "https://sparkstripe.com";
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] ?? "free";
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}/pricing/${APP_ID}`);
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 ? `#${el.id}` : 'input[type="range"]';
1402
+ selector = el.id ? idSelector(el.id) : 'input[type="range"]';
1331
1403
  } else if (el.type === "checkbox") {
1332
- selector = el.id ? `#${el.id}` : 'input[type="checkbox"]';
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 = `#${el.id}`;
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://sparkstripe.com/api/register-free`, {
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 { writeFile as writeFile2 } from "fs/promises";
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
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1682
- }));
1683
- return;
1684
- } catch (pythonErr) {
1685
- await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
1686
- }));
1687
- const pythonMsg = pythonErr instanceof Error ? pythonErr.message : String(pythonErr);
1688
- const maxRetries = 2;
1689
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1690
- try {
1691
- const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
1692
- const tts = new MsEdgeTTS();
1693
- await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
1694
- await tts.toFile(outputPath, text);
1695
- return;
1696
- } catch (nodeErr) {
1697
- if (attempt === maxRetries) {
1698
- const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
1699
- throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
1700
- }
1701
- await new Promise((r) => setTimeout(r, 500));
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,