mcp-scraper 0.3.47 → 0.4.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 (50) hide show
  1. package/dist/bin/api-server.cjs +2756 -334
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +3 -3
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +2092 -20
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +9 -4
  13. package/dist/bin/mcp-stdio-server.js.map +1 -1
  14. package/dist/bin/paa-harvest.js +3 -2
  15. package/dist/bin/paa-harvest.js.map +1 -1
  16. package/dist/{chunk-A6BAV444.js → chunk-3NIPPJGP.js} +2176 -511
  17. package/dist/chunk-3NIPPJGP.js.map +1 -0
  18. package/dist/chunk-3UH3BEIZ.js +158 -0
  19. package/dist/chunk-3UH3BEIZ.js.map +1 -0
  20. package/dist/{chunk-7TFJJAPE.js → chunk-6TDUY7CA.js} +2 -157
  21. package/dist/chunk-6TDUY7CA.js.map +1 -0
  22. package/dist/chunk-GWRIO6JT.js +419 -0
  23. package/dist/chunk-GWRIO6JT.js.map +1 -0
  24. package/dist/{chunk-RXNHY3TF.js → chunk-HBC4C75S.js} +11 -9
  25. package/dist/{chunk-RXNHY3TF.js.map → chunk-HBC4C75S.js.map} +1 -1
  26. package/dist/chunk-HPV4VOQX.js +27 -0
  27. package/dist/chunk-HPV4VOQX.js.map +1 -0
  28. package/dist/chunk-IXDYL7TE.js +7 -0
  29. package/dist/chunk-IXDYL7TE.js.map +1 -0
  30. package/dist/chunk-JOEZJL4I.js +130 -0
  31. package/dist/chunk-JOEZJL4I.js.map +1 -0
  32. package/dist/{chunk-3PRO376E.js → chunk-M2S27J6Z.js} +1 -25
  33. package/dist/chunk-M2S27J6Z.js.map +1 -0
  34. package/dist/extract-bundle-UKE273TV.js +276 -0
  35. package/dist/extract-bundle-UKE273TV.js.map +1 -0
  36. package/dist/index.js +3 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/{server-H6GAAEQ6.js → server-Z4WKTE6F.js} +136 -188
  39. package/dist/server-Z4WKTE6F.js.map +1 -0
  40. package/dist/site-extract-repository-2MQBAOX5.js +30 -0
  41. package/dist/site-extract-repository-2MQBAOX5.js.map +1 -0
  42. package/dist/{worker-WY7C45MP.js → worker-RVZXIALX.js} +8 -5
  43. package/dist/{worker-WY7C45MP.js.map → worker-RVZXIALX.js.map} +1 -1
  44. package/package.json +2 -1
  45. package/dist/chunk-3PRO376E.js.map +0 -1
  46. package/dist/chunk-7TFJJAPE.js.map +0 -1
  47. package/dist/chunk-A6BAV444.js.map +0 -1
  48. package/dist/chunk-NTCAVBHU.js +0 -7
  49. package/dist/chunk-NTCAVBHU.js.map +0 -1
  50. package/dist/server-H6GAAEQ6.js.map +0 -1
package/dist/index.js.map CHANGED
@@ -1 +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, extractEpisodePrompts } from './promptBuilder.js'\nimport type { EpisodeBrief } from './episodeBrief.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}\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),\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 async generateEpisode(\n brief: EpisodeBrief,\n opts: ClipPairOptions = {},\n ): Promise<ClipPairResult> {\n const outDir = opts.outputDir ?? join(tmpdir(), `episode-${brief.episodeNumber}-${Date.now()}`)\n mkdirSync(outDir, { recursive: true })\n\n const prompts = extractEpisodePrompts(brief)\n console.log(`\\n[Episode ${brief.episodeNumber}/${brief.episodeCount}] ${brief.sectionTitle}`)\n console.log(' Voiceover:', prompts.voiceover)\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),\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\nexport interface EpisodeBriefInput {\n storyMoment: string\n characterName: string\n sectionTitle: string\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\nexport function extractEpisodePrompts(brief: EpisodeBriefInput): ClipPromptPair {\n if (!brief.clip1 || !brief.clip2 || !brief.voiceover || !brief.audioMood) {\n throw new Error('Episode brief is missing prompt fields — run blog-to-video skill to regenerate')\n }\n return { clip1: brief.clip1, clip2: brief.clip2, voiceover: brief.voiceover, audioMood: brief.audioMood }\n}\n\nconst EPISODE_SYSTEM_PROMPT = `You are a video prompt engineer for Seedance 2.0 text-to-video AI.\n\nYou are working with a character-driven narrative brief. The clips must feel like a continuous 16-second moment in one person's story — not an explainer video.\n\nIMPORTANT TECHNICAL CONTEXT: Clip 2 will start from the exact last frame of Clip 1. Both clips must show the same person, same location, same lighting. Clip 2 continues motion from Clip 1's frozen ending frame.\n\nUse the Kling prompt format for every clip:\n[Shot type] of [character name] [action]. The scene starts at [keyframe 1]. Camera Motion: [motion] ending in [keyframe 2]. Lighting: [style]. Style: [style].\n\nShot types: Close-up, Medium shot, Over-the-shoulder, Wide shot, Tracking shot.\nCamera motions: Slow push in, Slow pull back, Rack focus, Static hold, Subtle drift right/left, Pan left/right.\n\nRules for visuals:\n- No text, no captions, no UI overlays, no readable code on screen\n- Photorealistic, specific objects, natural-feeling details\n- Keyframe 2 (clip 1's end) must be a specific held moment clip 2 continues from\n\nvoiceover: The character's internal tension, in 22 words maximum. Not a summary. Count every word.\naudioMood: Specific instruments + tempo + emotional register + \"no vocals, no lyrics\".\n\nRespond with JSON only: {\"clip1\": \"...\", \"clip2\": \"...\", \"voiceover\": \"...\", \"audioMood\": \"...\"}`\n\nasync function callEpisodeLLM(apiKey: string, baseUrl: string, brief: EpisodeBriefInput): Promise<ClipPromptPair> {\n const userContent = `Character: ${brief.characterName}\nSection: ${brief.sectionTitle}\nStory moment: ${brief.storyMoment}\n\nExisting clip1 (refine if needed): ${brief.clip1}\nExisting clip2 (refine if needed): ${brief.clip2}\nExisting voiceover: ${brief.voiceover}\nExisting audioMood: ${brief.audioMood}\n\nProduce refined Kling-format prompts that follow all rules. Keep what's good, fix what violates the format.`\n\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.5,\n messages: [\n { role: 'system', content: EPISODE_SYSTEM_PROMPT },\n { role: 'user', content: userContent },\n ],\n }),\n })\n if (!res.ok) throw new Error(`LLM episode 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 episode 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(`Episode response missing fields: ${raw.slice(0, 200)}`)\n }\n return parsed as ClipPromptPair\n}\n\nexport async function regenerateEpisodePrompts(brief: EpisodeBriefInput): 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 callEpisodeLLM(deepinfraKey, DEEPINFRA_URL, brief)\n } catch (err) {\n console.warn('[promptBuilder] DeepInfra failed, trying OpenRouter:', (err as Error).message)\n }\n }\n if (openrouterKey) {\n return await callEpisodeLLM(openrouterKey, OPENROUTER_URL, brief)\n }\n throw new Error('No LLM key — set DEEPINFRA_API_KEY or OPENROUTER_API_KEY')\n}\n","import { writeFileSync, mkdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { fal } from '@fal-ai/client'\n\nconst MMAUDIO_MODEL = 'fal-ai/mmaudio-v2'\nconst ELEVENLABS_MODEL = 'fal-ai/elevenlabs/tts'\nconst GEMINI_TTS_MODEL = 'fal-ai/google/gemini-2.5-flash-preview-tts'\n\ninterface VideoOutput { video: { url: string } }\ninterface TTSOutput { audio: { url: string } }\n\nasync function downloadAudio(url: string): Promise<Buffer> {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`Failed to download TTS audio (${res.status})`)\n return Buffer.from(await res.arrayBuffer())\n}\n\nexport async function generateVoiceover(text: string): Promise<string> {\n console.log('[AudioGenerator] Generating voiceover...')\n\n const outDir = join(tmpdir(), `tts-${Date.now()}`)\n mkdirSync(outDir, { recursive: true })\n const outPath = join(outDir, 'voiceover.mp3')\n\n try {\n const voiceId = process.env['ELEVENLABS_VOICE_ID'] ?? 'pNInz6obpgDQGcFmaJgB'\n const result = await fal.run(ELEVENLABS_MODEL, {\n input: { text, voice_id: voiceId, model_id: 'eleven_v3' },\n }) as unknown as TTSOutput\n writeFileSync(outPath, await downloadAudio(result.audio.url))\n console.log('[AudioGenerator] TTS: ElevenLabs via fal')\n return outPath\n } catch (err) {\n console.warn('[AudioGenerator] ElevenLabs via fal failed, trying Gemini:', (err as Error).message)\n }\n\n const voice = process.env['GEMINI_TTS_VOICE'] ?? 'Kore'\n const result = await fal.run(GEMINI_TTS_MODEL, { input: { text, voice } }) as unknown as TTSOutput\n writeFileSync(outPath, await downloadAudio(result.audio.url))\n console.log('[AudioGenerator] TTS: Gemini via fal')\n return outPath\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 result = await fal.run(MMAUDIO_MODEL, {\n input: {\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 },\n }) as unknown as VideoOutput\n return result.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":"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","names":["execSync","writeFileSync","mkdirSync","tmpdir","join","fal","result","writeFileSync","mkdirSync","join","fal","readFileSync","fal","join","writeFileSync","execSync","tmpdir","mkdirSync"]}
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, extractEpisodePrompts } from './promptBuilder.js'\nimport type { EpisodeBrief } from './episodeBrief.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}\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),\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 async generateEpisode(\n brief: EpisodeBrief,\n opts: ClipPairOptions = {},\n ): Promise<ClipPairResult> {\n const outDir = opts.outputDir ?? join(tmpdir(), `episode-${brief.episodeNumber}-${Date.now()}`)\n mkdirSync(outDir, { recursive: true })\n\n const prompts = extractEpisodePrompts(brief)\n console.log(`\\n[Episode ${brief.episodeNumber}/${brief.episodeCount}] ${brief.sectionTitle}`)\n console.log(' Voiceover:', prompts.voiceover)\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),\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\nexport interface EpisodeBriefInput {\n storyMoment: string\n characterName: string\n sectionTitle: string\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\nexport function extractEpisodePrompts(brief: EpisodeBriefInput): ClipPromptPair {\n if (!brief.clip1 || !brief.clip2 || !brief.voiceover || !brief.audioMood) {\n throw new Error('Episode brief is missing prompt fields — run blog-to-video skill to regenerate')\n }\n return { clip1: brief.clip1, clip2: brief.clip2, voiceover: brief.voiceover, audioMood: brief.audioMood }\n}\n\nconst EPISODE_SYSTEM_PROMPT = `You are a video prompt engineer for Seedance 2.0 text-to-video AI.\n\nYou are working with a character-driven narrative brief. The clips must feel like a continuous 16-second moment in one person's story — not an explainer video.\n\nIMPORTANT TECHNICAL CONTEXT: Clip 2 will start from the exact last frame of Clip 1. Both clips must show the same person, same location, same lighting. Clip 2 continues motion from Clip 1's frozen ending frame.\n\nUse the Kling prompt format for every clip:\n[Shot type] of [character name] [action]. The scene starts at [keyframe 1]. Camera Motion: [motion] ending in [keyframe 2]. Lighting: [style]. Style: [style].\n\nShot types: Close-up, Medium shot, Over-the-shoulder, Wide shot, Tracking shot.\nCamera motions: Slow push in, Slow pull back, Rack focus, Static hold, Subtle drift right/left, Pan left/right.\n\nRules for visuals:\n- No text, no captions, no UI overlays, no readable code on screen\n- Photorealistic, specific objects, natural-feeling details\n- Keyframe 2 (clip 1's end) must be a specific held moment clip 2 continues from\n\nvoiceover: The character's internal tension, in 22 words maximum. Not a summary. Count every word.\naudioMood: Specific instruments + tempo + emotional register + \"no vocals, no lyrics\".\n\nRespond with JSON only: {\"clip1\": \"...\", \"clip2\": \"...\", \"voiceover\": \"...\", \"audioMood\": \"...\"}`\n\nasync function callEpisodeLLM(apiKey: string, baseUrl: string, brief: EpisodeBriefInput): Promise<ClipPromptPair> {\n const userContent = `Character: ${brief.characterName}\nSection: ${brief.sectionTitle}\nStory moment: ${brief.storyMoment}\n\nExisting clip1 (refine if needed): ${brief.clip1}\nExisting clip2 (refine if needed): ${brief.clip2}\nExisting voiceover: ${brief.voiceover}\nExisting audioMood: ${brief.audioMood}\n\nProduce refined Kling-format prompts that follow all rules. Keep what's good, fix what violates the format.`\n\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.5,\n messages: [\n { role: 'system', content: EPISODE_SYSTEM_PROMPT },\n { role: 'user', content: userContent },\n ],\n }),\n })\n if (!res.ok) throw new Error(`LLM episode 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 episode 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(`Episode response missing fields: ${raw.slice(0, 200)}`)\n }\n return parsed as ClipPromptPair\n}\n\nexport async function regenerateEpisodePrompts(brief: EpisodeBriefInput): 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 callEpisodeLLM(deepinfraKey, DEEPINFRA_URL, brief)\n } catch (err) {\n console.warn('[promptBuilder] DeepInfra failed, trying OpenRouter:', (err as Error).message)\n }\n }\n if (openrouterKey) {\n return await callEpisodeLLM(openrouterKey, OPENROUTER_URL, brief)\n }\n throw new Error('No LLM key — set DEEPINFRA_API_KEY or OPENROUTER_API_KEY')\n}\n","import { writeFileSync, mkdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { fal } from '@fal-ai/client'\n\nconst MMAUDIO_MODEL = 'fal-ai/mmaudio-v2'\nconst ELEVENLABS_MODEL = 'fal-ai/elevenlabs/tts'\nconst GEMINI_TTS_MODEL = 'fal-ai/google/gemini-2.5-flash-preview-tts'\n\ninterface VideoOutput { video: { url: string } }\ninterface TTSOutput { audio: { url: string } }\n\nasync function downloadAudio(url: string): Promise<Buffer> {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`Failed to download TTS audio (${res.status})`)\n return Buffer.from(await res.arrayBuffer())\n}\n\nexport async function generateVoiceover(text: string): Promise<string> {\n console.log('[AudioGenerator] Generating voiceover...')\n\n const outDir = join(tmpdir(), `tts-${Date.now()}`)\n mkdirSync(outDir, { recursive: true })\n const outPath = join(outDir, 'voiceover.mp3')\n\n try {\n const voiceId = process.env['ELEVENLABS_VOICE_ID'] ?? 'pNInz6obpgDQGcFmaJgB'\n const result = await fal.run(ELEVENLABS_MODEL, {\n input: { text, voice_id: voiceId, model_id: 'eleven_v3' },\n }) as unknown as TTSOutput\n writeFileSync(outPath, await downloadAudio(result.audio.url))\n console.log('[AudioGenerator] TTS: ElevenLabs via fal')\n return outPath\n } catch (err) {\n console.warn('[AudioGenerator] ElevenLabs via fal failed, trying Gemini:', (err as Error).message)\n }\n\n const voice = process.env['GEMINI_TTS_VOICE'] ?? 'Kore'\n const result = await fal.run(GEMINI_TTS_MODEL, { input: { text, voice } }) as unknown as TTSOutput\n writeFileSync(outPath, await downloadAudio(result.audio.url))\n console.log('[AudioGenerator] TTS: Gemini via fal')\n return outPath\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 result = await fal.run(MMAUDIO_MODEL, {\n input: {\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 },\n }) as unknown as VideoOutput\n return result.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":"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","names":["execSync","writeFileSync","mkdirSync","tmpdir","join","fal","result","writeFileSync","mkdirSync","join","fal","readFileSync","fal","join","writeFileSync","execSync","tmpdir","mkdirSync"]}
@@ -1,24 +1,47 @@
1
+ import {
2
+ classifyHarvestProblem,
3
+ createHarvestAttemptRecorder,
4
+ harvestProblemResponse,
5
+ serializeHarvestProblem
6
+ } from "./chunk-3UH3BEIZ.js";
7
+ import {
8
+ BrowserDriver,
9
+ MapsSelectors,
10
+ buildYouTubeChannelVideosUrl,
11
+ currentCostContext,
12
+ deleteKernelProxyId,
13
+ harvest,
14
+ migrateCostTelemetry,
15
+ recordKernelSession,
16
+ recordVendorUsage,
17
+ resolveKernelProxyId,
18
+ runWithCostContext
19
+ } from "./chunk-HBC4C75S.js";
1
20
  import {
2
21
  HttpMcpToolExecutor,
3
- auditImages,
4
- buildLinkGraph,
5
- buildLinkReport,
22
+ MemoryMcpToolExecutor,
6
23
  buildPaaExtractorMcpServer,
7
- computeIssues,
8
24
  configureReportSaving,
9
25
  exportFanout,
10
- getBlobStore,
11
26
  harvestTimeoutBudget,
12
27
  hashOwnerId,
13
28
  outputBaseDir,
14
29
  registerBrowserAgentMcpTools,
30
+ registerMemoryMcpTools,
15
31
  registerSerpIntelligenceCaptureTools,
16
- renderImageSection,
17
- renderIssueReport,
18
- renderLinkReport,
19
32
  sanitizeAttempts,
20
33
  sanitizeHarvestResult
21
- } from "./chunk-A6BAV444.js";
34
+ } from "./chunk-3NIPPJGP.js";
35
+ import {
36
+ auditImages,
37
+ getBlobStore
38
+ } from "./chunk-GWRIO6JT.js";
39
+ import {
40
+ browserServiceApiKey,
41
+ browserServiceProfileName,
42
+ browserServiceProfileSaveChanges,
43
+ browserServiceProxyId
44
+ } from "./chunk-HPV4VOQX.js";
22
45
  import {
23
46
  csvRecords,
24
47
  listWorkflowDefinitions,
@@ -31,7 +54,29 @@ import {
31
54
  workflowStepCount,
32
55
  workflowSupportsSteps
33
56
  } from "./chunk-NNEIXK5L.js";
34
- import "./chunk-NTCAVBHU.js";
57
+ import "./chunk-K443GQY5.js";
58
+ import {
59
+ DEFAULT_MAPS_PROXY_MODE,
60
+ DEFAULT_PROXY_MODE,
61
+ MapsPlaceOptionsSchema,
62
+ MapsSearchOptionsSchema,
63
+ RawMapsAboutAttributeSchema,
64
+ RawMapsHoursRowSchema,
65
+ RawMapsOverviewSchema,
66
+ RawMapsReviewStatsSchema
67
+ } from "./chunk-BP27CZ5Q.js";
68
+ import "./chunk-IXDYL7TE.js";
69
+ import {
70
+ completeExtractJob,
71
+ countSuccessfulPages,
72
+ createExtractJob,
73
+ failExtractJob,
74
+ getExtractJob,
75
+ getExtractedPages,
76
+ saveExtractPages,
77
+ setExtractJobTotal,
78
+ settleExtractJob
79
+ } from "./chunk-JOEZJL4I.js";
35
80
  import {
36
81
  BROWSER_OPEN_MIN_BALANCE_MC,
37
82
  CONCURRENCY_PRICE_ID,
@@ -48,46 +93,14 @@ import {
48
93
  SUBSCRIPTION_TIERS,
49
94
  SUBSCRIPTION_TIER_BY_KEY,
50
95
  browserActiveCostMc,
51
- classifyHarvestProblem,
52
96
  concurrencySlotBillingInfo,
53
- createHarvestAttemptRecorder,
54
- harvestProblemResponse,
55
- insufficientBalanceResponse,
56
- serializeHarvestProblem
57
- } from "./chunk-7TFJJAPE.js";
58
- import {
59
- BrowserDriver,
60
- MapsSelectors,
61
- buildYouTubeChannelVideosUrl,
62
- currentCostContext,
63
- deleteKernelProxyId,
64
- harvest,
65
- migrateCostTelemetry,
66
- recordKernelSession,
67
- recordVendorUsage,
68
- resolveKernelProxyId,
69
- runWithCostContext
70
- } from "./chunk-RXNHY3TF.js";
97
+ insufficientBalanceResponse
98
+ } from "./chunk-6TDUY7CA.js";
71
99
  import {
72
100
  CaptchaError,
73
101
  RECAPTCHA_INSTRUCTIONS,
74
- browserServiceApiKey,
75
- browserServiceProfileName,
76
- browserServiceProfileSaveChanges,
77
- browserServiceProxyId,
78
102
  sanitizeVendorName
79
- } from "./chunk-3PRO376E.js";
80
- import "./chunk-K443GQY5.js";
81
- import {
82
- DEFAULT_MAPS_PROXY_MODE,
83
- DEFAULT_PROXY_MODE,
84
- MapsPlaceOptionsSchema,
85
- MapsSearchOptionsSchema,
86
- RawMapsAboutAttributeSchema,
87
- RawMapsHoursRowSchema,
88
- RawMapsOverviewSchema,
89
- RawMapsReviewStatsSchema
90
- } from "./chunk-BP27CZ5Q.js";
103
+ } from "./chunk-M2S27J6Z.js";
91
104
  import {
92
105
  SiteAuditJobRowSchema,
93
106
  advanceWorkflowRunStep,
@@ -7422,135 +7435,8 @@ var siteAuditFn = inngest.createFunction(
7422
7435
  }
7423
7436
  );
7424
7437
 
7425
- // src/api/site-extract-repository.ts
7426
- function rowToJob(r) {
7427
- return {
7428
- id: String(r.id),
7429
- userId: r.user_id != null ? Number(r.user_id) : null,
7430
- status: r.status != null ? String(r.status) : "pending",
7431
- startUrl: String(r.start_url ?? ""),
7432
- options: r.options ? JSON.parse(String(r.options)) : {},
7433
- totalUrls: Number(r.total_urls ?? 0),
7434
- doneUrls: Number(r.done_urls ?? 0),
7435
- artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,
7436
- error: r.error != null ? String(r.error) : null,
7437
- billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,
7438
- createdAt: String(r.created_at ?? ""),
7439
- updatedAt: String(r.updated_at ?? "")
7440
- };
7441
- }
7442
- async function createExtractJob(jobId, userId, startUrl, options) {
7443
- const db = getDb();
7444
- await db.execute({
7445
- sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)
7446
- VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,
7447
- args: [jobId, userId, startUrl, JSON.stringify(options)]
7448
- });
7449
- }
7450
- async function getExtractJob(jobId) {
7451
- const db = getDb();
7452
- const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] });
7453
- return res.rows[0] ? rowToJob(res.rows[0]) : null;
7454
- }
7455
- async function setExtractJobTotal(jobId, totalUrls) {
7456
- const db = getDb();
7457
- await db.execute({
7458
- sql: `UPDATE site_extract_jobs SET status = 'running', total_urls = ?, updated_at = datetime('now') WHERE id = ?`,
7459
- args: [totalUrls, jobId]
7460
- });
7461
- }
7462
- async function saveExtractPages(jobId, pages) {
7463
- if (pages.length === 0) return;
7464
- const db = getDb();
7465
- await db.batch(
7466
- pages.map((p) => ({
7467
- sql: `INSERT OR REPLACE INTO site_extract_pages (job_id, url, page) VALUES (?, ?, ?)`,
7468
- args: [jobId, p.url, JSON.stringify({ ...p, bodyMarkdown: "", schema: [] })]
7469
- }))
7470
- );
7471
- await db.execute({
7472
- sql: `UPDATE site_extract_jobs
7473
- SET done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?), updated_at = datetime('now')
7474
- WHERE id = ?`,
7475
- args: [jobId, jobId]
7476
- });
7477
- }
7478
- async function getExtractedPages(jobId) {
7479
- const db = getDb();
7480
- const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
7481
- return res.rows.map((r) => JSON.parse(String(r.page)));
7482
- }
7483
- async function countSuccessfulPages(jobId) {
7484
- const db = getDb();
7485
- const res = await db.execute({
7486
- sql: `SELECT COUNT(*) AS n FROM site_extract_pages
7487
- WHERE job_id = ? AND json_extract(page, '$.status') = 200 AND json_extract(page, '$.wordCount') > 0`,
7488
- args: [jobId]
7489
- });
7490
- return Number(res.rows[0]?.n ?? 0);
7491
- }
7492
- async function completeExtractJob(jobId, artifacts) {
7493
- const db = getDb();
7494
- await db.execute({
7495
- sql: `UPDATE site_extract_jobs SET status = 'complete', artifacts = ?, updated_at = datetime('now') WHERE id = ?`,
7496
- args: [JSON.stringify(artifacts ?? []), jobId]
7497
- });
7498
- }
7499
- async function failExtractJob(jobId, error) {
7500
- const db = getDb();
7501
- await db.execute({
7502
- sql: `UPDATE site_extract_jobs SET status = 'failed', error = ?, updated_at = datetime('now') WHERE id = ?`,
7503
- args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
7504
- });
7505
- }
7506
- async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
7507
- const db = getDb();
7508
- const job = await getExtractJob(jobId);
7509
- if (!job || job.billedMc != null) return;
7510
- if (refundMc <= 0) {
7511
- await db.execute({ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] });
7512
- return;
7513
- }
7514
- await db.batch([
7515
- { sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [refundMc, userId] },
7516
- { sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)", args: [userId, refundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference] },
7517
- { sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] }
7518
- ]);
7519
- }
7520
-
7521
7438
  // src/inngest/functions/site-extract.ts
7522
7439
  var BATCH = 30;
7523
- function buildArtifacts(job, pages, branding, imageAudit) {
7524
- const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
7525
- const issues = computeIssues(pages, metrics);
7526
- const linkReport = buildLinkReport(edges, [...metrics.values()], job.startUrl);
7527
- let reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
7528
- if (imageAudit) reportMd += `
7529
-
7530
- ${renderImageSection(imageAudit)}`;
7531
- const pageRows = pages.map((p) => {
7532
- const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
7533
- const m = metrics.get(p.url);
7534
- return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
7535
- });
7536
- const jsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
7537
- const files = [
7538
- { name: "report.md", body: reportMd, type: "text/markdown" },
7539
- { name: "issues.json", body: JSON.stringify(issues, null, 2), type: "application/json" },
7540
- { name: "pages.jsonl", body: jsonl(pageRows), type: "application/x-ndjson" },
7541
- { name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
7542
- { name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" },
7543
- { name: "link-report.md", body: renderLinkReport(linkReport), type: "text/markdown" },
7544
- { name: "links-summary.json", body: JSON.stringify(linkReport.summary, null, 2), type: "application/json" },
7545
- { name: "external-domains.json", body: JSON.stringify(linkReport.externalDomains, null, 2), type: "application/json" }
7546
- ];
7547
- if (imageAudit) {
7548
- files.push({ name: "images.jsonl", body: jsonl(imageAudit.rows), type: "application/x-ndjson" });
7549
- files.push({ name: "images-summary.json", body: JSON.stringify(imageAudit.summary, null, 2), type: "application/json" });
7550
- }
7551
- if (branding) files.push({ name: "branding.json", body: JSON.stringify(branding, null, 2), type: "application/json" });
7552
- return files;
7553
- }
7554
7440
  var siteExtractFn = inngest.createFunction(
7555
7441
  {
7556
7442
  id: "site-extract",
@@ -7565,7 +7451,9 @@ var siteExtractFn = inngest.createFunction(
7565
7451
  await settleExtractJob(jobId, current.userId, heldMc, 0, "crawl failed refund").catch(() => {
7566
7452
  });
7567
7453
  }
7568
- await failExtractJob(jobId, String(event?.data?.error?.message ?? "crawl failed")).catch(() => {
7454
+ const rawErr = String(event?.data?.error?.message ?? "crawl failed");
7455
+ const friendly = /FUNCTION_INVOCATION_FAILED|out of memory|heap/i.test(rawErr) ? "Result assembly failed on our side \u2014 your crawl data is safe and the job can be re-finalized without re-crawling. Contact support if this persists." : rawErr;
7456
+ await failExtractJob(jobId, friendly).catch(() => {
7569
7457
  });
7570
7458
  }
7571
7459
  },
@@ -7641,11 +7529,8 @@ var siteExtractFn = inngest.createFunction(
7641
7529
  return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
7642
7530
  }) : null;
7643
7531
  const artifacts = await step.run("finalize", async () => {
7644
- const pages = await getExtractedPages(jobId);
7645
- const store = getBlobStore();
7646
- const files = buildArtifacts(job, pages, branding, imageAudit);
7647
- const stored = [];
7648
- for (const f of files) stored.push(await store.put(`extract/${jobId}/${f.name}`, f.body, f.type));
7532
+ const { assembleExtractArtifacts } = await import("./extract-bundle-UKE273TV.js");
7533
+ const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
7649
7534
  await completeExtractJob(jobId, stored);
7650
7535
  return stored;
7651
7536
  });
@@ -11961,8 +11846,13 @@ function verifySession(token) {
11961
11846
  }
11962
11847
 
11963
11848
  // src/api/memory.ts
11964
- var MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://mcp-memory-omega.vercel.app").replace(/\/$/, "");
11849
+ var MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
11965
11850
  var ADMIN_KEY = () => process.env.MCP_MEMORY_ADMIN_KEY?.trim() ?? "";
11851
+ function parseMemoryRpcBody(text) {
11852
+ const dataLine = text.split("\n").find((line) => line.startsWith("data:"));
11853
+ const payload = dataLine ? dataLine.slice(5).trim() : text;
11854
+ return JSON.parse(payload);
11855
+ }
11966
11856
  function isMemoryOperator(email) {
11967
11857
  if (!email) return false;
11968
11858
  const ops = (process.env.MEMORY_OPERATOR_IDENTITIES ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
@@ -11990,15 +11880,34 @@ function decryptMemoryKey(stored) {
11990
11880
  }
11991
11881
  async function memoryCall(toolName, args, userMemoryKey) {
11992
11882
  try {
11993
- const res = await fetch(`${MEMORY_BASE_URL()}/api/mcp/memoryServer/tools/${toolName}/execute`, {
11883
+ const res = await fetch(`${MEMORY_BASE_URL()}/mcp`, {
11994
11884
  method: "POST",
11995
- headers: { "content-type": "application/json" },
11996
- body: JSON.stringify({ data: { ...args, apiKey: userMemoryKey } })
11885
+ headers: {
11886
+ "content-type": "application/json",
11887
+ accept: "application/json, text/event-stream",
11888
+ authorization: `Bearer ${userMemoryKey}`
11889
+ },
11890
+ body: JSON.stringify({
11891
+ jsonrpc: "2.0",
11892
+ id: 1,
11893
+ method: "tools/call",
11894
+ params: { name: toolName, arguments: args }
11895
+ })
11997
11896
  });
11998
- const body = await res.json().catch(() => null);
11999
- if (!res.ok) return { ok: false, error: body?.error ?? `memory ${toolName} failed (${res.status})` };
12000
- if (body?.result) return body.result;
12001
- return { ok: false, error: body?.error ?? `memory ${toolName} returned no result` };
11897
+ const text = await res.text();
11898
+ if (!res.ok) return { ok: false, error: `memory ${toolName} failed (${res.status})` };
11899
+ const body = parseMemoryRpcBody(text);
11900
+ if (body.error) return { ok: false, error: body.error.message ?? `memory ${toolName} failed` };
11901
+ if (body.result?.structuredContent) return body.result.structuredContent;
11902
+ const textContent = body.result?.content?.find((c) => c.type === "text")?.text;
11903
+ if (textContent) {
11904
+ try {
11905
+ return JSON.parse(textContent);
11906
+ } catch {
11907
+ return { ok: false, error: textContent };
11908
+ }
11909
+ }
11910
+ return { ok: false, error: `memory ${toolName} returned no result` };
12002
11911
  } catch (err) {
12003
11912
  return { ok: false, error: err?.message ?? "memory call failed" };
12004
11913
  }
@@ -15096,6 +15005,7 @@ mcpApp.all("/", async (c) => {
15096
15005
  const server = buildPaaExtractorMcpServer(executor, { savesReportsLocally: false, ownerId: hashOwnerId(callerKey) });
15097
15006
  registerSerpIntelligenceCaptureTools(server, executor);
15098
15007
  registerBrowserAgentMcpTools(server, { baseUrl, apiKey: callerKey, consoleBaseUrl, savesReportsLocally: false });
15008
+ registerMemoryMcpTools(server, new MemoryMcpToolExecutor(baseUrl, callerKey));
15099
15009
  await server.connect(transport);
15100
15010
  return transport.handleRequest(c.req.raw);
15101
15011
  } catch {
@@ -19484,6 +19394,15 @@ app.get("/memory/vaults", auth2, async (c) => {
19484
19394
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
19485
19395
  }
19486
19396
  });
19397
+ app.post("/memory/mcp-call", auth2, async (c) => {
19398
+ const user = c.get("user");
19399
+ const body = await c.req.json().catch(() => ({}));
19400
+ if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
19401
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19402
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19403
+ const result = await memoryCall(body.toolName, body.args ?? {}, key);
19404
+ return c.json(result);
19405
+ });
19487
19406
  app.get("/memory/approved-senders", auth2, async (c) => {
19488
19407
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19489
19408
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
@@ -20548,7 +20467,7 @@ app.get("/cron/tick", async (c) => {
20548
20467
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
20549
20468
  return c.json({ error: "Unauthorized" }, 401);
20550
20469
  }
20551
- const { drainQueue } = await import("./worker-WY7C45MP.js");
20470
+ const { drainQueue } = await import("./worker-RVZXIALX.js");
20552
20471
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
20553
20472
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
20554
20473
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -20560,6 +20479,35 @@ app.get("/cron/tick", async (c) => {
20560
20479
  ]);
20561
20480
  return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, expired: expiredResult, blobCleanup, workflowDispatch: workflowDispatchResult });
20562
20481
  });
20482
+ app.post("/api/internal/extract-refinalize/:id", async (c) => {
20483
+ const secret2 = c.req.header("authorization");
20484
+ if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
20485
+ return c.json({ error: "Unauthorized" }, 401);
20486
+ }
20487
+ const jobId = c.req.param("id");
20488
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-2MQBAOX5.js");
20489
+ const { assembleExtractArtifacts } = await import("./extract-bundle-UKE273TV.js");
20490
+ const job = await getExtractJob2(jobId);
20491
+ if (!job) return c.json({ error: "job not found" }, 404);
20492
+ const stored = await assembleExtractArtifacts(job, {});
20493
+ await completeExtractJob2(jobId, stored);
20494
+ let settlement = "already_settled_or_refunded";
20495
+ if (job.billedMc == null && job.userId != null) {
20496
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-2MQBAOX5.js");
20497
+ const heldMc = Number(job.options.heldMc ?? 0);
20498
+ const successful = await countSuccessfulPages2(jobId);
20499
+ const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
20500
+ let host = "site";
20501
+ try {
20502
+ host = new URL(job.startUrl).hostname;
20503
+ } catch {
20504
+ }
20505
+ await settleExtractJob2(jobId, job.userId, heldMc - usedMc, usedMc, host);
20506
+ settlement = `settled: billed ${usedMc} mc, refunded ${heldMc - usedMc} mc`;
20507
+ }
20508
+ const bundle = stored.find((a) => a.key.endsWith("bundle.zip"));
20509
+ return c.json({ ok: true, jobId, artifacts: stored.length, settlement, bundleUrl: bundle?.url ?? null, bundleBytes: bundle?.bytes ?? null });
20510
+ });
20563
20511
  app.on(["GET", "POST", "PUT"], "/api/inngest", serveInngest({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
20564
20512
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);
20565
20513
  app.route("/api/internal/memory", internalMemoryApp);
@@ -20701,4 +20649,4 @@ app.get("/blog/:slug/", (c) => {
20701
20649
  export {
20702
20650
  app
20703
20651
  };
20704
- //# sourceMappingURL=server-H6GAAEQ6.js.map
20652
+ //# sourceMappingURL=server-Z4WKTE6F.js.map