@spark-apps/quickpeek 1.1.3 → 1.2.0

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -182,6 +182,10 @@ Edit `demo/plan.json` to customize your demo. Each step uses one of these action
182
182
  | `demo/demo.mp4` | Final video |
183
183
  | `demo/plan.json` | Editable script (re-run to apply changes) |
184
184
 
185
+ > `plan.json` is also accepted by [VidLet](https://vidlet.app)'s `create_project`
186
+ > MCP tool, which turns the steps into a `.vidlet` timeline project for further
187
+ > editing and narration.
188
+
185
189
  ## 🌱 Support & Contributions
186
190
 
187
191
  ⭐ **Star the repo** & I power up like Mario 🍄
package/dist/assets/qp.wm CHANGED
File without changes
File without changes
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import { U as UserTier, C as Config } from './voices-BtYZey4Y.mjs';
2
- export { A as AIError, a as AIResponse, b as AIResult, c as CONFIG_FILE, d as CaptionStyle, e as CaptionWordStyle, D as DEFAULT_CAPTIONS, f as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, g as ElementInfo, h as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, L as LANGUAGE_VOICES, P as Plan, R as RateLimitInfo, S as SparkStatus, i as SparkSubscription, j as SparkTrial, k as Step, l as SuggestedAction, T as Tier, V as VERSION, m as buildSystemPrompt, n as buildUserPrompt, o as callAI, p as crawlPage, q as getErrorMessage, r as getLanguageName, s as getStatus, t as getStatusCached, u as getTierByEmail, v as hasAccess, w as hasTrialRemaining, x as hexToASS, y as isCanceling, z as isExcludedLink, B as isPaid, G as isVerified, J as normalizeUrl, K as parseAIPlanResponse, M as parsePartial, N as pricingUrl, O as rgbaToHex, Q as shouldSkipLink, W as showPaywall, X as toTitleCase, Y as windowsPathToWSL } from './voices-BtYZey4Y.mjs';
1
+ import { U as UserTier, C as Config } from './scraping-BK8pIJhm.mjs';
2
+ export { A as AIError, a as AIResponse, b as AIResult, c as CONFIG_FILE, d as CaptionStyle, e as CaptionWordStyle, D as DEFAULT_CAPTIONS, f as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, g as ElementInfo, h as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, P as Plan, R as RateLimitInfo, S as SparkStatus, i as SparkSubscription, j as SparkTrial, k as Step, l as SuggestedAction, T as Tier, V as VERSION, m as buildSystemPrompt, n as buildUserPrompt, o as callAI, p as callAIViaRelay, q as crawlPage, r as getErrorMessage, s as getLanguageName, t as getStatus, u as getStatusCached, v as getTierByEmail, w as hasAccess, x as hasTrialRemaining, y as isCanceling, z as isExcludedLink, B as isPaid, G as isVerified, J as normalizeUrl, K as parseAIPlanResponse, L as parsePartial, M as pricingUrl, N as rgbaToHex, O as shouldSkipLink, Q as showPaywall, W as toTitleCase } from './scraping-BK8pIJhm.mjs';
3
+ export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
3
4
 
4
5
  /**
5
6
  * Billing utilities — watermark decisions and upgrade URL generation
@@ -171,9 +172,6 @@ declare function buildStepTransitions(steps: {
171
172
  */
172
173
  declare function applyTransitions(opts: ApplyTransitionsOptions): Promise<void>;
173
174
 
174
- /**
175
- * CSS and ASS styling for QuickPeek
176
- */
177
175
  declare const CURSOR_CSS = "\n #qp-cursor {\n position: fixed;\n width: 32px;\n height: 38px;\n background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsBAMAAAB01OGNAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAtUExURUdwTAAAAAAAAAAAAAAAAAAAAAAAAB0dHVZWVtvb2/X19f///6mpqXV1df////vm1ewAAAAGdFJOUwAltTfpcLO208EAAAABYktHRA5vvTBPAAAAB3RJTUUH6QwFFREKplBWTAAABqdJREFUeNq93c+OFFUUx/EeCHud6H7A4HpCOu6NiWsWZh5Ax8YFbAdNWBPxBcCo+yGjD+CAW1ywh0Rexu6uulX3Vp269/c7f7o3Bpjhk6+H6TrU0LdXR6sDPI4/OgCyvn0I5NsDpKw3B0hZbw6Qst4cIGWLxKdskfiUHRKeskPCU/ZIdMoeiU7pkOCUDglO6ZHYlB6JTUlIaEpCQlMGJDJlQCJTRiQwZUQCUzIkLiVD4lJyJCwlR8JSCiQqpUCiUkokKKVEglImSEzKBIlJmSIhKT3yY2hKjzx7GpnSI7+8iExJyN+RKQl5FZkyIJEpAxKZMiKBKSMSmJIhcSkZEpeSI2EpORKWUiBRKQUSlVIiQSklEpQyQWJSJkhMyhQJSZkiISkzJCJlhkSkzJGAlDkSkCIg/ikC4p8iIe4pEuKeIiLeKSLinSIjziky4pyygPimLCC+KUuIa8oS4pqyiHimLCKeKcuIY8oy4phSQfxSKohfSg1xS6khbilVxCulinil1BGnlDrilNJAfFIaiE9KC3FJaSEuKU3EI6WJeKS0EYeUNuKQAiD2FACxpyCIOQVBzCkQYk2BEGsKhhhTMMSYAiK2FBCxpaCIKQVFTCkwYkmBEUsKjhhScMSQQiD6FALRpzCIOoVB1CkUok2hEG0KhyhTOESZQiK6FBLRpbCIKoVFVCk0okmhEU0KjyhSeESRokD4FAXCp2gQOkWD0CkqhE1RIWyKDiFTdAiZokS4FCXCpWgRKkWLUClqhElRI0yKHiFS9AiRYkDwFAOCp1gQOMWCwCkmBE0xIWiKDQFTbAiYYkSwFCOCpVgRKMWKQClmBEkxI0iKHQFS7AiQ4oC0UxyQdooH0kzxQJopLkgrxQVppfggjRQfpJHihNRTnJB6ihdSTfFCqiluSC3FDaml+CGVFD+kkuKILKc4IsspnshiiieymOKKLKW4Iq8u5RRf5FpO8UUWpuKMyCnOiJzijYgp3oiY4o5IKe6IlEIhL38FHr9fzFK47/0On195PL6YpVDIX4CRP1IKhbxGSvLHbQUy/snhUrg/XW9IpE/hkOcs0qVwyD8s0qWQ349nJ9+lcAg9+S6FfFqhJ79PIZFLGtmlkMifPLJNYf/5wgWvfAYg11f5j951H39+B3/cBZA/3uY/+nccJ/5oItdPi19Lkz91RV5snuQ/fN0jJ57I9svvkTT5rzyR7RX7wW/C5L93RPbPI8Xknysmj7zWQZz8fTeke0J8aJ088vqTR1czl5t8Fel/w2Ly12/6oXghaRn8IEz+nJh8DRkuUc/yn02XYGLy0Ou0ysn3X44+/75rvNb+IE3+OxdkXM/Lr3l+8stIvjSIkz91QMaQzeYn2+QXkWL7KSafnogd/jlcHiJPHn8iXkLKNe7BW9Pk26/73T/+y3/tkp38AjLdR4vJp+XrxIhMQuRL8Jc2ZLZYF5OnL8Gt18ff6f9bTJ5dvkQku6vw6Ub4AHb5ap1ZcPS5MHn2Eiwhxe2Rs8rk0Utw8xyJ/gNMy5eAlPd5bkiTJ5ev5tkeN7+uTP6+FpncsEqTt6zd7fNWpMmTy9cMmd15+0SYPLl8tc/AuVWZPLh8TZH5LURx8unOFzb59rlEafKG5WuCSPdC0+SvhMljyxdwVtSx9DVPTb5ExPvTafIfpMmf8oh4pz1NXr92F8jCtz+kyVPLF3Km2jfdz+mXrxxZCLFPHjnnTpw8s3xlyFLIMHn18gWdPVibPLJ8jchiyPBB5eTf4ZOHzoNMky+eiInlCzrZMk1eWruR5Qs6o/NImjxxCcZOGz2rTB64BGPnpkqTJ5Yv7ARY4/LVIz9XQ4bJK5cv7FRe49oNni/cT165fIEnJdfW7vYTMXjm8w3pax6ePHh6tW35As/hltfui8ZniUglvDb55vKFno0urd3wJRg95b26dp8SSO3/rTh5dPlCT94XJ48uX/B7CKTJa5Yv+N0QLJOH39fBsnavwRBx7X75vv/kExBpftn2k38oGOAraYBnuS+6DxwvwaPRmvwaDZmt3ZkBvlwHuFBPJp8breVrjYZM1u7SaEx+jYaUk58Y58gLj5i/lO0mPzE2d4GZYPetxrWbNDoEu68wrN2ssUfAG3Bp7X7CGnsE/R7F2UZ6tI0dwt5Dpo3dJ8LfbDlWGivm3dpuKY0V875z6RLMGtw76J3pDO69ANc6Y3WPCBn+wkUaq48JY3giJg3ukU8+ysgnH2ek5SvUGCYfaaTJhxr95GONbvLRxm7y4cb2iTjeWN08gLFaaYz/AWAIeJtzdhugAAAAAElFTkSuQmCC\") no-repeat;\n background-size: contain;\n pointer-events: none;\n z-index: 2147483647;\n transform: translate(-3px, -3px);\n transition: left 800ms ease-out, top 800ms ease-out, transform 0.08s ease-out;\n filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));\n }\n #qp-cursor.clicking {\n transform: translate(-3px, -3px) scale(0.9);\n }\n";
178
176
  /**
179
177
  * JavaScript to inject for cursor tracking
package/dist/index.mjs CHANGED
@@ -9,18 +9,12 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/lib/utils.ts
12
+ import { hexToAss } from "@spark-apps/video-kit";
13
+ import { windowsDrivePathToWsl } from "@spark-apps/video-kit";
12
14
  function getLanguageName(lang) {
13
15
  if (!lang || lang === "en") return null;
14
16
  return LANGUAGE_NAMES[lang] || lang.toUpperCase();
15
17
  }
16
- function hexToASS(hex, alpha = 0) {
17
- const clean = hex.replace("#", "");
18
- const r = clean.slice(0, 2);
19
- const g = clean.slice(2, 4);
20
- const b = clean.slice(4, 6);
21
- const a = alpha.toString(16).padStart(2, "0").toUpperCase();
22
- return `&H${a}${b}${g}${r}`.toUpperCase();
23
- }
24
18
  function rgbaToHex(color) {
25
19
  if (color.startsWith("#")) return color;
26
20
  const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
@@ -30,12 +24,6 @@ function rgbaToHex(color) {
30
24
  const b = parseInt(match[3], 10).toString(16).padStart(2, "0");
31
25
  return `#${r}${g}${b}`.toUpperCase();
32
26
  }
33
- function windowsPathToWSL(path) {
34
- const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
35
- if (!driveMatch) return null;
36
- const drive = driveMatch[1].toLowerCase();
37
- return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
38
- }
39
27
  function parsePartial(partial) {
40
28
  if (!partial) return {};
41
29
  const parts = partial.split("-");
@@ -147,10 +135,10 @@ var init_watermark = __esm({
147
135
  import { spawn } from "child_process";
148
136
  function toWSLPath(path) {
149
137
  if (!isWindowsRuntime) return path;
150
- return windowsPathToWSL(path) ?? path.replace(/\\/g, "/");
138
+ return windowsDrivePathToWsl(path) ?? path.replace(/\\/g, "/");
151
139
  }
152
140
  function convertArgToWSL(arg) {
153
- const wslPath = windowsPathToWSL(arg);
141
+ const wslPath = windowsDrivePathToWsl(arg);
154
142
  if (wslPath) return wslPath;
155
143
  if (arg.includes("\\")) return arg.replace(/\\/g, "/");
156
144
  return arg;
@@ -186,6 +174,7 @@ var init_spawn = __esm({
186
174
  });
187
175
 
188
176
  // src/lib/ffmpeg/compose.ts
177
+ import { MASTER_CHAIN } from "@spark-apps/video-kit";
189
178
  function composeVideo(opts) {
190
179
  return new Promise((resolve3, reject) => {
191
180
  const {
@@ -246,20 +235,24 @@ function composeVideo(opts) {
246
235
  `[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
247
236
  // Music: fade in and fade out
248
237
  `[2:a]aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut}[music]`,
249
- `[voice][music]amix=inputs=2:duration=longest[aout]`
238
+ // Normalise the MIX and hard-limit its true peak - ported from
239
+ // VidLet, where single-pass loudnorm alone measured +0.9 dBTP on
240
+ // sharp TTS consonants. -14 LUFS is the YouTube/IG/TikTok playback
241
+ // target, so the platform leaves the audio untouched.
242
+ `[voice][music]amix=inputs=2:duration=longest,${MASTER_CHAIN}[aout]`
250
243
  ].join(";");
251
244
  args.push("-vf", vfStr);
252
245
  args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
253
246
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
254
247
  args.push("-c:a", "aac", "-b:a", audioBitrate);
255
248
  } else if (hasVoiceover) {
256
- const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}`;
249
+ const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn},${MASTER_CHAIN}`;
257
250
  args.push("-vf", vfStr);
258
251
  args.push("-map", "0:v", "-map", "1:a", "-af", af);
259
252
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
260
253
  args.push("-c:a", "aac", "-b:a", audioBitrate);
261
254
  } else if (hasMusic) {
262
- const musicFilter = `aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut}`;
255
+ const musicFilter = `aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut},${MASTER_CHAIN}`;
263
256
  args.push("-vf", vfStr);
264
257
  args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
265
258
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
@@ -531,14 +524,13 @@ function applyTransitions(opts) {
531
524
  filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
532
525
  }
533
526
  let currentV = "v0";
527
+ let chainDuration = boundaries[1] - boundaries[0];
534
528
  for (let i = 0; i < sorted.length; i++) {
535
529
  const trans = sorted[i];
536
530
  const duration = trans.duration ?? 0.3;
537
531
  const nextIdx = i + 1;
538
- const segStart = boundaries[i];
539
- const segEnd = boundaries[i + 1];
540
- const segDuration = segEnd - segStart;
541
- const offset = Math.max(0, segDuration - duration);
532
+ const offset = Math.max(0, chainDuration - duration);
533
+ chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
542
534
  const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
543
535
  const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
544
536
  filterParts.push(
@@ -610,7 +602,7 @@ import { existsSync } from "fs";
610
602
  import { readFile, unlink, writeFile } from "fs/promises";
611
603
  import { isAbsolute, join, resolve } from "path";
612
604
  function toWSLPathForTTS(path) {
613
- return windowsPathToWSL(path) ?? path;
605
+ return windowsDrivePathToWsl(path) ?? path;
614
606
  }
615
607
  function estimateActionDuration(step) {
616
608
  if (step.action === "drag" && step.value === "around") {
@@ -677,7 +669,7 @@ async function saveVoiceoverMeta(outputDir, meta) {
677
669
  async function concatenateAudio(inputs, output, workDir) {
678
670
  const formatPath = (f) => {
679
671
  const resolved = resolve(f);
680
- return isWindows ? windowsPathToWSL(resolved) ?? resolved : resolved;
672
+ return isWindows ? windowsDrivePathToWsl(resolved) ?? resolved : resolved;
681
673
  };
682
674
  const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
683
675
  const listPath = join(workDir, "concat.txt");
@@ -786,23 +778,30 @@ init_utils();
786
778
  var SPARK_AI_BASE = "https://sparkbrain.app";
787
779
  var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
788
780
  var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
781
+ var RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
782
+ var RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
783
+ var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
789
784
  var API_KEY = process.env.SPARK_AI_KEY || "";
790
- var cachedToken = null;
791
- async function getOrRefreshToken() {
785
+ var cachedDirectToken = null;
786
+ var cachedRelayToken = null;
787
+ async function getOrRefreshDirectToken() {
792
788
  const now = Date.now();
793
- if (cachedToken && cachedToken.expiresAt > now + 3e5) {
794
- return cachedToken.token;
789
+ if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
790
+ return cachedDirectToken.token;
795
791
  }
796
792
  try {
797
793
  const response = await fetch(SPARK_AI_TOKEN, {
798
794
  method: "POST",
799
795
  headers: { "X-Api-Key": API_KEY }
800
796
  });
797
+ if (!response.ok) {
798
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
799
+ }
801
800
  const data = await response.json();
802
- if (!response.ok || !data.success || !data.token) {
803
- throw new Error(data.error || `Token exchange failed: ${response.status}`);
801
+ if (!data.success || !data.token) {
802
+ throw new Error(data.error || `Token exchange failed: unexpected response`);
804
803
  }
805
- cachedToken = {
804
+ cachedDirectToken = {
806
805
  token: data.token,
807
806
  expiresAt: data.expiresAt || now + 36e5
808
807
  };
@@ -811,77 +810,130 @@ async function getOrRefreshToken() {
811
810
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
812
811
  }
813
812
  }
814
- async function callAI(systemPrompt, userPrompt, tier = "free") {
815
- if (!API_KEY) {
816
- return {
817
- success: false,
818
- error: "AI service is not configured. Please report this issue.",
819
- isAuthError: true
820
- };
813
+ async function getOrRefreshRelayToken(email) {
814
+ const now = Date.now();
815
+ if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
816
+ return cachedRelayToken.token;
821
817
  }
822
818
  try {
823
- const token = await getOrRefreshToken();
824
- let lastRateLimitError = null;
825
- for (let attempt = 1; attempt <= 3; attempt++) {
826
- try {
827
- const response = await fetch(SPARK_AI_CHAT, {
828
- method: "POST",
829
- headers: {
830
- "Content-Type": "application/json",
831
- Authorization: `Bearer ${token}`
832
- },
833
- body: JSON.stringify({
834
- messages: [
835
- { role: "system", content: systemPrompt },
836
- { role: "user", content: userPrompt }
837
- ]
838
- })
839
- });
840
- const data = await response.json();
841
- if (!response.ok) {
842
- const error = data.error || `Spark AI error: ${response.status}`;
843
- if (response.status === 429 && attempt < 3) {
844
- const retryAfter = response.headers.get("Retry-After");
845
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt - 1);
846
- lastRateLimitError = {
847
- success: false,
848
- error,
849
- isRateLimit: true,
850
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
851
- };
852
- await new Promise((resolve3) => setTimeout(resolve3, Math.min(delayMs, 3e4)));
853
- continue;
854
- }
855
- return {
819
+ const response = await fetch(RELAY_TOKEN_URL, {
820
+ method: "POST",
821
+ headers: { "Content-Type": "application/json" },
822
+ body: JSON.stringify({ email })
823
+ });
824
+ const data = await response.json().catch(() => ({}));
825
+ if (!response.ok || !data.success || !data.token) {
826
+ throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
827
+ }
828
+ cachedRelayToken = {
829
+ token: data.token,
830
+ expiresAt: data.expiresAt || now + 36e5,
831
+ email
832
+ };
833
+ return data.token;
834
+ } catch (err) {
835
+ throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
836
+ }
837
+ }
838
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
839
+ let lastRateLimitError = null;
840
+ for (let attempt = 1; attempt <= 3; attempt++) {
841
+ try {
842
+ const response = await fetch(chatUrl, {
843
+ method: "POST",
844
+ headers: {
845
+ "Content-Type": "application/json",
846
+ Authorization: `Bearer ${bearerToken}`
847
+ },
848
+ body: JSON.stringify({
849
+ messages: [
850
+ { role: "system", content: systemPrompt },
851
+ { role: "user", content: userPrompt }
852
+ ]
853
+ })
854
+ });
855
+ let data = {};
856
+ const contentType = response.headers.get("content-type") ?? "";
857
+ if (contentType.includes("application/json")) {
858
+ data = await response.json();
859
+ }
860
+ if (!response.ok) {
861
+ const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
862
+ if (response.status === 429 && attempt < 3) {
863
+ const retryAfter = response.headers.get("Retry-After");
864
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
865
+ lastRateLimitError = {
856
866
  success: false,
857
867
  error,
858
- isRateLimit: response.status === 429,
859
- isAuthError: response.status === 401 || response.status === 403
860
- };
861
- }
862
- if (!data.success || !data.response) {
863
- return {
864
- success: false,
865
- error: data.error || "No response from Spark AI"
868
+ isRateLimit: true,
869
+ ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
866
870
  };
871
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(delayMs, 3e4)));
872
+ continue;
867
873
  }
868
874
  return {
869
- success: true,
870
- content: data.response,
871
- limits: {
872
- used: data.limits?.used ?? 0,
873
- limit: data.limits?.limit ?? 100,
874
- tier,
875
- ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
876
- }
875
+ success: false,
876
+ error,
877
+ isRateLimit: response.status === 429,
878
+ isAuthError: response.status === 401 || response.status === 403
877
879
  };
878
- } catch (err) {
879
- if (attempt === 3) throw err;
880
- const delayMs = 1e3 * Math.pow(2, attempt - 1);
881
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
882
880
  }
881
+ if (!data.success || !data.response) {
882
+ return {
883
+ success: false,
884
+ error: data.error || "No response from Spark AI"
885
+ };
886
+ }
887
+ return {
888
+ success: true,
889
+ content: data.response,
890
+ limits: {
891
+ used: data.limits?.used ?? 0,
892
+ limit: data.limits?.limit ?? 100,
893
+ tier,
894
+ ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
895
+ }
896
+ };
897
+ } catch (err) {
898
+ if (attempt === 3) throw err;
899
+ const delayMs = 1e3 * 2 ** (attempt - 1);
900
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
901
+ }
902
+ }
903
+ return lastRateLimitError || { success: false, error: "Request failed after retries" };
904
+ }
905
+ async function callAI(systemPrompt, userPrompt, tier = "free") {
906
+ if (!API_KEY) {
907
+ return {
908
+ success: false,
909
+ error: "AI service is not configured. Please report this issue.",
910
+ isAuthError: true
911
+ };
912
+ }
913
+ try {
914
+ const token = await getOrRefreshDirectToken();
915
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
916
+ } catch (err) {
917
+ return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
918
+ }
919
+ }
920
+ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
921
+ if (!email) {
922
+ return {
923
+ success: false,
924
+ error: "Sign in required. Run qp login to connect your account.",
925
+ isAuthError: true
926
+ };
927
+ }
928
+ try {
929
+ let token = await getOrRefreshRelayToken(email);
930
+ let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
931
+ if (!result.success && result.isAuthError) {
932
+ cachedRelayToken = null;
933
+ token = await getOrRefreshRelayToken(email);
934
+ result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
883
935
  }
884
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
936
+ return result;
885
937
  } catch (err) {
886
938
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
887
939
  }
@@ -916,26 +968,32 @@ function normalizeUrl(input) {
916
968
  var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
917
969
  var BASE = "https://sparkstripe.com";
918
970
  var TIERS = {
919
- "price_1TADnPEf9opkuWid9NtqOYON": "pro",
920
- "price_1TADnPEf9opkuWidgbvvX9ma": "pro",
921
- "price_1TADm7Ef9opkuWid3I9tquIB": "pro",
922
- "price_1TADm8Ef9opkuWid1RSA2wCa": "pro",
923
- "price_1TCREmEf9opkuWidvhuqzqDM": "pro",
924
- "price_1TCREmEf9opkuWidacf1tiC7": "pro"
971
+ price_1TADnPEf9opkuWid9NtqOYON: "pro",
972
+ price_1TADnPEf9opkuWidgbvvX9ma: "pro",
973
+ price_1TADm7Ef9opkuWid3I9tquIB: "pro",
974
+ price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
975
+ price_1TCREmEf9opkuWidvhuqzqDM: "pro",
976
+ price_1TCREmEf9opkuWidacf1tiC7: "pro"
925
977
  };
926
978
  var RANK = {
927
- "free": 0,
928
- "pro": 1
979
+ free: 0,
980
+ pro: 1
981
+ };
982
+ var FEATURES = {
983
+ free: [],
984
+ pro: [
985
+ "Everything in Free",
986
+ "No watermark",
987
+ "Full video composition (voiceover + captions + music + transitions)",
988
+ "Unlimited videos",
989
+ "Priority support"
990
+ ]
929
991
  };
930
- var FEATURES = { "free": [], "pro": [
931
- "Everything in Free",
932
- "No watermark",
933
- "Full video composition (voiceover + captions + music + transitions)",
934
- "Unlimited videos",
935
- "Priority support"
936
- ] };
937
992
  async function _fetch(email, init) {
938
- const r = await fetch(`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`, init);
993
+ const r = await fetch(
994
+ `${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
995
+ init
996
+ );
939
997
  if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
940
998
  return r.json();
941
999
  }
@@ -1152,11 +1210,20 @@ function getActionForType(type) {
1152
1210
  }
1153
1211
  var noopTask = () => ({ succeed: () => {
1154
1212
  } });
1155
- async function crawlPage(url, task = noopTask) {
1213
+ var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
1214
+ var CRAWL_CACHE_MAX_ENTRIES = 50;
1215
+ var crawlCache = /* @__PURE__ */ new Map();
1216
+ async function crawlPage(url, task = noopTask, storageStatePath) {
1217
+ const cached = crawlCache.get(url);
1218
+ if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
1219
+ task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
1220
+ return { title: cached.title, elements: [...cached.elements] };
1221
+ }
1156
1222
  const browserTask = task("Launching browser");
1157
1223
  const browser = await chromium.launch({ headless: true });
1158
- const page = await browser.newPage();
1159
- browserTask.succeed("Browser launched");
1224
+ const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
1225
+ const page = await context.newPage();
1226
+ browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
1160
1227
  const crawlTask = task("Crawling page");
1161
1228
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
1162
1229
  const title = await page.title();
@@ -1289,6 +1356,11 @@ async function crawlPage(url, task = noopTask) {
1289
1356
  });
1290
1357
  await browser.close();
1291
1358
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
1359
+ crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
1360
+ if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
1361
+ const oldest = crawlCache.keys().next().value;
1362
+ if (oldest !== void 0) crawlCache.delete(oldest);
1363
+ }
1292
1364
  return { title, elements };
1293
1365
  }
1294
1366
 
@@ -1426,6 +1498,7 @@ TARGET SYNTAX:
1426
1498
  }
1427
1499
 
1428
1500
  // src/lib/styles.ts
1501
+ import { buildAssHeader } from "@spark-apps/video-kit";
1429
1502
  var CURSOR_MOVE_DURATION = 800;
1430
1503
  var CURSOR_CSS = `
1431
1504
  #qp-cursor {
@@ -1465,19 +1538,14 @@ var CURSOR_SCRIPT = `
1465
1538
  })();
1466
1539
  `;
1467
1540
  function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
1468
- return `[Script Info]
1469
- Title: QuickPeek Demo
1470
- ScriptType: v4.00+
1471
- PlayResX: ${width}
1472
- PlayResY: ${height}
1473
-
1474
- [V4+ Styles]
1475
- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1476
- Style: Karaoke,${font},${size},${primaryColor},${secondaryColor},${outlineColor},${backColor},${bold ? 1 : 0},0,0,0,100,100,0,0,1,${outlineWidth},${shadow},2,40,40,${marginBottom},1
1477
-
1478
- [Events]
1479
- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
1480
- `;
1541
+ return buildAssHeader({
1542
+ title: "QuickPeek Demo",
1543
+ width,
1544
+ height,
1545
+ styles: [
1546
+ `Style: Karaoke,${font},${size},${primaryColor},${secondaryColor},${outlineColor},${backColor},${bold ? 1 : 0},0,0,0,100,100,0,0,1,${outlineWidth},${shadow},2,40,40,${marginBottom},1`
1547
+ ]
1548
+ });
1481
1549
  }
1482
1550
 
1483
1551
  // src/lib/subscription-client.ts
@@ -1560,75 +1628,7 @@ import { mkdir } from "fs/promises";
1560
1628
  import { join as join5 } from "path";
1561
1629
 
1562
1630
  // src/lib/voices.ts
1563
- var LANGUAGE_VOICES = {
1564
- en: "en-GB-SoniaNeural",
1565
- ar: "ar-LB-LaylaNeural",
1566
- es: "es-ES-ElviraNeural",
1567
- fr: "fr-FR-DeniseNeural",
1568
- de: "de-DE-KatjaNeural",
1569
- it: "it-IT-ElsaNeural",
1570
- pt: "pt-BR-FranciscaNeural",
1571
- ru: "ru-RU-SvetlanaNeural",
1572
- zh: "zh-CN-XiaoxiaoNeural",
1573
- ja: "ja-JP-NanamiNeural",
1574
- ko: "ko-KR-SunHiNeural",
1575
- hi: "hi-IN-SwaraNeural",
1576
- tr: "tr-TR-EmelNeural",
1577
- nl: "nl-NL-ColetteNeural",
1578
- pl: "pl-PL-AgnieszkaNeural",
1579
- sv: "sv-SE-SofieNeural",
1580
- he: "he-IL-HilaNeural",
1581
- th: "th-TH-PremwadeeNeural",
1582
- vi: "vi-VN-HoaiMyNeural",
1583
- id: "id-ID-GadisNeural",
1584
- ms: "ms-MY-YasminNeural",
1585
- fil: "fil-PH-BlessicaNeural",
1586
- uk: "uk-UA-PolinaNeural",
1587
- cs: "cs-CZ-VlastaNeural",
1588
- el: "el-GR-AthinaNeural",
1589
- ro: "ro-RO-AlinaNeural",
1590
- hu: "hu-HU-NoemiNeural",
1591
- da: "da-DK-ChristelNeural",
1592
- fi: "fi-FI-NooraNeural",
1593
- no: "nb-NO-PernilleNeural",
1594
- sk: "sk-SK-ViktoriaNeural",
1595
- bg: "bg-BG-KalinaNeural",
1596
- hr: "hr-HR-GabrijelaNeural",
1597
- sl: "sl-SI-PetraNeural",
1598
- et: "et-EE-AnuNeural",
1599
- lv: "lv-LV-EveritaNeural",
1600
- lt: "lt-LT-OnaNeural",
1601
- bn: "bn-IN-TanishaaNeural",
1602
- ta: "ta-IN-PallaviNeural",
1603
- te: "te-IN-ShrutiNeural",
1604
- mr: "mr-IN-AarohiNeural",
1605
- gu: "gu-IN-DhwaniNeural",
1606
- kn: "kn-IN-SapnaNeural",
1607
- ml: "ml-IN-SobhanaNeural",
1608
- ur: "ur-PK-UzmaNeural",
1609
- fa: "fa-IR-DilaraNeural",
1610
- sw: "sw-KE-ZuriNeural",
1611
- af: "af-ZA-AdriNeural",
1612
- ca: "ca-ES-JoanaNeural",
1613
- cy: "cy-GB-NiaNeural",
1614
- ga: "ga-IE-OrlaNeural",
1615
- mt: "mt-MT-GraceNeural",
1616
- is: "is-IS-GudrunNeural",
1617
- mk: "mk-MK-MarijaNeural",
1618
- sr: "sr-RS-SophieNeural",
1619
- bs: "bs-BA-VesnaNeural",
1620
- sq: "sq-AL-AnilaNeural",
1621
- am: "am-ET-MekdesNeural",
1622
- az: "az-AZ-BabekNeural",
1623
- ka: "ka-GE-EkaNeural",
1624
- kk: "kk-KZ-AigulNeural",
1625
- mn: "mn-MN-YesUINeural",
1626
- ne: "ne-NP-HemkalaNeural",
1627
- ps: "ps-AF-LatifaNeural",
1628
- si: "si-LK-ThiliniNeural",
1629
- so: "so-SO-UbaxNeural",
1630
- zu: "zu-ZA-ThandoNeural"
1631
- };
1631
+ import { WIDE_VOICES } from "@spark-apps/video-kit";
1632
1632
 
1633
1633
  // src/lib/tts/generate.ts
1634
1634
  init_helpers();
@@ -1695,7 +1695,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
1695
1695
  }
1696
1696
  function resolveVoice(config) {
1697
1697
  if (config.audio?.voice) return config.audio.voice;
1698
- if (config.lang) return LANGUAGE_VOICES[config.lang] || null;
1698
+ if (config.lang) return WIDE_VOICES[config.lang] || null;
1699
1699
  return null;
1700
1700
  }
1701
1701
  async function generateVoiceover(steps, config, workDir, outputDir, logError, task, info) {
@@ -1803,7 +1803,7 @@ export {
1803
1803
  EXCLUDED_LINK_PATTERNS,
1804
1804
  FEATURES,
1805
1805
  INTERACTIVE_SELECTORS,
1806
- LANGUAGE_VOICES,
1806
+ WIDE_VOICES as LANGUAGE_VOICES,
1807
1807
  VERSION,
1808
1808
  alignVoiceoverToRecording,
1809
1809
  applyTransitions,
@@ -1813,6 +1813,7 @@ export {
1813
1813
  buildUserPrompt,
1814
1814
  buildWatermarkFilters,
1815
1815
  callAI,
1816
+ callAIViaRelay,
1816
1817
  canReuseVoiceover,
1817
1818
  composeVideo,
1818
1819
  concatMedia,
@@ -1831,7 +1832,7 @@ export {
1831
1832
  getUpgradeUrl,
1832
1833
  hasAccess,
1833
1834
  hasTrialRemaining,
1834
- hexToASS,
1835
+ hexToAss as hexToASS,
1835
1836
  isCanceling,
1836
1837
  isExcludedLink,
1837
1838
  isPaid,
@@ -1853,5 +1854,5 @@ export {
1853
1854
  spliceVideos,
1854
1855
  synthesizeSpeech,
1855
1856
  toTitleCase,
1856
- windowsPathToWSL
1857
+ windowsDrivePathToWsl as windowsPathToWSL
1857
1858
  };