@spark-apps/quickpeek 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +4 -0
- package/dist/assets/qp.wm +0 -0
- package/dist/highlight.css +0 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +199 -198
- package/dist/mcp-tools.mjs +5909 -0
- package/dist/qp.js +1316 -1154
- package/dist/{voices-BtYZey4Y.d.mts → scraping-BK8pIJhm.d.mts} +30 -27
- package/dist/web.d.mts +2 -1
- package/dist/web.mjs +129 -176
- package/mcp.mjs +75 -0
- package/package.json +80 -76
package/dist/qp.js
CHANGED
|
@@ -35,14 +35,6 @@ function getLanguageName(lang) {
|
|
|
35
35
|
if (!lang || lang === "en") return null;
|
|
36
36
|
return LANGUAGE_NAMES[lang] || lang.toUpperCase();
|
|
37
37
|
}
|
|
38
|
-
function hexToASS(hex, alpha = 0) {
|
|
39
|
-
const clean = hex.replace("#", "");
|
|
40
|
-
const r = clean.slice(0, 2);
|
|
41
|
-
const g = clean.slice(2, 4);
|
|
42
|
-
const b = clean.slice(4, 6);
|
|
43
|
-
const a = alpha.toString(16).padStart(2, "0").toUpperCase();
|
|
44
|
-
return `&H${a}${b}${g}${r}`.toUpperCase();
|
|
45
|
-
}
|
|
46
38
|
function rgbaToHex(color) {
|
|
47
39
|
if (color.startsWith("#")) return color;
|
|
48
40
|
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
@@ -52,12 +44,6 @@ function rgbaToHex(color) {
|
|
|
52
44
|
const b = parseInt(match[3], 10).toString(16).padStart(2, "0");
|
|
53
45
|
return `#${r}${g}${b}`.toUpperCase();
|
|
54
46
|
}
|
|
55
|
-
function windowsPathToWSL(path) {
|
|
56
|
-
const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
|
|
57
|
-
if (!driveMatch) return null;
|
|
58
|
-
const drive = driveMatch[1].toLowerCase();
|
|
59
|
-
return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
|
|
60
|
-
}
|
|
61
47
|
function parsePartial(partial) {
|
|
62
48
|
if (!partial) return {};
|
|
63
49
|
const parts = partial.split("-");
|
|
@@ -83,10 +69,12 @@ function toTitleCase(text) {
|
|
|
83
69
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
84
70
|
});
|
|
85
71
|
}
|
|
86
|
-
var LANGUAGE_NAMES, LOWERCASE_WORDS;
|
|
72
|
+
var import_video_kit, import_video_kit2, LANGUAGE_NAMES, LOWERCASE_WORDS;
|
|
87
73
|
var init_utils = __esm({
|
|
88
74
|
"src/lib/utils.ts"() {
|
|
89
75
|
"use strict";
|
|
76
|
+
import_video_kit = require("@spark-apps/video-kit");
|
|
77
|
+
import_video_kit2 = require("@spark-apps/video-kit");
|
|
90
78
|
LANGUAGE_NAMES = {
|
|
91
79
|
ar: "Arabic",
|
|
92
80
|
es: "Spanish",
|
|
@@ -137,21 +125,24 @@ var init_utils = __esm({
|
|
|
137
125
|
});
|
|
138
126
|
|
|
139
127
|
// src/lib/api-client.ts
|
|
140
|
-
async function
|
|
128
|
+
async function getOrRefreshDirectToken() {
|
|
141
129
|
const now = Date.now();
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
130
|
+
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
131
|
+
return cachedDirectToken.token;
|
|
144
132
|
}
|
|
145
133
|
try {
|
|
146
134
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
147
135
|
method: "POST",
|
|
148
136
|
headers: { "X-Api-Key": API_KEY }
|
|
149
137
|
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
140
|
+
}
|
|
150
141
|
const data = await response.json();
|
|
151
|
-
if (!
|
|
152
|
-
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`);
|
|
153
144
|
}
|
|
154
|
-
|
|
145
|
+
cachedDirectToken = {
|
|
155
146
|
token: data.token,
|
|
156
147
|
expiresAt: data.expiresAt || now + 36e5
|
|
157
148
|
};
|
|
@@ -160,77 +151,130 @@ async function getOrRefreshToken() {
|
|
|
160
151
|
throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
|
|
161
152
|
}
|
|
162
153
|
}
|
|
163
|
-
async function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
error: "AI service is not configured. Please report this issue.",
|
|
168
|
-
isAuthError: true
|
|
169
|
-
};
|
|
154
|
+
async function getOrRefreshRelayToken(email) {
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
|
|
157
|
+
return cachedRelayToken.token;
|
|
170
158
|
}
|
|
171
159
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 = {
|
|
205
207
|
success: false,
|
|
206
208
|
error,
|
|
207
|
-
isRateLimit:
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
if (!data.success || !data.response) {
|
|
212
|
-
return {
|
|
213
|
-
success: false,
|
|
214
|
-
error: data.error || "No response from Spark AI"
|
|
209
|
+
isRateLimit: true,
|
|
210
|
+
...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
|
|
215
211
|
};
|
|
212
|
+
await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
|
|
213
|
+
continue;
|
|
216
214
|
}
|
|
217
215
|
return {
|
|
218
|
-
success:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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"
|
|
226
226
|
};
|
|
227
|
-
} catch (err) {
|
|
228
|
-
if (attempt === 3) throw err;
|
|
229
|
-
const delayMs = 1e3 * Math.pow(2, attempt - 1);
|
|
230
|
-
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
231
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));
|
|
232
242
|
}
|
|
233
|
-
|
|
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;
|
|
234
278
|
} catch (err) {
|
|
235
279
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
236
280
|
}
|
|
@@ -260,7 +304,7 @@ function normalizeUrl(input) {
|
|
|
260
304
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
261
305
|
return `https://${url}`;
|
|
262
306
|
}
|
|
263
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, API_KEY,
|
|
307
|
+
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, cachedDirectToken, cachedRelayToken;
|
|
264
308
|
var init_api_client = __esm({
|
|
265
309
|
"src/lib/api-client.ts"() {
|
|
266
310
|
"use strict";
|
|
@@ -268,14 +312,21 @@ var init_api_client = __esm({
|
|
|
268
312
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
269
313
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
270
314
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
271
|
-
|
|
272
|
-
|
|
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;
|
|
273
321
|
}
|
|
274
322
|
});
|
|
275
323
|
|
|
276
324
|
// src/lib/SparkStripe-feature-gating.ts
|
|
277
325
|
async function _fetch(email, init) {
|
|
278
|
-
const r = await fetch(
|
|
326
|
+
const r = await fetch(
|
|
327
|
+
`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
|
|
328
|
+
init
|
|
329
|
+
);
|
|
279
330
|
if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
|
|
280
331
|
return r.json();
|
|
281
332
|
}
|
|
@@ -318,24 +369,27 @@ var init_SparkStripe_feature_gating = __esm({
|
|
|
318
369
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
319
370
|
BASE = "https://sparkstripe.com";
|
|
320
371
|
TIERS = {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
372
|
+
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
373
|
+
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
374
|
+
price_1TADm7Ef9opkuWid3I9tquIB: "pro",
|
|
375
|
+
price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
|
|
376
|
+
price_1TCREmEf9opkuWidvhuqzqDM: "pro",
|
|
377
|
+
price_1TCREmEf9opkuWidacf1tiC7: "pro"
|
|
327
378
|
};
|
|
328
379
|
RANK = {
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
]
|
|
331
392
|
};
|
|
332
|
-
FEATURES = { "free": [], "pro": [
|
|
333
|
-
"Everything in Free",
|
|
334
|
-
"No watermark",
|
|
335
|
-
"Full video composition (voiceover + captions + music + transitions)",
|
|
336
|
-
"Unlimited videos",
|
|
337
|
-
"Priority support"
|
|
338
|
-
] };
|
|
339
393
|
getStatus = (email) => _fetch(email);
|
|
340
394
|
_cache = /* @__PURE__ */ new Map();
|
|
341
395
|
isPaid = (d) => getTier(d) !== "free";
|
|
@@ -532,11 +586,17 @@ function getActionForType(type) {
|
|
|
532
586
|
return "type";
|
|
533
587
|
}
|
|
534
588
|
}
|
|
535
|
-
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
|
+
}
|
|
536
595
|
const browserTask = task2("Launching browser");
|
|
537
596
|
const browser = await import_playwright.chromium.launch({ headless: true });
|
|
538
|
-
const
|
|
539
|
-
|
|
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");
|
|
540
600
|
const crawlTask = task2("Crawling page");
|
|
541
601
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
542
602
|
const title = await page.title();
|
|
@@ -669,9 +729,14 @@ async function crawlPage(url, task2 = noopTask) {
|
|
|
669
729
|
});
|
|
670
730
|
await browser.close();
|
|
671
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
|
+
}
|
|
672
737
|
return { title, elements };
|
|
673
738
|
}
|
|
674
|
-
var import_playwright, noopTask;
|
|
739
|
+
var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
675
740
|
var init_crawl = __esm({
|
|
676
741
|
"src/lib/crawl.ts"() {
|
|
677
742
|
"use strict";
|
|
@@ -679,6 +744,9 @@ var init_crawl = __esm({
|
|
|
679
744
|
init_scraping();
|
|
680
745
|
noopTask = () => ({ succeed: () => {
|
|
681
746
|
} });
|
|
747
|
+
CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
748
|
+
CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
749
|
+
crawlCache = /* @__PURE__ */ new Map();
|
|
682
750
|
}
|
|
683
751
|
});
|
|
684
752
|
|
|
@@ -723,10 +791,10 @@ var init_watermark = __esm({
|
|
|
723
791
|
// src/lib/ffmpeg/spawn.ts
|
|
724
792
|
function toWSLPath(path) {
|
|
725
793
|
if (!isWindowsRuntime) return path;
|
|
726
|
-
return
|
|
794
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path.replace(/\\/g, "/");
|
|
727
795
|
}
|
|
728
796
|
function convertArgToWSL(arg2) {
|
|
729
|
-
const wslPath =
|
|
797
|
+
const wslPath = (0, import_video_kit2.windowsDrivePathToWsl)(arg2);
|
|
730
798
|
if (wslPath) return wslPath;
|
|
731
799
|
if (arg2.includes("\\")) return arg2.replace(/\\/g, "/");
|
|
732
800
|
return arg2;
|
|
@@ -823,20 +891,24 @@ function composeVideo(opts) {
|
|
|
823
891
|
`[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
|
|
824
892
|
// Music: fade in and fade out
|
|
825
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]`,
|
|
826
|
-
|
|
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]`
|
|
827
899
|
].join(";");
|
|
828
900
|
args.push("-vf", vfStr);
|
|
829
901
|
args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
|
|
830
902
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
831
903
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
832
904
|
} else if (hasVoiceover) {
|
|
833
|
-
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}`;
|
|
834
906
|
args.push("-vf", vfStr);
|
|
835
907
|
args.push("-map", "0:v", "-map", "1:a", "-af", af);
|
|
836
908
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
837
909
|
args.push("-c:a", "aac", "-b:a", audioBitrate);
|
|
838
910
|
} else if (hasMusic) {
|
|
839
|
-
const musicFilter = `aloop=loop=-1:size=2e+09,atrim=0:${effectiveDuration},volume=${musicVolume},afade=t=in:st=0:d=${musicFadeIn},afade=t=out:st=${musicFadeOutStart}:d=${musicFadeOut}`;
|
|
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}`;
|
|
840
912
|
args.push("-vf", vfStr);
|
|
841
913
|
args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
|
|
842
914
|
args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
|
|
@@ -849,9 +921,11 @@ function composeVideo(opts) {
|
|
|
849
921
|
runFFmpeg(args, "ffmpeg compose failed").then(resolve9, reject);
|
|
850
922
|
});
|
|
851
923
|
}
|
|
924
|
+
var import_video_kit3;
|
|
852
925
|
var init_compose = __esm({
|
|
853
926
|
"src/lib/ffmpeg/compose.ts"() {
|
|
854
927
|
"use strict";
|
|
928
|
+
import_video_kit3 = require("@spark-apps/video-kit");
|
|
855
929
|
init_watermark();
|
|
856
930
|
init_spawn();
|
|
857
931
|
}
|
|
@@ -1108,14 +1182,13 @@ function applyTransitions(opts) {
|
|
|
1108
1182
|
filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
|
|
1109
1183
|
}
|
|
1110
1184
|
let currentV = "v0";
|
|
1185
|
+
let chainDuration = boundaries[1] - boundaries[0];
|
|
1111
1186
|
for (let i = 0; i < sorted.length; i++) {
|
|
1112
1187
|
const trans = sorted[i];
|
|
1113
1188
|
const duration = trans.duration ?? 0.3;
|
|
1114
1189
|
const nextIdx = i + 1;
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
const segDuration = segEnd - segStart;
|
|
1118
|
-
const offset = Math.max(0, segDuration - duration);
|
|
1190
|
+
const offset = Math.max(0, chainDuration - duration);
|
|
1191
|
+
chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
|
|
1119
1192
|
const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
|
|
1120
1193
|
const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
|
|
1121
1194
|
filterParts.push(
|
|
@@ -1316,24 +1389,20 @@ var init_prompts = __esm({
|
|
|
1316
1389
|
|
|
1317
1390
|
// src/lib/styles.ts
|
|
1318
1391
|
function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
|
|
1319
|
-
return
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
Style: Karaoke,${font},${size},${primaryColor},${secondaryColor},${outlineColor},${backColor},${bold ? 1 : 0},0,0,0,100,100,0,0,1,${outlineWidth},${shadow},2,40,40,${marginBottom},1
|
|
1328
|
-
|
|
1329
|
-
[Events]
|
|
1330
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
1331
|
-
`;
|
|
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
|
+
});
|
|
1332
1400
|
}
|
|
1333
|
-
var CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1401
|
+
var import_video_kit4, CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
|
|
1334
1402
|
var init_styles = __esm({
|
|
1335
1403
|
"src/lib/styles.ts"() {
|
|
1336
1404
|
"use strict";
|
|
1405
|
+
import_video_kit4 = require("@spark-apps/video-kit");
|
|
1337
1406
|
init_cursor();
|
|
1338
1407
|
CURSOR_MOVE_DURATION = 800;
|
|
1339
1408
|
CURSOR_CSS = `
|
|
@@ -1416,7 +1485,7 @@ var init_subscription_client = __esm({
|
|
|
1416
1485
|
|
|
1417
1486
|
// src/lib/tts/helpers.ts
|
|
1418
1487
|
function toWSLPathForTTS(path) {
|
|
1419
|
-
return
|
|
1488
|
+
return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path;
|
|
1420
1489
|
}
|
|
1421
1490
|
function estimateActionDuration(step) {
|
|
1422
1491
|
if (step.action === "drag" && step.value === "around") {
|
|
@@ -1446,8 +1515,8 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1446
1515
|
if (!["mp4", "webm", "mov"].includes(ext)) {
|
|
1447
1516
|
return 0;
|
|
1448
1517
|
}
|
|
1449
|
-
const mediaPath = (0,
|
|
1450
|
-
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)) {
|
|
1451
1520
|
return 0;
|
|
1452
1521
|
}
|
|
1453
1522
|
try {
|
|
@@ -1467,38 +1536,38 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1467
1536
|
}
|
|
1468
1537
|
}
|
|
1469
1538
|
async function loadVoiceoverMeta(outputDir) {
|
|
1470
|
-
const metaPath = (0,
|
|
1539
|
+
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1471
1540
|
try {
|
|
1472
|
-
if (!(0,
|
|
1473
|
-
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");
|
|
1474
1543
|
return JSON.parse(content);
|
|
1475
1544
|
} catch {
|
|
1476
1545
|
return null;
|
|
1477
1546
|
}
|
|
1478
1547
|
}
|
|
1479
1548
|
async function saveVoiceoverMeta(outputDir, meta) {
|
|
1480
|
-
const metaPath = (0,
|
|
1481
|
-
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));
|
|
1482
1551
|
}
|
|
1483
1552
|
async function concatenateAudio(inputs, output, workDir) {
|
|
1484
1553
|
const formatPath = (f) => {
|
|
1485
|
-
const resolved = (0,
|
|
1486
|
-
return isWindows ?
|
|
1554
|
+
const resolved = (0, import_node_path2.resolve)(f);
|
|
1555
|
+
return isWindows ? (0, import_video_kit2.windowsDrivePathToWsl)(resolved) ?? resolved : resolved;
|
|
1487
1556
|
};
|
|
1488
1557
|
const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
|
|
1489
|
-
const listPath = (0,
|
|
1490
|
-
await (0,
|
|
1558
|
+
const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
|
|
1559
|
+
await (0, import_promises2.writeFile)(listPath, listContent);
|
|
1491
1560
|
await concatMedia(listPath, output);
|
|
1492
|
-
await (0,
|
|
1561
|
+
await (0, import_promises2.unlink)(listPath).catch(() => {
|
|
1493
1562
|
});
|
|
1494
1563
|
}
|
|
1495
|
-
var
|
|
1564
|
+
var import_node_fs2, import_promises2, import_node_path2, isWindows;
|
|
1496
1565
|
var init_helpers = __esm({
|
|
1497
1566
|
"src/lib/tts/helpers.ts"() {
|
|
1498
1567
|
"use strict";
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1568
|
+
import_node_fs2 = require("fs");
|
|
1569
|
+
import_promises2 = require("fs/promises");
|
|
1570
|
+
import_node_path2 = require("path");
|
|
1502
1571
|
init_ffmpeg2();
|
|
1503
1572
|
init_utils();
|
|
1504
1573
|
isWindows = process.platform === "win32";
|
|
@@ -1534,7 +1603,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1534
1603
|
if (!needsRealignment) {
|
|
1535
1604
|
return stepAudio;
|
|
1536
1605
|
}
|
|
1537
|
-
const audioDir = (0,
|
|
1606
|
+
const audioDir = (0, import_node_path3.join)(workDir, "audio");
|
|
1538
1607
|
const segments = [];
|
|
1539
1608
|
const alignedStepAudio = /* @__PURE__ */ new Map();
|
|
1540
1609
|
for (let i = 0; i < actualRecordingDurations.length; i++) {
|
|
@@ -1543,20 +1612,20 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1543
1612
|
const voiceDuration = voiceData?.duration ?? 0;
|
|
1544
1613
|
const recordingDuration = actualRecordingDurations[i] ?? 0;
|
|
1545
1614
|
const targetDuration = Math.max(voiceDuration, recordingDuration);
|
|
1546
|
-
const audioPath = (0,
|
|
1547
|
-
const originalPaddingPath = (0,
|
|
1548
|
-
const silencePath = (0,
|
|
1549
|
-
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)) {
|
|
1550
1619
|
segments.push(audioPath);
|
|
1551
|
-
if ((0,
|
|
1620
|
+
if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
|
|
1552
1621
|
segments.push(originalPaddingPath);
|
|
1553
1622
|
}
|
|
1554
1623
|
if (recordingDuration > voiceDuration) {
|
|
1555
|
-
const alignPaddingPath = (0,
|
|
1624
|
+
const alignPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
|
|
1556
1625
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1557
1626
|
segments.push(alignPaddingPath);
|
|
1558
1627
|
}
|
|
1559
|
-
} else if ((0,
|
|
1628
|
+
} else if ((0, import_node_fs3.existsSync)(silencePath)) {
|
|
1560
1629
|
if (targetDuration !== voiceDuration) {
|
|
1561
1630
|
await generateSilence(silencePath, targetDuration);
|
|
1562
1631
|
}
|
|
@@ -1568,7 +1637,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1568
1637
|
});
|
|
1569
1638
|
}
|
|
1570
1639
|
if (segments.length > 0) {
|
|
1571
|
-
const voiceoverPath = (0,
|
|
1640
|
+
const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
|
|
1572
1641
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1573
1642
|
}
|
|
1574
1643
|
const existingMeta = await loadVoiceoverMeta(outputDir);
|
|
@@ -1580,12 +1649,12 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1580
1649
|
}
|
|
1581
1650
|
return alignedStepAudio;
|
|
1582
1651
|
}
|
|
1583
|
-
var
|
|
1652
|
+
var import_node_fs3, import_node_path3;
|
|
1584
1653
|
var init_timing = __esm({
|
|
1585
1654
|
"src/lib/tts/timing.ts"() {
|
|
1586
1655
|
"use strict";
|
|
1587
|
-
|
|
1588
|
-
|
|
1656
|
+
import_node_fs3 = require("fs");
|
|
1657
|
+
import_node_path3 = require("path");
|
|
1589
1658
|
init_ffmpeg2();
|
|
1590
1659
|
init_helpers();
|
|
1591
1660
|
}
|
|
@@ -1616,7 +1685,7 @@ function canReuseVoiceover(steps, voice, existingMeta) {
|
|
|
1616
1685
|
}
|
|
1617
1686
|
async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
1618
1687
|
const { estimateWordTimings: estimateWordTimings2 } = await Promise.resolve().then(() => (init_timing(), timing_exports));
|
|
1619
|
-
const voiceoverPath = (0,
|
|
1688
|
+
const voiceoverPath = (0, import_node_path4.join)(outputDir, "voiceover.wav");
|
|
1620
1689
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1621
1690
|
for (let i = 0; i < steps.length; i++) {
|
|
1622
1691
|
const step = steps[i];
|
|
@@ -1629,97 +1698,29 @@ async function loadExistingVoiceover(steps, outputDir, meta, config) {
|
|
|
1629
1698
|
}
|
|
1630
1699
|
return { voiceoverPath, stepAudio };
|
|
1631
1700
|
}
|
|
1632
|
-
var
|
|
1701
|
+
var import_node_path4;
|
|
1633
1702
|
var init_cache = __esm({
|
|
1634
1703
|
"src/lib/tts/cache.ts"() {
|
|
1635
1704
|
"use strict";
|
|
1636
|
-
|
|
1705
|
+
import_node_path4 = require("path");
|
|
1637
1706
|
init_helpers();
|
|
1638
1707
|
}
|
|
1639
1708
|
});
|
|
1640
1709
|
|
|
1641
1710
|
// src/lib/voices.ts
|
|
1642
|
-
var
|
|
1711
|
+
var import_video_kit5;
|
|
1643
1712
|
var init_voices = __esm({
|
|
1644
1713
|
"src/lib/voices.ts"() {
|
|
1645
1714
|
"use strict";
|
|
1646
|
-
|
|
1647
|
-
en: "en-GB-SoniaNeural",
|
|
1648
|
-
ar: "ar-LB-LaylaNeural",
|
|
1649
|
-
es: "es-ES-ElviraNeural",
|
|
1650
|
-
fr: "fr-FR-DeniseNeural",
|
|
1651
|
-
de: "de-DE-KatjaNeural",
|
|
1652
|
-
it: "it-IT-ElsaNeural",
|
|
1653
|
-
pt: "pt-BR-FranciscaNeural",
|
|
1654
|
-
ru: "ru-RU-SvetlanaNeural",
|
|
1655
|
-
zh: "zh-CN-XiaoxiaoNeural",
|
|
1656
|
-
ja: "ja-JP-NanamiNeural",
|
|
1657
|
-
ko: "ko-KR-SunHiNeural",
|
|
1658
|
-
hi: "hi-IN-SwaraNeural",
|
|
1659
|
-
tr: "tr-TR-EmelNeural",
|
|
1660
|
-
nl: "nl-NL-ColetteNeural",
|
|
1661
|
-
pl: "pl-PL-AgnieszkaNeural",
|
|
1662
|
-
sv: "sv-SE-SofieNeural",
|
|
1663
|
-
he: "he-IL-HilaNeural",
|
|
1664
|
-
th: "th-TH-PremwadeeNeural",
|
|
1665
|
-
vi: "vi-VN-HoaiMyNeural",
|
|
1666
|
-
id: "id-ID-GadisNeural",
|
|
1667
|
-
ms: "ms-MY-YasminNeural",
|
|
1668
|
-
fil: "fil-PH-BlessicaNeural",
|
|
1669
|
-
uk: "uk-UA-PolinaNeural",
|
|
1670
|
-
cs: "cs-CZ-VlastaNeural",
|
|
1671
|
-
el: "el-GR-AthinaNeural",
|
|
1672
|
-
ro: "ro-RO-AlinaNeural",
|
|
1673
|
-
hu: "hu-HU-NoemiNeural",
|
|
1674
|
-
da: "da-DK-ChristelNeural",
|
|
1675
|
-
fi: "fi-FI-NooraNeural",
|
|
1676
|
-
no: "nb-NO-PernilleNeural",
|
|
1677
|
-
sk: "sk-SK-ViktoriaNeural",
|
|
1678
|
-
bg: "bg-BG-KalinaNeural",
|
|
1679
|
-
hr: "hr-HR-GabrijelaNeural",
|
|
1680
|
-
sl: "sl-SI-PetraNeural",
|
|
1681
|
-
et: "et-EE-AnuNeural",
|
|
1682
|
-
lv: "lv-LV-EveritaNeural",
|
|
1683
|
-
lt: "lt-LT-OnaNeural",
|
|
1684
|
-
bn: "bn-IN-TanishaaNeural",
|
|
1685
|
-
ta: "ta-IN-PallaviNeural",
|
|
1686
|
-
te: "te-IN-ShrutiNeural",
|
|
1687
|
-
mr: "mr-IN-AarohiNeural",
|
|
1688
|
-
gu: "gu-IN-DhwaniNeural",
|
|
1689
|
-
kn: "kn-IN-SapnaNeural",
|
|
1690
|
-
ml: "ml-IN-SobhanaNeural",
|
|
1691
|
-
ur: "ur-PK-UzmaNeural",
|
|
1692
|
-
fa: "fa-IR-DilaraNeural",
|
|
1693
|
-
sw: "sw-KE-ZuriNeural",
|
|
1694
|
-
af: "af-ZA-AdriNeural",
|
|
1695
|
-
ca: "ca-ES-JoanaNeural",
|
|
1696
|
-
cy: "cy-GB-NiaNeural",
|
|
1697
|
-
ga: "ga-IE-OrlaNeural",
|
|
1698
|
-
mt: "mt-MT-GraceNeural",
|
|
1699
|
-
is: "is-IS-GudrunNeural",
|
|
1700
|
-
mk: "mk-MK-MarijaNeural",
|
|
1701
|
-
sr: "sr-RS-SophieNeural",
|
|
1702
|
-
bs: "bs-BA-VesnaNeural",
|
|
1703
|
-
sq: "sq-AL-AnilaNeural",
|
|
1704
|
-
am: "am-ET-MekdesNeural",
|
|
1705
|
-
az: "az-AZ-BabekNeural",
|
|
1706
|
-
ka: "ka-GE-EkaNeural",
|
|
1707
|
-
kk: "kk-KZ-AigulNeural",
|
|
1708
|
-
mn: "mn-MN-YesUINeural",
|
|
1709
|
-
ne: "ne-NP-HemkalaNeural",
|
|
1710
|
-
ps: "ps-AF-LatifaNeural",
|
|
1711
|
-
si: "si-LK-ThiliniNeural",
|
|
1712
|
-
so: "so-SO-UbaxNeural",
|
|
1713
|
-
zu: "zu-ZA-ThandoNeural"
|
|
1714
|
-
};
|
|
1715
|
+
import_video_kit5 = require("@spark-apps/video-kit");
|
|
1715
1716
|
}
|
|
1716
1717
|
});
|
|
1717
1718
|
|
|
1718
1719
|
// src/lib/tts/synthesize.ts
|
|
1719
1720
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1720
|
-
const tempScriptPath = (0,
|
|
1721
|
-
const wslScriptPath = isWindows ? toWSLPathForTTS((0,
|
|
1722
|
-
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;
|
|
1723
1724
|
const pythonScript = `
|
|
1724
1725
|
import asyncio
|
|
1725
1726
|
from edge_tts import Communicate
|
|
@@ -1734,7 +1735,7 @@ async def main():
|
|
|
1734
1735
|
asyncio.run(main())
|
|
1735
1736
|
`;
|
|
1736
1737
|
try {
|
|
1737
|
-
await (0,
|
|
1738
|
+
await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
|
|
1738
1739
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1739
1740
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1740
1741
|
await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
|
|
@@ -1762,13 +1763,13 @@ asyncio.run(main())
|
|
|
1762
1763
|
}
|
|
1763
1764
|
}
|
|
1764
1765
|
}
|
|
1765
|
-
var import_node_child_process2,
|
|
1766
|
+
var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
|
|
1766
1767
|
var init_synthesize = __esm({
|
|
1767
1768
|
"src/lib/tts/synthesize.ts"() {
|
|
1768
1769
|
"use strict";
|
|
1769
1770
|
import_node_child_process2 = require("child_process");
|
|
1770
|
-
|
|
1771
|
-
|
|
1771
|
+
import_promises3 = require("fs/promises");
|
|
1772
|
+
import_node_path5 = require("path");
|
|
1772
1773
|
import_node_util = require("util");
|
|
1773
1774
|
init_helpers();
|
|
1774
1775
|
execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
|
|
@@ -1782,7 +1783,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
|
1782
1783
|
}
|
|
1783
1784
|
function resolveVoice(config) {
|
|
1784
1785
|
if (config.audio?.voice) return config.audio.voice;
|
|
1785
|
-
if (config.lang) return
|
|
1786
|
+
if (config.lang) return import_video_kit5.WIDE_VOICES[config.lang] || null;
|
|
1786
1787
|
return null;
|
|
1787
1788
|
}
|
|
1788
1789
|
async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
|
|
@@ -1807,8 +1808,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1807
1808
|
info2("No voice configured");
|
|
1808
1809
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1809
1810
|
}
|
|
1810
|
-
const audioDir = (0,
|
|
1811
|
-
await (0,
|
|
1811
|
+
const audioDir = (0, import_node_path6.join)(workDir, "audio");
|
|
1812
|
+
await (0, import_promises4.mkdir)(audioDir, { recursive: true });
|
|
1812
1813
|
const segments = [];
|
|
1813
1814
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1814
1815
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1820,13 +1821,13 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1820
1821
|
const manualDuration = (step.duration ?? 0) * 1e3;
|
|
1821
1822
|
const mediaDuration = await getMediaStepDuration(step, outputDir);
|
|
1822
1823
|
const silenceDuration = Math.max(actionDuration, manualDuration, mediaDuration, config.timing.minStepDuration);
|
|
1823
|
-
const silencePath = (0,
|
|
1824
|
+
const silencePath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1824
1825
|
await generateSilence(silencePath, silenceDuration);
|
|
1825
1826
|
segments.push(silencePath);
|
|
1826
1827
|
stepAudio.set(stepNum, { duration: silenceDuration, words: [] });
|
|
1827
1828
|
continue;
|
|
1828
1829
|
}
|
|
1829
|
-
const audioPath = (0,
|
|
1830
|
+
const audioPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1830
1831
|
if (segments.length > 0) await new Promise((r) => setTimeout(r, 200));
|
|
1831
1832
|
try {
|
|
1832
1833
|
await synthesizeSpeech(step.caption, audioPath, voice);
|
|
@@ -1843,7 +1844,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1843
1844
|
mediaDuration
|
|
1844
1845
|
);
|
|
1845
1846
|
if (stepDuration > audioDuration) {
|
|
1846
|
-
const paddingPath = (0,
|
|
1847
|
+
const paddingPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1847
1848
|
await generateSilence(paddingPath, stepDuration - audioDuration);
|
|
1848
1849
|
segments.push(paddingPath);
|
|
1849
1850
|
}
|
|
@@ -1859,8 +1860,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1859
1860
|
return { voiceoverPath: "", stepAudio };
|
|
1860
1861
|
}
|
|
1861
1862
|
const concatTask = task2("Combining audio");
|
|
1862
|
-
await (0,
|
|
1863
|
-
const voiceoverPath = (0,
|
|
1863
|
+
await (0, import_promises4.mkdir)(outputDir, { recursive: true });
|
|
1864
|
+
const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
|
|
1864
1865
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1865
1866
|
concatTask.succeed("Done");
|
|
1866
1867
|
const meta = {
|
|
@@ -1873,12 +1874,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1873
1874
|
await saveVoiceoverMeta(outputDir, meta);
|
|
1874
1875
|
return { voiceoverPath, stepAudio };
|
|
1875
1876
|
}
|
|
1876
|
-
var
|
|
1877
|
+
var import_promises4, import_node_path6;
|
|
1877
1878
|
var init_generate = __esm({
|
|
1878
1879
|
"src/lib/tts/generate.ts"() {
|
|
1879
1880
|
"use strict";
|
|
1880
|
-
|
|
1881
|
-
|
|
1881
|
+
import_promises4 = require("fs/promises");
|
|
1882
|
+
import_node_path6 = require("path");
|
|
1882
1883
|
init_ffmpeg2();
|
|
1883
1884
|
init_voices();
|
|
1884
1885
|
init_helpers();
|
|
@@ -1933,7 +1934,7 @@ __export(lib_exports, {
|
|
|
1933
1934
|
EXCLUDED_LINK_PATTERNS: () => EXCLUDED_LINK_PATTERNS,
|
|
1934
1935
|
FEATURES: () => FEATURES,
|
|
1935
1936
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1936
|
-
LANGUAGE_VOICES: () =>
|
|
1937
|
+
LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
|
|
1937
1938
|
VERSION: () => VERSION,
|
|
1938
1939
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1939
1940
|
applyTransitions: () => applyTransitions,
|
|
@@ -1943,6 +1944,7 @@ __export(lib_exports, {
|
|
|
1943
1944
|
buildUserPrompt: () => buildUserPrompt,
|
|
1944
1945
|
buildWatermarkFilters: () => buildWatermarkFilters,
|
|
1945
1946
|
callAI: () => callAI,
|
|
1947
|
+
callAIViaRelay: () => callAIViaRelay,
|
|
1946
1948
|
canReuseVoiceover: () => canReuseVoiceover,
|
|
1947
1949
|
composeVideo: () => composeVideo,
|
|
1948
1950
|
concatMedia: () => concatMedia,
|
|
@@ -1961,7 +1963,7 @@ __export(lib_exports, {
|
|
|
1961
1963
|
getUpgradeUrl: () => getUpgradeUrl,
|
|
1962
1964
|
hasAccess: () => hasAccess,
|
|
1963
1965
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1964
|
-
hexToASS: () =>
|
|
1966
|
+
hexToASS: () => import_video_kit.hexToAss,
|
|
1965
1967
|
isCanceling: () => isCanceling,
|
|
1966
1968
|
isExcludedLink: () => isExcludedLink,
|
|
1967
1969
|
isPaid: () => isPaid,
|
|
@@ -1983,7 +1985,7 @@ __export(lib_exports, {
|
|
|
1983
1985
|
spliceVideos: () => spliceVideos,
|
|
1984
1986
|
synthesizeSpeech: () => synthesizeSpeech,
|
|
1985
1987
|
toTitleCase: () => toTitleCase,
|
|
1986
|
-
windowsPathToWSL: () =>
|
|
1988
|
+
windowsPathToWSL: () => import_video_kit2.windowsDrivePathToWsl
|
|
1987
1989
|
});
|
|
1988
1990
|
var init_lib = __esm({
|
|
1989
1991
|
"src/lib/index.ts"() {
|
|
@@ -1995,9 +1997,9 @@ var init_lib = __esm({
|
|
|
1995
1997
|
init_cursor();
|
|
1996
1998
|
init_ffmpeg2();
|
|
1997
1999
|
init_prompts();
|
|
2000
|
+
init_SparkStripe_feature_gating();
|
|
1998
2001
|
init_scraping();
|
|
1999
2002
|
init_styles();
|
|
2000
|
-
init_SparkStripe_feature_gating();
|
|
2001
2003
|
init_subscription_client();
|
|
2002
2004
|
init_tts2();
|
|
2003
2005
|
init_types3();
|
|
@@ -2009,198 +2011,682 @@ var init_lib = __esm({
|
|
|
2009
2011
|
|
|
2010
2012
|
// src/qp.ts
|
|
2011
2013
|
var import_dotenv = __toESM(require("dotenv"));
|
|
2012
|
-
var
|
|
2013
|
-
|
|
2014
|
-
// src/commands/capture-cmd.ts
|
|
2015
|
-
var import_node_path19 = require("path");
|
|
2016
|
-
var import_chalk9 = __toESM(require("chalk"));
|
|
2017
|
-
|
|
2018
|
-
// src/ai/narrate.ts
|
|
2019
|
-
init_lib();
|
|
2014
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
2020
2015
|
|
|
2021
|
-
// src/
|
|
2022
|
-
var
|
|
2023
|
-
var
|
|
2024
|
-
|
|
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");
|
|
2025
2020
|
|
|
2026
|
-
// src/
|
|
2027
|
-
var
|
|
2028
|
-
var
|
|
2029
|
-
var
|
|
2030
|
-
|
|
2031
|
-
var
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
});
|
|
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
|
+
}
|
|
2039
2033
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
let timer;
|
|
2044
|
-
const cleanup = (email) => {
|
|
2045
|
-
clearTimeout(timer);
|
|
2046
|
-
server?.close();
|
|
2047
|
-
resolve9(email);
|
|
2048
|
-
};
|
|
2049
|
-
server = (0, import_node_http.createServer)((req, res) => {
|
|
2050
|
-
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2051
|
-
const path = url.pathname;
|
|
2052
|
-
if (path === "/callback" || path === "/") {
|
|
2053
|
-
const email = url.searchParams.get("email");
|
|
2054
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2055
|
-
res.end(`
|
|
2056
|
-
<!DOCTYPE html>
|
|
2057
|
-
<html>
|
|
2058
|
-
<head><title>QuickPeek</title><style>
|
|
2059
|
-
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
|
|
2060
|
-
.card { text-align: center; padding: 2rem; }
|
|
2061
|
-
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
2062
|
-
p { color: #888; }
|
|
2063
|
-
</style></head>
|
|
2064
|
-
<body><div class="card">
|
|
2065
|
-
<h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
|
|
2066
|
-
<p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
|
|
2067
|
-
<p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
|
|
2068
|
-
</div></body>
|
|
2069
|
-
</html>
|
|
2070
|
-
`);
|
|
2071
|
-
if (email) {
|
|
2072
|
-
cleanup(email);
|
|
2073
|
-
}
|
|
2074
|
-
} else {
|
|
2075
|
-
res.writeHead(404);
|
|
2076
|
-
res.end();
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2080
|
-
const addr = server.address();
|
|
2081
|
-
if (!addr || typeof addr === "string") {
|
|
2082
|
-
cleanup(null);
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
const port = addr.port;
|
|
2086
|
-
const returnUrl = `http://localhost:${port}/callback`;
|
|
2087
|
-
const url = pricingUrl({ returnUrl });
|
|
2088
|
-
console.log();
|
|
2089
|
-
console.log(import_chalk.default.cyan(" Opening browser to sign in..."));
|
|
2090
|
-
console.log(import_chalk.default.dim(` ${url}`));
|
|
2091
|
-
console.log();
|
|
2092
|
-
console.log(import_chalk.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2093
|
-
openBrowser(url);
|
|
2094
|
-
});
|
|
2095
|
-
timer = setTimeout(() => {
|
|
2096
|
-
console.log();
|
|
2097
|
-
console.log(import_chalk.default.yellow(" Sign-in timed out."));
|
|
2098
|
-
cleanup(null);
|
|
2099
|
-
}, TIMEOUT_MS);
|
|
2100
|
-
});
|
|
2034
|
+
function authStatePath(url) {
|
|
2035
|
+
const host = hostOf(url);
|
|
2036
|
+
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
|
|
2101
2037
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
function getLastRateLimitInfo() {
|
|
2106
|
-
return lastRateLimitInfo;
|
|
2038
|
+
function findAuthState(url) {
|
|
2039
|
+
const path = authStatePath(url);
|
|
2040
|
+
return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
|
|
2107
2041
|
}
|
|
2108
|
-
function
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
});
|
|
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;
|
|
2116
2049
|
}
|
|
2117
|
-
async function
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
console.log(import_chalk2.default.white(" and unlock Pro features."));
|
|
2124
|
-
const browserEmail = await browserLogin();
|
|
2125
|
-
if (browserEmail) {
|
|
2126
|
-
console.log();
|
|
2127
|
-
console.log(import_chalk2.default.green(` \u2713 Signed in as ${browserEmail}`));
|
|
2128
|
-
console.log();
|
|
2129
|
-
return browserEmail;
|
|
2130
|
-
}
|
|
2131
|
-
console.log();
|
|
2132
|
-
console.log(import_chalk2.default.dim(" Or enter your email manually:"));
|
|
2133
|
-
const email = await prompt(import_chalk2.default.white(" Email: "));
|
|
2134
|
-
if (!email) {
|
|
2135
|
-
console.log();
|
|
2136
|
-
console.log(import_chalk2.default.red(" No email provided. Exiting."));
|
|
2137
|
-
process.exit(1);
|
|
2138
|
-
}
|
|
2139
|
-
if (!email.includes("@") || !email.includes(".")) {
|
|
2140
|
-
console.log();
|
|
2141
|
-
console.log(import_chalk2.default.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
2142
|
-
const proceed = await prompt(import_chalk2.default.white(" Continue anyway? ") + import_chalk2.default.dim("[y/N] "));
|
|
2143
|
-
if (proceed.toLowerCase() !== "y") {
|
|
2144
|
-
process.exit(1);
|
|
2145
|
-
}
|
|
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)];
|
|
2146
2056
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
return
|
|
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));
|
|
2151
2061
|
}
|
|
2152
|
-
async function
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
await logError2("AI API", new Error(result.error));
|
|
2156
|
-
if (result.isRateLimit) {
|
|
2157
|
-
console.log();
|
|
2158
|
-
console.log(import_chalk2.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2159
|
-
console.log();
|
|
2160
|
-
console.log(import_chalk2.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2161
|
-
console.log(import_chalk2.default.dim(" Options:"));
|
|
2162
|
-
if (result.retryAfterSeconds) {
|
|
2163
|
-
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2164
|
-
console.log(import_chalk2.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2165
|
-
} else {
|
|
2166
|
-
console.log(import_chalk2.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2167
|
-
}
|
|
2168
|
-
console.log(import_chalk2.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2169
|
-
console.log(import_chalk2.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2170
|
-
console.log();
|
|
2171
|
-
process.exit(1);
|
|
2172
|
-
}
|
|
2173
|
-
if (result.isAuthError) {
|
|
2174
|
-
console.log();
|
|
2175
|
-
console.log(import_chalk2.default.red(" \u26A0 Authentication Error"));
|
|
2176
|
-
console.log();
|
|
2177
|
-
console.log(import_chalk2.default.dim(` ${result.error}`));
|
|
2178
|
-
console.log();
|
|
2179
|
-
process.exit(1);
|
|
2180
|
-
}
|
|
2181
|
-
console.log();
|
|
2182
|
-
console.log(import_chalk2.default.red(" \u26A0 AI Error"));
|
|
2183
|
-
console.log();
|
|
2184
|
-
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2185
|
-
console.log(import_chalk2.default.dim(` ${firstLine.slice(0, 80)}`));
|
|
2186
|
-
console.log();
|
|
2187
|
-
process.exit(1);
|
|
2188
|
-
}
|
|
2189
|
-
lastRateLimitInfo = result.limits;
|
|
2190
|
-
return result.content;
|
|
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));
|
|
2191
2065
|
}
|
|
2192
2066
|
|
|
2193
|
-
// src/
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2067
|
+
// src/help.ts
|
|
2068
|
+
var import_chalk = __toESM(require("chalk"));
|
|
2069
|
+
init_lib();
|
|
2070
|
+
|
|
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.
|
|
2204
2690
|
|
|
2205
2691
|
RULES:
|
|
2206
2692
|
- Return ONLY a valid JSON array of strings, no explanation
|
|
@@ -2374,14 +2860,14 @@ function normalizeUrl2(url) {
|
|
|
2374
2860
|
// src/capture.ts
|
|
2375
2861
|
var import_node_child_process4 = require("child_process");
|
|
2376
2862
|
var import_node_http3 = require("http");
|
|
2377
|
-
var
|
|
2378
|
-
var
|
|
2863
|
+
var import_node_readline4 = require("readline");
|
|
2864
|
+
var import_playwright4 = require("playwright");
|
|
2379
2865
|
|
|
2380
2866
|
// src/browser/wsl.ts
|
|
2381
2867
|
var import_node_child_process3 = require("child_process");
|
|
2382
|
-
var
|
|
2868
|
+
var import_node_fs5 = require("fs");
|
|
2383
2869
|
var import_node_http2 = require("http");
|
|
2384
|
-
var
|
|
2870
|
+
var import_playwright3 = require("playwright");
|
|
2385
2871
|
function isWSL() {
|
|
2386
2872
|
if (process.platform !== "linux") return false;
|
|
2387
2873
|
try {
|
|
@@ -2392,7 +2878,7 @@ function isWSL() {
|
|
|
2392
2878
|
}
|
|
2393
2879
|
}
|
|
2394
2880
|
function tryEnableWSLInterop() {
|
|
2395
|
-
return (0,
|
|
2881
|
+
return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2396
2882
|
}
|
|
2397
2883
|
function wslToWindowsPath(wslPath) {
|
|
2398
2884
|
if (!wslPath) return wslPath;
|
|
@@ -2401,7 +2887,7 @@ function wslToWindowsPath(wslPath) {
|
|
|
2401
2887
|
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2402
2888
|
}
|
|
2403
2889
|
function findExe(whereExeQuery, ...hardcoded) {
|
|
2404
|
-
for (const p of hardcoded) if ((0,
|
|
2890
|
+
for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
|
|
2405
2891
|
try {
|
|
2406
2892
|
const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2407
2893
|
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
@@ -2424,7 +2910,7 @@ function getWindowsHostIP() {
|
|
|
2424
2910
|
} catch {
|
|
2425
2911
|
}
|
|
2426
2912
|
try {
|
|
2427
|
-
const m = (0,
|
|
2913
|
+
const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2428
2914
|
if (m?.[1]) return m[1];
|
|
2429
2915
|
} catch {
|
|
2430
2916
|
}
|
|
@@ -2500,7 +2986,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
2500
2986
|
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2501
2987
|
try {
|
|
2502
2988
|
await waitForChrome(windowsHost, proxyPort);
|
|
2503
|
-
const browser = await
|
|
2989
|
+
const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2504
2990
|
return {
|
|
2505
2991
|
browser,
|
|
2506
2992
|
cleanup: async () => {
|
|
@@ -2629,9 +3115,9 @@ var EVENT_TRACKER_SCRIPT = `
|
|
|
2629
3115
|
}, { passive: true, capture: true });
|
|
2630
3116
|
}());
|
|
2631
3117
|
`;
|
|
2632
|
-
function
|
|
3118
|
+
function waitForEnter2() {
|
|
2633
3119
|
return new Promise((resolve9) => {
|
|
2634
|
-
const rl = (0,
|
|
3120
|
+
const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2635
3121
|
process.stdout.write(" Press Enter when done...\n");
|
|
2636
3122
|
rl.once("line", () => {
|
|
2637
3123
|
rl.close();
|
|
@@ -2660,7 +3146,7 @@ async function launchCaptureChrome() {
|
|
|
2660
3146
|
req.end();
|
|
2661
3147
|
});
|
|
2662
3148
|
if (alreadyUp) {
|
|
2663
|
-
const browser = await
|
|
3149
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
|
|
2664
3150
|
return { browser, relay: probe.proc };
|
|
2665
3151
|
}
|
|
2666
3152
|
probe.proc.kill();
|
|
@@ -2680,7 +3166,7 @@ async function launchCaptureChrome() {
|
|
|
2680
3166
|
const relay = await startRelay(nodePath, windowsHost, debugPort);
|
|
2681
3167
|
try {
|
|
2682
3168
|
await waitForChrome(windowsHost, relay.port);
|
|
2683
|
-
const browser = await
|
|
3169
|
+
const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
|
|
2684
3170
|
return { browser, relay: relay.proc };
|
|
2685
3171
|
} catch (err) {
|
|
2686
3172
|
relay.proc.kill();
|
|
@@ -2695,7 +3181,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2695
3181
|
if (isWSL()) {
|
|
2696
3182
|
({ browser, relay } = await launchCaptureChrome());
|
|
2697
3183
|
} else {
|
|
2698
|
-
browser = await
|
|
3184
|
+
browser = await import_playwright4.chromium.launch({
|
|
2699
3185
|
headless: false,
|
|
2700
3186
|
args: ["--start-maximized", "--no-first-run"]
|
|
2701
3187
|
});
|
|
@@ -2737,7 +3223,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
2737
3223
|
return [title, parts.join(" | ")];
|
|
2738
3224
|
}).catch(() => ["", ""]);
|
|
2739
3225
|
startTime = Date.now();
|
|
2740
|
-
await
|
|
3226
|
+
await waitForEnter2();
|
|
2741
3227
|
const durationMs = Date.now() - startTime;
|
|
2742
3228
|
if (relay) {
|
|
2743
3229
|
try {
|
|
@@ -2752,195 +3238,31 @@ async function captureUserSession(url, _workDir) {
|
|
|
2752
3238
|
return { events, durationMs, pageTitle, pageContext };
|
|
2753
3239
|
}
|
|
2754
3240
|
|
|
2755
|
-
// src/utils/config-loader.ts
|
|
2756
|
-
var import_node_fs4 = require("fs");
|
|
2757
|
-
var import_promises4 = require("fs/promises");
|
|
2758
|
-
var import_node_os = require("os");
|
|
2759
|
-
var import_node_path6 = require("path");
|
|
2760
|
-
init_lib();
|
|
2761
|
-
async function loadConfig(_url, langOverride) {
|
|
2762
|
-
const configPath = (0, import_node_path6.join)(process.cwd(), CONFIG_FILE);
|
|
2763
|
-
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2764
|
-
try {
|
|
2765
|
-
const content = await (0, import_promises4.readFile)(configPath, "utf-8");
|
|
2766
|
-
const userConfig = JSON.parse(content);
|
|
2767
|
-
const config = {
|
|
2768
|
-
lang: langOverride || userConfig.lang || "en",
|
|
2769
|
-
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2770
|
-
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2771
|
-
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2772
|
-
music: userConfig.music,
|
|
2773
|
-
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2774
|
-
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2775
|
-
};
|
|
2776
|
-
return { config, created: false };
|
|
2777
|
-
} catch {
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
const fullDefaultConfig = {
|
|
2781
|
-
lang: "en",
|
|
2782
|
-
plan: { maxSteps: 8 },
|
|
2783
|
-
video: {
|
|
2784
|
-
width: 1080,
|
|
2785
|
-
height: 1920,
|
|
2786
|
-
fps: 30,
|
|
2787
|
-
quality: 0.5,
|
|
2788
|
-
preset: "fast",
|
|
2789
|
-
cursor: true,
|
|
2790
|
-
highlight: false,
|
|
2791
|
-
fadeIn: 500,
|
|
2792
|
-
// ms
|
|
2793
|
-
fadeOut: 500,
|
|
2794
|
-
// ms
|
|
2795
|
-
contrast: 0.25
|
|
2796
|
-
},
|
|
2797
|
-
audio: {
|
|
2798
|
-
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2799
|
-
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2800
|
-
speed: 1
|
|
2801
|
-
},
|
|
2802
|
-
music: {
|
|
2803
|
-
path: "",
|
|
2804
|
-
volume: 0.3
|
|
2805
|
-
},
|
|
2806
|
-
output: { dir: "./demo" }
|
|
2807
|
-
};
|
|
2808
|
-
await (0, import_promises4.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2809
|
-
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2810
|
-
}
|
|
2811
|
-
var WORK_DIR = ".quickpeek";
|
|
2812
|
-
async function prepareWorkDir() {
|
|
2813
|
-
const workDir = (0, import_node_path6.join)(process.cwd(), WORK_DIR);
|
|
2814
|
-
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2815
|
-
await (0, import_promises4.rm)(workDir, { recursive: true });
|
|
2816
|
-
}
|
|
2817
|
-
await (0, import_promises4.mkdir)(workDir, { recursive: true });
|
|
2818
|
-
return workDir;
|
|
2819
|
-
}
|
|
2820
|
-
var USER_DATA_DIR = (0, import_node_path6.join)((0, import_node_os.homedir)(), ".quickpeek");
|
|
2821
|
-
var USER_DATA_FILE = (0, import_node_path6.join)(USER_DATA_DIR, "user.json");
|
|
2822
|
-
async function loadUserData() {
|
|
2823
|
-
try {
|
|
2824
|
-
if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
|
|
2825
|
-
return JSON.parse(await (0, import_promises4.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2826
|
-
}
|
|
2827
|
-
} catch {
|
|
2828
|
-
}
|
|
2829
|
-
return {};
|
|
2830
|
-
}
|
|
2831
|
-
async function saveUserData(userData) {
|
|
2832
|
-
if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
|
|
2833
|
-
await (0, import_promises4.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2834
|
-
}
|
|
2835
|
-
const existing = await loadUserData();
|
|
2836
|
-
const merged = { ...existing, ...userData };
|
|
2837
|
-
await (0, import_promises4.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2838
|
-
}
|
|
2839
|
-
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2840
|
-
font-family: Arial, sans-serif;
|
|
2841
|
-
-webkit-text-stroke: 3px #000000;
|
|
2842
|
-
}
|
|
2843
|
-
.caption .regular {
|
|
2844
|
-
font-size: 56px;
|
|
2845
|
-
color: #FFFFFF;
|
|
2846
|
-
}
|
|
2847
|
-
.caption .highlight {
|
|
2848
|
-
font-size: 64px;
|
|
2849
|
-
color: #FFFF00;
|
|
2850
|
-
}
|
|
2851
|
-
`;
|
|
2852
|
-
function parseCaptionCSS(css) {
|
|
2853
|
-
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2854
|
-
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
2855
|
-
const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
|
|
2856
|
-
const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
|
|
2857
|
-
const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
|
|
2858
|
-
if (fontMatch) style.font = fontMatch[1].trim();
|
|
2859
|
-
const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
|
|
2860
|
-
if (strokeMatch) {
|
|
2861
|
-
style.outlineWidth = parseInt(strokeMatch[1], 10);
|
|
2862
|
-
style.outline = rgbaToHex(strokeMatch[2]);
|
|
2863
|
-
}
|
|
2864
|
-
const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
|
|
2865
|
-
if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
|
|
2866
|
-
const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
|
|
2867
|
-
if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
|
|
2868
|
-
const parseWordStyle = (block, target) => {
|
|
2869
|
-
const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2870
|
-
if (colorMatch) target.color = colorMatch[1];
|
|
2871
|
-
const sizeMatch = block.match(/font-size:\s*(\d+)/);
|
|
2872
|
-
if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
|
|
2873
|
-
const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2874
|
-
if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2875
|
-
};
|
|
2876
|
-
parseWordStyle(regularBlock, style.regular);
|
|
2877
|
-
parseWordStyle(highlightBlock, style.highlight);
|
|
2878
|
-
if (!regularBlock && !highlightBlock) {
|
|
2879
|
-
const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
|
|
2880
|
-
if (colorMatch) style.regular.color = colorMatch[1];
|
|
2881
|
-
const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
|
|
2882
|
-
if (sizeMatch) {
|
|
2883
|
-
style.regular.size = parseInt(sizeMatch[1], 10);
|
|
2884
|
-
style.highlight.size = Math.round(style.regular.size * 1.15);
|
|
2885
|
-
}
|
|
2886
|
-
const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
|
|
2887
|
-
if (highlightMatch) style.highlight.color = highlightMatch[1];
|
|
2888
|
-
const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
|
|
2889
|
-
if (weightMatch) {
|
|
2890
|
-
const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
|
|
2891
|
-
style.regular.bold = isBold;
|
|
2892
|
-
style.highlight.bold = isBold;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
return style;
|
|
2896
|
-
}
|
|
2897
|
-
async function loadCaptions() {
|
|
2898
|
-
const captionsPath = (0, import_node_path6.join)(process.cwd(), "captions.css");
|
|
2899
|
-
if (!(0, import_node_fs4.existsSync)(captionsPath)) {
|
|
2900
|
-
await (0, import_promises4.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2901
|
-
return DEFAULT_CAPTIONS;
|
|
2902
|
-
}
|
|
2903
|
-
try {
|
|
2904
|
-
const css = await (0, import_promises4.readFile)(captionsPath, "utf-8");
|
|
2905
|
-
return parseCaptionCSS(css);
|
|
2906
|
-
} catch {
|
|
2907
|
-
return DEFAULT_CAPTIONS;
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
async function savePlan(plan, planPath) {
|
|
2911
|
-
const cleanPlan = {
|
|
2912
|
-
...plan,
|
|
2913
|
-
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2914
|
-
};
|
|
2915
|
-
await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(planPath), { recursive: true });
|
|
2916
|
-
await (0, import_promises4.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
3241
|
// src/utils/logging.ts
|
|
2920
|
-
var
|
|
2921
|
-
var
|
|
3242
|
+
var import_promises6 = require("fs/promises");
|
|
3243
|
+
var import_node_path9 = require("path");
|
|
2922
3244
|
var LOG_FILE_NAME = "quickpeek.log";
|
|
2923
3245
|
var logFileInitialized = false;
|
|
2924
3246
|
var logFilePath = LOG_FILE_NAME;
|
|
2925
3247
|
async function writeLogEntry(entry) {
|
|
2926
3248
|
try {
|
|
2927
3249
|
if (!logFileInitialized) {
|
|
2928
|
-
await (0,
|
|
3250
|
+
await (0, import_promises6.writeFile)(logFilePath, entry);
|
|
2929
3251
|
logFileInitialized = true;
|
|
2930
3252
|
} else {
|
|
2931
|
-
await (0,
|
|
3253
|
+
await (0, import_promises6.appendFile)(logFilePath, entry);
|
|
2932
3254
|
}
|
|
2933
3255
|
} catch {
|
|
2934
3256
|
}
|
|
2935
3257
|
}
|
|
2936
3258
|
async function setLogFilePath(outputDir) {
|
|
2937
|
-
logFilePath = (0,
|
|
3259
|
+
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
2938
3260
|
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2939
3261
|
${"=".repeat(60)}
|
|
2940
3262
|
|
|
2941
3263
|
`;
|
|
2942
3264
|
try {
|
|
2943
|
-
await (0,
|
|
3265
|
+
await (0, import_promises6.writeFile)(logFilePath, header2);
|
|
2944
3266
|
logFileInitialized = true;
|
|
2945
3267
|
} catch {
|
|
2946
3268
|
}
|
|
@@ -2974,95 +3296,36 @@ async function logStep(stepNum, action, details) {
|
|
|
2974
3296
|
`);
|
|
2975
3297
|
}
|
|
2976
3298
|
|
|
2977
|
-
// src/utils/ui.ts
|
|
2978
|
-
var import_node_path8 = require("path");
|
|
2979
|
-
var import_node_readline3 = require("readline");
|
|
2980
|
-
var import_chalk3 = __toESM(require("chalk"));
|
|
2981
|
-
var import_ora = __toESM(require("ora"));
|
|
2982
|
-
var INDENT = " ";
|
|
2983
|
-
var autoYes = false;
|
|
2984
|
-
function setAutoYes(value) {
|
|
2985
|
-
autoYes = value;
|
|
2986
|
-
}
|
|
2987
|
-
function task(text) {
|
|
2988
|
-
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2989
|
-
}
|
|
2990
|
-
function header(text) {
|
|
2991
|
-
console.log();
|
|
2992
|
-
console.log(import_chalk3.default.bold.cyan(`\u25A0 ${text}`));
|
|
2993
|
-
}
|
|
2994
|
-
function info(text) {
|
|
2995
|
-
console.log(`${INDENT}${import_chalk3.default.dim("\u2502")} ${import_chalk3.default.dim(text)}`);
|
|
2996
|
-
}
|
|
2997
|
-
function confirm(prompt2) {
|
|
2998
|
-
if (autoYes) return Promise.resolve(true);
|
|
2999
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3000
|
-
return new Promise((res) => {
|
|
3001
|
-
rl.question(import_chalk3.default.yellow(`${prompt2} [Y/n] `), (answer) => {
|
|
3002
|
-
rl.close();
|
|
3003
|
-
res(answer.toLowerCase() !== "n");
|
|
3004
|
-
});
|
|
3005
|
-
});
|
|
3006
|
-
}
|
|
3007
|
-
function promptInput(prompt2) {
|
|
3008
|
-
if (autoYes) return Promise.resolve("");
|
|
3009
|
-
const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3010
|
-
return new Promise((res) => {
|
|
3011
|
-
rl.question(import_chalk3.default.cyan(`${INDENT}${prompt2}`), (answer) => {
|
|
3012
|
-
rl.close();
|
|
3013
|
-
res(answer.trim());
|
|
3014
|
-
});
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
3018
|
-
function formatClickablePath(filePath) {
|
|
3019
|
-
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
3020
|
-
if (_isWSL) {
|
|
3021
|
-
return absolutePath;
|
|
3022
|
-
}
|
|
3023
|
-
let formatted = absolutePath;
|
|
3024
|
-
formatted = formatted.replace(/\//g, "\\");
|
|
3025
|
-
if (formatted.includes(" ") && !formatted.startsWith('"')) {
|
|
3026
|
-
formatted = `"${formatted}"`;
|
|
3027
|
-
}
|
|
3028
|
-
return formatted;
|
|
3029
|
-
}
|
|
3030
|
-
function formatDuration(ms) {
|
|
3031
|
-
const mins = Math.floor(ms / 6e4);
|
|
3032
|
-
const secs = Math.floor(ms % 6e4 / 1e3);
|
|
3033
|
-
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
3299
|
// src/commands/phases/video-phase.ts
|
|
3037
|
-
var
|
|
3038
|
-
var
|
|
3039
|
-
var
|
|
3300
|
+
var import_node_fs12 = require("fs");
|
|
3301
|
+
var import_promises13 = require("fs/promises");
|
|
3302
|
+
var import_node_path18 = require("path");
|
|
3040
3303
|
init_lib();
|
|
3041
3304
|
|
|
3042
3305
|
// src/record.ts
|
|
3043
|
-
var
|
|
3044
|
-
var
|
|
3045
|
-
var
|
|
3046
|
-
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"));
|
|
3047
3310
|
|
|
3048
3311
|
// src/browser/actions.ts
|
|
3049
|
-
var
|
|
3050
|
-
var
|
|
3051
|
-
var
|
|
3312
|
+
var import_node_fs7 = require("fs");
|
|
3313
|
+
var import_promises7 = require("fs/promises");
|
|
3314
|
+
var import_node_path11 = require("path");
|
|
3052
3315
|
|
|
3053
3316
|
// src/browser/effects.ts
|
|
3054
|
-
var
|
|
3055
|
-
var
|
|
3317
|
+
var import_node_fs6 = require("fs");
|
|
3318
|
+
var import_node_path10 = require("path");
|
|
3056
3319
|
init_lib();
|
|
3057
3320
|
function loadHighlightCSS() {
|
|
3058
3321
|
const candidates = [
|
|
3059
|
-
(0,
|
|
3322
|
+
(0, import_node_path10.join)(__dirname, "highlight.css"),
|
|
3060
3323
|
// dist/ build
|
|
3061
|
-
(0,
|
|
3324
|
+
(0, import_node_path10.join)(__dirname, "..", "highlight.css")
|
|
3062
3325
|
// src/browser/ → src/
|
|
3063
3326
|
];
|
|
3064
3327
|
for (const p of candidates) {
|
|
3065
|
-
if ((0,
|
|
3328
|
+
if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
|
|
3066
3329
|
}
|
|
3067
3330
|
return "";
|
|
3068
3331
|
}
|
|
@@ -3403,8 +3666,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3403
3666
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3404
3667
|
if (!isPlaceholder) {
|
|
3405
3668
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3406
|
-
const filePath = (0,
|
|
3407
|
-
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)) {
|
|
3408
3671
|
await fileInput.setInputFiles(filePath);
|
|
3409
3672
|
await page.waitForTimeout(2e3);
|
|
3410
3673
|
break;
|
|
@@ -3520,8 +3783,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3520
3783
|
case "wait":
|
|
3521
3784
|
break;
|
|
3522
3785
|
case "image": {
|
|
3523
|
-
const imagePath = step.value ? (0,
|
|
3524
|
-
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)) {
|
|
3525
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%;";
|
|
3526
3789
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
3527
3790
|
}
|
|
@@ -3551,7 +3814,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3551
3814
|
await pendingDownload;
|
|
3552
3815
|
pendingDownload = null;
|
|
3553
3816
|
}
|
|
3554
|
-
if (lastDownloadPath && (0,
|
|
3817
|
+
if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
|
|
3555
3818
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
3556
3819
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
3557
3820
|
}
|
|
@@ -3562,7 +3825,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3562
3825
|
async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
3563
3826
|
const { width, height } = config.video;
|
|
3564
3827
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
3565
|
-
const fileBuffer = await (0,
|
|
3828
|
+
const fileBuffer = await (0, import_promises7.readFile)(filePath);
|
|
3566
3829
|
const base64 = fileBuffer.toString("base64");
|
|
3567
3830
|
const mimeTypes = {
|
|
3568
3831
|
png: "image/png",
|
|
@@ -3592,17 +3855,18 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
|
3592
3855
|
}
|
|
3593
3856
|
|
|
3594
3857
|
// src/browser/launch.ts
|
|
3595
|
-
var
|
|
3596
|
-
var
|
|
3597
|
-
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) {
|
|
3598
3861
|
const extensionPath = config.browser?.extension;
|
|
3599
3862
|
const viewport = { width: config.video.width, height: config.video.height };
|
|
3600
3863
|
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
3864
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
3601
3865
|
let context;
|
|
3602
3866
|
let browser = null;
|
|
3603
3867
|
let chromeSession = null;
|
|
3604
3868
|
if (extensionPath) {
|
|
3605
|
-
await (0,
|
|
3869
|
+
await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
|
|
3606
3870
|
const persistentArgs = [
|
|
3607
3871
|
`--disable-extensions-except=${extensionPath}`,
|
|
3608
3872
|
`--load-extension=${extensionPath}`,
|
|
@@ -3616,7 +3880,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3616
3880
|
const contexts = chromeSession.browser.contexts();
|
|
3617
3881
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3618
3882
|
} catch {
|
|
3619
|
-
context = await
|
|
3883
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3620
3884
|
headless: false,
|
|
3621
3885
|
args: persistentArgs,
|
|
3622
3886
|
viewport,
|
|
@@ -3625,7 +3889,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3625
3889
|
});
|
|
3626
3890
|
}
|
|
3627
3891
|
} else {
|
|
3628
|
-
context = await
|
|
3892
|
+
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3629
3893
|
headless: false,
|
|
3630
3894
|
args: persistentArgs,
|
|
3631
3895
|
viewport,
|
|
@@ -3634,8 +3898,8 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
|
|
|
3634
3898
|
});
|
|
3635
3899
|
}
|
|
3636
3900
|
} else {
|
|
3637
|
-
browser = await
|
|
3638
|
-
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 });
|
|
3639
3903
|
}
|
|
3640
3904
|
return { context, browser, chromeSession };
|
|
3641
3905
|
}
|
|
@@ -3652,23 +3916,23 @@ async function cleanupBrowserSession(session) {
|
|
|
3652
3916
|
init_lib();
|
|
3653
3917
|
|
|
3654
3918
|
// src/browser/dry-run.ts
|
|
3655
|
-
var
|
|
3919
|
+
var import_node_path13 = require("path");
|
|
3656
3920
|
init_utils();
|
|
3657
3921
|
|
|
3658
3922
|
// src/browser/step-validator.ts
|
|
3659
|
-
var
|
|
3660
|
-
var
|
|
3923
|
+
var import_node_fs8 = require("fs");
|
|
3924
|
+
var import_node_path12 = require("path");
|
|
3661
3925
|
init_utils();
|
|
3662
3926
|
function findMediaFile(filename, baseDir) {
|
|
3663
|
-
const name = (0,
|
|
3664
|
-
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")];
|
|
3665
3929
|
for (const dir of searchDirs) {
|
|
3666
|
-
if (!(0,
|
|
3930
|
+
if (!(0, import_node_fs8.existsSync)(dir)) continue;
|
|
3667
3931
|
try {
|
|
3668
|
-
const files = (0,
|
|
3669
|
-
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);
|
|
3670
3934
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
3671
|
-
if (match) return (0,
|
|
3935
|
+
if (match) return (0, import_node_path12.join)(dir, match);
|
|
3672
3936
|
} catch {
|
|
3673
3937
|
}
|
|
3674
3938
|
}
|
|
@@ -3740,8 +4004,8 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
3740
4004
|
if (!step.value) {
|
|
3741
4005
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
3742
4006
|
}
|
|
3743
|
-
const specifiedPath = (0,
|
|
3744
|
-
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 };
|
|
3745
4009
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
3746
4010
|
if (foundPath) {
|
|
3747
4011
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3862,8 +4126,8 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
3862
4126
|
const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
|
|
3863
4127
|
if (!isPlaceholder) {
|
|
3864
4128
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3865
|
-
let filePath = (0,
|
|
3866
|
-
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)) {
|
|
3867
4131
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
3868
4132
|
if (foundPath) {
|
|
3869
4133
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -3962,8 +4226,13 @@ async function getPageElements(page) {
|
|
|
3962
4226
|
});
|
|
3963
4227
|
}
|
|
3964
4228
|
async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
|
|
3965
|
-
const userDataDir = (0,
|
|
3966
|
-
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
|
+
);
|
|
3967
4236
|
const page = context.pages()[0] || await context.newPage();
|
|
3968
4237
|
setupDownloadTracking(page);
|
|
3969
4238
|
const results = [];
|
|
@@ -4022,27 +4291,27 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4022
4291
|
}
|
|
4023
4292
|
|
|
4024
4293
|
// src/browser/meta.ts
|
|
4025
|
-
var
|
|
4026
|
-
var
|
|
4027
|
-
var
|
|
4294
|
+
var import_node_fs9 = require("fs");
|
|
4295
|
+
var import_promises9 = require("fs/promises");
|
|
4296
|
+
var import_node_path14 = require("path");
|
|
4028
4297
|
var RECORDING_FILE = "recording.mp4";
|
|
4029
4298
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
4030
4299
|
function getRecordingPath(outputDir) {
|
|
4031
|
-
return (0,
|
|
4300
|
+
return (0, import_node_path14.join)(outputDir, RECORDING_FILE);
|
|
4032
4301
|
}
|
|
4033
4302
|
async function loadRecordingMeta(outputDir) {
|
|
4034
|
-
const metaPath = (0,
|
|
4035
|
-
if (!(0,
|
|
4303
|
+
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4304
|
+
if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
|
|
4036
4305
|
try {
|
|
4037
|
-
const content = await (0,
|
|
4306
|
+
const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
|
|
4038
4307
|
return JSON.parse(content);
|
|
4039
4308
|
} catch {
|
|
4040
4309
|
return null;
|
|
4041
4310
|
}
|
|
4042
4311
|
}
|
|
4043
4312
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4044
|
-
const metaPath = (0,
|
|
4045
|
-
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));
|
|
4046
4315
|
}
|
|
4047
4316
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
4048
4317
|
if (!existingMeta) {
|
|
@@ -4111,16 +4380,17 @@ function formatStepLabel(step) {
|
|
|
4111
4380
|
return `${caption.slice(0, 47)}...`;
|
|
4112
4381
|
}
|
|
4113
4382
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4114
|
-
const videoDir = (0,
|
|
4115
|
-
await (0,
|
|
4383
|
+
const videoDir = (0, import_node_path15.join)(workDir, "video");
|
|
4384
|
+
await (0, import_promises10.mkdir)(videoDir, { recursive: true });
|
|
4116
4385
|
const getStepDuration = (stepNum) => {
|
|
4117
4386
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4118
4387
|
};
|
|
4119
4388
|
const browserTask = task2("Starting browser recording");
|
|
4120
4389
|
const recordingStartTime = Date.now();
|
|
4121
|
-
const userDataDir = (0,
|
|
4390
|
+
const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
|
|
4122
4391
|
const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
|
|
4123
|
-
const
|
|
4392
|
+
const authState = findAuthState(plan.url);
|
|
4393
|
+
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4124
4394
|
const page = context.pages()[0] || await context.newPage();
|
|
4125
4395
|
setupDownloadTracking(page);
|
|
4126
4396
|
context.on("page", (p) => setupDownloadTracking(p));
|
|
@@ -4218,9 +4488,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4218
4488
|
actualStepDurations.push(actualDuration);
|
|
4219
4489
|
}
|
|
4220
4490
|
if (failures.length > 0) {
|
|
4221
|
-
console.log(
|
|
4491
|
+
console.log(import_chalk6.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
|
|
4222
4492
|
for (const f of failures) {
|
|
4223
|
-
console.log(
|
|
4493
|
+
console.log(import_chalk6.default.yellow(` - ${f}`));
|
|
4224
4494
|
}
|
|
4225
4495
|
}
|
|
4226
4496
|
const saveTask = task2("Saving video");
|
|
@@ -4229,7 +4499,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4229
4499
|
await page.close();
|
|
4230
4500
|
const videoPath = await video.path();
|
|
4231
4501
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4232
|
-
if (!videoPath || !(0,
|
|
4502
|
+
if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
|
|
4233
4503
|
throw new Error("Video file was not created");
|
|
4234
4504
|
}
|
|
4235
4505
|
saveTask.succeed("Video saved");
|
|
@@ -4237,33 +4507,18 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4237
4507
|
}
|
|
4238
4508
|
|
|
4239
4509
|
// src/utils/video-pipeline.ts
|
|
4240
|
-
var
|
|
4241
|
-
var
|
|
4242
|
-
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"));
|
|
4243
4515
|
init_lib();
|
|
4244
4516
|
var INDENT2 = " ";
|
|
4245
|
-
function chunkWords(words, maxWords) {
|
|
4246
|
-
const chunks = [];
|
|
4247
|
-
for (let i = 0; i < words.length; i += maxWords) {
|
|
4248
|
-
chunks.push(words.slice(i, i + maxWords));
|
|
4249
|
-
}
|
|
4250
|
-
return chunks;
|
|
4251
|
-
}
|
|
4252
|
-
function formatASSTime(ms) {
|
|
4253
|
-
const hours = Math.floor(ms / 36e5);
|
|
4254
|
-
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
4255
|
-
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
4256
|
-
const centis = Math.floor(ms % 1e3 / 10);
|
|
4257
|
-
return `${hours}:${pad(minutes)}:${pad(seconds)}.${pad(centis)}`;
|
|
4258
|
-
}
|
|
4259
|
-
function pad(n, len = 2) {
|
|
4260
|
-
return n.toString().padStart(len, "0");
|
|
4261
|
-
}
|
|
4262
4517
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
4263
|
-
const regularColor =
|
|
4264
|
-
const highlightColor =
|
|
4265
|
-
const outline =
|
|
4266
|
-
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);
|
|
4267
4522
|
const highlightScale = Math.round(captions.highlight.size / captions.regular.size * 100);
|
|
4268
4523
|
let ass = generateASSHeader(
|
|
4269
4524
|
config.video.width,
|
|
@@ -4288,7 +4543,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4288
4543
|
continue;
|
|
4289
4544
|
}
|
|
4290
4545
|
const words = audio.words.length > 0 ? audio.words : estimateWordTimings(step.caption || "", audio.duration);
|
|
4291
|
-
const chunks =
|
|
4546
|
+
const chunks = (0, import_video_kit6.chunkWordsByCount)(words, 3);
|
|
4292
4547
|
for (const chunk of chunks) {
|
|
4293
4548
|
if (chunk.length === 0) continue;
|
|
4294
4549
|
const chunkStart = currentTime + chunk[0].start;
|
|
@@ -4299,13 +4554,13 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4299
4554
|
const duration = Math.round((word.end - word.start) / 10);
|
|
4300
4555
|
karaokeText += `{\\kf${duration}\\fscx${highlightScale}\\fscy${highlightScale}}${word.word}{\\fscx100\\fscy100} `;
|
|
4301
4556
|
}
|
|
4302
|
-
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()}
|
|
4303
4558
|
`;
|
|
4304
4559
|
}
|
|
4305
4560
|
currentTime += audio.duration;
|
|
4306
4561
|
}
|
|
4307
|
-
const assPath = (0,
|
|
4308
|
-
await (0,
|
|
4562
|
+
const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
|
|
4563
|
+
await (0, import_promises11.writeFile)(assPath, ass);
|
|
4309
4564
|
return assPath;
|
|
4310
4565
|
}
|
|
4311
4566
|
function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
@@ -4318,8 +4573,8 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4318
4573
|
if ((step.action === "video" || step.action === "media") && step.value) {
|
|
4319
4574
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
4320
4575
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
4321
|
-
const mediaPath = (0,
|
|
4322
|
-
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)) {
|
|
4323
4578
|
const partial = parsePartial(step.partial);
|
|
4324
4579
|
overlays.push({
|
|
4325
4580
|
path: mediaPath,
|
|
@@ -4338,23 +4593,23 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4338
4593
|
}
|
|
4339
4594
|
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
4340
4595
|
header("Compose");
|
|
4341
|
-
const configPath = (0,
|
|
4342
|
-
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)}`)}`);
|
|
4343
4598
|
const proceed = await confirm(`${INDENT2}Review config or continue?`);
|
|
4344
4599
|
if (!proceed) {
|
|
4345
|
-
console.log(
|
|
4600
|
+
console.log(import_chalk7.default.yellow(`
|
|
4346
4601
|
Edit ${CONFIG_FILE} and run again.
|
|
4347
4602
|
`));
|
|
4348
4603
|
process.exit(0);
|
|
4349
4604
|
}
|
|
4350
4605
|
const composeTask = task("Building final video");
|
|
4351
|
-
await (0,
|
|
4606
|
+
await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
|
|
4352
4607
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4353
|
-
const finalPath = (0,
|
|
4608
|
+
const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4354
4609
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4355
|
-
const hasVoiceover = audioPath && (0,
|
|
4356
|
-
const musicPath = config.music?.path ? (0,
|
|
4357
|
-
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);
|
|
4358
4613
|
try {
|
|
4359
4614
|
await composeVideo({
|
|
4360
4615
|
videoPath,
|
|
@@ -4391,10 +4646,9 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
4391
4646
|
async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
4392
4647
|
const duration = await getMediaDuration(finalPath);
|
|
4393
4648
|
if (duration <= limitSecs * 1e3) return finalPath;
|
|
4394
|
-
const chalk19 = require("chalk");
|
|
4395
4649
|
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
4396
4650
|
console.log();
|
|
4397
|
-
console.log(
|
|
4651
|
+
console.log(import_chalk7.default.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
4398
4652
|
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
4399
4653
|
if (!speedUp) return finalPath;
|
|
4400
4654
|
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
@@ -4416,152 +4670,23 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
4416
4670
|
}
|
|
4417
4671
|
|
|
4418
4672
|
// src/commands/utils/record-helpers.ts
|
|
4419
|
-
var
|
|
4420
|
-
init_lib();
|
|
4421
|
-
|
|
4422
|
-
// src/help.ts
|
|
4423
|
-
var import_chalk5 = __toESM(require("chalk"));
|
|
4673
|
+
var import_chalk9 = __toESM(require("chalk"));
|
|
4424
4674
|
init_lib();
|
|
4425
4675
|
|
|
4426
|
-
// src/logo.ts
|
|
4427
|
-
var import_figlet = __toESM(require("figlet"));
|
|
4428
|
-
var import_gradient_string = __toESM(require("gradient-string"));
|
|
4429
|
-
var DEFAULT_LOGO_CONFIG = {
|
|
4430
|
-
text: "QuickPeek",
|
|
4431
|
-
font: "Slant",
|
|
4432
|
-
horizontalLayout: "default",
|
|
4433
|
-
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
4434
|
-
suffix: " \u{1F440}"
|
|
4435
|
-
};
|
|
4436
|
-
function renderLogo(config = {}) {
|
|
4437
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
4438
|
-
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
4439
|
-
font: resolved.font,
|
|
4440
|
-
horizontalLayout: resolved.horizontalLayout
|
|
4441
|
-
});
|
|
4442
|
-
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
4443
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
// src/help.ts
|
|
4447
|
-
var cmd = import_chalk5.default.cyan;
|
|
4448
|
-
var arg = import_chalk5.default.green;
|
|
4449
|
-
var opt = import_chalk5.default.magenta;
|
|
4450
|
-
var desc = import_chalk5.default.white;
|
|
4451
|
-
var ex = import_chalk5.default.dim;
|
|
4452
|
-
function pad2(colored, visible, width) {
|
|
4453
|
-
return colored + " ".repeat(Math.max(0, width - visible.length));
|
|
4454
|
-
}
|
|
4455
|
-
function showLogo() {
|
|
4456
|
-
const logo = renderLogo();
|
|
4457
|
-
console.log(logo);
|
|
4458
|
-
console.log(import_chalk5.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
4459
|
-
console.log(import_chalk5.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
4460
|
-
}
|
|
4461
|
-
function formatUsageLines(status, tier) {
|
|
4462
|
-
const lines = [];
|
|
4463
|
-
lines.push(`Videos generated: ${status.usage_count}`);
|
|
4464
|
-
if (tier !== "pro" && status.trial) {
|
|
4465
|
-
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
4466
|
-
const used = max_uses - uses_remaining;
|
|
4467
|
-
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
4468
|
-
if (!is_expired && days_remaining > 0) {
|
|
4469
|
-
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
4470
|
-
}
|
|
4471
|
-
}
|
|
4472
|
-
if (tier === "pro" && status.subscription) {
|
|
4473
|
-
const end = status.subscription.current_period_end;
|
|
4474
|
-
if (status.subscription.is_lifetime) {
|
|
4475
|
-
lines.push("Lifetime access");
|
|
4476
|
-
} else if (isCanceling(status)) {
|
|
4477
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
4478
|
-
lines.push(`Cancels: ${date}`);
|
|
4479
|
-
} else if (end) {
|
|
4480
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
4481
|
-
lines.push(`Renews: ${date}`);
|
|
4482
|
-
}
|
|
4483
|
-
}
|
|
4484
|
-
return lines;
|
|
4485
|
-
}
|
|
4486
|
-
async function showAccount() {
|
|
4487
|
-
const userData = await loadUserData();
|
|
4488
|
-
if (!userData.email) {
|
|
4489
|
-
console.log(import_chalk5.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk5.default.dim(" to sign in."));
|
|
4490
|
-
console.log();
|
|
4491
|
-
return;
|
|
4492
|
-
}
|
|
4493
|
-
const email = userData.email;
|
|
4494
|
-
const tier = userData.tier === "pro" ? "pro" : "free";
|
|
4495
|
-
const tierLabel = tier === "pro" ? import_chalk5.default.green("Pro") : "Free";
|
|
4496
|
-
console.log(` ${tierLabel} plan ${import_chalk5.default.dim("\xB7")} ${import_chalk5.default.dim(email)}`);
|
|
4497
|
-
try {
|
|
4498
|
-
const status = await getStatusCached(email);
|
|
4499
|
-
for (const line of formatUsageLines(status, tier)) {
|
|
4500
|
-
console.log(import_chalk5.default.dim(` ${line}`));
|
|
4501
|
-
}
|
|
4502
|
-
} catch {
|
|
4503
|
-
}
|
|
4504
|
-
console.log();
|
|
4505
|
-
}
|
|
4506
|
-
async function showHelp() {
|
|
4507
|
-
showLogo();
|
|
4508
|
-
await showAccount();
|
|
4509
|
-
const W = 28;
|
|
4510
|
-
const usage = [
|
|
4511
|
-
[`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
|
|
4512
|
-
[`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`, `qp <url> --script <file>`, "Use your own narration script"],
|
|
4513
|
-
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
4514
|
-
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
4515
|
-
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
4516
|
-
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
4517
|
-
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
4518
|
-
];
|
|
4519
|
-
const options = [
|
|
4520
|
-
[`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
|
|
4521
|
-
[`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`, `-d, --description "..."`, "Guide AI with an app description"],
|
|
4522
|
-
[`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`, `-s, --script <file>`, "Narration script (one caption per line)"],
|
|
4523
|
-
[`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
|
|
4524
|
-
[`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
|
|
4525
|
-
[`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
|
|
4526
|
-
];
|
|
4527
|
-
const examples = [
|
|
4528
|
-
`${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
|
|
4529
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
|
|
4530
|
-
`${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
|
|
4531
|
-
`${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
|
|
4532
|
-
];
|
|
4533
|
-
console.log();
|
|
4534
|
-
console.log(import_chalk5.default.yellow.bold("USAGE"));
|
|
4535
|
-
for (const [colored, plain, d] of usage) {
|
|
4536
|
-
console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
|
|
4537
|
-
}
|
|
4538
|
-
console.log();
|
|
4539
|
-
console.log(import_chalk5.default.yellow.bold("OPTIONS"));
|
|
4540
|
-
for (const [colored, plain, d] of options) {
|
|
4541
|
-
console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
|
|
4542
|
-
}
|
|
4543
|
-
console.log();
|
|
4544
|
-
console.log(import_chalk5.default.yellow.bold("EXAMPLES"));
|
|
4545
|
-
for (const e of examples) {
|
|
4546
|
-
console.log(` ${e}`);
|
|
4547
|
-
}
|
|
4548
|
-
console.log();
|
|
4549
|
-
}
|
|
4550
|
-
|
|
4551
4676
|
// src/plan.ts
|
|
4552
|
-
var
|
|
4553
|
-
var
|
|
4554
|
-
var
|
|
4555
|
-
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");
|
|
4556
4681
|
init_lib();
|
|
4557
4682
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
4558
4683
|
const browserTask = task2("Launching browser with extension");
|
|
4559
|
-
const userDataDir = (0,
|
|
4560
|
-
await (0,
|
|
4684
|
+
const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
|
|
4685
|
+
await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
|
|
4561
4686
|
let context;
|
|
4562
4687
|
let chromeSession = null;
|
|
4563
4688
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
4564
|
-
const launchPersistent = () =>
|
|
4689
|
+
const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
|
|
4565
4690
|
headless: false,
|
|
4566
4691
|
args: [
|
|
4567
4692
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -4577,7 +4702,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4577
4702
|
const contexts = chromeSession.browser.contexts();
|
|
4578
4703
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport: { width: 400, height: 600 } });
|
|
4579
4704
|
} catch (_err) {
|
|
4580
|
-
console.log(
|
|
4705
|
+
console.log(import_chalk8.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
|
|
4581
4706
|
context = await launchPersistent();
|
|
4582
4707
|
}
|
|
4583
4708
|
} else {
|
|
@@ -4654,8 +4779,34 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4654
4779
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
4655
4780
|
return { title, elements };
|
|
4656
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
|
+
}
|
|
4657
4808
|
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
|
|
4658
|
-
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));
|
|
4659
4810
|
const aiTask = task2("Generating plan with AI");
|
|
4660
4811
|
const langName = getLanguageName(lang);
|
|
4661
4812
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
@@ -4664,7 +4815,7 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4664
4815
|
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
4665
4816
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
4666
4817
|
if (!jsonMatch) throw new Error("No JSON found in response");
|
|
4667
|
-
const plan =
|
|
4818
|
+
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
4668
4819
|
if (lang !== void 0) plan.lang = lang;
|
|
4669
4820
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
4670
4821
|
...s,
|
|
@@ -4681,35 +4832,8 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4681
4832
|
}
|
|
4682
4833
|
}
|
|
4683
4834
|
async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
|
|
4684
|
-
const content = await (0,
|
|
4685
|
-
|
|
4686
|
-
try {
|
|
4687
|
-
plan = JSON.parse(content);
|
|
4688
|
-
} catch (_parseError) {
|
|
4689
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
4690
|
-
try {
|
|
4691
|
-
const fixed = await callGroq(
|
|
4692
|
-
"You fix malformed JSON. Return ONLY valid JSON, no explanation.",
|
|
4693
|
-
`Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4694
|
-
The JSON should have: url, title, description, steps array.
|
|
4695
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
4696
|
-
|
|
4697
|
-
Malformed content:
|
|
4698
|
-
${content}`,
|
|
4699
|
-
logError2,
|
|
4700
|
-
userEmail,
|
|
4701
|
-
tier
|
|
4702
|
-
);
|
|
4703
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4704
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4705
|
-
plan = JSON.parse(jsonMatch[0]);
|
|
4706
|
-
fixTask.succeed("JSON fixed by AI");
|
|
4707
|
-
} catch (err) {
|
|
4708
|
-
await logError2("JSON Fix", err);
|
|
4709
|
-
fixTask.fail("Could not fix JSON");
|
|
4710
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4711
|
-
}
|
|
4712
|
-
}
|
|
4835
|
+
const content = await (0, import_promises12.readFile)(planPath, "utf-8");
|
|
4836
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
4713
4837
|
if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
|
|
4714
4838
|
throw new Error("Invalid plan: missing url or steps");
|
|
4715
4839
|
}
|
|
@@ -4741,7 +4865,7 @@ ${content}`,
|
|
|
4741
4865
|
renumberTask.fail("No valid steps");
|
|
4742
4866
|
throw new Error("Plan has no valid steps");
|
|
4743
4867
|
}
|
|
4744
|
-
await (0,
|
|
4868
|
+
await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
|
|
4745
4869
|
const removed = originalCount - plan.steps.length;
|
|
4746
4870
|
renumberTask.succeed(
|
|
4747
4871
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -4773,41 +4897,41 @@ function displayRateLimitInfo() {
|
|
|
4773
4897
|
const percentage = Math.round(limits.used / limits.limit * 100);
|
|
4774
4898
|
if (percentage >= 90) {
|
|
4775
4899
|
console.log();
|
|
4776
|
-
console.log(
|
|
4900
|
+
console.log(import_chalk9.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
|
|
4777
4901
|
if (limits.tier === "free") {
|
|
4778
|
-
console.log(
|
|
4902
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${getUpgradeUrl()}`));
|
|
4779
4903
|
}
|
|
4780
4904
|
} else if (percentage >= 50) {
|
|
4781
|
-
console.log(
|
|
4905
|
+
console.log(import_chalk9.default.dim(` ${remaining}/${limits.limit} requests remaining`));
|
|
4782
4906
|
}
|
|
4783
4907
|
}
|
|
4784
4908
|
async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
|
|
4785
4909
|
const tier = userTier === "pro" ? "pro" : "free";
|
|
4786
|
-
const tierLabel = tier === "pro" ?
|
|
4787
|
-
console.log(
|
|
4788
|
-
console.log(
|
|
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}`));
|
|
4789
4913
|
if (userEmail && userEmail !== "anonymous") {
|
|
4790
4914
|
try {
|
|
4791
4915
|
const status = await getStatusCached(userEmail);
|
|
4792
4916
|
for (const line of formatUsageLines(status, tier)) {
|
|
4793
|
-
console.log(
|
|
4917
|
+
console.log(import_chalk9.default.dim(` ${line}`));
|
|
4794
4918
|
}
|
|
4795
4919
|
} catch {
|
|
4796
4920
|
}
|
|
4797
4921
|
}
|
|
4798
4922
|
if (userTier === "free") {
|
|
4799
4923
|
const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
|
|
4800
|
-
console.log(
|
|
4924
|
+
console.log(import_chalk9.default.dim(` Upgrade: ${upgradeUrl}`));
|
|
4801
4925
|
}
|
|
4802
4926
|
}
|
|
4803
4927
|
function displayPlanSummary(plan) {
|
|
4804
4928
|
console.log();
|
|
4805
|
-
console.log(
|
|
4806
|
-
console.log(
|
|
4929
|
+
console.log(import_chalk9.default.dim(` ${plan.title || "Untitled"}`));
|
|
4930
|
+
console.log(import_chalk9.default.dim(` ${plan.steps?.length || 0} steps`));
|
|
4807
4931
|
for (const step of plan.steps || []) {
|
|
4808
4932
|
const text = step.caption || step.action || "";
|
|
4809
4933
|
const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
|
|
4810
|
-
console.log(
|
|
4934
|
+
console.log(import_chalk9.default.dim(` ${step.id}. ${caption}`));
|
|
4811
4935
|
}
|
|
4812
4936
|
}
|
|
4813
4937
|
function getOutputDirFromUrl(url) {
|
|
@@ -4873,8 +4997,8 @@ async function runVideoPhase(params) {
|
|
|
4873
4997
|
if (trimStart2 !== void 0 && trimEnd !== void 0) {
|
|
4874
4998
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
4875
4999
|
} else {
|
|
4876
|
-
const mediaPath = (0,
|
|
4877
|
-
if ((0,
|
|
5000
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5001
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4878
5002
|
try {
|
|
4879
5003
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
4880
5004
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -4885,8 +5009,8 @@ async function runVideoPhase(params) {
|
|
|
4885
5009
|
}
|
|
4886
5010
|
}
|
|
4887
5011
|
} else {
|
|
4888
|
-
const mediaPath = (0,
|
|
4889
|
-
if ((0,
|
|
5012
|
+
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5013
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4890
5014
|
try {
|
|
4891
5015
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
4892
5016
|
} catch {
|
|
@@ -4899,7 +5023,7 @@ async function runVideoPhase(params) {
|
|
|
4899
5023
|
})
|
|
4900
5024
|
);
|
|
4901
5025
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
4902
|
-
const hasExistingRecording = existingRecordingMeta && (0,
|
|
5026
|
+
const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
|
|
4903
5027
|
let videoPath = recordingPath;
|
|
4904
5028
|
const firstStep = plan.steps[0];
|
|
4905
5029
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -4908,7 +5032,7 @@ async function runVideoPhase(params) {
|
|
|
4908
5032
|
header("Record");
|
|
4909
5033
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
4910
5034
|
stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
|
|
4911
|
-
await (0,
|
|
5035
|
+
await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
|
|
4912
5036
|
const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
|
|
4913
5037
|
meta.pageLoadTime = result.pageLoadTime;
|
|
4914
5038
|
await saveRecordingMeta(config.output.dir, meta);
|
|
@@ -4939,7 +5063,7 @@ async function runVideoPhase(params) {
|
|
|
4939
5063
|
if (videoOverlays.length > 0) {
|
|
4940
5064
|
const spliceTask = task("Splicing videos into recording");
|
|
4941
5065
|
try {
|
|
4942
|
-
const splicedPath = (0,
|
|
5066
|
+
const splicedPath = (0, import_node_path18.join)(workDir, "spliced.mp4");
|
|
4943
5067
|
await spliceVideos({
|
|
4944
5068
|
inputPath: videoPath,
|
|
4945
5069
|
outputPath: splicedPath,
|
|
@@ -4959,7 +5083,7 @@ async function runVideoPhase(params) {
|
|
|
4959
5083
|
if (stepTransitions.length > 0) {
|
|
4960
5084
|
const transitionTask = task("Applying transitions");
|
|
4961
5085
|
try {
|
|
4962
|
-
const transitionedPath = (0,
|
|
5086
|
+
const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
|
|
4963
5087
|
const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
|
|
4964
5088
|
await applyTransitions({
|
|
4965
5089
|
inputVideo: videoPath,
|
|
@@ -4980,15 +5104,15 @@ async function runVideoPhase(params) {
|
|
|
4980
5104
|
let mainContentDuration;
|
|
4981
5105
|
if (userTier === "free") {
|
|
4982
5106
|
const scriptDir = getScriptDir();
|
|
4983
|
-
let outroPath = (0,
|
|
4984
|
-
if (!(0,
|
|
4985
|
-
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");
|
|
4986
5110
|
}
|
|
4987
|
-
if ((0,
|
|
5111
|
+
if ((0, import_node_fs12.existsSync)(outroPath)) {
|
|
4988
5112
|
try {
|
|
4989
5113
|
const mainDuration = await getMediaDuration(videoPath);
|
|
4990
5114
|
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
4991
|
-
const combinedPath = (0,
|
|
5115
|
+
const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
|
|
4992
5116
|
await concatVideoWithOutro({
|
|
4993
5117
|
inputPath: videoPath,
|
|
4994
5118
|
outroPath,
|
|
@@ -5035,19 +5159,19 @@ async function runVideoPhase(params) {
|
|
|
5035
5159
|
}
|
|
5036
5160
|
|
|
5037
5161
|
// src/commands/phases/voiceover-phase.ts
|
|
5038
|
-
var
|
|
5039
|
-
var
|
|
5040
|
-
var
|
|
5162
|
+
var import_node_fs13 = require("fs");
|
|
5163
|
+
var import_node_path19 = require("path");
|
|
5164
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
5041
5165
|
init_lib();
|
|
5042
5166
|
var INDENT5 = " ";
|
|
5043
5167
|
async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
5044
5168
|
if (fresh) {
|
|
5045
5169
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5046
5170
|
for (const file of filesToDelete) {
|
|
5047
|
-
const filePath = (0,
|
|
5048
|
-
if ((0,
|
|
5049
|
-
const { unlink:
|
|
5050
|
-
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(() => {
|
|
5051
5175
|
});
|
|
5052
5176
|
}
|
|
5053
5177
|
}
|
|
@@ -5055,8 +5179,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5055
5179
|
}
|
|
5056
5180
|
const voice = resolveVoice(config);
|
|
5057
5181
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5058
|
-
const voiceoverFile = (0,
|
|
5059
|
-
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);
|
|
5060
5184
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5061
5185
|
let voiceoverPath;
|
|
5062
5186
|
let stepAudio;
|
|
@@ -5079,9 +5203,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5079
5203
|
const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
|
|
5080
5204
|
if (reuseCheck.canReuse) {
|
|
5081
5205
|
console.log();
|
|
5082
|
-
console.log(
|
|
5206
|
+
console.log(import_chalk10.default.dim(` Found existing voiceover in ${config.output.dir}`));
|
|
5083
5207
|
if (reuseCheck.reason) {
|
|
5084
|
-
console.log(
|
|
5208
|
+
console.log(import_chalk10.default.dim(` ${reuseCheck.reason}`));
|
|
5085
5209
|
}
|
|
5086
5210
|
const reuse = await confirm(`${INDENT5}Reuse existing voiceover?`);
|
|
5087
5211
|
if (reuse) {
|
|
@@ -5121,7 +5245,7 @@ async function captureCommand(url, lang) {
|
|
|
5121
5245
|
const captureResult = await captureUserSession(url, workDir);
|
|
5122
5246
|
info(`Captured ${captureResult.events.length} interactions over ${(captureResult.durationMs / 1e3).toFixed(0)}s`);
|
|
5123
5247
|
if (captureResult.events.length === 0) {
|
|
5124
|
-
console.log(
|
|
5248
|
+
console.log(import_chalk11.default.yellow(" No interactions captured. Exiting."));
|
|
5125
5249
|
return;
|
|
5126
5250
|
}
|
|
5127
5251
|
header("Convert");
|
|
@@ -5129,7 +5253,7 @@ async function captureCommand(url, lang) {
|
|
|
5129
5253
|
const rawSteps = eventsToSteps(captureResult.events, url);
|
|
5130
5254
|
if (rawSteps.length === 0) {
|
|
5131
5255
|
convertTask.fail("No actionable steps found");
|
|
5132
|
-
console.log(
|
|
5256
|
+
console.log(import_chalk11.default.yellow(" No actionable steps could be derived from the captured events."));
|
|
5133
5257
|
return;
|
|
5134
5258
|
}
|
|
5135
5259
|
convertTask.succeed(`${rawSteps.length} steps extracted`);
|
|
@@ -5161,16 +5285,16 @@ async function captureCommand(url, lang) {
|
|
|
5161
5285
|
...lang !== void 0 ? { lang } : {},
|
|
5162
5286
|
steps
|
|
5163
5287
|
};
|
|
5164
|
-
const planPath = (0,
|
|
5288
|
+
const planPath = (0, import_node_path20.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
5165
5289
|
await savePlan(plan, planPath);
|
|
5166
5290
|
console.log();
|
|
5167
|
-
console.log(
|
|
5168
|
-
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`));
|
|
5169
5293
|
console.log();
|
|
5170
5294
|
const makeVideo = await confirm(" Create polished demo video now?");
|
|
5171
5295
|
if (!makeVideo) {
|
|
5172
5296
|
console.log();
|
|
5173
|
-
console.log(
|
|
5297
|
+
console.log(import_chalk11.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
|
|
5174
5298
|
return;
|
|
5175
5299
|
}
|
|
5176
5300
|
header("Voiceover");
|
|
@@ -5186,20 +5310,20 @@ async function captureCommand(url, lang) {
|
|
|
5186
5310
|
userTier
|
|
5187
5311
|
});
|
|
5188
5312
|
console.log();
|
|
5189
|
-
console.log(
|
|
5190
|
-
console.log(
|
|
5313
|
+
console.log(import_chalk11.default.bold.green(" Done!"));
|
|
5314
|
+
console.log(import_chalk11.default.cyan(` ${finalPath}`));
|
|
5191
5315
|
}
|
|
5192
5316
|
|
|
5193
5317
|
// src/commands/init-cmd.ts
|
|
5194
|
-
var
|
|
5195
|
-
var
|
|
5196
|
-
var
|
|
5197
|
-
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"));
|
|
5198
5322
|
init_lib();
|
|
5199
5323
|
async function initCommand() {
|
|
5200
|
-
const configPath = (0,
|
|
5201
|
-
if ((0,
|
|
5202
|
-
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`));
|
|
5203
5327
|
return;
|
|
5204
5328
|
}
|
|
5205
5329
|
const defaultConfig = {
|
|
@@ -5222,32 +5346,32 @@ async function initCommand() {
|
|
|
5222
5346
|
volume: 0.3
|
|
5223
5347
|
}
|
|
5224
5348
|
};
|
|
5225
|
-
await (0,
|
|
5349
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
5226
5350
|
`);
|
|
5227
|
-
console.log(
|
|
5228
|
-
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>`));
|
|
5229
5353
|
}
|
|
5230
5354
|
async function configCommand(key, value) {
|
|
5231
|
-
const configPath = (0,
|
|
5355
|
+
const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
|
|
5232
5356
|
if (!key) {
|
|
5233
|
-
if (!(0,
|
|
5234
|
-
console.log(
|
|
5357
|
+
if (!(0, import_node_fs14.existsSync)(configPath)) {
|
|
5358
|
+
console.log(import_chalk12.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
5235
5359
|
return;
|
|
5236
5360
|
}
|
|
5237
|
-
const content = await (0,
|
|
5238
|
-
console.log(
|
|
5361
|
+
const content = await (0, import_promises14.readFile)(configPath, "utf-8");
|
|
5362
|
+
console.log(import_chalk12.default.cyan(` ${CONFIG_FILE}:`));
|
|
5239
5363
|
console.log(content);
|
|
5240
5364
|
return;
|
|
5241
5365
|
}
|
|
5242
5366
|
if (!value) {
|
|
5243
|
-
console.log(
|
|
5244
|
-
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>`));
|
|
5245
5369
|
return;
|
|
5246
5370
|
}
|
|
5247
5371
|
let config = {};
|
|
5248
|
-
if ((0,
|
|
5372
|
+
if ((0, import_node_fs14.existsSync)(configPath)) {
|
|
5249
5373
|
try {
|
|
5250
|
-
config = JSON.parse(await (0,
|
|
5374
|
+
config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
|
|
5251
5375
|
} catch {
|
|
5252
5376
|
}
|
|
5253
5377
|
}
|
|
@@ -5269,42 +5393,42 @@ async function configCommand(key, value) {
|
|
|
5269
5393
|
}
|
|
5270
5394
|
obj[keys[keys.length - 1]] = parsedValue;
|
|
5271
5395
|
}
|
|
5272
|
-
await (0,
|
|
5396
|
+
await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
5273
5397
|
`);
|
|
5274
|
-
console.log(
|
|
5398
|
+
console.log(import_chalk12.default.green(` \u2714 Set ${key} = ${value}`));
|
|
5275
5399
|
}
|
|
5276
5400
|
|
|
5277
5401
|
// src/commands/login-cmd.ts
|
|
5278
|
-
var
|
|
5402
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
5279
5403
|
init_lib();
|
|
5280
5404
|
async function loginCommand() {
|
|
5281
5405
|
console.log();
|
|
5282
5406
|
showLogo();
|
|
5283
5407
|
const existing = await loadUserData();
|
|
5284
5408
|
if (existing.email && existing.verified) {
|
|
5285
|
-
console.log(
|
|
5286
|
-
console.log(
|
|
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."));
|
|
5287
5411
|
console.log();
|
|
5288
5412
|
try {
|
|
5289
5413
|
const status = await getStatusCached(existing.email);
|
|
5290
5414
|
const tier2 = await getTierByEmail(existing.email);
|
|
5291
|
-
const tierLabel2 = tier2 === "pro" ?
|
|
5415
|
+
const tierLabel2 = tier2 === "pro" ? import_chalk13.default.green("Pro") : "Free";
|
|
5292
5416
|
console.log(` ${tierLabel2} plan`);
|
|
5293
5417
|
for (const line of formatUsageLines(status, tier2)) {
|
|
5294
|
-
console.log(
|
|
5418
|
+
console.log(import_chalk13.default.dim(` ${line}`));
|
|
5295
5419
|
}
|
|
5296
5420
|
} catch {
|
|
5297
5421
|
}
|
|
5298
5422
|
console.log();
|
|
5299
5423
|
return;
|
|
5300
5424
|
}
|
|
5301
|
-
console.log(
|
|
5302
|
-
console.log(
|
|
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."));
|
|
5303
5427
|
const email = await browserLogin();
|
|
5304
5428
|
if (!email) {
|
|
5305
5429
|
console.log();
|
|
5306
|
-
console.log(
|
|
5307
|
-
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."));
|
|
5308
5432
|
console.log();
|
|
5309
5433
|
return;
|
|
5310
5434
|
}
|
|
@@ -5315,8 +5439,8 @@ async function loginCommand() {
|
|
|
5315
5439
|
}
|
|
5316
5440
|
await saveUserData({ email, verified: true, tier });
|
|
5317
5441
|
console.log();
|
|
5318
|
-
const tierLabel = tier === "pro" ?
|
|
5319
|
-
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}`));
|
|
5320
5444
|
console.log(` ${tierLabel} plan`);
|
|
5321
5445
|
console.log();
|
|
5322
5446
|
}
|
|
@@ -5324,46 +5448,46 @@ async function logoutCommand() {
|
|
|
5324
5448
|
console.log();
|
|
5325
5449
|
const userData = await loadUserData();
|
|
5326
5450
|
if (!userData.email) {
|
|
5327
|
-
console.log(
|
|
5451
|
+
console.log(import_chalk13.default.dim(" Not signed in."));
|
|
5328
5452
|
console.log();
|
|
5329
5453
|
return;
|
|
5330
5454
|
}
|
|
5331
5455
|
const email = userData.email;
|
|
5332
5456
|
await saveUserData({ email: "", verified: false, tier: "free" });
|
|
5333
|
-
console.log(
|
|
5457
|
+
console.log(import_chalk13.default.green(` \u2713 Signed out (${email})`));
|
|
5334
5458
|
console.log();
|
|
5335
5459
|
}
|
|
5336
5460
|
|
|
5337
5461
|
// src/commands/record-cmd.ts
|
|
5338
|
-
var
|
|
5339
|
-
var
|
|
5340
|
-
var
|
|
5341
|
-
var
|
|
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"));
|
|
5342
5466
|
init_lib();
|
|
5343
5467
|
|
|
5344
5468
|
// src/commands/phases/auth-phase.ts
|
|
5345
|
-
var
|
|
5469
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
5346
5470
|
init_lib();
|
|
5347
5471
|
|
|
5348
5472
|
// src/commands/utils/auth-helpers.ts
|
|
5349
|
-
var
|
|
5473
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
5350
5474
|
init_lib();
|
|
5351
5475
|
async function handleRegistrationGate() {
|
|
5352
5476
|
console.log();
|
|
5353
|
-
console.log(
|
|
5354
|
-
console.log(
|
|
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."));
|
|
5355
5479
|
console.log();
|
|
5356
|
-
console.log(
|
|
5357
|
-
console.log(
|
|
5358
|
-
console.log(
|
|
5359
|
-
console.log(
|
|
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"));
|
|
5360
5484
|
let email = await browserLogin();
|
|
5361
5485
|
if (!email) {
|
|
5362
5486
|
console.log();
|
|
5363
5487
|
email = await promptInput("Email: ");
|
|
5364
5488
|
}
|
|
5365
5489
|
if (!email || !email.includes("@")) {
|
|
5366
|
-
console.log(
|
|
5490
|
+
console.log(import_chalk14.default.red(" Invalid email. Run qp again to try again."));
|
|
5367
5491
|
return null;
|
|
5368
5492
|
}
|
|
5369
5493
|
const registerTask = task("Sending verification email");
|
|
@@ -5378,10 +5502,10 @@ async function handleRegistrationGate() {
|
|
|
5378
5502
|
}
|
|
5379
5503
|
registerTask.succeed("Check your email!");
|
|
5380
5504
|
console.log();
|
|
5381
|
-
console.log(
|
|
5382
|
-
console.log(
|
|
5505
|
+
console.log(import_chalk14.default.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
5506
|
+
console.log(import_chalk14.default.white(` ${email}`));
|
|
5383
5507
|
console.log();
|
|
5384
|
-
console.log(
|
|
5508
|
+
console.log(import_chalk14.default.dim(" Click the link in the email, then press Enter to continue..."));
|
|
5385
5509
|
await promptInput("");
|
|
5386
5510
|
const verifyTask = task("Checking verification");
|
|
5387
5511
|
const verified = await isVerified(email);
|
|
@@ -5391,10 +5515,10 @@ async function handleRegistrationGate() {
|
|
|
5391
5515
|
}
|
|
5392
5516
|
verifyTask.fail("Not verified yet");
|
|
5393
5517
|
console.log();
|
|
5394
|
-
console.log(
|
|
5395
|
-
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."));
|
|
5396
5520
|
console.log();
|
|
5397
|
-
console.log(
|
|
5521
|
+
console.log(import_chalk14.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
5398
5522
|
console.log();
|
|
5399
5523
|
return { email, verified: false };
|
|
5400
5524
|
}
|
|
@@ -5422,7 +5546,7 @@ async function runAuthPhase(input) {
|
|
|
5422
5546
|
} else {
|
|
5423
5547
|
verifyTask.fail("Email not verified");
|
|
5424
5548
|
console.log();
|
|
5425
|
-
console.log(
|
|
5549
|
+
console.log(import_chalk15.default.yellow(" Check your inbox for the verification email."));
|
|
5426
5550
|
console.log();
|
|
5427
5551
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
5428
5552
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -5447,8 +5571,8 @@ async function runAuthPhase(input) {
|
|
|
5447
5571
|
registerTask.succeed("Check your email!");
|
|
5448
5572
|
await saveUserData({ email: newEmail, verified: false });
|
|
5449
5573
|
console.log();
|
|
5450
|
-
console.log(
|
|
5451
|
-
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."));
|
|
5452
5576
|
console.log();
|
|
5453
5577
|
gracefulExit();
|
|
5454
5578
|
return null;
|
|
@@ -5464,7 +5588,7 @@ async function runAuthPhase(input) {
|
|
|
5464
5588
|
} else {
|
|
5465
5589
|
recheckTask.fail("Still not verified");
|
|
5466
5590
|
console.log();
|
|
5467
|
-
console.log(
|
|
5591
|
+
console.log(import_chalk15.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
5468
5592
|
console.log();
|
|
5469
5593
|
gracefulExit();
|
|
5470
5594
|
return null;
|
|
@@ -5491,10 +5615,16 @@ async function runAuthPhase(input) {
|
|
|
5491
5615
|
} else {
|
|
5492
5616
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
5493
5617
|
console.log();
|
|
5494
|
-
console.log(
|
|
5495
|
-
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`));
|
|
5496
5620
|
userEmail = await setupUserEmail();
|
|
5497
|
-
|
|
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 });
|
|
5498
5628
|
}
|
|
5499
5629
|
return {
|
|
5500
5630
|
userEmail: userEmail ?? "anonymous",
|
|
@@ -5505,10 +5635,10 @@ async function runAuthPhase(input) {
|
|
|
5505
5635
|
}
|
|
5506
5636
|
|
|
5507
5637
|
// src/commands/phases/plan-phase.ts
|
|
5508
|
-
var
|
|
5509
|
-
var
|
|
5510
|
-
var
|
|
5511
|
-
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"));
|
|
5512
5642
|
|
|
5513
5643
|
// src/ai/fix.ts
|
|
5514
5644
|
init_lib();
|
|
@@ -5665,9 +5795,9 @@ async function runPlanPhase(params) {
|
|
|
5665
5795
|
const extensionPath = config.browser?.extension;
|
|
5666
5796
|
let existingPlan = null;
|
|
5667
5797
|
let existingPlanPath = null;
|
|
5668
|
-
if ((0,
|
|
5798
|
+
if ((0, import_node_fs15.existsSync)(planPath)) {
|
|
5669
5799
|
try {
|
|
5670
|
-
const parsed = JSON.parse(await (0,
|
|
5800
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5671
5801
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5672
5802
|
existingPlan = parsed;
|
|
5673
5803
|
existingPlanPath = planPath;
|
|
@@ -5677,12 +5807,12 @@ async function runPlanPhase(params) {
|
|
|
5677
5807
|
}
|
|
5678
5808
|
if (!existingPlan) {
|
|
5679
5809
|
try {
|
|
5680
|
-
const files = (0,
|
|
5681
|
-
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));
|
|
5682
5812
|
for (const file of planFiles) {
|
|
5683
|
-
const filePath = (0,
|
|
5813
|
+
const filePath = (0, import_node_path22.join)(config.output.dir, file);
|
|
5684
5814
|
try {
|
|
5685
|
-
const parsed = JSON.parse(await (0,
|
|
5815
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
|
|
5686
5816
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5687
5817
|
existingPlan = parsed;
|
|
5688
5818
|
existingPlanPath = filePath;
|
|
@@ -5694,9 +5824,9 @@ async function runPlanPhase(params) {
|
|
|
5694
5824
|
} catch {
|
|
5695
5825
|
}
|
|
5696
5826
|
}
|
|
5697
|
-
if (!existingPlan && (0,
|
|
5827
|
+
if (!existingPlan && (0, import_node_fs15.existsSync)(legacyPlanPath)) {
|
|
5698
5828
|
try {
|
|
5699
|
-
const parsed = JSON.parse(await (0,
|
|
5829
|
+
const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
|
|
5700
5830
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5701
5831
|
existingPlan = parsed;
|
|
5702
5832
|
existingPlanPath = legacyPlanPath;
|
|
@@ -5722,23 +5852,23 @@ async function runPlanPhase(params) {
|
|
|
5722
5852
|
};
|
|
5723
5853
|
if (existingPlan && existingPlanPath) {
|
|
5724
5854
|
console.log();
|
|
5725
|
-
console.log(
|
|
5726
|
-
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}`));
|
|
5727
5857
|
if (!urlMatches) {
|
|
5728
|
-
console.log(
|
|
5729
|
-
console.log(
|
|
5730
|
-
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}`));
|
|
5731
5861
|
}
|
|
5732
5862
|
const existingLang = existingPlan.lang || "en";
|
|
5733
5863
|
if (existingLang !== targetLang) {
|
|
5734
|
-
console.log(
|
|
5864
|
+
console.log(import_chalk16.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
5735
5865
|
}
|
|
5736
5866
|
const reuse = await confirm(`${INDENT6}Use existing plan?`);
|
|
5737
5867
|
if (reuse) {
|
|
5738
5868
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
5739
5869
|
if (existingPlanPath !== planPath) {
|
|
5740
5870
|
await savePlan(plan, planPath);
|
|
5741
|
-
console.log(
|
|
5871
|
+
console.log(import_chalk16.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
5742
5872
|
}
|
|
5743
5873
|
if (existingLang !== targetLang) {
|
|
5744
5874
|
const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
|
|
@@ -5773,7 +5903,7 @@ async function runPlanPhase(params) {
|
|
|
5773
5903
|
uploadIdx++;
|
|
5774
5904
|
}
|
|
5775
5905
|
}
|
|
5776
|
-
console.log(
|
|
5906
|
+
console.log(import_chalk16.default.dim(` Preserved upload paths from previous plan`));
|
|
5777
5907
|
}
|
|
5778
5908
|
await savePlan(plan, planPath);
|
|
5779
5909
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -5797,8 +5927,8 @@ async function runPlanPhase(params) {
|
|
|
5797
5927
|
}
|
|
5798
5928
|
displayPlanSummary(plan);
|
|
5799
5929
|
console.log();
|
|
5800
|
-
console.log(
|
|
5801
|
-
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.`));
|
|
5802
5932
|
const MAX_REFINEMENTS = 10;
|
|
5803
5933
|
let refinementCount = 0;
|
|
5804
5934
|
while (true) {
|
|
@@ -5816,15 +5946,15 @@ async function runPlanPhase(params) {
|
|
|
5816
5946
|
refineTask.succeed("Plan refined");
|
|
5817
5947
|
displayRateLimitInfo();
|
|
5818
5948
|
if (plan.steps.length > maxSteps) {
|
|
5819
|
-
console.log(
|
|
5949
|
+
console.log(import_chalk16.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
5820
5950
|
}
|
|
5821
5951
|
displayPlanSummary(plan);
|
|
5822
5952
|
console.log();
|
|
5823
|
-
console.log(
|
|
5953
|
+
console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
5824
5954
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
5825
5955
|
console.log();
|
|
5826
|
-
console.log(
|
|
5827
|
-
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.`));
|
|
5828
5958
|
const continueRefining = await confirm(`${INDENT6}Continue refining?`);
|
|
5829
5959
|
if (!continueRefining) break;
|
|
5830
5960
|
}
|
|
@@ -5836,12 +5966,12 @@ async function runPlanPhase(params) {
|
|
|
5836
5966
|
const planLang = plan.lang || "en";
|
|
5837
5967
|
if (planLang !== targetLang) {
|
|
5838
5968
|
let useExistingTranslation = false;
|
|
5839
|
-
if ((0,
|
|
5969
|
+
if ((0, import_node_fs15.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
5840
5970
|
try {
|
|
5841
|
-
const translatedPlan = JSON.parse(await (0,
|
|
5971
|
+
const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
|
|
5842
5972
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
5843
5973
|
console.log();
|
|
5844
|
-
console.log(
|
|
5974
|
+
console.log(import_chalk16.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
5845
5975
|
const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
|
|
5846
5976
|
if (useTranslated) {
|
|
5847
5977
|
plan = translatedPlan;
|
|
@@ -5853,7 +5983,7 @@ async function runPlanPhase(params) {
|
|
|
5853
5983
|
}
|
|
5854
5984
|
if (!useExistingTranslation) {
|
|
5855
5985
|
console.log();
|
|
5856
|
-
console.log(
|
|
5986
|
+
console.log(import_chalk16.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
5857
5987
|
const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
|
|
5858
5988
|
if (translate) await doTranslate("Translation failed, using original");
|
|
5859
5989
|
}
|
|
@@ -5862,8 +5992,8 @@ async function runPlanPhase(params) {
|
|
|
5862
5992
|
}
|
|
5863
5993
|
|
|
5864
5994
|
// src/commands/phases/validation-phase.ts
|
|
5865
|
-
var
|
|
5866
|
-
var
|
|
5995
|
+
var import_promises16 = require("fs/promises");
|
|
5996
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
5867
5997
|
async function runValidationPhase(params) {
|
|
5868
5998
|
let { plan } = params;
|
|
5869
5999
|
const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
|
|
@@ -5914,21 +6044,21 @@ async function runValidationPhase(params) {
|
|
|
5914
6044
|
const stepNum = result.stepIndex + 1;
|
|
5915
6045
|
const step = plan.steps[result.stepIndex];
|
|
5916
6046
|
console.log();
|
|
5917
|
-
console.log(
|
|
6047
|
+
console.log(import_chalk17.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
5918
6048
|
const caption = step.caption || "";
|
|
5919
|
-
if (caption) console.log(
|
|
5920
|
-
console.log(
|
|
5921
|
-
if (step.value) console.log(
|
|
5922
|
-
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}`));
|
|
5923
6053
|
if (result.suggestion) {
|
|
5924
|
-
console.log(
|
|
6054
|
+
console.log(import_chalk17.default.yellow(` Hint: ${result.suggestion}`));
|
|
5925
6055
|
}
|
|
5926
6056
|
console.log();
|
|
5927
6057
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
5928
6058
|
if (isUploadMissingFile) {
|
|
5929
6059
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
5930
6060
|
if (filePath.toLowerCase() === "skip") {
|
|
5931
|
-
console.log(
|
|
6061
|
+
console.log(import_chalk17.default.yellow(` Skipping step ${stepNum}`));
|
|
5932
6062
|
continue;
|
|
5933
6063
|
}
|
|
5934
6064
|
if (filePath) {
|
|
@@ -5937,13 +6067,13 @@ async function runValidationPhase(params) {
|
|
|
5937
6067
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5938
6068
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
5939
6069
|
if (newResult2?.success) {
|
|
5940
|
-
console.log(
|
|
6070
|
+
console.log(import_chalk17.default.green(` \u2713 Step ${stepNum} now passes`));
|
|
5941
6071
|
validationResult = revalidation2;
|
|
5942
6072
|
continue;
|
|
5943
6073
|
}
|
|
5944
6074
|
result.error = newResult2?.error;
|
|
5945
6075
|
result.suggestion = newResult2?.suggestion;
|
|
5946
|
-
console.log(
|
|
6076
|
+
console.log(import_chalk17.default.red(` Still failing: ${result.error}`));
|
|
5947
6077
|
}
|
|
5948
6078
|
}
|
|
5949
6079
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -5965,8 +6095,8 @@ async function runValidationPhase(params) {
|
|
|
5965
6095
|
await savePlan(plan, planPath);
|
|
5966
6096
|
fixTask.succeed("AI suggested a fix");
|
|
5967
6097
|
const fixedStep = plan.steps[result.stepIndex];
|
|
5968
|
-
console.log(
|
|
5969
|
-
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}`));
|
|
5970
6100
|
} catch {
|
|
5971
6101
|
fixTask.fail("AI could not fix automatically");
|
|
5972
6102
|
}
|
|
@@ -5982,14 +6112,14 @@ async function runValidationPhase(params) {
|
|
|
5982
6112
|
result.error = newResult?.error;
|
|
5983
6113
|
result.suggestion = newResult?.suggestion;
|
|
5984
6114
|
console.log();
|
|
5985
|
-
console.log(
|
|
6115
|
+
console.log(import_chalk17.default.yellow(` Edit step ${stepNum} in: `) + import_chalk17.default.cyan(formatClickablePath(planPath)));
|
|
5986
6116
|
console.log();
|
|
5987
6117
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
5988
6118
|
if (choice.toLowerCase() === "skip") {
|
|
5989
|
-
console.log(
|
|
6119
|
+
console.log(import_chalk17.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
5990
6120
|
continue;
|
|
5991
6121
|
}
|
|
5992
|
-
plan = JSON.parse(await (0,
|
|
6122
|
+
plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
5993
6123
|
const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
|
|
5994
6124
|
const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
5995
6125
|
const finalResult = finalRevalidation.results[result.stepIndex];
|
|
@@ -6013,31 +6143,31 @@ async function runValidationPhase(params) {
|
|
|
6013
6143
|
}
|
|
6014
6144
|
|
|
6015
6145
|
// src/commands/utils/chrome-extension.ts
|
|
6016
|
-
var
|
|
6017
|
-
var
|
|
6018
|
-
var
|
|
6146
|
+
var import_node_fs16 = require("fs");
|
|
6147
|
+
var import_node_os3 = require("os");
|
|
6148
|
+
var import_node_path23 = require("path");
|
|
6019
6149
|
function resolveChromeExtension(extensionId) {
|
|
6020
6150
|
const platform = process.platform;
|
|
6021
6151
|
const possibleDirs = [];
|
|
6022
6152
|
const getChromeExtDirs = (userDataPath) => {
|
|
6023
6153
|
const dirs = [];
|
|
6024
6154
|
try {
|
|
6025
|
-
const profiles = (0,
|
|
6155
|
+
const profiles = (0, import_node_fs16.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
6026
6156
|
for (const profile of profiles) {
|
|
6027
|
-
dirs.push((0,
|
|
6157
|
+
dirs.push((0, import_node_path23.join)(userDataPath, profile, "Extensions"));
|
|
6028
6158
|
}
|
|
6029
6159
|
} catch {
|
|
6030
|
-
dirs.push((0,
|
|
6160
|
+
dirs.push((0, import_node_path23.join)(userDataPath, "Default", "Extensions"));
|
|
6031
6161
|
}
|
|
6032
6162
|
return dirs;
|
|
6033
6163
|
};
|
|
6034
6164
|
if (platform === "win32") {
|
|
6035
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6165
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
6036
6166
|
} else if (platform === "darwin") {
|
|
6037
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6167
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
6038
6168
|
} else {
|
|
6039
6169
|
try {
|
|
6040
|
-
const windowsUsers = (0,
|
|
6170
|
+
const windowsUsers = (0, import_node_fs16.readdirSync)("/mnt/c/Users").filter(
|
|
6041
6171
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
6042
6172
|
);
|
|
6043
6173
|
for (const user of windowsUsers) {
|
|
@@ -6045,22 +6175,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
6045
6175
|
}
|
|
6046
6176
|
} catch {
|
|
6047
6177
|
}
|
|
6048
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6178
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
|
|
6049
6179
|
}
|
|
6050
6180
|
for (const chromeExtDir of possibleDirs) {
|
|
6051
|
-
const extDir = (0,
|
|
6052
|
-
if (!(0,
|
|
6181
|
+
const extDir = (0, import_node_path23.join)(chromeExtDir, extensionId);
|
|
6182
|
+
if (!(0, import_node_fs16.existsSync)(extDir)) continue;
|
|
6053
6183
|
try {
|
|
6054
|
-
const versions = (0,
|
|
6184
|
+
const versions = (0, import_node_fs16.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
6055
6185
|
if (versions.length === 0) continue;
|
|
6056
6186
|
const latestVersion = versions.sort().pop();
|
|
6057
|
-
const extPath = (0,
|
|
6187
|
+
const extPath = (0, import_node_path23.join)(extDir, latestVersion);
|
|
6058
6188
|
let name = extensionId;
|
|
6059
6189
|
let popupPath = "popup.html";
|
|
6060
|
-
const manifestPath = (0,
|
|
6061
|
-
if ((0,
|
|
6190
|
+
const manifestPath = (0, import_node_path23.join)(extPath, "manifest.json");
|
|
6191
|
+
if ((0, import_node_fs16.existsSync)(manifestPath)) {
|
|
6062
6192
|
try {
|
|
6063
|
-
const manifest = JSON.parse((0,
|
|
6193
|
+
const manifest = JSON.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
|
|
6064
6194
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
6065
6195
|
name = manifest.name;
|
|
6066
6196
|
}
|
|
@@ -6080,7 +6210,8 @@ function resolveChromeExtension(extensionId) {
|
|
|
6080
6210
|
}
|
|
6081
6211
|
|
|
6082
6212
|
// src/commands/record-cmd.ts
|
|
6083
|
-
async function recordCommand(url,
|
|
6213
|
+
async function recordCommand(url, options = {}) {
|
|
6214
|
+
const { langOverride, description, skipVoice, fresh, autoYes: autoYes2, rethrow } = options;
|
|
6084
6215
|
if (autoYes2) setAutoYes(true);
|
|
6085
6216
|
console.log();
|
|
6086
6217
|
showLogo();
|
|
@@ -6090,16 +6221,16 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6090
6221
|
const extensionId = url.slice(7);
|
|
6091
6222
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
6092
6223
|
if (!chromeExtension) {
|
|
6093
|
-
console.log(
|
|
6224
|
+
console.log(import_chalk18.default.red(`
|
|
6094
6225
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
6095
|
-
console.log(
|
|
6226
|
+
console.log(import_chalk18.default.dim(` Make sure the extension is installed in Chrome
|
|
6096
6227
|
`));
|
|
6097
6228
|
process.exit(1);
|
|
6098
6229
|
}
|
|
6099
6230
|
config.browser = { extension: chromeExtension.path };
|
|
6100
6231
|
url = chromeExtension.popupUrl;
|
|
6101
|
-
console.log(
|
|
6102
|
-
console.log(
|
|
6232
|
+
console.log(import_chalk18.default.dim(` Extension: ${chromeExtension.name}`));
|
|
6233
|
+
console.log(import_chalk18.default.dim(` Path: ${chromeExtension.path}`));
|
|
6103
6234
|
}
|
|
6104
6235
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
6105
6236
|
const userData = await loadUserData();
|
|
@@ -6112,20 +6243,20 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6112
6243
|
if (!authResult) return;
|
|
6113
6244
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
6114
6245
|
if (configCreated) {
|
|
6115
|
-
const configPath = (0,
|
|
6246
|
+
const configPath = (0, import_node_path24.join)(process.cwd(), CONFIG_FILE);
|
|
6116
6247
|
console.log();
|
|
6117
|
-
console.log(
|
|
6118
|
-
console.log(
|
|
6248
|
+
console.log(import_chalk18.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6249
|
+
console.log(import_chalk18.default.dim(` ${configPath}`));
|
|
6119
6250
|
console.log();
|
|
6120
|
-
console.log(
|
|
6121
|
-
console.log(
|
|
6122
|
-
console.log(
|
|
6123
|
-
console.log(
|
|
6124
|
-
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"));
|
|
6125
6256
|
console.log();
|
|
6126
6257
|
const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
|
|
6127
6258
|
if (!proceed) {
|
|
6128
|
-
console.log(
|
|
6259
|
+
console.log(import_chalk18.default.yellow(`
|
|
6129
6260
|
Edit ${CONFIG_FILE} and run again.
|
|
6130
6261
|
`));
|
|
6131
6262
|
process.exit(0);
|
|
@@ -6133,17 +6264,17 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6133
6264
|
}
|
|
6134
6265
|
if (langOverride) config.lang = langOverride;
|
|
6135
6266
|
if (config.music?.path) {
|
|
6136
|
-
const musicPath = (0,
|
|
6137
|
-
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)) {
|
|
6138
6269
|
console.log();
|
|
6139
|
-
console.log(
|
|
6140
|
-
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)}`));
|
|
6141
6272
|
console.log();
|
|
6142
6273
|
const proceed = await confirm(`${INDENT3}Continue without background music?`);
|
|
6143
6274
|
if (!proceed) {
|
|
6144
|
-
console.log(
|
|
6275
|
+
console.log(import_chalk18.default.yellow(`
|
|
6145
6276
|
Edit music.path in config and run again.`));
|
|
6146
|
-
console.log(
|
|
6277
|
+
console.log(import_chalk18.default.dim(` ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}
|
|
6147
6278
|
`));
|
|
6148
6279
|
process.exit(0);
|
|
6149
6280
|
}
|
|
@@ -6153,10 +6284,10 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6153
6284
|
const captions = await loadCaptions();
|
|
6154
6285
|
const workDir = await prepareWorkDir();
|
|
6155
6286
|
const targetLang = config.lang || "en";
|
|
6156
|
-
const planPath = (0,
|
|
6157
|
-
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");
|
|
6158
6289
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
6159
|
-
await (0,
|
|
6290
|
+
await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
|
|
6160
6291
|
await setLogFilePath(config.output.dir);
|
|
6161
6292
|
try {
|
|
6162
6293
|
const plan = await runPlanPhase({
|
|
@@ -6192,35 +6323,37 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
|
|
|
6192
6323
|
await saveUserData({ localVideoCount: newCount });
|
|
6193
6324
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
6194
6325
|
if (remaining > 0) {
|
|
6195
|
-
console.log(
|
|
6326
|
+
console.log(import_chalk18.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
6196
6327
|
}
|
|
6197
6328
|
}
|
|
6198
6329
|
console.log();
|
|
6199
|
-
console.log(
|
|
6330
|
+
console.log(import_chalk18.default.bold.green(" \u2713 Done!"));
|
|
6200
6331
|
console.log();
|
|
6201
|
-
console.log(
|
|
6332
|
+
console.log(import_chalk18.default.cyan(` ${formatClickablePath(finalPath)}`));
|
|
6202
6333
|
console.log();
|
|
6334
|
+
return finalPath;
|
|
6203
6335
|
} catch (err) {
|
|
6204
6336
|
await logError("Record Command", err);
|
|
6337
|
+
if (rethrow) throw err;
|
|
6205
6338
|
console.log();
|
|
6206
|
-
console.log(
|
|
6207
|
-
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`));
|
|
6208
6341
|
process.exit(1);
|
|
6209
6342
|
}
|
|
6210
6343
|
}
|
|
6211
6344
|
|
|
6212
6345
|
// src/commands/script-cmd.ts
|
|
6213
|
-
var
|
|
6214
|
-
var
|
|
6215
|
-
var
|
|
6346
|
+
var import_promises18 = require("fs/promises");
|
|
6347
|
+
var import_node_path25 = require("path");
|
|
6348
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
6216
6349
|
init_lib();
|
|
6217
6350
|
async function scriptCommand(url, scriptPath, lang) {
|
|
6218
6351
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
6219
6352
|
header("Script");
|
|
6220
|
-
const raw = await (0,
|
|
6353
|
+
const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
|
|
6221
6354
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6222
6355
|
if (lines.length === 0) {
|
|
6223
|
-
console.log(
|
|
6356
|
+
console.log(import_chalk19.default.red(" Script file is empty or has no non-comment lines"));
|
|
6224
6357
|
process.exit(1);
|
|
6225
6358
|
}
|
|
6226
6359
|
info(`${lines.length} captions from script`);
|
|
@@ -6232,7 +6365,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6232
6365
|
const planTask = task("Mapping script to UI actions");
|
|
6233
6366
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
6234
6367
|
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
6235
|
-
const planPath = (0,
|
|
6368
|
+
const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
6236
6369
|
await savePlan(plan, planPath);
|
|
6237
6370
|
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
6238
6371
|
header("Voiceover");
|
|
@@ -6252,8 +6385,8 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6252
6385
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
6253
6386
|
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
|
|
6254
6387
|
console.log();
|
|
6255
|
-
console.log(
|
|
6256
|
-
console.log(
|
|
6388
|
+
console.log(import_chalk19.default.bold.green(" Done!"));
|
|
6389
|
+
console.log(import_chalk19.default.cyan(` ${finalPath}`));
|
|
6257
6390
|
}
|
|
6258
6391
|
|
|
6259
6392
|
// src/qp.ts
|
|
@@ -6298,6 +6431,16 @@ function parseArgs(args) {
|
|
|
6298
6431
|
if (first === "logout") {
|
|
6299
6432
|
return { ...base, command: "logout" };
|
|
6300
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
|
+
}
|
|
6301
6444
|
if (first === "init") {
|
|
6302
6445
|
return { ...base, command: "init" };
|
|
6303
6446
|
}
|
|
@@ -6378,6 +6521,20 @@ async function main() {
|
|
|
6378
6521
|
case "logout":
|
|
6379
6522
|
await logoutCommand();
|
|
6380
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();
|
|
6537
|
+
break;
|
|
6381
6538
|
case "init":
|
|
6382
6539
|
await initCommand();
|
|
6383
6540
|
break;
|
|
@@ -6386,8 +6543,8 @@ async function main() {
|
|
|
6386
6543
|
break;
|
|
6387
6544
|
case "record":
|
|
6388
6545
|
if (!parsed.url) {
|
|
6389
|
-
console.log(
|
|
6390
|
-
console.log(
|
|
6546
|
+
console.log(import_chalk20.default.red(" Missing URL"));
|
|
6547
|
+
console.log(import_chalk20.default.dim(" Run: qp --help"));
|
|
6391
6548
|
process.exit(1);
|
|
6392
6549
|
}
|
|
6393
6550
|
setAutoYes(parsed.yes);
|
|
@@ -6396,13 +6553,18 @@ async function main() {
|
|
|
6396
6553
|
showLogo();
|
|
6397
6554
|
await scriptCommand(parsed.url, parsed.scriptPath, parsed.lang);
|
|
6398
6555
|
} else {
|
|
6399
|
-
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
|
+
});
|
|
6400
6562
|
}
|
|
6401
6563
|
break;
|
|
6402
6564
|
case "capture":
|
|
6403
6565
|
if (!parsed.url) {
|
|
6404
|
-
console.log(
|
|
6405
|
-
console.log(
|
|
6566
|
+
console.log(import_chalk20.default.red(" Missing URL"));
|
|
6567
|
+
console.log(import_chalk20.default.dim(" Run: qp --help"));
|
|
6406
6568
|
process.exit(1);
|
|
6407
6569
|
}
|
|
6408
6570
|
console.log();
|