diff --git a/app/api/grok-chat/route.ts b/app/api/grok-chat/route.ts deleted file mode 100644 index b863a45..0000000 --- a/app/api/grok-chat/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const CRAWL_SERVICE_URL = 'http://127.0.0.1:8000'; - -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - const { message, history } = body; - - console.log(`[Grok API] Incoming body:`, JSON.stringify(body, null, 2)); - - const proxyPayload = { - message, - history, - cookies: body.cookies - }; - console.log(`[Grok API] Proxy payload:`, JSON.stringify(proxyPayload, null, 2)); - - const response = await fetch(`${CRAWL_SERVICE_URL}/grok/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(proxyPayload), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`[Grok API] Service error: ${response.status} ${errorText}`); - try { - const errorJson = JSON.parse(errorText); - return NextResponse.json(errorJson, { status: response.status }); - } catch { - return NextResponse.json( - { error: `Service error: ${response.status} - ${errorText}` }, - { status: response.status } - ); - } - } - - const data = await response.json(); - return NextResponse.json(data); - - } catch (error: any) { - console.error('[Grok API] Proxy error:', error); - return NextResponse.json( - { error: error.message || 'Internal Server Error' }, - { status: 500 } - ); - } -} diff --git a/app/api/grok-image/route.ts b/app/api/grok-image/route.ts new file mode 100644 index 0000000..ed2838a --- /dev/null +++ b/app/api/grok-image/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from 'next/server'; + +/** + * Grok Image Generation API + * Uses xLmiler/grok2api_python backend with OpenAI-compatible format + */ +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { prompt, numImages = 1, grokApiUrl, apiKey, sso } = body; + + if (!prompt) { + return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); + } + + if (!grokApiUrl) { + return NextResponse.json({ error: "Grok API URL not configured" }, { status: 400 }); + } + + console.log(`[Grok Image] Generating ${numImages} image(s) for: "${prompt.substring(0, 50)}..."`); + + // Call xLmiler backend using OpenAI-compatible format + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; + } + + const response = await fetch(`${grokApiUrl}/v1/chat/completions`, { + method: 'POST', + headers, + body: JSON.stringify({ + model: 'grok-4-imageGen', + messages: [ + { + role: 'user', + content: prompt + } + ], + // The xLmiler backend handles image generation via chat completions + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Grok Image] Error:', response.status, errorText); + return NextResponse.json( + { error: `Grok API Error: ${response.status} - ${errorText.substring(0, 200)}` }, + { status: response.status } + ); + } + + const data = await response.json(); + console.log('[Grok Image] Response:', JSON.stringify(data, null, 2).substring(0, 500)); + + // Extract image URLs from the response + // xLmiler returns images in the message content or as markdown images + const content = data.choices?.[0]?.message?.content || ''; + + // Parse image URLs from markdown format: ![...](url) or direct URLs + const imageUrls: string[] = []; + + // Match markdown image syntax + const mdImageRegex = /!\[.*?\]\((https?:\/\/[^\s)]+)\)/g; + let match; + while ((match = mdImageRegex.exec(content)) !== null) { + imageUrls.push(match[1]); + } + + // Also match direct URLs + const urlRegex = /https:\/\/[^\s"'<>]+\.(png|jpg|jpeg|webp|gif)/gi; + while ((match = urlRegex.exec(content)) !== null) { + if (!imageUrls.includes(match[0])) { + imageUrls.push(match[0]); + } + } + + // Return images in our standard format + const images = imageUrls.slice(0, numImages).map(url => ({ + url, + prompt, + model: 'grok-4-imageGen' + })); + + if (images.length === 0) { + // Return the raw content for debugging + return NextResponse.json({ + error: "No images found in response", + rawContent: content.substring(0, 500) + }, { status: 500 }); + } + + return NextResponse.json({ images }); + + } catch (error: any) { + console.error('[Grok Image] Error:', error); + return NextResponse.json( + { error: error.message || 'Generation failed' }, + { status: 500 } + ); + } +} diff --git a/app/api/grok/generate/route.ts b/app/api/grok/generate/route.ts deleted file mode 100644 index 1b26bfc..0000000 --- a/app/api/grok/generate/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { GrokClient } from '@/lib/providers/grok-client'; - -export async function POST(req: NextRequest) { - try { - const { prompt, apiKey, cookies, imageCount = 1 } = await req.json(); - - if (!prompt) { - return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); - } - - if (!apiKey && !cookies) { - return NextResponse.json( - { error: "Grok API key or cookies required. Configure in Settings." }, - { status: 401 } - ); - } - - console.log(`[Grok API Route] Generating ${imageCount} image(s) for: "${prompt.substring(0, 30)}..."`); - - const client = new GrokClient({ apiKey, cookies }); - const results = await client.generate(prompt, imageCount); - - // Download images as base64 for storage - const images = await Promise.all( - results.map(async (img) => { - let base64 = img.data; - if (!base64 && img.url && !img.url.startsWith('data:')) { - try { - base64 = await client.downloadAsBase64(img.url); - } catch (e) { - console.warn("[Grok API Route] Failed to download image:", e); - } - } - return { - data: base64 || '', - url: img.url, - prompt: img.prompt, - model: img.model, - aspectRatio: '1:1' // Grok default - }; - }) - ); - - const validImages = images.filter(img => img.data || img.url); - - if (validImages.length === 0) { - throw new Error("No valid images generated"); - } - - return NextResponse.json({ images: validImages }); - - } catch (error: any) { - console.error("[Grok API Route] Error:", error); - return NextResponse.json( - { error: error.message || "Grok generation failed" }, - { status: 500 } - ); - } -} diff --git a/app/api/meta-chat/route.ts b/app/api/meta-chat/route.ts new file mode 100644 index 0000000..50ac189 --- /dev/null +++ b/app/api/meta-chat/route.ts @@ -0,0 +1,208 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MetaAIClient } from '@/lib/providers/meta-client'; + +/** + * Meta AI Chat API + * Uses MetaAIClient directly (GraphQL-based) for Llama 3 chat + * No external Crawl4AI service needed + */ +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { message, history, metaCookies } = body; + + if (!message) { + return NextResponse.json({ error: "Message is required" }, { status: 400 }); + } + + if (!metaCookies) { + return NextResponse.json( + { error: "Meta AI cookies required. Configure in Settings." }, + { status: 401 } + ); + } + + console.log(`[Meta Chat] Message: "${message.substring(0, 50)}..."`); + + // Use MetaAIClient to send chat message + // The Meta AI API is primarily designed for image generation, + // but we can use it for text chat by not prefixing with "Imagine" + const client = new MetaAIClient({ cookies: metaCookies }); + + // For chat, we need to initialize session and send via GraphQL + // Since MetaAIClient.generate() adds "Imagine" prefix for images, + // we'll create a direct chat method or adapt the prompt + + // Send message directly - the response will contain text, not images + const chatPrompt = message; // Don't add "Imagine" prefix for chat + + try { + // Use the internal sendPrompt mechanism via generate + // But we'll extract the text response instead of images + const response = await sendMetaChatMessage(client, chatPrompt, metaCookies); + return NextResponse.json({ response }); + } catch (chatError: any) { + // If the direct approach fails, provide helpful error + throw new Error(chatError.message || "Failed to get response from Meta AI"); + } + + } catch (error: any) { + console.error('[Meta Chat] Error:', error); + + const msg = error.message || ""; + const isAuthError = msg.includes("401") || msg.includes("cookies") || + msg.includes("expired") || msg.includes("Login"); + + return NextResponse.json( + { error: error.message || 'Internal Server Error' }, + { status: isAuthError ? 401 : 500 } + ); + } +} + +/** + * Send a chat message to Meta AI and extract text response + */ +async function sendMetaChatMessage(client: MetaAIClient, message: string, cookies: string): Promise { + const META_AI_BASE = "https://www.meta.ai"; + const GRAPHQL_ENDPOINT = `${META_AI_BASE}/api/graphql/`; + + // Normalize cookies from JSON array to string format + const normalizedCookies = normalizeCookies(cookies); + + // First we need to get session tokens + const sessionResponse = await fetch(META_AI_BASE, { + headers: { + "Cookie": normalizedCookies, + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + } + }); + const html = await sessionResponse.text(); + + // Extract LSD token + const lsdMatch = html.match(/\"LSD\",\[\],\{\"token\":\"([^\"]+)\"/) || + html.match(/\"lsd\":\"([^\"]+)\"/) || + html.match(/name=\"lsd\" value=\"([^\"]+)\"/); + const lsd = lsdMatch?.[1] || ''; + + // Extract access token + const tokenMatch = html.match(/\"accessToken\":\"([^\"]+)\"/); + const accessToken = tokenMatch?.[1]; + + if (html.includes('login_form') || html.includes('login_page')) { + throw new Error("Meta AI: Cookies expired. Please update in Settings."); + } + + // Send chat message + const variables = { + message: { + text: message, + content_type: "TEXT" + }, + source: "PDT_CHAT_INPUT", + external_message_id: Math.random().toString(36).substring(2) + Date.now().toString(36) + }; + + const body = new URLSearchParams({ + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "useAbraSendMessageMutation", + variables: JSON.stringify(variables), + doc_id: "7783822248314888", + ...(lsd && { lsd }), + }); + + const response = await fetch(GRAPHQL_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": normalizedCookies, + "Origin": META_AI_BASE, + "Referer": `${META_AI_BASE}/`, + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Accept": "application/json", + "Accept-Language": "en-US,en;q=0.9", + ...(accessToken && { "Authorization": `OAuth ${accessToken}` }) + }, + body: body.toString() + }); + + // Get response text first + const responseText = await response.text(); + + // Check if response is HTML (error page) + if (responseText.trim().startsWith('<') || responseText.includes(' "foo=bar; ..." + */ +function normalizeCookies(cookies: string): string { + if (!cookies) return ''; + + try { + const trimmed = cookies.trim(); + // Check if it's JSON array + if (trimmed.startsWith('[')) { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return parsed + .map((c: any) => `${c.name}=${c.value}`) + .join('; '); + } + } + // Check if it's JSON object with multiple arrays (merged cookies) + if (trimmed.startsWith('{')) { + const parsed = JSON.parse(trimmed); + // If it's an object, iterate values + const entries = Object.entries(parsed) + .map(([k, v]) => `${k}=${v}`) + .join('; '); + return entries; + } + } catch (e) { + // Not JSON, assume it's already a string format + } + + return cookies; +} diff --git a/app/api/meta-crawl/route.ts b/app/api/meta-crawl/route.ts deleted file mode 100644 index b113d5e..0000000 --- a/app/api/meta-crawl/route.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { MetaCrawlClient } from '@/lib/providers/meta-crawl-client'; - -/** - * API Route: /api/meta-crawl - * - * Proxies image generation requests to the Crawl4AI Python service - * which uses browser automation to interact with Meta AI. - */ - -const client = new MetaCrawlClient(); - -export async function POST(req: NextRequest) { - try { - const body = await req.json(); - // Support both numImages (camelCase) and num_images (snake_case) - const { prompt, cookies, numImages, num_images, async = false } = body; - const imageCount = num_images || numImages || 4; - - if (!prompt) { - return NextResponse.json( - { error: "Prompt is required" }, - { status: 400 } - ); - } - - if (!cookies) { - return NextResponse.json( - { error: "Meta AI cookies are required. Please configure in settings." }, - { status: 401 } - ); - } - - // Check if service is healthy - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - return NextResponse.json( - { error: "Crawl4AI service is not available. Please try again later." }, - { status: 503 } - ); - } - - if (async) { - // Async mode: return task_id for polling - const taskId = await client.generateAsync(prompt, cookies, imageCount); - return NextResponse.json({ - success: true, - task_id: taskId - }); - } - - // Sync mode: wait for completion - console.log(`[MetaCrawl API] Generating images for: "${prompt.substring(0, 50)}..."`); - - const images = await client.generate(prompt, cookies, imageCount); - - return NextResponse.json({ - success: true, - images: images.map(img => ({ - url: img.url, - data: img.data, - prompt: img.prompt, - model: img.model - })) - }); - - } catch (error: any) { - console.error("[MetaCrawl API] Error:", error); - return NextResponse.json( - { error: error.message || "Image generation failed" }, - { status: 500 } - ); - } -} - -/** - * GET /api/meta-crawl?task_id=xxx - * - * Get status of an async generation task - */ -export async function GET(req: NextRequest) { - const taskId = req.nextUrl.searchParams.get('task_id'); - - if (!taskId) { - // Return rate limit status - try { - const status = await client.getRateLimitStatus(); - return NextResponse.json(status); - } catch { - return NextResponse.json({ error: "Service not available" }, { status: 503 }); - } - } - - try { - const status = await client.getTaskStatus(taskId); - return NextResponse.json(status); - } catch (error: any) { - return NextResponse.json( - { error: error.message }, - { status: error.message === 'Task not found' ? 404 : 500 } - ); - } -} - -/** - * DELETE /api/meta-crawl?task_id=xxx - * - * Clean up a completed task - */ -export async function DELETE(req: NextRequest) { - const taskId = req.nextUrl.searchParams.get('task_id'); - - if (!taskId) { - return NextResponse.json({ error: "task_id is required" }, { status: 400 }); - } - - try { - const response = await fetch(`${process.env.CRAWL4AI_URL || 'http://localhost:8000'}/status/${taskId}`, { - method: 'DELETE' - }); - - if (!response.ok) { - return NextResponse.json({ error: "Failed to delete task" }, { status: response.status }); - } - - return NextResponse.json({ deleted: true }); - } catch (error: any) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/app/api/meta/generate/route.ts b/app/api/meta/generate/route.ts index 0dd753c..c0c8ef7 100644 --- a/app/api/meta/generate/route.ts +++ b/app/api/meta/generate/route.ts @@ -3,23 +3,39 @@ import { MetaAIClient } from '@/lib/providers/meta-client'; export async function POST(req: NextRequest) { try { - const { prompt, cookies, imageCount = 4 } = await req.json(); + const { prompt, cookies, imageCount = 4, aspectRatio = 'portrait', useMetaFreeWrapper, metaFreeWrapperUrl } = await req.json(); if (!prompt) { return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); } - if (!cookies) { + // Only check for cookies if NOT using free wrapper + if (!useMetaFreeWrapper && !cookies) { return NextResponse.json( - { error: "Meta AI cookies required. Configure in Settings." }, + { error: "Meta AI cookies required. Configure in Settings or use Free Wrapper." }, { status: 401 } ); } - console.log(`[Meta AI Route] Generating images for: "${prompt.substring(0, 30)}..."`); + console.log(`[Meta AI Route] Generating images for: "${prompt.substring(0, 30)}..." (${aspectRatio})`); - const client = new MetaAIClient({ cookies }); - const results = await client.generate(prompt, imageCount); + // Diagnostic: Check how many cookies we received + try { + const parsed = typeof cookies === 'string' && cookies.trim().startsWith('[') + ? JSON.parse(cookies) + : cookies; + const count = Array.isArray(parsed) ? parsed.length : (typeof cookies === 'string' ? cookies.split(';').length : 0); + console.log(`[Meta AI Route] Received ${count} cookies (Free Wrapper: ${useMetaFreeWrapper})`); + } catch { + console.log(`[Meta AI Route] Cookie format: ${typeof cookies}`); + } + + const client = new MetaAIClient({ + cookies: cookies || '', + useFreeWrapper: useMetaFreeWrapper, + freeWrapperUrl: metaFreeWrapperUrl + }); + const results = await client.generate(prompt, imageCount, aspectRatio); // Download images as base64 for storage const images = await Promise.all( @@ -48,7 +64,7 @@ export async function POST(req: NextRequest) { throw new Error("No valid images generated"); } - return NextResponse.json({ images: validImages }); + return NextResponse.json({ success: true, images: validImages }); } catch (error: any) { console.error("[Meta AI Route] Error:", error); diff --git a/app/api/meta/video/route.ts b/app/api/meta/video/route.ts index 9b8294e..44feac7 100644 --- a/app/api/meta/video/route.ts +++ b/app/api/meta/video/route.ts @@ -1,59 +1,66 @@ import { NextRequest, NextResponse } from 'next/server'; -import { MetaCrawlClient } from '@/lib/providers/meta-crawl-client'; +import { MetaAIClient } from '@/lib/providers/meta-client'; /** * POST /api/meta/video * - * Generate a video from a text prompt (and optionally an image) using Meta AI. - * - Text-to-Video: Just provide prompt and cookies - * - Image-to-Video: Also provide imageBase64 - * Video generation takes 30-60+ seconds, so this endpoint may take a while. + * Generate a video from a text prompt using Meta AI's Kadabra engine. + * Uses MetaAIClient for session initialization (which works for images). */ + +const META_AI_BASE = "https://www.meta.ai"; +const GRAPHQL_ENDPOINT = `${META_AI_BASE}/api/graphql/`; + +// Video generation doc IDs from metaai-api +const VIDEO_INITIATE_DOC_ID = "25290947477183545"; // useKadabraSendMessageMutation +const VIDEO_POLL_DOC_ID = "25290569913909283"; // KadabraPromptRootQuery + export async function POST(req: NextRequest) { try { - const { prompt, cookies: clientCookies, imageBase64 } = await req.json(); + const { prompt, cookies: clientCookies, aspectRatio = 'portrait' } = await req.json(); if (!prompt) { return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); } - // Get cookies from request body or cookie header - let cookieString = clientCookies || req.cookies.get('meta_cookies')?.value; - - if (!cookieString) { + if (!clientCookies) { return NextResponse.json( { error: "Meta AI cookies not found. Please configure settings." }, { status: 401 } ); } - const mode = imageBase64 ? 'image-to-video' : 'text-to-video'; - console.log(`[Meta Video API] Starting ${mode} for prompt: "${prompt.substring(0, 50)}..."`); + console.log(`[Meta Video API] Generating video for: "${prompt.substring(0, 50)}..." (${aspectRatio})`); - const client = new MetaCrawlClient(); + // Use MetaAIClient for session initialization (proven to work) + const client = new MetaAIClient({ cookies: clientCookies }); + const session = await client.getSession(); + const cookieString = client.getCookies(); - // Check if crawl4ai service is available - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - return NextResponse.json( - { error: "Meta AI video service is not available. Make sure crawl4ai is running." }, - { status: 503 } - ); + console.log("[Meta Video] Using MetaAIClient session:", { + hasLsd: !!session.lsd, + hasDtsg: !!session.fb_dtsg, + hasAccessToken: !!session.accessToken + }); + + // Generate unique IDs for this request + const externalConversationId = crypto.randomUUID(); + const offlineThreadingId = Date.now().toString() + Math.random().toString().substring(2, 8); + + // Initiate video generation with aspect ratio + await initiateVideoGeneration(prompt, externalConversationId, offlineThreadingId, session, cookieString, aspectRatio); + + // Poll for video completion + const videos = await pollForVideoResult(externalConversationId, session, cookieString); + + if (videos.length === 0) { + throw new Error("No videos generated"); } - // Generate video - this can take 30-60+ seconds - const result = await client.generateVideo(prompt, cookieString, imageBase64); - - if (!result.success || result.videos.length === 0) { - throw new Error(result.error || "No videos generated"); - } - - console.log(`[Meta Video API] Successfully generated ${result.videos.length} video(s)`); - return NextResponse.json({ success: true, - videos: result.videos, - conversation_id: result.conversation_id + videos: videos.map(v => ({ url: v.url, prompt: prompt })), + conversation_id: externalConversationId }); } catch (error: unknown) { @@ -62,7 +69,7 @@ export async function POST(req: NextRequest) { const msg = err.message || ""; const isAuthError = msg.includes("401") || msg.includes("403") || - msg.includes("auth") || msg.includes("cookies") || msg.includes("expired"); + msg.includes("auth") || msg.includes("cookies") || msg.includes("expired") || msg.includes("Login"); return NextResponse.json( { error: err.message || "Video generation failed" }, @@ -70,3 +77,295 @@ export async function POST(req: NextRequest) { ); } } + +interface MetaSession { + lsd?: string; + fb_dtsg?: string; + accessToken?: string; +} + +/** + * Normalize cookies from JSON array to string format + */ +function normalizeCookies(cookies: string): string { + if (!cookies) return ''; + + try { + const trimmed = cookies.trim(); + if (trimmed.startsWith('[')) { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return parsed.map((c: any) => `${c.name}=${c.value}`).join('; '); + } + } + } catch (e) { + // Not JSON, assume it's already a string format + } + + return cookies; +} + +/** + * Initialize session - get access token and LSD from meta.ai page + */ +async function initSession(cookies: string, retryCount: number = 0): Promise { + console.log("[Meta Video] Initializing session..."); + console.log("[Meta Video] Cookie string length:", cookies.length); + + // Add small delay to avoid rate limiting (especially after image generation) + if (retryCount > 0) { + await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); + } + + const response = await fetch(META_AI_BASE, { + headers: { + "Cookie": cookies, + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + } + }); + + const html = await response.text(); + console.log("[Meta Video] HTML Preview:", html.substring(0, 200)); + + // Detect Facebook error page and retry + if (html.includes('Error') || html.includes('id="facebook"')) { + console.warn("[Meta Video] Received Facebook error page, retrying..."); + if (retryCount < 3) { + return initSession(cookies, retryCount + 1); + } + throw new Error("Meta AI: Server temporarily unavailable. Please try again in a moment."); + } + + const session: MetaSession = {}; + + // Extract LSD token - multiple patterns for different Meta AI versions + const lsdMatch = html.match(/"LSD",\[\],\{"token":"([^"]+)"/) || + html.match(/"lsd":"([^"]+)"/) || + html.match(/name="lsd" value="([^"]+)"/) || + html.match(/"token":"([^"]+)".*?"name":"lsd"/); + if (lsdMatch) { + session.lsd = lsdMatch[1]; + } + + // Extract access token + const tokenMatch = html.match(/"accessToken":"([^"]+)"/) || + html.match(/accessToken['"]\s*:\s*['"]([^'"]+)['"]/); + if (tokenMatch) { + session.accessToken = tokenMatch[1]; + } + + // Extract DTSG token - try multiple patterns + const dtsgMatch = html.match(/DTSGInitData",\[\],\{"token":"([^"]+)"/) || + html.match(/"DTSGInitialData".*?"token":"([^"]+)"/) || + html.match(/fb_dtsg['"]\s*:\s*\{[^}]*['"]token['"]\s*:\s*['"]([^'"]+)['"]/); + if (dtsgMatch) { + session.fb_dtsg = dtsgMatch[1]; + } + + // Also try to extract from cookies if not found in HTML + if (!session.lsd) { + const lsdCookie = cookies.match(/lsd=([^;]+)/); + if (lsdCookie) session.lsd = lsdCookie[1]; + } + if (!session.fb_dtsg) { + const dtsgCookie = cookies.match(/fb_dtsg=([^;]+)/); + if (dtsgCookie) session.fb_dtsg = dtsgCookie[1]; + } + + if (html.includes('login_form') || html.includes('login_page')) { + throw new Error("Meta AI: Cookies expired. Please update in Settings."); + } + + console.log("[Meta Video] Session tokens extracted:", { + hasLsd: !!session.lsd, + hasDtsg: !!session.fb_dtsg, + hasAccessToken: !!session.accessToken + }); + + return session; +} + +/** + * Initiate video generation using Kadabra mutation + */ +async function initiateVideoGeneration( + prompt: string, + conversationId: string, + threadingId: string, + session: MetaSession, + cookies: string, + aspectRatio: string = 'portrait' +): Promise { + console.log("[Meta Video] Initiating video generation..."); + + // Map aspect ratio to orientation + const orientationMap: Record = { + 'portrait': 'VERTICAL', + 'landscape': 'HORIZONTAL', + 'square': 'SQUARE' + }; + const orientation = orientationMap[aspectRatio] || 'VERTICAL'; + + const variables = { + message: { + prompt_text: prompt, + external_conversation_id: conversationId, + offline_threading_id: threadingId, + imagineClientOptions: { orientation: orientation }, + selectedAgentType: "PLANNER" + }, + __relay_internal__pv__AbraArtifactsEnabledrelayprovider: true + }; + + const body = new URLSearchParams({ + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "useKadabraSendMessageMutation", + variables: JSON.stringify(variables), + doc_id: VIDEO_INITIATE_DOC_ID, + ...(session.lsd && { lsd: session.lsd }), + ...(session.fb_dtsg && { fb_dtsg: session.fb_dtsg }) + }); + + const response = await fetch(GRAPHQL_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookies, + "Origin": META_AI_BASE, + "Referer": `${META_AI_BASE}/`, + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + ...(session.accessToken && { "Authorization": `OAuth ${session.accessToken}` }) + }, + body: body.toString() + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("[Meta Video] Initiation failed:", response.status, errorText); + throw new Error(`Meta AI Error: ${response.status}`); + } + + const data = await response.json(); + console.log("[Meta Video] Initiation response:", JSON.stringify(data).substring(0, 200)); + + // Check for errors + if (data.errors) { + throw new Error(data.errors[0]?.message || "Video initiation failed"); + } +} + +/** + * Poll for video result using KadabraPromptRootQuery + */ +async function pollForVideoResult( + conversationId: string, + session: MetaSession, + cookies: string +): Promise<{ url: string }[]> { + const maxAttempts = 60; // 2 minutes max + const pollInterval = 2000; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + console.log(`[Meta Video] Polling attempt ${attempt + 1}/${maxAttempts}...`); + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + + const variables = { + external_conversation_id: conversationId + }; + + const body = new URLSearchParams({ + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "KadabraPromptRootQuery", + variables: JSON.stringify(variables), + doc_id: VIDEO_POLL_DOC_ID, + ...(session.lsd && { lsd: session.lsd }), + ...(session.fb_dtsg && { fb_dtsg: session.fb_dtsg }) + }); + + try { + const response = await fetch(GRAPHQL_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookies, + "Origin": META_AI_BASE, + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + ...(session.accessToken && { "Authorization": `OAuth ${session.accessToken}` }) + }, + body: body.toString() + }); + + const data = await response.json(); + + // Extract video URLs from response + const videos = extractVideosFromResponse(data); + + if (videos.length > 0) { + console.log(`[Meta Video] Got ${videos.length} video(s)!`); + return videos; + } + + // Check for error status + const status = data?.data?.kadabra_prompt?.status; + if (status === "FAILED" || status === "ERROR") { + throw new Error("Meta AI video generation failed"); + } + } catch (e: any) { + console.error("[Meta Video] Poll error:", e.message); + if (attempt === maxAttempts - 1) throw e; + } + } + + throw new Error("Meta AI: Video generation timed out"); +} + +/** + * Extract video URLs from GraphQL response + */ +function extractVideosFromResponse(response: any): { url: string }[] { + const videos: { url: string }[] = []; + + try { + // Navigate through possible response structures + const prompt = response?.data?.kadabra_prompt; + const messages = prompt?.messages?.edges || []; + + for (const edge of messages) { + const node = edge?.node; + const attachments = node?.attachments || []; + + for (const attachment of attachments) { + // Check for video media + const media = attachment?.media; + if (media?.video_uri) { + videos.push({ url: media.video_uri }); + } + if (media?.playable_url) { + videos.push({ url: media.playable_url }); + } + if (media?.browser_native_sd_url) { + videos.push({ url: media.browser_native_sd_url }); + } + } + + // Check imagine_card for video results + const imagineCard = node?.imagine_card; + if (imagineCard?.session?.media_sets) { + for (const mediaSet of imagineCard.session.media_sets) { + for (const media of mediaSet?.imagine_media || []) { + if (media?.video_uri) { + videos.push({ url: media.video_uri }); + } + } + } + } + } + } catch (e) { + console.error("[Meta Video] Error extracting videos:", e); + } + + return videos; +} diff --git a/app/page.tsx b/app/page.tsx index bd1fad9..1e6e15a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -52,8 +52,8 @@ export default function Home() { - {/* Floating Chat */} - {/* */} + {/* Floating AI Chat */} + ); diff --git a/components/EditPromptModal.tsx b/components/EditPromptModal.tsx index 2314e39..104d5b7 100644 --- a/components/EditPromptModal.tsx +++ b/components/EditPromptModal.tsx @@ -7,7 +7,7 @@ import { cn } from '@/lib/utils'; interface EditPromptModalProps { isOpen: boolean; onClose: () => void; - image: { data: string; prompt: string } | null; + image: { data: string; prompt: string; provider?: string } | null; onGenerate: (prompt: string, options: { keepSubject: boolean; keepScene: boolean; keepStyle: boolean }) => Promise; } @@ -18,12 +18,16 @@ export function EditPromptModal({ isOpen, onClose, image, onGenerate }: EditProm const [keepScene, setKeepScene] = React.useState(true); const [keepStyle, setKeepStyle] = React.useState(true); + const isMeta = image?.provider === 'meta'; + React.useEffect(() => { if (isOpen && image) { setPrompt(image.prompt); } }, [isOpen, image]); + // ... (lines 27-130 remain unchanged, so we skip them in replace tool if possible, + if (!isOpen) return null; const handleSubmit = async (e: React.FormEvent) => { @@ -122,43 +126,47 @@ export function EditPromptModal({ isOpen, onClose, image, onGenerate }: EditProm value={prompt} onChange={(e) => setPrompt(e.target.value)} className="w-full h-24 p-3 rounded-xl bg-white/5 border border-white/10 resize-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50 outline-none text-sm text-white placeholder:text-white/30 transition-all" - placeholder="Describe your remix... The selected consistency options will be preserved..." + placeholder={isMeta ? "Modify your prompt to generate a new variation..." : "Describe your remix... The selected consistency options will be preserved..."} autoFocus /> {/* Consistency Toggles */} -
- -
- - - + {!isMeta && ( +
+ +
+ + + +
-
+ )} {/* Info about consistency */} -
-

- 💡 Locked elements will be used as references to maintain visual consistency across generations. -

-
+ {!isMeta && ( +
+

+ 💡 Locked elements will be used as references to maintain visual consistency across generations. +

+
+ )} {/* Actions */}
diff --git a/components/Gallery.tsx b/components/Gallery.tsx index 22e07b2..5a7eca5 100644 --- a/components/Gallery.tsx +++ b/components/Gallery.tsx @@ -11,23 +11,47 @@ import { EditPromptModal } from './EditPromptModal'; // Helper function to get proper image src (handles URLs vs base64) const getImageSrc = (data: string): string => { if (!data) return ''; + const cleanData = data.trim(); // If it's already a URL, use it directly - if (data.startsWith('http://') || data.startsWith('https://') || data.startsWith('data:')) { - return data; + if (cleanData.indexOf('http') === 0 || cleanData.indexOf('data:') === 0) { + return cleanData; } - // Otherwise, treat as base64 - return `data:image/png;base64,${data}`; + + // Otherwise, treat as base64 (don't warn - base64 often contains 'http' as random characters) + return `data:image/png;base64,${cleanData}`; }; export function Gallery() { - const { gallery, clearGallery, removeFromGallery, setPrompt, addVideo, addToGallery, settings, videos, removeVideo, isGenerating } = useStore(); - const [selectedIndex, setSelectedIndex] = React.useState(null); + const { + gallery, loadGallery, addToGallery, removeFromGallery, clearGallery, + isGenerating, + settings, + videos, addVideo, + setPrompt + } = useStore(); const [videoModalOpen, setVideoModalOpen] = React.useState(false); - const [videoSource, setVideoSource] = React.useState<{ data: string, prompt: string } | null>(null); + const [videoSource, setVideoSource] = React.useState<{ data: string, prompt: string, provider?: string } | null>(null); const [editModalOpen, setEditModalOpen] = React.useState(false); const [editSource, setEditSource] = React.useState<{ data: string, prompt: string } | null>(null); + const [editPromptValue, setEditPromptValue] = React.useState(''); + const [videoPromptValue, setVideoPromptValue] = React.useState(''); + const [useSourceImage, setUseSourceImage] = React.useState(true); + const [selectedIndex, setSelectedIndex] = React.useState(null); - const openVideoModal = (img: { data: string, prompt: string }) => { + React.useEffect(() => { + if (selectedIndex !== null && gallery[selectedIndex]) { + setEditSource(gallery[selectedIndex]); + setEditPromptValue(gallery[selectedIndex].prompt || ''); + setVideoPromptValue(''); + setUseSourceImage(true); + } + }, [selectedIndex, gallery]); + + React.useEffect(() => { + loadGallery(); + }, []); // Only load on mount + + const openVideoModal = (img: { data: string, prompt: string, provider?: string }) => { setVideoSource(img); setVideoModalOpen(true); }; @@ -37,27 +61,52 @@ export function Gallery() { setEditModalOpen(true); }; - const [isGeneratingMetaVideo, setIsGeneratingMetaVideo] = React.useState(false); + const [isGeneratingMetaVideo, setIsGeneratingMetaVideo] = React.useState(false); // Kept for UI state compatibility + const [isGeneratingWhiskVideo, setIsGeneratingWhiskVideo] = React.useState(false); - // Handle Meta AI video generation (image-to-video) - const handleGenerateMetaVideo = async (img: { data: string; prompt: string }) => { - if (!settings.metaCookies) { - alert("Please set your Meta AI Cookies in Settings first!"); + // Handle Meta AI video generation (text-to-video via Kadabra) + const handleGenerateMetaVideo = async (img: { data: string; prompt: string }, customPrompt?: string) => { + if (!settings.metaCookies && !settings.facebookCookies) { + alert("Please set your Meta AI (or Facebook) Cookies in Settings first!"); return; } setIsGeneratingMetaVideo(true); try { - console.log("[Gallery] Starting Meta AI image-to-video..."); + console.log("[Gallery] Starting Meta AI video generation..."); + + // Create a descriptive prompt that includes the original image context + animation + const originalPrompt = img.prompt || ""; + const animationDescription = customPrompt || "natural movement"; + + // Combine original image description with animation instruction + const promptText = originalPrompt + ? `Create a video of: ${originalPrompt}. Animation: ${animationDescription}` + : `Create an animated video with: ${animationDescription}`; + + console.log("[Gallery] Meta video prompt:", promptText); + + // Merge cookies safely + let mergedCookies = settings.metaCookies; + try { + const safeParse = (str: string) => { + if (!str || str === "undefined" || str === "null") return []; + try { return JSON.parse(str); } catch { return []; } + }; + const m = safeParse(settings.metaCookies); + const f = safeParse(settings.facebookCookies); + if (Array.isArray(m) || Array.isArray(f)) { + mergedCookies = [...(Array.isArray(m) ? m : []), ...(Array.isArray(f) ? f : [])] as any; + } + } catch (e) { console.error("Cookie merge failed", e); } const res = await fetch('/api/meta/video', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - prompt: img.prompt || "Animate this image with natural movement", - cookies: settings.metaCookies, - imageBase64: img.data + prompt: promptText, + cookies: typeof mergedCookies === 'string' ? mergedCookies : JSON.stringify(mergedCookies) }) }); @@ -69,19 +118,20 @@ export function Gallery() { addVideo({ id: crypto.randomUUID(), url: video.url, - prompt: video.prompt || img.prompt, + prompt: promptText, thumbnail: img.data, createdAt: Date.now() }); } alert('🎬 Video generation complete! Scroll up to see your video.'); + setVideoModalOpen(false); } else { throw new Error(data.error || 'No videos generated'); } } catch (error: any) { console.error("[Gallery] Meta video error:", error); let errorMessage = error.message || 'Video generation failed'; - if (errorMessage.includes('401') || errorMessage.includes('cookies')) { + if (errorMessage.includes('401') || errorMessage.includes('cookies') || errorMessage.includes('expired')) { errorMessage = '🔐 Your Meta AI cookies have expired. Please go to Settings and update them.'; } alert(errorMessage); @@ -90,61 +140,137 @@ export function Gallery() { } }; - const handleGenerateVideo = async (prompt: string) => { - if (!videoSource) return; + const handleGenerateVideo = async (prompt: string, sourceOverride?: { data: string; prompt: string; provider?: string; aspectRatio?: string }) => { + const activeSource = sourceOverride || videoSource; + if (!activeSource) return; + + // Route to Meta AI video for meta provider + if (activeSource.provider === 'meta') { + await handleGenerateMetaVideo(activeSource, prompt); + return; + } if (!settings.whiskCookies) { alert("Please set your Whisk Cookies in Settings first!"); throw new Error("Missing Whisk cookies"); } - const res = await fetch('/api/video/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - prompt: prompt, - imageBase64: videoSource.data, - // imageGenerationId: (videoSource as any).id, // REMOVE: "id" is a local DB ID (e.g. 1), not a Whisk Media ID. - cookies: settings.whiskCookies - }) - }); + setIsGeneratingWhiskVideo(true); - const data = await res.json(); - console.log("[Gallery] Video API response:", data); - if (data.success) { - console.log("[Gallery] Adding video to store:", { id: data.id, url: data.url?.substring(0, 50) }); - addVideo({ - id: data.id, - url: data.url, - prompt: prompt, - thumbnail: videoSource.data, // Use source image as thumb - createdAt: Date.now() + try { + const res = await fetch('/api/video/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: prompt, + imageBase64: activeSource.data, + cookies: settings.whiskCookies + }) }); - // Success notification - setTimeout(() => { + + const data = await res.json(); + console.log("[Gallery] Video API response:", data); + if (data.success) { + console.log("[Gallery] Adding video to store:", { id: data.id, url: data.url?.substring(0, 50) }); + addVideo({ + id: data.id, + url: data.url, + prompt: prompt, + thumbnail: activeSource.data, + createdAt: Date.now() + }); alert('🎬 Video generation complete!\n\nYour video has been saved. Go to the "Uploads" page and select the "Videos" tab to view it.'); - }, 100); - } else { - console.error(data.error); - // Show user-friendly error messages for Google safety policies - let errorMessage = data.error; - if (data.error?.includes('NCII')) { - errorMessage = '🚫 Content Policy: Video blocked by Google\'s NCII (Non-Consensual Intimate Imagery) protection. Please try with a different source image.'; - } else if (data.error?.includes('PROMINENT_PEOPLE') || data.error?.includes('prominent')) { - errorMessage = '🚫 Content Policy: Video blocked because the image contains a recognizable person. Try using a different image.'; - } else if (data.error?.includes('safety') || data.error?.includes('SAFETY')) { - errorMessage = '⚠️ Content Policy: Video blocked by Google\'s safety filters. Try a different source image.'; - } else if (data.error?.includes('401') || data.error?.includes('UNAUTHENTICATED')) { - errorMessage = '🔐 Authentication Error: Your Whisk (Google) cookies have expired. Please go to Settings and update them.'; + } else { + console.error(data.error); + let errorMessage = data.error; + if (data.error?.includes('NCII')) { + errorMessage = '🚫 Content Policy: Video blocked by Google\'s NCII protection. Please try with a different source image.'; + } else if (data.error?.includes('PROMINENT_PEOPLE') || data.error?.includes('prominent')) { + errorMessage = '🚫 Content Policy: Video blocked because the image contains a recognizable person.'; + } else if (data.error?.includes('safety') || data.error?.includes('SAFETY')) { + errorMessage = '⚠️ Content Policy: Video blocked by Google\'s safety filters.'; + } else if (data.error?.includes('401') || data.error?.includes('UNAUTHENTICATED')) { + errorMessage = '🔐 Authentication Error: Your Whisk cookies have expired. Please update in Settings.'; + } else if (data.error?.includes('429') || data.error?.includes('RESOURCE_EXHAUSTED')) { + errorMessage = '⏱️ Rate Limit: Too many requests. Please wait a few minutes and try again.'; + } + alert(errorMessage); + throw new Error(data.error); } - alert(errorMessage); - throw new Error(data.error); + } finally { + setIsGeneratingWhiskVideo(false); } }; + + const handleRemix = async (prompt: string, options: { keepSubject: boolean; keepScene: boolean; keepStyle: boolean }) => { if (!editSource) return; + // Meta AI Remix Flow (Prompt Edit Only) + if (editSource.provider === 'meta') { + if (!settings.metaCookies && !settings.facebookCookies) { + alert("Please set your Meta AI (or Facebook) Cookies in Settings first!"); + return; + } + + try { + // Merge cookies safely + let mergedCookies = settings.metaCookies; + try { + const safeParse = (str: string) => { + if (!str || str === "undefined" || str === "null") return []; + try { return JSON.parse(str); } catch { return []; } + }; + const m = safeParse(settings.metaCookies); + const f = safeParse(settings.facebookCookies); + if (Array.isArray(m) || Array.isArray(f)) { + mergedCookies = [...(Array.isArray(m) ? m : []), ...(Array.isArray(f) ? f : [])] as any; + } + } catch (e) { console.error("Cookie merge failed", e); } + + const res = await fetch('/api/meta/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: prompt, + cookies: typeof mergedCookies === 'string' ? mergedCookies : JSON.stringify(mergedCookies), + imageCount: 4 + }) + }); + + const data = await res.json(); + if (data.error) throw new Error(data.error); + + if (data.success && data.images?.length > 0) { + // Add new images to gallery + const newImages = data.images.map((img: any) => ({ + id: crypto.randomUUID(), + data: img.data, // Base64 + prompt: prompt, + createdAt: Date.now(), + width: 1024, + height: 1024, + aspectRatio: settings.aspectRatio, + provider: 'meta' + })); + + // Add to store + newImages.forEach(addGeneratedImage); + + alert('✨ Remix complete! New images added to gallery.'); + setEditModalOpen(false); + } else { + throw new Error('No images generated'); + } + } catch (e: any) { + console.error("Meta Remix failed", e); + alert("Remix failed: " + e.message); + } + return; + } + + // Whisk Remix Flow (Reference Injection) if (!settings.whiskCookies) { alert("Please set your Whisk Cookies in Settings first!"); throw new Error("Missing Whisk cookies"); @@ -220,9 +346,41 @@ export function Gallery() { return null; // Or return generic empty state if controlled by parent, but parent checks length usually } - const handleClearAll = () => { - if (window.confirm("Delete all " + gallery.length + " images?")) { + const handleClearAll = async () => { + const count = gallery.length; + if (!window.confirm(`Delete all ${count} images? This will reset the gallery database.`)) return; + + try { + console.log("[Gallery] Hard clearing..."); + + // 1. Clear Zustand Store visual state immediate clearGallery(); + + // 2. Nuclear Option: Delete the entire database file + console.log("[Gallery] Deleting IndexedDB..."); + const req = window.indexedDB.deleteDatabase('kv-pix-db'); + + req.onsuccess = () => { + console.log("✅ DB Deleted successfully"); + // Clear localStorage persistence too just in case + localStorage.removeItem('kv-pix-storage'); + window.location.reload(); + }; + + req.onerror = (e) => { + console.error("❌ Failed to delete DB", e); + alert("Failed to delete database. Browser might be blocking it."); + window.location.reload(); + }; + + req.onblocked = () => { + console.warn("⚠️ DB Delete blocked - reloading to free locks"); + window.location.reload(); + }; + + } catch (e) { + console.error("[Gallery] Delete error:", e); + alert("❌ Failed to delete: " + String(e)); } }; @@ -377,7 +535,7 @@ export function Gallery() { )} {selectedIndex < gallery.length - 1 && (
)} - {/* Prompt Section */} + {/* Prompt Section (Editable) */}
-

Prompt

-

- {selectedImage.prompt || "No prompt available"} -

+
+

Prompt

+ {editPromptValue !== selectedImage.prompt && ( + Modified + )} +
+