import { NextRequest, NextResponse } from 'next/server'; import { MetaAIClient } from '@/lib/providers/meta-client'; /** * POST /api/meta/video * * 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, aspectRatio = 'portrait' } = await req.json(); if (!prompt) { return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); } if (!clientCookies) { return NextResponse.json( { error: "Meta AI cookies not found. Please configure settings." }, { status: 401 } ); } console.log(`[Meta Video API] Generating video for: "${prompt.substring(0, 50)}..." (${aspectRatio})`); // Use MetaAIClient for session initialization (proven to work) const client = new MetaAIClient({ cookies: clientCookies }); const session = await client.getSession(); const cookieString = client.getCookies(); 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"); } return NextResponse.json({ success: true, videos: videos.map(v => ({ url: v.url, prompt: prompt })), conversation_id: externalConversationId }); } catch (error: unknown) { const err = error as Error; console.error("[Meta Video API] Error:", err.message); const msg = err.message || ""; const isAuthError = msg.includes("401") || msg.includes("403") || msg.includes("auth") || msg.includes("cookies") || msg.includes("expired") || msg.includes("Login"); return NextResponse.json( { error: err.message || "Video generation failed" }, { status: isAuthError ? 401 : 500 } ); } } 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; }