apix/app/api/meta/video/route.ts
Khoa.vo e69c6ba64d
Some checks are pending
CI / build (18.x) (push) Waiting to run
CI / build (20.x) (push) Waiting to run
chore: Remove Grok integration, simplify Settings UI
- 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
2026-01-07 19:21:51 +07:00

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;
}