@spark-apps/quickpeek 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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 +306 -0
- package/dist/index.mjs +1858 -0
- package/dist/mcp-tools.mjs +5909 -0
- package/dist/qp.js +1563 -1129
- package/dist/scraping-BK8pIJhm.d.mts +290 -0
- package/dist/web.d.mts +103 -0
- package/dist/web.mjs +978 -0
- package/mcp.mjs +75 -0
- package/package.json +80 -64
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("-");
|
|
@@ -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,123 +125,186 @@ var init_utils = __esm({
|
|
|
137
125
|
});
|
|
138
126
|
|
|
139
127
|
// src/lib/api-client.ts
|
|
140
|
-
function
|
|
141
|
-
return Boolean(process.env[ENV_SPARK_API_KEY]);
|
|
142
|
-
}
|
|
143
|
-
function getGroqApiKey() {
|
|
144
|
-
return process.env[ENV_SPARK_API_KEY] || null;
|
|
145
|
-
}
|
|
146
|
-
function getStripeUrl() {
|
|
147
|
-
return process.env[ENV_STRIPE_BACKEND] || DEFAULT_STRIPE_BACKEND;
|
|
148
|
-
}
|
|
149
|
-
async function getOrRefreshToken(apiKey) {
|
|
128
|
+
async function getOrRefreshDirectToken() {
|
|
150
129
|
const now = Date.now();
|
|
151
|
-
if (
|
|
152
|
-
return
|
|
130
|
+
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
131
|
+
return cachedDirectToken.token;
|
|
153
132
|
}
|
|
154
133
|
try {
|
|
155
134
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
156
135
|
method: "POST",
|
|
157
|
-
headers: { "X-Api-Key":
|
|
136
|
+
headers: { "X-Api-Key": API_KEY }
|
|
158
137
|
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
140
|
+
}
|
|
159
141
|
const data = await response.json();
|
|
160
|
-
if (!
|
|
161
|
-
throw new Error(data.error || `Token exchange failed:
|
|
142
|
+
if (!data.success || !data.token) {
|
|
143
|
+
throw new Error(data.error || `Token exchange failed: unexpected response`);
|
|
162
144
|
}
|
|
163
|
-
|
|
145
|
+
cachedDirectToken = {
|
|
164
146
|
token: data.token,
|
|
165
147
|
expiresAt: data.expiresAt || now + 36e5
|
|
166
|
-
// Default 1 hour
|
|
167
148
|
};
|
|
168
149
|
return data.token;
|
|
169
150
|
} catch (err) {
|
|
170
151
|
throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
|
|
171
152
|
}
|
|
172
153
|
}
|
|
173
|
-
async function
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
return
|
|
177
|
-
success: false,
|
|
178
|
-
error: `Missing API key. Set ${ENV_SPARK_API_KEY} environment variable.`,
|
|
179
|
-
isAuthError: true
|
|
180
|
-
};
|
|
154
|
+
async function getOrRefreshRelayToken(email) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
|
|
157
|
+
return cachedRelayToken.token;
|
|
181
158
|
}
|
|
182
159
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
160
|
+
const response = await fetch(RELAY_TOKEN_URL, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ email })
|
|
164
|
+
});
|
|
165
|
+
const data = await response.json().catch(() => ({}));
|
|
166
|
+
if (!response.ok || !data.success || !data.token) {
|
|
167
|
+
throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
|
|
168
|
+
}
|
|
169
|
+
cachedRelayToken = {
|
|
170
|
+
token: data.token,
|
|
171
|
+
expiresAt: data.expiresAt || now + 36e5,
|
|
172
|
+
email
|
|
173
|
+
};
|
|
174
|
+
return data.token;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
180
|
+
let lastRateLimitError = null;
|
|
181
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(chatUrl, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
Authorization: `Bearer ${bearerToken}`
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
messages: [
|
|
191
|
+
{ role: "system", content: systemPrompt },
|
|
192
|
+
{ role: "user", content: userPrompt }
|
|
193
|
+
]
|
|
194
|
+
})
|
|
195
|
+
});
|
|
196
|
+
let data = {};
|
|
197
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
198
|
+
if (contentType.includes("application/json")) {
|
|
199
|
+
data = await response.json();
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
|
|
203
|
+
if (response.status === 429 && attempt < 3) {
|
|
204
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
205
|
+
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
|
|
206
|
+
lastRateLimitError = {
|
|
216
207
|
success: false,
|
|
217
208
|
error,
|
|
218
|
-
isRateLimit:
|
|
219
|
-
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
if (!data.success || !data.response) {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
error: data.error || "No response from Spark AI"
|
|
209
|
+
isRateLimit: true,
|
|
210
|
+
...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
|
|
226
211
|
};
|
|
212
|
+
await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
|
|
213
|
+
continue;
|
|
227
214
|
}
|
|
228
215
|
return {
|
|
229
|
-
success:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
216
|
+
success: false,
|
|
217
|
+
error,
|
|
218
|
+
isRateLimit: response.status === 429,
|
|
219
|
+
isAuthError: response.status === 401 || response.status === 403
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (!data.success || !data.response) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
error: data.error || "No response from Spark AI"
|
|
237
226
|
};
|
|
238
|
-
} catch (err) {
|
|
239
|
-
if (attempt === 3) throw err;
|
|
240
|
-
const delayMs = 1e3 * Math.pow(2, attempt - 1);
|
|
241
|
-
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
242
227
|
}
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
content: data.response,
|
|
231
|
+
limits: {
|
|
232
|
+
used: data.limits?.used ?? 0,
|
|
233
|
+
limit: data.limits?.limit ?? 100,
|
|
234
|
+
tier,
|
|
235
|
+
...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (attempt === 3) throw err;
|
|
240
|
+
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
241
|
+
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
243
242
|
}
|
|
244
|
-
|
|
243
|
+
}
|
|
244
|
+
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
245
|
+
}
|
|
246
|
+
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
247
|
+
if (!API_KEY) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
error: "AI service is not configured. Please report this issue.",
|
|
251
|
+
isAuthError: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const token = await getOrRefreshDirectToken();
|
|
256
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
262
|
+
if (!email) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
error: "Sign in required. Run qp login to connect your account.",
|
|
266
|
+
isAuthError: true
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
let token = await getOrRefreshRelayToken(email);
|
|
271
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
272
|
+
if (!result.success && result.isAuthError) {
|
|
273
|
+
cachedRelayToken = null;
|
|
274
|
+
token = await getOrRefreshRelayToken(email);
|
|
275
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
245
278
|
} catch (err) {
|
|
246
279
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
247
280
|
}
|
|
248
281
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
282
|
+
function parseAIPlanResponse(content) {
|
|
283
|
+
try {
|
|
284
|
+
let processedContent = content;
|
|
285
|
+
const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
286
|
+
if (codeBlockMatch?.[1]) {
|
|
287
|
+
processedContent = codeBlockMatch[1].trim();
|
|
288
|
+
}
|
|
289
|
+
const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
|
|
290
|
+
if (!jsonMatch) {
|
|
291
|
+
return { success: false, error: "No valid JSON found in AI response" };
|
|
292
|
+
}
|
|
293
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
294
|
+
return { success: true, plan: parsed };
|
|
295
|
+
} catch (err) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
251
301
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
302
|
+
function normalizeUrl(input) {
|
|
303
|
+
let url = input.trim();
|
|
304
|
+
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
305
|
+
return `https://${url}`;
|
|
255
306
|
}
|
|
256
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN,
|
|
307
|
+
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, cachedDirectToken, cachedRelayToken;
|
|
257
308
|
var init_api_client = __esm({
|
|
258
309
|
"src/lib/api-client.ts"() {
|
|
259
310
|
"use strict";
|
|
@@ -261,10 +312,91 @@ var init_api_client = __esm({
|
|
|
261
312
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
262
313
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
263
314
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
315
|
+
RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
316
|
+
RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
317
|
+
RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
318
|
+
API_KEY = process.env.SPARK_AI_KEY || "";
|
|
319
|
+
cachedDirectToken = null;
|
|
320
|
+
cachedRelayToken = null;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// src/lib/SparkStripe-feature-gating.ts
|
|
325
|
+
async function _fetch(email, init) {
|
|
326
|
+
const r = await fetch(
|
|
327
|
+
`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
|
|
328
|
+
init
|
|
329
|
+
);
|
|
330
|
+
if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
|
|
331
|
+
return r.json();
|
|
332
|
+
}
|
|
333
|
+
async function getStatusCached(email) {
|
|
334
|
+
const k = email.toLowerCase(), hit = _cache.get(k);
|
|
335
|
+
if (hit && Date.now() < hit.exp) return hit.data;
|
|
336
|
+
const data = await getStatus(email);
|
|
337
|
+
_cache.set(k, { data, exp: Date.now() + 6e4 });
|
|
338
|
+
return data;
|
|
339
|
+
}
|
|
340
|
+
function getTier(d) {
|
|
341
|
+
if (!d.subscription || !d.registered) return "free";
|
|
342
|
+
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
343
|
+
return TIERS[d.subscription.price_id] ?? "free";
|
|
344
|
+
}
|
|
345
|
+
async function getTierByEmail(email) {
|
|
346
|
+
try {
|
|
347
|
+
return getTier(await getStatusCached(email));
|
|
348
|
+
} catch {
|
|
349
|
+
return "free";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function isVerified(email) {
|
|
353
|
+
try {
|
|
354
|
+
return (await getStatusCached(email)).verified;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function pricingUrl(opts) {
|
|
360
|
+
const u = new URL(`${BASE}/pricing/${APP_ID}`);
|
|
361
|
+
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
362
|
+
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
363
|
+
return u.toString();
|
|
364
|
+
}
|
|
365
|
+
var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
366
|
+
var init_SparkStripe_feature_gating = __esm({
|
|
367
|
+
"src/lib/SparkStripe-feature-gating.ts"() {
|
|
368
|
+
"use strict";
|
|
369
|
+
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
370
|
+
BASE = "https://sparkstripe.com";
|
|
371
|
+
TIERS = {
|
|
372
|
+
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
373
|
+
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
374
|
+
price_1TADm7Ef9opkuWid3I9tquIB: "pro",
|
|
375
|
+
price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
|
|
376
|
+
price_1TCREmEf9opkuWidvhuqzqDM: "pro",
|
|
377
|
+
price_1TCREmEf9opkuWidacf1tiC7: "pro"
|
|
378
|
+
};
|
|
379
|
+
RANK = {
|
|
380
|
+
free: 0,
|
|
381
|
+
pro: 1
|
|
382
|
+
};
|
|
383
|
+
FEATURES = {
|
|
384
|
+
free: [],
|
|
385
|
+
pro: [
|
|
386
|
+
"Everything in Free",
|
|
387
|
+
"No watermark",
|
|
388
|
+
"Full video composition (voiceover + captions + music + transitions)",
|
|
389
|
+
"Unlimited videos",
|
|
390
|
+
"Priority support"
|
|
391
|
+
]
|
|
392
|
+
};
|
|
393
|
+
getStatus = (email) => _fetch(email);
|
|
394
|
+
_cache = /* @__PURE__ */ new Map();
|
|
395
|
+
isPaid = (d) => getTier(d) !== "free";
|
|
396
|
+
hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
|
|
397
|
+
hasTrialRemaining = (d) => isPaid(d) || !!d.trial && !d.trial.is_expired && d.trial.uses_remaining > 0;
|
|
398
|
+
showPaywall = (d) => !isPaid(d) && !hasTrialRemaining(d);
|
|
399
|
+
isCanceling = (d) => !!d.subscription && d.subscription.cancel_at_period_end && d.subscription.status !== "canceled";
|
|
268
400
|
}
|
|
269
401
|
});
|
|
270
402
|
|
|
@@ -273,17 +405,12 @@ function shouldWatermark(tier, videoDurationSeconds) {
|
|
|
273
405
|
return tier === "free" && videoDurationSeconds > 20;
|
|
274
406
|
}
|
|
275
407
|
function getUpgradeUrl(userEmail) {
|
|
276
|
-
|
|
277
|
-
let url = `${stripe}/pricing/quickpeek`;
|
|
278
|
-
if (userEmail) {
|
|
279
|
-
url += `?email=${encodeURIComponent(userEmail)}`;
|
|
280
|
-
}
|
|
281
|
-
return url;
|
|
408
|
+
return pricingUrl(userEmail ? { email: userEmail } : void 0);
|
|
282
409
|
}
|
|
283
410
|
var init_billing = __esm({
|
|
284
411
|
"src/lib/billing.ts"() {
|
|
285
412
|
"use strict";
|
|
286
|
-
|
|
413
|
+
init_SparkStripe_feature_gating();
|
|
287
414
|
}
|
|
288
415
|
});
|
|
289
416
|
|
|
@@ -340,7 +467,22 @@ var init_config = __esm({
|
|
|
340
467
|
});
|
|
341
468
|
|
|
342
469
|
// src/lib/scraping.ts
|
|
343
|
-
|
|
470
|
+
function isExcludedLink(text, href = "") {
|
|
471
|
+
const lowerText = text.toLowerCase();
|
|
472
|
+
const lowerHref = href.toLowerCase();
|
|
473
|
+
return EXCLUDED_LINK_PATTERNS.some((p) => lowerText.includes(p) || lowerHref.includes(p));
|
|
474
|
+
}
|
|
475
|
+
function shouldSkipLink(text, href, currentPath) {
|
|
476
|
+
if (!href || href === "#") return true;
|
|
477
|
+
try {
|
|
478
|
+
const linkPath = new URL(href).pathname;
|
|
479
|
+
if (linkPath === "/" || linkPath === currentPath) return true;
|
|
480
|
+
} catch {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
return isExcludedLink(text, href);
|
|
484
|
+
}
|
|
485
|
+
var EXCLUDED_LINK_PATTERNS, INTERACTIVE_SELECTORS;
|
|
344
486
|
var init_scraping = __esm({
|
|
345
487
|
"src/lib/scraping.ts"() {
|
|
346
488
|
"use strict";
|
|
@@ -407,6 +549,22 @@ var init_scraping = __esm({
|
|
|
407
549
|
"feed",
|
|
408
550
|
"home"
|
|
409
551
|
];
|
|
552
|
+
INTERACTIVE_SELECTORS = [
|
|
553
|
+
"button",
|
|
554
|
+
"input",
|
|
555
|
+
"select",
|
|
556
|
+
"textarea",
|
|
557
|
+
"a[href]",
|
|
558
|
+
'[role="button"]',
|
|
559
|
+
'[role="checkbox"]',
|
|
560
|
+
'[role="radio"]',
|
|
561
|
+
'[role="switch"]',
|
|
562
|
+
'[role="slider"]',
|
|
563
|
+
'[role="combobox"]',
|
|
564
|
+
'[role="menuitem"]',
|
|
565
|
+
'[type="submit"]',
|
|
566
|
+
"[onclick]"
|
|
567
|
+
];
|
|
410
568
|
}
|
|
411
569
|
});
|
|
412
570
|
|
|
@@ -428,11 +586,17 @@ function getActionForType(type) {
|
|
|
428
586
|
return "type";
|
|
429
587
|
}
|
|
430
588
|
}
|
|
431
|
-
async function crawlPage(url, task2 = noopTask) {
|
|
589
|
+
async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
590
|
+
const cached = crawlCache.get(url);
|
|
591
|
+
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
592
|
+
task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
593
|
+
return { title: cached.title, elements: [...cached.elements] };
|
|
594
|
+
}
|
|
432
595
|
const browserTask = task2("Launching browser");
|
|
433
596
|
const browser = await import_playwright.chromium.launch({ headless: true });
|
|
434
|
-
const
|
|
435
|
-
|
|
597
|
+
const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
|
|
598
|
+
const page = await context.newPage();
|
|
599
|
+
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
436
600
|
const crawlTask = task2("Crawling page");
|
|
437
601
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
438
602
|
const title = await page.title();
|
|
@@ -565,9 +729,14 @@ async function crawlPage(url, task2 = noopTask) {
|
|
|
565
729
|
});
|
|
566
730
|
await browser.close();
|
|
567
731
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
732
|
+
crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
|
|
733
|
+
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
734
|
+
const oldest = crawlCache.keys().next().value;
|
|
735
|
+
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
736
|
+
}
|
|
568
737
|
return { title, elements };
|
|
569
738
|
}
|
|
570
|
-
var import_playwright, noopTask;
|
|
739
|
+
var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
571
740
|
var init_crawl = __esm({
|
|
572
741
|
"src/lib/crawl.ts"() {
|
|
573
742
|
"use strict";
|
|
@@ -575,6 +744,9 @@ var init_crawl = __esm({
|
|
|
575
744
|
init_scraping();
|
|
576
745
|
noopTask = () => ({ succeed: () => {
|
|
577
746
|
} });
|
|
747
|
+
CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
748
|
+
CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
749
|
+
crawlCache = /* @__PURE__ */ new Map();
|
|
578
750
|
}
|
|
579
751
|
});
|
|
580
752
|
|
|
@@ -619,10 +791,10 @@ var init_watermark = __esm({
|
|
|
619
791
|
// src/lib/ffmpeg/spawn.ts
|
|
620
792
|
function toWSLPath(path) {
|
|
621
793
|
if (!isWindowsRuntime) return path;
|
|
622
|
-
return
|
|
794
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path.replace(/\\/g, "/");
|
|
623
795
|
}
|
|
624
796
|
function convertArgToWSL(arg2) {
|
|
625
|
-
const wslPath =
|
|
797
|
+
const wslPath = (0, import_video_kit2.windowsDrivePathToWsl)(arg2);
|
|
626
798
|
if (wslPath) return wslPath;
|
|
627
799
|
if (arg2.includes("\\")) return arg2.replace(/\\/g, "/");
|
|
628
800
|
return arg2;
|
|
@@ -719,20 +891,24 @@ function composeVideo(opts) {
|
|
|
719
891
|
`[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
|
|
720
892
|
// Music: fade in and fade out
|
|
721
893
|
`[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]`,
|
|
722
|
-
|
|
894
|
+
// Normalise the MIX and hard-limit its true peak - ported from
|
|
895
|
+
// VidLet, where single-pass loudnorm alone measured +0.9 dBTP on
|
|
896
|
+
// sharp TTS consonants. -14 LUFS is the YouTube/IG/TikTok playback
|
|
897
|
+
// target, so the platform leaves the audio untouched.
|
|
898
|
+
`[voice][music]amix=inputs=2:duration=longest,${import_video_kit3.MASTER_CHAIN}[aout]`
|
|
723
899
|
].join(";");
|
|
724
900
|
args.push("-vf", vfStr);
|
|
725
901
|
args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
|
|
726
902
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
727
903
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
728
904
|
} else if (hasVoiceover) {
|
|
729
|
-
const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}`;
|
|
905
|
+
const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn},${import_video_kit3.MASTER_CHAIN}`;
|
|
730
906
|
args.push("-vf", vfStr);
|
|
731
907
|
args.push("-map", "0:v", "-map", "1:a", "-af", af);
|
|
732
908
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
733
909
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
734
910
|
} else if (hasMusic) {
|
|
735
|
-
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}`;
|
|
911
|
+
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}`;
|
|
736
912
|
args.push("-vf", vfStr);
|
|
737
913
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
738
914
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
@@ -745,9 +921,11 @@ function composeVideo(opts) {
|
|
|
745
921
|
runFFmpeg(args, "ffmpeg compose failed").then(resolve9, reject);
|
|
746
922
|
});
|
|
747
923
|
}
|
|
924
|
+
var import_video_kit3;
|
|
748
925
|
var init_compose = __esm({
|
|
749
926
|
"src/lib/ffmpeg/compose.ts"() {
|
|
750
927
|
"use strict";
|
|
928
|
+
import_video_kit3 = require("@spark-apps/video-kit");
|
|
751
929
|
init_watermark();
|
|
752
930
|
init_spawn();
|
|
753
931
|
}
|
|
@@ -1004,14 +1182,13 @@ function applyTransitions(opts) {
|
|
|
1004
1182
|
filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
|
|
1005
1183
|
}
|
|
1006
1184
|
let currentV = "v0";
|
|
1185
|
+
let chainDuration = boundaries[1] - boundaries[0];
|
|
1007
1186
|
for (let i = 0; i < sorted.length; i++) {
|
|
1008
1187
|
const trans = sorted[i];
|
|
1009
1188
|
const duration = trans.duration ?? 0.3;
|
|
1010
1189
|
const nextIdx = i + 1;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
const segDuration = segEnd - segStart;
|
|
1014
|
-
const offset = Math.max(0, segDuration - duration);
|
|
1190
|
+
const offset = Math.max(0, chainDuration - duration);
|
|
1191
|
+
chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
|
|
1015
1192
|
const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
|
|
1016
1193
|
const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
|
|
1017
1194
|
filterParts.push(
|
|
@@ -1212,24 +1389,20 @@ var init_prompts = __esm({
|
|
|
1212
1389
|
|
|
1213
1390
|
// src/lib/styles.ts
|
|
1214
1391
|
function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
|
|
1215
|
-
return
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
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
|
|
1224
|
-
|
|
1225
|
-
[Events]
|
|
1226
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
1227
|
-
`;
|
|
1392
|
+
return (0, import_video_kit4.buildAssHeader)({
|
|
1393
|
+
title: "QuickPeek Demo",
|
|
1394
|
+
width,
|
|
1395
|
+
height,
|
|
1396
|
+
styles: [
|
|
1397
|
+
`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`
|
|
1398
|
+
]
|
|
1399
|
+
});
|
|
1228
1400
|
}
|
|
1229
|
-
var CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1401
|
+
var import_video_kit4, CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1230
1402
|
var init_styles = __esm({
|
|
1231
1403
|
"src/lib/styles.ts"() {
|
|
1232
1404
|
"use strict";
|
|
1405
|
+
import_video_kit4 = require("@spark-apps/video-kit");
|
|
1233
1406
|
init_cursor();
|
|
1234
1407
|
CURSOR_MOVE_DURATION = 800;
|
|
1235
1408
|
CURSOR_CSS = `
|
|
@@ -1273,49 +1446,13 @@ var init_styles = __esm({
|
|
|
1273
1446
|
});
|
|
1274
1447
|
|
|
1275
1448
|
// src/lib/subscription-client.ts
|
|
1276
|
-
async function fetchStatus(email) {
|
|
1277
|
-
const cacheKey = `${email}:${APP_ID}`;
|
|
1278
|
-
const cached = responseCache.get(cacheKey);
|
|
1279
|
-
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
|
|
1280
|
-
const stripe = getStripeUrl();
|
|
1281
|
-
try {
|
|
1282
|
-
const params = new URLSearchParams({ email, app_id: APP_ID });
|
|
1283
|
-
const response = await fetch(`${stripe}/api/subscription/status-public?${params}`);
|
|
1284
|
-
if (!response.ok) return null;
|
|
1285
|
-
const data = await response.json();
|
|
1286
|
-
responseCache.set(cacheKey, { data, timestamp: Date.now() });
|
|
1287
|
-
return data;
|
|
1288
|
-
} catch {
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
function resolveTier(status) {
|
|
1293
|
-
return status && PRO_STATUSES.includes(status) ? "pro" : "free";
|
|
1294
|
-
}
|
|
1295
|
-
async function getTier(userEmail) {
|
|
1296
|
-
const data = await fetchStatus(userEmail);
|
|
1297
|
-
if (!data) return "free";
|
|
1298
|
-
const status = data.subscription?.status;
|
|
1299
|
-
const tier = resolveTier(status);
|
|
1300
|
-
if (tier === "pro" && status === "trialing" && data.subscription?.current_period_end) {
|
|
1301
|
-
if (new Date(data.subscription.current_period_end) <= /* @__PURE__ */ new Date()) return "free";
|
|
1302
|
-
}
|
|
1303
|
-
return tier;
|
|
1304
|
-
}
|
|
1305
|
-
async function checkVerificationStatus(email) {
|
|
1306
|
-
const data = await fetchStatus(email);
|
|
1307
|
-
if (!data) return { verified: false, tier: "free" };
|
|
1308
|
-
const verified = data.verified === true || data.user?.verified === true || data.user?.email_verified === true;
|
|
1309
|
-
const tier = resolveTier(data.subscription?.status);
|
|
1310
|
-
return { verified, tier };
|
|
1311
|
-
}
|
|
1312
1449
|
async function registerFreeUser(email) {
|
|
1313
|
-
const
|
|
1450
|
+
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1314
1451
|
try {
|
|
1315
|
-
const response = await fetch(
|
|
1452
|
+
const response = await fetch(`https://sparkstripe.com/api/register-free`, {
|
|
1316
1453
|
method: "POST",
|
|
1317
1454
|
headers: { "Content-Type": "application/json" },
|
|
1318
|
-
body: JSON.stringify({ email, app_id:
|
|
1455
|
+
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
1319
1456
|
});
|
|
1320
1457
|
const text = await response.text();
|
|
1321
1458
|
let data = {};
|
|
@@ -1340,21 +1477,15 @@ async function registerFreeUser(email) {
|
|
|
1340
1477
|
return { success: false, error: err instanceof Error ? err.message : "Network error" };
|
|
1341
1478
|
}
|
|
1342
1479
|
}
|
|
1343
|
-
var APP_ID, PRO_STATUSES, CACHE_TTL_MS, responseCache;
|
|
1344
1480
|
var init_subscription_client = __esm({
|
|
1345
1481
|
"src/lib/subscription-client.ts"() {
|
|
1346
1482
|
"use strict";
|
|
1347
|
-
init_api_client();
|
|
1348
|
-
APP_ID = "quickpeek";
|
|
1349
|
-
PRO_STATUSES = ["active", "trialing", "lifetime"];
|
|
1350
|
-
CACHE_TTL_MS = 6e4;
|
|
1351
|
-
responseCache = /* @__PURE__ */ new Map();
|
|
1352
1483
|
}
|
|
1353
1484
|
});
|
|
1354
1485
|
|
|
1355
1486
|
// src/lib/tts/helpers.ts
|
|
1356
1487
|
function toWSLPathForTTS(path) {
|
|
1357
|
-
return
|
|
1488
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path;
|
|
1358
1489
|
}
|
|
1359
1490
|
function estimateActionDuration(step) {
|
|
1360
1491
|
if (step.action === "drag" && step.value === "around") {
|
|
@@ -1384,8 +1515,8 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1384
1515
|
if (!["mp4", "webm", "mov"].includes(ext)) {
|
|
1385
1516
|
return 0;
|
|
1386
1517
|
}
|
|
1387
|
-
const mediaPath = (0,
|
|
1388
|
-
if (!(0,
|
|
1518
|
+
const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
|
|
1519
|
+
if (!(0, import_node_fs2.existsSync)(mediaPath)) {
|
|
1389
1520
|
return 0;
|
|
1390
1521
|
}
|
|
1391
1522
|
try {
|
|
@@ -1405,38 +1536,38 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1405
1536
|
}
|
|
1406
1537
|
}
|
|
1407
1538
|
async function loadVoiceoverMeta(outputDir) {
|
|
1408
|
-
const metaPath = (0,
|
|
1539
|
+
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1409
1540
|
try {
|
|
1410
|
-
if (!(0,
|
|
1411
|
-
const content = await (0,
|
|
1541
|
+
if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
|
|
1542
|
+
const content = await (0, import_promises2.readFile)(metaPath, "utf-8");
|
|
1412
1543
|
return JSON.parse(content);
|
|
1413
1544
|
} catch {
|
|
1414
1545
|
return null;
|
|
1415
1546
|
}
|
|
1416
1547
|
}
|
|
1417
1548
|
async function saveVoiceoverMeta(outputDir, meta) {
|
|
1418
|
-
const metaPath = (0,
|
|
1419
|
-
await (0,
|
|
1549
|
+
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1550
|
+
await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
1420
1551
|
}
|
|
1421
1552
|
async function concatenateAudio(inputs, output, workDir) {
|
|
1422
1553
|
const formatPath = (f) => {
|
|
1423
|
-
const resolved = (0,
|
|
1424
|
-
return isWindows ?
|
|
1554
|
+
const resolved = (0, import_node_path2.resolve)(f);
|
|
1555
|
+
return isWindows ? (0, import_video_kit2.windowsDrivePathToWsl)(resolved) ?? resolved : resolved;
|
|
1425
1556
|
};
|
|
1426
1557
|
const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
|
|
1427
|
-
const listPath = (0,
|
|
1428
|
-
await (0,
|
|
1558
|
+
const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
|
|
1559
|
+
await (0, import_promises2.writeFile)(listPath, listContent);
|
|
1429
1560
|
await concatMedia(listPath, output);
|
|
1430
|
-
await (0,
|
|
1561
|
+
await (0, import_promises2.unlink)(listPath).catch(() => {
|
|
1431
1562
|
});
|
|
1432
1563
|
}
|
|
1433
|
-
var
|
|
1564
|
+
var import_node_fs2, import_promises2, import_node_path2, isWindows;
|
|
1434
1565
|
var init_helpers = __esm({
|
|
1435
1566
|
"src/lib/tts/helpers.ts"() {
|
|
1436
1567
|
"use strict";
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1568
|
+
import_node_fs2 = require("fs");
|
|
1569
|
+
import_promises2 = require("fs/promises");
|
|
1570
|
+
import_node_path2 = require("path");
|
|
1440
1571
|
init_ffmpeg2();
|
|
1441
1572
|
init_utils();
|
|
1442
1573
|
isWindows = process.platform === "win32";
|
|
@@ -1472,7 +1603,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1472
1603
|
if (!needsRealignment) {
|
|
1473
1604
|
return stepAudio;
|
|
1474
1605
|
}
|
|
1475
|
-
const audioDir = (0,
|
|
1606
|
+
const audioDir = (0, import_node_path3.join)(workDir, "audio");
|
|
1476
1607
|
const segments = [];
|
|
1477
1608
|
const alignedStepAudio = /* @__PURE__ */ new Map();
|
|
1478
1609
|
for (let i = 0; i < actualRecordingDurations.length; i++) {
|
|
@@ -1481,20 +1612,20 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1481
1612
|
const voiceDuration = voiceData?.duration ?? 0;
|
|
1482
1613
|
const recordingDuration = actualRecordingDurations[i] ?? 0;
|
|
1483
1614
|
const targetDuration = Math.max(voiceDuration, recordingDuration);
|
|
1484
|
-
const audioPath = (0,
|
|
1485
|
-
const originalPaddingPath = (0,
|
|
1486
|
-
const silencePath = (0,
|
|
1487
|
-
if ((0,
|
|
1615
|
+
const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1616
|
+
const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1617
|
+
const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1618
|
+
if ((0, import_node_fs3.existsSync)(audioPath)) {
|
|
1488
1619
|
segments.push(audioPath);
|
|
1489
|
-
if ((0,
|
|
1620
|
+
if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
|
|
1490
1621
|
segments.push(originalPaddingPath);
|
|
1491
1622
|
}
|
|
1492
1623
|
if (recordingDuration > voiceDuration) {
|
|
1493
|
-
const alignPaddingPath = (0,
|
|
1624
|
+
const alignPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
|
|
1494
1625
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1495
1626
|
segments.push(alignPaddingPath);
|
|
1496
1627
|
}
|
|
1497
|
-
} else if ((0,
|
|
1628
|
+
} else if ((0, import_node_fs3.existsSync)(silencePath)) {
|
|
1498
1629
|
if (targetDuration !== voiceDuration) {
|
|
1499
1630
|
await generateSilence(silencePath, targetDuration);
|
|
1500
1631
|
}
|
|
@@ -1506,7 +1637,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1506
1637
|
});
|
|
1507
1638
|
}
|
|
1508
1639
|
if (segments.length > 0) {
|
|
1509
|
-
const voiceoverPath = (0,
|
|
1640
|
+
const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
|
|
1510
1641
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1511
1642
|
}
|
|
1512
1643
|
const existingMeta = await loadVoiceoverMeta(outputDir);
|
|
@@ -1518,12 +1649,12 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1518
1649
|
}
|
|
1519
1650
|
return alignedStepAudio;
|
|
1520
1651
|
}
|
|
1521
|
-
var
|
|
1652
|
+
var import_node_fs3, import_node_path3;
|
|
1522
1653
|
var init_timing = __esm({
|
|
1523
1654
|
"src/lib/tts/timing.ts"() {
|
|
1524
1655
|
"use strict";
|
|
1525
|
-
|
|
1526
|
-
|
|
1656
|
+
import_node_fs3 = require("fs");
|
|
1657
|
+
import_node_path3 = require("path");
|
|
1527
1658
|
init_ffmpeg2();
|
|
1528
1659
|
init_helpers();
|
|
1529
1660
|
}
|
|
@@ -1554,7 +1685,7 @@ function canReuseVoiceover(steps, voice, existingMeta) {
|
|
|
1554
1685
|
}
|
|
1555
1686
|
async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
1556
1687
|
const { estimateWordTimings: estimateWordTimings2 } = await Promise.resolve().then(() => (init_timing(), timing_exports));
|
|
1557
|
-
const voiceoverPath = (0,
|
|
1688
|
+
const voiceoverPath = (0, import_node_path4.join)(outputDir, "voiceover.wav");
|
|
1558
1689
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1559
1690
|
for (let i = 0; i < steps.length; i++) {
|
|
1560
1691
|
const step = steps[i];
|
|
@@ -1567,97 +1698,29 @@ async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
|
1567
1698
|
}
|
|
1568
1699
|
return { voiceoverPath, stepAudio };
|
|
1569
1700
|
}
|
|
1570
|
-
var
|
|
1701
|
+
var import_node_path4;
|
|
1571
1702
|
var init_cache = __esm({
|
|
1572
1703
|
"src/lib/tts/cache.ts"() {
|
|
1573
1704
|
"use strict";
|
|
1574
|
-
|
|
1705
|
+
import_node_path4 = require("path");
|
|
1575
1706
|
init_helpers();
|
|
1576
1707
|
}
|
|
1577
1708
|
});
|
|
1578
1709
|
|
|
1579
1710
|
// src/lib/voices.ts
|
|
1580
|
-
var
|
|
1711
|
+
var import_video_kit5;
|
|
1581
1712
|
var init_voices = __esm({
|
|
1582
1713
|
"src/lib/voices.ts"() {
|
|
1583
1714
|
"use strict";
|
|
1584
|
-
|
|
1585
|
-
en: "en-GB-SoniaNeural",
|
|
1586
|
-
ar: "ar-LB-LaylaNeural",
|
|
1587
|
-
es: "es-ES-ElviraNeural",
|
|
1588
|
-
fr: "fr-FR-DeniseNeural",
|
|
1589
|
-
de: "de-DE-KatjaNeural",
|
|
1590
|
-
it: "it-IT-ElsaNeural",
|
|
1591
|
-
pt: "pt-BR-FranciscaNeural",
|
|
1592
|
-
ru: "ru-RU-SvetlanaNeural",
|
|
1593
|
-
zh: "zh-CN-XiaoxiaoNeural",
|
|
1594
|
-
ja: "ja-JP-NanamiNeural",
|
|
1595
|
-
ko: "ko-KR-SunHiNeural",
|
|
1596
|
-
hi: "hi-IN-SwaraNeural",
|
|
1597
|
-
tr: "tr-TR-EmelNeural",
|
|
1598
|
-
nl: "nl-NL-ColetteNeural",
|
|
1599
|
-
pl: "pl-PL-AgnieszkaNeural",
|
|
1600
|
-
sv: "sv-SE-SofieNeural",
|
|
1601
|
-
he: "he-IL-HilaNeural",
|
|
1602
|
-
th: "th-TH-PremwadeeNeural",
|
|
1603
|
-
vi: "vi-VN-HoaiMyNeural",
|
|
1604
|
-
id: "id-ID-GadisNeural",
|
|
1605
|
-
ms: "ms-MY-YasminNeural",
|
|
1606
|
-
fil: "fil-PH-BlessicaNeural",
|
|
1607
|
-
uk: "uk-UA-PolinaNeural",
|
|
1608
|
-
cs: "cs-CZ-VlastaNeural",
|
|
1609
|
-
el: "el-GR-AthinaNeural",
|
|
1610
|
-
ro: "ro-RO-AlinaNeural",
|
|
1611
|
-
hu: "hu-HU-NoemiNeural",
|
|
1612
|
-
da: "da-DK-ChristelNeural",
|
|
1613
|
-
fi: "fi-FI-NooraNeural",
|
|
1614
|
-
no: "nb-NO-PernilleNeural",
|
|
1615
|
-
sk: "sk-SK-ViktoriaNeural",
|
|
1616
|
-
bg: "bg-BG-KalinaNeural",
|
|
1617
|
-
hr: "hr-HR-GabrijelaNeural",
|
|
1618
|
-
sl: "sl-SI-PetraNeural",
|
|
1619
|
-
et: "et-EE-AnuNeural",
|
|
1620
|
-
lv: "lv-LV-EveritaNeural",
|
|
1621
|
-
lt: "lt-LT-OnaNeural",
|
|
1622
|
-
bn: "bn-IN-TanishaaNeural",
|
|
1623
|
-
ta: "ta-IN-PallaviNeural",
|
|
1624
|
-
te: "te-IN-ShrutiNeural",
|
|
1625
|
-
mr: "mr-IN-AarohiNeural",
|
|
1626
|
-
gu: "gu-IN-DhwaniNeural",
|
|
1627
|
-
kn: "kn-IN-SapnaNeural",
|
|
1628
|
-
ml: "ml-IN-SobhanaNeural",
|
|
1629
|
-
ur: "ur-PK-UzmaNeural",
|
|
1630
|
-
fa: "fa-IR-DilaraNeural",
|
|
1631
|
-
sw: "sw-KE-ZuriNeural",
|
|
1632
|
-
af: "af-ZA-AdriNeural",
|
|
1633
|
-
ca: "ca-ES-JoanaNeural",
|
|
1634
|
-
cy: "cy-GB-NiaNeural",
|
|
1635
|
-
ga: "ga-IE-OrlaNeural",
|
|
1636
|
-
mt: "mt-MT-GraceNeural",
|
|
1637
|
-
is: "is-IS-GudrunNeural",
|
|
1638
|
-
mk: "mk-MK-MarijaNeural",
|
|
1639
|
-
sr: "sr-RS-SophieNeural",
|
|
1640
|
-
bs: "bs-BA-VesnaNeural",
|
|
1641
|
-
sq: "sq-AL-AnilaNeural",
|
|
1642
|
-
am: "am-ET-MekdesNeural",
|
|
1643
|
-
az: "az-AZ-BabekNeural",
|
|
1644
|
-
ka: "ka-GE-EkaNeural",
|
|
1645
|
-
kk: "kk-KZ-AigulNeural",
|
|
1646
|
-
mn: "mn-MN-YesUINeural",
|
|
1647
|
-
ne: "ne-NP-HemkalaNeural",
|
|
1648
|
-
ps: "ps-AF-LatifaNeural",
|
|
1649
|
-
si: "si-LK-ThiliniNeural",
|
|
1650
|
-
so: "so-SO-UbaxNeural",
|
|
1651
|
-
zu: "zu-ZA-ThandoNeural"
|
|
1652
|
-
};
|
|
1715
|
+
import_video_kit5 = require("@spark-apps/video-kit");
|
|
1653
1716
|
}
|
|
1654
1717
|
});
|
|
1655
1718
|
|
|
1656
1719
|
// src/lib/tts/synthesize.ts
|
|
1657
1720
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1658
|
-
const tempScriptPath = (0,
|
|
1659
|
-
const wslScriptPath = isWindows ? toWSLPathForTTS((0,
|
|
1660
|
-
const wslOutputPath = isWindows ? toWSLPathForTTS((0,
|
|
1721
|
+
const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1722
|
+
const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
|
|
1723
|
+
const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
|
|
1661
1724
|
const pythonScript = `
|
|
1662
1725
|
import asyncio
|
|
1663
1726
|
from edge_tts import Communicate
|
|
@@ -1672,7 +1735,7 @@ async def main():
|
|
|
1672
1735
|
asyncio.run(main())
|
|
1673
1736
|
`;
|
|
1674
1737
|
try {
|
|
1675
|
-
await (0,
|
|
1738
|
+
await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
|
|
1676
1739
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1677
1740
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1678
1741
|
await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
|
|
@@ -1700,13 +1763,13 @@ asyncio.run(main())
|
|
|
1700
1763
|
}
|
|
1701
1764
|
}
|
|
1702
1765
|
}
|
|
1703
|
-
var import_node_child_process2,
|
|
1766
|
+
var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
|
|
1704
1767
|
var init_synthesize = __esm({
|
|
1705
1768
|
"src/lib/tts/synthesize.ts"() {
|
|
1706
1769
|
"use strict";
|
|
1707
1770
|
import_node_child_process2 = require("child_process");
|
|
1708
|
-
|
|
1709
|
-
|
|
1771
|
+
import_promises3 = require("fs/promises");
|
|
1772
|
+
import_node_path5 = require("path");
|
|
1710
1773
|
import_node_util = require("util");
|
|
1711
1774
|
init_helpers();
|
|
1712
1775
|
execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
|
|
@@ -1720,7 +1783,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
|
1720
1783
|
}
|
|
1721
1784
|
function resolveVoice(config) {
|
|
1722
1785
|
if (config.audio?.voice) return config.audio.voice;
|
|
1723
|
-
if (config.lang) return
|
|
1786
|
+
if (config.lang) return import_video_kit5.WIDE_VOICES[config.lang] || null;
|
|
1724
1787
|
return null;
|
|
1725
1788
|
}
|
|
1726
1789
|
async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
|
|
@@ -1745,8 +1808,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1745
1808
|
info2("No voice configured");
|
|
1746
1809
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1747
1810
|
}
|
|
1748
|
-
const audioDir = (0,
|
|
1749
|
-
await (0,
|
|
1811
|
+
const audioDir = (0, import_node_path6.join)(workDir, "audio");
|
|
1812
|
+
await (0, import_promises4.mkdir)(audioDir, { recursive: true });
|
|
1750
1813
|
const segments = [];
|
|
1751
1814
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1752
1815
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1758,13 +1821,13 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1758
1821
|
const manualDuration = (step.duration ?? 0) * 1e3;
|
|
1759
1822
|
const mediaDuration = await getMediaStepDuration(step, outputDir);
|
|
1760
1823
|
const silenceDuration = Math.max(actionDuration, manualDuration, mediaDuration, config.timing.minStepDuration);
|
|
1761
|
-
const silencePath = (0,
|
|
1824
|
+
const silencePath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1762
1825
|
await generateSilence(silencePath, silenceDuration);
|
|
1763
1826
|
segments.push(silencePath);
|
|
1764
1827
|
stepAudio.set(stepNum, { duration: silenceDuration, words: [] });
|
|
1765
1828
|
continue;
|
|
1766
1829
|
}
|
|
1767
|
-
const audioPath = (0,
|
|
1830
|
+
const audioPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1768
1831
|
if (segments.length > 0) await new Promise((r) => setTimeout(r, 200));
|
|
1769
1832
|
try {
|
|
1770
1833
|
await synthesizeSpeech(step.caption, audioPath, voice);
|
|
@@ -1781,7 +1844,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1781
1844
|
mediaDuration
|
|
1782
1845
|
);
|
|
1783
1846
|
if (stepDuration > audioDuration) {
|
|
1784
|
-
const paddingPath = (0,
|
|
1847
|
+
const paddingPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1785
1848
|
await generateSilence(paddingPath, stepDuration - audioDuration);
|
|
1786
1849
|
segments.push(paddingPath);
|
|
1787
1850
|
}
|
|
@@ -1797,8 +1860,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1797
1860
|
return { voiceoverPath: "", stepAudio };
|
|
1798
1861
|
}
|
|
1799
1862
|
const concatTask = task2("Combining audio");
|
|
1800
|
-
await (0,
|
|
1801
|
-
const voiceoverPath = (0,
|
|
1863
|
+
await (0, import_promises4.mkdir)(outputDir, { recursive: true });
|
|
1864
|
+
const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
|
|
1802
1865
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1803
1866
|
concatTask.succeed("Done");
|
|
1804
1867
|
const meta = {
|
|
@@ -1811,12 +1874,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1811
1874
|
await saveVoiceoverMeta(outputDir, meta);
|
|
1812
1875
|
return { voiceoverPath, stepAudio };
|
|
1813
1876
|
}
|
|
1814
|
-
var
|
|
1877
|
+
var import_promises4, import_node_path6;
|
|
1815
1878
|
var init_generate = __esm({
|
|
1816
1879
|
"src/lib/tts/generate.ts"() {
|
|
1817
1880
|
"use strict";
|
|
1818
|
-
|
|
1819
|
-
|
|
1881
|
+
import_promises4 = require("fs/promises");
|
|
1882
|
+
import_node_path6 = require("path");
|
|
1820
1883
|
init_ffmpeg2();
|
|
1821
1884
|
init_voices();
|
|
1822
1885
|
init_helpers();
|
|
@@ -1869,7 +1932,9 @@ __export(lib_exports, {
|
|
|
1869
1932
|
DEFAULT_CAPTIONS: () => DEFAULT_CAPTIONS,
|
|
1870
1933
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
1871
1934
|
EXCLUDED_LINK_PATTERNS: () => EXCLUDED_LINK_PATTERNS,
|
|
1872
|
-
|
|
1935
|
+
FEATURES: () => FEATURES,
|
|
1936
|
+
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1937
|
+
LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
|
|
1873
1938
|
VERSION: () => VERSION,
|
|
1874
1939
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1875
1940
|
applyTransitions: () => applyTransitions,
|
|
@@ -1879,8 +1944,8 @@ __export(lib_exports, {
|
|
|
1879
1944
|
buildUserPrompt: () => buildUserPrompt,
|
|
1880
1945
|
buildWatermarkFilters: () => buildWatermarkFilters,
|
|
1881
1946
|
callAI: () => callAI,
|
|
1947
|
+
callAIViaRelay: () => callAIViaRelay,
|
|
1882
1948
|
canReuseVoiceover: () => canReuseVoiceover,
|
|
1883
|
-
checkVerificationStatus: () => checkVerificationStatus,
|
|
1884
1949
|
composeVideo: () => composeVideo,
|
|
1885
1950
|
concatMedia: () => concatMedia,
|
|
1886
1951
|
concatVideoWithOutro: () => concatVideoWithOutro,
|
|
@@ -1889,30 +1954,38 @@ __export(lib_exports, {
|
|
|
1889
1954
|
generateASSHeader: () => generateASSHeader,
|
|
1890
1955
|
generateSilence: () => generateSilence,
|
|
1891
1956
|
generateVoiceover: () => generateVoiceover,
|
|
1892
|
-
getBackendUrls: () => getBackendUrls,
|
|
1893
|
-
getCredentials: () => getCredentials,
|
|
1894
1957
|
getErrorMessage: () => getErrorMessage,
|
|
1895
|
-
getGroqApiKey: () => getGroqApiKey,
|
|
1896
1958
|
getLanguageName: () => getLanguageName,
|
|
1897
1959
|
getMediaDuration: () => getMediaDuration,
|
|
1898
|
-
|
|
1899
|
-
|
|
1960
|
+
getStatus: () => getStatus,
|
|
1961
|
+
getStatusCached: () => getStatusCached,
|
|
1962
|
+
getTierByEmail: () => getTierByEmail,
|
|
1900
1963
|
getUpgradeUrl: () => getUpgradeUrl,
|
|
1901
|
-
|
|
1902
|
-
|
|
1964
|
+
hasAccess: () => hasAccess,
|
|
1965
|
+
hasTrialRemaining: () => hasTrialRemaining,
|
|
1966
|
+
hexToASS: () => import_video_kit.hexToAss,
|
|
1967
|
+
isCanceling: () => isCanceling,
|
|
1968
|
+
isExcludedLink: () => isExcludedLink,
|
|
1969
|
+
isPaid: () => isPaid,
|
|
1970
|
+
isVerified: () => isVerified,
|
|
1903
1971
|
loadExistingVoiceover: () => loadExistingVoiceover,
|
|
1904
1972
|
loadVoiceoverMeta: () => loadVoiceoverMeta,
|
|
1905
1973
|
mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
|
|
1974
|
+
normalizeUrl: () => normalizeUrl,
|
|
1975
|
+
parseAIPlanResponse: () => parseAIPlanResponse,
|
|
1906
1976
|
parsePartial: () => parsePartial,
|
|
1977
|
+
pricingUrl: () => pricingUrl,
|
|
1907
1978
|
registerFreeUser: () => registerFreeUser,
|
|
1908
1979
|
resolveVoice: () => resolveVoice,
|
|
1909
1980
|
rgbaToHex: () => rgbaToHex,
|
|
1981
|
+
shouldSkipLink: () => shouldSkipLink,
|
|
1910
1982
|
shouldWatermark: () => shouldWatermark,
|
|
1983
|
+
showPaywall: () => showPaywall,
|
|
1911
1984
|
speedUpVideo: () => speedUpVideo,
|
|
1912
1985
|
spliceVideos: () => spliceVideos,
|
|
1913
1986
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
1914
1987
|
toTitleCase: () => toTitleCase,
|
|
1915
|
-
windowsPathToWSL: () =>
|
|
1988
|
+
windowsPathToWSL: () => import_video_kit2.windowsDrivePathToWsl
|
|
1916
1989
|
});
|
|
1917
1990
|
var init_lib = __esm({
|
|
1918
1991
|
"src/lib/index.ts"() {
|
|
@@ -1924,6 +1997,7 @@ var init_lib = __esm({
|
|
|
1924
1997
|
init_cursor();
|
|
1925
1998
|
init_ffmpeg2();
|
|
1926
1999
|
init_prompts();
|
|
2000
|
+
init_SparkStripe_feature_gating();
|
|
1927
2001
|
init_scraping();
|
|
1928
2002
|
init_styles();
|
|
1929
2003
|
init_subscription_client();
|
|
@@ -1937,134 +2011,692 @@ var init_lib = __esm({
|
|
|
1937
2011
|
|
|
1938
2012
|
// src/qp.ts
|
|
1939
2013
|
var import_dotenv = __toESM(require("dotenv"));
|
|
1940
|
-
var
|
|
2014
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
1941
2015
|
|
|
1942
|
-
// src/commands/
|
|
1943
|
-
var
|
|
1944
|
-
var
|
|
1945
|
-
|
|
1946
|
-
// src/ai/narrate.ts
|
|
1947
|
-
init_lib();
|
|
2016
|
+
// src/commands/auth-cmd.ts
|
|
2017
|
+
var import_node_readline2 = require("readline");
|
|
2018
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
2019
|
+
var import_playwright2 = require("playwright");
|
|
1948
2020
|
|
|
1949
|
-
// src/
|
|
1950
|
-
var
|
|
1951
|
-
var
|
|
1952
|
-
|
|
1953
|
-
var
|
|
1954
|
-
|
|
1955
|
-
|
|
2021
|
+
// src/browser/auth-state.ts
|
|
2022
|
+
var import_node_fs = require("fs");
|
|
2023
|
+
var import_promises = require("fs/promises");
|
|
2024
|
+
var import_node_os = require("os");
|
|
2025
|
+
var import_node_path = require("path");
|
|
2026
|
+
var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
|
|
2027
|
+
function hostOf(url) {
|
|
2028
|
+
try {
|
|
2029
|
+
return new URL(url).host;
|
|
2030
|
+
} catch {
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
1956
2033
|
}
|
|
1957
|
-
function
|
|
1958
|
-
const
|
|
1959
|
-
return
|
|
1960
|
-
rl.question(question, (answer) => {
|
|
1961
|
-
rl.close();
|
|
1962
|
-
resolve9(answer.trim());
|
|
1963
|
-
});
|
|
1964
|
-
});
|
|
2034
|
+
function authStatePath(url) {
|
|
2035
|
+
const host = hostOf(url);
|
|
2036
|
+
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
|
|
1965
2037
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
console.log(import_chalk.default.dim(" \u2500".repeat(20)));
|
|
1970
|
-
console.log();
|
|
1971
|
-
console.log(import_chalk.default.white(" QuickPeek uses your email to track usage and"));
|
|
1972
|
-
console.log(import_chalk.default.white(" unlock Pro features when you subscribe."));
|
|
1973
|
-
console.log();
|
|
1974
|
-
const email = await prompt(import_chalk.default.white(" Email: "));
|
|
1975
|
-
if (!email) {
|
|
1976
|
-
console.log();
|
|
1977
|
-
console.log(import_chalk.default.red(" No email provided. Exiting."));
|
|
1978
|
-
process.exit(1);
|
|
1979
|
-
}
|
|
1980
|
-
if (!email.includes("@") || !email.includes(".")) {
|
|
1981
|
-
console.log();
|
|
1982
|
-
console.log(import_chalk.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
1983
|
-
const proceed = await prompt(import_chalk.default.white(" Continue anyway? ") + import_chalk.default.dim("[y/N] "));
|
|
1984
|
-
if (proceed.toLowerCase() !== "y") {
|
|
1985
|
-
process.exit(1);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
console.log();
|
|
1989
|
-
console.log(import_chalk.default.green(" \u2713 Email registered"));
|
|
1990
|
-
console.log();
|
|
1991
|
-
return email;
|
|
2038
|
+
function findAuthState(url) {
|
|
2039
|
+
const path = authStatePath(url);
|
|
2040
|
+
return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
|
|
1992
2041
|
}
|
|
1993
|
-
function
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
console.log();
|
|
2001
|
-
console.log(import_chalk.default.dim(" Get a key at: https://sparkbrain.app/dashboard"));
|
|
2002
|
-
console.log();
|
|
2003
|
-
process.exit(1);
|
|
2004
|
-
}
|
|
2042
|
+
async function saveAuthState(url, state) {
|
|
2043
|
+
const path = authStatePath(url);
|
|
2044
|
+
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2045
|
+
await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
|
|
2046
|
+
await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
|
|
2047
|
+
await (0, import_promises.chmod)(path, 384);
|
|
2048
|
+
return path;
|
|
2005
2049
|
}
|
|
2006
|
-
async function
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
console.log(import_chalk.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2013
|
-
console.log();
|
|
2014
|
-
console.log(import_chalk.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2015
|
-
console.log(import_chalk.default.dim(" Options:"));
|
|
2016
|
-
if (result.retryAfterSeconds) {
|
|
2017
|
-
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2018
|
-
console.log(import_chalk.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2019
|
-
} else {
|
|
2020
|
-
console.log(import_chalk.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2021
|
-
}
|
|
2022
|
-
console.log(import_chalk.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2023
|
-
console.log(import_chalk.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2024
|
-
console.log();
|
|
2025
|
-
process.exit(1);
|
|
2026
|
-
}
|
|
2027
|
-
if (result.isAuthError) {
|
|
2028
|
-
console.log();
|
|
2029
|
-
console.log(import_chalk.default.red(" \u26A0 Authentication Error"));
|
|
2030
|
-
console.log();
|
|
2031
|
-
console.log(import_chalk.default.dim(` ${result.error}`));
|
|
2032
|
-
console.log();
|
|
2033
|
-
process.exit(1);
|
|
2034
|
-
}
|
|
2035
|
-
console.log();
|
|
2036
|
-
console.log(import_chalk.default.red(" \u26A0 AI Error"));
|
|
2037
|
-
console.log();
|
|
2038
|
-
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2039
|
-
console.log(import_chalk.default.dim(` ${firstLine.slice(0, 80)}`));
|
|
2040
|
-
console.log();
|
|
2041
|
-
process.exit(1);
|
|
2050
|
+
async function clearAuthState(url) {
|
|
2051
|
+
if (url) {
|
|
2052
|
+
const path = authStatePath(url);
|
|
2053
|
+
if (!path || !(0, import_node_fs.existsSync)(path)) return [];
|
|
2054
|
+
await (0, import_promises.unlink)(path);
|
|
2055
|
+
return [hostOf(url)];
|
|
2042
2056
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2057
|
+
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2058
|
+
const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
|
|
2059
|
+
await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
|
|
2060
|
+
return files.map((f) => f.slice(0, -".json".length));
|
|
2061
|
+
}
|
|
2062
|
+
async function listAuthStates() {
|
|
2063
|
+
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2064
|
+
return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
|
|
2045
2065
|
}
|
|
2046
2066
|
|
|
2047
|
-
// src/
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
const stepList = steps.map((s, i) => {
|
|
2051
|
-
const parts = [`${i + 1}. ${s.action}`];
|
|
2052
|
-
if (s.target) parts.push(`on "${s.target}"`);
|
|
2053
|
-
if (s.value) parts.push(`= "${s.value}"`);
|
|
2054
|
-
return parts.join(" ");
|
|
2055
|
-
}).join("\n");
|
|
2056
|
-
const systemPrompt = `You write professional demo video captions in ${langName}.
|
|
2057
|
-
Given UI steps, return a JSON array of captions \u2014 one per step, in order.
|
|
2058
|
-
|
|
2059
|
-
RULES:
|
|
2060
|
-
- Return ONLY a valid JSON array of strings, no explanation
|
|
2061
|
-
- Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
|
|
2062
|
-
- Describe what the user is doing and why it matters in the context of the app
|
|
2067
|
+
// src/help.ts
|
|
2068
|
+
var import_chalk = __toESM(require("chalk"));
|
|
2069
|
+
init_lib();
|
|
2063
2070
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2071
|
+
// src/logo.ts
|
|
2072
|
+
var import_figlet = __toESM(require("figlet"));
|
|
2073
|
+
var import_gradient_string = __toESM(require("gradient-string"));
|
|
2074
|
+
var DEFAULT_LOGO_CONFIG = {
|
|
2075
|
+
text: "QuickPeek",
|
|
2076
|
+
font: "Slant",
|
|
2077
|
+
horizontalLayout: "default",
|
|
2078
|
+
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2079
|
+
suffix: " \u{1F440}"
|
|
2080
|
+
};
|
|
2081
|
+
function renderLogo(config = {}) {
|
|
2082
|
+
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2083
|
+
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
2084
|
+
font: resolved.font,
|
|
2085
|
+
horizontalLayout: resolved.horizontalLayout
|
|
2086
|
+
});
|
|
2087
|
+
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
2088
|
+
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// src/utils/config-loader.ts
|
|
2092
|
+
var import_node_fs4 = require("fs");
|
|
2093
|
+
var import_promises5 = require("fs/promises");
|
|
2094
|
+
var import_node_os2 = require("os");
|
|
2095
|
+
var import_node_path7 = require("path");
|
|
2096
|
+
init_lib();
|
|
2097
|
+
async function loadConfig(_url, langOverride) {
|
|
2098
|
+
const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
|
|
2099
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2100
|
+
try {
|
|
2101
|
+
const content = await (0, import_promises5.readFile)(configPath, "utf-8");
|
|
2102
|
+
const userConfig = JSON.parse(content);
|
|
2103
|
+
const config = {
|
|
2104
|
+
lang: langOverride || userConfig.lang || "en",
|
|
2105
|
+
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2106
|
+
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2107
|
+
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2108
|
+
music: userConfig.music,
|
|
2109
|
+
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2110
|
+
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2111
|
+
};
|
|
2112
|
+
return { config, created: false };
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
const fullDefaultConfig = {
|
|
2117
|
+
lang: "en",
|
|
2118
|
+
plan: { maxSteps: 8 },
|
|
2119
|
+
video: {
|
|
2120
|
+
width: 1080,
|
|
2121
|
+
height: 1920,
|
|
2122
|
+
fps: 30,
|
|
2123
|
+
quality: 0.5,
|
|
2124
|
+
preset: "fast",
|
|
2125
|
+
cursor: true,
|
|
2126
|
+
highlight: false,
|
|
2127
|
+
fadeIn: 500,
|
|
2128
|
+
// ms
|
|
2129
|
+
fadeOut: 500,
|
|
2130
|
+
// ms
|
|
2131
|
+
contrast: 0.25
|
|
2132
|
+
},
|
|
2133
|
+
audio: {
|
|
2134
|
+
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2135
|
+
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2136
|
+
speed: 1
|
|
2137
|
+
},
|
|
2138
|
+
music: {
|
|
2139
|
+
path: "",
|
|
2140
|
+
volume: 0.3
|
|
2141
|
+
},
|
|
2142
|
+
output: { dir: "./demo" }
|
|
2143
|
+
};
|
|
2144
|
+
await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2145
|
+
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2146
|
+
}
|
|
2147
|
+
var WORK_DIR = ".quickpeek";
|
|
2148
|
+
async function prepareWorkDir() {
|
|
2149
|
+
const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
|
|
2150
|
+
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2151
|
+
await (0, import_promises5.rm)(workDir, { recursive: true });
|
|
2152
|
+
}
|
|
2153
|
+
await (0, import_promises5.mkdir)(workDir, { recursive: true });
|
|
2154
|
+
return workDir;
|
|
2155
|
+
}
|
|
2156
|
+
var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
|
|
2157
|
+
var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
|
|
2158
|
+
async function loadUserData() {
|
|
2159
|
+
try {
|
|
2160
|
+
if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
|
|
2161
|
+
return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2162
|
+
}
|
|
2163
|
+
} catch {
|
|
2164
|
+
}
|
|
2165
|
+
return {};
|
|
2166
|
+
}
|
|
2167
|
+
async function saveUserData(userData) {
|
|
2168
|
+
if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
|
|
2169
|
+
await (0, import_promises5.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2170
|
+
}
|
|
2171
|
+
const existing = await loadUserData();
|
|
2172
|
+
const merged = { ...existing, ...userData };
|
|
2173
|
+
await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2174
|
+
}
|
|
2175
|
+
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2176
|
+
font-family: Arial, sans-serif;
|
|
2177
|
+
-webkit-text-stroke: 3px #000000;
|
|
2178
|
+
}
|
|
2179
|
+
.caption .regular {
|
|
2180
|
+
font-size: 56px;
|
|
2181
|
+
color: #FFFFFF;
|
|
2182
|
+
}
|
|
2183
|
+
.caption .highlight {
|
|
2184
|
+
font-size: 64px;
|
|
2185
|
+
color: #FFFF00;
|
|
2186
|
+
}
|
|
2187
|
+
`;
|
|
2188
|
+
function parseCaptionCSS(css) {
|
|
2189
|
+
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2190
|
+
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
2191
|
+
const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
|
|
2192
|
+
const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
|
|
2193
|
+
const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
|
|
2194
|
+
if (fontMatch) style.font = fontMatch[1].trim();
|
|
2195
|
+
const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
|
|
2196
|
+
if (strokeMatch) {
|
|
2197
|
+
style.outlineWidth = parseInt(strokeMatch[1], 10);
|
|
2198
|
+
style.outline = rgbaToHex(strokeMatch[2]);
|
|
2199
|
+
}
|
|
2200
|
+
const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
|
|
2201
|
+
if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
|
|
2202
|
+
const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
|
|
2203
|
+
if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
|
|
2204
|
+
const parseWordStyle = (block, target) => {
|
|
2205
|
+
const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2206
|
+
if (colorMatch) target.color = colorMatch[1];
|
|
2207
|
+
const sizeMatch = block.match(/font-size:\s*(\d+)/);
|
|
2208
|
+
if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
|
|
2209
|
+
const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2210
|
+
if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2211
|
+
};
|
|
2212
|
+
parseWordStyle(regularBlock, style.regular);
|
|
2213
|
+
parseWordStyle(highlightBlock, style.highlight);
|
|
2214
|
+
if (!regularBlock && !highlightBlock) {
|
|
2215
|
+
const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2216
|
+
if (colorMatch) style.regular.color = colorMatch[1];
|
|
2217
|
+
const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
|
|
2218
|
+
if (sizeMatch) {
|
|
2219
|
+
style.regular.size = parseInt(sizeMatch[1], 10);
|
|
2220
|
+
style.highlight.size = Math.round(style.regular.size * 1.15);
|
|
2221
|
+
}
|
|
2222
|
+
const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
|
|
2223
|
+
if (highlightMatch) style.highlight.color = highlightMatch[1];
|
|
2224
|
+
const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2225
|
+
if (weightMatch) {
|
|
2226
|
+
const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2227
|
+
style.regular.bold = isBold;
|
|
2228
|
+
style.highlight.bold = isBold;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return style;
|
|
2232
|
+
}
|
|
2233
|
+
async function loadCaptions() {
|
|
2234
|
+
const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
|
|
2235
|
+
if (!(0, import_node_fs4.existsSync)(captionsPath)) {
|
|
2236
|
+
await (0, import_promises5.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2237
|
+
return DEFAULT_CAPTIONS;
|
|
2238
|
+
}
|
|
2239
|
+
try {
|
|
2240
|
+
const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
|
|
2241
|
+
return parseCaptionCSS(css);
|
|
2242
|
+
} catch {
|
|
2243
|
+
return DEFAULT_CAPTIONS;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async function savePlan(plan, planPath) {
|
|
2247
|
+
const cleanPlan = {
|
|
2248
|
+
...plan,
|
|
2249
|
+
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2250
|
+
};
|
|
2251
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
|
|
2252
|
+
await (0, import_promises5.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/help.ts
|
|
2256
|
+
var cmd = import_chalk.default.cyan;
|
|
2257
|
+
var arg = import_chalk.default.green;
|
|
2258
|
+
var opt = import_chalk.default.magenta;
|
|
2259
|
+
var desc = import_chalk.default.white;
|
|
2260
|
+
var ex = import_chalk.default.dim;
|
|
2261
|
+
function pad(colored, visible, width) {
|
|
2262
|
+
return colored + " ".repeat(Math.max(0, width - visible.length));
|
|
2263
|
+
}
|
|
2264
|
+
function showLogo() {
|
|
2265
|
+
const logo = renderLogo();
|
|
2266
|
+
console.log(logo);
|
|
2267
|
+
console.log(import_chalk.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2268
|
+
console.log(import_chalk.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2269
|
+
}
|
|
2270
|
+
function formatUsageLines(status, tier) {
|
|
2271
|
+
const lines = [];
|
|
2272
|
+
lines.push(`Videos generated: ${status.usage_count}`);
|
|
2273
|
+
if (tier !== "pro" && status.trial) {
|
|
2274
|
+
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
2275
|
+
const used = max_uses - uses_remaining;
|
|
2276
|
+
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
2277
|
+
if (!is_expired && days_remaining > 0) {
|
|
2278
|
+
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (tier === "pro" && status.subscription) {
|
|
2282
|
+
const end = status.subscription.current_period_end;
|
|
2283
|
+
if (status.subscription.is_lifetime) {
|
|
2284
|
+
lines.push("Lifetime access");
|
|
2285
|
+
} else if (isCanceling(status)) {
|
|
2286
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2287
|
+
lines.push(`Cancels: ${date}`);
|
|
2288
|
+
} else if (end) {
|
|
2289
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2290
|
+
lines.push(`Renews: ${date}`);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return lines;
|
|
2294
|
+
}
|
|
2295
|
+
async function showAccount() {
|
|
2296
|
+
const userData = await loadUserData();
|
|
2297
|
+
if (!userData.email) {
|
|
2298
|
+
console.log(import_chalk.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk.default.dim(" to sign in."));
|
|
2299
|
+
console.log();
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const email = userData.email;
|
|
2303
|
+
const tier = userData.tier === "pro" ? "pro" : "free";
|
|
2304
|
+
const tierLabel = tier === "pro" ? import_chalk.default.green("Pro") : "Free";
|
|
2305
|
+
console.log(` ${tierLabel} plan ${import_chalk.default.dim("\xB7")} ${import_chalk.default.dim(email)}`);
|
|
2306
|
+
try {
|
|
2307
|
+
const status = await getStatusCached(email);
|
|
2308
|
+
for (const line of formatUsageLines(status, tier)) {
|
|
2309
|
+
console.log(import_chalk.default.dim(` ${line}`));
|
|
2310
|
+
}
|
|
2311
|
+
} catch {
|
|
2312
|
+
}
|
|
2313
|
+
console.log();
|
|
2314
|
+
}
|
|
2315
|
+
async function showHelp() {
|
|
2316
|
+
showLogo();
|
|
2317
|
+
await showAccount();
|
|
2318
|
+
const W = 28;
|
|
2319
|
+
const usage = [
|
|
2320
|
+
[`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
|
|
2321
|
+
[
|
|
2322
|
+
`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`,
|
|
2323
|
+
`qp <url> --script <file>`,
|
|
2324
|
+
"Use your own narration script"
|
|
2325
|
+
],
|
|
2326
|
+
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
2327
|
+
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
2328
|
+
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
2329
|
+
[`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
|
|
2330
|
+
[`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
|
|
2331
|
+
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
2332
|
+
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
2333
|
+
];
|
|
2334
|
+
const options = [
|
|
2335
|
+
[`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
|
|
2336
|
+
[
|
|
2337
|
+
`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`,
|
|
2338
|
+
`-d, --description "..."`,
|
|
2339
|
+
"Guide AI with an app description"
|
|
2340
|
+
],
|
|
2341
|
+
[
|
|
2342
|
+
`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`,
|
|
2343
|
+
`-s, --script <file>`,
|
|
2344
|
+
"Narration script (one caption per line)"
|
|
2345
|
+
],
|
|
2346
|
+
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
2347
|
+
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
2348
|
+
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
|
|
2349
|
+
];
|
|
2350
|
+
const examples = [
|
|
2351
|
+
`${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
|
|
2352
|
+
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
|
|
2353
|
+
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
|
|
2354
|
+
`${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
|
|
2355
|
+
];
|
|
2356
|
+
console.log();
|
|
2357
|
+
console.log(import_chalk.default.yellow.bold("USAGE"));
|
|
2358
|
+
for (const [colored, plain, d] of usage) {
|
|
2359
|
+
console.log(` ${pad(colored, plain, W)}${desc(d)}`);
|
|
2360
|
+
}
|
|
2361
|
+
console.log();
|
|
2362
|
+
console.log(import_chalk.default.yellow.bold("OPTIONS"));
|
|
2363
|
+
for (const [colored, plain, d] of options) {
|
|
2364
|
+
console.log(` ${pad(colored, plain, W)}${desc(d)}`);
|
|
2365
|
+
}
|
|
2366
|
+
console.log();
|
|
2367
|
+
console.log(import_chalk.default.yellow.bold("EXAMPLES"));
|
|
2368
|
+
for (const e of examples) {
|
|
2369
|
+
console.log(` ${e}`);
|
|
2370
|
+
}
|
|
2371
|
+
console.log();
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// src/utils/ui.ts
|
|
2375
|
+
var import_node_path8 = require("path");
|
|
2376
|
+
var import_node_readline = require("readline");
|
|
2377
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
2378
|
+
var import_ora = __toESM(require("ora"));
|
|
2379
|
+
var INDENT = " ";
|
|
2380
|
+
var autoYes = false;
|
|
2381
|
+
function setAutoYes(value) {
|
|
2382
|
+
autoYes = value;
|
|
2383
|
+
}
|
|
2384
|
+
function task(text) {
|
|
2385
|
+
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2386
|
+
}
|
|
2387
|
+
function header(text) {
|
|
2388
|
+
console.log();
|
|
2389
|
+
console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
|
|
2390
|
+
}
|
|
2391
|
+
function info(text) {
|
|
2392
|
+
console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
|
|
2393
|
+
}
|
|
2394
|
+
function confirm(prompt2) {
|
|
2395
|
+
if (autoYes) return Promise.resolve(true);
|
|
2396
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2397
|
+
return new Promise((res) => {
|
|
2398
|
+
rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
|
|
2399
|
+
rl.close();
|
|
2400
|
+
res(answer.toLowerCase() !== "n");
|
|
2401
|
+
});
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
function promptInput(prompt2) {
|
|
2405
|
+
if (autoYes) return Promise.resolve("");
|
|
2406
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2407
|
+
return new Promise((res) => {
|
|
2408
|
+
rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
|
|
2409
|
+
rl.close();
|
|
2410
|
+
res(answer.trim());
|
|
2411
|
+
});
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2415
|
+
function formatClickablePath(filePath) {
|
|
2416
|
+
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
2417
|
+
if (_isWSL) {
|
|
2418
|
+
return absolutePath;
|
|
2419
|
+
}
|
|
2420
|
+
let formatted = absolutePath;
|
|
2421
|
+
formatted = formatted.replace(/\//g, "\\");
|
|
2422
|
+
if (formatted.includes(" ") && !formatted.startsWith('"')) {
|
|
2423
|
+
formatted = `"${formatted}"`;
|
|
2424
|
+
}
|
|
2425
|
+
return formatted;
|
|
2426
|
+
}
|
|
2427
|
+
function formatDuration(ms) {
|
|
2428
|
+
const mins = Math.floor(ms / 6e4);
|
|
2429
|
+
const secs = Math.floor(ms % 6e4 / 1e3);
|
|
2430
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/commands/auth-cmd.ts
|
|
2434
|
+
function waitForEnter(prompt2) {
|
|
2435
|
+
return new Promise((resolve9) => {
|
|
2436
|
+
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2437
|
+
rl.question(prompt2, () => {
|
|
2438
|
+
rl.close();
|
|
2439
|
+
resolve9();
|
|
2440
|
+
});
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
async function authCommand(url) {
|
|
2444
|
+
console.log();
|
|
2445
|
+
showLogo();
|
|
2446
|
+
if (!authStatePath(url)) {
|
|
2447
|
+
console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
console.log(import_chalk3.default.white(" A browser will open. Sign in to the site, then come back here."));
|
|
2451
|
+
console.log();
|
|
2452
|
+
const browser = await import_playwright2.chromium.launch({ headless: false });
|
|
2453
|
+
const context = await browser.newContext();
|
|
2454
|
+
const page = await context.newPage();
|
|
2455
|
+
try {
|
|
2456
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
|
|
2460
|
+
try {
|
|
2461
|
+
const state = await context.storageState();
|
|
2462
|
+
const path = await saveAuthState(url, state);
|
|
2463
|
+
console.log();
|
|
2464
|
+
console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
|
|
2465
|
+
console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
|
|
2466
|
+
console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
|
|
2467
|
+
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."));
|
|
2468
|
+
} finally {
|
|
2469
|
+
await browser.close().catch(() => {
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
console.log();
|
|
2473
|
+
}
|
|
2474
|
+
async function authClearCommand(url) {
|
|
2475
|
+
console.log();
|
|
2476
|
+
const removed = await clearAuthState(url);
|
|
2477
|
+
if (removed.length === 0) {
|
|
2478
|
+
console.log(import_chalk3.default.dim(url ? ` No saved session for ${url}` : " No saved sessions."));
|
|
2479
|
+
} else {
|
|
2480
|
+
for (const host of removed) {
|
|
2481
|
+
console.log(import_chalk3.default.green(` \u2713 Forgot session for ${host}`));
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
console.log();
|
|
2485
|
+
}
|
|
2486
|
+
async function authListCommand() {
|
|
2487
|
+
console.log();
|
|
2488
|
+
const hosts = await listAuthStates();
|
|
2489
|
+
if (hosts.length === 0) {
|
|
2490
|
+
console.log(import_chalk3.default.dim(" No saved sessions. Run ") + import_chalk3.default.cyan("qp auth <url>") + import_chalk3.default.dim(" to add one."));
|
|
2491
|
+
} else {
|
|
2492
|
+
console.log(import_chalk3.default.white(" Saved sessions:"));
|
|
2493
|
+
for (const host of hosts) {
|
|
2494
|
+
console.log(` ${import_chalk3.default.green("\u25CF")} ${host}`);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
console.log();
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/commands/capture-cmd.ts
|
|
2501
|
+
var import_node_path20 = require("path");
|
|
2502
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
2503
|
+
|
|
2504
|
+
// src/ai/narrate.ts
|
|
2505
|
+
init_lib();
|
|
2506
|
+
|
|
2507
|
+
// src/ai/client.ts
|
|
2508
|
+
var import_node_readline3 = require("readline");
|
|
2509
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2510
|
+
init_lib();
|
|
2511
|
+
|
|
2512
|
+
// src/utils/browser-auth.ts
|
|
2513
|
+
var import_node_http = require("http");
|
|
2514
|
+
var import_node_url = require("url");
|
|
2515
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
2516
|
+
init_SparkStripe_feature_gating();
|
|
2517
|
+
var TIMEOUT_MS = 12e4;
|
|
2518
|
+
async function openBrowser(url) {
|
|
2519
|
+
const { exec: exec2 } = await import("child_process");
|
|
2520
|
+
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2521
|
+
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2522
|
+
return new Promise((resolve9) => {
|
|
2523
|
+
exec2(command, () => resolve9());
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
async function browserLogin() {
|
|
2527
|
+
return new Promise((resolve9) => {
|
|
2528
|
+
let server;
|
|
2529
|
+
let timer;
|
|
2530
|
+
const cleanup = (email) => {
|
|
2531
|
+
clearTimeout(timer);
|
|
2532
|
+
server?.close();
|
|
2533
|
+
resolve9(email);
|
|
2534
|
+
};
|
|
2535
|
+
server = (0, import_node_http.createServer)((req, res) => {
|
|
2536
|
+
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2537
|
+
const path = url.pathname;
|
|
2538
|
+
if (path === "/callback" || path === "/") {
|
|
2539
|
+
const email = url.searchParams.get("email");
|
|
2540
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2541
|
+
res.end(`
|
|
2542
|
+
<!DOCTYPE html>
|
|
2543
|
+
<html>
|
|
2544
|
+
<head><title>QuickPeek</title><style>
|
|
2545
|
+
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
|
|
2546
|
+
.card { text-align: center; padding: 2rem; }
|
|
2547
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
2548
|
+
p { color: #888; }
|
|
2549
|
+
</style></head>
|
|
2550
|
+
<body><div class="card">
|
|
2551
|
+
<h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
|
|
2552
|
+
<p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
|
|
2553
|
+
<p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
|
|
2554
|
+
</div></body>
|
|
2555
|
+
</html>
|
|
2556
|
+
`);
|
|
2557
|
+
if (email) {
|
|
2558
|
+
cleanup(email);
|
|
2559
|
+
}
|
|
2560
|
+
} else {
|
|
2561
|
+
res.writeHead(404);
|
|
2562
|
+
res.end();
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2566
|
+
const addr = server.address();
|
|
2567
|
+
if (!addr || typeof addr === "string") {
|
|
2568
|
+
cleanup(null);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const port = addr.port;
|
|
2572
|
+
const returnUrl = `http://localhost:${port}/callback`;
|
|
2573
|
+
const url = pricingUrl({ returnUrl });
|
|
2574
|
+
console.log();
|
|
2575
|
+
console.log(import_chalk4.default.cyan(" Opening browser to sign in..."));
|
|
2576
|
+
console.log(import_chalk4.default.dim(` ${url}`));
|
|
2577
|
+
console.log();
|
|
2578
|
+
console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2579
|
+
openBrowser(url);
|
|
2580
|
+
});
|
|
2581
|
+
timer = setTimeout(() => {
|
|
2582
|
+
console.log();
|
|
2583
|
+
console.log(import_chalk4.default.yellow(" Sign-in timed out."));
|
|
2584
|
+
cleanup(null);
|
|
2585
|
+
}, TIMEOUT_MS);
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/ai/client.ts
|
|
2590
|
+
var lastRateLimitInfo = null;
|
|
2591
|
+
function getLastRateLimitInfo() {
|
|
2592
|
+
return lastRateLimitInfo;
|
|
2593
|
+
}
|
|
2594
|
+
function prompt(question) {
|
|
2595
|
+
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2596
|
+
return new Promise((resolve9) => {
|
|
2597
|
+
rl.question(question, (answer) => {
|
|
2598
|
+
rl.close();
|
|
2599
|
+
resolve9(answer.trim());
|
|
2600
|
+
});
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
async function setupUserEmail() {
|
|
2604
|
+
console.log();
|
|
2605
|
+
console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
|
|
2606
|
+
console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
|
|
2607
|
+
console.log();
|
|
2608
|
+
console.log(import_chalk5.default.white(" Sign in with your Google account to track usage"));
|
|
2609
|
+
console.log(import_chalk5.default.white(" and unlock Pro features."));
|
|
2610
|
+
const browserEmail = await browserLogin();
|
|
2611
|
+
if (browserEmail) {
|
|
2612
|
+
console.log();
|
|
2613
|
+
console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
|
|
2614
|
+
console.log();
|
|
2615
|
+
return browserEmail;
|
|
2616
|
+
}
|
|
2617
|
+
console.log();
|
|
2618
|
+
console.log(import_chalk5.default.dim(" Or enter your email manually:"));
|
|
2619
|
+
const email = await prompt(import_chalk5.default.white(" Email: "));
|
|
2620
|
+
if (!email) {
|
|
2621
|
+
console.log();
|
|
2622
|
+
console.log(import_chalk5.default.red(" No email provided. Exiting."));
|
|
2623
|
+
process.exit(1);
|
|
2624
|
+
}
|
|
2625
|
+
if (!email.includes("@") || !email.includes(".")) {
|
|
2626
|
+
console.log();
|
|
2627
|
+
console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
2628
|
+
const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
|
|
2629
|
+
if (proceed.toLowerCase() !== "y") {
|
|
2630
|
+
process.exit(1);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
console.log();
|
|
2634
|
+
console.log(import_chalk5.default.green(" \u2713 Email registered"));
|
|
2635
|
+
console.log();
|
|
2636
|
+
return email;
|
|
2637
|
+
}
|
|
2638
|
+
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2639
|
+
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
|
|
2640
|
+
if (!result.success) {
|
|
2641
|
+
await logError2("AI API", new Error(result.error));
|
|
2642
|
+
if (result.isRateLimit) {
|
|
2643
|
+
console.log();
|
|
2644
|
+
console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2645
|
+
console.log();
|
|
2646
|
+
console.log(import_chalk5.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2647
|
+
console.log(import_chalk5.default.dim(" Options:"));
|
|
2648
|
+
if (result.retryAfterSeconds) {
|
|
2649
|
+
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2650
|
+
console.log(import_chalk5.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2651
|
+
} else {
|
|
2652
|
+
console.log(import_chalk5.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2653
|
+
}
|
|
2654
|
+
console.log(import_chalk5.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2655
|
+
console.log(import_chalk5.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2656
|
+
console.log();
|
|
2657
|
+
process.exit(1);
|
|
2658
|
+
}
|
|
2659
|
+
if (result.isAuthError) {
|
|
2660
|
+
console.log();
|
|
2661
|
+
console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
|
|
2662
|
+
console.log();
|
|
2663
|
+
console.log(import_chalk5.default.dim(` ${result.error}`));
|
|
2664
|
+
console.log();
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
console.log();
|
|
2668
|
+
console.log(import_chalk5.default.red(" \u26A0 AI Error"));
|
|
2669
|
+
console.log();
|
|
2670
|
+
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2671
|
+
console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
|
|
2672
|
+
console.log();
|
|
2673
|
+
process.exit(1);
|
|
2674
|
+
}
|
|
2675
|
+
lastRateLimitInfo = result.limits;
|
|
2676
|
+
return result.content;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/ai/narrate.ts
|
|
2680
|
+
async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
|
|
2681
|
+
const langName = getLanguageName(lang);
|
|
2682
|
+
const stepList = steps.map((s, i) => {
|
|
2683
|
+
const parts = [`${i + 1}. ${s.action}`];
|
|
2684
|
+
if (s.target) parts.push(`on "${s.target}"`);
|
|
2685
|
+
if (s.value) parts.push(`= "${s.value}"`);
|
|
2686
|
+
return parts.join(" ");
|
|
2687
|
+
}).join("\n");
|
|
2688
|
+
const systemPrompt = `You write professional demo video captions in ${langName}.
|
|
2689
|
+
Given UI steps, return a JSON array of captions \u2014 one per step, in order.
|
|
2690
|
+
|
|
2691
|
+
RULES:
|
|
2692
|
+
- Return ONLY a valid JSON array of strings, no explanation
|
|
2693
|
+
- Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
|
|
2694
|
+
- Describe what the user is doing and why it matters in the context of the app
|
|
2695
|
+
|
|
2696
|
+
OUTPUT: ["Caption for step 1.", "Caption for step 2.", ...]`;
|
|
2697
|
+
const contextLine = pageContext ? `
|
|
2698
|
+
App context: ${pageContext}` : "";
|
|
2699
|
+
const userMessage = `App: ${title} (${url})${contextLine}
|
|
2068
2700
|
|
|
2069
2701
|
Steps to caption:
|
|
2070
2702
|
${stepList}
|
|
@@ -2162,11 +2794,11 @@ function eventsToSteps(events, url) {
|
|
|
2162
2794
|
let prevClickSelector;
|
|
2163
2795
|
let prevClickTime = 0;
|
|
2164
2796
|
let lastNavigatedUrl;
|
|
2165
|
-
const normalizedOriginUrl =
|
|
2797
|
+
const normalizedOriginUrl = normalizeUrl2(url);
|
|
2166
2798
|
for (const event of events) {
|
|
2167
2799
|
if (event.type === "navigate") {
|
|
2168
2800
|
if (!event.url) continue;
|
|
2169
|
-
const normalized =
|
|
2801
|
+
const normalized = normalizeUrl2(event.url);
|
|
2170
2802
|
if (normalized === normalizedOriginUrl) continue;
|
|
2171
2803
|
if (lastNavigatedUrl !== void 0 && normalized === lastNavigatedUrl) continue;
|
|
2172
2804
|
lastNavigatedUrl = normalized;
|
|
@@ -2216,7 +2848,7 @@ function eventsToSteps(events, url) {
|
|
|
2216
2848
|
}
|
|
2217
2849
|
return steps;
|
|
2218
2850
|
}
|
|
2219
|
-
function
|
|
2851
|
+
function normalizeUrl2(url) {
|
|
2220
2852
|
try {
|
|
2221
2853
|
const u = new URL(url);
|
|
2222
2854
|
return u.origin + u.pathname.replace(/\/$/, "");
|
|
@@ -2227,15 +2859,15 @@ function normalizeUrl(url) {
|
|
|
2227
2859
|
|
|
2228
2860
|
// src/capture.ts
|
|
2229
2861
|
var import_node_child_process4 = require("child_process");
|
|
2230
|
-
var
|
|
2231
|
-
var
|
|
2232
|
-
var
|
|
2862
|
+
var import_node_http3 = require("http");
|
|
2863
|
+
var import_node_readline4 = require("readline");
|
|
2864
|
+
var import_playwright4 = require("playwright");
|
|
2233
2865
|
|
|
2234
2866
|
// src/browser/wsl.ts
|
|
2235
2867
|
var import_node_child_process3 = require("child_process");
|
|
2236
|
-
var
|
|
2237
|
-
var
|
|
2238
|
-
var
|
|
2868
|
+
var import_node_fs5 = require("fs");
|
|
2869
|
+
var import_node_http2 = require("http");
|
|
2870
|
+
var import_playwright3 = require("playwright");
|
|
2239
2871
|
function isWSL() {
|
|
2240
2872
|
if (process.platform !== "linux") return false;
|
|
2241
2873
|
try {
|
|
@@ -2246,7 +2878,7 @@ function isWSL() {
|
|
|
2246
2878
|
}
|
|
2247
2879
|
}
|
|
2248
2880
|
function tryEnableWSLInterop() {
|
|
2249
|
-
return (0,
|
|
2881
|
+
return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2250
2882
|
}
|
|
2251
2883
|
function wslToWindowsPath(wslPath) {
|
|
2252
2884
|
if (!wslPath) return wslPath;
|
|
@@ -2255,7 +2887,7 @@ function wslToWindowsPath(wslPath) {
|
|
|
2255
2887
|
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2256
2888
|
}
|
|
2257
2889
|
function findExe(whereExeQuery, ...hardcoded) {
|
|
2258
|
-
for (const p of hardcoded) if ((0,
|
|
2890
|
+
for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
|
|
2259
2891
|
try {
|
|
2260
2892
|
const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2261
2893
|
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
@@ -2278,7 +2910,7 @@ function getWindowsHostIP() {
|
|
|
2278
2910
|
} catch {
|
|
2279
2911
|
}
|
|
2280
2912
|
try {
|
|
2281
|
-
const m = (0,
|
|
2913
|
+
const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2282
2914
|
if (m?.[1]) return m[1];
|
|
2283
2915
|
} catch {
|
|
2284
2916
|
}
|
|
@@ -2312,7 +2944,7 @@ function waitForChrome(host, port) {
|
|
|
2312
2944
|
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2313
2945
|
return;
|
|
2314
2946
|
}
|
|
2315
|
-
const req = (0,
|
|
2947
|
+
const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2316
2948
|
res.resume();
|
|
2317
2949
|
resolve9();
|
|
2318
2950
|
});
|
|
@@ -2354,7 +2986,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
2354
2986
|
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2355
2987
|
try {
|
|
2356
2988
|
await waitForChrome(windowsHost, proxyPort);
|
|
2357
|
-
const browser = await
|
|
2989
|
+
const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2358
2990
|
return {
|
|
2359
2991
|
browser,
|
|
2360
2992
|
cleanup: async () => {
|
|
@@ -2483,9 +3115,9 @@ var EVENT_TRACKER_SCRIPT = `
|
|
|
2483
3115
|
}, { passive: true, capture: true });
|
|
2484
3116
|
}());
|
|
2485
3117
|
`;
|
|
2486
|
-
function
|
|
3118
|
+
function waitForEnter2() {
|
|
2487
3119
|
return new Promise((resolve9) => {
|
|
2488
|
-
const rl = (0,
|
|
3120
|
+
const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2489
3121
|
process.stdout.write(" Press Enter when done...\n");
|
|
2490
3122
|
rl.once("line", () => {
|
|
2491
3123
|
rl.close();
|
|
@@ -2502,7 +3134,7 @@ async function launchCaptureChrome() {
|
|
|
2502
3134
|
const windowsHost = getWindowsHostIP();
|
|
2503
3135
|
const probe = await startRelay(nodePath, windowsHost, debugPort);
|
|
2504
3136
|
const alreadyUp = await new Promise((resolve9) => {
|
|
2505
|
-
const req = (0,
|
|
3137
|
+
const req = (0, import_node_http3.request)({ host: windowsHost, port: probe.port, path: "/json/version", method: "GET" }, (res) => {
|
|
2506
3138
|
res.resume();
|
|
2507
3139
|
resolve9(true);
|
|
2508
3140
|
});
|
|
@@ -2514,7 +3146,7 @@ async function launchCaptureChrome() {
|
|
|
2514
3146
|
req.end();
|
|
2515
3147
|
});
|
|
2516
3148
|
if (alreadyUp) {
|
|
2517
|
-
const browser = await
|
|
3149
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
|
|
2518
3150
|
return { browser, relay: probe.proc };
|
|
2519
3151
|
}
|
|
2520
3152
|
probe.proc.kill();
|
|
@@ -2534,7 +3166,7 @@ async function launchCaptureChrome() {
|
|
|
2534
3166
|
const relay = await startRelay(nodePath, windowsHost, debugPort);
|
|
2535
3167
|
try {
|
|
2536
3168
|
await waitForChrome(windowsHost, relay.port);
|
|
2537
|
-
const browser = await
|
|
3169
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
|
|
2538
3170
|
return { browser, relay: relay.proc };
|
|
2539
3171
|
} catch (err) {
|
|
2540
3172
|
relay.proc.kill();
|
|
@@ -2549,7 +3181,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2549
3181
|
if (isWSL()) {
|
|
2550
3182
|
({ browser, relay } = await launchCaptureChrome());
|
|
2551
3183
|
} else {
|
|
2552
|
-
browser = await
|
|
3184
|
+
browser = await import_playwright4.chromium.launch({
|
|
2553
3185
|
headless: false,
|
|
2554
3186
|
args: ["--start-maximized", "--no-first-run"]
|
|
2555
3187
|
});
|
|
@@ -2591,7 +3223,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2591
3223
|
return [title, parts.join(" | ")];
|
|
2592
3224
|
}).catch(() => ["", ""]);
|
|
2593
3225
|
startTime = Date.now();
|
|
2594
|
-
await
|
|
3226
|
+
await waitForEnter2();
|
|
2595
3227
|
const durationMs = Date.now() - startTime;
|
|
2596
3228
|
if (relay) {
|
|
2597
3229
|
try {
|
|
@@ -2606,195 +3238,31 @@ async function captureUserSession(url, _workDir) {
|
|
|
2606
3238
|
return { events, durationMs, pageTitle, pageContext };
|
|
2607
3239
|
}
|
|
2608
3240
|
|
|
2609
|
-
// src/utils/config-loader.ts
|
|
2610
|
-
var import_node_fs4 = require("fs");
|
|
2611
|
-
var import_promises4 = require("fs/promises");
|
|
2612
|
-
var import_node_os = require("os");
|
|
2613
|
-
var import_node_path6 = require("path");
|
|
2614
|
-
init_lib();
|
|
2615
|
-
async function loadConfig(_url, langOverride) {
|
|
2616
|
-
const configPath = (0, import_node_path6.join)(process.cwd(), CONFIG_FILE);
|
|
2617
|
-
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2618
|
-
try {
|
|
2619
|
-
const content = await (0, import_promises4.readFile)(configPath, "utf-8");
|
|
2620
|
-
const userConfig = JSON.parse(content);
|
|
2621
|
-
const config = {
|
|
2622
|
-
lang: langOverride || userConfig.lang || "en",
|
|
2623
|
-
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2624
|
-
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2625
|
-
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2626
|
-
music: userConfig.music,
|
|
2627
|
-
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2628
|
-
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2629
|
-
};
|
|
2630
|
-
return { config, created: false };
|
|
2631
|
-
} catch {
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
const fullDefaultConfig = {
|
|
2635
|
-
lang: "en",
|
|
2636
|
-
plan: { maxSteps: 8 },
|
|
2637
|
-
video: {
|
|
2638
|
-
width: 1080,
|
|
2639
|
-
height: 1920,
|
|
2640
|
-
fps: 30,
|
|
2641
|
-
quality: 0.5,
|
|
2642
|
-
preset: "fast",
|
|
2643
|
-
cursor: true,
|
|
2644
|
-
highlight: false,
|
|
2645
|
-
fadeIn: 500,
|
|
2646
|
-
// ms
|
|
2647
|
-
fadeOut: 500,
|
|
2648
|
-
// ms
|
|
2649
|
-
contrast: 0.25
|
|
2650
|
-
},
|
|
2651
|
-
audio: {
|
|
2652
|
-
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2653
|
-
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2654
|
-
speed: 1
|
|
2655
|
-
},
|
|
2656
|
-
music: {
|
|
2657
|
-
path: "",
|
|
2658
|
-
volume: 0.3
|
|
2659
|
-
},
|
|
2660
|
-
output: { dir: "./demo" }
|
|
2661
|
-
};
|
|
2662
|
-
await (0, import_promises4.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2663
|
-
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2664
|
-
}
|
|
2665
|
-
var WORK_DIR = ".quickpeek";
|
|
2666
|
-
async function prepareWorkDir() {
|
|
2667
|
-
const workDir = (0, import_node_path6.join)(process.cwd(), WORK_DIR);
|
|
2668
|
-
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2669
|
-
await (0, import_promises4.rm)(workDir, { recursive: true });
|
|
2670
|
-
}
|
|
2671
|
-
await (0, import_promises4.mkdir)(workDir, { recursive: true });
|
|
2672
|
-
return workDir;
|
|
2673
|
-
}
|
|
2674
|
-
var USER_DATA_DIR = (0, import_node_path6.join)((0, import_node_os.homedir)(), ".quickpeek");
|
|
2675
|
-
var USER_DATA_FILE = (0, import_node_path6.join)(USER_DATA_DIR, "user.json");
|
|
2676
|
-
async function loadUserData() {
|
|
2677
|
-
try {
|
|
2678
|
-
if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
|
|
2679
|
-
return JSON.parse(await (0, import_promises4.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2680
|
-
}
|
|
2681
|
-
} catch {
|
|
2682
|
-
}
|
|
2683
|
-
return {};
|
|
2684
|
-
}
|
|
2685
|
-
async function saveUserData(userData) {
|
|
2686
|
-
if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
|
|
2687
|
-
await (0, import_promises4.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2688
|
-
}
|
|
2689
|
-
const existing = await loadUserData();
|
|
2690
|
-
const merged = { ...existing, ...userData };
|
|
2691
|
-
await (0, import_promises4.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2692
|
-
}
|
|
2693
|
-
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2694
|
-
font-family: Arial, sans-serif;
|
|
2695
|
-
-webkit-text-stroke: 3px #000000;
|
|
2696
|
-
}
|
|
2697
|
-
.caption .regular {
|
|
2698
|
-
font-size: 56px;
|
|
2699
|
-
color: #FFFFFF;
|
|
2700
|
-
}
|
|
2701
|
-
.caption .highlight {
|
|
2702
|
-
font-size: 64px;
|
|
2703
|
-
color: #FFFF00;
|
|
2704
|
-
}
|
|
2705
|
-
`;
|
|
2706
|
-
function parseCaptionCSS(css) {
|
|
2707
|
-
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2708
|
-
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
2709
|
-
const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
|
|
2710
|
-
const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
|
|
2711
|
-
const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
|
|
2712
|
-
if (fontMatch) style.font = fontMatch[1].trim();
|
|
2713
|
-
const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
|
|
2714
|
-
if (strokeMatch) {
|
|
2715
|
-
style.outlineWidth = parseInt(strokeMatch[1], 10);
|
|
2716
|
-
style.outline = rgbaToHex(strokeMatch[2]);
|
|
2717
|
-
}
|
|
2718
|
-
const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
|
|
2719
|
-
if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
|
|
2720
|
-
const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
|
|
2721
|
-
if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
|
|
2722
|
-
const parseWordStyle = (block, target) => {
|
|
2723
|
-
const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2724
|
-
if (colorMatch) target.color = colorMatch[1];
|
|
2725
|
-
const sizeMatch = block.match(/font-size:\s*(\d+)/);
|
|
2726
|
-
if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
|
|
2727
|
-
const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2728
|
-
if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2729
|
-
};
|
|
2730
|
-
parseWordStyle(regularBlock, style.regular);
|
|
2731
|
-
parseWordStyle(highlightBlock, style.highlight);
|
|
2732
|
-
if (!regularBlock && !highlightBlock) {
|
|
2733
|
-
const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2734
|
-
if (colorMatch) style.regular.color = colorMatch[1];
|
|
2735
|
-
const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
|
|
2736
|
-
if (sizeMatch) {
|
|
2737
|
-
style.regular.size = parseInt(sizeMatch[1], 10);
|
|
2738
|
-
style.highlight.size = Math.round(style.regular.size * 1.15);
|
|
2739
|
-
}
|
|
2740
|
-
const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
|
|
2741
|
-
if (highlightMatch) style.highlight.color = highlightMatch[1];
|
|
2742
|
-
const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2743
|
-
if (weightMatch) {
|
|
2744
|
-
const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2745
|
-
style.regular.bold = isBold;
|
|
2746
|
-
style.highlight.bold = isBold;
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
return style;
|
|
2750
|
-
}
|
|
2751
|
-
async function loadCaptions() {
|
|
2752
|
-
const captionsPath = (0, import_node_path6.join)(process.cwd(), "captions.css");
|
|
2753
|
-
if (!(0, import_node_fs4.existsSync)(captionsPath)) {
|
|
2754
|
-
await (0, import_promises4.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2755
|
-
return DEFAULT_CAPTIONS;
|
|
2756
|
-
}
|
|
2757
|
-
try {
|
|
2758
|
-
const css = await (0, import_promises4.readFile)(captionsPath, "utf-8");
|
|
2759
|
-
return parseCaptionCSS(css);
|
|
2760
|
-
} catch {
|
|
2761
|
-
return DEFAULT_CAPTIONS;
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
async function savePlan(plan, planPath) {
|
|
2765
|
-
const cleanPlan = {
|
|
2766
|
-
...plan,
|
|
2767
|
-
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2768
|
-
};
|
|
2769
|
-
await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(planPath), { recursive: true });
|
|
2770
|
-
await (0, import_promises4.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2771
|
-
}
|
|
2772
|
-
|
|
2773
3241
|
// src/utils/logging.ts
|
|
2774
|
-
var
|
|
2775
|
-
var
|
|
3242
|
+
var import_promises6 = require("fs/promises");
|
|
3243
|
+
var import_node_path9 = require("path");
|
|
2776
3244
|
var LOG_FILE_NAME = "quickpeek.log";
|
|
2777
3245
|
var logFileInitialized = false;
|
|
2778
3246
|
var logFilePath = LOG_FILE_NAME;
|
|
2779
3247
|
async function writeLogEntry(entry) {
|
|
2780
3248
|
try {
|
|
2781
3249
|
if (!logFileInitialized) {
|
|
2782
|
-
await (0,
|
|
3250
|
+
await (0, import_promises6.writeFile)(logFilePath, entry);
|
|
2783
3251
|
logFileInitialized = true;
|
|
2784
3252
|
} else {
|
|
2785
|
-
await (0,
|
|
3253
|
+
await (0, import_promises6.appendFile)(logFilePath, entry);
|
|
2786
3254
|
}
|
|
2787
3255
|
} catch {
|
|
2788
3256
|
}
|
|
2789
3257
|
}
|
|
2790
3258
|
async function setLogFilePath(outputDir) {
|
|
2791
|
-
logFilePath = (0,
|
|
3259
|
+
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
2792
3260
|
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2793
3261
|
${"=".repeat(60)}
|
|
2794
3262
|
|
|
2795
3263
|
`;
|
|
2796
3264
|
try {
|
|
2797
|
-
await (0,
|
|
3265
|
+
await (0, import_promises6.writeFile)(logFilePath, header2);
|
|
2798
3266
|
logFileInitialized = true;
|
|
2799
3267
|
} catch {
|
|
2800
3268
|
}
|
|
@@ -2822,101 +3290,42 @@ ${errorMsg}
|
|
|
2822
3290
|
|
|
2823
3291
|
`);
|
|
2824
3292
|
}
|
|
2825
|
-
async function logStep(stepNum, action, details) {
|
|
2826
|
-
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
2827
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
2828
|
-
`);
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
// src/utils/ui.ts
|
|
2832
|
-
var import_node_path8 = require("path");
|
|
2833
|
-
var import_node_readline3 = require("readline");
|
|
2834
|
-
var import_chalk2 = __toESM(require("chalk"));
|
|
2835
|
-
var import_ora = __toESM(require("ora"));
|
|
2836
|
-
var INDENT = " ";
|
|
2837
|
-
var autoYes = false;
|
|
2838
|
-
function setAutoYes(value) {
|
|
2839
|
-
autoYes = value;
|
|
2840
|
-
}
|
|
2841
|
-
function task(text) {
|
|
2842
|
-
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2843
|
-
}
|
|
2844
|
-
function header(text) {
|
|
2845
|
-
console.log();
|
|
2846
|
-
console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
|
|
2847
|
-
}
|
|
2848
|
-
function info(text) {
|
|
2849
|
-
console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
|
|
2850
|
-
}
|
|
2851
|
-
function confirm(prompt2) {
|
|
2852
|
-
if (autoYes) return Promise.resolve(true);
|
|
2853
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2854
|
-
return new Promise((res) => {
|
|
2855
|
-
rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
|
|
2856
|
-
rl.close();
|
|
2857
|
-
res(answer.toLowerCase() !== "n");
|
|
2858
|
-
});
|
|
2859
|
-
});
|
|
2860
|
-
}
|
|
2861
|
-
function promptInput(prompt2) {
|
|
2862
|
-
if (autoYes) return Promise.resolve("");
|
|
2863
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2864
|
-
return new Promise((res) => {
|
|
2865
|
-
rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
|
|
2866
|
-
rl.close();
|
|
2867
|
-
res(answer.trim());
|
|
2868
|
-
});
|
|
2869
|
-
});
|
|
2870
|
-
}
|
|
2871
|
-
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2872
|
-
function formatClickablePath(filePath) {
|
|
2873
|
-
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
2874
|
-
if (_isWSL) {
|
|
2875
|
-
return absolutePath;
|
|
2876
|
-
}
|
|
2877
|
-
let formatted = absolutePath;
|
|
2878
|
-
formatted = formatted.replace(/\//g, "\\");
|
|
2879
|
-
if (formatted.includes(" ") && !formatted.startsWith('"')) {
|
|
2880
|
-
formatted = `"${formatted}"`;
|
|
2881
|
-
}
|
|
2882
|
-
return formatted;
|
|
2883
|
-
}
|
|
2884
|
-
function formatDuration(ms) {
|
|
2885
|
-
const mins = Math.floor(ms / 6e4);
|
|
2886
|
-
const secs = Math.floor(ms % 6e4 / 1e3);
|
|
2887
|
-
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2888
|
-
}
|
|
3293
|
+
async function logStep(stepNum, action, details) {
|
|
3294
|
+
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3295
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3296
|
+
`);
|
|
3297
|
+
}
|
|
2889
3298
|
|
|
2890
3299
|
// src/commands/phases/video-phase.ts
|
|
2891
|
-
var
|
|
2892
|
-
var
|
|
2893
|
-
var
|
|
3300
|
+
var import_node_fs12 = require("fs");
|
|
3301
|
+
var import_promises13 = require("fs/promises");
|
|
3302
|
+
var import_node_path18 = require("path");
|
|
2894
3303
|
init_lib();
|
|
2895
3304
|
|
|
2896
3305
|
// src/record.ts
|
|
2897
|
-
var
|
|
2898
|
-
var
|
|
2899
|
-
var
|
|
2900
|
-
var
|
|
3306
|
+
var import_node_fs10 = require("fs");
|
|
3307
|
+
var import_promises10 = require("fs/promises");
|
|
3308
|
+
var import_node_path15 = require("path");
|
|
3309
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
2901
3310
|
|
|
2902
3311
|
// src/browser/actions.ts
|
|
2903
|
-
var
|
|
2904
|
-
var
|
|
2905
|
-
var
|
|
3312
|
+
var import_node_fs7 = require("fs");
|
|
3313
|
+
var import_promises7 = require("fs/promises");
|
|
3314
|
+
var import_node_path11 = require("path");
|
|
2906
3315
|
|
|
2907
3316
|
// src/browser/effects.ts
|
|
2908
|
-
var
|
|
2909
|
-
var
|
|
3317
|
+
var import_node_fs6 = require("fs");
|
|
3318
|
+
var import_node_path10 = require("path");
|
|
2910
3319
|
init_lib();
|
|
2911
3320
|
function loadHighlightCSS() {
|
|
2912
3321
|
const candidates = [
|
|
2913
|
-
(0,
|
|
3322
|
+
(0, import_node_path10.join)(__dirname, "highlight.css"),
|
|
2914
3323
|
// dist/ build
|
|
2915
|
-
(0,
|
|
3324
|
+
(0, import_node_path10.join)(__dirname, "..", "highlight.css")
|
|
2916
3325
|
// src/browser/ → src/
|
|
2917
3326
|
];
|
|
2918
3327
|
for (const p of candidates) {
|
|
2919
|
-
if ((0,
|
|
3328
|
+
if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
|
|
2920
3329
|
}
|
|
2921
3330
|
return "";
|
|
2922
3331
|
}
|
|
@@ -3257,8 +3666,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3257
3666
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3258
3667
|
if (!isPlaceholder) {
|
|
3259
3668
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3260
|
-
const filePath = (0,
|
|
3261
|
-
if ((0,
|
|
3669
|
+
const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
|
|
3670
|
+
if ((0, import_node_fs7.existsSync)(filePath)) {
|
|
3262
3671
|
await fileInput.setInputFiles(filePath);
|
|
3263
3672
|
await page.waitForTimeout(2e3);
|
|
3264
3673
|
break;
|
|
@@ -3374,8 +3783,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3374
3783
|
case "wait":
|
|
3375
3784
|
break;
|
|
3376
3785
|
case "image": {
|
|
3377
|
-
const imagePath = step.value ? (0,
|
|
3378
|
-
if (imagePath && (0,
|
|
3786
|
+
const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
|
|
3787
|
+
if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
|
|
3379
3788
|
const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
|
|
3380
3789
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
3381
3790
|
}
|
|
@@ -3405,7 +3814,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3405
3814
|
await pendingDownload;
|
|
3406
3815
|
pendingDownload = null;
|
|
3407
3816
|
}
|
|
3408
|
-
if (lastDownloadPath && (0,
|
|
3817
|
+
if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
|
|
3409
3818
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
3410
3819
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
3411
3820
|
}
|
|
@@ -3416,7 +3825,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3416
3825
|
async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
3417
3826
|
const { width, height } = config.video;
|
|
3418
3827
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
3419
|
-
const fileBuffer = await (0,
|
|
3828
|
+
const fileBuffer = await (0, import_promises7.readFile)(filePath);
|
|
3420
3829
|
const base64 = fileBuffer.toString("base64");
|
|
3421
3830
|
const mimeTypes = {
|
|
3422
3831
|
png: "image/png",
|
|
@@ -3446,17 +3855,18 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
|
3446
3855
|
}
|
|
3447
3856
|
|
|
3448
3857
|
// src/browser/launch.ts
|
|
3449
|
-
var
|
|
3450
|
-
var
|
|
3451
|
-
async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
3858
|
+
var import_promises8 = require("fs/promises");
|
|
3859
|
+
var import_playwright5 = require("playwright");
|
|
3860
|
+
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
3452
3861
|
const extensionPath = config.browser?.extension;
|
|
3453
3862
|
const viewport = { width: config.video.width, height: config.video.height };
|
|
3454
3863
|
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
3864
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
3455
3865
|
let context;
|
|
3456
3866
|
let browser = null;
|
|
3457
3867
|
let chromeSession = null;
|
|
3458
3868
|
if (extensionPath) {
|
|
3459
|
-
await (0,
|
|
3869
|
+
await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
|
|
3460
3870
|
const persistentArgs = [
|
|
3461
3871
|
`--disable-extensions-except=${extensionPath}`,
|
|
3462
3872
|
`--load-extension=${extensionPath}`,
|
|
@@ -3470,7 +3880,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3470
3880
|
const contexts = chromeSession.browser.contexts();
|
|
3471
3881
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3472
3882
|
} catch {
|
|
3473
|
-
context = await
|
|
3883
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3474
3884
|
headless: false,
|
|
3475
3885
|
args: persistentArgs,
|
|
3476
3886
|
viewport,
|
|
@@ -3479,7 +3889,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3479
3889
|
});
|
|
3480
3890
|
}
|
|
3481
3891
|
} else {
|
|
3482
|
-
context = await
|
|
3892
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3483
3893
|
headless: false,
|
|
3484
3894
|
args: persistentArgs,
|
|
3485
3895
|
viewport,
|
|
@@ -3488,8 +3898,8 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3488
3898
|
});
|
|
3489
3899
|
}
|
|
3490
3900
|
} else {
|
|
3491
|
-
browser = await
|
|
3492
|
-
context = await browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3901
|
+
browser = await import_playwright5.chromium.launch({ headless: true });
|
|
3902
|
+
context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
|
|
3493
3903
|
}
|
|
3494
3904
|
return { context, browser, chromeSession };
|
|
3495
3905
|
}
|
|
@@ -3506,23 +3916,23 @@ async function cleanupBrowserSession(session) {
|
|
|
3506
3916
|
init_lib();
|
|
3507
3917
|
|
|
3508
3918
|
// src/browser/dry-run.ts
|
|
3509
|
-
var
|
|
3919
|
+
var import_node_path13 = require("path");
|
|
3510
3920
|
init_utils();
|
|
3511
3921
|
|
|
3512
3922
|
// src/browser/step-validator.ts
|
|
3513
|
-
var
|
|
3514
|
-
var
|
|
3923
|
+
var import_node_fs8 = require("fs");
|
|
3924
|
+
var import_node_path12 = require("path");
|
|
3515
3925
|
init_utils();
|
|
3516
3926
|
function findMediaFile(filename, baseDir) {
|
|
3517
|
-
const name = (0,
|
|
3518
|
-
const searchDirs = [baseDir, (0,
|
|
3927
|
+
const name = (0, import_node_path12.basename)(filename);
|
|
3928
|
+
const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
|
|
3519
3929
|
for (const dir of searchDirs) {
|
|
3520
|
-
if (!(0,
|
|
3930
|
+
if (!(0, import_node_fs8.existsSync)(dir)) continue;
|
|
3521
3931
|
try {
|
|
3522
|
-
const files = (0,
|
|
3523
|
-
if (files.includes(name)) return (0,
|
|
3932
|
+
const files = (0, import_node_fs8.readdirSync)(dir);
|
|
3933
|
+
if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
|
|
3524
3934
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
3525
|
-
if (match) return (0,
|
|
3935
|
+
if (match) return (0, import_node_path12.join)(dir, match);
|
|
3526
3936
|
} catch {
|
|
3527
3937
|
}
|
|
3528
3938
|
}
|
|
@@ -3594,8 +4004,8 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
3594
4004
|
if (!step.value) {
|
|
3595
4005
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
3596
4006
|
}
|
|
3597
|
-
const specifiedPath = (0,
|
|
3598
|
-
if ((0,
|
|
4007
|
+
const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
|
|
4008
|
+
if ((0, import_node_fs8.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
3599
4009
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
3600
4010
|
if (foundPath) {
|
|
3601
4011
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3716,8 +4126,8 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
3716
4126
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3717
4127
|
if (!isPlaceholder) {
|
|
3718
4128
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3719
|
-
let filePath = (0,
|
|
3720
|
-
if (!(0,
|
|
4129
|
+
let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
|
|
4130
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) {
|
|
3721
4131
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
3722
4132
|
if (foundPath) {
|
|
3723
4133
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3816,8 +4226,13 @@ async function getPageElements(page) {
|
|
|
3816
4226
|
});
|
|
3817
4227
|
}
|
|
3818
4228
|
async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
|
|
3819
|
-
const userDataDir = (0,
|
|
3820
|
-
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4229
|
+
const userDataDir = (0, import_node_path13.join)(baseDir, ".browser-data");
|
|
4230
|
+
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4231
|
+
config,
|
|
4232
|
+
userDataDir,
|
|
4233
|
+
void 0,
|
|
4234
|
+
findAuthState(plan.url)
|
|
4235
|
+
);
|
|
3821
4236
|
const page = context.pages()[0] || await context.newPage();
|
|
3822
4237
|
setupDownloadTracking(page);
|
|
3823
4238
|
const results = [];
|
|
@@ -3876,27 +4291,27 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
3876
4291
|
}
|
|
3877
4292
|
|
|
3878
4293
|
// src/browser/meta.ts
|
|
3879
|
-
var
|
|
3880
|
-
var
|
|
3881
|
-
var
|
|
4294
|
+
var import_node_fs9 = require("fs");
|
|
4295
|
+
var import_promises9 = require("fs/promises");
|
|
4296
|
+
var import_node_path14 = require("path");
|
|
3882
4297
|
var RECORDING_FILE = "recording.mp4";
|
|
3883
4298
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
3884
4299
|
function getRecordingPath(outputDir) {
|
|
3885
|
-
return (0,
|
|
4300
|
+
return (0, import_node_path14.join)(outputDir, RECORDING_FILE);
|
|
3886
4301
|
}
|
|
3887
4302
|
async function loadRecordingMeta(outputDir) {
|
|
3888
|
-
const metaPath = (0,
|
|
3889
|
-
if (!(0,
|
|
4303
|
+
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4304
|
+
if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
|
|
3890
4305
|
try {
|
|
3891
|
-
const content = await (0,
|
|
4306
|
+
const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
|
|
3892
4307
|
return JSON.parse(content);
|
|
3893
4308
|
} catch {
|
|
3894
4309
|
return null;
|
|
3895
4310
|
}
|
|
3896
4311
|
}
|
|
3897
4312
|
async function saveRecordingMeta(outputDir, meta) {
|
|
3898
|
-
const metaPath = (0,
|
|
3899
|
-
await (0,
|
|
4313
|
+
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4314
|
+
await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
3900
4315
|
}
|
|
3901
4316
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
3902
4317
|
if (!existingMeta) {
|
|
@@ -3965,16 +4380,17 @@ function formatStepLabel(step) {
|
|
|
3965
4380
|
return `${caption.slice(0, 47)}...`;
|
|
3966
4381
|
}
|
|
3967
4382
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
3968
|
-
const videoDir = (0,
|
|
3969
|
-
await (0,
|
|
4383
|
+
const videoDir = (0, import_node_path15.join)(workDir, "video");
|
|
4384
|
+
await (0, import_promises10.mkdir)(videoDir, { recursive: true });
|
|
3970
4385
|
const getStepDuration = (stepNum) => {
|
|
3971
4386
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
3972
4387
|
};
|
|
3973
4388
|
const browserTask = task2("Starting browser recording");
|
|
3974
4389
|
const recordingStartTime = Date.now();
|
|
3975
|
-
const userDataDir = (0,
|
|
4390
|
+
const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
|
|
3976
4391
|
const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
|
|
3977
|
-
const
|
|
4392
|
+
const authState = findAuthState(plan.url);
|
|
4393
|
+
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
3978
4394
|
const page = context.pages()[0] || await context.newPage();
|
|
3979
4395
|
setupDownloadTracking(page);
|
|
3980
4396
|
context.on("page", (p) => setupDownloadTracking(p));
|
|
@@ -4072,9 +4488,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4072
4488
|
actualStepDurations.push(actualDuration);
|
|
4073
4489
|
}
|
|
4074
4490
|
if (failures.length > 0) {
|
|
4075
|
-
console.log(
|
|
4491
|
+
console.log(import_chalk6.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
|
|
4076
4492
|
for (const f of failures) {
|
|
4077
|
-
console.log(
|
|
4493
|
+
console.log(import_chalk6.default.yellow(` - ${f}`));
|
|
4078
4494
|
}
|
|
4079
4495
|
}
|
|
4080
4496
|
const saveTask = task2("Saving video");
|
|
@@ -4083,7 +4499,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4083
4499
|
await page.close();
|
|
4084
4500
|
const videoPath = await video.path();
|
|
4085
4501
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4086
|
-
if (!videoPath || !(0,
|
|
4502
|
+
if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
|
|
4087
4503
|
throw new Error("Video file was not created");
|
|
4088
4504
|
}
|
|
4089
4505
|
saveTask.succeed("Video saved");
|
|
@@ -4091,33 +4507,18 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4091
4507
|
}
|
|
4092
4508
|
|
|
4093
4509
|
// src/utils/video-pipeline.ts
|
|
4094
|
-
var
|
|
4095
|
-
var
|
|
4096
|
-
var
|
|
4510
|
+
var import_video_kit6 = require("@spark-apps/video-kit");
|
|
4511
|
+
var import_node_fs11 = require("fs");
|
|
4512
|
+
var import_promises11 = require("fs/promises");
|
|
4513
|
+
var import_node_path16 = require("path");
|
|
4514
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
4097
4515
|
init_lib();
|
|
4098
4516
|
var INDENT2 = " ";
|
|
4099
|
-
function chunkWords(words, maxWords) {
|
|
4100
|
-
const chunks = [];
|
|
4101
|
-
for (let i = 0; i < words.length; i += maxWords) {
|
|
4102
|
-
chunks.push(words.slice(i, i + maxWords));
|
|
4103
|
-
}
|
|
4104
|
-
return chunks;
|
|
4105
|
-
}
|
|
4106
|
-
function formatASSTime(ms) {
|
|
4107
|
-
const hours = Math.floor(ms / 36e5);
|
|
4108
|
-
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
4109
|
-
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
4110
|
-
const centis = Math.floor(ms % 1e3 / 10);
|
|
4111
|
-
return `${hours}:${pad(minutes)}:${pad(seconds)}.${pad(centis)}`;
|
|
4112
|
-
}
|
|
4113
|
-
function pad(n, len = 2) {
|
|
4114
|
-
return n.toString().padStart(len, "0");
|
|
4115
|
-
}
|
|
4116
4517
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
4117
|
-
const regularColor =
|
|
4118
|
-
const highlightColor =
|
|
4119
|
-
const outline =
|
|
4120
|
-
const back =
|
|
4518
|
+
const regularColor = (0, import_video_kit.hexToAss)(captions.regular.color);
|
|
4519
|
+
const highlightColor = (0, import_video_kit.hexToAss)(captions.highlight.color);
|
|
4520
|
+
const outline = (0, import_video_kit.hexToAss)(captions.outline);
|
|
4521
|
+
const back = (0, import_video_kit.hexToAss)(captions.outline, 128);
|
|
4121
4522
|
const highlightScale = Math.round(captions.highlight.size / captions.regular.size * 100);
|
|
4122
4523
|
let ass = generateASSHeader(
|
|
4123
4524
|
config.video.width,
|
|
@@ -4142,7 +4543,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4142
4543
|
continue;
|
|
4143
4544
|
}
|
|
4144
4545
|
const words = audio.words.length > 0 ? audio.words : estimateWordTimings(step.caption || "", audio.duration);
|
|
4145
|
-
const chunks =
|
|
4546
|
+
const chunks = (0, import_video_kit6.chunkWordsByCount)(words, 3);
|
|
4146
4547
|
for (const chunk of chunks) {
|
|
4147
4548
|
if (chunk.length === 0) continue;
|
|
4148
4549
|
const chunkStart = currentTime + chunk[0].start;
|
|
@@ -4153,13 +4554,13 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4153
4554
|
const duration = Math.round((word.end - word.start) / 10);
|
|
4154
4555
|
karaokeText += `{\\kf${duration}\\fscx${highlightScale}\\fscy${highlightScale}}${word.word}{\\fscx100\\fscy100} `;
|
|
4155
4556
|
}
|
|
4156
|
-
ass += `Dialogue: 0,${
|
|
4557
|
+
ass += `Dialogue: 0,${(0, import_video_kit6.assTimeFromMs)(chunkStart)},${(0, import_video_kit6.assTimeFromMs)(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
|
|
4157
4558
|
`;
|
|
4158
4559
|
}
|
|
4159
4560
|
currentTime += audio.duration;
|
|
4160
4561
|
}
|
|
4161
|
-
const assPath = (0,
|
|
4162
|
-
await (0,
|
|
4562
|
+
const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
|
|
4563
|
+
await (0, import_promises11.writeFile)(assPath, ass);
|
|
4163
4564
|
return assPath;
|
|
4164
4565
|
}
|
|
4165
4566
|
function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
@@ -4172,8 +4573,8 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4172
4573
|
if ((step.action === "video" || step.action === "media") && step.value) {
|
|
4173
4574
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
4174
4575
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
4175
|
-
const mediaPath = (0,
|
|
4176
|
-
if ((0,
|
|
4576
|
+
const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
|
|
4577
|
+
if ((0, import_node_fs11.existsSync)(mediaPath)) {
|
|
4177
4578
|
const partial = parsePartial(step.partial);
|
|
4178
4579
|
overlays.push({
|
|
4179
4580
|
path: mediaPath,
|
|
@@ -4192,23 +4593,23 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4192
4593
|
}
|
|
4193
4594
|
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
4194
4595
|
header("Compose");
|
|
4195
|
-
const configPath = (0,
|
|
4196
|
-
console.log(`${INDENT2}${
|
|
4596
|
+
const configPath = (0, import_node_path16.join)(process.cwd(), CONFIG_FILE);
|
|
4597
|
+
console.log(`${INDENT2}${import_chalk7.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
4197
4598
|
const proceed = await confirm(`${INDENT2}Review config or continue?`);
|
|
4198
4599
|
if (!proceed) {
|
|
4199
|
-
console.log(
|
|
4600
|
+
console.log(import_chalk7.default.yellow(`
|
|
4200
4601
|
Edit ${CONFIG_FILE} and run again.
|
|
4201
4602
|
`));
|
|
4202
4603
|
process.exit(0);
|
|
4203
4604
|
}
|
|
4204
4605
|
const composeTask = task("Building final video");
|
|
4205
|
-
await (0,
|
|
4606
|
+
await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
|
|
4206
4607
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4207
|
-
const finalPath = (0,
|
|
4608
|
+
const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4208
4609
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4209
|
-
const hasVoiceover = audioPath && (0,
|
|
4210
|
-
const musicPath = config.music?.path ? (0,
|
|
4211
|
-
const hasMusic = musicPath && (0,
|
|
4610
|
+
const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
|
|
4611
|
+
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) : "";
|
|
4612
|
+
const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
|
|
4212
4613
|
try {
|
|
4213
4614
|
await composeVideo({
|
|
4214
4615
|
videoPath,
|
|
@@ -4245,10 +4646,9 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
4245
4646
|
async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
4246
4647
|
const duration = await getMediaDuration(finalPath);
|
|
4247
4648
|
if (duration <= limitSecs * 1e3) return finalPath;
|
|
4248
|
-
const chalk17 = require("chalk");
|
|
4249
4649
|
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
4250
4650
|
console.log();
|
|
4251
|
-
console.log(
|
|
4651
|
+
console.log(import_chalk7.default.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
4252
4652
|
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
4253
4653
|
if (!speedUp) return finalPath;
|
|
4254
4654
|
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
@@ -4270,23 +4670,23 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
4270
4670
|
}
|
|
4271
4671
|
|
|
4272
4672
|
// src/commands/utils/record-helpers.ts
|
|
4273
|
-
var
|
|
4673
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
4274
4674
|
init_lib();
|
|
4275
4675
|
|
|
4276
4676
|
// src/plan.ts
|
|
4277
|
-
var
|
|
4278
|
-
var
|
|
4279
|
-
var
|
|
4280
|
-
var
|
|
4677
|
+
var import_promises12 = require("fs/promises");
|
|
4678
|
+
var import_node_path17 = require("path");
|
|
4679
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
4680
|
+
var import_playwright6 = require("playwright");
|
|
4281
4681
|
init_lib();
|
|
4282
4682
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
4283
4683
|
const browserTask = task2("Launching browser with extension");
|
|
4284
|
-
const userDataDir = (0,
|
|
4285
|
-
await (0,
|
|
4684
|
+
const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
|
|
4685
|
+
await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
|
|
4286
4686
|
let context;
|
|
4287
4687
|
let chromeSession = null;
|
|
4288
4688
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
4289
|
-
const launchPersistent = () =>
|
|
4689
|
+
const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
|
|
4290
4690
|
headless: false,
|
|
4291
4691
|
args: [
|
|
4292
4692
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -4302,7 +4702,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4302
4702
|
const contexts = chromeSession.browser.contexts();
|
|
4303
4703
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport: { width: 400, height: 600 } });
|
|
4304
4704
|
} catch (_err) {
|
|
4305
|
-
console.log(
|
|
4705
|
+
console.log(import_chalk8.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
|
|
4306
4706
|
context = await launchPersistent();
|
|
4307
4707
|
}
|
|
4308
4708
|
} else {
|
|
@@ -4379,8 +4779,34 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4379
4779
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
4380
4780
|
return { title, elements };
|
|
4381
4781
|
}
|
|
4782
|
+
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
4783
|
+
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4784
|
+
The JSON should have: url, title, description, steps array.
|
|
4785
|
+
Each step has: id (number), action, caption, and optionally target, value.
|
|
4786
|
+
|
|
4787
|
+
Malformed content:
|
|
4788
|
+
${content}`;
|
|
4789
|
+
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
4790
|
+
try {
|
|
4791
|
+
return JSON.parse(content);
|
|
4792
|
+
} catch {
|
|
4793
|
+
const fixTask = task2("Fixing malformed JSON");
|
|
4794
|
+
try {
|
|
4795
|
+
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
4796
|
+
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4797
|
+
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4798
|
+
const plan = JSON.parse(jsonMatch[0]);
|
|
4799
|
+
fixTask.succeed("JSON fixed by AI");
|
|
4800
|
+
return plan;
|
|
4801
|
+
} catch (err) {
|
|
4802
|
+
await logError2("JSON Fix", err);
|
|
4803
|
+
fixTask.fail("Could not fix JSON");
|
|
4804
|
+
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4382
4808
|
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
|
|
4383
|
-
const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2);
|
|
4809
|
+
const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
|
|
4384
4810
|
const aiTask = task2("Generating plan with AI");
|
|
4385
4811
|
const langName = getLanguageName(lang);
|
|
4386
4812
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
@@ -4389,7 +4815,7 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4389
4815
|
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
4390
4816
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
4391
4817
|
if (!jsonMatch) throw new Error("No JSON found in response");
|
|
4392
|
-
const plan =
|
|
4818
|
+
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
4393
4819
|
if (lang !== void 0) plan.lang = lang;
|
|
4394
4820
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
4395
4821
|
...s,
|
|
@@ -4406,36 +4832,8 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4406
4832
|
}
|
|
4407
4833
|
}
|
|
4408
4834
|
async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
|
|
4409
|
-
const content = await (0,
|
|
4410
|
-
|
|
4411
|
-
try {
|
|
4412
|
-
plan = JSON.parse(content);
|
|
4413
|
-
} catch (_parseError) {
|
|
4414
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
4415
|
-
checkCredentials();
|
|
4416
|
-
try {
|
|
4417
|
-
const fixed = await callGroq(
|
|
4418
|
-
"You fix malformed JSON. Return ONLY valid JSON, no explanation.",
|
|
4419
|
-
`Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4420
|
-
The JSON should have: url, title, description, steps array.
|
|
4421
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
4422
|
-
|
|
4423
|
-
Malformed content:
|
|
4424
|
-
${content}`,
|
|
4425
|
-
logError2,
|
|
4426
|
-
userEmail,
|
|
4427
|
-
tier
|
|
4428
|
-
);
|
|
4429
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4430
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4431
|
-
plan = JSON.parse(jsonMatch[0]);
|
|
4432
|
-
fixTask.succeed("JSON fixed by AI");
|
|
4433
|
-
} catch (err) {
|
|
4434
|
-
await logError2("JSON Fix", err);
|
|
4435
|
-
fixTask.fail("Could not fix JSON");
|
|
4436
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4835
|
+
const content = await (0, import_promises12.readFile)(planPath, "utf-8");
|
|
4836
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
4439
4837
|
if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
|
|
4440
4838
|
throw new Error("Invalid plan: missing url or steps");
|
|
4441
4839
|
}
|
|
@@ -4467,7 +4865,7 @@ ${content}`,
|
|
|
4467
4865
|
renumberTask.fail("No valid steps");
|
|
4468
4866
|
throw new Error("Plan has no valid steps");
|
|
4469
4867
|
}
|
|
4470
|
-
await (0,
|
|
4868
|
+
await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
|
|
4471
4869
|
const removed = originalCount - plan.steps.length;
|
|
4472
4870
|
renumberTask.succeed(
|
|
4473
4871
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -4499,31 +4897,41 @@ function displayRateLimitInfo() {
|
|
|
4499
4897
|
const percentage = Math.round(limits.used / limits.limit * 100);
|
|
4500
4898
|
if (percentage >= 90) {
|
|
4501
4899
|
console.log();
|
|
4502
|
-
console.log(
|
|
4900
|
+
console.log(import_chalk9.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
|
|
4503
4901
|
if (limits.tier === "free") {
|
|
4504
|
-
console.log(
|
|
4902
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${getUpgradeUrl()}`));
|
|
4505
4903
|
}
|
|
4506
4904
|
} else if (percentage >= 50) {
|
|
4507
|
-
console.log(
|
|
4905
|
+
console.log(import_chalk9.default.dim(` ${remaining}/${limits.limit} requests remaining`));
|
|
4508
4906
|
}
|
|
4509
4907
|
}
|
|
4510
|
-
function displayUsageStatus(userEmail, userTier,
|
|
4511
|
-
const
|
|
4512
|
-
const
|
|
4513
|
-
console.log(
|
|
4908
|
+
async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
|
|
4909
|
+
const tier = userTier === "pro" ? "pro" : "free";
|
|
4910
|
+
const tierLabel = tier === "pro" ? import_chalk9.default.green("Pro") : "Free";
|
|
4911
|
+
console.log(import_chalk9.default.cyan(` \u2714 ${tierLabel} plan`));
|
|
4912
|
+
console.log(import_chalk9.default.dim(` ${userEmail}`));
|
|
4913
|
+
if (userEmail && userEmail !== "anonymous") {
|
|
4914
|
+
try {
|
|
4915
|
+
const status = await getStatusCached(userEmail);
|
|
4916
|
+
for (const line of formatUsageLines(status, tier)) {
|
|
4917
|
+
console.log(import_chalk9.default.dim(` ${line}`));
|
|
4918
|
+
}
|
|
4919
|
+
} catch {
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4514
4922
|
if (userTier === "free") {
|
|
4515
4923
|
const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
|
|
4516
|
-
console.log(
|
|
4924
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${upgradeUrl}`));
|
|
4517
4925
|
}
|
|
4518
4926
|
}
|
|
4519
4927
|
function displayPlanSummary(plan) {
|
|
4520
4928
|
console.log();
|
|
4521
|
-
console.log(
|
|
4522
|
-
console.log(
|
|
4929
|
+
console.log(import_chalk9.default.dim(` ${plan.title || "Untitled"}`));
|
|
4930
|
+
console.log(import_chalk9.default.dim(` ${plan.steps?.length || 0} steps`));
|
|
4523
4931
|
for (const step of plan.steps || []) {
|
|
4524
4932
|
const text = step.caption || step.action || "";
|
|
4525
4933
|
const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
|
|
4526
|
-
console.log(
|
|
4934
|
+
console.log(import_chalk9.default.dim(` ${step.id}. ${caption}`));
|
|
4527
4935
|
}
|
|
4528
4936
|
}
|
|
4529
4937
|
function getOutputDirFromUrl(url) {
|
|
@@ -4589,8 +4997,8 @@ async function runVideoPhase(params) {
|
|
|
4589
4997
|
if (trimStart2 !== void 0 && trimEnd !== void 0) {
|
|
4590
4998
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
4591
4999
|
} else {
|
|
4592
|
-
const mediaPath = (0,
|
|
4593
|
-
if ((0,
|
|
5000
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5001
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4594
5002
|
try {
|
|
4595
5003
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
4596
5004
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -4601,8 +5009,8 @@ async function runVideoPhase(params) {
|
|
|
4601
5009
|
}
|
|
4602
5010
|
}
|
|
4603
5011
|
} else {
|
|
4604
|
-
const mediaPath = (0,
|
|
4605
|
-
if ((0,
|
|
5012
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5013
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4606
5014
|
try {
|
|
4607
5015
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
4608
5016
|
} catch {
|
|
@@ -4615,7 +5023,7 @@ async function runVideoPhase(params) {
|
|
|
4615
5023
|
})
|
|
4616
5024
|
);
|
|
4617
5025
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
4618
|
-
const hasExistingRecording = existingRecordingMeta && (0,
|
|
5026
|
+
const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
|
|
4619
5027
|
let videoPath = recordingPath;
|
|
4620
5028
|
const firstStep = plan.steps[0];
|
|
4621
5029
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -4624,7 +5032,7 @@ async function runVideoPhase(params) {
|
|
|
4624
5032
|
header("Record");
|
|
4625
5033
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
4626
5034
|
stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
|
|
4627
|
-
await (0,
|
|
5035
|
+
await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
|
|
4628
5036
|
const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
|
|
4629
5037
|
meta.pageLoadTime = result.pageLoadTime;
|
|
4630
5038
|
await saveRecordingMeta(config.output.dir, meta);
|
|
@@ -4655,7 +5063,7 @@ async function runVideoPhase(params) {
|
|
|
4655
5063
|
if (videoOverlays.length > 0) {
|
|
4656
5064
|
const spliceTask = task("Splicing videos into recording");
|
|
4657
5065
|
try {
|
|
4658
|
-
const splicedPath = (0,
|
|
5066
|
+
const splicedPath = (0, import_node_path18.join)(workDir, "spliced.mp4");
|
|
4659
5067
|
await spliceVideos({
|
|
4660
5068
|
inputPath: videoPath,
|
|
4661
5069
|
outputPath: splicedPath,
|
|
@@ -4675,7 +5083,7 @@ async function runVideoPhase(params) {
|
|
|
4675
5083
|
if (stepTransitions.length > 0) {
|
|
4676
5084
|
const transitionTask = task("Applying transitions");
|
|
4677
5085
|
try {
|
|
4678
|
-
const transitionedPath = (0,
|
|
5086
|
+
const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
|
|
4679
5087
|
const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
|
|
4680
5088
|
await applyTransitions({
|
|
4681
5089
|
inputVideo: videoPath,
|
|
@@ -4696,15 +5104,15 @@ async function runVideoPhase(params) {
|
|
|
4696
5104
|
let mainContentDuration;
|
|
4697
5105
|
if (userTier === "free") {
|
|
4698
5106
|
const scriptDir = getScriptDir();
|
|
4699
|
-
let outroPath = (0,
|
|
4700
|
-
if (!(0,
|
|
4701
|
-
outroPath = (0,
|
|
5107
|
+
let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
|
|
5108
|
+
if (!(0, import_node_fs12.existsSync)(outroPath)) {
|
|
5109
|
+
outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
4702
5110
|
}
|
|
4703
|
-
if ((0,
|
|
5111
|
+
if ((0, import_node_fs12.existsSync)(outroPath)) {
|
|
4704
5112
|
try {
|
|
4705
5113
|
const mainDuration = await getMediaDuration(videoPath);
|
|
4706
5114
|
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
4707
|
-
const combinedPath = (0,
|
|
5115
|
+
const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
|
|
4708
5116
|
await concatVideoWithOutro({
|
|
4709
5117
|
inputPath: videoPath,
|
|
4710
5118
|
outroPath,
|
|
@@ -4751,19 +5159,19 @@ async function runVideoPhase(params) {
|
|
|
4751
5159
|
}
|
|
4752
5160
|
|
|
4753
5161
|
// src/commands/phases/voiceover-phase.ts
|
|
4754
|
-
var
|
|
4755
|
-
var
|
|
4756
|
-
var
|
|
5162
|
+
var import_node_fs13 = require("fs");
|
|
5163
|
+
var import_node_path19 = require("path");
|
|
5164
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
4757
5165
|
init_lib();
|
|
4758
5166
|
var INDENT5 = " ";
|
|
4759
5167
|
async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
4760
5168
|
if (fresh) {
|
|
4761
5169
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
4762
5170
|
for (const file of filesToDelete) {
|
|
4763
|
-
const filePath = (0,
|
|
4764
|
-
if ((0,
|
|
4765
|
-
const { unlink:
|
|
4766
|
-
await
|
|
5171
|
+
const filePath = (0, import_node_path19.join)(config.output.dir, file);
|
|
5172
|
+
if ((0, import_node_fs13.existsSync)(filePath)) {
|
|
5173
|
+
const { unlink: unlink3 } = await import("fs/promises");
|
|
5174
|
+
await unlink3(filePath).catch(() => {
|
|
4767
5175
|
});
|
|
4768
5176
|
}
|
|
4769
5177
|
}
|
|
@@ -4771,8 +5179,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
4771
5179
|
}
|
|
4772
5180
|
const voice = resolveVoice(config);
|
|
4773
5181
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
4774
|
-
const voiceoverFile = (0,
|
|
4775
|
-
const hasExistingVoiceover = existingMeta && (0,
|
|
5182
|
+
const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
|
|
5183
|
+
const hasExistingVoiceover = existingMeta && (0, import_node_fs13.existsSync)(voiceoverFile);
|
|
4776
5184
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
4777
5185
|
let voiceoverPath;
|
|
4778
5186
|
let stepAudio;
|
|
@@ -4795,9 +5203,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
4795
5203
|
const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
|
|
4796
5204
|
if (reuseCheck.canReuse) {
|
|
4797
5205
|
console.log();
|
|
4798
|
-
console.log(
|
|
5206
|
+
console.log(import_chalk10.default.dim(` Found existing voiceover in ${config.output.dir}`));
|
|
4799
5207
|
if (reuseCheck.reason) {
|
|
4800
|
-
console.log(
|
|
5208
|
+
console.log(import_chalk10.default.dim(` ${reuseCheck.reason}`));
|
|
4801
5209
|
}
|
|
4802
5210
|
const reuse = await confirm(`${INDENT5}Reuse existing voiceover?`);
|
|
4803
5211
|
if (reuse) {
|
|
@@ -4837,7 +5245,7 @@ async function captureCommand(url, lang) {
|
|
|
4837
5245
|
const captureResult = await captureUserSession(url, workDir);
|
|
4838
5246
|
info(`Captured ${captureResult.events.length} interactions over ${(captureResult.durationMs / 1e3).toFixed(0)}s`);
|
|
4839
5247
|
if (captureResult.events.length === 0) {
|
|
4840
|
-
console.log(
|
|
5248
|
+
console.log(import_chalk11.default.yellow(" No interactions captured. Exiting."));
|
|
4841
5249
|
return;
|
|
4842
5250
|
}
|
|
4843
5251
|
header("Convert");
|
|
@@ -4845,7 +5253,7 @@ async function captureCommand(url, lang) {
|
|
|
4845
5253
|
const rawSteps = eventsToSteps(captureResult.events, url);
|
|
4846
5254
|
if (rawSteps.length === 0) {
|
|
4847
5255
|
convertTask.fail("No actionable steps found");
|
|
4848
|
-
console.log(
|
|
5256
|
+
console.log(import_chalk11.default.yellow(" No actionable steps could be derived from the captured events."));
|
|
4849
5257
|
return;
|
|
4850
5258
|
}
|
|
4851
5259
|
convertTask.succeed(`${rawSteps.length} steps extracted`);
|
|
@@ -4877,16 +5285,16 @@ async function captureCommand(url, lang) {
|
|
|
4877
5285
|
...lang !== void 0 ? { lang } : {},
|
|
4878
5286
|
steps
|
|
4879
5287
|
};
|
|
4880
|
-
const planPath = (0,
|
|
5288
|
+
const planPath = (0, import_node_path20.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
4881
5289
|
await savePlan(plan, planPath);
|
|
4882
5290
|
console.log();
|
|
4883
|
-
console.log(
|
|
4884
|
-
console.log(
|
|
5291
|
+
console.log(import_chalk11.default.bold.green(` Plan saved: ${planPath}`));
|
|
5292
|
+
console.log(import_chalk11.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
|
|
4885
5293
|
console.log();
|
|
4886
5294
|
const makeVideo = await confirm(" Create polished demo video now?");
|
|
4887
5295
|
if (!makeVideo) {
|
|
4888
5296
|
console.log();
|
|
4889
|
-
console.log(
|
|
5297
|
+
console.log(import_chalk11.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
|
|
4890
5298
|
return;
|
|
4891
5299
|
}
|
|
4892
5300
|
header("Voiceover");
|
|
@@ -4902,20 +5310,20 @@ async function captureCommand(url, lang) {
|
|
|
4902
5310
|
userTier
|
|
4903
5311
|
});
|
|
4904
5312
|
console.log();
|
|
4905
|
-
console.log(
|
|
4906
|
-
console.log(
|
|
5313
|
+
console.log(import_chalk11.default.bold.green(" Done!"));
|
|
5314
|
+
console.log(import_chalk11.default.cyan(` ${finalPath}`));
|
|
4907
5315
|
}
|
|
4908
5316
|
|
|
4909
5317
|
// src/commands/init-cmd.ts
|
|
4910
|
-
var
|
|
4911
|
-
var
|
|
4912
|
-
var
|
|
4913
|
-
var
|
|
5318
|
+
var import_node_fs14 = require("fs");
|
|
5319
|
+
var import_promises14 = require("fs/promises");
|
|
5320
|
+
var import_node_path21 = require("path");
|
|
5321
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
4914
5322
|
init_lib();
|
|
4915
5323
|
async function initCommand() {
|
|
4916
|
-
const configPath = (0,
|
|
4917
|
-
if ((0,
|
|
4918
|
-
console.log(
|
|
5324
|
+
const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
|
|
5325
|
+
if ((0, import_node_fs14.existsSync)(configPath)) {
|
|
5326
|
+
console.log(import_chalk12.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
4919
5327
|
return;
|
|
4920
5328
|
}
|
|
4921
5329
|
const defaultConfig = {
|
|
@@ -4938,32 +5346,32 @@ async function initCommand() {
|
|
|
4938
5346
|
volume: 0.3
|
|
4939
5347
|
}
|
|
4940
5348
|
};
|
|
4941
|
-
await (0,
|
|
5349
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
4942
5350
|
`);
|
|
4943
|
-
console.log(
|
|
4944
|
-
console.log(
|
|
5351
|
+
console.log(import_chalk12.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
5352
|
+
console.log(import_chalk12.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
4945
5353
|
}
|
|
4946
5354
|
async function configCommand(key, value) {
|
|
4947
|
-
const configPath = (0,
|
|
5355
|
+
const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
|
|
4948
5356
|
if (!key) {
|
|
4949
|
-
if (!(0,
|
|
4950
|
-
console.log(
|
|
5357
|
+
if (!(0, import_node_fs14.existsSync)(configPath)) {
|
|
5358
|
+
console.log(import_chalk12.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
4951
5359
|
return;
|
|
4952
5360
|
}
|
|
4953
|
-
const content = await (0,
|
|
4954
|
-
console.log(
|
|
5361
|
+
const content = await (0, import_promises14.readFile)(configPath, "utf-8");
|
|
5362
|
+
console.log(import_chalk12.default.cyan(` ${CONFIG_FILE}:`));
|
|
4955
5363
|
console.log(content);
|
|
4956
5364
|
return;
|
|
4957
5365
|
}
|
|
4958
5366
|
if (!value) {
|
|
4959
|
-
console.log(
|
|
4960
|
-
console.log(
|
|
5367
|
+
console.log(import_chalk12.default.red(` Missing value for ${key}`));
|
|
5368
|
+
console.log(import_chalk12.default.dim(` Usage: qp config ${key} <value>`));
|
|
4961
5369
|
return;
|
|
4962
5370
|
}
|
|
4963
5371
|
let config = {};
|
|
4964
|
-
if ((0,
|
|
5372
|
+
if ((0, import_node_fs14.existsSync)(configPath)) {
|
|
4965
5373
|
try {
|
|
4966
|
-
config = JSON.parse(await (0,
|
|
5374
|
+
config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
|
|
4967
5375
|
} catch {
|
|
4968
5376
|
}
|
|
4969
5377
|
}
|
|
@@ -4985,124 +5393,101 @@ async function configCommand(key, value) {
|
|
|
4985
5393
|
}
|
|
4986
5394
|
obj[keys[keys.length - 1]] = parsedValue;
|
|
4987
5395
|
}
|
|
4988
|
-
await (0,
|
|
5396
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
4989
5397
|
`);
|
|
4990
|
-
console.log(
|
|
5398
|
+
console.log(import_chalk12.default.green(` \u2714 Set ${key} = ${value}`));
|
|
4991
5399
|
}
|
|
4992
5400
|
|
|
4993
|
-
// src/commands/
|
|
4994
|
-
var
|
|
4995
|
-
var import_promises16 = require("fs/promises");
|
|
4996
|
-
var import_node_path23 = require("path");
|
|
4997
|
-
var import_chalk14 = __toESM(require("chalk"));
|
|
4998
|
-
|
|
4999
|
-
// src/help.ts
|
|
5000
|
-
var import_chalk9 = __toESM(require("chalk"));
|
|
5401
|
+
// src/commands/login-cmd.ts
|
|
5402
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
5001
5403
|
init_lib();
|
|
5002
|
-
|
|
5003
|
-
// src/logo.ts
|
|
5004
|
-
var import_figlet = __toESM(require("figlet"));
|
|
5005
|
-
var import_gradient_string = __toESM(require("gradient-string"));
|
|
5006
|
-
var DEFAULT_LOGO_CONFIG = {
|
|
5007
|
-
text: "QuickPeek",
|
|
5008
|
-
font: "Slant",
|
|
5009
|
-
horizontalLayout: "default",
|
|
5010
|
-
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
5011
|
-
suffix: " \u{1F440}"
|
|
5012
|
-
};
|
|
5013
|
-
function renderLogo(config = {}) {
|
|
5014
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
5015
|
-
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
5016
|
-
font: resolved.font,
|
|
5017
|
-
horizontalLayout: resolved.horizontalLayout
|
|
5018
|
-
});
|
|
5019
|
-
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
5020
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
5021
|
-
}
|
|
5022
|
-
|
|
5023
|
-
// src/help.ts
|
|
5024
|
-
var cmd = import_chalk9.default.cyan;
|
|
5025
|
-
var arg = import_chalk9.default.green;
|
|
5026
|
-
var opt = import_chalk9.default.magenta;
|
|
5027
|
-
var desc = import_chalk9.default.white;
|
|
5028
|
-
var ex = import_chalk9.default.dim;
|
|
5029
|
-
function pad2(colored, visible, width) {
|
|
5030
|
-
return colored + " ".repeat(Math.max(0, width - visible.length));
|
|
5031
|
-
}
|
|
5032
|
-
function showLogo() {
|
|
5033
|
-
const logo = renderLogo();
|
|
5034
|
-
console.log(logo);
|
|
5035
|
-
console.log(import_chalk9.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
5036
|
-
console.log(import_chalk9.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
5037
|
-
}
|
|
5038
|
-
function showHelp() {
|
|
5039
|
-
showLogo();
|
|
5040
|
-
const W = 28;
|
|
5041
|
-
const usage = [
|
|
5042
|
-
[`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
|
|
5043
|
-
[`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`, `qp <url> --script <file>`, "Use your own narration script"],
|
|
5044
|
-
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
5045
|
-
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
5046
|
-
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
5047
|
-
];
|
|
5048
|
-
const options = [
|
|
5049
|
-
[`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
|
|
5050
|
-
[`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`, `-d, --description "..."`, "Guide AI with an app description"],
|
|
5051
|
-
[`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`, `-s, --script <file>`, "Narration script (one caption per line)"],
|
|
5052
|
-
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
5053
|
-
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
5054
|
-
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
|
|
5055
|
-
];
|
|
5056
|
-
const examples = [
|
|
5057
|
-
`${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
|
|
5058
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
|
|
5059
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
|
|
5060
|
-
`${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
|
|
5061
|
-
];
|
|
5404
|
+
async function loginCommand() {
|
|
5062
5405
|
console.log();
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5406
|
+
showLogo();
|
|
5407
|
+
const existing = await loadUserData();
|
|
5408
|
+
if (existing.email && existing.verified) {
|
|
5409
|
+
console.log(import_chalk13.default.dim(` Already signed in as ${existing.email}`));
|
|
5410
|
+
console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp logout") + import_chalk13.default.dim(" first to switch accounts."));
|
|
5411
|
+
console.log();
|
|
5412
|
+
try {
|
|
5413
|
+
const status = await getStatusCached(existing.email);
|
|
5414
|
+
const tier2 = await getTierByEmail(existing.email);
|
|
5415
|
+
const tierLabel2 = tier2 === "pro" ? import_chalk13.default.green("Pro") : "Free";
|
|
5416
|
+
console.log(` ${tierLabel2} plan`);
|
|
5417
|
+
for (const line of formatUsageLines(status, tier2)) {
|
|
5418
|
+
console.log(import_chalk13.default.dim(` ${line}`));
|
|
5419
|
+
}
|
|
5420
|
+
} catch {
|
|
5421
|
+
}
|
|
5422
|
+
console.log();
|
|
5423
|
+
return;
|
|
5066
5424
|
}
|
|
5067
|
-
console.log();
|
|
5068
|
-
console.log(
|
|
5069
|
-
|
|
5070
|
-
|
|
5425
|
+
console.log(import_chalk13.default.white(" Sign in with your Google account to track usage"));
|
|
5426
|
+
console.log(import_chalk13.default.white(" and unlock Pro features."));
|
|
5427
|
+
const email = await browserLogin();
|
|
5428
|
+
if (!email) {
|
|
5429
|
+
console.log();
|
|
5430
|
+
console.log(import_chalk13.default.yellow(" Sign-in cancelled or timed out."));
|
|
5431
|
+
console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp login") + import_chalk13.default.dim(" to try again."));
|
|
5432
|
+
console.log();
|
|
5433
|
+
return;
|
|
5071
5434
|
}
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5435
|
+
let tier = "free";
|
|
5436
|
+
try {
|
|
5437
|
+
tier = await getTierByEmail(email);
|
|
5438
|
+
} catch {
|
|
5076
5439
|
}
|
|
5440
|
+
await saveUserData({ email, verified: true, tier });
|
|
5441
|
+
console.log();
|
|
5442
|
+
const tierLabel = tier === "pro" ? import_chalk13.default.green("Pro") : "Free";
|
|
5443
|
+
console.log(import_chalk13.default.green(` \u2713 Signed in as ${email}`));
|
|
5444
|
+
console.log(` ${tierLabel} plan`);
|
|
5445
|
+
console.log();
|
|
5446
|
+
}
|
|
5447
|
+
async function logoutCommand() {
|
|
5077
5448
|
console.log();
|
|
5078
|
-
|
|
5079
|
-
|
|
5449
|
+
const userData = await loadUserData();
|
|
5450
|
+
if (!userData.email) {
|
|
5451
|
+
console.log(import_chalk13.default.dim(" Not signed in."));
|
|
5452
|
+
console.log();
|
|
5453
|
+
return;
|
|
5454
|
+
}
|
|
5455
|
+
const email = userData.email;
|
|
5456
|
+
await saveUserData({ email: "", verified: false, tier: "free" });
|
|
5457
|
+
console.log(import_chalk13.default.green(` \u2713 Signed out (${email})`));
|
|
5080
5458
|
console.log();
|
|
5081
5459
|
}
|
|
5082
5460
|
|
|
5083
5461
|
// src/commands/record-cmd.ts
|
|
5462
|
+
var import_node_fs17 = require("fs");
|
|
5463
|
+
var import_promises17 = require("fs/promises");
|
|
5464
|
+
var import_node_path24 = require("path");
|
|
5465
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
5084
5466
|
init_lib();
|
|
5085
5467
|
|
|
5086
5468
|
// src/commands/phases/auth-phase.ts
|
|
5087
|
-
var
|
|
5469
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
5088
5470
|
init_lib();
|
|
5089
5471
|
|
|
5090
5472
|
// src/commands/utils/auth-helpers.ts
|
|
5091
|
-
var
|
|
5473
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
5092
5474
|
init_lib();
|
|
5093
5475
|
async function handleRegistrationGate() {
|
|
5094
5476
|
console.log();
|
|
5095
|
-
console.log(
|
|
5096
|
-
console.log(
|
|
5097
|
-
console.log();
|
|
5098
|
-
console.log(import_chalk10.default.dim(" Benefits of registering:"));
|
|
5099
|
-
console.log(import_chalk10.default.dim(" \u2022 20 more videos per month"));
|
|
5100
|
-
console.log(import_chalk10.default.dim(" \u2022 Keep your video history"));
|
|
5101
|
-
console.log(import_chalk10.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
5477
|
+
console.log(import_chalk14.default.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
5478
|
+
console.log(import_chalk14.default.white(" Sign in to continue creating videos."));
|
|
5102
5479
|
console.log();
|
|
5103
|
-
|
|
5480
|
+
console.log(import_chalk14.default.dim(" Benefits of signing in:"));
|
|
5481
|
+
console.log(import_chalk14.default.dim(" \u2022 20 more videos per month"));
|
|
5482
|
+
console.log(import_chalk14.default.dim(" \u2022 Keep your video history"));
|
|
5483
|
+
console.log(import_chalk14.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
5484
|
+
let email = await browserLogin();
|
|
5485
|
+
if (!email) {
|
|
5486
|
+
console.log();
|
|
5487
|
+
email = await promptInput("Email: ");
|
|
5488
|
+
}
|
|
5104
5489
|
if (!email || !email.includes("@")) {
|
|
5105
|
-
console.log(
|
|
5490
|
+
console.log(import_chalk14.default.red(" Invalid email. Run qp again to try again."));
|
|
5106
5491
|
return null;
|
|
5107
5492
|
}
|
|
5108
5493
|
const registerTask = task("Sending verification email");
|
|
@@ -5117,23 +5502,23 @@ async function handleRegistrationGate() {
|
|
|
5117
5502
|
}
|
|
5118
5503
|
registerTask.succeed("Check your email!");
|
|
5119
5504
|
console.log();
|
|
5120
|
-
console.log(
|
|
5121
|
-
console.log(
|
|
5505
|
+
console.log(import_chalk14.default.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
5506
|
+
console.log(import_chalk14.default.white(` ${email}`));
|
|
5122
5507
|
console.log();
|
|
5123
|
-
console.log(
|
|
5508
|
+
console.log(import_chalk14.default.dim(" Click the link in the email, then press Enter to continue..."));
|
|
5124
5509
|
await promptInput("");
|
|
5125
5510
|
const verifyTask = task("Checking verification");
|
|
5126
|
-
const
|
|
5127
|
-
if (
|
|
5511
|
+
const verified = await isVerified(email);
|
|
5512
|
+
if (verified) {
|
|
5128
5513
|
verifyTask.succeed("Email verified!");
|
|
5129
5514
|
return { email, verified: true };
|
|
5130
5515
|
}
|
|
5131
5516
|
verifyTask.fail("Not verified yet");
|
|
5132
5517
|
console.log();
|
|
5133
|
-
console.log(
|
|
5134
|
-
console.log(
|
|
5518
|
+
console.log(import_chalk14.default.yellow(" Email not verified. Check your inbox and spam folder."));
|
|
5519
|
+
console.log(import_chalk14.default.dim(" Run qp again after clicking the verification link."));
|
|
5135
5520
|
console.log();
|
|
5136
|
-
console.log(
|
|
5521
|
+
console.log(import_chalk14.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
5137
5522
|
console.log();
|
|
5138
5523
|
return { email, verified: false };
|
|
5139
5524
|
}
|
|
@@ -5141,27 +5526,27 @@ async function handleRegistrationGate() {
|
|
|
5141
5526
|
// src/commands/phases/auth-phase.ts
|
|
5142
5527
|
async function runAuthPhase(input) {
|
|
5143
5528
|
let { userEmail, userTier, localVideoCount } = input;
|
|
5144
|
-
const { isVerified } = input;
|
|
5145
|
-
if (userEmail &&
|
|
5529
|
+
const { isVerified: isVerified2 } = input;
|
|
5530
|
+
if (userEmail && isVerified2) {
|
|
5146
5531
|
const tierTask = task("Checking subscription");
|
|
5147
|
-
userTier = await
|
|
5532
|
+
userTier = await getTierByEmail(userEmail);
|
|
5148
5533
|
tierTask.stop();
|
|
5149
|
-
displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5534
|
+
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5150
5535
|
if (userTier !== input.userTier) {
|
|
5151
5536
|
await saveUserData({ tier: userTier });
|
|
5152
5537
|
}
|
|
5153
|
-
} else if (userEmail && !
|
|
5538
|
+
} else if (userEmail && !isVerified2) {
|
|
5154
5539
|
const verifyTask = task("Checking verification");
|
|
5155
|
-
const
|
|
5156
|
-
if (
|
|
5540
|
+
const verified = await isVerified(userEmail);
|
|
5541
|
+
if (verified) {
|
|
5157
5542
|
verifyTask.stop();
|
|
5158
|
-
await
|
|
5159
|
-
|
|
5160
|
-
displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5543
|
+
userTier = await getTierByEmail(userEmail);
|
|
5544
|
+
await saveUserData({ verified: true, tier: userTier });
|
|
5545
|
+
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5161
5546
|
} else {
|
|
5162
5547
|
verifyTask.fail("Email not verified");
|
|
5163
5548
|
console.log();
|
|
5164
|
-
console.log(
|
|
5549
|
+
console.log(import_chalk15.default.yellow(" Check your inbox for the verification email."));
|
|
5165
5550
|
console.log();
|
|
5166
5551
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
5167
5552
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -5181,29 +5566,29 @@ async function runAuthPhase(input) {
|
|
|
5181
5566
|
registerTask.stop();
|
|
5182
5567
|
userEmail = newEmail;
|
|
5183
5568
|
await saveUserData({ email: newEmail, verified: true });
|
|
5184
|
-
displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5569
|
+
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5185
5570
|
} else {
|
|
5186
5571
|
registerTask.succeed("Check your email!");
|
|
5187
5572
|
await saveUserData({ email: newEmail, verified: false });
|
|
5188
5573
|
console.log();
|
|
5189
|
-
console.log(
|
|
5190
|
-
console.log(
|
|
5574
|
+
console.log(import_chalk15.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
5575
|
+
console.log(import_chalk15.default.dim(" Click the link in the email, then run qp again."));
|
|
5191
5576
|
console.log();
|
|
5192
5577
|
gracefulExit();
|
|
5193
5578
|
return null;
|
|
5194
5579
|
}
|
|
5195
5580
|
} else {
|
|
5196
5581
|
const recheckTask = task("Rechecking verification");
|
|
5197
|
-
const
|
|
5198
|
-
if (
|
|
5582
|
+
const recheckVerified = await isVerified(userEmail);
|
|
5583
|
+
if (recheckVerified) {
|
|
5199
5584
|
recheckTask.stop();
|
|
5200
|
-
await
|
|
5201
|
-
|
|
5202
|
-
displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5585
|
+
userTier = await getTierByEmail(userEmail);
|
|
5586
|
+
await saveUserData({ verified: true, tier: userTier });
|
|
5587
|
+
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5203
5588
|
} else {
|
|
5204
5589
|
recheckTask.fail("Still not verified");
|
|
5205
5590
|
console.log();
|
|
5206
|
-
console.log(
|
|
5591
|
+
console.log(import_chalk15.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
5207
5592
|
console.log();
|
|
5208
5593
|
gracefulExit();
|
|
5209
5594
|
return null;
|
|
@@ -5226,28 +5611,34 @@ async function runAuthPhase(input) {
|
|
|
5226
5611
|
gracefulExit();
|
|
5227
5612
|
return null;
|
|
5228
5613
|
}
|
|
5229
|
-
displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5614
|
+
await displayUsageStatus(userEmail, userTier, localVideoCount);
|
|
5230
5615
|
} else {
|
|
5231
5616
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
5232
5617
|
console.log();
|
|
5233
|
-
console.log(
|
|
5234
|
-
console.log(
|
|
5618
|
+
console.log(import_chalk15.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
5619
|
+
console.log(import_chalk15.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
|
|
5235
5620
|
userEmail = await setupUserEmail();
|
|
5236
|
-
|
|
5621
|
+
let registeredVerified = false;
|
|
5622
|
+
try {
|
|
5623
|
+
const reg = await registerFreeUser(userEmail);
|
|
5624
|
+
registeredVerified = Boolean(reg.success && reg.alreadyVerified);
|
|
5625
|
+
} catch {
|
|
5626
|
+
}
|
|
5627
|
+
await saveUserData({ email: userEmail, verified: registeredVerified, localVideoCount });
|
|
5237
5628
|
}
|
|
5238
5629
|
return {
|
|
5239
5630
|
userEmail: userEmail ?? "anonymous",
|
|
5240
5631
|
userTier,
|
|
5241
5632
|
localVideoCount,
|
|
5242
|
-
isVerified
|
|
5633
|
+
isVerified: isVerified2
|
|
5243
5634
|
};
|
|
5244
5635
|
}
|
|
5245
5636
|
|
|
5246
5637
|
// src/commands/phases/plan-phase.ts
|
|
5247
|
-
var
|
|
5248
|
-
var
|
|
5249
|
-
var
|
|
5250
|
-
var
|
|
5638
|
+
var import_node_fs15 = require("fs");
|
|
5639
|
+
var import_promises15 = require("fs/promises");
|
|
5640
|
+
var import_node_path22 = require("path");
|
|
5641
|
+
var import_chalk16 = __toESM(require("chalk"));
|
|
5251
5642
|
|
|
5252
5643
|
// src/ai/fix.ts
|
|
5253
5644
|
init_lib();
|
|
@@ -5404,9 +5795,9 @@ async function runPlanPhase(params) {
|
|
|
5404
5795
|
const extensionPath = config.browser?.extension;
|
|
5405
5796
|
let existingPlan = null;
|
|
5406
5797
|
let existingPlanPath = null;
|
|
5407
|
-
if ((0,
|
|
5798
|
+
if ((0, import_node_fs15.existsSync)(planPath)) {
|
|
5408
5799
|
try {
|
|
5409
|
-
const parsed = JSON.parse(await (0,
|
|
5800
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5410
5801
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5411
5802
|
existingPlan = parsed;
|
|
5412
5803
|
existingPlanPath = planPath;
|
|
@@ -5416,12 +5807,12 @@ async function runPlanPhase(params) {
|
|
|
5416
5807
|
}
|
|
5417
5808
|
if (!existingPlan) {
|
|
5418
5809
|
try {
|
|
5419
|
-
const files = (0,
|
|
5420
|
-
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0,
|
|
5810
|
+
const files = (0, import_node_fs15.readdirSync)(config.output.dir);
|
|
5811
|
+
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path22.basename)(planPath));
|
|
5421
5812
|
for (const file of planFiles) {
|
|
5422
|
-
const filePath = (0,
|
|
5813
|
+
const filePath = (0, import_node_path22.join)(config.output.dir, file);
|
|
5423
5814
|
try {
|
|
5424
|
-
const parsed = JSON.parse(await (0,
|
|
5815
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
|
|
5425
5816
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5426
5817
|
existingPlan = parsed;
|
|
5427
5818
|
existingPlanPath = filePath;
|
|
@@ -5433,9 +5824,9 @@ async function runPlanPhase(params) {
|
|
|
5433
5824
|
} catch {
|
|
5434
5825
|
}
|
|
5435
5826
|
}
|
|
5436
|
-
if (!existingPlan && (0,
|
|
5827
|
+
if (!existingPlan && (0, import_node_fs15.existsSync)(legacyPlanPath)) {
|
|
5437
5828
|
try {
|
|
5438
|
-
const parsed = JSON.parse(await (0,
|
|
5829
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
|
|
5439
5830
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5440
5831
|
existingPlan = parsed;
|
|
5441
5832
|
existingPlanPath = legacyPlanPath;
|
|
@@ -5443,8 +5834,8 @@ async function runPlanPhase(params) {
|
|
|
5443
5834
|
} catch {
|
|
5444
5835
|
}
|
|
5445
5836
|
}
|
|
5446
|
-
const
|
|
5447
|
-
const urlMatches = existingPlan &&
|
|
5837
|
+
const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
5838
|
+
const urlMatches = existingPlan && normalizeUrl4(existingPlan.url) === normalizeUrl4(url);
|
|
5448
5839
|
let plan;
|
|
5449
5840
|
const doTranslate = async (failMsg) => {
|
|
5450
5841
|
const translateTask = task(`Translating to ${targetLang}`);
|
|
@@ -5461,23 +5852,23 @@ async function runPlanPhase(params) {
|
|
|
5461
5852
|
};
|
|
5462
5853
|
if (existingPlan && existingPlanPath) {
|
|
5463
5854
|
console.log();
|
|
5464
|
-
console.log(
|
|
5465
|
-
console.log(
|
|
5855
|
+
console.log(import_chalk16.default.dim(` Found existing plan: `) + import_chalk16.default.cyan(formatClickablePath(existingPlanPath)));
|
|
5856
|
+
console.log(import_chalk16.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
|
|
5466
5857
|
if (!urlMatches) {
|
|
5467
|
-
console.log(
|
|
5468
|
-
console.log(
|
|
5469
|
-
console.log(
|
|
5858
|
+
console.log(import_chalk16.default.yellow(` \u26A0 Plan URL differs from argument:`));
|
|
5859
|
+
console.log(import_chalk16.default.yellow(` Plan: ${existingPlan.url}`));
|
|
5860
|
+
console.log(import_chalk16.default.yellow(` Arg: ${url}`));
|
|
5470
5861
|
}
|
|
5471
5862
|
const existingLang = existingPlan.lang || "en";
|
|
5472
5863
|
if (existingLang !== targetLang) {
|
|
5473
|
-
console.log(
|
|
5864
|
+
console.log(import_chalk16.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
5474
5865
|
}
|
|
5475
5866
|
const reuse = await confirm(`${INDENT6}Use existing plan?`);
|
|
5476
5867
|
if (reuse) {
|
|
5477
5868
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
5478
5869
|
if (existingPlanPath !== planPath) {
|
|
5479
5870
|
await savePlan(plan, planPath);
|
|
5480
|
-
console.log(
|
|
5871
|
+
console.log(import_chalk16.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
5481
5872
|
}
|
|
5482
5873
|
if (existingLang !== targetLang) {
|
|
5483
5874
|
const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
|
|
@@ -5512,7 +5903,7 @@ async function runPlanPhase(params) {
|
|
|
5512
5903
|
uploadIdx++;
|
|
5513
5904
|
}
|
|
5514
5905
|
}
|
|
5515
|
-
console.log(
|
|
5906
|
+
console.log(import_chalk16.default.dim(` Preserved upload paths from previous plan`));
|
|
5516
5907
|
}
|
|
5517
5908
|
await savePlan(plan, planPath);
|
|
5518
5909
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -5536,8 +5927,8 @@ async function runPlanPhase(params) {
|
|
|
5536
5927
|
}
|
|
5537
5928
|
displayPlanSummary(plan);
|
|
5538
5929
|
console.log();
|
|
5539
|
-
console.log(
|
|
5540
|
-
console.log(
|
|
5930
|
+
console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
5931
|
+
console.log(import_chalk16.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
|
|
5541
5932
|
const MAX_REFINEMENTS = 10;
|
|
5542
5933
|
let refinementCount = 0;
|
|
5543
5934
|
while (true) {
|
|
@@ -5555,15 +5946,15 @@ async function runPlanPhase(params) {
|
|
|
5555
5946
|
refineTask.succeed("Plan refined");
|
|
5556
5947
|
displayRateLimitInfo();
|
|
5557
5948
|
if (plan.steps.length > maxSteps) {
|
|
5558
|
-
console.log(
|
|
5949
|
+
console.log(import_chalk16.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
5559
5950
|
}
|
|
5560
5951
|
displayPlanSummary(plan);
|
|
5561
5952
|
console.log();
|
|
5562
|
-
console.log(
|
|
5953
|
+
console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
5563
5954
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
5564
5955
|
console.log();
|
|
5565
|
-
console.log(
|
|
5566
|
-
console.log(
|
|
5956
|
+
console.log(import_chalk16.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
|
|
5957
|
+
console.log(import_chalk16.default.dim(` Further refinements may incur additional costs.`));
|
|
5567
5958
|
const continueRefining = await confirm(`${INDENT6}Continue refining?`);
|
|
5568
5959
|
if (!continueRefining) break;
|
|
5569
5960
|
}
|
|
@@ -5575,12 +5966,12 @@ async function runPlanPhase(params) {
|
|
|
5575
5966
|
const planLang = plan.lang || "en";
|
|
5576
5967
|
if (planLang !== targetLang) {
|
|
5577
5968
|
let useExistingTranslation = false;
|
|
5578
|
-
if ((0,
|
|
5969
|
+
if ((0, import_node_fs15.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
5579
5970
|
try {
|
|
5580
|
-
const translatedPlan = JSON.parse(await (0,
|
|
5971
|
+
const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5581
5972
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
5582
5973
|
console.log();
|
|
5583
|
-
console.log(
|
|
5974
|
+
console.log(import_chalk16.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
5584
5975
|
const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
|
|
5585
5976
|
if (useTranslated) {
|
|
5586
5977
|
plan = translatedPlan;
|
|
@@ -5592,7 +5983,7 @@ async function runPlanPhase(params) {
|
|
|
5592
5983
|
}
|
|
5593
5984
|
if (!useExistingTranslation) {
|
|
5594
5985
|
console.log();
|
|
5595
|
-
console.log(
|
|
5986
|
+
console.log(import_chalk16.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
5596
5987
|
const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
|
|
5597
5988
|
if (translate) await doTranslate("Translation failed, using original");
|
|
5598
5989
|
}
|
|
@@ -5601,8 +5992,8 @@ async function runPlanPhase(params) {
|
|
|
5601
5992
|
}
|
|
5602
5993
|
|
|
5603
5994
|
// src/commands/phases/validation-phase.ts
|
|
5604
|
-
var
|
|
5605
|
-
var
|
|
5995
|
+
var import_promises16 = require("fs/promises");
|
|
5996
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
5606
5997
|
async function runValidationPhase(params) {
|
|
5607
5998
|
let { plan } = params;
|
|
5608
5999
|
const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
|
|
@@ -5653,21 +6044,21 @@ async function runValidationPhase(params) {
|
|
|
5653
6044
|
const stepNum = result.stepIndex + 1;
|
|
5654
6045
|
const step = plan.steps[result.stepIndex];
|
|
5655
6046
|
console.log();
|
|
5656
|
-
console.log(
|
|
6047
|
+
console.log(import_chalk17.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
5657
6048
|
const caption = step.caption || "";
|
|
5658
|
-
if (caption) console.log(
|
|
5659
|
-
console.log(
|
|
5660
|
-
if (step.value) console.log(
|
|
5661
|
-
console.log(
|
|
6049
|
+
if (caption) console.log(import_chalk17.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
|
|
6050
|
+
console.log(import_chalk17.default.dim(` Target: ${step.target || "none"}`));
|
|
6051
|
+
if (step.value) console.log(import_chalk17.default.dim(` Value: ${step.value}`));
|
|
6052
|
+
console.log(import_chalk17.default.dim(` Error: ${result.error}`));
|
|
5662
6053
|
if (result.suggestion) {
|
|
5663
|
-
console.log(
|
|
6054
|
+
console.log(import_chalk17.default.yellow(` Hint: ${result.suggestion}`));
|
|
5664
6055
|
}
|
|
5665
6056
|
console.log();
|
|
5666
6057
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
5667
6058
|
if (isUploadMissingFile) {
|
|
5668
6059
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
5669
6060
|
if (filePath.toLowerCase() === "skip") {
|
|
5670
|
-
console.log(
|
|
6061
|
+
console.log(import_chalk17.default.yellow(` Skipping step ${stepNum}`));
|
|
5671
6062
|
continue;
|
|
5672
6063
|
}
|
|
5673
6064
|
if (filePath) {
|
|
@@ -5676,13 +6067,13 @@ async function runValidationPhase(params) {
|
|
|
5676
6067
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5677
6068
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
5678
6069
|
if (newResult2?.success) {
|
|
5679
|
-
console.log(
|
|
6070
|
+
console.log(import_chalk17.default.green(` \u2713 Step ${stepNum} now passes`));
|
|
5680
6071
|
validationResult = revalidation2;
|
|
5681
6072
|
continue;
|
|
5682
6073
|
}
|
|
5683
6074
|
result.error = newResult2?.error;
|
|
5684
6075
|
result.suggestion = newResult2?.suggestion;
|
|
5685
|
-
console.log(
|
|
6076
|
+
console.log(import_chalk17.default.red(` Still failing: ${result.error}`));
|
|
5686
6077
|
}
|
|
5687
6078
|
}
|
|
5688
6079
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -5704,8 +6095,8 @@ async function runValidationPhase(params) {
|
|
|
5704
6095
|
await savePlan(plan, planPath);
|
|
5705
6096
|
fixTask.succeed("AI suggested a fix");
|
|
5706
6097
|
const fixedStep = plan.steps[result.stepIndex];
|
|
5707
|
-
console.log(
|
|
5708
|
-
if (fixedStep.value) console.log(
|
|
6098
|
+
console.log(import_chalk17.default.dim(` New target: ${fixedStep.target || "none"}`));
|
|
6099
|
+
if (fixedStep.value) console.log(import_chalk17.default.dim(` New value: ${fixedStep.value}`));
|
|
5709
6100
|
} catch {
|
|
5710
6101
|
fixTask.fail("AI could not fix automatically");
|
|
5711
6102
|
}
|
|
@@ -5721,14 +6112,14 @@ async function runValidationPhase(params) {
|
|
|
5721
6112
|
result.error = newResult?.error;
|
|
5722
6113
|
result.suggestion = newResult?.suggestion;
|
|
5723
6114
|
console.log();
|
|
5724
|
-
console.log(
|
|
6115
|
+
console.log(import_chalk17.default.yellow(` Edit step ${stepNum} in: `) + import_chalk17.default.cyan(formatClickablePath(planPath)));
|
|
5725
6116
|
console.log();
|
|
5726
6117
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
5727
6118
|
if (choice.toLowerCase() === "skip") {
|
|
5728
|
-
console.log(
|
|
6119
|
+
console.log(import_chalk17.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
5729
6120
|
continue;
|
|
5730
6121
|
}
|
|
5731
|
-
plan = JSON.parse(await (0,
|
|
6122
|
+
plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
5732
6123
|
const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
|
|
5733
6124
|
const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5734
6125
|
const finalResult = finalRevalidation.results[result.stepIndex];
|
|
@@ -5752,31 +6143,31 @@ async function runValidationPhase(params) {
|
|
|
5752
6143
|
}
|
|
5753
6144
|
|
|
5754
6145
|
// src/commands/utils/chrome-extension.ts
|
|
5755
|
-
var
|
|
5756
|
-
var
|
|
5757
|
-
var
|
|
6146
|
+
var import_node_fs16 = require("fs");
|
|
6147
|
+
var import_node_os3 = require("os");
|
|
6148
|
+
var import_node_path23 = require("path");
|
|
5758
6149
|
function resolveChromeExtension(extensionId) {
|
|
5759
6150
|
const platform = process.platform;
|
|
5760
6151
|
const possibleDirs = [];
|
|
5761
6152
|
const getChromeExtDirs = (userDataPath) => {
|
|
5762
6153
|
const dirs = [];
|
|
5763
6154
|
try {
|
|
5764
|
-
const profiles = (0,
|
|
6155
|
+
const profiles = (0, import_node_fs16.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
5765
6156
|
for (const profile of profiles) {
|
|
5766
|
-
dirs.push((0,
|
|
6157
|
+
dirs.push((0, import_node_path23.join)(userDataPath, profile, "Extensions"));
|
|
5767
6158
|
}
|
|
5768
6159
|
} catch {
|
|
5769
|
-
dirs.push((0,
|
|
6160
|
+
dirs.push((0, import_node_path23.join)(userDataPath, "Default", "Extensions"));
|
|
5770
6161
|
}
|
|
5771
6162
|
return dirs;
|
|
5772
6163
|
};
|
|
5773
6164
|
if (platform === "win32") {
|
|
5774
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6165
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
5775
6166
|
} else if (platform === "darwin") {
|
|
5776
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6167
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
5777
6168
|
} else {
|
|
5778
6169
|
try {
|
|
5779
|
-
const windowsUsers = (0,
|
|
6170
|
+
const windowsUsers = (0, import_node_fs16.readdirSync)("/mnt/c/Users").filter(
|
|
5780
6171
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
5781
6172
|
);
|
|
5782
6173
|
for (const user of windowsUsers) {
|
|
@@ -5784,22 +6175,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
5784
6175
|
}
|
|
5785
6176
|
} catch {
|
|
5786
6177
|
}
|
|
5787
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6178
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
|
|
5788
6179
|
}
|
|
5789
6180
|
for (const chromeExtDir of possibleDirs) {
|
|
5790
|
-
const extDir = (0,
|
|
5791
|
-
if (!(0,
|
|
6181
|
+
const extDir = (0, import_node_path23.join)(chromeExtDir, extensionId);
|
|
6182
|
+
if (!(0, import_node_fs16.existsSync)(extDir)) continue;
|
|
5792
6183
|
try {
|
|
5793
|
-
const versions = (0,
|
|
6184
|
+
const versions = (0, import_node_fs16.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
5794
6185
|
if (versions.length === 0) continue;
|
|
5795
6186
|
const latestVersion = versions.sort().pop();
|
|
5796
|
-
const extPath = (0,
|
|
6187
|
+
const extPath = (0, import_node_path23.join)(extDir, latestVersion);
|
|
5797
6188
|
let name = extensionId;
|
|
5798
6189
|
let popupPath = "popup.html";
|
|
5799
|
-
const manifestPath = (0,
|
|
5800
|
-
if ((0,
|
|
6190
|
+
const manifestPath = (0, import_node_path23.join)(extPath, "manifest.json");
|
|
6191
|
+
if ((0, import_node_fs16.existsSync)(manifestPath)) {
|
|
5801
6192
|
try {
|
|
5802
|
-
const manifest = JSON.parse((0,
|
|
6193
|
+
const manifest = JSON.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
|
|
5803
6194
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
5804
6195
|
name = manifest.name;
|
|
5805
6196
|
}
|
|
@@ -5819,27 +6210,27 @@ function resolveChromeExtension(extensionId) {
|
|
|
5819
6210
|
}
|
|
5820
6211
|
|
|
5821
6212
|
// src/commands/record-cmd.ts
|
|
5822
|
-
async function recordCommand(url,
|
|
6213
|
+
async function recordCommand(url, options = {}) {
|
|
6214
|
+
const { langOverride, description, skipVoice, fresh, autoYes: autoYes2, rethrow } = options;
|
|
5823
6215
|
if (autoYes2) setAutoYes(true);
|
|
5824
6216
|
console.log();
|
|
5825
6217
|
showLogo();
|
|
5826
|
-
checkCredentials();
|
|
5827
6218
|
const { config, created: configCreated } = await loadConfig(url, langOverride);
|
|
5828
6219
|
let chromeExtension = null;
|
|
5829
6220
|
if (url.startsWith("chrome:")) {
|
|
5830
6221
|
const extensionId = url.slice(7);
|
|
5831
6222
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
5832
6223
|
if (!chromeExtension) {
|
|
5833
|
-
console.log(
|
|
6224
|
+
console.log(import_chalk18.default.red(`
|
|
5834
6225
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
5835
|
-
console.log(
|
|
6226
|
+
console.log(import_chalk18.default.dim(` Make sure the extension is installed in Chrome
|
|
5836
6227
|
`));
|
|
5837
6228
|
process.exit(1);
|
|
5838
6229
|
}
|
|
5839
6230
|
config.browser = { extension: chromeExtension.path };
|
|
5840
6231
|
url = chromeExtension.popupUrl;
|
|
5841
|
-
console.log(
|
|
5842
|
-
console.log(
|
|
6232
|
+
console.log(import_chalk18.default.dim(` Extension: ${chromeExtension.name}`));
|
|
6233
|
+
console.log(import_chalk18.default.dim(` Path: ${chromeExtension.path}`));
|
|
5843
6234
|
}
|
|
5844
6235
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
5845
6236
|
const userData = await loadUserData();
|
|
@@ -5852,20 +6243,20 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
5852
6243
|
if (!authResult) return;
|
|
5853
6244
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
5854
6245
|
if (configCreated) {
|
|
5855
|
-
const configPath = (0,
|
|
6246
|
+
const configPath = (0, import_node_path24.join)(process.cwd(), CONFIG_FILE);
|
|
5856
6247
|
console.log();
|
|
5857
|
-
console.log(
|
|
5858
|
-
console.log(
|
|
6248
|
+
console.log(import_chalk18.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6249
|
+
console.log(import_chalk18.default.dim(` ${configPath}`));
|
|
5859
6250
|
console.log();
|
|
5860
|
-
console.log(
|
|
5861
|
-
console.log(
|
|
5862
|
-
console.log(
|
|
5863
|
-
console.log(
|
|
5864
|
-
console.log(
|
|
6251
|
+
console.log(import_chalk18.default.cyan(" Review the config file to customize:"));
|
|
6252
|
+
console.log(import_chalk18.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
|
|
6253
|
+
console.log(import_chalk18.default.dim(" \u2022 video: Resolution, quality, effects"));
|
|
6254
|
+
console.log(import_chalk18.default.dim(" \u2022 music: Background music path and volume"));
|
|
6255
|
+
console.log(import_chalk18.default.dim(" \u2022 output: Output directory"));
|
|
5865
6256
|
console.log();
|
|
5866
6257
|
const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
|
|
5867
6258
|
if (!proceed) {
|
|
5868
|
-
console.log(
|
|
6259
|
+
console.log(import_chalk18.default.yellow(`
|
|
5869
6260
|
Edit ${CONFIG_FILE} and run again.
|
|
5870
6261
|
`));
|
|
5871
6262
|
process.exit(0);
|
|
@@ -5873,17 +6264,17 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
5873
6264
|
}
|
|
5874
6265
|
if (langOverride) config.lang = langOverride;
|
|
5875
6266
|
if (config.music?.path) {
|
|
5876
|
-
const musicPath = (0,
|
|
5877
|
-
if (!(0,
|
|
6267
|
+
const musicPath = (0, import_node_path24.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path24.resolve)(config.output.dir, config.music.path);
|
|
6268
|
+
if (!(0, import_node_fs17.existsSync)(musicPath)) {
|
|
5878
6269
|
console.log();
|
|
5879
|
-
console.log(
|
|
5880
|
-
console.log(
|
|
6270
|
+
console.log(import_chalk18.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
6271
|
+
console.log(import_chalk18.default.dim(` Config: ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}`));
|
|
5881
6272
|
console.log();
|
|
5882
6273
|
const proceed = await confirm(`${INDENT3}Continue without background music?`);
|
|
5883
6274
|
if (!proceed) {
|
|
5884
|
-
console.log(
|
|
6275
|
+
console.log(import_chalk18.default.yellow(`
|
|
5885
6276
|
Edit music.path in config and run again.`));
|
|
5886
|
-
console.log(
|
|
6277
|
+
console.log(import_chalk18.default.dim(` ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}
|
|
5887
6278
|
`));
|
|
5888
6279
|
process.exit(0);
|
|
5889
6280
|
}
|
|
@@ -5893,10 +6284,10 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
5893
6284
|
const captions = await loadCaptions();
|
|
5894
6285
|
const workDir = await prepareWorkDir();
|
|
5895
6286
|
const targetLang = config.lang || "en";
|
|
5896
|
-
const planPath = (0,
|
|
5897
|
-
const legacyPlanPath = (0,
|
|
6287
|
+
const planPath = (0, import_node_path24.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
6288
|
+
const legacyPlanPath = (0, import_node_path24.join)(config.output.dir, "plan.json");
|
|
5898
6289
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
5899
|
-
await (0,
|
|
6290
|
+
await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
|
|
5900
6291
|
await setLogFilePath(config.output.dir);
|
|
5901
6292
|
try {
|
|
5902
6293
|
const plan = await runPlanPhase({
|
|
@@ -5932,35 +6323,37 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
5932
6323
|
await saveUserData({ localVideoCount: newCount });
|
|
5933
6324
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
5934
6325
|
if (remaining > 0) {
|
|
5935
|
-
console.log(
|
|
6326
|
+
console.log(import_chalk18.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
5936
6327
|
}
|
|
5937
6328
|
}
|
|
5938
6329
|
console.log();
|
|
5939
|
-
console.log(
|
|
6330
|
+
console.log(import_chalk18.default.bold.green(" \u2713 Done!"));
|
|
5940
6331
|
console.log();
|
|
5941
|
-
console.log(
|
|
6332
|
+
console.log(import_chalk18.default.cyan(` ${formatClickablePath(finalPath)}`));
|
|
5942
6333
|
console.log();
|
|
6334
|
+
return finalPath;
|
|
5943
6335
|
} catch (err) {
|
|
5944
6336
|
await logError("Record Command", err);
|
|
6337
|
+
if (rethrow) throw err;
|
|
5945
6338
|
console.log();
|
|
5946
|
-
console.log(
|
|
5947
|
-
console.log(
|
|
6339
|
+
console.log(import_chalk18.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
6340
|
+
console.log(import_chalk18.default.dim(` See ${logFilePath} for details`));
|
|
5948
6341
|
process.exit(1);
|
|
5949
6342
|
}
|
|
5950
6343
|
}
|
|
5951
6344
|
|
|
5952
6345
|
// src/commands/script-cmd.ts
|
|
5953
|
-
var
|
|
5954
|
-
var
|
|
5955
|
-
var
|
|
6346
|
+
var import_promises18 = require("fs/promises");
|
|
6347
|
+
var import_node_path25 = require("path");
|
|
6348
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
5956
6349
|
init_lib();
|
|
5957
6350
|
async function scriptCommand(url, scriptPath, lang) {
|
|
5958
6351
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
5959
6352
|
header("Script");
|
|
5960
|
-
const raw = await (0,
|
|
6353
|
+
const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
|
|
5961
6354
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
5962
6355
|
if (lines.length === 0) {
|
|
5963
|
-
console.log(
|
|
6356
|
+
console.log(import_chalk19.default.red(" Script file is empty or has no non-comment lines"));
|
|
5964
6357
|
process.exit(1);
|
|
5965
6358
|
}
|
|
5966
6359
|
info(`${lines.length} captions from script`);
|
|
@@ -5972,7 +6365,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5972
6365
|
const planTask = task("Mapping script to UI actions");
|
|
5973
6366
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
5974
6367
|
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
5975
|
-
const planPath = (0,
|
|
6368
|
+
const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
5976
6369
|
await savePlan(plan, planPath);
|
|
5977
6370
|
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
5978
6371
|
header("Voiceover");
|
|
@@ -5992,15 +6385,15 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5992
6385
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5993
6386
|
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
|
|
5994
6387
|
console.log();
|
|
5995
|
-
console.log(
|
|
5996
|
-
console.log(
|
|
6388
|
+
console.log(import_chalk19.default.bold.green(" Done!"));
|
|
6389
|
+
console.log(import_chalk19.default.cyan(` ${finalPath}`));
|
|
5997
6390
|
}
|
|
5998
6391
|
|
|
5999
6392
|
// src/qp.ts
|
|
6000
6393
|
init_lib();
|
|
6001
6394
|
import_dotenv.default.config({ path: ".env.local", quiet: true });
|
|
6002
6395
|
import_dotenv.default.config({ quiet: true });
|
|
6003
|
-
function
|
|
6396
|
+
function normalizeUrl3(input) {
|
|
6004
6397
|
if (input.startsWith("chrome-extension://") || input.startsWith("chrome:")) {
|
|
6005
6398
|
return input;
|
|
6006
6399
|
}
|
|
@@ -6032,6 +6425,22 @@ function parseArgs(args) {
|
|
|
6032
6425
|
return { ...base, command: "help" };
|
|
6033
6426
|
}
|
|
6034
6427
|
const first = args[0];
|
|
6428
|
+
if (first === "login") {
|
|
6429
|
+
return { ...base, command: "login" };
|
|
6430
|
+
}
|
|
6431
|
+
if (first === "logout") {
|
|
6432
|
+
return { ...base, command: "logout" };
|
|
6433
|
+
}
|
|
6434
|
+
if (first === "auth") {
|
|
6435
|
+
const sub = args[1];
|
|
6436
|
+
if (sub === "list" || sub === void 0) {
|
|
6437
|
+
return { ...base, command: "auth-list" };
|
|
6438
|
+
}
|
|
6439
|
+
if (sub === "clear") {
|
|
6440
|
+
return { ...base, command: "auth-clear", url: args[2] ? normalizeUrl3(args[2]) : void 0 };
|
|
6441
|
+
}
|
|
6442
|
+
return { ...base, command: "auth", url: normalizeUrl3(sub) };
|
|
6443
|
+
}
|
|
6035
6444
|
if (first === "init") {
|
|
6036
6445
|
return { ...base, command: "init" };
|
|
6037
6446
|
}
|
|
@@ -6055,7 +6464,7 @@ function parseArgs(args) {
|
|
|
6055
6464
|
if (arg2.startsWith("lang=")) {
|
|
6056
6465
|
lang2 = arg2.slice(5);
|
|
6057
6466
|
} else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
|
|
6058
|
-
url2 =
|
|
6467
|
+
url2 = normalizeUrl3(arg2);
|
|
6059
6468
|
}
|
|
6060
6469
|
}
|
|
6061
6470
|
if (!url2) return { ...base, command: "help" };
|
|
@@ -6088,7 +6497,7 @@ function parseArgs(args) {
|
|
|
6088
6497
|
if (arg2.endsWith(".js") || arg2.endsWith(".ts") || arg2.startsWith("./") || arg2.startsWith("/")) {
|
|
6089
6498
|
continue;
|
|
6090
6499
|
}
|
|
6091
|
-
url =
|
|
6500
|
+
url = normalizeUrl3(arg2);
|
|
6092
6501
|
}
|
|
6093
6502
|
}
|
|
6094
6503
|
if (!url) {
|
|
@@ -6104,7 +6513,27 @@ async function main() {
|
|
|
6104
6513
|
console.log(`quickpeek v${VERSION}`);
|
|
6105
6514
|
break;
|
|
6106
6515
|
case "help":
|
|
6107
|
-
showHelp();
|
|
6516
|
+
await showHelp();
|
|
6517
|
+
break;
|
|
6518
|
+
case "login":
|
|
6519
|
+
await loginCommand();
|
|
6520
|
+
break;
|
|
6521
|
+
case "logout":
|
|
6522
|
+
await logoutCommand();
|
|
6523
|
+
break;
|
|
6524
|
+
case "auth":
|
|
6525
|
+
if (!parsed.url) {
|
|
6526
|
+
console.log(import_chalk20.default.red(" Missing URL"));
|
|
6527
|
+
console.log(import_chalk20.default.dim(" Run: qp auth <url>"));
|
|
6528
|
+
process.exit(1);
|
|
6529
|
+
}
|
|
6530
|
+
await authCommand(parsed.url);
|
|
6531
|
+
break;
|
|
6532
|
+
case "auth-clear":
|
|
6533
|
+
await authClearCommand(parsed.url);
|
|
6534
|
+
break;
|
|
6535
|
+
case "auth-list":
|
|
6536
|
+
await authListCommand();
|
|
6108
6537
|
break;
|
|
6109
6538
|
case "init":
|
|
6110
6539
|
await initCommand();
|
|
@@ -6114,8 +6543,8 @@ async function main() {
|
|
|
6114
6543
|
break;
|
|
6115
6544
|
case "record":
|
|
6116
6545
|
if (!parsed.url) {
|
|
6117
|
-
console.log(
|
|
6118
|
-
console.log(
|
|
6546
|
+
console.log(import_chalk20.default.red(" Missing URL"));
|
|
6547
|
+
console.log(import_chalk20.default.dim(" Run: qp --help"));
|
|
6119
6548
|
process.exit(1);
|
|
6120
6549
|
}
|
|
6121
6550
|
setAutoYes(parsed.yes);
|
|
@@ -6124,13 +6553,18 @@ async function main() {
|
|
|
6124
6553
|
showLogo();
|
|
6125
6554
|
await scriptCommand(parsed.url, parsed.scriptPath, parsed.lang);
|
|
6126
6555
|
} else {
|
|
6127
|
-
await recordCommand(parsed.url,
|
|
6556
|
+
await recordCommand(parsed.url, {
|
|
6557
|
+
langOverride: parsed.lang,
|
|
6558
|
+
description: parsed.description,
|
|
6559
|
+
skipVoice: parsed.skipVoice,
|
|
6560
|
+
fresh: parsed.fresh
|
|
6561
|
+
});
|
|
6128
6562
|
}
|
|
6129
6563
|
break;
|
|
6130
6564
|
case "capture":
|
|
6131
6565
|
if (!parsed.url) {
|
|
6132
|
-
console.log(
|
|
6133
|
-
console.log(
|
|
6566
|
+
console.log(import_chalk20.default.red(" Missing URL"));
|
|
6567
|
+
console.log(import_chalk20.default.dim(" Run: qp --help"));
|
|
6134
6568
|
process.exit(1);
|
|
6135
6569
|
}
|
|
6136
6570
|
console.log();
|