@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/qp.js
CHANGED
|
@@ -35,14 +35,6 @@ function getLanguageName(lang) {
|
|
|
35
35
|
if (!lang || lang === "en") return null;
|
|
36
36
|
return LANGUAGE_NAMES[lang] || lang.toUpperCase();
|
|
37
37
|
}
|
|
38
|
-
function hexToASS(hex, alpha = 0) {
|
|
39
|
-
const clean = hex.replace("#", "");
|
|
40
|
-
const r = clean.slice(0, 2);
|
|
41
|
-
const g = clean.slice(2, 4);
|
|
42
|
-
const b = clean.slice(4, 6);
|
|
43
|
-
const a = alpha.toString(16).padStart(2, "0").toUpperCase();
|
|
44
|
-
return `&H${a}${b}${g}${r}`.toUpperCase();
|
|
45
|
-
}
|
|
46
38
|
function rgbaToHex(color) {
|
|
47
39
|
if (color.startsWith("#")) return color;
|
|
48
40
|
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
@@ -52,12 +44,6 @@ function rgbaToHex(color) {
|
|
|
52
44
|
const b = parseInt(match[3], 10).toString(16).padStart(2, "0");
|
|
53
45
|
return `#${r}${g}${b}`.toUpperCase();
|
|
54
46
|
}
|
|
55
|
-
function windowsPathToWSL(path) {
|
|
56
|
-
const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
|
|
57
|
-
if (!driveMatch) return null;
|
|
58
|
-
const drive = driveMatch[1].toLowerCase();
|
|
59
|
-
return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
|
|
60
|
-
}
|
|
61
47
|
function parsePartial(partial) {
|
|
62
48
|
if (!partial) return {};
|
|
63
49
|
const parts = partial.split("-");
|
|
@@ -73,7 +59,7 @@ function getErrorMessage(err) {
|
|
|
73
59
|
return err instanceof Error ? err.message : String(err);
|
|
74
60
|
}
|
|
75
61
|
function toTitleCase(text) {
|
|
76
|
-
return text.replace(/\w
|
|
62
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
77
63
|
if (index === 0) {
|
|
78
64
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
79
65
|
}
|
|
@@ -83,10 +69,12 @@ function toTitleCase(text) {
|
|
|
83
69
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
84
70
|
});
|
|
85
71
|
}
|
|
86
|
-
var LANGUAGE_NAMES, LOWERCASE_WORDS;
|
|
72
|
+
var import_video_kit, import_video_kit2, LANGUAGE_NAMES, LOWERCASE_WORDS;
|
|
87
73
|
var init_utils = __esm({
|
|
88
74
|
"src/lib/utils.ts"() {
|
|
89
75
|
"use strict";
|
|
76
|
+
import_video_kit = require("@spark-apps/video-kit");
|
|
77
|
+
import_video_kit2 = require("@spark-apps/video-kit");
|
|
90
78
|
LANGUAGE_NAMES = {
|
|
91
79
|
ar: "Arabic",
|
|
92
80
|
es: "Spanish",
|
|
@@ -137,21 +125,27 @@ var init_utils = __esm({
|
|
|
137
125
|
});
|
|
138
126
|
|
|
139
127
|
// src/lib/api-client.ts
|
|
140
|
-
|
|
128
|
+
function apiKey() {
|
|
129
|
+
return process.env.SPARK_AI_KEY || "";
|
|
130
|
+
}
|
|
131
|
+
async function getOrRefreshDirectToken() {
|
|
141
132
|
const now = Date.now();
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
133
|
+
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
134
|
+
return cachedDirectToken.token;
|
|
144
135
|
}
|
|
145
136
|
try {
|
|
146
137
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
147
138
|
method: "POST",
|
|
148
|
-
headers: { "X-Api-Key":
|
|
139
|
+
headers: { "X-Api-Key": apiKey() }
|
|
149
140
|
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
143
|
+
}
|
|
150
144
|
const data = await response.json();
|
|
151
|
-
if (!
|
|
152
|
-
throw new Error(data.error || `Token exchange failed:
|
|
145
|
+
if (!data.success || !data.token) {
|
|
146
|
+
throw new Error(data.error || `Token exchange failed: unexpected response`);
|
|
153
147
|
}
|
|
154
|
-
|
|
148
|
+
cachedDirectToken = {
|
|
155
149
|
token: data.token,
|
|
156
150
|
expiresAt: data.expiresAt || now + 36e5
|
|
157
151
|
};
|
|
@@ -160,77 +154,130 @@ async function getOrRefreshToken() {
|
|
|
160
154
|
throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
|
|
161
155
|
}
|
|
162
156
|
}
|
|
163
|
-
async function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
error: "AI service is not configured. Please report this issue.",
|
|
168
|
-
isAuthError: true
|
|
169
|
-
};
|
|
157
|
+
async function getOrRefreshRelayToken(email) {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
|
|
160
|
+
return cachedRelayToken.token;
|
|
170
161
|
}
|
|
171
162
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
const response = await fetch(RELAY_TOKEN_URL, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify({ email })
|
|
167
|
+
});
|
|
168
|
+
const data = await response.json().catch(() => ({}));
|
|
169
|
+
if (!response.ok || !data.success || !data.token) {
|
|
170
|
+
throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
|
|
171
|
+
}
|
|
172
|
+
cachedRelayToken = {
|
|
173
|
+
token: data.token,
|
|
174
|
+
expiresAt: data.expiresAt || now + 36e5,
|
|
175
|
+
email
|
|
176
|
+
};
|
|
177
|
+
return data.token;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
183
|
+
let lastRateLimitError = null;
|
|
184
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(chatUrl, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
Authorization: `Bearer ${bearerToken}`
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
messages: [
|
|
194
|
+
{ role: "system", content: systemPrompt },
|
|
195
|
+
{ role: "user", content: userPrompt }
|
|
196
|
+
]
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
let data = {};
|
|
200
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
201
|
+
if (contentType.includes("application/json")) {
|
|
202
|
+
data = await response.json();
|
|
203
|
+
}
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
|
|
206
|
+
if (response.status === 429 && attempt < 3) {
|
|
207
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
208
|
+
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
|
|
209
|
+
lastRateLimitError = {
|
|
205
210
|
success: false,
|
|
206
211
|
error,
|
|
207
|
-
isRateLimit:
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
if (!data.success || !data.response) {
|
|
212
|
-
return {
|
|
213
|
-
success: false,
|
|
214
|
-
error: data.error || "No response from Spark AI"
|
|
212
|
+
isRateLimit: true,
|
|
213
|
+
...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
|
|
215
214
|
};
|
|
215
|
+
await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
|
|
216
|
+
continue;
|
|
216
217
|
}
|
|
217
218
|
return {
|
|
218
|
-
success:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
success: false,
|
|
220
|
+
error,
|
|
221
|
+
isRateLimit: response.status === 429,
|
|
222
|
+
isAuthError: response.status === 401 || response.status === 403
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (!data.success || !data.response) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: data.error || "No response from Spark AI"
|
|
226
229
|
};
|
|
227
|
-
} catch (err) {
|
|
228
|
-
if (attempt === 3) throw err;
|
|
229
|
-
const delayMs = 1e3 * Math.pow(2, attempt - 1);
|
|
230
|
-
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
231
230
|
}
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
content: data.response,
|
|
234
|
+
limits: {
|
|
235
|
+
used: data.limits?.used ?? 0,
|
|
236
|
+
limit: data.limits?.limit ?? 100,
|
|
237
|
+
tier,
|
|
238
|
+
...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (attempt === 3) throw err;
|
|
243
|
+
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
244
|
+
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
232
245
|
}
|
|
233
|
-
|
|
246
|
+
}
|
|
247
|
+
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
248
|
+
}
|
|
249
|
+
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
250
|
+
if (!apiKey()) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: "AI service is not configured. Please report this issue.",
|
|
254
|
+
isAuthError: true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const token = await getOrRefreshDirectToken();
|
|
259
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
265
|
+
if (!email) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: "Sign in required. Run qp login to connect your account.",
|
|
269
|
+
isAuthError: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
let token = await getOrRefreshRelayToken(email);
|
|
274
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
275
|
+
if (!result.success && result.isAuthError) {
|
|
276
|
+
cachedRelayToken = null;
|
|
277
|
+
token = await getOrRefreshRelayToken(email);
|
|
278
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
234
281
|
} catch (err) {
|
|
235
282
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
236
283
|
}
|
|
@@ -260,7 +307,7 @@ function normalizeUrl(input) {
|
|
|
260
307
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
261
308
|
return `https://${url}`;
|
|
262
309
|
}
|
|
263
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN,
|
|
310
|
+
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
264
311
|
var init_api_client = __esm({
|
|
265
312
|
"src/lib/api-client.ts"() {
|
|
266
313
|
"use strict";
|
|
@@ -268,14 +315,20 @@ var init_api_client = __esm({
|
|
|
268
315
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
269
316
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
270
317
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
319
|
+
RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
320
|
+
RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
321
|
+
cachedDirectToken = null;
|
|
322
|
+
cachedRelayToken = null;
|
|
273
323
|
}
|
|
274
324
|
});
|
|
275
325
|
|
|
276
326
|
// src/lib/SparkStripe-feature-gating.ts
|
|
277
327
|
async function _fetch(email, init) {
|
|
278
|
-
const r = await fetch(
|
|
328
|
+
const r = await fetch(
|
|
329
|
+
`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
|
|
330
|
+
init
|
|
331
|
+
);
|
|
279
332
|
if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
|
|
280
333
|
return r.json();
|
|
281
334
|
}
|
|
@@ -318,24 +371,27 @@ var init_SparkStripe_feature_gating = __esm({
|
|
|
318
371
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
319
372
|
BASE = "https://sparkstripe.com";
|
|
320
373
|
TIERS = {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
374
|
+
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
375
|
+
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
376
|
+
price_1TADm7Ef9opkuWid3I9tquIB: "pro",
|
|
377
|
+
price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
|
|
378
|
+
price_1TCREmEf9opkuWidvhuqzqDM: "pro",
|
|
379
|
+
price_1TCREmEf9opkuWidacf1tiC7: "pro"
|
|
327
380
|
};
|
|
328
381
|
RANK = {
|
|
329
|
-
|
|
330
|
-
|
|
382
|
+
free: 0,
|
|
383
|
+
pro: 1
|
|
384
|
+
};
|
|
385
|
+
FEATURES = {
|
|
386
|
+
free: [],
|
|
387
|
+
pro: [
|
|
388
|
+
"Everything in Free",
|
|
389
|
+
"No watermark",
|
|
390
|
+
"Full video composition (voiceover + captions + music + transitions)",
|
|
391
|
+
"Unlimited videos",
|
|
392
|
+
"Priority support"
|
|
393
|
+
]
|
|
331
394
|
};
|
|
332
|
-
FEATURES = { "free": [], "pro": [
|
|
333
|
-
"Everything in Free",
|
|
334
|
-
"No watermark",
|
|
335
|
-
"Full video composition (voiceover + captions + music + transitions)",
|
|
336
|
-
"Unlimited videos",
|
|
337
|
-
"Priority support"
|
|
338
|
-
] };
|
|
339
395
|
getStatus = (email) => _fetch(email);
|
|
340
396
|
_cache = /* @__PURE__ */ new Map();
|
|
341
397
|
isPaid = (d) => getTier(d) !== "free";
|
|
@@ -365,7 +421,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
365
421
|
var init_config = __esm({
|
|
366
422
|
"src/lib/config.ts"() {
|
|
367
423
|
"use strict";
|
|
368
|
-
VERSION = "1.
|
|
424
|
+
VERSION = "1.2.1";
|
|
369
425
|
CONFIG_FILE = "quickpeek.config.json";
|
|
370
426
|
DEFAULT_CONFIG = {
|
|
371
427
|
plan: { maxSteps: 8 },
|
|
@@ -532,14 +588,29 @@ function getActionForType(type) {
|
|
|
532
588
|
return "type";
|
|
533
589
|
}
|
|
534
590
|
}
|
|
535
|
-
async function crawlPage(url, task2 = noopTask) {
|
|
591
|
+
async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
592
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
593
|
+
const cached = crawlCache.get(cacheKey);
|
|
594
|
+
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
595
|
+
task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
596
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
597
|
+
}
|
|
536
598
|
const browserTask = task2("Launching browser");
|
|
537
599
|
const browser = await import_playwright.chromium.launch({ headless: true });
|
|
538
|
-
const
|
|
539
|
-
|
|
600
|
+
const context = await browser.newContext({
|
|
601
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
602
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
603
|
+
// a page nobody will ever see.
|
|
604
|
+
...viewport ? { viewport } : {}
|
|
605
|
+
});
|
|
606
|
+
const page = await context.newPage();
|
|
607
|
+
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
540
608
|
const crawlTask = task2("Crawling page");
|
|
541
609
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
542
610
|
const title = await page.title();
|
|
611
|
+
const scrollable = await page.evaluate(
|
|
612
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
613
|
+
);
|
|
543
614
|
const rawElements = await page.evaluate(`
|
|
544
615
|
(() => {
|
|
545
616
|
const items = [];
|
|
@@ -669,9 +740,14 @@ async function crawlPage(url, task2 = noopTask) {
|
|
|
669
740
|
});
|
|
670
741
|
await browser.close();
|
|
671
742
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
672
|
-
|
|
743
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
744
|
+
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
745
|
+
const oldest = crawlCache.keys().next().value;
|
|
746
|
+
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
747
|
+
}
|
|
748
|
+
return { title, elements, scrollable };
|
|
673
749
|
}
|
|
674
|
-
var import_playwright, noopTask;
|
|
750
|
+
var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
675
751
|
var init_crawl = __esm({
|
|
676
752
|
"src/lib/crawl.ts"() {
|
|
677
753
|
"use strict";
|
|
@@ -679,6 +755,9 @@ var init_crawl = __esm({
|
|
|
679
755
|
init_scraping();
|
|
680
756
|
noopTask = () => ({ succeed: () => {
|
|
681
757
|
} });
|
|
758
|
+
CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
759
|
+
CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
760
|
+
crawlCache = /* @__PURE__ */ new Map();
|
|
682
761
|
}
|
|
683
762
|
});
|
|
684
763
|
|
|
@@ -723,10 +802,10 @@ var init_watermark = __esm({
|
|
|
723
802
|
// src/lib/ffmpeg/spawn.ts
|
|
724
803
|
function toWSLPath(path) {
|
|
725
804
|
if (!isWindowsRuntime) return path;
|
|
726
|
-
return
|
|
805
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path.replace(/\\/g, "/");
|
|
727
806
|
}
|
|
728
807
|
function convertArgToWSL(arg2) {
|
|
729
|
-
const wslPath =
|
|
808
|
+
const wslPath = (0, import_video_kit2.windowsDrivePathToWsl)(arg2);
|
|
730
809
|
if (wslPath) return wslPath;
|
|
731
810
|
if (arg2.includes("\\")) return arg2.replace(/\\/g, "/");
|
|
732
811
|
return arg2;
|
|
@@ -823,20 +902,24 @@ function composeVideo(opts) {
|
|
|
823
902
|
`[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
|
|
824
903
|
// Music: fade in and fade out
|
|
825
904
|
`[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]`,
|
|
826
|
-
|
|
905
|
+
// Normalise the MIX and hard-limit its true peak - ported from
|
|
906
|
+
// VidLet, where single-pass loudnorm alone measured +0.9 dBTP on
|
|
907
|
+
// sharp TTS consonants. -14 LUFS is the YouTube/IG/TikTok playback
|
|
908
|
+
// target, so the platform leaves the audio untouched.
|
|
909
|
+
`[voice][music]amix=inputs=2:duration=longest,${import_video_kit3.MASTER_CHAIN}[aout]`
|
|
827
910
|
].join(";");
|
|
828
911
|
args.push("-vf", vfStr);
|
|
829
912
|
args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
|
|
830
913
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
831
914
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
832
915
|
} else if (hasVoiceover) {
|
|
833
|
-
const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}`;
|
|
916
|
+
const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn},${import_video_kit3.MASTER_CHAIN}`;
|
|
834
917
|
args.push("-vf", vfStr);
|
|
835
918
|
args.push("-map", "0:v", "-map", "1:a", "-af", af);
|
|
836
919
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
837
920
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
838
921
|
} else if (hasMusic) {
|
|
839
|
-
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}`;
|
|
922
|
+
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},${import_video_kit3.MASTER_CHAIN}`;
|
|
840
923
|
args.push("-vf", vfStr);
|
|
841
924
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
842
925
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
@@ -849,9 +932,11 @@ function composeVideo(opts) {
|
|
|
849
932
|
runFFmpeg(args, "ffmpeg compose failed").then(resolve9, reject);
|
|
850
933
|
});
|
|
851
934
|
}
|
|
935
|
+
var import_video_kit3;
|
|
852
936
|
var init_compose = __esm({
|
|
853
937
|
"src/lib/ffmpeg/compose.ts"() {
|
|
854
938
|
"use strict";
|
|
939
|
+
import_video_kit3 = require("@spark-apps/video-kit");
|
|
855
940
|
init_watermark();
|
|
856
941
|
init_spawn();
|
|
857
942
|
}
|
|
@@ -1108,14 +1193,13 @@ function applyTransitions(opts) {
|
|
|
1108
1193
|
filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
|
|
1109
1194
|
}
|
|
1110
1195
|
let currentV = "v0";
|
|
1196
|
+
let chainDuration = boundaries[1] - boundaries[0];
|
|
1111
1197
|
for (let i = 0; i < sorted.length; i++) {
|
|
1112
1198
|
const trans = sorted[i];
|
|
1113
1199
|
const duration = trans.duration ?? 0.3;
|
|
1114
1200
|
const nextIdx = i + 1;
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
const segDuration = segEnd - segStart;
|
|
1118
|
-
const offset = Math.max(0, segDuration - duration);
|
|
1201
|
+
const offset = Math.max(0, chainDuration - duration);
|
|
1202
|
+
chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
|
|
1119
1203
|
const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
|
|
1120
1204
|
const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
|
|
1121
1205
|
filterParts.push(
|
|
@@ -1235,7 +1319,7 @@ EXAMPLES:
|
|
|
1235
1319
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1236
1320
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1237
1321
|
}
|
|
1238
|
-
function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
|
|
1322
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1239
1323
|
const lang = langName ? `
|
|
1240
1324
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1241
1325
|
const purpose = description ? `
|
|
@@ -1273,8 +1357,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1273
1357
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1274
1358
|
}
|
|
1275
1359
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1360
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1361
|
+
const scrolling = cannotScroll ? `
|
|
1362
|
+
|
|
1363
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1364
|
+
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" : "";
|
|
1276
1365
|
return `URL: ${url}
|
|
1277
|
-
Title: ${title}${purpose}
|
|
1366
|
+
Title: ${title}${purpose}${scrolling}
|
|
1278
1367
|
|
|
1279
1368
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1280
1369
|
${elementSummary}
|
|
@@ -1284,7 +1373,7 @@ REQUIREMENTS:
|
|
|
1284
1373
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1285
1374
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1286
1375
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1287
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1376
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1288
1377
|
|
|
1289
1378
|
Return JSON:
|
|
1290
1379
|
{
|
|
@@ -1307,33 +1396,31 @@ TARGET SYNTAX:
|
|
|
1307
1396
|
\u2022 Sliders: input[type="range"]
|
|
1308
1397
|
\u2022 Text inputs: Use selector from list above`;
|
|
1309
1398
|
}
|
|
1399
|
+
var MIN_USEFUL_SCROLL_PX;
|
|
1310
1400
|
var init_prompts = __esm({
|
|
1311
1401
|
"src/lib/prompts.ts"() {
|
|
1312
1402
|
"use strict";
|
|
1313
1403
|
init_utils();
|
|
1404
|
+
MIN_USEFUL_SCROLL_PX = 200;
|
|
1314
1405
|
}
|
|
1315
1406
|
});
|
|
1316
1407
|
|
|
1317
1408
|
// src/lib/styles.ts
|
|
1318
1409
|
function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
|
|
1319
|
-
return
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
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
|
|
1328
|
-
|
|
1329
|
-
[Events]
|
|
1330
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
1331
|
-
`;
|
|
1410
|
+
return (0, import_video_kit4.buildAssHeader)({
|
|
1411
|
+
title: "QuickPeek Demo",
|
|
1412
|
+
width,
|
|
1413
|
+
height,
|
|
1414
|
+
styles: [
|
|
1415
|
+
`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`
|
|
1416
|
+
]
|
|
1417
|
+
});
|
|
1332
1418
|
}
|
|
1333
|
-
var CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1419
|
+
var import_video_kit4, CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1334
1420
|
var init_styles = __esm({
|
|
1335
1421
|
"src/lib/styles.ts"() {
|
|
1336
1422
|
"use strict";
|
|
1423
|
+
import_video_kit4 = require("@spark-apps/video-kit");
|
|
1337
1424
|
init_cursor();
|
|
1338
1425
|
CURSOR_MOVE_DURATION = 800;
|
|
1339
1426
|
CURSOR_CSS = `
|
|
@@ -1416,7 +1503,7 @@ var init_subscription_client = __esm({
|
|
|
1416
1503
|
|
|
1417
1504
|
// src/lib/tts/helpers.ts
|
|
1418
1505
|
function toWSLPathForTTS(path) {
|
|
1419
|
-
return
|
|
1506
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path;
|
|
1420
1507
|
}
|
|
1421
1508
|
function estimateActionDuration(step) {
|
|
1422
1509
|
if (step.action === "drag" && step.value === "around") {
|
|
@@ -1446,8 +1533,8 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1446
1533
|
if (!["mp4", "webm", "mov"].includes(ext)) {
|
|
1447
1534
|
return 0;
|
|
1448
1535
|
}
|
|
1449
|
-
const mediaPath = (0,
|
|
1450
|
-
if (!(0,
|
|
1536
|
+
const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
|
|
1537
|
+
if (!(0, import_node_fs2.existsSync)(mediaPath)) {
|
|
1451
1538
|
return 0;
|
|
1452
1539
|
}
|
|
1453
1540
|
try {
|
|
@@ -1467,38 +1554,38 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1467
1554
|
}
|
|
1468
1555
|
}
|
|
1469
1556
|
async function loadVoiceoverMeta(outputDir) {
|
|
1470
|
-
const metaPath = (0,
|
|
1557
|
+
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1471
1558
|
try {
|
|
1472
|
-
if (!(0,
|
|
1473
|
-
const content = await (0,
|
|
1559
|
+
if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
|
|
1560
|
+
const content = await (0, import_promises2.readFile)(metaPath, "utf-8");
|
|
1474
1561
|
return JSON.parse(content);
|
|
1475
1562
|
} catch {
|
|
1476
1563
|
return null;
|
|
1477
1564
|
}
|
|
1478
1565
|
}
|
|
1479
1566
|
async function saveVoiceoverMeta(outputDir, meta) {
|
|
1480
|
-
const metaPath = (0,
|
|
1481
|
-
await (0,
|
|
1567
|
+
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1568
|
+
await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
1482
1569
|
}
|
|
1483
1570
|
async function concatenateAudio(inputs, output, workDir) {
|
|
1484
1571
|
const formatPath = (f) => {
|
|
1485
|
-
const resolved = (0,
|
|
1486
|
-
return isWindows ?
|
|
1572
|
+
const resolved = (0, import_node_path2.resolve)(f);
|
|
1573
|
+
return isWindows ? (0, import_video_kit2.windowsDrivePathToWsl)(resolved) ?? resolved : resolved;
|
|
1487
1574
|
};
|
|
1488
1575
|
const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
|
|
1489
|
-
const listPath = (0,
|
|
1490
|
-
await (0,
|
|
1576
|
+
const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
|
|
1577
|
+
await (0, import_promises2.writeFile)(listPath, listContent);
|
|
1491
1578
|
await concatMedia(listPath, output);
|
|
1492
|
-
await (0,
|
|
1579
|
+
await (0, import_promises2.unlink)(listPath).catch(() => {
|
|
1493
1580
|
});
|
|
1494
1581
|
}
|
|
1495
|
-
var
|
|
1582
|
+
var import_node_fs2, import_promises2, import_node_path2, isWindows;
|
|
1496
1583
|
var init_helpers = __esm({
|
|
1497
1584
|
"src/lib/tts/helpers.ts"() {
|
|
1498
1585
|
"use strict";
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1586
|
+
import_node_fs2 = require("fs");
|
|
1587
|
+
import_promises2 = require("fs/promises");
|
|
1588
|
+
import_node_path2 = require("path");
|
|
1502
1589
|
init_ffmpeg2();
|
|
1503
1590
|
init_utils();
|
|
1504
1591
|
isWindows = process.platform === "win32";
|
|
@@ -1534,7 +1621,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1534
1621
|
if (!needsRealignment) {
|
|
1535
1622
|
return stepAudio;
|
|
1536
1623
|
}
|
|
1537
|
-
const audioDir = (0,
|
|
1624
|
+
const audioDir = (0, import_node_path3.join)(workDir, "audio");
|
|
1538
1625
|
const segments = [];
|
|
1539
1626
|
const alignedStepAudio = /* @__PURE__ */ new Map();
|
|
1540
1627
|
for (let i = 0; i < actualRecordingDurations.length; i++) {
|
|
@@ -1543,20 +1630,20 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1543
1630
|
const voiceDuration = voiceData?.duration ?? 0;
|
|
1544
1631
|
const recordingDuration = actualRecordingDurations[i] ?? 0;
|
|
1545
1632
|
const targetDuration = Math.max(voiceDuration, recordingDuration);
|
|
1546
|
-
const audioPath = (0,
|
|
1547
|
-
const originalPaddingPath = (0,
|
|
1548
|
-
const silencePath = (0,
|
|
1549
|
-
if ((0,
|
|
1633
|
+
const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1634
|
+
const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1635
|
+
const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1636
|
+
if ((0, import_node_fs3.existsSync)(audioPath)) {
|
|
1550
1637
|
segments.push(audioPath);
|
|
1551
|
-
if ((0,
|
|
1638
|
+
if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
|
|
1552
1639
|
segments.push(originalPaddingPath);
|
|
1553
1640
|
}
|
|
1554
1641
|
if (recordingDuration > voiceDuration) {
|
|
1555
|
-
const alignPaddingPath = (0,
|
|
1642
|
+
const alignPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
|
|
1556
1643
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1557
1644
|
segments.push(alignPaddingPath);
|
|
1558
1645
|
}
|
|
1559
|
-
} else if ((0,
|
|
1646
|
+
} else if ((0, import_node_fs3.existsSync)(silencePath)) {
|
|
1560
1647
|
if (targetDuration !== voiceDuration) {
|
|
1561
1648
|
await generateSilence(silencePath, targetDuration);
|
|
1562
1649
|
}
|
|
@@ -1568,7 +1655,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1568
1655
|
});
|
|
1569
1656
|
}
|
|
1570
1657
|
if (segments.length > 0) {
|
|
1571
|
-
const voiceoverPath = (0,
|
|
1658
|
+
const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
|
|
1572
1659
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1573
1660
|
}
|
|
1574
1661
|
const existingMeta = await loadVoiceoverMeta(outputDir);
|
|
@@ -1580,12 +1667,12 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1580
1667
|
}
|
|
1581
1668
|
return alignedStepAudio;
|
|
1582
1669
|
}
|
|
1583
|
-
var
|
|
1670
|
+
var import_node_fs3, import_node_path3;
|
|
1584
1671
|
var init_timing = __esm({
|
|
1585
1672
|
"src/lib/tts/timing.ts"() {
|
|
1586
1673
|
"use strict";
|
|
1587
|
-
|
|
1588
|
-
|
|
1674
|
+
import_node_fs3 = require("fs");
|
|
1675
|
+
import_node_path3 = require("path");
|
|
1589
1676
|
init_ffmpeg2();
|
|
1590
1677
|
init_helpers();
|
|
1591
1678
|
}
|
|
@@ -1616,7 +1703,7 @@ function canReuseVoiceover(steps, voice, existingMeta) {
|
|
|
1616
1703
|
}
|
|
1617
1704
|
async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
1618
1705
|
const { estimateWordTimings: estimateWordTimings2 } = await Promise.resolve().then(() => (init_timing(), timing_exports));
|
|
1619
|
-
const voiceoverPath = (0,
|
|
1706
|
+
const voiceoverPath = (0, import_node_path4.join)(outputDir, "voiceover.wav");
|
|
1620
1707
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1621
1708
|
for (let i = 0; i < steps.length; i++) {
|
|
1622
1709
|
const step = steps[i];
|
|
@@ -1629,97 +1716,29 @@ async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
|
1629
1716
|
}
|
|
1630
1717
|
return { voiceoverPath, stepAudio };
|
|
1631
1718
|
}
|
|
1632
|
-
var
|
|
1719
|
+
var import_node_path4;
|
|
1633
1720
|
var init_cache = __esm({
|
|
1634
1721
|
"src/lib/tts/cache.ts"() {
|
|
1635
1722
|
"use strict";
|
|
1636
|
-
|
|
1723
|
+
import_node_path4 = require("path");
|
|
1637
1724
|
init_helpers();
|
|
1638
1725
|
}
|
|
1639
1726
|
});
|
|
1640
1727
|
|
|
1641
1728
|
// src/lib/voices.ts
|
|
1642
|
-
var
|
|
1729
|
+
var import_video_kit5;
|
|
1643
1730
|
var init_voices = __esm({
|
|
1644
1731
|
"src/lib/voices.ts"() {
|
|
1645
1732
|
"use strict";
|
|
1646
|
-
|
|
1647
|
-
en: "en-GB-SoniaNeural",
|
|
1648
|
-
ar: "ar-LB-LaylaNeural",
|
|
1649
|
-
es: "es-ES-ElviraNeural",
|
|
1650
|
-
fr: "fr-FR-DeniseNeural",
|
|
1651
|
-
de: "de-DE-KatjaNeural",
|
|
1652
|
-
it: "it-IT-ElsaNeural",
|
|
1653
|
-
pt: "pt-BR-FranciscaNeural",
|
|
1654
|
-
ru: "ru-RU-SvetlanaNeural",
|
|
1655
|
-
zh: "zh-CN-XiaoxiaoNeural",
|
|
1656
|
-
ja: "ja-JP-NanamiNeural",
|
|
1657
|
-
ko: "ko-KR-SunHiNeural",
|
|
1658
|
-
hi: "hi-IN-SwaraNeural",
|
|
1659
|
-
tr: "tr-TR-EmelNeural",
|
|
1660
|
-
nl: "nl-NL-ColetteNeural",
|
|
1661
|
-
pl: "pl-PL-AgnieszkaNeural",
|
|
1662
|
-
sv: "sv-SE-SofieNeural",
|
|
1663
|
-
he: "he-IL-HilaNeural",
|
|
1664
|
-
th: "th-TH-PremwadeeNeural",
|
|
1665
|
-
vi: "vi-VN-HoaiMyNeural",
|
|
1666
|
-
id: "id-ID-GadisNeural",
|
|
1667
|
-
ms: "ms-MY-YasminNeural",
|
|
1668
|
-
fil: "fil-PH-BlessicaNeural",
|
|
1669
|
-
uk: "uk-UA-PolinaNeural",
|
|
1670
|
-
cs: "cs-CZ-VlastaNeural",
|
|
1671
|
-
el: "el-GR-AthinaNeural",
|
|
1672
|
-
ro: "ro-RO-AlinaNeural",
|
|
1673
|
-
hu: "hu-HU-NoemiNeural",
|
|
1674
|
-
da: "da-DK-ChristelNeural",
|
|
1675
|
-
fi: "fi-FI-NooraNeural",
|
|
1676
|
-
no: "nb-NO-PernilleNeural",
|
|
1677
|
-
sk: "sk-SK-ViktoriaNeural",
|
|
1678
|
-
bg: "bg-BG-KalinaNeural",
|
|
1679
|
-
hr: "hr-HR-GabrijelaNeural",
|
|
1680
|
-
sl: "sl-SI-PetraNeural",
|
|
1681
|
-
et: "et-EE-AnuNeural",
|
|
1682
|
-
lv: "lv-LV-EveritaNeural",
|
|
1683
|
-
lt: "lt-LT-OnaNeural",
|
|
1684
|
-
bn: "bn-IN-TanishaaNeural",
|
|
1685
|
-
ta: "ta-IN-PallaviNeural",
|
|
1686
|
-
te: "te-IN-ShrutiNeural",
|
|
1687
|
-
mr: "mr-IN-AarohiNeural",
|
|
1688
|
-
gu: "gu-IN-DhwaniNeural",
|
|
1689
|
-
kn: "kn-IN-SapnaNeural",
|
|
1690
|
-
ml: "ml-IN-SobhanaNeural",
|
|
1691
|
-
ur: "ur-PK-UzmaNeural",
|
|
1692
|
-
fa: "fa-IR-DilaraNeural",
|
|
1693
|
-
sw: "sw-KE-ZuriNeural",
|
|
1694
|
-
af: "af-ZA-AdriNeural",
|
|
1695
|
-
ca: "ca-ES-JoanaNeural",
|
|
1696
|
-
cy: "cy-GB-NiaNeural",
|
|
1697
|
-
ga: "ga-IE-OrlaNeural",
|
|
1698
|
-
mt: "mt-MT-GraceNeural",
|
|
1699
|
-
is: "is-IS-GudrunNeural",
|
|
1700
|
-
mk: "mk-MK-MarijaNeural",
|
|
1701
|
-
sr: "sr-RS-SophieNeural",
|
|
1702
|
-
bs: "bs-BA-VesnaNeural",
|
|
1703
|
-
sq: "sq-AL-AnilaNeural",
|
|
1704
|
-
am: "am-ET-MekdesNeural",
|
|
1705
|
-
az: "az-AZ-BabekNeural",
|
|
1706
|
-
ka: "ka-GE-EkaNeural",
|
|
1707
|
-
kk: "kk-KZ-AigulNeural",
|
|
1708
|
-
mn: "mn-MN-YesUINeural",
|
|
1709
|
-
ne: "ne-NP-HemkalaNeural",
|
|
1710
|
-
ps: "ps-AF-LatifaNeural",
|
|
1711
|
-
si: "si-LK-ThiliniNeural",
|
|
1712
|
-
so: "so-SO-UbaxNeural",
|
|
1713
|
-
zu: "zu-ZA-ThandoNeural"
|
|
1714
|
-
};
|
|
1733
|
+
import_video_kit5 = require("@spark-apps/video-kit");
|
|
1715
1734
|
}
|
|
1716
1735
|
});
|
|
1717
1736
|
|
|
1718
1737
|
// src/lib/tts/synthesize.ts
|
|
1719
1738
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1720
|
-
const tempScriptPath = (0,
|
|
1721
|
-
const wslScriptPath = isWindows ? toWSLPathForTTS((0,
|
|
1722
|
-
const wslOutputPath = isWindows ? toWSLPathForTTS((0,
|
|
1739
|
+
const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1740
|
+
const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
|
|
1741
|
+
const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
|
|
1723
1742
|
const pythonScript = `
|
|
1724
1743
|
import asyncio
|
|
1725
1744
|
from edge_tts import Communicate
|
|
@@ -1734,7 +1753,7 @@ async def main():
|
|
|
1734
1753
|
asyncio.run(main())
|
|
1735
1754
|
`;
|
|
1736
1755
|
try {
|
|
1737
|
-
await (0,
|
|
1756
|
+
await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
|
|
1738
1757
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1739
1758
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1740
1759
|
await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
|
|
@@ -1762,13 +1781,13 @@ asyncio.run(main())
|
|
|
1762
1781
|
}
|
|
1763
1782
|
}
|
|
1764
1783
|
}
|
|
1765
|
-
var import_node_child_process2,
|
|
1784
|
+
var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
|
|
1766
1785
|
var init_synthesize = __esm({
|
|
1767
1786
|
"src/lib/tts/synthesize.ts"() {
|
|
1768
1787
|
"use strict";
|
|
1769
1788
|
import_node_child_process2 = require("child_process");
|
|
1770
|
-
|
|
1771
|
-
|
|
1789
|
+
import_promises3 = require("fs/promises");
|
|
1790
|
+
import_node_path5 = require("path");
|
|
1772
1791
|
import_node_util = require("util");
|
|
1773
1792
|
init_helpers();
|
|
1774
1793
|
execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
|
|
@@ -1782,7 +1801,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
|
1782
1801
|
}
|
|
1783
1802
|
function resolveVoice(config) {
|
|
1784
1803
|
if (config.audio?.voice) return config.audio.voice;
|
|
1785
|
-
if (config.lang) return
|
|
1804
|
+
if (config.lang) return import_video_kit5.WIDE_VOICES[config.lang] || null;
|
|
1786
1805
|
return null;
|
|
1787
1806
|
}
|
|
1788
1807
|
async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
|
|
@@ -1807,8 +1826,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1807
1826
|
info2("No voice configured");
|
|
1808
1827
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1809
1828
|
}
|
|
1810
|
-
const audioDir = (0,
|
|
1811
|
-
await (0,
|
|
1829
|
+
const audioDir = (0, import_node_path6.join)(workDir, "audio");
|
|
1830
|
+
await (0, import_promises4.mkdir)(audioDir, { recursive: true });
|
|
1812
1831
|
const segments = [];
|
|
1813
1832
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1814
1833
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1820,13 +1839,13 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1820
1839
|
const manualDuration = (step.duration ?? 0) * 1e3;
|
|
1821
1840
|
const mediaDuration = await getMediaStepDuration(step, outputDir);
|
|
1822
1841
|
const silenceDuration = Math.max(actionDuration, manualDuration, mediaDuration, config.timing.minStepDuration);
|
|
1823
|
-
const silencePath = (0,
|
|
1842
|
+
const silencePath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1824
1843
|
await generateSilence(silencePath, silenceDuration);
|
|
1825
1844
|
segments.push(silencePath);
|
|
1826
1845
|
stepAudio.set(stepNum, { duration: silenceDuration, words: [] });
|
|
1827
1846
|
continue;
|
|
1828
1847
|
}
|
|
1829
|
-
const audioPath = (0,
|
|
1848
|
+
const audioPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1830
1849
|
if (segments.length > 0) await new Promise((r) => setTimeout(r, 200));
|
|
1831
1850
|
try {
|
|
1832
1851
|
await synthesizeSpeech(step.caption, audioPath, voice);
|
|
@@ -1843,7 +1862,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1843
1862
|
mediaDuration
|
|
1844
1863
|
);
|
|
1845
1864
|
if (stepDuration > audioDuration) {
|
|
1846
|
-
const paddingPath = (0,
|
|
1865
|
+
const paddingPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1847
1866
|
await generateSilence(paddingPath, stepDuration - audioDuration);
|
|
1848
1867
|
segments.push(paddingPath);
|
|
1849
1868
|
}
|
|
@@ -1859,8 +1878,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1859
1878
|
return { voiceoverPath: "", stepAudio };
|
|
1860
1879
|
}
|
|
1861
1880
|
const concatTask = task2("Combining audio");
|
|
1862
|
-
await (0,
|
|
1863
|
-
const voiceoverPath = (0,
|
|
1881
|
+
await (0, import_promises4.mkdir)(outputDir, { recursive: true });
|
|
1882
|
+
const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
|
|
1864
1883
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1865
1884
|
concatTask.succeed("Done");
|
|
1866
1885
|
const meta = {
|
|
@@ -1873,12 +1892,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1873
1892
|
await saveVoiceoverMeta(outputDir, meta);
|
|
1874
1893
|
return { voiceoverPath, stepAudio };
|
|
1875
1894
|
}
|
|
1876
|
-
var
|
|
1895
|
+
var import_promises4, import_node_path6;
|
|
1877
1896
|
var init_generate = __esm({
|
|
1878
1897
|
"src/lib/tts/generate.ts"() {
|
|
1879
1898
|
"use strict";
|
|
1880
|
-
|
|
1881
|
-
|
|
1899
|
+
import_promises4 = require("fs/promises");
|
|
1900
|
+
import_node_path6 = require("path");
|
|
1882
1901
|
init_ffmpeg2();
|
|
1883
1902
|
init_voices();
|
|
1884
1903
|
init_helpers();
|
|
@@ -1933,7 +1952,7 @@ __export(lib_exports, {
|
|
|
1933
1952
|
EXCLUDED_LINK_PATTERNS: () => EXCLUDED_LINK_PATTERNS,
|
|
1934
1953
|
FEATURES: () => FEATURES,
|
|
1935
1954
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1936
|
-
LANGUAGE_VOICES: () =>
|
|
1955
|
+
LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
|
|
1937
1956
|
VERSION: () => VERSION,
|
|
1938
1957
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1939
1958
|
applyTransitions: () => applyTransitions,
|
|
@@ -1943,6 +1962,7 @@ __export(lib_exports, {
|
|
|
1943
1962
|
buildUserPrompt: () => buildUserPrompt,
|
|
1944
1963
|
buildWatermarkFilters: () => buildWatermarkFilters,
|
|
1945
1964
|
callAI: () => callAI,
|
|
1965
|
+
callAIViaRelay: () => callAIViaRelay,
|
|
1946
1966
|
canReuseVoiceover: () => canReuseVoiceover,
|
|
1947
1967
|
composeVideo: () => composeVideo,
|
|
1948
1968
|
concatMedia: () => concatMedia,
|
|
@@ -1961,7 +1981,7 @@ __export(lib_exports, {
|
|
|
1961
1981
|
getUpgradeUrl: () => getUpgradeUrl,
|
|
1962
1982
|
hasAccess: () => hasAccess,
|
|
1963
1983
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1964
|
-
hexToASS: () =>
|
|
1984
|
+
hexToASS: () => import_video_kit.hexToAss,
|
|
1965
1985
|
isCanceling: () => isCanceling,
|
|
1966
1986
|
isExcludedLink: () => isExcludedLink,
|
|
1967
1987
|
isPaid: () => isPaid,
|
|
@@ -1983,7 +2003,7 @@ __export(lib_exports, {
|
|
|
1983
2003
|
spliceVideos: () => spliceVideos,
|
|
1984
2004
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
1985
2005
|
toTitleCase: () => toTitleCase,
|
|
1986
|
-
windowsPathToWSL: () =>
|
|
2006
|
+
windowsPathToWSL: () => import_video_kit2.windowsDrivePathToWsl
|
|
1987
2007
|
});
|
|
1988
2008
|
var init_lib = __esm({
|
|
1989
2009
|
"src/lib/index.ts"() {
|
|
@@ -1995,9 +2015,9 @@ var init_lib = __esm({
|
|
|
1995
2015
|
init_cursor();
|
|
1996
2016
|
init_ffmpeg2();
|
|
1997
2017
|
init_prompts();
|
|
2018
|
+
init_SparkStripe_feature_gating();
|
|
1998
2019
|
init_scraping();
|
|
1999
2020
|
init_styles();
|
|
2000
|
-
init_SparkStripe_feature_gating();
|
|
2001
2021
|
init_subscription_client();
|
|
2002
2022
|
init_tts2();
|
|
2003
2023
|
init_types3();
|
|
@@ -2009,199 +2029,691 @@ var init_lib = __esm({
|
|
|
2009
2029
|
|
|
2010
2030
|
// src/qp.ts
|
|
2011
2031
|
var import_dotenv = __toESM(require("dotenv"));
|
|
2012
|
-
var
|
|
2013
|
-
|
|
2014
|
-
// src/commands/capture-cmd.ts
|
|
2015
|
-
var import_node_path19 = require("path");
|
|
2016
|
-
var import_chalk9 = __toESM(require("chalk"));
|
|
2017
|
-
|
|
2018
|
-
// src/ai/narrate.ts
|
|
2019
|
-
init_lib();
|
|
2032
|
+
var import_chalk21 = __toESM(require("chalk"));
|
|
2020
2033
|
|
|
2021
|
-
// src/
|
|
2022
|
-
var
|
|
2023
|
-
var
|
|
2024
|
-
|
|
2034
|
+
// src/commands/auth-cmd.ts
|
|
2035
|
+
var import_node_readline2 = require("readline");
|
|
2036
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
2037
|
+
var import_playwright2 = require("playwright");
|
|
2025
2038
|
|
|
2026
|
-
// src/
|
|
2027
|
-
var
|
|
2028
|
-
var
|
|
2029
|
-
var
|
|
2030
|
-
|
|
2031
|
-
var
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
});
|
|
2039
|
+
// src/browser/auth-state.ts
|
|
2040
|
+
var import_node_fs = require("fs");
|
|
2041
|
+
var import_promises = require("fs/promises");
|
|
2042
|
+
var import_node_os = require("os");
|
|
2043
|
+
var import_node_path = require("path");
|
|
2044
|
+
var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
|
|
2045
|
+
function hostOf(url) {
|
|
2046
|
+
try {
|
|
2047
|
+
return new URL(url).host;
|
|
2048
|
+
} catch {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2039
2051
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
let timer;
|
|
2044
|
-
const cleanup = (email) => {
|
|
2045
|
-
clearTimeout(timer);
|
|
2046
|
-
server?.close();
|
|
2047
|
-
resolve9(email);
|
|
2048
|
-
};
|
|
2049
|
-
server = (0, import_node_http.createServer)((req, res) => {
|
|
2050
|
-
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2051
|
-
const path = url.pathname;
|
|
2052
|
-
if (path === "/callback" || path === "/") {
|
|
2053
|
-
const email = url.searchParams.get("email");
|
|
2054
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2055
|
-
res.end(`
|
|
2056
|
-
<!DOCTYPE html>
|
|
2057
|
-
<html>
|
|
2058
|
-
<head><title>QuickPeek</title><style>
|
|
2059
|
-
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
|
|
2060
|
-
.card { text-align: center; padding: 2rem; }
|
|
2061
|
-
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
2062
|
-
p { color: #888; }
|
|
2063
|
-
</style></head>
|
|
2064
|
-
<body><div class="card">
|
|
2065
|
-
<h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
|
|
2066
|
-
<p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
|
|
2067
|
-
<p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
|
|
2068
|
-
</div></body>
|
|
2069
|
-
</html>
|
|
2070
|
-
`);
|
|
2071
|
-
if (email) {
|
|
2072
|
-
cleanup(email);
|
|
2073
|
-
}
|
|
2074
|
-
} else {
|
|
2075
|
-
res.writeHead(404);
|
|
2076
|
-
res.end();
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2080
|
-
const addr = server.address();
|
|
2081
|
-
if (!addr || typeof addr === "string") {
|
|
2082
|
-
cleanup(null);
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
const port = addr.port;
|
|
2086
|
-
const returnUrl = `http://localhost:${port}/callback`;
|
|
2087
|
-
const url = pricingUrl({ returnUrl });
|
|
2088
|
-
console.log();
|
|
2089
|
-
console.log(import_chalk.default.cyan(" Opening browser to sign in..."));
|
|
2090
|
-
console.log(import_chalk.default.dim(` ${url}`));
|
|
2091
|
-
console.log();
|
|
2092
|
-
console.log(import_chalk.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2093
|
-
openBrowser(url);
|
|
2094
|
-
});
|
|
2095
|
-
timer = setTimeout(() => {
|
|
2096
|
-
console.log();
|
|
2097
|
-
console.log(import_chalk.default.yellow(" Sign-in timed out."));
|
|
2098
|
-
cleanup(null);
|
|
2099
|
-
}, TIMEOUT_MS);
|
|
2100
|
-
});
|
|
2052
|
+
function authStatePath(url) {
|
|
2053
|
+
const host = hostOf(url);
|
|
2054
|
+
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
|
|
2101
2055
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
function getLastRateLimitInfo() {
|
|
2106
|
-
return lastRateLimitInfo;
|
|
2056
|
+
function findAuthState(url) {
|
|
2057
|
+
const path = authStatePath(url);
|
|
2058
|
+
return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
|
|
2107
2059
|
}
|
|
2108
|
-
function
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
});
|
|
2060
|
+
async function saveAuthState(url, state) {
|
|
2061
|
+
const path = authStatePath(url);
|
|
2062
|
+
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2063
|
+
await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
|
|
2064
|
+
await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
|
|
2065
|
+
await (0, import_promises.chmod)(path, 384);
|
|
2066
|
+
return path;
|
|
2116
2067
|
}
|
|
2117
|
-
async function
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
console.log(import_chalk2.default.white(" and unlock Pro features."));
|
|
2124
|
-
const browserEmail = await browserLogin();
|
|
2125
|
-
if (browserEmail) {
|
|
2126
|
-
console.log();
|
|
2127
|
-
console.log(import_chalk2.default.green(` \u2713 Signed in as ${browserEmail}`));
|
|
2128
|
-
console.log();
|
|
2129
|
-
return browserEmail;
|
|
2130
|
-
}
|
|
2131
|
-
console.log();
|
|
2132
|
-
console.log(import_chalk2.default.dim(" Or enter your email manually:"));
|
|
2133
|
-
const email = await prompt(import_chalk2.default.white(" Email: "));
|
|
2134
|
-
if (!email) {
|
|
2135
|
-
console.log();
|
|
2136
|
-
console.log(import_chalk2.default.red(" No email provided. Exiting."));
|
|
2137
|
-
process.exit(1);
|
|
2138
|
-
}
|
|
2139
|
-
if (!email.includes("@") || !email.includes(".")) {
|
|
2140
|
-
console.log();
|
|
2141
|
-
console.log(import_chalk2.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
2142
|
-
const proceed = await prompt(import_chalk2.default.white(" Continue anyway? ") + import_chalk2.default.dim("[y/N] "));
|
|
2143
|
-
if (proceed.toLowerCase() !== "y") {
|
|
2144
|
-
process.exit(1);
|
|
2145
|
-
}
|
|
2068
|
+
async function clearAuthState(url) {
|
|
2069
|
+
if (url) {
|
|
2070
|
+
const path = authStatePath(url);
|
|
2071
|
+
if (!path || !(0, import_node_fs.existsSync)(path)) return [];
|
|
2072
|
+
await (0, import_promises.unlink)(path);
|
|
2073
|
+
return [hostOf(url)];
|
|
2146
2074
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
return
|
|
2075
|
+
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2076
|
+
const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
|
|
2077
|
+
await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
|
|
2078
|
+
return files.map((f) => f.slice(0, -".json".length));
|
|
2151
2079
|
}
|
|
2152
|
-
async function
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
await logError2("AI API", new Error(result.error));
|
|
2156
|
-
if (result.isRateLimit) {
|
|
2157
|
-
console.log();
|
|
2158
|
-
console.log(import_chalk2.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2159
|
-
console.log();
|
|
2160
|
-
console.log(import_chalk2.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2161
|
-
console.log(import_chalk2.default.dim(" Options:"));
|
|
2162
|
-
if (result.retryAfterSeconds) {
|
|
2163
|
-
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2164
|
-
console.log(import_chalk2.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2165
|
-
} else {
|
|
2166
|
-
console.log(import_chalk2.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2167
|
-
}
|
|
2168
|
-
console.log(import_chalk2.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2169
|
-
console.log(import_chalk2.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2170
|
-
console.log();
|
|
2171
|
-
process.exit(1);
|
|
2172
|
-
}
|
|
2173
|
-
if (result.isAuthError) {
|
|
2174
|
-
console.log();
|
|
2175
|
-
console.log(import_chalk2.default.red(" \u26A0 Authentication Error"));
|
|
2176
|
-
console.log();
|
|
2177
|
-
console.log(import_chalk2.default.dim(` ${result.error}`));
|
|
2178
|
-
console.log();
|
|
2179
|
-
process.exit(1);
|
|
2180
|
-
}
|
|
2181
|
-
console.log();
|
|
2182
|
-
console.log(import_chalk2.default.red(" \u26A0 AI Error"));
|
|
2183
|
-
console.log();
|
|
2184
|
-
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2185
|
-
console.log(import_chalk2.default.dim(` ${firstLine.slice(0, 80)}`));
|
|
2186
|
-
console.log();
|
|
2187
|
-
process.exit(1);
|
|
2188
|
-
}
|
|
2189
|
-
lastRateLimitInfo = result.limits;
|
|
2190
|
-
return result.content;
|
|
2080
|
+
async function listAuthStates() {
|
|
2081
|
+
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2082
|
+
return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
|
|
2191
2083
|
}
|
|
2192
2084
|
|
|
2193
|
-
// src/
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2085
|
+
// src/help.ts
|
|
2086
|
+
var import_chalk = __toESM(require("chalk"));
|
|
2087
|
+
init_lib();
|
|
2088
|
+
|
|
2089
|
+
// src/logo.ts
|
|
2090
|
+
var import_figlet = __toESM(require("figlet"));
|
|
2091
|
+
var import_gradient_string = __toESM(require("gradient-string"));
|
|
2092
|
+
var DEFAULT_LOGO_CONFIG = {
|
|
2093
|
+
text: "QuickPeek",
|
|
2094
|
+
font: "Slant",
|
|
2095
|
+
horizontalLayout: "default",
|
|
2096
|
+
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2097
|
+
suffix: " \u{1F440}"
|
|
2098
|
+
};
|
|
2099
|
+
function renderLogo(config = {}) {
|
|
2100
|
+
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2101
|
+
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
2102
|
+
font: resolved.font,
|
|
2103
|
+
horizontalLayout: resolved.horizontalLayout
|
|
2104
|
+
});
|
|
2105
|
+
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
2106
|
+
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/utils/config-loader.ts
|
|
2110
|
+
var import_node_fs4 = require("fs");
|
|
2111
|
+
var import_promises5 = require("fs/promises");
|
|
2112
|
+
var import_node_os2 = require("os");
|
|
2113
|
+
var import_node_path7 = require("path");
|
|
2114
|
+
init_lib();
|
|
2115
|
+
async function loadConfig(_url, langOverride) {
|
|
2116
|
+
const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
|
|
2117
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2118
|
+
try {
|
|
2119
|
+
const content = await (0, import_promises5.readFile)(configPath, "utf-8");
|
|
2120
|
+
const userConfig = JSON.parse(content);
|
|
2121
|
+
const config = {
|
|
2122
|
+
lang: langOverride || userConfig.lang || "en",
|
|
2123
|
+
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2124
|
+
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2125
|
+
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2126
|
+
music: userConfig.music,
|
|
2127
|
+
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2128
|
+
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2129
|
+
};
|
|
2130
|
+
return { config, created: false };
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
const fullDefaultConfig = {
|
|
2135
|
+
lang: "en",
|
|
2136
|
+
plan: { maxSteps: 8 },
|
|
2137
|
+
video: {
|
|
2138
|
+
width: 1080,
|
|
2139
|
+
height: 1920,
|
|
2140
|
+
fps: 30,
|
|
2141
|
+
quality: 0.5,
|
|
2142
|
+
preset: "fast",
|
|
2143
|
+
cursor: true,
|
|
2144
|
+
highlight: false,
|
|
2145
|
+
fadeIn: 500,
|
|
2146
|
+
// ms
|
|
2147
|
+
fadeOut: 500,
|
|
2148
|
+
// ms
|
|
2149
|
+
contrast: 0.25
|
|
2150
|
+
},
|
|
2151
|
+
audio: {
|
|
2152
|
+
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2153
|
+
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2154
|
+
speed: 1
|
|
2155
|
+
},
|
|
2156
|
+
music: {
|
|
2157
|
+
path: "",
|
|
2158
|
+
volume: 0.3
|
|
2159
|
+
},
|
|
2160
|
+
output: { dir: "./demo" }
|
|
2161
|
+
};
|
|
2162
|
+
await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2163
|
+
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2164
|
+
}
|
|
2165
|
+
var WORK_DIR = ".quickpeek";
|
|
2166
|
+
async function prepareWorkDir() {
|
|
2167
|
+
const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
|
|
2168
|
+
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2169
|
+
await (0, import_promises5.rm)(workDir, { recursive: true });
|
|
2170
|
+
}
|
|
2171
|
+
await (0, import_promises5.mkdir)(workDir, { recursive: true });
|
|
2172
|
+
return workDir;
|
|
2173
|
+
}
|
|
2174
|
+
var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
|
|
2175
|
+
var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
|
|
2176
|
+
async function loadUserData() {
|
|
2177
|
+
try {
|
|
2178
|
+
if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
|
|
2179
|
+
return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2180
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
return {};
|
|
2184
|
+
}
|
|
2185
|
+
async function saveUserData(userData) {
|
|
2186
|
+
if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
|
|
2187
|
+
await (0, import_promises5.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2188
|
+
}
|
|
2189
|
+
const existing = await loadUserData();
|
|
2190
|
+
const merged = { ...existing, ...userData };
|
|
2191
|
+
await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2192
|
+
}
|
|
2193
|
+
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2194
|
+
font-family: Arial, sans-serif;
|
|
2195
|
+
-webkit-text-stroke: 3px #000000;
|
|
2196
|
+
}
|
|
2197
|
+
.caption .regular {
|
|
2198
|
+
font-size: 56px;
|
|
2199
|
+
color: #FFFFFF;
|
|
2200
|
+
}
|
|
2201
|
+
.caption .highlight {
|
|
2202
|
+
font-size: 64px;
|
|
2203
|
+
color: #FFFF00;
|
|
2204
|
+
}
|
|
2205
|
+
`;
|
|
2206
|
+
function parseCaptionCSS(css) {
|
|
2207
|
+
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2208
|
+
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
2209
|
+
const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
|
|
2210
|
+
const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
|
|
2211
|
+
const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
|
|
2212
|
+
if (fontMatch) style.font = fontMatch[1].trim();
|
|
2213
|
+
const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
|
|
2214
|
+
if (strokeMatch) {
|
|
2215
|
+
style.outlineWidth = parseInt(strokeMatch[1], 10);
|
|
2216
|
+
style.outline = rgbaToHex(strokeMatch[2]);
|
|
2217
|
+
}
|
|
2218
|
+
const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
|
|
2219
|
+
if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
|
|
2220
|
+
const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
|
|
2221
|
+
if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
|
|
2222
|
+
const parseWordStyle = (block, target) => {
|
|
2223
|
+
const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2224
|
+
if (colorMatch) target.color = colorMatch[1];
|
|
2225
|
+
const sizeMatch = block.match(/font-size:\s*(\d+)/);
|
|
2226
|
+
if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
|
|
2227
|
+
const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2228
|
+
if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2229
|
+
};
|
|
2230
|
+
parseWordStyle(regularBlock, style.regular);
|
|
2231
|
+
parseWordStyle(highlightBlock, style.highlight);
|
|
2232
|
+
if (!regularBlock && !highlightBlock) {
|
|
2233
|
+
const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2234
|
+
if (colorMatch) style.regular.color = colorMatch[1];
|
|
2235
|
+
const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
|
|
2236
|
+
if (sizeMatch) {
|
|
2237
|
+
style.regular.size = parseInt(sizeMatch[1], 10);
|
|
2238
|
+
style.highlight.size = Math.round(style.regular.size * 1.15);
|
|
2239
|
+
}
|
|
2240
|
+
const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
|
|
2241
|
+
if (highlightMatch) style.highlight.color = highlightMatch[1];
|
|
2242
|
+
const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2243
|
+
if (weightMatch) {
|
|
2244
|
+
const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2245
|
+
style.regular.bold = isBold;
|
|
2246
|
+
style.highlight.bold = isBold;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return style;
|
|
2250
|
+
}
|
|
2251
|
+
async function loadCaptions() {
|
|
2252
|
+
const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
|
|
2253
|
+
if (!(0, import_node_fs4.existsSync)(captionsPath)) {
|
|
2254
|
+
await (0, import_promises5.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2255
|
+
return DEFAULT_CAPTIONS;
|
|
2256
|
+
}
|
|
2257
|
+
try {
|
|
2258
|
+
const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
|
|
2259
|
+
return parseCaptionCSS(css);
|
|
2260
|
+
} catch {
|
|
2261
|
+
return DEFAULT_CAPTIONS;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
async function savePlan(plan, planPath) {
|
|
2265
|
+
const cleanPlan = {
|
|
2266
|
+
...plan,
|
|
2267
|
+
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2268
|
+
};
|
|
2269
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
|
|
2270
|
+
await (0, import_promises5.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/help.ts
|
|
2274
|
+
var cmd = import_chalk.default.cyan;
|
|
2275
|
+
var arg = import_chalk.default.green;
|
|
2276
|
+
var opt = import_chalk.default.magenta;
|
|
2277
|
+
var desc = import_chalk.default.white;
|
|
2278
|
+
var ex = import_chalk.default.dim;
|
|
2279
|
+
function pad(colored, visible, width) {
|
|
2280
|
+
return colored + " ".repeat(Math.max(0, width - visible.length));
|
|
2281
|
+
}
|
|
2282
|
+
function showLogo() {
|
|
2283
|
+
const logo = renderLogo();
|
|
2284
|
+
console.log(logo);
|
|
2285
|
+
console.log(import_chalk.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2286
|
+
console.log(import_chalk.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2287
|
+
}
|
|
2288
|
+
function formatUsageLines(status, tier) {
|
|
2289
|
+
const lines = [];
|
|
2290
|
+
lines.push(`Videos generated: ${status.usage_count}`);
|
|
2291
|
+
if (tier !== "pro" && status.trial) {
|
|
2292
|
+
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
2293
|
+
const used = max_uses - uses_remaining;
|
|
2294
|
+
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
2295
|
+
if (!is_expired && days_remaining > 0) {
|
|
2296
|
+
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (tier === "pro" && status.subscription) {
|
|
2300
|
+
const end = status.subscription.current_period_end;
|
|
2301
|
+
if (status.subscription.is_lifetime) {
|
|
2302
|
+
lines.push("Lifetime access");
|
|
2303
|
+
} else if (isCanceling(status)) {
|
|
2304
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2305
|
+
lines.push(`Cancels: ${date}`);
|
|
2306
|
+
} else if (end) {
|
|
2307
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2308
|
+
lines.push(`Renews: ${date}`);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
return lines;
|
|
2312
|
+
}
|
|
2313
|
+
async function showAccount() {
|
|
2314
|
+
const userData = await loadUserData();
|
|
2315
|
+
if (!userData.email) {
|
|
2316
|
+
console.log(import_chalk.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk.default.dim(" to sign in."));
|
|
2317
|
+
console.log();
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
const email = userData.email;
|
|
2321
|
+
const tier = userData.tier === "pro" ? "pro" : "free";
|
|
2322
|
+
const tierLabel = tier === "pro" ? import_chalk.default.green("Pro") : "Free";
|
|
2323
|
+
console.log(` ${tierLabel} plan ${import_chalk.default.dim("\xB7")} ${import_chalk.default.dim(email)}`);
|
|
2324
|
+
try {
|
|
2325
|
+
const status = await getStatusCached(email);
|
|
2326
|
+
for (const line of formatUsageLines(status, tier)) {
|
|
2327
|
+
console.log(import_chalk.default.dim(` ${line}`));
|
|
2328
|
+
}
|
|
2329
|
+
} catch {
|
|
2330
|
+
}
|
|
2331
|
+
console.log();
|
|
2332
|
+
}
|
|
2333
|
+
async function showHelp() {
|
|
2334
|
+
showLogo();
|
|
2335
|
+
await showAccount();
|
|
2336
|
+
const W = 28;
|
|
2337
|
+
const usage = [
|
|
2338
|
+
[`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
|
|
2339
|
+
[
|
|
2340
|
+
`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`,
|
|
2341
|
+
`qp <url> --script <file>`,
|
|
2342
|
+
"Use your own narration script"
|
|
2343
|
+
],
|
|
2344
|
+
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
2345
|
+
[`${cmd("qp edit")} ${arg("<url>")}`, `qp edit <url>`, "Edit the demo plan in the browser"],
|
|
2346
|
+
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
2347
|
+
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
2348
|
+
[`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
|
|
2349
|
+
[`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
|
|
2350
|
+
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
2351
|
+
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
2352
|
+
];
|
|
2353
|
+
const options = [
|
|
2354
|
+
[`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
|
|
2355
|
+
[
|
|
2356
|
+
`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`,
|
|
2357
|
+
`-d, --description "..."`,
|
|
2358
|
+
"Guide AI with an app description"
|
|
2359
|
+
],
|
|
2360
|
+
[
|
|
2361
|
+
`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`,
|
|
2362
|
+
`-s, --script <file>`,
|
|
2363
|
+
"Narration script (one caption per line)"
|
|
2364
|
+
],
|
|
2365
|
+
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
2366
|
+
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
2367
|
+
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
|
|
2368
|
+
];
|
|
2369
|
+
const examples = [
|
|
2370
|
+
`${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
|
|
2371
|
+
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
|
|
2372
|
+
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
|
|
2373
|
+
`${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
|
|
2374
|
+
];
|
|
2375
|
+
console.log();
|
|
2376
|
+
console.log(import_chalk.default.yellow.bold("USAGE"));
|
|
2377
|
+
for (const [colored, plain, d] of usage) {
|
|
2378
|
+
console.log(` ${pad(colored, plain, W)}${desc(d)}`);
|
|
2379
|
+
}
|
|
2380
|
+
console.log();
|
|
2381
|
+
console.log(import_chalk.default.yellow.bold("OPTIONS"));
|
|
2382
|
+
for (const [colored, plain, d] of options) {
|
|
2383
|
+
console.log(` ${pad(colored, plain, W)}${desc(d)}`);
|
|
2384
|
+
}
|
|
2385
|
+
console.log();
|
|
2386
|
+
console.log(import_chalk.default.yellow.bold("EXAMPLES"));
|
|
2387
|
+
for (const e of examples) {
|
|
2388
|
+
console.log(` ${e}`);
|
|
2389
|
+
}
|
|
2390
|
+
console.log();
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/utils/ui.ts
|
|
2394
|
+
var import_node_path8 = require("path");
|
|
2395
|
+
var import_node_readline = require("readline");
|
|
2396
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
2397
|
+
var import_ora = __toESM(require("ora"));
|
|
2398
|
+
var INDENT = " ";
|
|
2399
|
+
var autoYes = false;
|
|
2400
|
+
function setAutoYes(value) {
|
|
2401
|
+
autoYes = value;
|
|
2402
|
+
}
|
|
2403
|
+
function task(text) {
|
|
2404
|
+
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2405
|
+
}
|
|
2406
|
+
function header(text) {
|
|
2407
|
+
console.log();
|
|
2408
|
+
console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
|
|
2409
|
+
}
|
|
2410
|
+
function info(text) {
|
|
2411
|
+
console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
|
|
2412
|
+
}
|
|
2413
|
+
function confirm(prompt2) {
|
|
2414
|
+
if (autoYes) return Promise.resolve(true);
|
|
2415
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2416
|
+
return new Promise((res) => {
|
|
2417
|
+
rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
|
|
2418
|
+
rl.close();
|
|
2419
|
+
res(answer.toLowerCase() !== "n");
|
|
2420
|
+
});
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
function promptInput(prompt2) {
|
|
2424
|
+
if (autoYes) return Promise.resolve("");
|
|
2425
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2426
|
+
return new Promise((res) => {
|
|
2427
|
+
rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
|
|
2428
|
+
rl.close();
|
|
2429
|
+
res(answer.trim());
|
|
2430
|
+
});
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2434
|
+
function formatClickablePath(filePath) {
|
|
2435
|
+
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
2436
|
+
if (_isWSL || process.platform !== "win32") {
|
|
2437
|
+
return absolutePath;
|
|
2438
|
+
}
|
|
2439
|
+
let formatted = absolutePath;
|
|
2440
|
+
formatted = formatted.replace(/\//g, "\\");
|
|
2441
|
+
if (formatted.includes(" ") && !formatted.startsWith('"')) {
|
|
2442
|
+
formatted = `"${formatted}"`;
|
|
2443
|
+
}
|
|
2444
|
+
return formatted;
|
|
2445
|
+
}
|
|
2446
|
+
function formatDuration(ms) {
|
|
2447
|
+
const mins = Math.floor(ms / 6e4);
|
|
2448
|
+
const secs = Math.floor(ms % 6e4 / 1e3);
|
|
2449
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/commands/auth-cmd.ts
|
|
2453
|
+
function waitForEnter(prompt2) {
|
|
2454
|
+
return new Promise((resolve9) => {
|
|
2455
|
+
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2456
|
+
rl.question(prompt2, () => {
|
|
2457
|
+
rl.close();
|
|
2458
|
+
resolve9();
|
|
2459
|
+
});
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
async function authCommand(url) {
|
|
2463
|
+
console.log();
|
|
2464
|
+
showLogo();
|
|
2465
|
+
if (!authStatePath(url)) {
|
|
2466
|
+
console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
console.log(import_chalk3.default.white(" A browser will open. Sign in to the site, then come back here."));
|
|
2470
|
+
console.log();
|
|
2471
|
+
const browser = await import_playwright2.chromium.launch({ headless: false });
|
|
2472
|
+
const context = await browser.newContext();
|
|
2473
|
+
const page = await context.newPage();
|
|
2474
|
+
try {
|
|
2475
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2476
|
+
} catch {
|
|
2477
|
+
}
|
|
2478
|
+
await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
|
|
2479
|
+
try {
|
|
2480
|
+
const state = await context.storageState();
|
|
2481
|
+
const path = await saveAuthState(url, state);
|
|
2482
|
+
console.log();
|
|
2483
|
+
console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
|
|
2484
|
+
console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
|
|
2485
|
+
console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
|
|
2486
|
+
console.log(import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan(`qp auth clear ${new URL(url).host}`) + import_chalk3.default.dim(" to forget it."));
|
|
2487
|
+
} finally {
|
|
2488
|
+
await browser.close().catch(() => {
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
console.log();
|
|
2492
|
+
}
|
|
2493
|
+
async function authClearCommand(url) {
|
|
2494
|
+
console.log();
|
|
2495
|
+
const removed = await clearAuthState(url);
|
|
2496
|
+
if (removed.length === 0) {
|
|
2497
|
+
console.log(import_chalk3.default.dim(url ? ` No saved session for ${url}` : " No saved sessions."));
|
|
2498
|
+
} else {
|
|
2499
|
+
for (const host of removed) {
|
|
2500
|
+
console.log(import_chalk3.default.green(` \u2713 Forgot session for ${host}`));
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
console.log();
|
|
2504
|
+
}
|
|
2505
|
+
async function authListCommand() {
|
|
2506
|
+
console.log();
|
|
2507
|
+
const hosts = await listAuthStates();
|
|
2508
|
+
if (hosts.length === 0) {
|
|
2509
|
+
console.log(import_chalk3.default.dim(" No saved sessions. Run ") + import_chalk3.default.cyan("qp auth <url>") + import_chalk3.default.dim(" to add one."));
|
|
2510
|
+
} else {
|
|
2511
|
+
console.log(import_chalk3.default.white(" Saved sessions:"));
|
|
2512
|
+
for (const host of hosts) {
|
|
2513
|
+
console.log(` ${import_chalk3.default.green("\u25CF")} ${host}`);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
console.log();
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/commands/capture-cmd.ts
|
|
2520
|
+
var import_node_path20 = require("path");
|
|
2521
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
2522
|
+
|
|
2523
|
+
// src/ai/narrate.ts
|
|
2524
|
+
init_lib();
|
|
2525
|
+
|
|
2526
|
+
// src/ai/client.ts
|
|
2527
|
+
var import_node_readline3 = require("readline");
|
|
2528
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2529
|
+
init_lib();
|
|
2530
|
+
|
|
2531
|
+
// src/utils/browser-auth.ts
|
|
2532
|
+
var import_node_http = require("http");
|
|
2533
|
+
var import_node_url = require("url");
|
|
2534
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
2535
|
+
|
|
2536
|
+
// src/lib/browser-open.ts
|
|
2537
|
+
var import_node_child_process3 = require("child_process");
|
|
2538
|
+
function editorBaseUrl() {
|
|
2539
|
+
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
2540
|
+
}
|
|
2541
|
+
function openInBrowser(url) {
|
|
2542
|
+
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2543
|
+
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2544
|
+
return new Promise((resolve9) => {
|
|
2545
|
+
(0, import_node_child_process3.exec)(command, () => resolve9());
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// src/utils/browser-auth.ts
|
|
2550
|
+
init_SparkStripe_feature_gating();
|
|
2551
|
+
var TIMEOUT_MS = 12e4;
|
|
2552
|
+
async function browserLogin() {
|
|
2553
|
+
return new Promise((resolve9) => {
|
|
2554
|
+
let server;
|
|
2555
|
+
let timer;
|
|
2556
|
+
const cleanup = (email) => {
|
|
2557
|
+
clearTimeout(timer);
|
|
2558
|
+
server?.close();
|
|
2559
|
+
resolve9(email);
|
|
2560
|
+
};
|
|
2561
|
+
server = (0, import_node_http.createServer)((req, res) => {
|
|
2562
|
+
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2563
|
+
const path = url.pathname;
|
|
2564
|
+
if (path === "/callback" || path === "/") {
|
|
2565
|
+
const email = url.searchParams.get("email");
|
|
2566
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2567
|
+
res.end(`
|
|
2568
|
+
<!DOCTYPE html>
|
|
2569
|
+
<html>
|
|
2570
|
+
<head><title>QuickPeek</title><style>
|
|
2571
|
+
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
|
|
2572
|
+
.card { text-align: center; padding: 2rem; }
|
|
2573
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
2574
|
+
p { color: #888; }
|
|
2575
|
+
</style></head>
|
|
2576
|
+
<body><div class="card">
|
|
2577
|
+
<h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
|
|
2578
|
+
<p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
|
|
2579
|
+
<p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
|
|
2580
|
+
</div></body>
|
|
2581
|
+
</html>
|
|
2582
|
+
`);
|
|
2583
|
+
if (email) {
|
|
2584
|
+
cleanup(email);
|
|
2585
|
+
}
|
|
2586
|
+
} else {
|
|
2587
|
+
res.writeHead(404);
|
|
2588
|
+
res.end();
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2591
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2592
|
+
const addr = server.address();
|
|
2593
|
+
if (!addr || typeof addr === "string") {
|
|
2594
|
+
cleanup(null);
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
const port = addr.port;
|
|
2598
|
+
const returnUrl = `http://localhost:${port}/callback`;
|
|
2599
|
+
const url = pricingUrl({ returnUrl });
|
|
2600
|
+
console.log();
|
|
2601
|
+
console.log(import_chalk4.default.cyan(" Opening browser to sign in..."));
|
|
2602
|
+
console.log(import_chalk4.default.dim(` ${url}`));
|
|
2603
|
+
console.log();
|
|
2604
|
+
console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2605
|
+
void openInBrowser(url);
|
|
2606
|
+
});
|
|
2607
|
+
timer = setTimeout(() => {
|
|
2608
|
+
console.log();
|
|
2609
|
+
console.log(import_chalk4.default.yellow(" Sign-in timed out."));
|
|
2610
|
+
cleanup(null);
|
|
2611
|
+
}, TIMEOUT_MS);
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// src/ai/client.ts
|
|
2616
|
+
var lastRateLimitInfo = null;
|
|
2617
|
+
function getLastRateLimitInfo() {
|
|
2618
|
+
return lastRateLimitInfo;
|
|
2619
|
+
}
|
|
2620
|
+
function prompt(question) {
|
|
2621
|
+
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2622
|
+
return new Promise((resolve9) => {
|
|
2623
|
+
rl.question(question, (answer) => {
|
|
2624
|
+
rl.close();
|
|
2625
|
+
resolve9(answer.trim());
|
|
2626
|
+
});
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
async function setupUserEmail() {
|
|
2630
|
+
console.log();
|
|
2631
|
+
console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
|
|
2632
|
+
console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
|
|
2633
|
+
console.log();
|
|
2634
|
+
console.log(import_chalk5.default.white(" Sign in with your Google account to track usage"));
|
|
2635
|
+
console.log(import_chalk5.default.white(" and unlock Pro features."));
|
|
2636
|
+
const browserEmail = await browserLogin();
|
|
2637
|
+
if (browserEmail) {
|
|
2638
|
+
console.log();
|
|
2639
|
+
console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
|
|
2640
|
+
console.log();
|
|
2641
|
+
return browserEmail;
|
|
2642
|
+
}
|
|
2643
|
+
console.log();
|
|
2644
|
+
console.log(import_chalk5.default.dim(" Or enter your email manually:"));
|
|
2645
|
+
const email = await prompt(import_chalk5.default.white(" Email: "));
|
|
2646
|
+
if (!email) {
|
|
2647
|
+
console.log();
|
|
2648
|
+
console.log(import_chalk5.default.red(" No email provided. Exiting."));
|
|
2649
|
+
process.exit(1);
|
|
2650
|
+
}
|
|
2651
|
+
if (!email.includes("@") || !email.includes(".")) {
|
|
2652
|
+
console.log();
|
|
2653
|
+
console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
2654
|
+
const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
|
|
2655
|
+
if (proceed.toLowerCase() !== "y") {
|
|
2656
|
+
process.exit(1);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
console.log();
|
|
2660
|
+
console.log(import_chalk5.default.green(" \u2713 Email registered"));
|
|
2661
|
+
console.log();
|
|
2662
|
+
return email;
|
|
2663
|
+
}
|
|
2664
|
+
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2665
|
+
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
|
|
2666
|
+
if (!result.success) {
|
|
2667
|
+
await logError2("AI API", new Error(result.error));
|
|
2668
|
+
if (result.isRateLimit) {
|
|
2669
|
+
console.log();
|
|
2670
|
+
console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2671
|
+
console.log();
|
|
2672
|
+
console.log(import_chalk5.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2673
|
+
console.log(import_chalk5.default.dim(" Options:"));
|
|
2674
|
+
if (result.retryAfterSeconds) {
|
|
2675
|
+
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2676
|
+
console.log(import_chalk5.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2677
|
+
} else {
|
|
2678
|
+
console.log(import_chalk5.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2679
|
+
}
|
|
2680
|
+
console.log(import_chalk5.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2681
|
+
console.log(import_chalk5.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2682
|
+
console.log();
|
|
2683
|
+
process.exit(1);
|
|
2684
|
+
}
|
|
2685
|
+
if (result.isAuthError) {
|
|
2686
|
+
console.log();
|
|
2687
|
+
console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
|
|
2688
|
+
console.log();
|
|
2689
|
+
console.log(import_chalk5.default.dim(` ${result.error}`));
|
|
2690
|
+
console.log();
|
|
2691
|
+
process.exit(1);
|
|
2692
|
+
}
|
|
2693
|
+
console.log();
|
|
2694
|
+
console.log(import_chalk5.default.red(" \u26A0 AI Error"));
|
|
2695
|
+
console.log();
|
|
2696
|
+
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2697
|
+
console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
|
|
2698
|
+
console.log();
|
|
2699
|
+
process.exit(1);
|
|
2700
|
+
}
|
|
2701
|
+
lastRateLimitInfo = result.limits;
|
|
2702
|
+
return result.content;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// src/ai/narrate.ts
|
|
2706
|
+
async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
|
|
2707
|
+
const langName = getLanguageName(lang);
|
|
2708
|
+
const stepList = steps.map((s, i) => {
|
|
2709
|
+
const parts = [`${i + 1}. ${s.action}`];
|
|
2710
|
+
if (s.target) parts.push(`on "${s.target}"`);
|
|
2711
|
+
if (s.value) parts.push(`= "${s.value}"`);
|
|
2712
|
+
return parts.join(" ");
|
|
2713
|
+
}).join("\n");
|
|
2714
|
+
const systemPrompt = `You write professional demo video captions in ${langName}.
|
|
2715
|
+
Given UI steps, return a JSON array of captions \u2014 one per step, in order.
|
|
2716
|
+
|
|
2205
2717
|
RULES:
|
|
2206
2718
|
- Return ONLY a valid JSON array of strings, no explanation
|
|
2207
2719
|
- Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
|
|
@@ -2372,27 +2884,27 @@ function normalizeUrl2(url) {
|
|
|
2372
2884
|
}
|
|
2373
2885
|
|
|
2374
2886
|
// src/capture.ts
|
|
2375
|
-
var
|
|
2887
|
+
var import_node_child_process5 = require("child_process");
|
|
2376
2888
|
var import_node_http3 = require("http");
|
|
2377
|
-
var
|
|
2378
|
-
var
|
|
2889
|
+
var import_node_readline4 = require("readline");
|
|
2890
|
+
var import_playwright4 = require("playwright");
|
|
2379
2891
|
|
|
2380
2892
|
// src/browser/wsl.ts
|
|
2381
|
-
var
|
|
2382
|
-
var
|
|
2893
|
+
var import_node_child_process4 = require("child_process");
|
|
2894
|
+
var import_node_fs5 = require("fs");
|
|
2383
2895
|
var import_node_http2 = require("http");
|
|
2384
|
-
var
|
|
2896
|
+
var import_playwright3 = require("playwright");
|
|
2385
2897
|
function isWSL() {
|
|
2386
2898
|
if (process.platform !== "linux") return false;
|
|
2387
2899
|
try {
|
|
2388
|
-
const r = (0,
|
|
2900
|
+
const r = (0, import_node_child_process4.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2389
2901
|
return r.includes("microsoft") || r.includes("wsl");
|
|
2390
2902
|
} catch {
|
|
2391
2903
|
return false;
|
|
2392
2904
|
}
|
|
2393
2905
|
}
|
|
2394
2906
|
function tryEnableWSLInterop() {
|
|
2395
|
-
return (0,
|
|
2907
|
+
return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2396
2908
|
}
|
|
2397
2909
|
function wslToWindowsPath(wslPath) {
|
|
2398
2910
|
if (!wslPath) return wslPath;
|
|
@@ -2401,9 +2913,9 @@ function wslToWindowsPath(wslPath) {
|
|
|
2401
2913
|
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2402
2914
|
}
|
|
2403
2915
|
function findExe(whereExeQuery, ...hardcoded) {
|
|
2404
|
-
for (const p of hardcoded) if ((0,
|
|
2916
|
+
for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
|
|
2405
2917
|
try {
|
|
2406
|
-
const line = (0,
|
|
2918
|
+
const line = (0, import_node_child_process4.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2407
2919
|
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2408
2920
|
} catch {
|
|
2409
2921
|
}
|
|
@@ -2417,14 +2929,14 @@ var findWindowsChrome = () => findExe(
|
|
|
2417
2929
|
var findWindowsNode = () => findExe("node.exe");
|
|
2418
2930
|
function getWindowsHostIP() {
|
|
2419
2931
|
try {
|
|
2420
|
-
const m = (0,
|
|
2932
|
+
const m = (0, import_node_child_process4.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2421
2933
|
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2422
2934
|
);
|
|
2423
2935
|
if (m?.[1]) return m[1];
|
|
2424
2936
|
} catch {
|
|
2425
2937
|
}
|
|
2426
2938
|
try {
|
|
2427
|
-
const m = (0,
|
|
2939
|
+
const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2428
2940
|
if (m?.[1]) return m[1];
|
|
2429
2941
|
} catch {
|
|
2430
2942
|
}
|
|
@@ -2433,7 +2945,7 @@ function getWindowsHostIP() {
|
|
|
2433
2945
|
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2434
2946
|
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2435
2947
|
return new Promise((resolve9, reject) => {
|
|
2436
|
-
const proc = (0,
|
|
2948
|
+
const proc = (0, import_node_child_process4.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2437
2949
|
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2438
2950
|
let buf = "";
|
|
2439
2951
|
proc.stdout.on("data", (d) => {
|
|
@@ -2480,7 +2992,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
2480
2992
|
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2481
2993
|
const winUserData = wslToWindowsPath(userDataDir);
|
|
2482
2994
|
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2483
|
-
const chromeProc = (0,
|
|
2995
|
+
const chromeProc = (0, import_node_child_process4.spawn)(
|
|
2484
2996
|
chromePath,
|
|
2485
2997
|
[
|
|
2486
2998
|
`--remote-debugging-port=${debugPort}`,
|
|
@@ -2500,7 +3012,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
2500
3012
|
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2501
3013
|
try {
|
|
2502
3014
|
await waitForChrome(windowsHost, proxyPort);
|
|
2503
|
-
const browser = await
|
|
3015
|
+
const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2504
3016
|
return {
|
|
2505
3017
|
browser,
|
|
2506
3018
|
cleanup: async () => {
|
|
@@ -2629,9 +3141,9 @@ var EVENT_TRACKER_SCRIPT = `
|
|
|
2629
3141
|
}, { passive: true, capture: true });
|
|
2630
3142
|
}());
|
|
2631
3143
|
`;
|
|
2632
|
-
function
|
|
3144
|
+
function waitForEnter2() {
|
|
2633
3145
|
return new Promise((resolve9) => {
|
|
2634
|
-
const rl = (0,
|
|
3146
|
+
const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2635
3147
|
process.stdout.write(" Press Enter when done...\n");
|
|
2636
3148
|
rl.once("line", () => {
|
|
2637
3149
|
rl.close();
|
|
@@ -2660,13 +3172,13 @@ async function launchCaptureChrome() {
|
|
|
2660
3172
|
req.end();
|
|
2661
3173
|
});
|
|
2662
3174
|
if (alreadyUp) {
|
|
2663
|
-
const browser = await
|
|
3175
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
|
|
2664
3176
|
return { browser, relay: probe.proc };
|
|
2665
3177
|
}
|
|
2666
3178
|
probe.proc.kill();
|
|
2667
|
-
const winTemp = (0,
|
|
3179
|
+
const winTemp = (0, import_node_child_process5.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
|
|
2668
3180
|
const userDataDir = `${winTemp}\\qp-capture`;
|
|
2669
|
-
(0,
|
|
3181
|
+
(0, import_node_child_process5.spawn)(
|
|
2670
3182
|
chromePath,
|
|
2671
3183
|
[
|
|
2672
3184
|
`--remote-debugging-port=${debugPort}`,
|
|
@@ -2680,7 +3192,7 @@ async function launchCaptureChrome() {
|
|
|
2680
3192
|
const relay = await startRelay(nodePath, windowsHost, debugPort);
|
|
2681
3193
|
try {
|
|
2682
3194
|
await waitForChrome(windowsHost, relay.port);
|
|
2683
|
-
const browser = await
|
|
3195
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
|
|
2684
3196
|
return { browser, relay: relay.proc };
|
|
2685
3197
|
} catch (err) {
|
|
2686
3198
|
relay.proc.kill();
|
|
@@ -2695,7 +3207,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2695
3207
|
if (isWSL()) {
|
|
2696
3208
|
({ browser, relay } = await launchCaptureChrome());
|
|
2697
3209
|
} else {
|
|
2698
|
-
browser = await
|
|
3210
|
+
browser = await import_playwright4.chromium.launch({
|
|
2699
3211
|
headless: false,
|
|
2700
3212
|
args: ["--start-maximized", "--no-first-run"]
|
|
2701
3213
|
});
|
|
@@ -2737,7 +3249,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2737
3249
|
return [title, parts.join(" | ")];
|
|
2738
3250
|
}).catch(() => ["", ""]);
|
|
2739
3251
|
startTime = Date.now();
|
|
2740
|
-
await
|
|
3252
|
+
await waitForEnter2();
|
|
2741
3253
|
const durationMs = Date.now() - startTime;
|
|
2742
3254
|
if (relay) {
|
|
2743
3255
|
try {
|
|
@@ -2752,195 +3264,31 @@ async function captureUserSession(url, _workDir) {
|
|
|
2752
3264
|
return { events, durationMs, pageTitle, pageContext };
|
|
2753
3265
|
}
|
|
2754
3266
|
|
|
2755
|
-
// src/utils/config-loader.ts
|
|
2756
|
-
var import_node_fs4 = require("fs");
|
|
2757
|
-
var import_promises4 = require("fs/promises");
|
|
2758
|
-
var import_node_os = require("os");
|
|
2759
|
-
var import_node_path6 = require("path");
|
|
2760
|
-
init_lib();
|
|
2761
|
-
async function loadConfig(_url, langOverride) {
|
|
2762
|
-
const configPath = (0, import_node_path6.join)(process.cwd(), CONFIG_FILE);
|
|
2763
|
-
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2764
|
-
try {
|
|
2765
|
-
const content = await (0, import_promises4.readFile)(configPath, "utf-8");
|
|
2766
|
-
const userConfig = JSON.parse(content);
|
|
2767
|
-
const config = {
|
|
2768
|
-
lang: langOverride || userConfig.lang || "en",
|
|
2769
|
-
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2770
|
-
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2771
|
-
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2772
|
-
music: userConfig.music,
|
|
2773
|
-
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2774
|
-
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2775
|
-
};
|
|
2776
|
-
return { config, created: false };
|
|
2777
|
-
} catch {
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
const fullDefaultConfig = {
|
|
2781
|
-
lang: "en",
|
|
2782
|
-
plan: { maxSteps: 8 },
|
|
2783
|
-
video: {
|
|
2784
|
-
width: 1080,
|
|
2785
|
-
height: 1920,
|
|
2786
|
-
fps: 30,
|
|
2787
|
-
quality: 0.5,
|
|
2788
|
-
preset: "fast",
|
|
2789
|
-
cursor: true,
|
|
2790
|
-
highlight: false,
|
|
2791
|
-
fadeIn: 500,
|
|
2792
|
-
// ms
|
|
2793
|
-
fadeOut: 500,
|
|
2794
|
-
// ms
|
|
2795
|
-
contrast: 0.25
|
|
2796
|
-
},
|
|
2797
|
-
audio: {
|
|
2798
|
-
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2799
|
-
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2800
|
-
speed: 1
|
|
2801
|
-
},
|
|
2802
|
-
music: {
|
|
2803
|
-
path: "",
|
|
2804
|
-
volume: 0.3
|
|
2805
|
-
},
|
|
2806
|
-
output: { dir: "./demo" }
|
|
2807
|
-
};
|
|
2808
|
-
await (0, import_promises4.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2809
|
-
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2810
|
-
}
|
|
2811
|
-
var WORK_DIR = ".quickpeek";
|
|
2812
|
-
async function prepareWorkDir() {
|
|
2813
|
-
const workDir = (0, import_node_path6.join)(process.cwd(), WORK_DIR);
|
|
2814
|
-
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2815
|
-
await (0, import_promises4.rm)(workDir, { recursive: true });
|
|
2816
|
-
}
|
|
2817
|
-
await (0, import_promises4.mkdir)(workDir, { recursive: true });
|
|
2818
|
-
return workDir;
|
|
2819
|
-
}
|
|
2820
|
-
var USER_DATA_DIR = (0, import_node_path6.join)((0, import_node_os.homedir)(), ".quickpeek");
|
|
2821
|
-
var USER_DATA_FILE = (0, import_node_path6.join)(USER_DATA_DIR, "user.json");
|
|
2822
|
-
async function loadUserData() {
|
|
2823
|
-
try {
|
|
2824
|
-
if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
|
|
2825
|
-
return JSON.parse(await (0, import_promises4.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2826
|
-
}
|
|
2827
|
-
} catch {
|
|
2828
|
-
}
|
|
2829
|
-
return {};
|
|
2830
|
-
}
|
|
2831
|
-
async function saveUserData(userData) {
|
|
2832
|
-
if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
|
|
2833
|
-
await (0, import_promises4.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2834
|
-
}
|
|
2835
|
-
const existing = await loadUserData();
|
|
2836
|
-
const merged = { ...existing, ...userData };
|
|
2837
|
-
await (0, import_promises4.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2838
|
-
}
|
|
2839
|
-
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2840
|
-
font-family: Arial, sans-serif;
|
|
2841
|
-
-webkit-text-stroke: 3px #000000;
|
|
2842
|
-
}
|
|
2843
|
-
.caption .regular {
|
|
2844
|
-
font-size: 56px;
|
|
2845
|
-
color: #FFFFFF;
|
|
2846
|
-
}
|
|
2847
|
-
.caption .highlight {
|
|
2848
|
-
font-size: 64px;
|
|
2849
|
-
color: #FFFF00;
|
|
2850
|
-
}
|
|
2851
|
-
`;
|
|
2852
|
-
function parseCaptionCSS(css) {
|
|
2853
|
-
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2854
|
-
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
2855
|
-
const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
|
|
2856
|
-
const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
|
|
2857
|
-
const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
|
|
2858
|
-
if (fontMatch) style.font = fontMatch[1].trim();
|
|
2859
|
-
const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
|
|
2860
|
-
if (strokeMatch) {
|
|
2861
|
-
style.outlineWidth = parseInt(strokeMatch[1], 10);
|
|
2862
|
-
style.outline = rgbaToHex(strokeMatch[2]);
|
|
2863
|
-
}
|
|
2864
|
-
const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
|
|
2865
|
-
if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
|
|
2866
|
-
const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
|
|
2867
|
-
if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
|
|
2868
|
-
const parseWordStyle = (block, target) => {
|
|
2869
|
-
const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2870
|
-
if (colorMatch) target.color = colorMatch[1];
|
|
2871
|
-
const sizeMatch = block.match(/font-size:\s*(\d+)/);
|
|
2872
|
-
if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
|
|
2873
|
-
const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2874
|
-
if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2875
|
-
};
|
|
2876
|
-
parseWordStyle(regularBlock, style.regular);
|
|
2877
|
-
parseWordStyle(highlightBlock, style.highlight);
|
|
2878
|
-
if (!regularBlock && !highlightBlock) {
|
|
2879
|
-
const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2880
|
-
if (colorMatch) style.regular.color = colorMatch[1];
|
|
2881
|
-
const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
|
|
2882
|
-
if (sizeMatch) {
|
|
2883
|
-
style.regular.size = parseInt(sizeMatch[1], 10);
|
|
2884
|
-
style.highlight.size = Math.round(style.regular.size * 1.15);
|
|
2885
|
-
}
|
|
2886
|
-
const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
|
|
2887
|
-
if (highlightMatch) style.highlight.color = highlightMatch[1];
|
|
2888
|
-
const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2889
|
-
if (weightMatch) {
|
|
2890
|
-
const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2891
|
-
style.regular.bold = isBold;
|
|
2892
|
-
style.highlight.bold = isBold;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
return style;
|
|
2896
|
-
}
|
|
2897
|
-
async function loadCaptions() {
|
|
2898
|
-
const captionsPath = (0, import_node_path6.join)(process.cwd(), "captions.css");
|
|
2899
|
-
if (!(0, import_node_fs4.existsSync)(captionsPath)) {
|
|
2900
|
-
await (0, import_promises4.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2901
|
-
return DEFAULT_CAPTIONS;
|
|
2902
|
-
}
|
|
2903
|
-
try {
|
|
2904
|
-
const css = await (0, import_promises4.readFile)(captionsPath, "utf-8");
|
|
2905
|
-
return parseCaptionCSS(css);
|
|
2906
|
-
} catch {
|
|
2907
|
-
return DEFAULT_CAPTIONS;
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
async function savePlan(plan, planPath) {
|
|
2911
|
-
const cleanPlan = {
|
|
2912
|
-
...plan,
|
|
2913
|
-
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2914
|
-
};
|
|
2915
|
-
await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(planPath), { recursive: true });
|
|
2916
|
-
await (0, import_promises4.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
3267
|
// src/utils/logging.ts
|
|
2920
|
-
var
|
|
2921
|
-
var
|
|
3268
|
+
var import_promises6 = require("fs/promises");
|
|
3269
|
+
var import_node_path9 = require("path");
|
|
2922
3270
|
var LOG_FILE_NAME = "quickpeek.log";
|
|
2923
3271
|
var logFileInitialized = false;
|
|
2924
3272
|
var logFilePath = LOG_FILE_NAME;
|
|
2925
3273
|
async function writeLogEntry(entry) {
|
|
2926
3274
|
try {
|
|
2927
3275
|
if (!logFileInitialized) {
|
|
2928
|
-
await (0,
|
|
3276
|
+
await (0, import_promises6.writeFile)(logFilePath, entry);
|
|
2929
3277
|
logFileInitialized = true;
|
|
2930
3278
|
} else {
|
|
2931
|
-
await (0,
|
|
3279
|
+
await (0, import_promises6.appendFile)(logFilePath, entry);
|
|
2932
3280
|
}
|
|
2933
3281
|
} catch {
|
|
2934
3282
|
}
|
|
2935
3283
|
}
|
|
2936
3284
|
async function setLogFilePath(outputDir) {
|
|
2937
|
-
logFilePath = (0,
|
|
3285
|
+
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
2938
3286
|
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2939
3287
|
${"=".repeat(60)}
|
|
2940
3288
|
|
|
2941
3289
|
`;
|
|
2942
3290
|
try {
|
|
2943
|
-
await (0,
|
|
3291
|
+
await (0, import_promises6.writeFile)(logFilePath, header2);
|
|
2944
3292
|
logFileInitialized = true;
|
|
2945
3293
|
} catch {
|
|
2946
3294
|
}
|
|
@@ -2954,115 +3302,56 @@ async function logDebug(context, message) {
|
|
|
2954
3302
|
`);
|
|
2955
3303
|
}
|
|
2956
3304
|
async function logError(context, error) {
|
|
2957
|
-
let errorMsg;
|
|
2958
|
-
if (error instanceof Error) {
|
|
2959
|
-
errorMsg = `${error.message}
|
|
2960
|
-
${error.stack}`;
|
|
2961
|
-
} else if (typeof error === "object" && error !== null) {
|
|
2962
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
2963
|
-
} else {
|
|
2964
|
-
errorMsg = String(error);
|
|
2965
|
-
}
|
|
2966
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
2967
|
-
${errorMsg}
|
|
2968
|
-
|
|
2969
|
-
`);
|
|
2970
|
-
}
|
|
2971
|
-
async function logStep(stepNum, action, details) {
|
|
2972
|
-
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
2973
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
2974
|
-
`);
|
|
2975
|
-
}
|
|
2976
|
-
|
|
2977
|
-
// src/utils/ui.ts
|
|
2978
|
-
var import_node_path8 = require("path");
|
|
2979
|
-
var import_node_readline3 = require("readline");
|
|
2980
|
-
var import_chalk3 = __toESM(require("chalk"));
|
|
2981
|
-
var import_ora = __toESM(require("ora"));
|
|
2982
|
-
var INDENT = " ";
|
|
2983
|
-
var autoYes = false;
|
|
2984
|
-
function setAutoYes(value) {
|
|
2985
|
-
autoYes = value;
|
|
2986
|
-
}
|
|
2987
|
-
function task(text) {
|
|
2988
|
-
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2989
|
-
}
|
|
2990
|
-
function header(text) {
|
|
2991
|
-
console.log();
|
|
2992
|
-
console.log(import_chalk3.default.bold.cyan(`\u25A0 ${text}`));
|
|
2993
|
-
}
|
|
2994
|
-
function info(text) {
|
|
2995
|
-
console.log(`${INDENT}${import_chalk3.default.dim("\u2502")} ${import_chalk3.default.dim(text)}`);
|
|
2996
|
-
}
|
|
2997
|
-
function confirm(prompt2) {
|
|
2998
|
-
if (autoYes) return Promise.resolve(true);
|
|
2999
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3000
|
-
return new Promise((res) => {
|
|
3001
|
-
rl.question(import_chalk3.default.yellow(`${prompt2} [Y/n] `), (answer) => {
|
|
3002
|
-
rl.close();
|
|
3003
|
-
res(answer.toLowerCase() !== "n");
|
|
3004
|
-
});
|
|
3005
|
-
});
|
|
3006
|
-
}
|
|
3007
|
-
function promptInput(prompt2) {
|
|
3008
|
-
if (autoYes) return Promise.resolve("");
|
|
3009
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3010
|
-
return new Promise((res) => {
|
|
3011
|
-
rl.question(import_chalk3.default.cyan(`${INDENT}${prompt2}`), (answer) => {
|
|
3012
|
-
rl.close();
|
|
3013
|
-
res(answer.trim());
|
|
3014
|
-
});
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
3018
|
-
function formatClickablePath(filePath) {
|
|
3019
|
-
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
3020
|
-
if (_isWSL) {
|
|
3021
|
-
return absolutePath;
|
|
3022
|
-
}
|
|
3023
|
-
let formatted = absolutePath;
|
|
3024
|
-
formatted = formatted.replace(/\//g, "\\");
|
|
3025
|
-
if (formatted.includes(" ") && !formatted.startsWith('"')) {
|
|
3026
|
-
formatted = `"${formatted}"`;
|
|
3305
|
+
let errorMsg;
|
|
3306
|
+
if (error instanceof Error) {
|
|
3307
|
+
errorMsg = `${error.message}
|
|
3308
|
+
${error.stack}`;
|
|
3309
|
+
} else if (typeof error === "object" && error !== null) {
|
|
3310
|
+
errorMsg = JSON.stringify(error, null, 2);
|
|
3311
|
+
} else {
|
|
3312
|
+
errorMsg = String(error);
|
|
3027
3313
|
}
|
|
3028
|
-
|
|
3314
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3315
|
+
${errorMsg}
|
|
3316
|
+
|
|
3317
|
+
`);
|
|
3029
3318
|
}
|
|
3030
|
-
function
|
|
3031
|
-
const
|
|
3032
|
-
|
|
3033
|
-
|
|
3319
|
+
async function logStep(stepNum, action, details) {
|
|
3320
|
+
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3321
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3322
|
+
`);
|
|
3034
3323
|
}
|
|
3035
3324
|
|
|
3036
3325
|
// src/commands/phases/video-phase.ts
|
|
3037
|
-
var
|
|
3038
|
-
var
|
|
3039
|
-
var
|
|
3326
|
+
var import_node_fs12 = require("fs");
|
|
3327
|
+
var import_promises13 = require("fs/promises");
|
|
3328
|
+
var import_node_path18 = require("path");
|
|
3040
3329
|
init_lib();
|
|
3041
3330
|
|
|
3042
3331
|
// src/record.ts
|
|
3043
|
-
var
|
|
3044
|
-
var
|
|
3045
|
-
var
|
|
3046
|
-
var
|
|
3332
|
+
var import_node_fs10 = require("fs");
|
|
3333
|
+
var import_promises10 = require("fs/promises");
|
|
3334
|
+
var import_node_path15 = require("path");
|
|
3335
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
3047
3336
|
|
|
3048
3337
|
// src/browser/actions.ts
|
|
3049
|
-
var
|
|
3050
|
-
var
|
|
3051
|
-
var
|
|
3338
|
+
var import_node_fs7 = require("fs");
|
|
3339
|
+
var import_promises7 = require("fs/promises");
|
|
3340
|
+
var import_node_path11 = require("path");
|
|
3052
3341
|
|
|
3053
3342
|
// src/browser/effects.ts
|
|
3054
|
-
var
|
|
3055
|
-
var
|
|
3343
|
+
var import_node_fs6 = require("fs");
|
|
3344
|
+
var import_node_path10 = require("path");
|
|
3056
3345
|
init_lib();
|
|
3057
3346
|
function loadHighlightCSS() {
|
|
3058
3347
|
const candidates = [
|
|
3059
|
-
(0,
|
|
3348
|
+
(0, import_node_path10.join)(__dirname, "highlight.css"),
|
|
3060
3349
|
// dist/ build
|
|
3061
|
-
(0,
|
|
3350
|
+
(0, import_node_path10.join)(__dirname, "..", "highlight.css")
|
|
3062
3351
|
// src/browser/ → src/
|
|
3063
3352
|
];
|
|
3064
3353
|
for (const p of candidates) {
|
|
3065
|
-
if ((0,
|
|
3354
|
+
if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
|
|
3066
3355
|
}
|
|
3067
3356
|
return "";
|
|
3068
3357
|
}
|
|
@@ -3386,7 +3675,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3386
3675
|
}
|
|
3387
3676
|
} else {
|
|
3388
3677
|
const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
|
|
3389
|
-
await page.evaluate((y) =>
|
|
3678
|
+
const moved = await page.evaluate((y) => {
|
|
3679
|
+
const before = window.scrollY;
|
|
3680
|
+
window.scrollBy(0, y);
|
|
3681
|
+
return window.scrollY - before;
|
|
3682
|
+
}, amount);
|
|
3683
|
+
if (moved === 0) {
|
|
3684
|
+
throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
|
|
3685
|
+
}
|
|
3390
3686
|
}
|
|
3391
3687
|
break;
|
|
3392
3688
|
}
|
|
@@ -3403,8 +3699,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3403
3699
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3404
3700
|
if (!isPlaceholder) {
|
|
3405
3701
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3406
|
-
const filePath = (0,
|
|
3407
|
-
if ((0,
|
|
3702
|
+
const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
|
|
3703
|
+
if ((0, import_node_fs7.existsSync)(filePath)) {
|
|
3408
3704
|
await fileInput.setInputFiles(filePath);
|
|
3409
3705
|
await page.waitForTimeout(2e3);
|
|
3410
3706
|
break;
|
|
@@ -3520,8 +3816,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3520
3816
|
case "wait":
|
|
3521
3817
|
break;
|
|
3522
3818
|
case "image": {
|
|
3523
|
-
const imagePath = step.value ? (0,
|
|
3524
|
-
if (imagePath && (0,
|
|
3819
|
+
const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
|
|
3820
|
+
if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
|
|
3525
3821
|
const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
|
|
3526
3822
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
3527
3823
|
}
|
|
@@ -3551,7 +3847,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3551
3847
|
await pendingDownload;
|
|
3552
3848
|
pendingDownload = null;
|
|
3553
3849
|
}
|
|
3554
|
-
if (lastDownloadPath && (0,
|
|
3850
|
+
if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
|
|
3555
3851
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
3556
3852
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
3557
3853
|
}
|
|
@@ -3562,7 +3858,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3562
3858
|
async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
3563
3859
|
const { width, height } = config.video;
|
|
3564
3860
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
3565
|
-
const fileBuffer = await (0,
|
|
3861
|
+
const fileBuffer = await (0, import_promises7.readFile)(filePath);
|
|
3566
3862
|
const base64 = fileBuffer.toString("base64");
|
|
3567
3863
|
const mimeTypes = {
|
|
3568
3864
|
png: "image/png",
|
|
@@ -3592,17 +3888,18 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
|
3592
3888
|
}
|
|
3593
3889
|
|
|
3594
3890
|
// src/browser/launch.ts
|
|
3595
|
-
var
|
|
3596
|
-
var
|
|
3597
|
-
async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
3891
|
+
var import_promises8 = require("fs/promises");
|
|
3892
|
+
var import_playwright5 = require("playwright");
|
|
3893
|
+
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
3598
3894
|
const extensionPath = config.browser?.extension;
|
|
3599
3895
|
const viewport = { width: config.video.width, height: config.video.height };
|
|
3600
3896
|
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
3897
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
3601
3898
|
let context;
|
|
3602
3899
|
let browser = null;
|
|
3603
3900
|
let chromeSession = null;
|
|
3604
3901
|
if (extensionPath) {
|
|
3605
|
-
await (0,
|
|
3902
|
+
await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
|
|
3606
3903
|
const persistentArgs = [
|
|
3607
3904
|
`--disable-extensions-except=${extensionPath}`,
|
|
3608
3905
|
`--load-extension=${extensionPath}`,
|
|
@@ -3616,7 +3913,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3616
3913
|
const contexts = chromeSession.browser.contexts();
|
|
3617
3914
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3618
3915
|
} catch {
|
|
3619
|
-
context = await
|
|
3916
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3620
3917
|
headless: false,
|
|
3621
3918
|
args: persistentArgs,
|
|
3622
3919
|
viewport,
|
|
@@ -3625,7 +3922,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3625
3922
|
});
|
|
3626
3923
|
}
|
|
3627
3924
|
} else {
|
|
3628
|
-
context = await
|
|
3925
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3629
3926
|
headless: false,
|
|
3630
3927
|
args: persistentArgs,
|
|
3631
3928
|
viewport,
|
|
@@ -3634,8 +3931,8 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3634
3931
|
});
|
|
3635
3932
|
}
|
|
3636
3933
|
} else {
|
|
3637
|
-
browser = await
|
|
3638
|
-
context = await browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3934
|
+
browser = await import_playwright5.chromium.launch({ headless: true });
|
|
3935
|
+
context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
|
|
3639
3936
|
}
|
|
3640
3937
|
return { context, browser, chromeSession };
|
|
3641
3938
|
}
|
|
@@ -3652,23 +3949,23 @@ async function cleanupBrowserSession(session) {
|
|
|
3652
3949
|
init_lib();
|
|
3653
3950
|
|
|
3654
3951
|
// src/browser/dry-run.ts
|
|
3655
|
-
var
|
|
3952
|
+
var import_node_path13 = require("path");
|
|
3656
3953
|
init_utils();
|
|
3657
3954
|
|
|
3658
3955
|
// src/browser/step-validator.ts
|
|
3659
|
-
var
|
|
3660
|
-
var
|
|
3956
|
+
var import_node_fs8 = require("fs");
|
|
3957
|
+
var import_node_path12 = require("path");
|
|
3661
3958
|
init_utils();
|
|
3662
3959
|
function findMediaFile(filename, baseDir) {
|
|
3663
|
-
const name = (0,
|
|
3664
|
-
const searchDirs = [baseDir, (0,
|
|
3960
|
+
const name = (0, import_node_path12.basename)(filename);
|
|
3961
|
+
const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
|
|
3665
3962
|
for (const dir of searchDirs) {
|
|
3666
|
-
if (!(0,
|
|
3963
|
+
if (!(0, import_node_fs8.existsSync)(dir)) continue;
|
|
3667
3964
|
try {
|
|
3668
|
-
const files = (0,
|
|
3669
|
-
if (files.includes(name)) return (0,
|
|
3965
|
+
const files = (0, import_node_fs8.readdirSync)(dir);
|
|
3966
|
+
if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
|
|
3670
3967
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
3671
|
-
if (match) return (0,
|
|
3968
|
+
if (match) return (0, import_node_path12.join)(dir, match);
|
|
3672
3969
|
} catch {
|
|
3673
3970
|
}
|
|
3674
3971
|
}
|
|
@@ -3740,8 +4037,8 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
3740
4037
|
if (!step.value) {
|
|
3741
4038
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
3742
4039
|
}
|
|
3743
|
-
const specifiedPath = (0,
|
|
3744
|
-
if ((0,
|
|
4040
|
+
const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
|
|
4041
|
+
if ((0, import_node_fs8.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
3745
4042
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
3746
4043
|
if (foundPath) {
|
|
3747
4044
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3862,8 +4159,8 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
3862
4159
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3863
4160
|
if (!isPlaceholder) {
|
|
3864
4161
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3865
|
-
let filePath = (0,
|
|
3866
|
-
if (!(0,
|
|
4162
|
+
let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
|
|
4163
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) {
|
|
3867
4164
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
3868
4165
|
if (foundPath) {
|
|
3869
4166
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3962,8 +4259,13 @@ async function getPageElements(page) {
|
|
|
3962
4259
|
});
|
|
3963
4260
|
}
|
|
3964
4261
|
async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
|
|
3965
|
-
const userDataDir = (0,
|
|
3966
|
-
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4262
|
+
const userDataDir = (0, import_node_path13.join)(baseDir, ".browser-data");
|
|
4263
|
+
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4264
|
+
config,
|
|
4265
|
+
userDataDir,
|
|
4266
|
+
void 0,
|
|
4267
|
+
findAuthState(plan.url)
|
|
4268
|
+
);
|
|
3967
4269
|
const page = context.pages()[0] || await context.newPage();
|
|
3968
4270
|
setupDownloadTracking(page);
|
|
3969
4271
|
const results = [];
|
|
@@ -4022,27 +4324,27 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4022
4324
|
}
|
|
4023
4325
|
|
|
4024
4326
|
// src/browser/meta.ts
|
|
4025
|
-
var
|
|
4026
|
-
var
|
|
4027
|
-
var
|
|
4327
|
+
var import_node_fs9 = require("fs");
|
|
4328
|
+
var import_promises9 = require("fs/promises");
|
|
4329
|
+
var import_node_path14 = require("path");
|
|
4028
4330
|
var RECORDING_FILE = "recording.mp4";
|
|
4029
4331
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
4030
4332
|
function getRecordingPath(outputDir) {
|
|
4031
|
-
return (0,
|
|
4333
|
+
return (0, import_node_path14.join)(outputDir, RECORDING_FILE);
|
|
4032
4334
|
}
|
|
4033
4335
|
async function loadRecordingMeta(outputDir) {
|
|
4034
|
-
const metaPath = (0,
|
|
4035
|
-
if (!(0,
|
|
4336
|
+
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4337
|
+
if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
|
|
4036
4338
|
try {
|
|
4037
|
-
const content = await (0,
|
|
4339
|
+
const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
|
|
4038
4340
|
return JSON.parse(content);
|
|
4039
4341
|
} catch {
|
|
4040
4342
|
return null;
|
|
4041
4343
|
}
|
|
4042
4344
|
}
|
|
4043
4345
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4044
|
-
const metaPath = (0,
|
|
4045
|
-
await (0,
|
|
4346
|
+
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4347
|
+
await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
4046
4348
|
}
|
|
4047
4349
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
4048
4350
|
if (!existingMeta) {
|
|
@@ -4111,16 +4413,17 @@ function formatStepLabel(step) {
|
|
|
4111
4413
|
return `${caption.slice(0, 47)}...`;
|
|
4112
4414
|
}
|
|
4113
4415
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4114
|
-
const videoDir = (0,
|
|
4115
|
-
await (0,
|
|
4416
|
+
const videoDir = (0, import_node_path15.join)(workDir, "video");
|
|
4417
|
+
await (0, import_promises10.mkdir)(videoDir, { recursive: true });
|
|
4116
4418
|
const getStepDuration = (stepNum) => {
|
|
4117
4419
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4118
4420
|
};
|
|
4119
4421
|
const browserTask = task2("Starting browser recording");
|
|
4120
4422
|
const recordingStartTime = Date.now();
|
|
4121
|
-
const userDataDir = (0,
|
|
4423
|
+
const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
|
|
4122
4424
|
const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
|
|
4123
|
-
const
|
|
4425
|
+
const authState = findAuthState(plan.url);
|
|
4426
|
+
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4124
4427
|
const page = context.pages()[0] || await context.newPage();
|
|
4125
4428
|
setupDownloadTracking(page);
|
|
4126
4429
|
context.on("page", (p) => setupDownloadTracking(p));
|
|
@@ -4218,9 +4521,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4218
4521
|
actualStepDurations.push(actualDuration);
|
|
4219
4522
|
}
|
|
4220
4523
|
if (failures.length > 0) {
|
|
4221
|
-
console.log(
|
|
4524
|
+
console.log(import_chalk6.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
|
|
4222
4525
|
for (const f of failures) {
|
|
4223
|
-
console.log(
|
|
4526
|
+
console.log(import_chalk6.default.yellow(` - ${f}`));
|
|
4224
4527
|
}
|
|
4225
4528
|
}
|
|
4226
4529
|
const saveTask = task2("Saving video");
|
|
@@ -4229,7 +4532,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4229
4532
|
await page.close();
|
|
4230
4533
|
const videoPath = await video.path();
|
|
4231
4534
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4232
|
-
if (!videoPath || !(0,
|
|
4535
|
+
if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
|
|
4233
4536
|
throw new Error("Video file was not created");
|
|
4234
4537
|
}
|
|
4235
4538
|
saveTask.succeed("Video saved");
|
|
@@ -4237,33 +4540,18 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4237
4540
|
}
|
|
4238
4541
|
|
|
4239
4542
|
// src/utils/video-pipeline.ts
|
|
4240
|
-
var
|
|
4241
|
-
var
|
|
4242
|
-
var
|
|
4543
|
+
var import_video_kit6 = require("@spark-apps/video-kit");
|
|
4544
|
+
var import_node_fs11 = require("fs");
|
|
4545
|
+
var import_promises11 = require("fs/promises");
|
|
4546
|
+
var import_node_path16 = require("path");
|
|
4547
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
4243
4548
|
init_lib();
|
|
4244
4549
|
var INDENT2 = " ";
|
|
4245
|
-
function chunkWords(words, maxWords) {
|
|
4246
|
-
const chunks = [];
|
|
4247
|
-
for (let i = 0; i < words.length; i += maxWords) {
|
|
4248
|
-
chunks.push(words.slice(i, i + maxWords));
|
|
4249
|
-
}
|
|
4250
|
-
return chunks;
|
|
4251
|
-
}
|
|
4252
|
-
function formatASSTime(ms) {
|
|
4253
|
-
const hours = Math.floor(ms / 36e5);
|
|
4254
|
-
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
4255
|
-
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
4256
|
-
const centis = Math.floor(ms % 1e3 / 10);
|
|
4257
|
-
return `${hours}:${pad(minutes)}:${pad(seconds)}.${pad(centis)}`;
|
|
4258
|
-
}
|
|
4259
|
-
function pad(n, len = 2) {
|
|
4260
|
-
return n.toString().padStart(len, "0");
|
|
4261
|
-
}
|
|
4262
4550
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
4263
|
-
const regularColor =
|
|
4264
|
-
const highlightColor =
|
|
4265
|
-
const outline =
|
|
4266
|
-
const back =
|
|
4551
|
+
const regularColor = (0, import_video_kit.hexToAss)(captions.regular.color);
|
|
4552
|
+
const highlightColor = (0, import_video_kit.hexToAss)(captions.highlight.color);
|
|
4553
|
+
const outline = (0, import_video_kit.hexToAss)(captions.outline);
|
|
4554
|
+
const back = (0, import_video_kit.hexToAss)(captions.outline, 128);
|
|
4267
4555
|
const highlightScale = Math.round(captions.highlight.size / captions.regular.size * 100);
|
|
4268
4556
|
let ass = generateASSHeader(
|
|
4269
4557
|
config.video.width,
|
|
@@ -4288,7 +4576,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4288
4576
|
continue;
|
|
4289
4577
|
}
|
|
4290
4578
|
const words = audio.words.length > 0 ? audio.words : estimateWordTimings(step.caption || "", audio.duration);
|
|
4291
|
-
const chunks =
|
|
4579
|
+
const chunks = (0, import_video_kit6.chunkWordsByCount)(words, 3);
|
|
4292
4580
|
for (const chunk of chunks) {
|
|
4293
4581
|
if (chunk.length === 0) continue;
|
|
4294
4582
|
const chunkStart = currentTime + chunk[0].start;
|
|
@@ -4299,13 +4587,13 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4299
4587
|
const duration = Math.round((word.end - word.start) / 10);
|
|
4300
4588
|
karaokeText += `{\\kf${duration}\\fscx${highlightScale}\\fscy${highlightScale}}${word.word}{\\fscx100\\fscy100} `;
|
|
4301
4589
|
}
|
|
4302
|
-
ass += `Dialogue: 0,${
|
|
4590
|
+
ass += `Dialogue: 0,${(0, import_video_kit6.assTimeFromMs)(chunkStart)},${(0, import_video_kit6.assTimeFromMs)(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
|
|
4303
4591
|
`;
|
|
4304
4592
|
}
|
|
4305
4593
|
currentTime += audio.duration;
|
|
4306
4594
|
}
|
|
4307
|
-
const assPath = (0,
|
|
4308
|
-
await (0,
|
|
4595
|
+
const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
|
|
4596
|
+
await (0, import_promises11.writeFile)(assPath, ass);
|
|
4309
4597
|
return assPath;
|
|
4310
4598
|
}
|
|
4311
4599
|
function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
@@ -4318,250 +4606,120 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4318
4606
|
if ((step.action === "video" || step.action === "media") && step.value) {
|
|
4319
4607
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
4320
4608
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
4321
|
-
const mediaPath = (0,
|
|
4322
|
-
if ((0,
|
|
4609
|
+
const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
|
|
4610
|
+
if ((0, import_node_fs11.existsSync)(mediaPath)) {
|
|
4323
4611
|
const partial = parsePartial(step.partial);
|
|
4324
4612
|
overlays.push({
|
|
4325
4613
|
path: mediaPath,
|
|
4326
4614
|
startTime: cumulativeTime,
|
|
4327
4615
|
duration: durationSec,
|
|
4328
4616
|
mode: step.target === "stretch" ? "stretch" : "center",
|
|
4329
|
-
...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
|
|
4330
|
-
...partial.trimEnd !== void 0 ? { trimEnd: partial.trimEnd } : {}
|
|
4331
|
-
});
|
|
4332
|
-
}
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
cumulativeTime += durationSec;
|
|
4336
|
-
}
|
|
4337
|
-
return overlays;
|
|
4338
|
-
}
|
|
4339
|
-
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
4340
|
-
header("Compose");
|
|
4341
|
-
const configPath = (0, import_node_path15.join)(process.cwd(), CONFIG_FILE);
|
|
4342
|
-
console.log(`${INDENT2}${require("chalk").dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
4343
|
-
const proceed = await confirm(`${INDENT2}Review config or continue?`);
|
|
4344
|
-
if (!proceed) {
|
|
4345
|
-
console.log(require("chalk").yellow(`
|
|
4346
|
-
Edit ${CONFIG_FILE} and run again.
|
|
4347
|
-
`));
|
|
4348
|
-
process.exit(0);
|
|
4349
|
-
}
|
|
4350
|
-
const composeTask = task("Building final video");
|
|
4351
|
-
await (0, import_promises10.mkdir)(config.output.dir, { recursive: true });
|
|
4352
|
-
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4353
|
-
const finalPath = (0, import_node_path15.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4354
|
-
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4355
|
-
const hasVoiceover = audioPath && (0, import_node_fs10.existsSync)(audioPath);
|
|
4356
|
-
const musicPath = config.music?.path ? (0, import_node_path15.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path15.resolve)(config.output.dir, config.music.path) : "";
|
|
4357
|
-
const hasMusic = musicPath && (0, import_node_fs10.existsSync)(musicPath);
|
|
4358
|
-
try {
|
|
4359
|
-
await composeVideo({
|
|
4360
|
-
videoPath,
|
|
4361
|
-
assPath,
|
|
4362
|
-
outputPath: finalPath,
|
|
4363
|
-
voiceoverPath: hasVoiceover ? audioPath : void 0,
|
|
4364
|
-
musicPath: hasMusic ? musicPath : void 0,
|
|
4365
|
-
musicVolume: config.music?.volume,
|
|
4366
|
-
musicFadeIn: config.video.fadeIn / 1e3,
|
|
4367
|
-
musicFadeOut: config.video.fadeOut / 1e3,
|
|
4368
|
-
width: config.video.width,
|
|
4369
|
-
height: config.video.height,
|
|
4370
|
-
fps: config.video.fps,
|
|
4371
|
-
preset: config.video.preset,
|
|
4372
|
-
quality: config.video.quality,
|
|
4373
|
-
fadeIn: config.video.fadeIn / 1e3,
|
|
4374
|
-
fadeOut: config.video.fadeOut / 1e3,
|
|
4375
|
-
videoDuration,
|
|
4376
|
-
audioBitrate: config.audio?.bitrate,
|
|
4377
|
-
contrast: config.video.contrast,
|
|
4378
|
-
trimStart,
|
|
4379
|
-
userTier,
|
|
4380
|
-
mainContentDuration,
|
|
4381
|
-
stepTransitions
|
|
4382
|
-
});
|
|
4383
|
-
composeTask.succeed("Video composed");
|
|
4384
|
-
return finalPath;
|
|
4385
|
-
} catch (err) {
|
|
4386
|
-
await logError("Video Compose", err);
|
|
4387
|
-
composeTask.fail(err.message);
|
|
4388
|
-
throw err;
|
|
4389
|
-
}
|
|
4390
|
-
}
|
|
4391
|
-
async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
4392
|
-
const duration = await getMediaDuration(finalPath);
|
|
4393
|
-
if (duration <= limitSecs * 1e3) return finalPath;
|
|
4394
|
-
const chalk19 = require("chalk");
|
|
4395
|
-
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
4396
|
-
console.log();
|
|
4397
|
-
console.log(chalk19.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
4398
|
-
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
4399
|
-
if (!speedUp) return finalPath;
|
|
4400
|
-
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
4401
|
-
const outputPath = finalPath.replace(".mp4", "-fast.mp4");
|
|
4402
|
-
try {
|
|
4403
|
-
await speedUpVideo({
|
|
4404
|
-
inputPath: finalPath,
|
|
4405
|
-
outputPath,
|
|
4406
|
-
factor: speedFactor,
|
|
4407
|
-
preset: config.video.preset
|
|
4408
|
-
});
|
|
4409
|
-
speedTask.succeed(`Saved: ${formatClickablePath(outputPath)}`);
|
|
4410
|
-
return outputPath;
|
|
4411
|
-
} catch (err) {
|
|
4412
|
-
await logError("Video SpeedUp", err);
|
|
4413
|
-
speedTask.fail(err.message);
|
|
4414
|
-
throw err;
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
|
|
4418
|
-
// src/commands/utils/record-helpers.ts
|
|
4419
|
-
var import_chalk7 = __toESM(require("chalk"));
|
|
4420
|
-
init_lib();
|
|
4421
|
-
|
|
4422
|
-
// src/help.ts
|
|
4423
|
-
var import_chalk5 = __toESM(require("chalk"));
|
|
4424
|
-
init_lib();
|
|
4425
|
-
|
|
4426
|
-
// src/logo.ts
|
|
4427
|
-
var import_figlet = __toESM(require("figlet"));
|
|
4428
|
-
var import_gradient_string = __toESM(require("gradient-string"));
|
|
4429
|
-
var DEFAULT_LOGO_CONFIG = {
|
|
4430
|
-
text: "QuickPeek",
|
|
4431
|
-
font: "Slant",
|
|
4432
|
-
horizontalLayout: "default",
|
|
4433
|
-
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
4434
|
-
suffix: " \u{1F440}"
|
|
4435
|
-
};
|
|
4436
|
-
function renderLogo(config = {}) {
|
|
4437
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
4438
|
-
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
4439
|
-
font: resolved.font,
|
|
4440
|
-
horizontalLayout: resolved.horizontalLayout
|
|
4441
|
-
});
|
|
4442
|
-
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
4443
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
// src/help.ts
|
|
4447
|
-
var cmd = import_chalk5.default.cyan;
|
|
4448
|
-
var arg = import_chalk5.default.green;
|
|
4449
|
-
var opt = import_chalk5.default.magenta;
|
|
4450
|
-
var desc = import_chalk5.default.white;
|
|
4451
|
-
var ex = import_chalk5.default.dim;
|
|
4452
|
-
function pad2(colored, visible, width) {
|
|
4453
|
-
return colored + " ".repeat(Math.max(0, width - visible.length));
|
|
4454
|
-
}
|
|
4455
|
-
function showLogo() {
|
|
4456
|
-
const logo = renderLogo();
|
|
4457
|
-
console.log(logo);
|
|
4458
|
-
console.log(import_chalk5.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
4459
|
-
console.log(import_chalk5.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
4460
|
-
}
|
|
4461
|
-
function formatUsageLines(status, tier) {
|
|
4462
|
-
const lines = [];
|
|
4463
|
-
lines.push(`Videos generated: ${status.usage_count}`);
|
|
4464
|
-
if (tier !== "pro" && status.trial) {
|
|
4465
|
-
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
4466
|
-
const used = max_uses - uses_remaining;
|
|
4467
|
-
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
4468
|
-
if (!is_expired && days_remaining > 0) {
|
|
4469
|
-
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
4470
|
-
}
|
|
4471
|
-
}
|
|
4472
|
-
if (tier === "pro" && status.subscription) {
|
|
4473
|
-
const end = status.subscription.current_period_end;
|
|
4474
|
-
if (status.subscription.is_lifetime) {
|
|
4475
|
-
lines.push("Lifetime access");
|
|
4476
|
-
} else if (isCanceling(status)) {
|
|
4477
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
4478
|
-
lines.push(`Cancels: ${date}`);
|
|
4479
|
-
} else if (end) {
|
|
4480
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
4481
|
-
lines.push(`Renews: ${date}`);
|
|
4482
|
-
}
|
|
4483
|
-
}
|
|
4484
|
-
return lines;
|
|
4485
|
-
}
|
|
4486
|
-
async function showAccount() {
|
|
4487
|
-
const userData = await loadUserData();
|
|
4488
|
-
if (!userData.email) {
|
|
4489
|
-
console.log(import_chalk5.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk5.default.dim(" to sign in."));
|
|
4490
|
-
console.log();
|
|
4491
|
-
return;
|
|
4492
|
-
}
|
|
4493
|
-
const email = userData.email;
|
|
4494
|
-
const tier = userData.tier === "pro" ? "pro" : "free";
|
|
4495
|
-
const tierLabel = tier === "pro" ? import_chalk5.default.green("Pro") : "Free";
|
|
4496
|
-
console.log(` ${tierLabel} plan ${import_chalk5.default.dim("\xB7")} ${import_chalk5.default.dim(email)}`);
|
|
4497
|
-
try {
|
|
4498
|
-
const status = await getStatusCached(email);
|
|
4499
|
-
for (const line of formatUsageLines(status, tier)) {
|
|
4500
|
-
console.log(import_chalk5.default.dim(` ${line}`));
|
|
4501
|
-
}
|
|
4502
|
-
} catch {
|
|
4503
|
-
}
|
|
4504
|
-
console.log();
|
|
4505
|
-
}
|
|
4506
|
-
async function showHelp() {
|
|
4507
|
-
showLogo();
|
|
4508
|
-
await showAccount();
|
|
4509
|
-
const W = 28;
|
|
4510
|
-
const usage = [
|
|
4511
|
-
[`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
|
|
4512
|
-
[`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`, `qp <url> --script <file>`, "Use your own narration script"],
|
|
4513
|
-
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
4514
|
-
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
4515
|
-
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
4516
|
-
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
4517
|
-
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
4518
|
-
];
|
|
4519
|
-
const options = [
|
|
4520
|
-
[`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
|
|
4521
|
-
[`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`, `-d, --description "..."`, "Guide AI with an app description"],
|
|
4522
|
-
[`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`, `-s, --script <file>`, "Narration script (one caption per line)"],
|
|
4523
|
-
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
4524
|
-
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
4525
|
-
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
|
|
4526
|
-
];
|
|
4527
|
-
const examples = [
|
|
4528
|
-
`${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
|
|
4529
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
|
|
4530
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
|
|
4531
|
-
`${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
|
|
4532
|
-
];
|
|
4533
|
-
console.log();
|
|
4534
|
-
console.log(import_chalk5.default.yellow.bold("USAGE"));
|
|
4535
|
-
for (const [colored, plain, d] of usage) {
|
|
4536
|
-
console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
|
|
4617
|
+
...partial.trimStart !== void 0 ? { trimStart: partial.trimStart } : {},
|
|
4618
|
+
...partial.trimEnd !== void 0 ? { trimEnd: partial.trimEnd } : {}
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
cumulativeTime += durationSec;
|
|
4537
4624
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4625
|
+
return overlays;
|
|
4626
|
+
}
|
|
4627
|
+
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
4628
|
+
header("Compose");
|
|
4629
|
+
const configPath = (0, import_node_path16.join)(process.cwd(), CONFIG_FILE);
|
|
4630
|
+
console.log(`${INDENT2}${import_chalk7.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
4631
|
+
const proceed = await confirm(`${INDENT2}Review config or continue?`);
|
|
4632
|
+
if (!proceed) {
|
|
4633
|
+
console.log(import_chalk7.default.yellow(`
|
|
4634
|
+
Edit ${CONFIG_FILE} and run again.
|
|
4635
|
+
`));
|
|
4636
|
+
process.exit(0);
|
|
4542
4637
|
}
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4638
|
+
const composeTask = task("Building final video");
|
|
4639
|
+
await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
|
|
4640
|
+
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4641
|
+
const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4642
|
+
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4643
|
+
const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
|
|
4644
|
+
const musicPath = config.music?.path ? (0, import_node_path16.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path16.resolve)(config.output.dir, config.music.path) : "";
|
|
4645
|
+
const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
|
|
4646
|
+
try {
|
|
4647
|
+
await composeVideo({
|
|
4648
|
+
videoPath,
|
|
4649
|
+
assPath,
|
|
4650
|
+
outputPath: finalPath,
|
|
4651
|
+
voiceoverPath: hasVoiceover ? audioPath : void 0,
|
|
4652
|
+
musicPath: hasMusic ? musicPath : void 0,
|
|
4653
|
+
musicVolume: config.music?.volume,
|
|
4654
|
+
musicFadeIn: config.video.fadeIn / 1e3,
|
|
4655
|
+
musicFadeOut: config.video.fadeOut / 1e3,
|
|
4656
|
+
width: config.video.width,
|
|
4657
|
+
height: config.video.height,
|
|
4658
|
+
fps: config.video.fps,
|
|
4659
|
+
preset: config.video.preset,
|
|
4660
|
+
quality: config.video.quality,
|
|
4661
|
+
fadeIn: config.video.fadeIn / 1e3,
|
|
4662
|
+
fadeOut: config.video.fadeOut / 1e3,
|
|
4663
|
+
videoDuration,
|
|
4664
|
+
audioBitrate: config.audio?.bitrate,
|
|
4665
|
+
contrast: config.video.contrast,
|
|
4666
|
+
trimStart,
|
|
4667
|
+
userTier,
|
|
4668
|
+
mainContentDuration,
|
|
4669
|
+
stepTransitions
|
|
4670
|
+
});
|
|
4671
|
+
composeTask.succeed("Video composed");
|
|
4672
|
+
return finalPath;
|
|
4673
|
+
} catch (err) {
|
|
4674
|
+
await logError("Video Compose", err);
|
|
4675
|
+
composeTask.fail(err.message);
|
|
4676
|
+
throw err;
|
|
4547
4677
|
}
|
|
4678
|
+
}
|
|
4679
|
+
async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
4680
|
+
const duration = await getMediaDuration(finalPath);
|
|
4681
|
+
if (duration <= limitSecs * 1e3) return finalPath;
|
|
4682
|
+
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
4548
4683
|
console.log();
|
|
4684
|
+
console.log(import_chalk7.default.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
4685
|
+
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
4686
|
+
if (!speedUp) return finalPath;
|
|
4687
|
+
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
4688
|
+
const outputPath = finalPath.replace(".mp4", "-fast.mp4");
|
|
4689
|
+
try {
|
|
4690
|
+
await speedUpVideo({
|
|
4691
|
+
inputPath: finalPath,
|
|
4692
|
+
outputPath,
|
|
4693
|
+
factor: speedFactor,
|
|
4694
|
+
preset: config.video.preset
|
|
4695
|
+
});
|
|
4696
|
+
speedTask.succeed(`Saved: ${formatClickablePath(outputPath)}`);
|
|
4697
|
+
return outputPath;
|
|
4698
|
+
} catch (err) {
|
|
4699
|
+
await logError("Video SpeedUp", err);
|
|
4700
|
+
speedTask.fail(err.message);
|
|
4701
|
+
throw err;
|
|
4702
|
+
}
|
|
4549
4703
|
}
|
|
4550
4704
|
|
|
4705
|
+
// src/commands/utils/record-helpers.ts
|
|
4706
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
4707
|
+
init_lib();
|
|
4708
|
+
|
|
4551
4709
|
// src/plan.ts
|
|
4552
|
-
var
|
|
4553
|
-
var
|
|
4554
|
-
var
|
|
4555
|
-
var
|
|
4710
|
+
var import_promises12 = require("fs/promises");
|
|
4711
|
+
var import_node_path17 = require("path");
|
|
4712
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
4713
|
+
var import_playwright6 = require("playwright");
|
|
4556
4714
|
init_lib();
|
|
4557
4715
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
4558
4716
|
const browserTask = task2("Launching browser with extension");
|
|
4559
|
-
const userDataDir = (0,
|
|
4560
|
-
await (0,
|
|
4717
|
+
const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
|
|
4718
|
+
await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
|
|
4561
4719
|
let context;
|
|
4562
4720
|
let chromeSession = null;
|
|
4563
4721
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
4564
|
-
const launchPersistent = () =>
|
|
4722
|
+
const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
|
|
4565
4723
|
headless: false,
|
|
4566
4724
|
args: [
|
|
4567
4725
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -4577,7 +4735,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4577
4735
|
const contexts = chromeSession.browser.contexts();
|
|
4578
4736
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport: { width: 400, height: 600 } });
|
|
4579
4737
|
} catch (_err) {
|
|
4580
|
-
console.log(
|
|
4738
|
+
console.log(import_chalk8.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
|
|
4581
4739
|
context = await launchPersistent();
|
|
4582
4740
|
}
|
|
4583
4741
|
} else {
|
|
@@ -4654,17 +4812,43 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4654
4812
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
4655
4813
|
return { title, elements };
|
|
4656
4814
|
}
|
|
4657
|
-
|
|
4658
|
-
|
|
4815
|
+
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
4816
|
+
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4817
|
+
The JSON should have: url, title, description, steps array.
|
|
4818
|
+
Each step has: id (number), action, caption, and optionally target, value.
|
|
4819
|
+
|
|
4820
|
+
Malformed content:
|
|
4821
|
+
${content}`;
|
|
4822
|
+
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
4823
|
+
try {
|
|
4824
|
+
return JSON.parse(content);
|
|
4825
|
+
} catch {
|
|
4826
|
+
const fixTask = task2("Fixing malformed JSON");
|
|
4827
|
+
try {
|
|
4828
|
+
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
4829
|
+
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4830
|
+
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4831
|
+
const plan = JSON.parse(jsonMatch[0]);
|
|
4832
|
+
fixTask.succeed("JSON fixed by AI");
|
|
4833
|
+
return plan;
|
|
4834
|
+
} catch (err) {
|
|
4835
|
+
await logError2("JSON Fix", err);
|
|
4836
|
+
fixTask.fail("Could not fix JSON");
|
|
4837
|
+
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
4842
|
+
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
4659
4843
|
const aiTask = task2("Generating plan with AI");
|
|
4660
4844
|
const langName = getLanguageName(lang);
|
|
4661
4845
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
4662
|
-
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
|
|
4846
|
+
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
4663
4847
|
try {
|
|
4664
4848
|
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
4665
4849
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
4666
4850
|
if (!jsonMatch) throw new Error("No JSON found in response");
|
|
4667
|
-
const plan =
|
|
4851
|
+
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
4668
4852
|
if (lang !== void 0) plan.lang = lang;
|
|
4669
4853
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
4670
4854
|
...s,
|
|
@@ -4681,41 +4865,14 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4681
4865
|
}
|
|
4682
4866
|
}
|
|
4683
4867
|
async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
|
|
4684
|
-
const content = await (0,
|
|
4685
|
-
|
|
4686
|
-
try {
|
|
4687
|
-
plan = JSON.parse(content);
|
|
4688
|
-
} catch (_parseError) {
|
|
4689
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
4690
|
-
try {
|
|
4691
|
-
const fixed = await callGroq(
|
|
4692
|
-
"You fix malformed JSON. Return ONLY valid JSON, no explanation.",
|
|
4693
|
-
`Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4694
|
-
The JSON should have: url, title, description, steps array.
|
|
4695
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
4696
|
-
|
|
4697
|
-
Malformed content:
|
|
4698
|
-
${content}`,
|
|
4699
|
-
logError2,
|
|
4700
|
-
userEmail,
|
|
4701
|
-
tier
|
|
4702
|
-
);
|
|
4703
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4704
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4705
|
-
plan = JSON.parse(jsonMatch[0]);
|
|
4706
|
-
fixTask.succeed("JSON fixed by AI");
|
|
4707
|
-
} catch (err) {
|
|
4708
|
-
await logError2("JSON Fix", err);
|
|
4709
|
-
fixTask.fail("Could not fix JSON");
|
|
4710
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4711
|
-
}
|
|
4712
|
-
}
|
|
4868
|
+
const content = await (0, import_promises12.readFile)(planPath, "utf-8");
|
|
4869
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
4713
4870
|
if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
|
|
4714
4871
|
throw new Error("Invalid plan: missing url or steps");
|
|
4715
4872
|
}
|
|
4716
4873
|
const renumberTask = task2("Validating plan structure");
|
|
4717
4874
|
const originalCount = plan.steps.length;
|
|
4718
|
-
const
|
|
4875
|
+
const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
|
|
4719
4876
|
const isValidStep = (s) => {
|
|
4720
4877
|
if (!s?.action) return false;
|
|
4721
4878
|
if (s.action === "navigate") {
|
|
@@ -4724,7 +4881,7 @@ ${content}`,
|
|
|
4724
4881
|
}
|
|
4725
4882
|
if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
|
|
4726
4883
|
if (!s.target) return false;
|
|
4727
|
-
if (
|
|
4884
|
+
if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
|
|
4728
4885
|
}
|
|
4729
4886
|
if (s.action === "upload") {
|
|
4730
4887
|
if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
|
|
@@ -4741,7 +4898,7 @@ ${content}`,
|
|
|
4741
4898
|
renumberTask.fail("No valid steps");
|
|
4742
4899
|
throw new Error("Plan has no valid steps");
|
|
4743
4900
|
}
|
|
4744
|
-
await (0,
|
|
4901
|
+
await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
|
|
4745
4902
|
const removed = originalCount - plan.steps.length;
|
|
4746
4903
|
renumberTask.succeed(
|
|
4747
4904
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -4773,41 +4930,41 @@ function displayRateLimitInfo() {
|
|
|
4773
4930
|
const percentage = Math.round(limits.used / limits.limit * 100);
|
|
4774
4931
|
if (percentage >= 90) {
|
|
4775
4932
|
console.log();
|
|
4776
|
-
console.log(
|
|
4933
|
+
console.log(import_chalk9.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
|
|
4777
4934
|
if (limits.tier === "free") {
|
|
4778
|
-
console.log(
|
|
4935
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${getUpgradeUrl()}`));
|
|
4779
4936
|
}
|
|
4780
4937
|
} else if (percentage >= 50) {
|
|
4781
|
-
console.log(
|
|
4938
|
+
console.log(import_chalk9.default.dim(` ${remaining}/${limits.limit} requests remaining`));
|
|
4782
4939
|
}
|
|
4783
4940
|
}
|
|
4784
4941
|
async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
|
|
4785
4942
|
const tier = userTier === "pro" ? "pro" : "free";
|
|
4786
|
-
const tierLabel = tier === "pro" ?
|
|
4787
|
-
console.log(
|
|
4788
|
-
console.log(
|
|
4943
|
+
const tierLabel = tier === "pro" ? import_chalk9.default.green("Pro") : "Free";
|
|
4944
|
+
console.log(import_chalk9.default.cyan(` \u2714 ${tierLabel} plan`));
|
|
4945
|
+
console.log(import_chalk9.default.dim(` ${userEmail}`));
|
|
4789
4946
|
if (userEmail && userEmail !== "anonymous") {
|
|
4790
4947
|
try {
|
|
4791
4948
|
const status = await getStatusCached(userEmail);
|
|
4792
4949
|
for (const line of formatUsageLines(status, tier)) {
|
|
4793
|
-
console.log(
|
|
4950
|
+
console.log(import_chalk9.default.dim(` ${line}`));
|
|
4794
4951
|
}
|
|
4795
4952
|
} catch {
|
|
4796
4953
|
}
|
|
4797
4954
|
}
|
|
4798
4955
|
if (userTier === "free") {
|
|
4799
4956
|
const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
|
|
4800
|
-
console.log(
|
|
4957
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${upgradeUrl}`));
|
|
4801
4958
|
}
|
|
4802
4959
|
}
|
|
4803
4960
|
function displayPlanSummary(plan) {
|
|
4804
4961
|
console.log();
|
|
4805
|
-
console.log(
|
|
4806
|
-
console.log(
|
|
4962
|
+
console.log(import_chalk9.default.dim(` ${plan.title || "Untitled"}`));
|
|
4963
|
+
console.log(import_chalk9.default.dim(` ${plan.steps?.length || 0} steps`));
|
|
4807
4964
|
for (const step of plan.steps || []) {
|
|
4808
4965
|
const text = step.caption || step.action || "";
|
|
4809
4966
|
const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
|
|
4810
|
-
console.log(
|
|
4967
|
+
console.log(import_chalk9.default.dim(` ${step.id}. ${caption}`));
|
|
4811
4968
|
}
|
|
4812
4969
|
}
|
|
4813
4970
|
function getOutputDirFromUrl(url) {
|
|
@@ -4873,8 +5030,8 @@ async function runVideoPhase(params) {
|
|
|
4873
5030
|
if (trimStart2 !== void 0 && trimEnd !== void 0) {
|
|
4874
5031
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
4875
5032
|
} else {
|
|
4876
|
-
const mediaPath = (0,
|
|
4877
|
-
if ((0,
|
|
5033
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5034
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4878
5035
|
try {
|
|
4879
5036
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
4880
5037
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -4885,8 +5042,8 @@ async function runVideoPhase(params) {
|
|
|
4885
5042
|
}
|
|
4886
5043
|
}
|
|
4887
5044
|
} else {
|
|
4888
|
-
const mediaPath = (0,
|
|
4889
|
-
if ((0,
|
|
5045
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5046
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4890
5047
|
try {
|
|
4891
5048
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
4892
5049
|
} catch {
|
|
@@ -4899,7 +5056,7 @@ async function runVideoPhase(params) {
|
|
|
4899
5056
|
})
|
|
4900
5057
|
);
|
|
4901
5058
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
4902
|
-
const hasExistingRecording = existingRecordingMeta && (0,
|
|
5059
|
+
const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
|
|
4903
5060
|
let videoPath = recordingPath;
|
|
4904
5061
|
const firstStep = plan.steps[0];
|
|
4905
5062
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -4908,7 +5065,7 @@ async function runVideoPhase(params) {
|
|
|
4908
5065
|
header("Record");
|
|
4909
5066
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
4910
5067
|
stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
|
|
4911
|
-
await (0,
|
|
5068
|
+
await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
|
|
4912
5069
|
const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
|
|
4913
5070
|
meta.pageLoadTime = result.pageLoadTime;
|
|
4914
5071
|
await saveRecordingMeta(config.output.dir, meta);
|
|
@@ -4939,7 +5096,7 @@ async function runVideoPhase(params) {
|
|
|
4939
5096
|
if (videoOverlays.length > 0) {
|
|
4940
5097
|
const spliceTask = task("Splicing videos into recording");
|
|
4941
5098
|
try {
|
|
4942
|
-
const splicedPath = (0,
|
|
5099
|
+
const splicedPath = (0, import_node_path18.join)(workDir, "spliced.mp4");
|
|
4943
5100
|
await spliceVideos({
|
|
4944
5101
|
inputPath: videoPath,
|
|
4945
5102
|
outputPath: splicedPath,
|
|
@@ -4959,7 +5116,7 @@ async function runVideoPhase(params) {
|
|
|
4959
5116
|
if (stepTransitions.length > 0) {
|
|
4960
5117
|
const transitionTask = task("Applying transitions");
|
|
4961
5118
|
try {
|
|
4962
|
-
const transitionedPath = (0,
|
|
5119
|
+
const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
|
|
4963
5120
|
const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
|
|
4964
5121
|
await applyTransitions({
|
|
4965
5122
|
inputVideo: videoPath,
|
|
@@ -4980,15 +5137,15 @@ async function runVideoPhase(params) {
|
|
|
4980
5137
|
let mainContentDuration;
|
|
4981
5138
|
if (userTier === "free") {
|
|
4982
5139
|
const scriptDir = getScriptDir();
|
|
4983
|
-
let outroPath = (0,
|
|
4984
|
-
if (!(0,
|
|
4985
|
-
outroPath = (0,
|
|
5140
|
+
let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
|
|
5141
|
+
if (!(0, import_node_fs12.existsSync)(outroPath)) {
|
|
5142
|
+
outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
4986
5143
|
}
|
|
4987
|
-
if ((0,
|
|
5144
|
+
if ((0, import_node_fs12.existsSync)(outroPath)) {
|
|
4988
5145
|
try {
|
|
4989
5146
|
const mainDuration = await getMediaDuration(videoPath);
|
|
4990
5147
|
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
4991
|
-
const combinedPath = (0,
|
|
5148
|
+
const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
|
|
4992
5149
|
await concatVideoWithOutro({
|
|
4993
5150
|
inputPath: videoPath,
|
|
4994
5151
|
outroPath,
|
|
@@ -5035,19 +5192,19 @@ async function runVideoPhase(params) {
|
|
|
5035
5192
|
}
|
|
5036
5193
|
|
|
5037
5194
|
// src/commands/phases/voiceover-phase.ts
|
|
5038
|
-
var
|
|
5039
|
-
var
|
|
5040
|
-
var
|
|
5195
|
+
var import_node_fs13 = require("fs");
|
|
5196
|
+
var import_node_path19 = require("path");
|
|
5197
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
5041
5198
|
init_lib();
|
|
5042
5199
|
var INDENT5 = " ";
|
|
5043
5200
|
async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
5044
5201
|
if (fresh) {
|
|
5045
5202
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5046
5203
|
for (const file of filesToDelete) {
|
|
5047
|
-
const filePath = (0,
|
|
5048
|
-
if ((0,
|
|
5049
|
-
const { unlink:
|
|
5050
|
-
await
|
|
5204
|
+
const filePath = (0, import_node_path19.join)(config.output.dir, file);
|
|
5205
|
+
if ((0, import_node_fs13.existsSync)(filePath)) {
|
|
5206
|
+
const { unlink: unlink3 } = await import("fs/promises");
|
|
5207
|
+
await unlink3(filePath).catch(() => {
|
|
5051
5208
|
});
|
|
5052
5209
|
}
|
|
5053
5210
|
}
|
|
@@ -5055,8 +5212,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5055
5212
|
}
|
|
5056
5213
|
const voice = resolveVoice(config);
|
|
5057
5214
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5058
|
-
const voiceoverFile = (0,
|
|
5059
|
-
const hasExistingVoiceover = existingMeta && (0,
|
|
5215
|
+
const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
|
|
5216
|
+
const hasExistingVoiceover = existingMeta && (0, import_node_fs13.existsSync)(voiceoverFile);
|
|
5060
5217
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5061
5218
|
let voiceoverPath;
|
|
5062
5219
|
let stepAudio;
|
|
@@ -5079,9 +5236,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5079
5236
|
const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
|
|
5080
5237
|
if (reuseCheck.canReuse) {
|
|
5081
5238
|
console.log();
|
|
5082
|
-
console.log(
|
|
5239
|
+
console.log(import_chalk10.default.dim(` Found existing voiceover in ${config.output.dir}`));
|
|
5083
5240
|
if (reuseCheck.reason) {
|
|
5084
|
-
console.log(
|
|
5241
|
+
console.log(import_chalk10.default.dim(` ${reuseCheck.reason}`));
|
|
5085
5242
|
}
|
|
5086
5243
|
const reuse = await confirm(`${INDENT5}Reuse existing voiceover?`);
|
|
5087
5244
|
if (reuse) {
|
|
@@ -5121,7 +5278,7 @@ async function captureCommand(url, lang) {
|
|
|
5121
5278
|
const captureResult = await captureUserSession(url, workDir);
|
|
5122
5279
|
info(`Captured ${captureResult.events.length} interactions over ${(captureResult.durationMs / 1e3).toFixed(0)}s`);
|
|
5123
5280
|
if (captureResult.events.length === 0) {
|
|
5124
|
-
console.log(
|
|
5281
|
+
console.log(import_chalk11.default.yellow(" No interactions captured. Exiting."));
|
|
5125
5282
|
return;
|
|
5126
5283
|
}
|
|
5127
5284
|
header("Convert");
|
|
@@ -5129,7 +5286,7 @@ async function captureCommand(url, lang) {
|
|
|
5129
5286
|
const rawSteps = eventsToSteps(captureResult.events, url);
|
|
5130
5287
|
if (rawSteps.length === 0) {
|
|
5131
5288
|
convertTask.fail("No actionable steps found");
|
|
5132
|
-
console.log(
|
|
5289
|
+
console.log(import_chalk11.default.yellow(" No actionable steps could be derived from the captured events."));
|
|
5133
5290
|
return;
|
|
5134
5291
|
}
|
|
5135
5292
|
convertTask.succeed(`${rawSteps.length} steps extracted`);
|
|
@@ -5161,16 +5318,16 @@ async function captureCommand(url, lang) {
|
|
|
5161
5318
|
...lang !== void 0 ? { lang } : {},
|
|
5162
5319
|
steps
|
|
5163
5320
|
};
|
|
5164
|
-
const planPath = (0,
|
|
5321
|
+
const planPath = (0, import_node_path20.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
5165
5322
|
await savePlan(plan, planPath);
|
|
5166
5323
|
console.log();
|
|
5167
|
-
console.log(
|
|
5168
|
-
console.log(
|
|
5324
|
+
console.log(import_chalk11.default.bold.green(` Plan saved: ${planPath}`));
|
|
5325
|
+
console.log(import_chalk11.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
|
|
5169
5326
|
console.log();
|
|
5170
5327
|
const makeVideo = await confirm(" Create polished demo video now?");
|
|
5171
5328
|
if (!makeVideo) {
|
|
5172
5329
|
console.log();
|
|
5173
|
-
console.log(
|
|
5330
|
+
console.log(import_chalk11.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
|
|
5174
5331
|
return;
|
|
5175
5332
|
}
|
|
5176
5333
|
header("Voiceover");
|
|
@@ -5186,20 +5343,176 @@ async function captureCommand(url, lang) {
|
|
|
5186
5343
|
userTier
|
|
5187
5344
|
});
|
|
5188
5345
|
console.log();
|
|
5189
|
-
console.log(
|
|
5190
|
-
console.log(
|
|
5346
|
+
console.log(import_chalk11.default.bold.green(" Done!"));
|
|
5347
|
+
console.log(import_chalk11.default.cyan(` ${finalPath}`));
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
// src/commands/edit-cmd.ts
|
|
5351
|
+
var import_node_fs14 = require("fs");
|
|
5352
|
+
var import_node_path21 = require("path");
|
|
5353
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
5354
|
+
|
|
5355
|
+
// src/lib/plan-validation.ts
|
|
5356
|
+
var INVALID_TARGETS = [
|
|
5357
|
+
/\.png$/i,
|
|
5358
|
+
/\.jpg$/i,
|
|
5359
|
+
/\.gif$/i,
|
|
5360
|
+
/\.webp$/i,
|
|
5361
|
+
/\.svg$/i,
|
|
5362
|
+
/sprite_sheet/i,
|
|
5363
|
+
/downloaded/i
|
|
5364
|
+
];
|
|
5365
|
+
var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
|
|
5366
|
+
function isFilePath(value) {
|
|
5367
|
+
if (!value) return false;
|
|
5368
|
+
return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
|
|
5369
|
+
}
|
|
5370
|
+
function isInvalidTarget(target) {
|
|
5371
|
+
return INVALID_TARGETS.some((pattern) => pattern.test(target));
|
|
5372
|
+
}
|
|
5373
|
+
function validateStep(step) {
|
|
5374
|
+
if (!step?.action) {
|
|
5375
|
+
return "Missing action";
|
|
5376
|
+
}
|
|
5377
|
+
if (step.action === "navigate") {
|
|
5378
|
+
if (!step.target) return "Navigate requires a URL target";
|
|
5379
|
+
if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
|
|
5380
|
+
return "Navigate target must be a valid URL";
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
if (step.action === "media") {
|
|
5384
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
5385
|
+
return "Media step requires a file (image or video)";
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
if (step.action === "upload") {
|
|
5389
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
5390
|
+
return "Upload step requires a file";
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
|
|
5394
|
+
if (!step.target) return `${step.action} requires a target`;
|
|
5395
|
+
if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
|
|
5396
|
+
}
|
|
5397
|
+
return null;
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
// src/lib/plan-file.ts
|
|
5401
|
+
var MAX_HANDOFF_URL_CHARS = 32e3;
|
|
5402
|
+
function serializePlan(plan) {
|
|
5403
|
+
return `${JSON.stringify(plan, null, 2)}
|
|
5404
|
+
`;
|
|
5405
|
+
}
|
|
5406
|
+
function parsePlanText(text) {
|
|
5407
|
+
let raw;
|
|
5408
|
+
try {
|
|
5409
|
+
raw = JSON.parse(text);
|
|
5410
|
+
} catch (e) {
|
|
5411
|
+
return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
5412
|
+
}
|
|
5413
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5414
|
+
return { ok: false, error: "A plan must be a JSON object." };
|
|
5415
|
+
}
|
|
5416
|
+
const candidate = raw;
|
|
5417
|
+
if (typeof candidate.url !== "string" || !candidate.url.trim()) {
|
|
5418
|
+
return { ok: false, error: "A plan needs a `url` to record." };
|
|
5419
|
+
}
|
|
5420
|
+
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
|
|
5421
|
+
return { ok: false, error: "A plan needs at least one step." };
|
|
5422
|
+
}
|
|
5423
|
+
const steps = [];
|
|
5424
|
+
for (const [index, step] of candidate.steps.entries()) {
|
|
5425
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
5426
|
+
return { ok: false, error: `Step ${index + 1} is not an object.` };
|
|
5427
|
+
}
|
|
5428
|
+
const typed = step;
|
|
5429
|
+
if (typeof typed.action !== "string" || !typed.action.trim()) {
|
|
5430
|
+
return { ok: false, error: `Step ${index + 1} has no action.` };
|
|
5431
|
+
}
|
|
5432
|
+
const failure = validateStep(typed);
|
|
5433
|
+
if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
|
|
5434
|
+
steps.push({ ...typed, id: typed.id ?? index + 1 });
|
|
5435
|
+
}
|
|
5436
|
+
return {
|
|
5437
|
+
ok: true,
|
|
5438
|
+
plan: {
|
|
5439
|
+
url: candidate.url,
|
|
5440
|
+
title: candidate.title ?? "",
|
|
5441
|
+
description: candidate.description ?? "",
|
|
5442
|
+
...candidate.lang ? { lang: candidate.lang } : {},
|
|
5443
|
+
steps
|
|
5444
|
+
}
|
|
5445
|
+
};
|
|
5446
|
+
}
|
|
5447
|
+
function toBase64Url(text) {
|
|
5448
|
+
const bytes = new TextEncoder().encode(text);
|
|
5449
|
+
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
5450
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
|
|
5451
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5452
|
+
}
|
|
5453
|
+
function encodePlan(plan) {
|
|
5454
|
+
return toBase64Url(serializePlan(plan));
|
|
5455
|
+
}
|
|
5456
|
+
function planEditorUrl(baseUrl, plan) {
|
|
5457
|
+
const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
|
|
5458
|
+
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
// src/commands/edit-cmd.ts
|
|
5462
|
+
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
5463
|
+
function findPlanFile(dir, lang) {
|
|
5464
|
+
if (lang) {
|
|
5465
|
+
const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
|
|
5466
|
+
return (0, import_node_fs14.existsSync)(exact) ? exact : null;
|
|
5467
|
+
}
|
|
5468
|
+
if (!(0, import_node_fs14.existsSync)(dir)) return null;
|
|
5469
|
+
const plans = (0, import_node_fs14.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
|
|
5470
|
+
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
5471
|
+
return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
|
|
5472
|
+
}
|
|
5473
|
+
async function editCommand(url, lang) {
|
|
5474
|
+
console.log();
|
|
5475
|
+
showLogo();
|
|
5476
|
+
const dir = getOutputDirFromUrl(url);
|
|
5477
|
+
const planPath = findPlanFile(dir, lang);
|
|
5478
|
+
if (!planPath) {
|
|
5479
|
+
console.log(import_chalk12.default.red(` No plan found in ${dir}`));
|
|
5480
|
+
console.log(import_chalk12.default.dim(` Record the demo first: `) + import_chalk12.default.cyan(`qp ${url}`));
|
|
5481
|
+
console.log();
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
const parsed = parsePlanText((0, import_node_fs14.readFileSync)(planPath, "utf8"));
|
|
5485
|
+
if (!parsed.ok) {
|
|
5486
|
+
console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
|
|
5487
|
+
console.log();
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
const base = editorBaseUrl();
|
|
5491
|
+
const editorUrl = planEditorUrl(base, parsed.plan);
|
|
5492
|
+
if (!editorUrl) {
|
|
5493
|
+
console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
|
|
5494
|
+
console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
|
|
5495
|
+
console.log();
|
|
5496
|
+
await openInBrowser(`${base}/edit`);
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
await openInBrowser(editorUrl);
|
|
5500
|
+
console.log(import_chalk12.default.green(` \u2713 Opened ${parsed.plan.steps.length} steps in the editor`));
|
|
5501
|
+
console.log(import_chalk12.default.dim(` ${formatClickablePath(planPath)}`));
|
|
5502
|
+
console.log(import_chalk12.default.dim(" Save from the editor over this file, then record again."));
|
|
5503
|
+
console.log();
|
|
5191
5504
|
}
|
|
5192
5505
|
|
|
5193
5506
|
// src/commands/init-cmd.ts
|
|
5194
|
-
var
|
|
5195
|
-
var
|
|
5196
|
-
var
|
|
5197
|
-
var
|
|
5507
|
+
var import_node_fs15 = require("fs");
|
|
5508
|
+
var import_promises14 = require("fs/promises");
|
|
5509
|
+
var import_node_path22 = require("path");
|
|
5510
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
5198
5511
|
init_lib();
|
|
5199
5512
|
async function initCommand() {
|
|
5200
|
-
const configPath = (0,
|
|
5201
|
-
if ((0,
|
|
5202
|
-
console.log(
|
|
5513
|
+
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
5514
|
+
if ((0, import_node_fs15.existsSync)(configPath)) {
|
|
5515
|
+
console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
5203
5516
|
return;
|
|
5204
5517
|
}
|
|
5205
5518
|
const defaultConfig = {
|
|
@@ -5222,32 +5535,32 @@ async function initCommand() {
|
|
|
5222
5535
|
volume: 0.3
|
|
5223
5536
|
}
|
|
5224
5537
|
};
|
|
5225
|
-
await (0,
|
|
5538
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
5226
5539
|
`);
|
|
5227
|
-
console.log(
|
|
5228
|
-
console.log(
|
|
5540
|
+
console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
5541
|
+
console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
5229
5542
|
}
|
|
5230
5543
|
async function configCommand(key, value) {
|
|
5231
|
-
const configPath = (0,
|
|
5544
|
+
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
5232
5545
|
if (!key) {
|
|
5233
|
-
if (!(0,
|
|
5234
|
-
console.log(
|
|
5546
|
+
if (!(0, import_node_fs15.existsSync)(configPath)) {
|
|
5547
|
+
console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
5235
5548
|
return;
|
|
5236
5549
|
}
|
|
5237
|
-
const content = await (0,
|
|
5238
|
-
console.log(
|
|
5550
|
+
const content = await (0, import_promises14.readFile)(configPath, "utf-8");
|
|
5551
|
+
console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
|
|
5239
5552
|
console.log(content);
|
|
5240
5553
|
return;
|
|
5241
5554
|
}
|
|
5242
5555
|
if (!value) {
|
|
5243
|
-
console.log(
|
|
5244
|
-
console.log(
|
|
5556
|
+
console.log(import_chalk13.default.red(` Missing value for ${key}`));
|
|
5557
|
+
console.log(import_chalk13.default.dim(` Usage: qp config ${key} <value>`));
|
|
5245
5558
|
return;
|
|
5246
5559
|
}
|
|
5247
5560
|
let config = {};
|
|
5248
|
-
if ((0,
|
|
5561
|
+
if ((0, import_node_fs15.existsSync)(configPath)) {
|
|
5249
5562
|
try {
|
|
5250
|
-
config = JSON.parse(await (0,
|
|
5563
|
+
config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
|
|
5251
5564
|
} catch {
|
|
5252
5565
|
}
|
|
5253
5566
|
}
|
|
@@ -5269,42 +5582,42 @@ async function configCommand(key, value) {
|
|
|
5269
5582
|
}
|
|
5270
5583
|
obj[keys[keys.length - 1]] = parsedValue;
|
|
5271
5584
|
}
|
|
5272
|
-
await (0,
|
|
5585
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
5273
5586
|
`);
|
|
5274
|
-
console.log(
|
|
5587
|
+
console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
|
|
5275
5588
|
}
|
|
5276
5589
|
|
|
5277
5590
|
// src/commands/login-cmd.ts
|
|
5278
|
-
var
|
|
5591
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
5279
5592
|
init_lib();
|
|
5280
5593
|
async function loginCommand() {
|
|
5281
5594
|
console.log();
|
|
5282
5595
|
showLogo();
|
|
5283
5596
|
const existing = await loadUserData();
|
|
5284
5597
|
if (existing.email && existing.verified) {
|
|
5285
|
-
console.log(
|
|
5286
|
-
console.log(
|
|
5598
|
+
console.log(import_chalk14.default.dim(` Already signed in as ${existing.email}`));
|
|
5599
|
+
console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp logout") + import_chalk14.default.dim(" first to switch accounts."));
|
|
5287
5600
|
console.log();
|
|
5288
5601
|
try {
|
|
5289
5602
|
const status = await getStatusCached(existing.email);
|
|
5290
5603
|
const tier2 = await getTierByEmail(existing.email);
|
|
5291
|
-
const tierLabel2 = tier2 === "pro" ?
|
|
5604
|
+
const tierLabel2 = tier2 === "pro" ? import_chalk14.default.green("Pro") : "Free";
|
|
5292
5605
|
console.log(` ${tierLabel2} plan`);
|
|
5293
5606
|
for (const line of formatUsageLines(status, tier2)) {
|
|
5294
|
-
console.log(
|
|
5607
|
+
console.log(import_chalk14.default.dim(` ${line}`));
|
|
5295
5608
|
}
|
|
5296
5609
|
} catch {
|
|
5297
5610
|
}
|
|
5298
5611
|
console.log();
|
|
5299
5612
|
return;
|
|
5300
5613
|
}
|
|
5301
|
-
console.log(
|
|
5302
|
-
console.log(
|
|
5614
|
+
console.log(import_chalk14.default.white(" Sign in with your Google account to track usage"));
|
|
5615
|
+
console.log(import_chalk14.default.white(" and unlock Pro features."));
|
|
5303
5616
|
const email = await browserLogin();
|
|
5304
5617
|
if (!email) {
|
|
5305
5618
|
console.log();
|
|
5306
|
-
console.log(
|
|
5307
|
-
console.log(
|
|
5619
|
+
console.log(import_chalk14.default.yellow(" Sign-in cancelled or timed out."));
|
|
5620
|
+
console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp login") + import_chalk14.default.dim(" to try again."));
|
|
5308
5621
|
console.log();
|
|
5309
5622
|
return;
|
|
5310
5623
|
}
|
|
@@ -5315,8 +5628,8 @@ async function loginCommand() {
|
|
|
5315
5628
|
}
|
|
5316
5629
|
await saveUserData({ email, verified: true, tier });
|
|
5317
5630
|
console.log();
|
|
5318
|
-
const tierLabel = tier === "pro" ?
|
|
5319
|
-
console.log(
|
|
5631
|
+
const tierLabel = tier === "pro" ? import_chalk14.default.green("Pro") : "Free";
|
|
5632
|
+
console.log(import_chalk14.default.green(` \u2713 Signed in as ${email}`));
|
|
5320
5633
|
console.log(` ${tierLabel} plan`);
|
|
5321
5634
|
console.log();
|
|
5322
5635
|
}
|
|
@@ -5324,46 +5637,46 @@ async function logoutCommand() {
|
|
|
5324
5637
|
console.log();
|
|
5325
5638
|
const userData = await loadUserData();
|
|
5326
5639
|
if (!userData.email) {
|
|
5327
|
-
console.log(
|
|
5640
|
+
console.log(import_chalk14.default.dim(" Not signed in."));
|
|
5328
5641
|
console.log();
|
|
5329
5642
|
return;
|
|
5330
5643
|
}
|
|
5331
5644
|
const email = userData.email;
|
|
5332
5645
|
await saveUserData({ email: "", verified: false, tier: "free" });
|
|
5333
|
-
console.log(
|
|
5646
|
+
console.log(import_chalk14.default.green(` \u2713 Signed out (${email})`));
|
|
5334
5647
|
console.log();
|
|
5335
5648
|
}
|
|
5336
5649
|
|
|
5337
5650
|
// src/commands/record-cmd.ts
|
|
5338
|
-
var
|
|
5339
|
-
var
|
|
5340
|
-
var
|
|
5341
|
-
var
|
|
5651
|
+
var import_node_fs18 = require("fs");
|
|
5652
|
+
var import_promises17 = require("fs/promises");
|
|
5653
|
+
var import_node_path25 = require("path");
|
|
5654
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
5342
5655
|
init_lib();
|
|
5343
5656
|
|
|
5344
5657
|
// src/commands/phases/auth-phase.ts
|
|
5345
|
-
var
|
|
5658
|
+
var import_chalk16 = __toESM(require("chalk"));
|
|
5346
5659
|
init_lib();
|
|
5347
5660
|
|
|
5348
5661
|
// src/commands/utils/auth-helpers.ts
|
|
5349
|
-
var
|
|
5662
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
5350
5663
|
init_lib();
|
|
5351
5664
|
async function handleRegistrationGate() {
|
|
5352
5665
|
console.log();
|
|
5353
|
-
console.log(
|
|
5354
|
-
console.log(
|
|
5666
|
+
console.log(import_chalk15.default.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
5667
|
+
console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
|
|
5355
5668
|
console.log();
|
|
5356
|
-
console.log(
|
|
5357
|
-
console.log(
|
|
5358
|
-
console.log(
|
|
5359
|
-
console.log(
|
|
5669
|
+
console.log(import_chalk15.default.dim(" Benefits of signing in:"));
|
|
5670
|
+
console.log(import_chalk15.default.dim(" \u2022 20 more videos per month"));
|
|
5671
|
+
console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
|
|
5672
|
+
console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
5360
5673
|
let email = await browserLogin();
|
|
5361
5674
|
if (!email) {
|
|
5362
5675
|
console.log();
|
|
5363
5676
|
email = await promptInput("Email: ");
|
|
5364
5677
|
}
|
|
5365
5678
|
if (!email || !email.includes("@")) {
|
|
5366
|
-
console.log(
|
|
5679
|
+
console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
|
|
5367
5680
|
return null;
|
|
5368
5681
|
}
|
|
5369
5682
|
const registerTask = task("Sending verification email");
|
|
@@ -5378,10 +5691,10 @@ async function handleRegistrationGate() {
|
|
|
5378
5691
|
}
|
|
5379
5692
|
registerTask.succeed("Check your email!");
|
|
5380
5693
|
console.log();
|
|
5381
|
-
console.log(
|
|
5382
|
-
console.log(
|
|
5694
|
+
console.log(import_chalk15.default.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
5695
|
+
console.log(import_chalk15.default.white(` ${email}`));
|
|
5383
5696
|
console.log();
|
|
5384
|
-
console.log(
|
|
5697
|
+
console.log(import_chalk15.default.dim(" Click the link in the email, then press Enter to continue..."));
|
|
5385
5698
|
await promptInput("");
|
|
5386
5699
|
const verifyTask = task("Checking verification");
|
|
5387
5700
|
const verified = await isVerified(email);
|
|
@@ -5391,10 +5704,10 @@ async function handleRegistrationGate() {
|
|
|
5391
5704
|
}
|
|
5392
5705
|
verifyTask.fail("Not verified yet");
|
|
5393
5706
|
console.log();
|
|
5394
|
-
console.log(
|
|
5395
|
-
console.log(
|
|
5707
|
+
console.log(import_chalk15.default.yellow(" Email not verified. Check your inbox and spam folder."));
|
|
5708
|
+
console.log(import_chalk15.default.dim(" Run qp again after clicking the verification link."));
|
|
5396
5709
|
console.log();
|
|
5397
|
-
console.log(
|
|
5710
|
+
console.log(import_chalk15.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
5398
5711
|
console.log();
|
|
5399
5712
|
return { email, verified: false };
|
|
5400
5713
|
}
|
|
@@ -5422,7 +5735,7 @@ async function runAuthPhase(input) {
|
|
|
5422
5735
|
} else {
|
|
5423
5736
|
verifyTask.fail("Email not verified");
|
|
5424
5737
|
console.log();
|
|
5425
|
-
console.log(
|
|
5738
|
+
console.log(import_chalk16.default.yellow(" Check your inbox for the verification email."));
|
|
5426
5739
|
console.log();
|
|
5427
5740
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
5428
5741
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -5447,8 +5760,8 @@ async function runAuthPhase(input) {
|
|
|
5447
5760
|
registerTask.succeed("Check your email!");
|
|
5448
5761
|
await saveUserData({ email: newEmail, verified: false });
|
|
5449
5762
|
console.log();
|
|
5450
|
-
console.log(
|
|
5451
|
-
console.log(
|
|
5763
|
+
console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
5764
|
+
console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
|
|
5452
5765
|
console.log();
|
|
5453
5766
|
gracefulExit();
|
|
5454
5767
|
return null;
|
|
@@ -5464,7 +5777,7 @@ async function runAuthPhase(input) {
|
|
|
5464
5777
|
} else {
|
|
5465
5778
|
recheckTask.fail("Still not verified");
|
|
5466
5779
|
console.log();
|
|
5467
|
-
console.log(
|
|
5780
|
+
console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
5468
5781
|
console.log();
|
|
5469
5782
|
gracefulExit();
|
|
5470
5783
|
return null;
|
|
@@ -5491,10 +5804,16 @@ async function runAuthPhase(input) {
|
|
|
5491
5804
|
} else {
|
|
5492
5805
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
5493
5806
|
console.log();
|
|
5494
|
-
console.log(
|
|
5495
|
-
console.log(
|
|
5807
|
+
console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
5808
|
+
console.log(import_chalk16.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
|
|
5496
5809
|
userEmail = await setupUserEmail();
|
|
5497
|
-
|
|
5810
|
+
let registeredVerified = false;
|
|
5811
|
+
try {
|
|
5812
|
+
const reg = await registerFreeUser(userEmail);
|
|
5813
|
+
registeredVerified = Boolean(reg.success && reg.alreadyVerified);
|
|
5814
|
+
} catch {
|
|
5815
|
+
}
|
|
5816
|
+
await saveUserData({ email: userEmail, verified: registeredVerified, localVideoCount });
|
|
5498
5817
|
}
|
|
5499
5818
|
return {
|
|
5500
5819
|
userEmail: userEmail ?? "anonymous",
|
|
@@ -5505,10 +5824,10 @@ async function runAuthPhase(input) {
|
|
|
5505
5824
|
}
|
|
5506
5825
|
|
|
5507
5826
|
// src/commands/phases/plan-phase.ts
|
|
5508
|
-
var
|
|
5509
|
-
var
|
|
5510
|
-
var
|
|
5511
|
-
var
|
|
5827
|
+
var import_node_fs16 = require("fs");
|
|
5828
|
+
var import_promises15 = require("fs/promises");
|
|
5829
|
+
var import_node_path23 = require("path");
|
|
5830
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
5512
5831
|
|
|
5513
5832
|
// src/ai/fix.ts
|
|
5514
5833
|
init_lib();
|
|
@@ -5660,14 +5979,24 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
5660
5979
|
// src/commands/phases/plan-phase.ts
|
|
5661
5980
|
var INDENT6 = " ";
|
|
5662
5981
|
async function runPlanPhase(params) {
|
|
5663
|
-
const {
|
|
5982
|
+
const {
|
|
5983
|
+
url,
|
|
5984
|
+
config,
|
|
5985
|
+
userEmail,
|
|
5986
|
+
description,
|
|
5987
|
+
planPath,
|
|
5988
|
+
legacyPlanPath,
|
|
5989
|
+
targetLang,
|
|
5990
|
+
userTier = "free",
|
|
5991
|
+
fresh = false
|
|
5992
|
+
} = params;
|
|
5664
5993
|
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
5665
5994
|
const extensionPath = config.browser?.extension;
|
|
5666
5995
|
let existingPlan = null;
|
|
5667
5996
|
let existingPlanPath = null;
|
|
5668
|
-
if ((0,
|
|
5997
|
+
if ((0, import_node_fs16.existsSync)(planPath)) {
|
|
5669
5998
|
try {
|
|
5670
|
-
const parsed = JSON.parse(await (0,
|
|
5999
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5671
6000
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5672
6001
|
existingPlan = parsed;
|
|
5673
6002
|
existingPlanPath = planPath;
|
|
@@ -5677,12 +6006,12 @@ async function runPlanPhase(params) {
|
|
|
5677
6006
|
}
|
|
5678
6007
|
if (!existingPlan) {
|
|
5679
6008
|
try {
|
|
5680
|
-
const files = (0,
|
|
5681
|
-
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0,
|
|
6009
|
+
const files = (0, import_node_fs16.readdirSync)(config.output.dir);
|
|
6010
|
+
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
|
|
5682
6011
|
for (const file of planFiles) {
|
|
5683
|
-
const filePath = (0,
|
|
6012
|
+
const filePath = (0, import_node_path23.join)(config.output.dir, file);
|
|
5684
6013
|
try {
|
|
5685
|
-
const parsed = JSON.parse(await (0,
|
|
6014
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
|
|
5686
6015
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5687
6016
|
existingPlan = parsed;
|
|
5688
6017
|
existingPlanPath = filePath;
|
|
@@ -5694,9 +6023,9 @@ async function runPlanPhase(params) {
|
|
|
5694
6023
|
} catch {
|
|
5695
6024
|
}
|
|
5696
6025
|
}
|
|
5697
|
-
if (!existingPlan && (0,
|
|
6026
|
+
if (!existingPlan && (0, import_node_fs16.existsSync)(legacyPlanPath)) {
|
|
5698
6027
|
try {
|
|
5699
|
-
const parsed = JSON.parse(await (0,
|
|
6028
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
|
|
5700
6029
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5701
6030
|
existingPlan = parsed;
|
|
5702
6031
|
existingPlanPath = legacyPlanPath;
|
|
@@ -5720,25 +6049,25 @@ async function runPlanPhase(params) {
|
|
|
5720
6049
|
translateTask.fail(failMsg);
|
|
5721
6050
|
}
|
|
5722
6051
|
};
|
|
5723
|
-
if (existingPlan && existingPlanPath) {
|
|
6052
|
+
if (existingPlan && existingPlanPath && !fresh) {
|
|
5724
6053
|
console.log();
|
|
5725
|
-
console.log(
|
|
5726
|
-
console.log(
|
|
6054
|
+
console.log(import_chalk17.default.dim(` Found existing plan: `) + import_chalk17.default.cyan(formatClickablePath(existingPlanPath)));
|
|
6055
|
+
console.log(import_chalk17.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
|
|
5727
6056
|
if (!urlMatches) {
|
|
5728
|
-
console.log(
|
|
5729
|
-
console.log(
|
|
5730
|
-
console.log(
|
|
6057
|
+
console.log(import_chalk17.default.yellow(` \u26A0 Plan URL differs from argument:`));
|
|
6058
|
+
console.log(import_chalk17.default.yellow(` Plan: ${existingPlan.url}`));
|
|
6059
|
+
console.log(import_chalk17.default.yellow(` Arg: ${url}`));
|
|
5731
6060
|
}
|
|
5732
6061
|
const existingLang = existingPlan.lang || "en";
|
|
5733
6062
|
if (existingLang !== targetLang) {
|
|
5734
|
-
console.log(
|
|
6063
|
+
console.log(import_chalk17.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
5735
6064
|
}
|
|
5736
6065
|
const reuse = await confirm(`${INDENT6}Use existing plan?`);
|
|
5737
6066
|
if (reuse) {
|
|
5738
6067
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
5739
6068
|
if (existingPlanPath !== planPath) {
|
|
5740
6069
|
await savePlan(plan, planPath);
|
|
5741
|
-
console.log(
|
|
6070
|
+
console.log(import_chalk17.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
5742
6071
|
}
|
|
5743
6072
|
if (existingLang !== targetLang) {
|
|
5744
6073
|
const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
|
|
@@ -5756,7 +6085,8 @@ async function runPlanPhase(params) {
|
|
|
5756
6085
|
userEmail,
|
|
5757
6086
|
extensionPath,
|
|
5758
6087
|
config.output.dir,
|
|
5759
|
-
userTier
|
|
6088
|
+
userTier,
|
|
6089
|
+
{ width: config.video.width, height: config.video.height }
|
|
5760
6090
|
);
|
|
5761
6091
|
displayRateLimitInfo();
|
|
5762
6092
|
if (urlMatches) {
|
|
@@ -5773,7 +6103,7 @@ async function runPlanPhase(params) {
|
|
|
5773
6103
|
uploadIdx++;
|
|
5774
6104
|
}
|
|
5775
6105
|
}
|
|
5776
|
-
console.log(
|
|
6106
|
+
console.log(import_chalk17.default.dim(` Preserved upload paths from previous plan`));
|
|
5777
6107
|
}
|
|
5778
6108
|
await savePlan(plan, planPath);
|
|
5779
6109
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -5789,7 +6119,9 @@ async function runPlanPhase(params) {
|
|
|
5789
6119
|
logError,
|
|
5790
6120
|
userEmail,
|
|
5791
6121
|
extensionPath,
|
|
5792
|
-
config.output.dir
|
|
6122
|
+
config.output.dir,
|
|
6123
|
+
void 0,
|
|
6124
|
+
{ width: config.video.width, height: config.video.height }
|
|
5793
6125
|
);
|
|
5794
6126
|
displayRateLimitInfo();
|
|
5795
6127
|
await savePlan(plan, planPath);
|
|
@@ -5797,8 +6129,8 @@ async function runPlanPhase(params) {
|
|
|
5797
6129
|
}
|
|
5798
6130
|
displayPlanSummary(plan);
|
|
5799
6131
|
console.log();
|
|
5800
|
-
console.log(
|
|
5801
|
-
console.log(
|
|
6132
|
+
console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
6133
|
+
console.log(import_chalk17.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
|
|
5802
6134
|
const MAX_REFINEMENTS = 10;
|
|
5803
6135
|
let refinementCount = 0;
|
|
5804
6136
|
while (true) {
|
|
@@ -5816,15 +6148,15 @@ async function runPlanPhase(params) {
|
|
|
5816
6148
|
refineTask.succeed("Plan refined");
|
|
5817
6149
|
displayRateLimitInfo();
|
|
5818
6150
|
if (plan.steps.length > maxSteps) {
|
|
5819
|
-
console.log(
|
|
6151
|
+
console.log(import_chalk17.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
5820
6152
|
}
|
|
5821
6153
|
displayPlanSummary(plan);
|
|
5822
6154
|
console.log();
|
|
5823
|
-
console.log(
|
|
6155
|
+
console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
5824
6156
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
5825
6157
|
console.log();
|
|
5826
|
-
console.log(
|
|
5827
|
-
console.log(
|
|
6158
|
+
console.log(import_chalk17.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
|
|
6159
|
+
console.log(import_chalk17.default.dim(` Further refinements may incur additional costs.`));
|
|
5828
6160
|
const continueRefining = await confirm(`${INDENT6}Continue refining?`);
|
|
5829
6161
|
if (!continueRefining) break;
|
|
5830
6162
|
}
|
|
@@ -5836,12 +6168,12 @@ async function runPlanPhase(params) {
|
|
|
5836
6168
|
const planLang = plan.lang || "en";
|
|
5837
6169
|
if (planLang !== targetLang) {
|
|
5838
6170
|
let useExistingTranslation = false;
|
|
5839
|
-
if ((0,
|
|
6171
|
+
if ((0, import_node_fs16.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
5840
6172
|
try {
|
|
5841
|
-
const translatedPlan = JSON.parse(await (0,
|
|
6173
|
+
const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5842
6174
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
5843
6175
|
console.log();
|
|
5844
|
-
console.log(
|
|
6176
|
+
console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
5845
6177
|
const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
|
|
5846
6178
|
if (useTranslated) {
|
|
5847
6179
|
plan = translatedPlan;
|
|
@@ -5853,7 +6185,7 @@ async function runPlanPhase(params) {
|
|
|
5853
6185
|
}
|
|
5854
6186
|
if (!useExistingTranslation) {
|
|
5855
6187
|
console.log();
|
|
5856
|
-
console.log(
|
|
6188
|
+
console.log(import_chalk17.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
5857
6189
|
const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
|
|
5858
6190
|
if (translate) await doTranslate("Translation failed, using original");
|
|
5859
6191
|
}
|
|
@@ -5862,8 +6194,8 @@ async function runPlanPhase(params) {
|
|
|
5862
6194
|
}
|
|
5863
6195
|
|
|
5864
6196
|
// src/commands/phases/validation-phase.ts
|
|
5865
|
-
var
|
|
5866
|
-
var
|
|
6197
|
+
var import_promises16 = require("fs/promises");
|
|
6198
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
5867
6199
|
async function runValidationPhase(params) {
|
|
5868
6200
|
let { plan } = params;
|
|
5869
6201
|
const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
|
|
@@ -5914,21 +6246,21 @@ async function runValidationPhase(params) {
|
|
|
5914
6246
|
const stepNum = result.stepIndex + 1;
|
|
5915
6247
|
const step = plan.steps[result.stepIndex];
|
|
5916
6248
|
console.log();
|
|
5917
|
-
console.log(
|
|
6249
|
+
console.log(import_chalk18.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
5918
6250
|
const caption = step.caption || "";
|
|
5919
|
-
if (caption) console.log(
|
|
5920
|
-
console.log(
|
|
5921
|
-
if (step.value) console.log(
|
|
5922
|
-
console.log(
|
|
6251
|
+
if (caption) console.log(import_chalk18.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
|
|
6252
|
+
console.log(import_chalk18.default.dim(` Target: ${step.target || "none"}`));
|
|
6253
|
+
if (step.value) console.log(import_chalk18.default.dim(` Value: ${step.value}`));
|
|
6254
|
+
console.log(import_chalk18.default.dim(` Error: ${result.error}`));
|
|
5923
6255
|
if (result.suggestion) {
|
|
5924
|
-
console.log(
|
|
6256
|
+
console.log(import_chalk18.default.yellow(` Hint: ${result.suggestion}`));
|
|
5925
6257
|
}
|
|
5926
6258
|
console.log();
|
|
5927
6259
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
5928
6260
|
if (isUploadMissingFile) {
|
|
5929
6261
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
5930
6262
|
if (filePath.toLowerCase() === "skip") {
|
|
5931
|
-
console.log(
|
|
6263
|
+
console.log(import_chalk18.default.yellow(` Skipping step ${stepNum}`));
|
|
5932
6264
|
continue;
|
|
5933
6265
|
}
|
|
5934
6266
|
if (filePath) {
|
|
@@ -5937,13 +6269,13 @@ async function runValidationPhase(params) {
|
|
|
5937
6269
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5938
6270
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
5939
6271
|
if (newResult2?.success) {
|
|
5940
|
-
console.log(
|
|
6272
|
+
console.log(import_chalk18.default.green(` \u2713 Step ${stepNum} now passes`));
|
|
5941
6273
|
validationResult = revalidation2;
|
|
5942
6274
|
continue;
|
|
5943
6275
|
}
|
|
5944
6276
|
result.error = newResult2?.error;
|
|
5945
6277
|
result.suggestion = newResult2?.suggestion;
|
|
5946
|
-
console.log(
|
|
6278
|
+
console.log(import_chalk18.default.red(` Still failing: ${result.error}`));
|
|
5947
6279
|
}
|
|
5948
6280
|
}
|
|
5949
6281
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -5965,8 +6297,8 @@ async function runValidationPhase(params) {
|
|
|
5965
6297
|
await savePlan(plan, planPath);
|
|
5966
6298
|
fixTask.succeed("AI suggested a fix");
|
|
5967
6299
|
const fixedStep = plan.steps[result.stepIndex];
|
|
5968
|
-
console.log(
|
|
5969
|
-
if (fixedStep.value) console.log(
|
|
6300
|
+
console.log(import_chalk18.default.dim(` New target: ${fixedStep.target || "none"}`));
|
|
6301
|
+
if (fixedStep.value) console.log(import_chalk18.default.dim(` New value: ${fixedStep.value}`));
|
|
5970
6302
|
} catch {
|
|
5971
6303
|
fixTask.fail("AI could not fix automatically");
|
|
5972
6304
|
}
|
|
@@ -5982,14 +6314,14 @@ async function runValidationPhase(params) {
|
|
|
5982
6314
|
result.error = newResult?.error;
|
|
5983
6315
|
result.suggestion = newResult?.suggestion;
|
|
5984
6316
|
console.log();
|
|
5985
|
-
console.log(
|
|
6317
|
+
console.log(import_chalk18.default.yellow(` Edit step ${stepNum} in: `) + import_chalk18.default.cyan(formatClickablePath(planPath)));
|
|
5986
6318
|
console.log();
|
|
5987
6319
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
5988
6320
|
if (choice.toLowerCase() === "skip") {
|
|
5989
|
-
console.log(
|
|
6321
|
+
console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
5990
6322
|
continue;
|
|
5991
6323
|
}
|
|
5992
|
-
plan = JSON.parse(await (0,
|
|
6324
|
+
plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
5993
6325
|
const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
|
|
5994
6326
|
const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5995
6327
|
const finalResult = finalRevalidation.results[result.stepIndex];
|
|
@@ -6013,31 +6345,31 @@ async function runValidationPhase(params) {
|
|
|
6013
6345
|
}
|
|
6014
6346
|
|
|
6015
6347
|
// src/commands/utils/chrome-extension.ts
|
|
6016
|
-
var
|
|
6017
|
-
var
|
|
6018
|
-
var
|
|
6348
|
+
var import_node_fs17 = require("fs");
|
|
6349
|
+
var import_node_os3 = require("os");
|
|
6350
|
+
var import_node_path24 = require("path");
|
|
6019
6351
|
function resolveChromeExtension(extensionId) {
|
|
6020
6352
|
const platform = process.platform;
|
|
6021
6353
|
const possibleDirs = [];
|
|
6022
6354
|
const getChromeExtDirs = (userDataPath) => {
|
|
6023
6355
|
const dirs = [];
|
|
6024
6356
|
try {
|
|
6025
|
-
const profiles = (0,
|
|
6357
|
+
const profiles = (0, import_node_fs17.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
6026
6358
|
for (const profile of profiles) {
|
|
6027
|
-
dirs.push((0,
|
|
6359
|
+
dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
|
|
6028
6360
|
}
|
|
6029
6361
|
} catch {
|
|
6030
|
-
dirs.push((0,
|
|
6362
|
+
dirs.push((0, import_node_path24.join)(userDataPath, "Default", "Extensions"));
|
|
6031
6363
|
}
|
|
6032
6364
|
return dirs;
|
|
6033
6365
|
};
|
|
6034
6366
|
if (platform === "win32") {
|
|
6035
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6367
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
6036
6368
|
} else if (platform === "darwin") {
|
|
6037
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6369
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
6038
6370
|
} else {
|
|
6039
6371
|
try {
|
|
6040
|
-
const windowsUsers = (0,
|
|
6372
|
+
const windowsUsers = (0, import_node_fs17.readdirSync)("/mnt/c/Users").filter(
|
|
6041
6373
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
6042
6374
|
);
|
|
6043
6375
|
for (const user of windowsUsers) {
|
|
@@ -6045,22 +6377,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
6045
6377
|
}
|
|
6046
6378
|
} catch {
|
|
6047
6379
|
}
|
|
6048
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6380
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
|
|
6049
6381
|
}
|
|
6050
6382
|
for (const chromeExtDir of possibleDirs) {
|
|
6051
|
-
const extDir = (0,
|
|
6052
|
-
if (!(0,
|
|
6383
|
+
const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
|
|
6384
|
+
if (!(0, import_node_fs17.existsSync)(extDir)) continue;
|
|
6053
6385
|
try {
|
|
6054
|
-
const versions = (0,
|
|
6386
|
+
const versions = (0, import_node_fs17.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
6055
6387
|
if (versions.length === 0) continue;
|
|
6056
6388
|
const latestVersion = versions.sort().pop();
|
|
6057
|
-
const extPath = (0,
|
|
6389
|
+
const extPath = (0, import_node_path24.join)(extDir, latestVersion);
|
|
6058
6390
|
let name = extensionId;
|
|
6059
6391
|
let popupPath = "popup.html";
|
|
6060
|
-
const manifestPath = (0,
|
|
6061
|
-
if ((0,
|
|
6392
|
+
const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
|
|
6393
|
+
if ((0, import_node_fs17.existsSync)(manifestPath)) {
|
|
6062
6394
|
try {
|
|
6063
|
-
const manifest = JSON.parse((0,
|
|
6395
|
+
const manifest = JSON.parse((0, import_node_fs17.readFileSync)(manifestPath, "utf-8"));
|
|
6064
6396
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
6065
6397
|
name = manifest.name;
|
|
6066
6398
|
}
|
|
@@ -6080,7 +6412,8 @@ function resolveChromeExtension(extensionId) {
|
|
|
6080
6412
|
}
|
|
6081
6413
|
|
|
6082
6414
|
// src/commands/record-cmd.ts
|
|
6083
|
-
async function recordCommand(url,
|
|
6415
|
+
async function recordCommand(url, options = {}) {
|
|
6416
|
+
const { langOverride, description, skipVoice, fresh, autoYes: autoYes2, rethrow } = options;
|
|
6084
6417
|
if (autoYes2) setAutoYes(true);
|
|
6085
6418
|
console.log();
|
|
6086
6419
|
showLogo();
|
|
@@ -6090,16 +6423,16 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6090
6423
|
const extensionId = url.slice(7);
|
|
6091
6424
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
6092
6425
|
if (!chromeExtension) {
|
|
6093
|
-
console.log(
|
|
6426
|
+
console.log(import_chalk19.default.red(`
|
|
6094
6427
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
6095
|
-
console.log(
|
|
6428
|
+
console.log(import_chalk19.default.dim(` Make sure the extension is installed in Chrome
|
|
6096
6429
|
`));
|
|
6097
6430
|
process.exit(1);
|
|
6098
6431
|
}
|
|
6099
6432
|
config.browser = { extension: chromeExtension.path };
|
|
6100
6433
|
url = chromeExtension.popupUrl;
|
|
6101
|
-
console.log(
|
|
6102
|
-
console.log(
|
|
6434
|
+
console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
|
|
6435
|
+
console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
|
|
6103
6436
|
}
|
|
6104
6437
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
6105
6438
|
const userData = await loadUserData();
|
|
@@ -6112,20 +6445,20 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6112
6445
|
if (!authResult) return;
|
|
6113
6446
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
6114
6447
|
if (configCreated) {
|
|
6115
|
-
const configPath = (0,
|
|
6448
|
+
const configPath = (0, import_node_path25.join)(process.cwd(), CONFIG_FILE);
|
|
6116
6449
|
console.log();
|
|
6117
|
-
console.log(
|
|
6118
|
-
console.log(
|
|
6450
|
+
console.log(import_chalk19.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6451
|
+
console.log(import_chalk19.default.dim(` ${configPath}`));
|
|
6119
6452
|
console.log();
|
|
6120
|
-
console.log(
|
|
6121
|
-
console.log(
|
|
6122
|
-
console.log(
|
|
6123
|
-
console.log(
|
|
6124
|
-
console.log(
|
|
6453
|
+
console.log(import_chalk19.default.cyan(" Review the config file to customize:"));
|
|
6454
|
+
console.log(import_chalk19.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
|
|
6455
|
+
console.log(import_chalk19.default.dim(" \u2022 video: Resolution, quality, effects"));
|
|
6456
|
+
console.log(import_chalk19.default.dim(" \u2022 music: Background music path and volume"));
|
|
6457
|
+
console.log(import_chalk19.default.dim(" \u2022 output: Output directory"));
|
|
6125
6458
|
console.log();
|
|
6126
6459
|
const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
|
|
6127
6460
|
if (!proceed) {
|
|
6128
|
-
console.log(
|
|
6461
|
+
console.log(import_chalk19.default.yellow(`
|
|
6129
6462
|
Edit ${CONFIG_FILE} and run again.
|
|
6130
6463
|
`));
|
|
6131
6464
|
process.exit(0);
|
|
@@ -6133,17 +6466,17 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6133
6466
|
}
|
|
6134
6467
|
if (langOverride) config.lang = langOverride;
|
|
6135
6468
|
if (config.music?.path) {
|
|
6136
|
-
const musicPath = (0,
|
|
6137
|
-
if (!(0,
|
|
6469
|
+
const musicPath = (0, import_node_path25.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path25.resolve)(config.output.dir, config.music.path);
|
|
6470
|
+
if (!(0, import_node_fs18.existsSync)(musicPath)) {
|
|
6138
6471
|
console.log();
|
|
6139
|
-
console.log(
|
|
6140
|
-
console.log(
|
|
6472
|
+
console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
6473
|
+
console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
|
|
6141
6474
|
console.log();
|
|
6142
6475
|
const proceed = await confirm(`${INDENT3}Continue without background music?`);
|
|
6143
6476
|
if (!proceed) {
|
|
6144
|
-
console.log(
|
|
6477
|
+
console.log(import_chalk19.default.yellow(`
|
|
6145
6478
|
Edit music.path in config and run again.`));
|
|
6146
|
-
console.log(
|
|
6479
|
+
console.log(import_chalk19.default.dim(` ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}
|
|
6147
6480
|
`));
|
|
6148
6481
|
process.exit(0);
|
|
6149
6482
|
}
|
|
@@ -6153,10 +6486,10 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6153
6486
|
const captions = await loadCaptions();
|
|
6154
6487
|
const workDir = await prepareWorkDir();
|
|
6155
6488
|
const targetLang = config.lang || "en";
|
|
6156
|
-
const planPath = (0,
|
|
6157
|
-
const legacyPlanPath = (0,
|
|
6489
|
+
const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
6490
|
+
const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
|
|
6158
6491
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
6159
|
-
await (0,
|
|
6492
|
+
await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
|
|
6160
6493
|
await setLogFilePath(config.output.dir);
|
|
6161
6494
|
try {
|
|
6162
6495
|
const plan = await runPlanPhase({
|
|
@@ -6167,7 +6500,8 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6167
6500
|
...description !== void 0 && { description },
|
|
6168
6501
|
planPath,
|
|
6169
6502
|
legacyPlanPath,
|
|
6170
|
-
targetLang
|
|
6503
|
+
targetLang,
|
|
6504
|
+
...fresh !== void 0 && { fresh }
|
|
6171
6505
|
});
|
|
6172
6506
|
const validatedPlan = await runValidationPhase({
|
|
6173
6507
|
plan,
|
|
@@ -6192,35 +6526,37 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6192
6526
|
await saveUserData({ localVideoCount: newCount });
|
|
6193
6527
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
6194
6528
|
if (remaining > 0) {
|
|
6195
|
-
console.log(
|
|
6529
|
+
console.log(import_chalk19.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
6196
6530
|
}
|
|
6197
6531
|
}
|
|
6198
6532
|
console.log();
|
|
6199
|
-
console.log(
|
|
6533
|
+
console.log(import_chalk19.default.bold.green(" \u2713 Done!"));
|
|
6200
6534
|
console.log();
|
|
6201
|
-
console.log(
|
|
6535
|
+
console.log(import_chalk19.default.cyan(` ${formatClickablePath(finalPath)}`));
|
|
6202
6536
|
console.log();
|
|
6537
|
+
return finalPath;
|
|
6203
6538
|
} catch (err) {
|
|
6204
6539
|
await logError("Record Command", err);
|
|
6540
|
+
if (rethrow) throw err;
|
|
6205
6541
|
console.log();
|
|
6206
|
-
console.log(
|
|
6207
|
-
console.log(
|
|
6542
|
+
console.log(import_chalk19.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
6543
|
+
console.log(import_chalk19.default.dim(` See ${logFilePath} for details`));
|
|
6208
6544
|
process.exit(1);
|
|
6209
6545
|
}
|
|
6210
6546
|
}
|
|
6211
6547
|
|
|
6212
6548
|
// src/commands/script-cmd.ts
|
|
6213
|
-
var
|
|
6214
|
-
var
|
|
6215
|
-
var
|
|
6549
|
+
var import_promises18 = require("fs/promises");
|
|
6550
|
+
var import_node_path26 = require("path");
|
|
6551
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
6216
6552
|
init_lib();
|
|
6217
6553
|
async function scriptCommand(url, scriptPath, lang) {
|
|
6218
6554
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
6219
6555
|
header("Script");
|
|
6220
|
-
const raw = await (0,
|
|
6556
|
+
const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
|
|
6221
6557
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6222
6558
|
if (lines.length === 0) {
|
|
6223
|
-
console.log(
|
|
6559
|
+
console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
|
|
6224
6560
|
process.exit(1);
|
|
6225
6561
|
}
|
|
6226
6562
|
info(`${lines.length} captions from script`);
|
|
@@ -6232,7 +6568,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6232
6568
|
const planTask = task("Mapping script to UI actions");
|
|
6233
6569
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
6234
6570
|
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
6235
|
-
const planPath = (0,
|
|
6571
|
+
const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
6236
6572
|
await savePlan(plan, planPath);
|
|
6237
6573
|
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
6238
6574
|
header("Voiceover");
|
|
@@ -6252,8 +6588,8 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6252
6588
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
6253
6589
|
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
|
|
6254
6590
|
console.log();
|
|
6255
|
-
console.log(
|
|
6256
|
-
console.log(
|
|
6591
|
+
console.log(import_chalk20.default.bold.green(" Done!"));
|
|
6592
|
+
console.log(import_chalk20.default.cyan(` ${finalPath}`));
|
|
6257
6593
|
}
|
|
6258
6594
|
|
|
6259
6595
|
// src/qp.ts
|
|
@@ -6298,6 +6634,16 @@ function parseArgs(args) {
|
|
|
6298
6634
|
if (first === "logout") {
|
|
6299
6635
|
return { ...base, command: "logout" };
|
|
6300
6636
|
}
|
|
6637
|
+
if (first === "auth") {
|
|
6638
|
+
const sub = args[1];
|
|
6639
|
+
if (sub === "list" || sub === void 0) {
|
|
6640
|
+
return { ...base, command: "auth-list" };
|
|
6641
|
+
}
|
|
6642
|
+
if (sub === "clear") {
|
|
6643
|
+
return { ...base, command: "auth-clear", url: args[2] ? normalizeUrl3(args[2]) : void 0 };
|
|
6644
|
+
}
|
|
6645
|
+
return { ...base, command: "auth", url: normalizeUrl3(sub) };
|
|
6646
|
+
}
|
|
6301
6647
|
if (first === "init") {
|
|
6302
6648
|
return { ...base, command: "init" };
|
|
6303
6649
|
}
|
|
@@ -6313,6 +6659,20 @@ function parseArgs(args) {
|
|
|
6313
6659
|
if (first === "record") {
|
|
6314
6660
|
args = args.slice(1);
|
|
6315
6661
|
}
|
|
6662
|
+
if (first === "edit") {
|
|
6663
|
+
const rest = args.slice(1);
|
|
6664
|
+
let url2;
|
|
6665
|
+
let lang2;
|
|
6666
|
+
for (const arg2 of rest) {
|
|
6667
|
+
if (arg2.startsWith("lang=")) {
|
|
6668
|
+
lang2 = arg2.slice(5);
|
|
6669
|
+
} else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
|
|
6670
|
+
url2 = normalizeUrl3(arg2);
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
if (!url2) return { ...base, command: "help" };
|
|
6674
|
+
return { ...base, command: "edit", url: url2, lang: lang2 };
|
|
6675
|
+
}
|
|
6316
6676
|
if (first === "capture") {
|
|
6317
6677
|
const rest = args.slice(1);
|
|
6318
6678
|
let url2;
|
|
@@ -6378,6 +6738,28 @@ async function main() {
|
|
|
6378
6738
|
case "logout":
|
|
6379
6739
|
await logoutCommand();
|
|
6380
6740
|
break;
|
|
6741
|
+
case "auth":
|
|
6742
|
+
if (!parsed.url) {
|
|
6743
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
6744
|
+
console.log(import_chalk21.default.dim(" Run: qp auth <url>"));
|
|
6745
|
+
process.exit(1);
|
|
6746
|
+
}
|
|
6747
|
+
await authCommand(parsed.url);
|
|
6748
|
+
break;
|
|
6749
|
+
case "auth-clear":
|
|
6750
|
+
await authClearCommand(parsed.url);
|
|
6751
|
+
break;
|
|
6752
|
+
case "auth-list":
|
|
6753
|
+
await authListCommand();
|
|
6754
|
+
break;
|
|
6755
|
+
case "edit":
|
|
6756
|
+
if (!parsed.url) {
|
|
6757
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
6758
|
+
console.log(import_chalk21.default.dim(" Run: qp edit <url>"));
|
|
6759
|
+
process.exit(1);
|
|
6760
|
+
}
|
|
6761
|
+
await editCommand(parsed.url, parsed.lang);
|
|
6762
|
+
break;
|
|
6381
6763
|
case "init":
|
|
6382
6764
|
await initCommand();
|
|
6383
6765
|
break;
|
|
@@ -6386,8 +6768,8 @@ async function main() {
|
|
|
6386
6768
|
break;
|
|
6387
6769
|
case "record":
|
|
6388
6770
|
if (!parsed.url) {
|
|
6389
|
-
console.log(
|
|
6390
|
-
console.log(
|
|
6771
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
6772
|
+
console.log(import_chalk21.default.dim(" Run: qp --help"));
|
|
6391
6773
|
process.exit(1);
|
|
6392
6774
|
}
|
|
6393
6775
|
setAutoYes(parsed.yes);
|
|
@@ -6396,13 +6778,18 @@ async function main() {
|
|
|
6396
6778
|
showLogo();
|
|
6397
6779
|
await scriptCommand(parsed.url, parsed.scriptPath, parsed.lang);
|
|
6398
6780
|
} else {
|
|
6399
|
-
await recordCommand(parsed.url,
|
|
6781
|
+
await recordCommand(parsed.url, {
|
|
6782
|
+
langOverride: parsed.lang,
|
|
6783
|
+
description: parsed.description,
|
|
6784
|
+
skipVoice: parsed.skipVoice,
|
|
6785
|
+
fresh: parsed.fresh
|
|
6786
|
+
});
|
|
6400
6787
|
}
|
|
6401
6788
|
break;
|
|
6402
6789
|
case "capture":
|
|
6403
6790
|
if (!parsed.url) {
|
|
6404
|
-
console.log(
|
|
6405
|
-
console.log(
|
|
6791
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
6792
|
+
console.log(import_chalk21.default.dim(" Run: qp --help"));
|
|
6406
6793
|
process.exit(1);
|
|
6407
6794
|
}
|
|
6408
6795
|
console.log();
|