- 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
620 lines
23 KiB
TypeScript
620 lines
23 KiB
TypeScript
/**
|
|
* Meta AI Client for Image Generation
|
|
*
|
|
* Uses the Meta AI web interface via GraphQL
|
|
* Requires cookies from a logged-in meta.ai session
|
|
*
|
|
* Image Model: Imagine (Emu)
|
|
*
|
|
* Based on: https://github.com/Strvm/meta-ai-api
|
|
*/
|
|
|
|
const META_AI_BASE = "https://www.meta.ai";
|
|
const GRAPHQL_ENDPOINT = `${META_AI_BASE}/api/graphql/`;
|
|
|
|
interface MetaAIOptions {
|
|
cookies: string;
|
|
}
|
|
|
|
interface MetaImageResult {
|
|
url: string;
|
|
data?: string; // base64
|
|
prompt: string;
|
|
model: string;
|
|
}
|
|
|
|
interface MetaSession {
|
|
lsd?: string;
|
|
fb_dtsg?: string;
|
|
accessToken?: string;
|
|
externalConversationId?: string;
|
|
}
|
|
|
|
// Aspect ratio types for Meta AI
|
|
export type AspectRatio = 'portrait' | 'landscape' | 'square';
|
|
const ORIENTATION_MAP: Record<AspectRatio, string> = {
|
|
'portrait': 'VERTICAL', // 9:16
|
|
'landscape': 'HORIZONTAL', // 16:9
|
|
'square': 'SQUARE' // 1:1
|
|
};
|
|
|
|
export class MetaAIClient {
|
|
private cookies: string;
|
|
private session: MetaSession = {};
|
|
private useFreeWrapper: boolean = true;
|
|
private freeWrapperUrl: string = 'http://localhost:8000';
|
|
|
|
constructor(options: MetaAIOptions & { useFreeWrapper?: boolean; freeWrapperUrl?: string }) {
|
|
this.cookies = this.normalizeCookies(options.cookies);
|
|
this.useFreeWrapper = options.useFreeWrapper !== undefined ? options.useFreeWrapper : true;
|
|
this.freeWrapperUrl = options.freeWrapperUrl || 'http://localhost:8000';
|
|
console.log("[Meta AI] Cookie string length:", this.cookies.length);
|
|
if (this.cookies) {
|
|
this.parseSessionFromCookies();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize cookies from string or JSON format
|
|
* Handles cases where user pastes JSON array from extension/devtools
|
|
*/
|
|
private normalizeCookies(cookies: string): string {
|
|
if (!cookies) return "";
|
|
|
|
try {
|
|
// Check if it looks like JSON
|
|
if (cookies.trim().startsWith('[')) {
|
|
const parsed = JSON.parse(cookies);
|
|
if (Array.isArray(parsed)) {
|
|
return parsed
|
|
.map((c: any) => `${c.name}=${c.value}`)
|
|
.join('; ');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Not JSON, assume string
|
|
}
|
|
|
|
return cookies;
|
|
}
|
|
|
|
/**
|
|
* Parse session tokens from cookies
|
|
*/
|
|
private parseSessionFromCookies(): void {
|
|
// Extract lsd token if present in cookies
|
|
const lsdMatch = this.cookies.match(/lsd=([^;]+)/);
|
|
if (lsdMatch) {
|
|
this.session.lsd = lsdMatch[1];
|
|
}
|
|
|
|
// Extract fb_dtsg if present
|
|
const dtsgMatch = this.cookies.match(/fb_dtsg=([^;]+)/);
|
|
if (dtsgMatch) {
|
|
this.session.fb_dtsg = dtsgMatch[1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the initialized session tokens (call initSession first if not done)
|
|
*/
|
|
async getSession(): Promise<MetaSession & { externalConversationId?: string }> {
|
|
if (!this.useFreeWrapper && !this.session.lsd && !this.session.fb_dtsg) {
|
|
await this.initSession();
|
|
}
|
|
return this.session;
|
|
}
|
|
|
|
/**
|
|
* Get the normalized cookie string
|
|
*/
|
|
getCookies(): string {
|
|
return this.cookies;
|
|
}
|
|
|
|
/**
|
|
* Generate images using Meta AI's Imagine model
|
|
*/
|
|
async generate(prompt: string, numImages: number = 4, aspectRatio: AspectRatio = 'portrait'): Promise<MetaImageResult[]> {
|
|
console.log(`[Meta AI] Generating images for: "${prompt.substring(0, 50)}..." (${aspectRatio})`);
|
|
|
|
if (this.useFreeWrapper) {
|
|
return this.generateWithFreeWrapper(prompt, numImages);
|
|
}
|
|
|
|
// First, get the access token and session info if not already fetched
|
|
if (!this.session.accessToken) {
|
|
await this.initSession();
|
|
}
|
|
|
|
// Use "Imagine" prefix for image generation
|
|
const imagePrompt = prompt.toLowerCase().startsWith('imagine')
|
|
? prompt
|
|
: `Imagine ${prompt}`;
|
|
|
|
// Send the prompt via GraphQL with aspect ratio
|
|
const response = await this.sendPrompt(imagePrompt, aspectRatio);
|
|
|
|
// Extract image URLs from response
|
|
const images = this.extractImages(response, prompt);
|
|
|
|
if (images.length === 0) {
|
|
// Meta AI might return the prompt response without images immediately
|
|
// We may need to poll for the result
|
|
console.log("[Meta AI] No images in initial response, polling...");
|
|
return this.pollForImages(response, prompt);
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
/**
|
|
* Generate using free API wrapper (mir-ashiq/metaai-api)
|
|
* Connects to local docker service
|
|
*/
|
|
/**
|
|
* Generate using free API wrapper (mir-ashiq/metaai-api)
|
|
* Connects to local docker service
|
|
*/
|
|
private async generateWithFreeWrapper(prompt: string, numImages: number): Promise<MetaImageResult[]> {
|
|
console.log(`[Meta Wrapper] Generating image for: "${prompt.substring(0, 50)}..." via ${this.freeWrapperUrl}`);
|
|
|
|
const cookieDict = this.parseCookiesToDict(this.cookies);
|
|
|
|
const response = await fetch(`${this.freeWrapperUrl}/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
message: `Imagine ${prompt}`,
|
|
stream: false,
|
|
cookies: cookieDict
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("[Meta Wrapper] Error:", response.status, errorText);
|
|
throw new Error(`Meta Wrapper Error: ${response.status} - ${errorText.substring(0, 200)}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log("[Meta Wrapper] Response:", JSON.stringify(data, null, 2).substring(0, 500));
|
|
|
|
// Check for images in response
|
|
const images: MetaImageResult[] = [];
|
|
|
|
// mir-ashiq/metaai-api returns { media: [{ url: "...", ... }] }
|
|
if (data.media && Array.isArray(data.media)) {
|
|
data.media.forEach((m: any) => {
|
|
if (m.url) images.push({ url: m.url, prompt, model: "meta-wrapper" });
|
|
});
|
|
}
|
|
|
|
// Fallback checks
|
|
if (images.length === 0 && data.images && Array.isArray(data.images)) {
|
|
data.images.forEach((url: string) => {
|
|
images.push({ url, prompt, model: "meta-wrapper" });
|
|
});
|
|
}
|
|
|
|
if (images.length === 0 && data.sources && Array.isArray(data.sources)) {
|
|
data.sources.forEach((s: any) => {
|
|
if (s.url && (s.url.includes('.jpg') || s.url.includes('.png') || s.url.includes('.webp'))) {
|
|
images.push({ url: s.url, prompt, model: "meta-wrapper-source" });
|
|
}
|
|
});
|
|
}
|
|
|
|
if (images.length === 0) {
|
|
console.warn("[Meta Wrapper] No images found via /chat endpoint", data);
|
|
throw new Error("Meta Wrapper returned no images. Please check if the prompt triggered image generation.");
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
private parseCookiesToDict(cookieStr: string): Record<string, string> {
|
|
const dict: Record<string, string> = {};
|
|
if (!cookieStr) return dict;
|
|
|
|
// Handle basic key=value; format
|
|
cookieStr.split(';').forEach(pair => {
|
|
const [key, ...rest] = pair.trim().split('=');
|
|
if (key && rest.length > 0) {
|
|
dict[key] = rest.join('=');
|
|
}
|
|
});
|
|
return dict;
|
|
}
|
|
|
|
private async generateWithFreeWrapperFallback(prompt: string): Promise<MetaImageResult[]> {
|
|
// Fallback logic not needed if /chat works
|
|
throw new Error("Meta Wrapper endpoint /chat failed.");
|
|
}
|
|
|
|
/**
|
|
* Initialize session - get access token from meta.ai page
|
|
*/
|
|
private async initSession(): Promise<void> {
|
|
console.log("[Meta AI] Initializing session...");
|
|
|
|
const response = await fetch(META_AI_BASE, {
|
|
headers: {
|
|
"Cookie": this.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",
|
|
"Sec-Fetch-Site": "same-origin",
|
|
"Sec-Fetch-Mode": "navigate",
|
|
"Sec-Fetch-Dest": "document",
|
|
"Accept-Language": "en-US,en;q=0.9"
|
|
}
|
|
});
|
|
|
|
const html = await response.text();
|
|
|
|
// Extract access token from page HTML
|
|
// Pattern 1: Simple JSON key
|
|
let tokenMatch = html.match(/"accessToken":"([^"]+)"/);
|
|
|
|
// Pattern 2: Inside a config object or different spacing
|
|
if (!tokenMatch) {
|
|
tokenMatch = html.match(/accessToken["']\s*:\s*["']([^"']+)["']/);
|
|
}
|
|
|
|
// Pattern 3: LSD token backup (sometimes needed)
|
|
const lsdMatch = html.match(/"LSD",\[\],{"token":"([^"]+)"/) ||
|
|
html.match(/"lsd":"([^"]+)"/) ||
|
|
html.match(/name="lsd" value="([^"]+)"/);
|
|
if (lsdMatch) {
|
|
this.session.lsd = lsdMatch[1];
|
|
}
|
|
|
|
// Pattern 4: DTSG token (critical for some requests)
|
|
const dtsgMatch = html.match(/"DTSGInitialData".*?"token":"([^"]+)"/) ||
|
|
html.match(/"token":"([^"]+)"/); // Less specific fallback
|
|
|
|
if (tokenMatch) {
|
|
this.session.accessToken = tokenMatch[1];
|
|
console.log("[Meta AI] Got access token");
|
|
} else if (html.includes('login_form') || html.includes('login_page')) {
|
|
throw new Error("Meta AI: Cookies expired or invalid (Login page detected). Please update cookies.");
|
|
} else {
|
|
console.warn("[Meta AI] Warning: Failed to extract access token. functionality may be limited.");
|
|
console.log("HTML Preview:", html.substring(0, 200));
|
|
// Don't throw here, try to proceed with just cookies/LSD
|
|
}
|
|
|
|
if (dtsgMatch) {
|
|
this.session.fb_dtsg = dtsgMatch[1];
|
|
}
|
|
|
|
// Enhanced logging for debugging
|
|
console.log("[Meta AI] Session tokens extracted:", {
|
|
hasAccessToken: !!this.session.accessToken,
|
|
hasLsd: !!this.session.lsd,
|
|
hasDtsg: !!this.session.fb_dtsg
|
|
});
|
|
|
|
if (!this.session.accessToken && !this.session.lsd) {
|
|
console.warn("[Meta AI] CRITICAL: No authentication tokens found. Check if cookies are valid.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send prompt via GraphQL mutation
|
|
*/
|
|
/**
|
|
* Send prompt via GraphQL mutation (Abra - for Image Generation)
|
|
* Using EXACT variable structure from Python Strvm/meta-ai-api library
|
|
*/
|
|
private async sendPrompt(prompt: string, aspectRatio: AspectRatio = 'portrait'): Promise<any> {
|
|
// Generate external conversation ID (UUID) and offline threading ID (Snowflake-like)
|
|
const externalConversationId = crypto.randomUUID();
|
|
const timestamp = Date.now();
|
|
const randomPart = Math.floor(Math.random() * 4194304); // 22 bits
|
|
const offlineThreadingId = ((BigInt(timestamp) << 22n) | BigInt(randomPart)).toString();
|
|
|
|
// Store for polling
|
|
this.session.externalConversationId = externalConversationId;
|
|
|
|
// Map aspect ratio to Meta AI orientation
|
|
const orientation = ORIENTATION_MAP[aspectRatio];
|
|
|
|
const variables = {
|
|
message: {
|
|
sensitive_string_value: prompt // Python uses sensitive_string_value, not text!
|
|
},
|
|
externalConversationId: externalConversationId,
|
|
offlineThreadingId: offlineThreadingId,
|
|
suggestedPromptIndex: null,
|
|
flashVideoRecapInput: { images: [] },
|
|
flashPreviewInput: null,
|
|
promptPrefix: null,
|
|
entrypoint: "ABRA__CHAT__TEXT",
|
|
icebreaker_type: "TEXT",
|
|
imagineClientOptions: { orientation: orientation }, // Aspect ratio control
|
|
__relay_internal__pv__AbraDebugDevOnlyrelayprovider: false,
|
|
__relay_internal__pv__WebPixelRatiorelayprovider: 1
|
|
};
|
|
|
|
console.log("[Meta AI] Sending Variables:", JSON.stringify(variables, null, 2));
|
|
|
|
const body = new URLSearchParams({
|
|
fb_api_caller_class: "RelayModern",
|
|
fb_api_req_friendly_name: "useAbraSendMessageMutation",
|
|
variables: JSON.stringify(variables),
|
|
server_timestamps: "true",
|
|
doc_id: "7783822248314888", // Abra Mutation ID
|
|
...(this.session.lsd && { lsd: this.session.lsd }),
|
|
...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg })
|
|
});
|
|
|
|
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Cookie": this.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 (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-Language": "en-US,en;q=0.9",
|
|
...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` })
|
|
},
|
|
body: body.toString()
|
|
});
|
|
|
|
const rawText = await response.text();
|
|
console.log("[Meta AI] Response received, parsing streaming data...");
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Meta AI Error: ${response.status} - ${rawText.substring(0, 500)}`);
|
|
}
|
|
|
|
// Meta AI returns streaming response (multiple JSON lines)
|
|
// We need to find the final response where streaming_state === "OVERALL_DONE"
|
|
let lastValidResponse: any = null;
|
|
const lines = rawText.split('\n');
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
|
|
// Check for streaming state in both direct and nested paths
|
|
const streamingState =
|
|
parsed?.data?.xfb_abra_send_message?.bot_response_message?.streaming_state ||
|
|
parsed?.data?.node?.bot_response_message?.streaming_state;
|
|
|
|
if (streamingState === "OVERALL_DONE") {
|
|
console.log("[Meta AI] Found OVERALL_DONE response");
|
|
lastValidResponse = parsed;
|
|
break;
|
|
}
|
|
|
|
// Keep track of any valid response with imagine_card
|
|
const imagineCard =
|
|
parsed?.data?.xfb_abra_send_message?.bot_response_message?.imagine_card ||
|
|
parsed?.data?.node?.bot_response_message?.imagine_card;
|
|
if (imagineCard?.session?.media_sets) {
|
|
lastValidResponse = parsed;
|
|
}
|
|
} catch (e) {
|
|
// Skip non-JSON lines
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!lastValidResponse) {
|
|
if (rawText.includes("login_form") || rawText.includes("facebook.com/login")) {
|
|
throw new Error("Meta AI: Session expired. Please refresh your cookies.");
|
|
}
|
|
throw new Error("Meta AI: No valid response found in streaming data");
|
|
}
|
|
|
|
console.log("[Meta AI] Successfully parsed streaming response");
|
|
return lastValidResponse;
|
|
}
|
|
|
|
/**
|
|
* Extract image URLs from Meta AI response
|
|
*/
|
|
private extractImages(response: any, originalPrompt: string): MetaImageResult[] {
|
|
const images: MetaImageResult[] = [];
|
|
|
|
// Navigate through the response structure
|
|
// Abra streaming: xfb_abra_send_message.bot_response_message
|
|
// Abra direct: node.bot_response_message
|
|
// Kadabra: useKadabraSendMessageMutation.node.bot_response_message
|
|
const messageData = response?.data?.xfb_abra_send_message?.bot_response_message ||
|
|
response?.data?.useKadabraSendMessageMutation?.node?.bot_response_message ||
|
|
response?.data?.node?.bot_response_message ||
|
|
response?.data?.xabraAIPreviewMessageSendMutation?.message;
|
|
|
|
// For Polling/KadabraPromptRootQuery which also contains imagine_cards
|
|
const pollMessages = response?.data?.kadabra_prompt?.messages?.edges || [];
|
|
if (pollMessages.length > 0) {
|
|
for (const edge of pollMessages) {
|
|
const nodeImages = this.extractImagesFromMessage(edge?.node, originalPrompt);
|
|
images.push(...nodeImages);
|
|
}
|
|
}
|
|
|
|
if (messageData) {
|
|
images.push(...this.extractImagesFromMessage(messageData, originalPrompt));
|
|
}
|
|
|
|
// --- STRENGTHENED FALLBACK ---
|
|
// If still no images, but we have data, do a recursive search for any CDN URLs
|
|
if (images.length === 0 && response?.data) {
|
|
console.log("[Meta AI] Structured extraction failed, attempting recursive search...");
|
|
const foundUrls = this.recursiveSearchForImages(response.data);
|
|
for (const url of foundUrls) {
|
|
images.push({
|
|
url: url,
|
|
prompt: originalPrompt,
|
|
model: "meta"
|
|
});
|
|
}
|
|
}
|
|
|
|
if (images.length === 0) {
|
|
console.log("[Meta AI] Extraction failed. Response keys:", Object.keys(response || {}));
|
|
if (response?.data) console.log("[Meta AI] Data keys:", Object.keys(response.data));
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
/**
|
|
* Recursive search for image-like URLs in the JSON tree
|
|
*/
|
|
private recursiveSearchForImages(obj: any, found: Set<string> = new Set()): string[] {
|
|
if (!obj || typeof obj !== 'object') return [];
|
|
|
|
for (const key in obj) {
|
|
const val = obj[key];
|
|
if (typeof val === 'string') {
|
|
if ((val.includes('fbcdn.net') || val.includes('meta.ai')) &&
|
|
(val.includes('.jpg') || val.includes('.png') || val.includes('.webp') || val.includes('image_uri=') || val.includes('/imagine/'))) {
|
|
found.add(val);
|
|
}
|
|
} else if (typeof val === 'object') {
|
|
this.recursiveSearchForImages(val, found);
|
|
}
|
|
}
|
|
return Array.from(found);
|
|
}
|
|
|
|
/**
|
|
* Helper to extract images from a single message node
|
|
*/
|
|
private extractImagesFromMessage(messageData: any, originalPrompt: string): MetaImageResult[] {
|
|
const images: MetaImageResult[] = [];
|
|
if (!messageData) return images;
|
|
|
|
// Check for imagine_card (image generation response)
|
|
const imagineCard = messageData?.imagine_card;
|
|
if (imagineCard?.session?.media_sets) {
|
|
for (const mediaSet of imagineCard.session.media_sets) {
|
|
if (mediaSet?.imagine_media) {
|
|
for (const media of mediaSet.imagine_media) {
|
|
const url = media?.uri || media?.image_uri || media?.image?.uri;
|
|
if (url) {
|
|
images.push({
|
|
url: url,
|
|
prompt: originalPrompt,
|
|
model: "meta"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for attachments (alternative path)
|
|
const attachments = messageData?.attachments;
|
|
if (attachments) {
|
|
for (const attachment of attachments) {
|
|
const media = attachment?.media;
|
|
const url = media?.image_uri || media?.uri || media?.image?.uri;
|
|
if (url) {
|
|
images.push({
|
|
url: url,
|
|
prompt: originalPrompt,
|
|
model: "meta"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
/**
|
|
* Poll for image generation completion
|
|
*/
|
|
private async pollForImages(initialResponse: any, prompt: string): Promise<MetaImageResult[]> {
|
|
// Kadabra uses external_conversation_id for polling
|
|
const conversationId = initialResponse?.data?.useKadabraSendMessageMutation?.node?.external_conversation_id ||
|
|
initialResponse?.data?.node?.external_conversation_id;
|
|
|
|
if (!conversationId) {
|
|
console.warn("[Meta AI] No conversation ID found for polling");
|
|
return [];
|
|
}
|
|
|
|
const maxAttempts = 30;
|
|
const pollInterval = 2000;
|
|
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
console.log(`[Meta AI] 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: "25290569913909283", // KadabraPromptRootQuery ID
|
|
...(this.session.lsd && { lsd: this.session.lsd }),
|
|
...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg })
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Cookie": this.cookies,
|
|
"Origin": META_AI_BASE,
|
|
...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` })
|
|
},
|
|
body: body.toString()
|
|
});
|
|
|
|
const data = await response.json();
|
|
const images = this.extractImages(data, prompt);
|
|
|
|
if (images.length > 0) {
|
|
console.log(`[Meta AI] Got ${images.length} image(s) after polling!`);
|
|
return images;
|
|
}
|
|
|
|
// Check for failure status
|
|
const status = data?.data?.kadabra_prompt?.status;
|
|
if (status === "FAILED" || status === "ERROR") {
|
|
console.error("[Meta AI] Generation failed during polling");
|
|
break;
|
|
}
|
|
} catch (e: any) {
|
|
console.error("[Meta AI] Poll error:", e.message);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Download image from URL and convert to base64
|
|
*/
|
|
async downloadAsBase64(url: string): Promise<string> {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
"Cookie": this.cookies,
|
|
"Referer": META_AI_BASE
|
|
}
|
|
});
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer).toString('base64');
|
|
}
|
|
}
|