mcp-scraper 0.1.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.
Files changed (38) hide show
  1. package/README.md +56 -0
  2. package/dist/bin/api-server.cjs +9256 -0
  3. package/dist/bin/api-server.cjs.map +1 -0
  4. package/dist/bin/api-server.d.cts +1 -0
  5. package/dist/bin/api-server.d.ts +1 -0
  6. package/dist/bin/api-server.js +38 -0
  7. package/dist/bin/api-server.js.map +1 -0
  8. package/dist/bin/mcp-stdio-server.cjs +840 -0
  9. package/dist/bin/mcp-stdio-server.cjs.map +1 -0
  10. package/dist/bin/mcp-stdio-server.d.cts +1 -0
  11. package/dist/bin/mcp-stdio-server.d.ts +1 -0
  12. package/dist/bin/mcp-stdio-server.js +41 -0
  13. package/dist/bin/mcp-stdio-server.js.map +1 -0
  14. package/dist/bin/paa-harvest.cjs +1438 -0
  15. package/dist/bin/paa-harvest.cjs.map +1 -0
  16. package/dist/bin/paa-harvest.d.cts +1 -0
  17. package/dist/bin/paa-harvest.d.ts +1 -0
  18. package/dist/bin/paa-harvest.js +37 -0
  19. package/dist/bin/paa-harvest.js.map +1 -0
  20. package/dist/chunk-4API3ZCT.js +1387 -0
  21. package/dist/chunk-4API3ZCT.js.map +1 -0
  22. package/dist/chunk-LXZDJJXR.js +476 -0
  23. package/dist/chunk-LXZDJJXR.js.map +1 -0
  24. package/dist/chunk-ZBP4RHNW.js +805 -0
  25. package/dist/chunk-ZBP4RHNW.js.map +1 -0
  26. package/dist/db-IOYMX64U.js +87 -0
  27. package/dist/db-IOYMX64U.js.map +1 -0
  28. package/dist/index.cjs +1689 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +210 -0
  31. package/dist/index.d.ts +210 -0
  32. package/dist/index.js +275 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/server-63DR2HE5.js +6062 -0
  35. package/dist/server-63DR2HE5.js.map +1 -0
  36. package/dist/worker-3ECJHPRE.js +88 -0
  37. package/dist/worker-3ECJHPRE.js.map +1 -0
  38. package/package.json +76 -0
@@ -0,0 +1,210 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const HarvestOptionsSchema: z.ZodObject<{
4
+ query: z.ZodString;
5
+ location: z.ZodOptional<z.ZodString>;
6
+ gl: z.ZodDefault<z.ZodString>;
7
+ hl: z.ZodDefault<z.ZodString>;
8
+ depth: z.ZodDefault<z.ZodNumber>;
9
+ maxQuestions: z.ZodDefault<z.ZodNumber>;
10
+ headless: z.ZodDefault<z.ZodBoolean>;
11
+ profileDir: z.ZodOptional<z.ZodString>;
12
+ proxy: z.ZodOptional<z.ZodString>;
13
+ kernelApiKey: z.ZodOptional<z.ZodString>;
14
+ kernelProxyId: z.ZodOptional<z.ZodString>;
15
+ outputDir: z.ZodDefault<z.ZodString>;
16
+ format: z.ZodDefault<z.ZodEnum<["json", "csv", "both"]>>;
17
+ serpOnly: z.ZodDefault<z.ZodBoolean>;
18
+ pages: z.ZodDefault<z.ZodNumber>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ query: string;
21
+ gl: string;
22
+ hl: string;
23
+ depth: number;
24
+ maxQuestions: number;
25
+ headless: boolean;
26
+ outputDir: string;
27
+ format: "json" | "csv" | "both";
28
+ serpOnly: boolean;
29
+ pages: number;
30
+ location?: string | undefined;
31
+ profileDir?: string | undefined;
32
+ proxy?: string | undefined;
33
+ kernelApiKey?: string | undefined;
34
+ kernelProxyId?: string | undefined;
35
+ }, {
36
+ query: string;
37
+ location?: string | undefined;
38
+ gl?: string | undefined;
39
+ hl?: string | undefined;
40
+ depth?: number | undefined;
41
+ maxQuestions?: number | undefined;
42
+ headless?: boolean | undefined;
43
+ profileDir?: string | undefined;
44
+ proxy?: string | undefined;
45
+ kernelApiKey?: string | undefined;
46
+ kernelProxyId?: string | undefined;
47
+ outputDir?: string | undefined;
48
+ format?: "json" | "csv" | "both" | undefined;
49
+ serpOnly?: boolean | undefined;
50
+ pages?: number | undefined;
51
+ }>;
52
+
53
+ interface HarvestStats {
54
+ seed: string;
55
+ totalQuestions: number;
56
+ maxDepthReached: number;
57
+ durationMs: number;
58
+ errorCount: number;
59
+ }
60
+ interface PAANode {
61
+ question: string;
62
+ answer: string | null;
63
+ sourceTitle: string | null;
64
+ sourceSite: string | null;
65
+ sourceCite: string | null;
66
+ depth: number;
67
+ parentQuestion: string | null;
68
+ children: PAANode[];
69
+ }
70
+ interface FlatRow {
71
+ seed_query: string;
72
+ question: string;
73
+ answer: string;
74
+ source_title: string;
75
+ source_site: string;
76
+ source_cite: string;
77
+ depth: number;
78
+ parent_question: string;
79
+ extracted_at: string;
80
+ }
81
+ interface VideoResult {
82
+ type: 'video' | 'short_video';
83
+ title: string;
84
+ channel: string;
85
+ platform: string;
86
+ duration: string;
87
+ url: string;
88
+ }
89
+ interface ForumResult {
90
+ title: string;
91
+ source: string;
92
+ url: string;
93
+ }
94
+ interface WhatPeopleSayingCard {
95
+ type: 'reddit' | 'facebook' | 'instagram' | 'tiktok' | 'youtube' | 'news' | 'unknown';
96
+ title: string;
97
+ url: string;
98
+ source: string;
99
+ platform: string;
100
+ popularComment: string | null;
101
+ engagement: string;
102
+ date: string;
103
+ duration: string | null;
104
+ authorNote: string | null;
105
+ }
106
+ interface AICitation {
107
+ text: string;
108
+ href: string;
109
+ }
110
+ interface AIOverviewResult {
111
+ detected: boolean;
112
+ text: string | null;
113
+ citations: AICitation[];
114
+ }
115
+ interface AIModeResult {
116
+ detected: boolean;
117
+ text: string | null;
118
+ citations: AICitation[];
119
+ }
120
+ type GoogleSurface = 'web' | 'aim' | 'unknown';
121
+ interface OrganicResult {
122
+ position: number;
123
+ title: string;
124
+ url: string;
125
+ domain: string;
126
+ cite: string | null;
127
+ snippet: string | null;
128
+ isRedditStyle: boolean;
129
+ inlineRating: {
130
+ value: string;
131
+ count: string;
132
+ } | null;
133
+ }
134
+ interface LocalPackBusiness {
135
+ position: number;
136
+ name: string;
137
+ cid: string | null;
138
+ rating: string | null;
139
+ reviewCount: string | null;
140
+ metadata: string[];
141
+ websiteUrl: string | null;
142
+ directionsUrl: string | null;
143
+ }
144
+ interface EntityRecord {
145
+ name: string;
146
+ kgId: string | null;
147
+ cid: string | null;
148
+ gcid: string | null;
149
+ }
150
+ interface EntityIds {
151
+ entities: EntityRecord[];
152
+ kgIds: string[];
153
+ cids: string[];
154
+ gcids: string[];
155
+ }
156
+ interface HarvestResult {
157
+ seed: string;
158
+ location: string | null;
159
+ extractedAt: string;
160
+ totalQuestions: number;
161
+ surface: GoogleSurface;
162
+ aiOverview: AIOverviewResult;
163
+ aiMode: AIModeResult;
164
+ whatPeopleSaying: WhatPeopleSayingCard[];
165
+ tree: PAANode[];
166
+ flat: FlatRow[];
167
+ videos: VideoResult[];
168
+ forums: ForumResult[];
169
+ organicResults: OrganicResult[];
170
+ localPack: LocalPackBusiness[];
171
+ entityIds: EntityIds;
172
+ stats: HarvestStats;
173
+ }
174
+ type HarvestOptions = z.infer<typeof HarvestOptionsSchema>;
175
+
176
+ declare function harvest(rawOptions: unknown): Promise<HarvestResult>;
177
+
178
+ interface ClipPairOptions {
179
+ resolution?: '480p' | '720p' | '1080p';
180
+ aspectRatio?: '16:9' | '9:16' | '1:1';
181
+ clipDurationSeconds?: number;
182
+ generateAudio?: boolean;
183
+ seed?: number;
184
+ outputDir?: string;
185
+ ttsVoice?: string;
186
+ }
187
+ interface ClipPairResult {
188
+ clip1Url: string;
189
+ clip2Url: string;
190
+ finalVideoPath: string;
191
+ seed: number;
192
+ promptClip1: string;
193
+ promptClip2: string;
194
+ voiceover: string;
195
+ audioMood: string;
196
+ }
197
+ declare class VideoGenerator {
198
+ constructor(apiKey?: string);
199
+ generateClipPair(question: string, answer: string, opts?: ClipPairOptions): Promise<ClipPairResult>;
200
+ }
201
+
202
+ interface ClipPromptPair {
203
+ clip1: string;
204
+ clip2: string;
205
+ voiceover: string;
206
+ audioMood: string;
207
+ }
208
+ declare function buildClipPrompts(question: string, answer: string): Promise<ClipPromptPair>;
209
+
210
+ export { type AICitation, type AIModeResult, type AIOverviewResult, type ClipPairOptions, type ClipPairResult, type FlatRow, type GoogleSurface, type HarvestOptions, type HarvestResult, type HarvestStats, type PAANode, VideoGenerator, type WhatPeopleSayingCard, buildClipPrompts, harvest };
package/dist/index.js ADDED
@@ -0,0 +1,275 @@
1
+ import {
2
+ harvest
3
+ } from "./chunk-4API3ZCT.js";
4
+
5
+ // src/video/VideoGenerator.ts
6
+ import { execSync as execSync2 } from "child_process";
7
+ import { readFileSync, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
8
+ import { tmpdir } from "os";
9
+ import { join as join2 } from "path";
10
+ import { fal as fal2 } from "@fal-ai/client";
11
+
12
+ // src/video/promptBuilder.ts
13
+ var DEEPINFRA_URL = "https://api.deepinfra.com/v1/openai/chat/completions";
14
+ var OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
15
+ var QWEN_MODEL = "Qwen/Qwen3.6-35B-A3B";
16
+ var SYSTEM_PROMPT = `You are a video prompt engineer for Seedance 2.0 text-to-video AI.
17
+
18
+ IMPORTANT TECHNICAL CONTEXT: Clip 2 will be generated using image-to-video, meaning it will start from the exact last frame of Clip 1. The two clips will be visually seamless \u2014 same location, same characters, continuous motion. You must write prompts that make this feel like one uninterrupted 16-second video.
19
+
20
+ Your job: turn a PAA question and its answer into a complete short-form video with visuals, narration, and a background audio mood.
21
+
22
+ Produce four things:
23
+
24
+ 1. clip1 (~8s): Show a real person experiencing the situation the question describes. End on a specific frozen moment \u2014 a held expression, a paused action \u2014 that clip 2 continues from.
25
+
26
+ 2. clip2 (~8s): Continue from that exact frozen frame. Deliver the informational payoff from the answer. Show specific facts playing out visually. Describe motion continuing from clip 1's ending frame, not a new scene.
27
+
28
+ 3. voiceover: A spoken narration that fits in 10 seconds of TTS audio. HARD LIMIT: 22 words maximum. Count every word before finalizing \u2014 if it exceeds 22 words, cut it. Starts with the problem, ends with the answer. No filler, no "in conclusion". Informational and direct.
29
+
30
+ 4. audioMood: 6-10 words describing INSTRUMENTAL background music only \u2014 no vocals, no voice, no lyrics. Describe instruments and mood (e.g. "warm acoustic guitar, uplifting, professional home service, no vocals"). Will be passed to an AI audio model.
31
+
32
+ Rules for visuals:
33
+ - No text, captions, graphics, or overlays
34
+ - Photorealistic, natural lighting, specific details
35
+ - Describe exactly what the camera sees
36
+
37
+ Respond with JSON only:
38
+ {"clip1": "...", "clip2": "...", "voiceover": "...", "audioMood": "..."}`;
39
+ async function callLLM(apiKey, baseUrl, question, answer) {
40
+ const res = await fetch(baseUrl, {
41
+ method: "POST",
42
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
43
+ body: JSON.stringify({
44
+ model: QWEN_MODEL,
45
+ temperature: 0.7,
46
+ messages: [
47
+ { role: "system", content: SYSTEM_PROMPT },
48
+ { role: "user", content: `Question: ${question}
49
+
50
+ Answer: ${answer.slice(0, 500)}` }
51
+ ]
52
+ })
53
+ });
54
+ if (!res.ok) throw new Error(`LLM call failed (${res.status}): ${await res.text()}`);
55
+ const data = await res.json();
56
+ const raw = data.choices[0]?.message?.content?.trim() ?? "";
57
+ const match = raw.match(/\{[\s\S]*\}/);
58
+ if (!match) throw new Error(`No JSON in QWEN response: ${raw.slice(0, 200)}`);
59
+ const parsed = JSON.parse(match[0]);
60
+ if (!parsed.clip1 || !parsed.clip2 || !parsed.voiceover || !parsed.audioMood) {
61
+ throw new Error(`QWEN response missing fields: ${raw.slice(0, 200)}`);
62
+ }
63
+ return parsed;
64
+ }
65
+ async function buildClipPrompts(question, answer) {
66
+ const deepinfraKey = process.env["DEEPINFRA_API_KEY"];
67
+ const openrouterKey = process.env["OPENROUTER_API_KEY"];
68
+ if (deepinfraKey) {
69
+ try {
70
+ return await callLLM(deepinfraKey, DEEPINFRA_URL, question, answer);
71
+ } catch (err) {
72
+ console.warn("[promptBuilder] DeepInfra failed, trying OpenRouter:", err.message);
73
+ }
74
+ }
75
+ if (openrouterKey) {
76
+ return await callLLM(openrouterKey, OPENROUTER_URL, question, answer);
77
+ }
78
+ throw new Error("No LLM key \u2014 set DEEPINFRA_API_KEY or OPENROUTER_API_KEY");
79
+ }
80
+
81
+ // src/video/AudioGenerator.ts
82
+ var TTS_MODEL = "fal-ai/inworld-tts";
83
+ var MMAUDIO_MODEL = "fal-ai/mmaudio-v2";
84
+ var QUEUE_BASE = "https://queue.fal.run";
85
+ async function rawQueueRun(model, input, apiKey) {
86
+ const headers = { "Authorization": `Key ${apiKey}`, "Content-Type": "application/json" };
87
+ const submitRes = await fetch(`${QUEUE_BASE}/${model}`, {
88
+ method: "POST",
89
+ headers,
90
+ body: JSON.stringify(input)
91
+ });
92
+ if (!submitRes.ok) throw new Error(`${model} submit failed (${submitRes.status}): ${await submitRes.text()}`);
93
+ const { request_id } = await submitRes.json();
94
+ console.log(`[fal] submitted ${model} \u2192 ${request_id}`);
95
+ while (true) {
96
+ await new Promise((r) => setTimeout(r, 5e3));
97
+ const statusRes = await fetch(`${QUEUE_BASE}/${model}/requests/${request_id}/status`, { headers });
98
+ if (!statusRes.ok) continue;
99
+ const { status } = await statusRes.json();
100
+ console.log(`[fal] ${request_id} \u2192 ${status}`);
101
+ if (status === "FAILED") throw new Error(`${model} request ${request_id} failed`);
102
+ if (status !== "COMPLETED") continue;
103
+ const resultRes = await fetch(`${QUEUE_BASE}/${model}/requests/${request_id}`, { headers });
104
+ if (!resultRes.ok) throw new Error(`Result fetch failed (${resultRes.status})`);
105
+ return await resultRes.json();
106
+ }
107
+ }
108
+ function getKey() {
109
+ const key = process.env["FAL_KEY"];
110
+ if (!key) throw new Error("FAL_KEY required");
111
+ return key;
112
+ }
113
+ async function generateVoiceover(text, voice = "Serena (en)") {
114
+ console.log("[AudioGenerator] Generating voiceover...");
115
+ const out = await rawQueueRun(TTS_MODEL, { text, voice, sample_rate_hertz: 48e3 }, getKey());
116
+ return out.audio.url;
117
+ }
118
+ async function addBackgroundAudio(videoUrl, mood, durationSeconds) {
119
+ console.log("[AudioGenerator] Adding background audio via MMAudio V2...");
120
+ const out = await rawQueueRun(MMAUDIO_MODEL, {
121
+ video_url: videoUrl,
122
+ prompt: mood,
123
+ negative_prompt: "speech, voice, talking, dialogue, narration, vocals, singing, human voice, conversation, words, lyrics, announcer, commentary",
124
+ duration: durationSeconds,
125
+ cfg_strength: 4.5
126
+ }, getKey());
127
+ return out.video.url;
128
+ }
129
+
130
+ // src/video/VideoMixer.ts
131
+ import { execSync } from "child_process";
132
+ import { writeFileSync, mkdirSync } from "fs";
133
+ import { join } from "path";
134
+ import { fal } from "@fal-ai/client";
135
+ async function download(url, destPath) {
136
+ const res = await fetch(url);
137
+ if (!res.ok) throw new Error(`Download failed (${res.status}): ${url}`);
138
+ writeFileSync(destPath, Buffer.from(await res.arrayBuffer()));
139
+ }
140
+ async function concatenateClips(clip1Url, clip2Url, outDir) {
141
+ mkdirSync(outDir, { recursive: true });
142
+ const ts = Date.now();
143
+ const p1 = join(outDir, `clip1-${ts}.mp4`);
144
+ const p2 = join(outDir, `clip2-${ts}.mp4`);
145
+ const out = join(outDir, `combined-${ts}.mp4`);
146
+ console.log("[VideoMixer] Downloading clips...");
147
+ await Promise.all([download(clip1Url, p1), download(clip2Url, p2)]);
148
+ console.log("[VideoMixer] Concatenating...");
149
+ execSync(
150
+ `ffmpeg -i "${p1}" -i "${p2}" -filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" -map "[v]" -y "${out}" -loglevel error`
151
+ );
152
+ return out;
153
+ }
154
+ async function uploadToFal(localPath) {
155
+ const { readFileSync: readFileSync2 } = await import("fs");
156
+ const blob = new Blob([readFileSync2(localPath)], { type: "video/mp4" });
157
+ const url = await fal.storage.upload(blob);
158
+ console.log("[VideoMixer] Uploaded to fal:", url);
159
+ return url;
160
+ }
161
+ async function overlayVoiceover(videoPath, voiceoverUrl, outDir) {
162
+ const ts = Date.now();
163
+ const wav = join(outDir, `voiceover-${ts}.wav`);
164
+ const out = join(outDir, `final-${ts}.mp4`);
165
+ console.log("[VideoMixer] Downloading voiceover...");
166
+ await download(voiceoverUrl, wav);
167
+ console.log("[VideoMixer] Mixing voiceover over background audio...");
168
+ execSync(
169
+ `ffmpeg -i "${videoPath}" -i "${wav}" -filter_complex "[0:a]volume=0.2[bg];[1:a]volume=1.0[vo];[bg][vo]amix=inputs=2:duration=first[a]" -map 0:v -map "[a]" -c:v copy -y "${out}" -loglevel error`
170
+ );
171
+ return out;
172
+ }
173
+
174
+ // src/video/VideoGenerator.ts
175
+ var T2V = "bytedance/seedance-2.0/text-to-video";
176
+ var I2V = "bytedance/seedance-2.0/image-to-video";
177
+ function buildInput(prompt, opts, seed, imageUrl) {
178
+ return {
179
+ prompt,
180
+ resolution: opts.resolution ?? "720p",
181
+ duration: opts.clipDurationSeconds ?? 8,
182
+ aspect_ratio: opts.aspectRatio ?? "16:9",
183
+ generate_audio: false,
184
+ ...seed !== void 0 ? { seed } : {},
185
+ ...imageUrl !== void 0 ? { image_url: imageUrl } : {}
186
+ };
187
+ }
188
+ async function generate(model, input) {
189
+ const { request_id } = await fal2.queue.submit(model, { input });
190
+ console.log(`[fal] submitted ${model} \u2192 ${request_id}`);
191
+ while (true) {
192
+ await new Promise((r) => setTimeout(r, 5e3));
193
+ const s = await fal2.queue.status(model, { requestId: request_id, logs: false });
194
+ console.log(`[fal] ${request_id} \u2192 ${s.status}`);
195
+ if (s.status === "FAILED") throw new Error(`Request ${request_id} failed`);
196
+ if (s.status !== "COMPLETED") continue;
197
+ const result = await fal2.queue.result(model, { requestId: request_id });
198
+ return result.data;
199
+ }
200
+ }
201
+ async function extractLastFrame(videoUrl, outDir) {
202
+ const ts = Date.now();
203
+ const mp4Path = join2(outDir, `clip1-raw-${ts}.mp4`);
204
+ const jpgPath = join2(outDir, `last-frame-${ts}.jpg`);
205
+ const res = await fetch(videoUrl);
206
+ if (!res.ok) throw new Error(`Failed to download clip 1 (${res.status})`);
207
+ writeFileSync2(mp4Path, Buffer.from(await res.arrayBuffer()));
208
+ try {
209
+ execSync2(`ffmpeg -sseof -0.1 -i "${mp4Path}" -vframes 1 -y "${jpgPath}" -loglevel error`);
210
+ } finally {
211
+ try {
212
+ unlinkSync(mp4Path);
213
+ } catch {
214
+ }
215
+ }
216
+ return jpgPath;
217
+ }
218
+ var VideoGenerator = class {
219
+ constructor(apiKey) {
220
+ const key = apiKey ?? process.env["FAL_KEY"];
221
+ if (!key) throw new Error("FAL_KEY is required");
222
+ fal2.config({ credentials: key });
223
+ }
224
+ async generateClipPair(question, answer, opts = {}) {
225
+ const outDir = opts.outputDir ?? join2(tmpdir(), `paa-video-${Date.now()}`);
226
+ mkdirSync2(outDir, { recursive: true });
227
+ console.log("\n[1/7] Generating prompts via QWEN 3.6...");
228
+ const prompts = await buildClipPrompts(question, answer);
229
+ console.log(" Voiceover:", prompts.voiceover);
230
+ console.log(" Audio mood:", prompts.audioMood);
231
+ console.log("\n[2/7] Generating clip 1 (text-to-video)...");
232
+ const result1 = await generate(T2V, buildInput(prompts.clip1, opts, opts.seed));
233
+ console.log("\n[3/7] Extracting last frame \u2192 clip 2 start...");
234
+ const jpgPath = await extractLastFrame(result1.video.url, outDir);
235
+ const imageBlob = new Blob([readFileSync(jpgPath)], { type: "image/jpeg" });
236
+ const frameUrl = await fal2.storage.upload(imageBlob);
237
+ try {
238
+ unlinkSync(jpgPath);
239
+ } catch {
240
+ }
241
+ console.log("\n[4/7] Generating clip 2 (image-to-video from last frame)...");
242
+ const seed2 = opts.seed !== void 0 ? opts.seed + 1 : void 0;
243
+ const result2 = await generate(I2V, buildInput(prompts.clip2, opts, seed2, frameUrl));
244
+ console.log("\n[5/7] Concatenating clips + generating voiceover (parallel)...");
245
+ const [combinedPath, voiceoverUrl] = await Promise.all([
246
+ concatenateClips(result1.video.url, result2.video.url, outDir),
247
+ generateVoiceover(prompts.voiceover, opts.ttsVoice)
248
+ ]);
249
+ console.log("\n[6/7] Adding background audio via MMAudio V2...");
250
+ const falVideoUrl = await uploadToFal(combinedPath);
251
+ const totalDuration = (opts.clipDurationSeconds ?? 8) * 2;
252
+ const videoWithAudioUrl = await addBackgroundAudio(falVideoUrl, prompts.audioMood, totalDuration);
253
+ console.log("\n[7/7] Overlaying voiceover on final video...");
254
+ const videoWithAudioPath = join2(outDir, `with-bg-audio-${Date.now()}.mp4`);
255
+ const bgRes = await fetch(videoWithAudioUrl);
256
+ writeFileSync2(videoWithAudioPath, Buffer.from(await bgRes.arrayBuffer()));
257
+ const finalVideoPath = await overlayVoiceover(videoWithAudioPath, voiceoverUrl, outDir);
258
+ return {
259
+ clip1Url: result1.video.url,
260
+ clip2Url: result2.video.url,
261
+ finalVideoPath,
262
+ seed: result1.seed,
263
+ promptClip1: prompts.clip1,
264
+ promptClip2: prompts.clip2,
265
+ voiceover: prompts.voiceover,
266
+ audioMood: prompts.audioMood
267
+ };
268
+ }
269
+ };
270
+ export {
271
+ VideoGenerator,
272
+ buildClipPrompts,
273
+ harvest
274
+ };
275
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/video/VideoGenerator.ts","../src/video/promptBuilder.ts","../src/video/AudioGenerator.ts","../src/video/VideoMixer.ts"],"sourcesContent":["import { execSync } from 'node:child_process'\nimport { readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\nimport { fal } from '@fal-ai/client'\nimport { buildClipPrompts } from './promptBuilder.js'\nimport { generateVoiceover, addBackgroundAudio } from './AudioGenerator.js'\nimport { concatenateClips, uploadToFal, overlayVoiceover } from './VideoMixer.js'\n\nexport interface ClipPairOptions {\n resolution?: '480p' | '720p' | '1080p'\n aspectRatio?: '16:9' | '9:16' | '1:1'\n clipDurationSeconds?: number\n generateAudio?: boolean\n seed?: number\n outputDir?: string\n ttsVoice?: string\n}\n\nexport interface ClipPairResult {\n clip1Url: string\n clip2Url: string\n finalVideoPath: string\n seed: number\n promptClip1: string\n promptClip2: string\n voiceover: string\n audioMood: string\n}\n\ninterface VideoOutput { video: { url: string }; seed: number }\n\nconst T2V = 'bytedance/seedance-2.0/text-to-video'\nconst I2V = 'bytedance/seedance-2.0/image-to-video'\n\nfunction buildInput(prompt: string, opts: ClipPairOptions, seed?: number, imageUrl?: string) {\n return {\n prompt,\n resolution: opts.resolution ?? '720p',\n duration: opts.clipDurationSeconds ?? 8,\n aspect_ratio: opts.aspectRatio ?? '16:9',\n generate_audio: false,\n ...(seed !== undefined ? { seed } : {}),\n ...(imageUrl !== undefined ? { image_url: imageUrl } : {}),\n }\n}\n\nasync function generate(model: string, input: Record<string, unknown>): Promise<VideoOutput> {\n const { request_id } = await fal.queue.submit(model, { input })\n console.log(`[fal] submitted ${model} → ${request_id}`)\n while (true) {\n await new Promise(r => setTimeout(r, 5000))\n const s = await fal.queue.status(model, { requestId: request_id, logs: false })\n console.log(`[fal] ${request_id} → ${s.status}`)\n if ((s.status as string) === 'FAILED') throw new Error(`Request ${request_id} failed`)\n if ((s.status as string) !== 'COMPLETED') continue\n const result = await fal.queue.result(model, { requestId: request_id })\n return result.data as VideoOutput\n }\n}\n\nasync function extractLastFrame(videoUrl: string, outDir: string): Promise<string> {\n const ts = Date.now()\n const mp4Path = join(outDir, `clip1-raw-${ts}.mp4`)\n const jpgPath = join(outDir, `last-frame-${ts}.jpg`)\n\n const res = await fetch(videoUrl)\n if (!res.ok) throw new Error(`Failed to download clip 1 (${res.status})`)\n writeFileSync(mp4Path, Buffer.from(await res.arrayBuffer()))\n\n try {\n execSync(`ffmpeg -sseof -0.1 -i \"${mp4Path}\" -vframes 1 -y \"${jpgPath}\" -loglevel error`)\n } finally {\n try { unlinkSync(mp4Path) } catch {}\n }\n return jpgPath\n}\n\nexport class VideoGenerator {\n constructor(apiKey?: string) {\n const key = apiKey ?? process.env['FAL_KEY']\n if (!key) throw new Error('FAL_KEY is required')\n fal.config({ credentials: key })\n }\n\n async generateClipPair(\n question: string,\n answer: string,\n opts: ClipPairOptions = {},\n ): Promise<ClipPairResult> {\n const outDir = opts.outputDir ?? join(tmpdir(), `paa-video-${Date.now()}`)\n mkdirSync(outDir, { recursive: true })\n\n console.log('\\n[1/7] Generating prompts via QWEN 3.6...')\n const prompts = await buildClipPrompts(question, answer)\n console.log(' Voiceover:', prompts.voiceover)\n console.log(' Audio mood:', prompts.audioMood)\n\n console.log('\\n[2/7] Generating clip 1 (text-to-video)...')\n const result1 = await generate(T2V, buildInput(prompts.clip1, opts, opts.seed))\n\n console.log('\\n[3/7] Extracting last frame → clip 2 start...')\n const jpgPath = await extractLastFrame(result1.video.url, outDir)\n const imageBlob = new Blob([readFileSync(jpgPath)], { type: 'image/jpeg' })\n const frameUrl = await fal.storage.upload(imageBlob)\n try { unlinkSync(jpgPath) } catch {}\n\n console.log('\\n[4/7] Generating clip 2 (image-to-video from last frame)...')\n const seed2 = opts.seed !== undefined ? opts.seed + 1 : undefined\n const result2 = await generate(I2V, buildInput(prompts.clip2, opts, seed2, frameUrl))\n\n console.log('\\n[5/7] Concatenating clips + generating voiceover (parallel)...')\n const [combinedPath, voiceoverUrl] = await Promise.all([\n concatenateClips(result1.video.url, result2.video.url, outDir),\n generateVoiceover(prompts.voiceover, opts.ttsVoice),\n ])\n\n console.log('\\n[6/7] Adding background audio via MMAudio V2...')\n const falVideoUrl = await uploadToFal(combinedPath)\n const totalDuration = (opts.clipDurationSeconds ?? 8) * 2\n const videoWithAudioUrl = await addBackgroundAudio(falVideoUrl, prompts.audioMood, totalDuration)\n\n console.log('\\n[7/7] Overlaying voiceover on final video...')\n const videoWithAudioPath = join(outDir, `with-bg-audio-${Date.now()}.mp4`)\n const bgRes = await fetch(videoWithAudioUrl)\n writeFileSync(videoWithAudioPath, Buffer.from(await bgRes.arrayBuffer()))\n const finalVideoPath = await overlayVoiceover(videoWithAudioPath, voiceoverUrl, outDir)\n\n return {\n clip1Url: result1.video.url,\n clip2Url: result2.video.url,\n finalVideoPath,\n seed: result1.seed,\n promptClip1: prompts.clip1,\n promptClip2: prompts.clip2,\n voiceover: prompts.voiceover,\n audioMood: prompts.audioMood,\n }\n }\n}\n","export interface ClipPromptPair {\n clip1: string\n clip2: string\n voiceover: string\n audioMood: string\n}\n\nconst DEEPINFRA_URL = 'https://api.deepinfra.com/v1/openai/chat/completions'\nconst OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'\nconst QWEN_MODEL = 'Qwen/Qwen3.6-35B-A3B'\n\nconst SYSTEM_PROMPT = `You are a video prompt engineer for Seedance 2.0 text-to-video AI.\n\nIMPORTANT TECHNICAL CONTEXT: Clip 2 will be generated using image-to-video, meaning it will start from the exact last frame of Clip 1. The two clips will be visually seamless — same location, same characters, continuous motion. You must write prompts that make this feel like one uninterrupted 16-second video.\n\nYour job: turn a PAA question and its answer into a complete short-form video with visuals, narration, and a background audio mood.\n\nProduce four things:\n\n1. clip1 (~8s): Show a real person experiencing the situation the question describes. End on a specific frozen moment — a held expression, a paused action — that clip 2 continues from.\n\n2. clip2 (~8s): Continue from that exact frozen frame. Deliver the informational payoff from the answer. Show specific facts playing out visually. Describe motion continuing from clip 1's ending frame, not a new scene.\n\n3. voiceover: A spoken narration that fits in 10 seconds of TTS audio. HARD LIMIT: 22 words maximum. Count every word before finalizing — if it exceeds 22 words, cut it. Starts with the problem, ends with the answer. No filler, no \"in conclusion\". Informational and direct.\n\n4. audioMood: 6-10 words describing INSTRUMENTAL background music only — no vocals, no voice, no lyrics. Describe instruments and mood (e.g. \"warm acoustic guitar, uplifting, professional home service, no vocals\"). Will be passed to an AI audio model.\n\nRules for visuals:\n- No text, captions, graphics, or overlays\n- Photorealistic, natural lighting, specific details\n- Describe exactly what the camera sees\n\nRespond with JSON only:\n{\"clip1\": \"...\", \"clip2\": \"...\", \"voiceover\": \"...\", \"audioMood\": \"...\"}`\n\ninterface ChatResponse {\n choices: Array<{ message: { content: string } }>\n}\n\nasync function callLLM(apiKey: string, baseUrl: string, question: string, answer: string): Promise<ClipPromptPair> {\n const res = await fetch(baseUrl, {\n method: 'POST',\n headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: QWEN_MODEL,\n temperature: 0.7,\n messages: [\n { role: 'system', content: SYSTEM_PROMPT },\n { role: 'user', content: `Question: ${question}\\n\\nAnswer: ${answer.slice(0, 500)}` },\n ],\n }),\n })\n if (!res.ok) throw new Error(`LLM call failed (${res.status}): ${await res.text()}`)\n\n const data = (await res.json()) as ChatResponse\n const raw = data.choices[0]?.message?.content?.trim() ?? ''\n const match = raw.match(/\\{[\\s\\S]*\\}/)\n if (!match) throw new Error(`No JSON in QWEN response: ${raw.slice(0, 200)}`)\n\n const parsed = JSON.parse(match[0]) as Partial<ClipPromptPair>\n if (!parsed.clip1 || !parsed.clip2 || !parsed.voiceover || !parsed.audioMood) {\n throw new Error(`QWEN response missing fields: ${raw.slice(0, 200)}`)\n }\n return parsed as ClipPromptPair\n}\n\nexport async function buildClipPrompts(question: string, answer: string): Promise<ClipPromptPair> {\n const deepinfraKey = process.env['DEEPINFRA_API_KEY']\n const openrouterKey = process.env['OPENROUTER_API_KEY']\n\n if (deepinfraKey) {\n try {\n return await callLLM(deepinfraKey, DEEPINFRA_URL, question, answer)\n } catch (err) {\n console.warn('[promptBuilder] DeepInfra failed, trying OpenRouter:', (err as Error).message)\n }\n }\n if (openrouterKey) {\n return await callLLM(openrouterKey, OPENROUTER_URL, question, answer)\n }\n throw new Error('No LLM key — set DEEPINFRA_API_KEY or OPENROUTER_API_KEY')\n}\n","const TTS_MODEL = 'fal-ai/inworld-tts'\nconst MMAUDIO_MODEL = 'fal-ai/mmaudio-v2'\nconst QUEUE_BASE = 'https://queue.fal.run'\n\ninterface TtsOutput { audio: { url: string } }\ninterface VideoOutput { video: { url: string } }\n\nasync function rawQueueRun(model: string, input: Record<string, unknown>, apiKey: string): Promise<Record<string, unknown>> {\n const headers = { 'Authorization': `Key ${apiKey}`, 'Content-Type': 'application/json' }\n\n const submitRes = await fetch(`${QUEUE_BASE}/${model}`, {\n method: 'POST', headers, body: JSON.stringify(input),\n })\n if (!submitRes.ok) throw new Error(`${model} submit failed (${submitRes.status}): ${await submitRes.text()}`)\n const { request_id } = await submitRes.json() as { request_id: string }\n console.log(`[fal] submitted ${model} → ${request_id}`)\n\n while (true) {\n await new Promise(r => setTimeout(r, 5000))\n const statusRes = await fetch(`${QUEUE_BASE}/${model}/requests/${request_id}/status`, { headers })\n if (!statusRes.ok) continue\n const { status } = await statusRes.json() as { status: string }\n console.log(`[fal] ${request_id} → ${status}`)\n if (status === 'FAILED') throw new Error(`${model} request ${request_id} failed`)\n if (status !== 'COMPLETED') continue\n const resultRes = await fetch(`${QUEUE_BASE}/${model}/requests/${request_id}`, { headers })\n if (!resultRes.ok) throw new Error(`Result fetch failed (${resultRes.status})`)\n return await resultRes.json() as Record<string, unknown>\n }\n}\n\nfunction getKey(): string {\n const key = process.env['FAL_KEY']\n if (!key) throw new Error('FAL_KEY required')\n return key\n}\n\nexport async function generateVoiceover(text: string, voice = 'Serena (en)'): Promise<string> {\n console.log('[AudioGenerator] Generating voiceover...')\n const out = await rawQueueRun(TTS_MODEL, { text, voice, sample_rate_hertz: 48000 }, getKey()) as unknown as TtsOutput\n return out.audio.url\n}\n\nexport async function addBackgroundAudio(videoUrl: string, mood: string, durationSeconds: number): Promise<string> {\n console.log('[AudioGenerator] Adding background audio via MMAudio V2...')\n const out = await rawQueueRun(MMAUDIO_MODEL, {\n video_url: videoUrl,\n prompt: mood,\n negative_prompt: 'speech, voice, talking, dialogue, narration, vocals, singing, human voice, conversation, words, lyrics, announcer, commentary',\n duration: durationSeconds,\n cfg_strength: 4.5,\n }, getKey()) as unknown as VideoOutput\n return out.video.url\n}\n","import { execSync } from 'node:child_process'\nimport { writeFileSync, mkdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { fal } from '@fal-ai/client'\n\nasync function download(url: string, destPath: string): Promise<void> {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`Download failed (${res.status}): ${url}`)\n writeFileSync(destPath, Buffer.from(await res.arrayBuffer()))\n}\n\nexport async function concatenateClips(clip1Url: string, clip2Url: string, outDir: string): Promise<string> {\n mkdirSync(outDir, { recursive: true })\n const ts = Date.now()\n const p1 = join(outDir, `clip1-${ts}.mp4`)\n const p2 = join(outDir, `clip2-${ts}.mp4`)\n const out = join(outDir, `combined-${ts}.mp4`)\n\n console.log('[VideoMixer] Downloading clips...')\n await Promise.all([download(clip1Url, p1), download(clip2Url, p2)])\n\n console.log('[VideoMixer] Concatenating...')\n execSync(\n `ffmpeg -i \"${p1}\" -i \"${p2}\" -filter_complex \"[0:v][1:v]concat=n=2:v=1:a=0[v]\" -map \"[v]\" -y \"${out}\" -loglevel error`\n )\n return out\n}\n\nexport async function uploadToFal(localPath: string): Promise<string> {\n const { readFileSync } = await import('node:fs')\n const blob = new Blob([readFileSync(localPath)], { type: 'video/mp4' })\n const url = await fal.storage.upload(blob)\n console.log('[VideoMixer] Uploaded to fal:', url)\n return url\n}\n\nexport async function overlayVoiceover(\n videoPath: string,\n voiceoverUrl: string,\n outDir: string,\n): Promise<string> {\n const ts = Date.now()\n const wav = join(outDir, `voiceover-${ts}.wav`)\n const out = join(outDir, `final-${ts}.mp4`)\n\n console.log('[VideoMixer] Downloading voiceover...')\n await download(voiceoverUrl, wav)\n\n console.log('[VideoMixer] Mixing voiceover over background audio...')\n execSync(\n `ffmpeg -i \"${videoPath}\" -i \"${wav}\" ` +\n `-filter_complex \"[0:a]volume=0.2[bg];[1:a]volume=1.0[vo];[bg][vo]amix=inputs=2:duration=first[a]\" ` +\n `-map 0:v -map \"[a]\" -c:v copy -y \"${out}\" -loglevel error`\n )\n return out\n}\n"],"mappings":";;;;;AAAA,SAAS,YAAAA,iBAAgB;AACzB,SAAS,cAAc,iBAAAC,gBAAe,YAAY,aAAAC,kBAAiB;AACnE,SAAS,cAAc;AACvB,SAAS,QAAAC,aAAY;AACrB,SAAS,OAAAC,YAAW;;;ACGpB,IAAM,gBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,aAAiB;AAEvB,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BtB,eAAe,QAAQ,QAAgB,SAAiB,UAAkB,QAAyC;AACjH,QAAM,MAAM,MAAM,MAAM,SAAS;AAAA,IAC/B,QAAS;AAAA,IACT,SAAS,EAAE,iBAAiB,UAAU,MAAM,IAAI,gBAAgB,mBAAmB;AAAA,IACnF,MAAM,KAAK,UAAU;AAAA,MACnB,OAAa;AAAA,MACb,aAAa;AAAA,MACb,UAAU;AAAA,QACR,EAAE,MAAM,UAAU,SAAS,cAAc;AAAA,QACzC,EAAE,MAAM,QAAU,SAAS,aAAa,QAAQ;AAAA;AAAA,UAAe,OAAO,MAAM,GAAG,GAAG,CAAC,GAAG;AAAA,MACxF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,MAAM,MAAM,IAAI,KAAK,CAAC,EAAE;AAEnF,QAAM,OAAS,MAAM,IAAI,KAAK;AAC9B,QAAM,MAAQ,KAAK,QAAQ,CAAC,GAAG,SAAS,SAAS,KAAK,KAAK;AAC3D,QAAM,QAAQ,IAAI,MAAM,aAAa;AACrC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,GAAG,GAAG,CAAC,EAAE;AAE5E,QAAM,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAClC,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,SAAS,CAAC,OAAO,aAAa,CAAC,OAAO,WAAW;AAC5E,UAAM,IAAI,MAAM,iCAAiC,IAAI,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,EACtE;AACA,SAAO;AACT;AAEA,eAAsB,iBAAiB,UAAkB,QAAyC;AAChG,QAAM,eAAgB,QAAQ,IAAI,mBAAmB;AACrD,QAAM,gBAAgB,QAAQ,IAAI,oBAAoB;AAEtD,MAAI,cAAc;AAChB,QAAI;AACF,aAAO,MAAM,QAAQ,cAAc,eAAe,UAAU,MAAM;AAAA,IACpE,SAAS,KAAK;AACZ,cAAQ,KAAK,wDAAyD,IAAc,OAAO;AAAA,IAC7F;AAAA,EACF;AACA,MAAI,eAAe;AACjB,WAAO,MAAM,QAAQ,eAAe,gBAAgB,UAAU,MAAM;AAAA,EACtE;AACA,QAAM,IAAI,MAAM,+DAA0D;AAC5E;;;ACjFA,IAAM,YAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,aAAgB;AAKtB,eAAe,YAAY,OAAe,OAAgC,QAAkD;AAC1H,QAAM,UAAU,EAAE,iBAAiB,OAAO,MAAM,IAAI,gBAAgB,mBAAmB;AAEvF,QAAM,YAAY,MAAM,MAAM,GAAG,UAAU,IAAI,KAAK,IAAI;AAAA,IACtD,QAAQ;AAAA,IAAQ;AAAA,IAAS,MAAM,KAAK,UAAU,KAAK;AAAA,EACrD,CAAC;AACD,MAAI,CAAC,UAAU,GAAI,OAAM,IAAI,MAAM,GAAG,KAAK,mBAAmB,UAAU,MAAM,MAAM,MAAM,UAAU,KAAK,CAAC,EAAE;AAC5G,QAAM,EAAE,WAAW,IAAI,MAAM,UAAU,KAAK;AAC5C,UAAQ,IAAI,mBAAmB,KAAK,WAAM,UAAU,EAAE;AAEtD,SAAO,MAAM;AACX,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAI,CAAC;AAC1C,UAAM,YAAY,MAAM,MAAM,GAAG,UAAU,IAAI,KAAK,aAAa,UAAU,WAAW,EAAE,QAAQ,CAAC;AACjG,QAAI,CAAC,UAAU,GAAI;AACnB,UAAM,EAAE,OAAO,IAAI,MAAM,UAAU,KAAK;AACxC,YAAQ,IAAI,SAAS,UAAU,WAAM,MAAM,EAAE;AAC7C,QAAI,WAAW,SAAU,OAAM,IAAI,MAAM,GAAG,KAAK,YAAY,UAAU,SAAS;AAChF,QAAI,WAAW,YAAa;AAC5B,UAAM,YAAY,MAAM,MAAM,GAAG,UAAU,IAAI,KAAK,aAAa,UAAU,IAAI,EAAE,QAAQ,CAAC;AAC1F,QAAI,CAAC,UAAU,GAAI,OAAM,IAAI,MAAM,wBAAwB,UAAU,MAAM,GAAG;AAC9E,WAAO,MAAM,UAAU,KAAK;AAAA,EAC9B;AACF;AAEA,SAAS,SAAiB;AACxB,QAAM,MAAM,QAAQ,IAAI,SAAS;AACjC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,kBAAkB;AAC5C,SAAO;AACT;AAEA,eAAsB,kBAAkB,MAAc,QAAQ,eAAgC;AAC5F,UAAQ,IAAI,0CAA0C;AACtD,QAAM,MAAM,MAAM,YAAY,WAAW,EAAE,MAAM,OAAO,mBAAmB,KAAM,GAAG,OAAO,CAAC;AAC5F,SAAO,IAAI,MAAM;AACnB;AAEA,eAAsB,mBAAmB,UAAkB,MAAc,iBAA0C;AACjH,UAAQ,IAAI,4DAA4D;AACxE,QAAM,MAAM,MAAM,YAAY,eAAe;AAAA,IAC3C,WAAiB;AAAA,IACjB,QAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,UAAiB;AAAA,IACjB,cAAiB;AAAA,EACnB,GAAG,OAAO,CAAC;AACX,SAAO,IAAI,MAAM;AACnB;;;ACrDA,SAAS,gBAAgB;AACzB,SAAS,eAAe,iBAAiB;AACzC,SAAS,YAAY;AACrB,SAAS,WAAW;AAEpB,eAAe,SAAS,KAAa,UAAiC;AACpE,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,MAAM,GAAG,EAAE;AACtE,gBAAc,UAAU,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC;AAC9D;AAEA,eAAsB,iBAAiB,UAAkB,UAAkB,QAAiC;AAC1G,YAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,KAAO,KAAK,IAAI;AACtB,QAAM,KAAO,KAAK,QAAQ,SAAS,EAAE,MAAM;AAC3C,QAAM,KAAO,KAAK,QAAQ,SAAS,EAAE,MAAM;AAC3C,QAAM,MAAO,KAAK,QAAQ,YAAY,EAAE,MAAM;AAE9C,UAAQ,IAAI,mCAAmC;AAC/C,QAAM,QAAQ,IAAI,CAAC,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE,CAAC,CAAC;AAElE,UAAQ,IAAI,+BAA+B;AAC3C;AAAA,IACE,cAAc,EAAE,SAAS,EAAE,sEAAsE,GAAG;AAAA,EACtG;AACA,SAAO;AACT;AAEA,eAAsB,YAAY,WAAoC;AACpE,QAAM,EAAE,cAAAC,cAAa,IAAI,MAAM,OAAO,IAAS;AAC/C,QAAM,OAAO,IAAI,KAAK,CAACA,cAAa,SAAS,CAAC,GAAG,EAAE,MAAM,YAAY,CAAC;AACtE,QAAM,MAAO,MAAM,IAAI,QAAQ,OAAO,IAAI;AAC1C,UAAQ,IAAI,iCAAiC,GAAG;AAChD,SAAO;AACT;AAEA,eAAsB,iBACpB,WACA,cACA,QACiB;AACjB,QAAM,KAAM,KAAK,IAAI;AACrB,QAAM,MAAM,KAAK,QAAQ,aAAa,EAAE,MAAM;AAC9C,QAAM,MAAM,KAAK,QAAQ,SAAS,EAAE,MAAM;AAE1C,UAAQ,IAAI,uCAAuC;AACnD,QAAM,SAAS,cAAc,GAAG;AAEhC,UAAQ,IAAI,wDAAwD;AACpE;AAAA,IACE,cAAc,SAAS,SAAS,GAAG,yIAEE,GAAG;AAAA,EAC1C;AACA,SAAO;AACT;;;AHvBA,IAAM,MAAM;AACZ,IAAM,MAAM;AAEZ,SAAS,WAAW,QAAgB,MAAuB,MAAe,UAAmB;AAC3F,SAAO;AAAA,IACL;AAAA,IACA,YAAgB,KAAK,cAAc;AAAA,IACnC,UAAgB,KAAK,uBAAuB;AAAA,IAC5C,cAAgB,KAAK,eAAe;AAAA,IACpC,gBAAgB;AAAA,IAChB,GAAI,SAAa,SAAY,EAAE,KAAK,IAAkB,CAAC;AAAA,IACvD,GAAI,aAAa,SAAY,EAAE,WAAW,SAAS,IAAI,CAAC;AAAA,EAC1D;AACF;AAEA,eAAe,SAAS,OAAe,OAAsD;AAC3F,QAAM,EAAE,WAAW,IAAI,MAAMC,KAAI,MAAM,OAAO,OAAO,EAAE,MAAM,CAAC;AAC9D,UAAQ,IAAI,mBAAmB,KAAK,WAAM,UAAU,EAAE;AACtD,SAAO,MAAM;AACX,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAI,CAAC;AAC1C,UAAM,IAAI,MAAMA,KAAI,MAAM,OAAO,OAAO,EAAE,WAAW,YAAY,MAAM,MAAM,CAAC;AAC9E,YAAQ,IAAI,SAAS,UAAU,WAAM,EAAE,MAAM,EAAE;AAC/C,QAAK,EAAE,WAAsB,SAAU,OAAM,IAAI,MAAM,WAAW,UAAU,SAAS;AACrF,QAAK,EAAE,WAAsB,YAAa;AAC1C,UAAM,SAAS,MAAMA,KAAI,MAAM,OAAO,OAAO,EAAE,WAAW,WAAW,CAAC;AACtE,WAAO,OAAO;AAAA,EAChB;AACF;AAEA,eAAe,iBAAiB,UAAkB,QAAiC;AACjF,QAAM,KAAU,KAAK,IAAI;AACzB,QAAM,UAAUC,MAAK,QAAQ,aAAa,EAAE,MAAM;AAClD,QAAM,UAAUA,MAAK,QAAQ,cAAc,EAAE,MAAM;AAEnD,QAAM,MAAM,MAAM,MAAM,QAAQ;AAChC,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,GAAG;AACxE,EAAAC,eAAc,SAAS,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC;AAE3D,MAAI;AACF,IAAAC,UAAS,0BAA0B,OAAO,oBAAoB,OAAO,mBAAmB;AAAA,EAC1F,UAAE;AACA,QAAI;AAAE,iBAAW,OAAO;AAAA,IAAE,QAAQ;AAAA,IAAC;AAAA,EACrC;AACA,SAAO;AACT;AAEO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YAAY,QAAiB;AAC3B,UAAM,MAAM,UAAU,QAAQ,IAAI,SAAS;AAC3C,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,qBAAqB;AAC/C,IAAAH,KAAI,OAAO,EAAE,aAAa,IAAI,CAAC;AAAA,EACjC;AAAA,EAEA,MAAM,iBACJ,UACA,QACA,OAA4B,CAAC,GACJ;AACzB,UAAM,SAAS,KAAK,aAAaC,MAAK,OAAO,GAAG,aAAa,KAAK,IAAI,CAAC,EAAE;AACzE,IAAAG,WAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAErC,YAAQ,IAAI,4CAA4C;AACxD,UAAM,UAAU,MAAM,iBAAiB,UAAU,MAAM;AACvD,YAAQ,IAAI,gBAAgB,QAAQ,SAAS;AAC7C,YAAQ,IAAI,iBAAiB,QAAQ,SAAS;AAE9C,YAAQ,IAAI,8CAA8C;AAC1D,UAAM,UAAU,MAAM,SAAS,KAAK,WAAW,QAAQ,OAAO,MAAM,KAAK,IAAI,CAAC;AAE9E,YAAQ,IAAI,sDAAiD;AAC7D,UAAM,UAAW,MAAM,iBAAiB,QAAQ,MAAM,KAAK,MAAM;AACjE,UAAM,YAAY,IAAI,KAAK,CAAC,aAAa,OAAO,CAAC,GAAG,EAAE,MAAM,aAAa,CAAC;AAC1E,UAAM,WAAY,MAAMJ,KAAI,QAAQ,OAAO,SAAS;AACpD,QAAI;AAAE,iBAAW,OAAO;AAAA,IAAE,QAAQ;AAAA,IAAC;AAEnC,YAAQ,IAAI,+DAA+D;AAC3E,UAAM,QAAU,KAAK,SAAS,SAAY,KAAK,OAAO,IAAI;AAC1D,UAAM,UAAU,MAAM,SAAS,KAAK,WAAW,QAAQ,OAAO,MAAM,OAAO,QAAQ,CAAC;AAEpF,YAAQ,IAAI,kEAAkE;AAC9E,UAAM,CAAC,cAAc,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,MACrD,iBAAiB,QAAQ,MAAM,KAAK,QAAQ,MAAM,KAAK,MAAM;AAAA,MAC7D,kBAAkB,QAAQ,WAAW,KAAK,QAAQ;AAAA,IACpD,CAAC;AAED,YAAQ,IAAI,mDAAmD;AAC/D,UAAM,cAAqB,MAAM,YAAY,YAAY;AACzD,UAAM,iBAAsB,KAAK,uBAAuB,KAAK;AAC7D,UAAM,oBAAqB,MAAM,mBAAmB,aAAa,QAAQ,WAAW,aAAa;AAEjG,YAAQ,IAAI,gDAAgD;AAC5D,UAAM,qBAAqBC,MAAK,QAAQ,iBAAiB,KAAK,IAAI,CAAC,MAAM;AACzE,UAAM,QAAQ,MAAM,MAAM,iBAAiB;AAC3C,IAAAC,eAAc,oBAAoB,OAAO,KAAK,MAAM,MAAM,YAAY,CAAC,CAAC;AACxE,UAAM,iBAAiB,MAAM,iBAAiB,oBAAoB,cAAc,MAAM;AAEtF,WAAO;AAAA,MACL,UAAgB,QAAQ,MAAM;AAAA,MAC9B,UAAgB,QAAQ,MAAM;AAAA,MAC9B;AAAA,MACA,MAAgB,QAAQ;AAAA,MACxB,aAAgB,QAAQ;AAAA,MACxB,aAAgB,QAAQ;AAAA,MACxB,WAAgB,QAAQ;AAAA,MACxB,WAAgB,QAAQ;AAAA,IAC1B;AAAA,EACF;AACF;","names":["execSync","writeFileSync","mkdirSync","join","fal","readFileSync","fal","join","writeFileSync","execSync","mkdirSync"]}