@spark-apps/quickpeek 1.1.3 → 1.2.1
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 +0 -0
- package/README.md +4 -0
- package/dist/assets/qp.wm +0 -0
- package/dist/highlight.css +0 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +224 -206
- package/dist/mcp-tools.mjs +6206 -0
- package/dist/qp.js +1660 -1273
- package/dist/{voices-BtYZey4Y.d.mts → scraping-CHN4ClyT.d.mts} +35 -28
- package/dist/web.d.mts +43 -2
- package/dist/web.mjs +234 -184
- package/mcp.mjs +98 -0
- package/package.json +80 -76
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("-");
|
|
@@ -51,7 +39,7 @@ function getErrorMessage(err) {
|
|
|
51
39
|
return err instanceof Error ? err.message : String(err);
|
|
52
40
|
}
|
|
53
41
|
function toTitleCase(text) {
|
|
54
|
-
return text.replace(/\w
|
|
42
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
55
43
|
if (index === 0) {
|
|
56
44
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
57
45
|
}
|
|
@@ -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
|
|
138
|
+
return windowsDrivePathToWsl(path) ?? path.replace(/\\/g, "/");
|
|
151
139
|
}
|
|
152
140
|
function convertArgToWSL(arg) {
|
|
153
|
-
const wslPath =
|
|
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
|
-
|
|
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
|
|
539
|
-
|
|
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
|
|
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 ?
|
|
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,32 @@ 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`;
|
|
789
|
-
var
|
|
790
|
-
var
|
|
791
|
-
|
|
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`;
|
|
784
|
+
function apiKey() {
|
|
785
|
+
return process.env.SPARK_AI_KEY || "";
|
|
786
|
+
}
|
|
787
|
+
var cachedDirectToken = null;
|
|
788
|
+
var cachedRelayToken = null;
|
|
789
|
+
async function getOrRefreshDirectToken() {
|
|
792
790
|
const now = Date.now();
|
|
793
|
-
if (
|
|
794
|
-
return
|
|
791
|
+
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
792
|
+
return cachedDirectToken.token;
|
|
795
793
|
}
|
|
796
794
|
try {
|
|
797
795
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
798
796
|
method: "POST",
|
|
799
|
-
headers: { "X-Api-Key":
|
|
797
|
+
headers: { "X-Api-Key": apiKey() }
|
|
800
798
|
});
|
|
799
|
+
if (!response.ok) {
|
|
800
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
801
|
+
}
|
|
801
802
|
const data = await response.json();
|
|
802
|
-
if (!
|
|
803
|
-
throw new Error(data.error || `Token exchange failed:
|
|
803
|
+
if (!data.success || !data.token) {
|
|
804
|
+
throw new Error(data.error || `Token exchange failed: unexpected response`);
|
|
804
805
|
}
|
|
805
|
-
|
|
806
|
+
cachedDirectToken = {
|
|
806
807
|
token: data.token,
|
|
807
808
|
expiresAt: data.expiresAt || now + 36e5
|
|
808
809
|
};
|
|
@@ -811,77 +812,130 @@ async function getOrRefreshToken() {
|
|
|
811
812
|
throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
|
|
812
813
|
}
|
|
813
814
|
}
|
|
814
|
-
async function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
error: "AI service is not configured. Please report this issue.",
|
|
819
|
-
isAuthError: true
|
|
820
|
-
};
|
|
815
|
+
async function getOrRefreshRelayToken(email) {
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
|
|
818
|
+
return cachedRelayToken.token;
|
|
821
819
|
}
|
|
822
820
|
try {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
821
|
+
const response = await fetch(RELAY_TOKEN_URL, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
headers: { "Content-Type": "application/json" },
|
|
824
|
+
body: JSON.stringify({ email })
|
|
825
|
+
});
|
|
826
|
+
const data = await response.json().catch(() => ({}));
|
|
827
|
+
if (!response.ok || !data.success || !data.token) {
|
|
828
|
+
throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
|
|
829
|
+
}
|
|
830
|
+
cachedRelayToken = {
|
|
831
|
+
token: data.token,
|
|
832
|
+
expiresAt: data.expiresAt || now + 36e5,
|
|
833
|
+
email
|
|
834
|
+
};
|
|
835
|
+
return data.token;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
841
|
+
let lastRateLimitError = null;
|
|
842
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
843
|
+
try {
|
|
844
|
+
const response = await fetch(chatUrl, {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: {
|
|
847
|
+
"Content-Type": "application/json",
|
|
848
|
+
Authorization: `Bearer ${bearerToken}`
|
|
849
|
+
},
|
|
850
|
+
body: JSON.stringify({
|
|
851
|
+
messages: [
|
|
852
|
+
{ role: "system", content: systemPrompt },
|
|
853
|
+
{ role: "user", content: userPrompt }
|
|
854
|
+
]
|
|
855
|
+
})
|
|
856
|
+
});
|
|
857
|
+
let data = {};
|
|
858
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
859
|
+
if (contentType.includes("application/json")) {
|
|
860
|
+
data = await response.json();
|
|
861
|
+
}
|
|
862
|
+
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 = {
|
|
856
868
|
success: false,
|
|
857
869
|
error,
|
|
858
|
-
isRateLimit:
|
|
859
|
-
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
if (!data.success || !data.response) {
|
|
863
|
-
return {
|
|
864
|
-
success: false,
|
|
865
|
-
error: data.error || "No response from Spark AI"
|
|
870
|
+
isRateLimit: true,
|
|
871
|
+
...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
|
|
866
872
|
};
|
|
873
|
+
await new Promise((resolve3) => setTimeout(resolve3, Math.min(delayMs, 3e4)));
|
|
874
|
+
continue;
|
|
867
875
|
}
|
|
868
876
|
return {
|
|
869
|
-
success:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
+
success: false,
|
|
878
|
+
error,
|
|
879
|
+
isRateLimit: response.status === 429,
|
|
880
|
+
isAuthError: response.status === 401 || response.status === 403
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
if (!data.success || !data.response) {
|
|
884
|
+
return {
|
|
885
|
+
success: false,
|
|
886
|
+
error: data.error || "No response from Spark AI"
|
|
877
887
|
};
|
|
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
888
|
}
|
|
889
|
+
return {
|
|
890
|
+
success: true,
|
|
891
|
+
content: data.response,
|
|
892
|
+
limits: {
|
|
893
|
+
used: data.limits?.used ?? 0,
|
|
894
|
+
limit: data.limits?.limit ?? 100,
|
|
895
|
+
tier,
|
|
896
|
+
...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (attempt === 3) throw err;
|
|
901
|
+
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
902
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
883
903
|
}
|
|
884
|
-
|
|
904
|
+
}
|
|
905
|
+
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
906
|
+
}
|
|
907
|
+
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
908
|
+
if (!apiKey()) {
|
|
909
|
+
return {
|
|
910
|
+
success: false,
|
|
911
|
+
error: "AI service is not configured. Please report this issue.",
|
|
912
|
+
isAuthError: true
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const token = await getOrRefreshDirectToken();
|
|
917
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
918
|
+
} catch (err) {
|
|
919
|
+
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
923
|
+
if (!email) {
|
|
924
|
+
return {
|
|
925
|
+
success: false,
|
|
926
|
+
error: "Sign in required. Run qp login to connect your account.",
|
|
927
|
+
isAuthError: true
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
let token = await getOrRefreshRelayToken(email);
|
|
932
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
933
|
+
if (!result.success && result.isAuthError) {
|
|
934
|
+
cachedRelayToken = null;
|
|
935
|
+
token = await getOrRefreshRelayToken(email);
|
|
936
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
937
|
+
}
|
|
938
|
+
return result;
|
|
885
939
|
} catch (err) {
|
|
886
940
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
887
941
|
}
|
|
@@ -916,26 +970,32 @@ function normalizeUrl(input) {
|
|
|
916
970
|
var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
917
971
|
var BASE = "https://sparkstripe.com";
|
|
918
972
|
var TIERS = {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
973
|
+
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
974
|
+
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
975
|
+
price_1TADm7Ef9opkuWid3I9tquIB: "pro",
|
|
976
|
+
price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
|
|
977
|
+
price_1TCREmEf9opkuWidvhuqzqDM: "pro",
|
|
978
|
+
price_1TCREmEf9opkuWidacf1tiC7: "pro"
|
|
925
979
|
};
|
|
926
980
|
var RANK = {
|
|
927
|
-
|
|
928
|
-
|
|
981
|
+
free: 0,
|
|
982
|
+
pro: 1
|
|
983
|
+
};
|
|
984
|
+
var FEATURES = {
|
|
985
|
+
free: [],
|
|
986
|
+
pro: [
|
|
987
|
+
"Everything in Free",
|
|
988
|
+
"No watermark",
|
|
989
|
+
"Full video composition (voiceover + captions + music + transitions)",
|
|
990
|
+
"Unlimited videos",
|
|
991
|
+
"Priority support"
|
|
992
|
+
]
|
|
929
993
|
};
|
|
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
994
|
async function _fetch(email, init) {
|
|
938
|
-
const r = await fetch(
|
|
995
|
+
const r = await fetch(
|
|
996
|
+
`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
|
|
997
|
+
init
|
|
998
|
+
);
|
|
939
999
|
if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
|
|
940
1000
|
return r.json();
|
|
941
1001
|
}
|
|
@@ -988,7 +1048,7 @@ function getUpgradeUrl(userEmail) {
|
|
|
988
1048
|
}
|
|
989
1049
|
|
|
990
1050
|
// src/lib/config.ts
|
|
991
|
-
var VERSION = "1.
|
|
1051
|
+
var VERSION = "1.2.1";
|
|
992
1052
|
var CONFIG_FILE = "quickpeek.config.json";
|
|
993
1053
|
var DEFAULT_CONFIG = {
|
|
994
1054
|
plan: { maxSteps: 8 },
|
|
@@ -1152,14 +1212,32 @@ function getActionForType(type) {
|
|
|
1152
1212
|
}
|
|
1153
1213
|
var noopTask = () => ({ succeed: () => {
|
|
1154
1214
|
} });
|
|
1155
|
-
|
|
1215
|
+
var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1216
|
+
var CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
1217
|
+
var crawlCache = /* @__PURE__ */ new Map();
|
|
1218
|
+
async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
1219
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
1220
|
+
const cached = crawlCache.get(cacheKey);
|
|
1221
|
+
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
1222
|
+
task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
1223
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
1224
|
+
}
|
|
1156
1225
|
const browserTask = task("Launching browser");
|
|
1157
1226
|
const browser = await chromium.launch({ headless: true });
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1227
|
+
const context = await browser.newContext({
|
|
1228
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
1229
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
1230
|
+
// a page nobody will ever see.
|
|
1231
|
+
...viewport ? { viewport } : {}
|
|
1232
|
+
});
|
|
1233
|
+
const page = await context.newPage();
|
|
1234
|
+
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
1160
1235
|
const crawlTask = task("Crawling page");
|
|
1161
1236
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
1162
1237
|
const title = await page.title();
|
|
1238
|
+
const scrollable = await page.evaluate(
|
|
1239
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
1240
|
+
);
|
|
1163
1241
|
const rawElements = await page.evaluate(`
|
|
1164
1242
|
(() => {
|
|
1165
1243
|
const items = [];
|
|
@@ -1289,7 +1367,12 @@ async function crawlPage(url, task = noopTask) {
|
|
|
1289
1367
|
});
|
|
1290
1368
|
await browser.close();
|
|
1291
1369
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
1292
|
-
|
|
1370
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
1371
|
+
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
1372
|
+
const oldest = crawlCache.keys().next().value;
|
|
1373
|
+
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
1374
|
+
}
|
|
1375
|
+
return { title, elements, scrollable };
|
|
1293
1376
|
}
|
|
1294
1377
|
|
|
1295
1378
|
// src/lib/cursor.ts
|
|
@@ -1352,7 +1435,8 @@ EXAMPLES:
|
|
|
1352
1435
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1353
1436
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1354
1437
|
}
|
|
1355
|
-
|
|
1438
|
+
var MIN_USEFUL_SCROLL_PX = 200;
|
|
1439
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1356
1440
|
const lang = langName ? `
|
|
1357
1441
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1358
1442
|
const purpose = description ? `
|
|
@@ -1390,8 +1474,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1390
1474
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1391
1475
|
}
|
|
1392
1476
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1477
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1478
|
+
const scrolling = cannotScroll ? `
|
|
1479
|
+
|
|
1480
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1481
|
+
const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
|
|
1393
1482
|
return `URL: ${url}
|
|
1394
|
-
Title: ${title}${purpose}
|
|
1483
|
+
Title: ${title}${purpose}${scrolling}
|
|
1395
1484
|
|
|
1396
1485
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1397
1486
|
${elementSummary}
|
|
@@ -1401,7 +1490,7 @@ REQUIREMENTS:
|
|
|
1401
1490
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1402
1491
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1403
1492
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1404
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1493
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1405
1494
|
|
|
1406
1495
|
Return JSON:
|
|
1407
1496
|
{
|
|
@@ -1426,6 +1515,7 @@ TARGET SYNTAX:
|
|
|
1426
1515
|
}
|
|
1427
1516
|
|
|
1428
1517
|
// src/lib/styles.ts
|
|
1518
|
+
import { buildAssHeader } from "@spark-apps/video-kit";
|
|
1429
1519
|
var CURSOR_MOVE_DURATION = 800;
|
|
1430
1520
|
var CURSOR_CSS = `
|
|
1431
1521
|
#qp-cursor {
|
|
@@ -1465,19 +1555,14 @@ var CURSOR_SCRIPT = `
|
|
|
1465
1555
|
})();
|
|
1466
1556
|
`;
|
|
1467
1557
|
function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
|
|
1468
|
-
return
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
-
`;
|
|
1558
|
+
return buildAssHeader({
|
|
1559
|
+
title: "QuickPeek Demo",
|
|
1560
|
+
width,
|
|
1561
|
+
height,
|
|
1562
|
+
styles: [
|
|
1563
|
+
`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`
|
|
1564
|
+
]
|
|
1565
|
+
});
|
|
1481
1566
|
}
|
|
1482
1567
|
|
|
1483
1568
|
// src/lib/subscription-client.ts
|
|
@@ -1560,75 +1645,7 @@ import { mkdir } from "fs/promises";
|
|
|
1560
1645
|
import { join as join5 } from "path";
|
|
1561
1646
|
|
|
1562
1647
|
// src/lib/voices.ts
|
|
1563
|
-
|
|
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
|
-
};
|
|
1648
|
+
import { WIDE_VOICES } from "@spark-apps/video-kit";
|
|
1632
1649
|
|
|
1633
1650
|
// src/lib/tts/generate.ts
|
|
1634
1651
|
init_helpers();
|
|
@@ -1695,7 +1712,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
|
1695
1712
|
}
|
|
1696
1713
|
function resolveVoice(config) {
|
|
1697
1714
|
if (config.audio?.voice) return config.audio.voice;
|
|
1698
|
-
if (config.lang) return
|
|
1715
|
+
if (config.lang) return WIDE_VOICES[config.lang] || null;
|
|
1699
1716
|
return null;
|
|
1700
1717
|
}
|
|
1701
1718
|
async function generateVoiceover(steps, config, workDir, outputDir, logError, task, info) {
|
|
@@ -1803,7 +1820,7 @@ export {
|
|
|
1803
1820
|
EXCLUDED_LINK_PATTERNS,
|
|
1804
1821
|
FEATURES,
|
|
1805
1822
|
INTERACTIVE_SELECTORS,
|
|
1806
|
-
LANGUAGE_VOICES,
|
|
1823
|
+
WIDE_VOICES as LANGUAGE_VOICES,
|
|
1807
1824
|
VERSION,
|
|
1808
1825
|
alignVoiceoverToRecording,
|
|
1809
1826
|
applyTransitions,
|
|
@@ -1813,6 +1830,7 @@ export {
|
|
|
1813
1830
|
buildUserPrompt,
|
|
1814
1831
|
buildWatermarkFilters,
|
|
1815
1832
|
callAI,
|
|
1833
|
+
callAIViaRelay,
|
|
1816
1834
|
canReuseVoiceover,
|
|
1817
1835
|
composeVideo,
|
|
1818
1836
|
concatMedia,
|
|
@@ -1831,7 +1849,7 @@ export {
|
|
|
1831
1849
|
getUpgradeUrl,
|
|
1832
1850
|
hasAccess,
|
|
1833
1851
|
hasTrialRemaining,
|
|
1834
|
-
hexToASS,
|
|
1852
|
+
hexToAss as hexToASS,
|
|
1835
1853
|
isCanceling,
|
|
1836
1854
|
isExcludedLink,
|
|
1837
1855
|
isPaid,
|
|
@@ -1853,5 +1871,5 @@ export {
|
|
|
1853
1871
|
spliceVideos,
|
|
1854
1872
|
synthesizeSpeech,
|
|
1855
1873
|
toTitleCase,
|
|
1856
|
-
windowsPathToWSL
|
|
1874
|
+
windowsDrivePathToWsl as windowsPathToWSL
|
|
1857
1875
|
};
|