- Removed all Grok-related code, API routes, and services - Removed crawl4ai service and meta-crawl client - Simplified Settings to always show cookie inputs for Meta AI - Hid advanced wrapper settings behind collapsible section - Provider selection now only shows Whisk and Meta AI - Fixed unused imports and type definitions
371 lines
13 KiB
TypeScript
371 lines
13 KiB
TypeScript
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<MetaSession> {
|
|
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('<title>Error</title>') || 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<void> {
|
|
console.log("[Meta Video] Initiating video generation...");
|
|
|
|
// Map aspect ratio to orientation
|
|
const orientationMap: Record<string, string> = {
|
|
'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;
|
|
}
|