diff --git a/.dockerignore b/.dockerignore index 135fa6c..c6a0400 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ README.md .git .env* ! .env.example +.venv diff --git a/.gitignore b/.gitignore index 486b0ec..cfd72ff 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ next-env.d.ts .venv error.log __pycache__ +*.log diff --git a/app/api/meta/video/route.ts b/app/api/meta/video/route.ts new file mode 100644 index 0000000..d6db68a --- /dev/null +++ b/app/api/meta/video/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MetaCrawlClient } from '@/lib/providers/meta-crawl-client'; + +/** + * POST /api/meta/video + * + * Generate a video from a text prompt using Meta AI. + * Video generation takes 30-60+ seconds, so this endpoint may take a while. + */ +export async function POST(req: NextRequest) { + try { + const { prompt, cookies: clientCookies } = 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) { + return NextResponse.json( + { error: "Meta AI cookies not found. Please configure settings." }, + { status: 401 } + ); + } + + console.log(`[Meta Video API] Starting video generation for prompt: "${prompt.substring(0, 50)}..."`); + + const client = new MetaCrawlClient(); + + // 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 } + ); + } + + // Generate video - this can take 30-60+ seconds + const result = await client.generateVideo(prompt, cookieString); + + 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 + }); + + } 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"); + + return NextResponse.json( + { error: err.message || "Video generation failed" }, + { status: isAuthError ? 401 : 500 } + ); + } +} diff --git a/components/PromptHero.tsx b/components/PromptHero.tsx index b73988e..03214b7 100644 --- a/components/PromptHero.tsx +++ b/components/PromptHero.tsx @@ -3,7 +3,7 @@ import React, { useRef, useState, useEffect } from "react"; import { useStore, ReferenceCategory } from "@/lib/store"; import { cn } from "@/lib/utils"; -import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2 } from "lucide-react"; +import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2, Video } from "lucide-react"; const IMAGE_COUNTS = [1, 2, 4]; @@ -19,6 +19,7 @@ export function PromptHero() { } = useStore(); const [isGenerating, setLocalIsGenerating] = useState(false); + const [isGeneratingVideo, setIsGeneratingVideo] = useState(false); const [uploadingRefs, setUploadingRefs] = useState>({}); const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null); const textareaRef = useRef(null); @@ -204,6 +205,76 @@ export function PromptHero() { } }; + // Handle video generation (Meta AI only) + const handleGenerateVideo = async () => { + let finalPrompt = prompt.trim(); + if (!finalPrompt || isGeneratingVideo || settings.provider !== 'meta') return; + + setIsGeneratingVideo(true); + setIsGenerating(true); + + try { + console.log('[PromptHero] Starting Meta AI video generation...'); + + const res = await fetch('/api/meta/video', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: finalPrompt, + cookies: settings.metaCookies + }) + }); + + const responseText = await res.text(); + let data; + try { + data = JSON.parse(responseText); + } catch (e) { + console.error("API Error (Non-JSON response):", responseText.substring(0, 500)); + throw new Error(`Server Error: ${res.status} ${res.statusText}`); + } + + if (data.error) throw new Error(data.error); + + if (data.videos && data.videos.length > 0) { + // Add videos to store + for (const video of data.videos) { + useStore.getState().addVideo({ + id: crypto.randomUUID(), + url: video.url, + prompt: video.prompt || finalPrompt, + createdAt: Date.now() + }); + } + // Show success notification + setErrorNotification({ + message: `🎬 Success! Generated ${data.videos.length} video(s). Check the Videos tab.`, + type: 'warning' // Using warning for visibility (amber color) + }); + setTimeout(() => setErrorNotification(null), 5000); + } else { + throw new Error('No videos were generated'); + } + + } catch (e: any) { + console.error('[Video Gen]', e); + const errorMessage = e.message || ''; + + if (errorMessage.includes('401') || errorMessage.includes('cookies')) { + setShowCookieExpired(true); + } + + setErrorNotification({ + message: `🎬 Video Error: ${errorMessage}`, + type: 'error' + }); + setTimeout(() => setErrorNotification(null), 8000); + } finally { + setIsGeneratingVideo(false); + setIsGenerating(false); + } + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); @@ -626,7 +697,7 @@ export function PromptHero() { )} >
- {isGenerating ? ( + {isGenerating && !isGeneratingVideo ? ( <>
Dreaming... @@ -643,6 +714,35 @@ export function PromptHero() { )}
+ + {/* Generate Video Button - Only for Meta AI */} + {settings.provider === 'meta' && ( + + )}
diff --git a/lib/providers/meta-crawl-client.ts b/lib/providers/meta-crawl-client.ts index a1b0a83..9b93bda 100644 --- a/lib/providers/meta-crawl-client.ts +++ b/lib/providers/meta-crawl-client.ts @@ -171,4 +171,52 @@ export class MetaCrawlClient { const response = await fetch(`${this.baseUrl}/rate-limit`); return response.json(); } + + /** + * Generate video from text prompt using Meta AI + * Video generation takes longer than image generation (30-60+ seconds) + */ + async generateVideo( + prompt: string, + cookies: string + ): Promise { + console.log(`[MetaCrawl] Sending video request to ${this.baseUrl}/video/generate`); + + const response = await fetch(`${this.baseUrl}/video/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt, + cookies + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`MetaCrawl video service error: ${response.status} - ${errorText}`); + } + + const data: MetaCrawlVideoResponse = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Video generation failed'); + } + + return data; + } +} + +export interface MetaCrawlVideo { + url: string; + prompt: string; + model: string; +} + +export interface MetaCrawlVideoResponse { + success: boolean; + videos: MetaCrawlVideo[]; + error?: string; + conversation_id?: string; } diff --git a/services/crawl4ai/app/main.py b/services/crawl4ai/app/main.py index 306d1cb..3f9e6d8 100644 --- a/services/crawl4ai/app/main.py +++ b/services/crawl4ai/app/main.py @@ -19,7 +19,10 @@ from .models import ( TaskStatusResponse, HealthResponse, GrokChatRequest, - GrokChatResponse + GrokChatResponse, + VideoGenerateRequest, + VideoGenerateResponse, + VideoResult ) from .grok_client import GrokChatClient from .meta_crawler import meta_crawler @@ -171,7 +174,6 @@ async def delete_task(task_id: str): raise HTTPException(status_code=404, detail="Task not found") - @app.post("/grok/chat", response_model=GrokChatResponse) async def grok_chat(request: GrokChatRequest): """ @@ -182,3 +184,102 @@ async def grok_chat(request: GrokChatRequest): return GrokChatResponse(response=response) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/video/generate", response_model=VideoGenerateResponse) +async def generate_video(request: VideoGenerateRequest): + """ + Generate a video from a text prompt using Meta AI. + + This uses the metaai_api library's video generation feature. + Video generation takes longer than image generation (30-60+ seconds). + + Requires: + - prompt: The video generation prompt + - cookies: Facebook/Meta cookies (JSON array or string format) + """ + import json + import asyncio + from concurrent.futures import ThreadPoolExecutor + + try: + # Parse cookies to dict format for MetaAI + cookies_str = request.cookies.strip() + cookies_dict = {} + + if cookies_str.startswith('['): + # JSON array format from Cookie Editor + parsed = json.loads(cookies_str) + if isinstance(parsed, list): + cookies_dict = {c['name']: c['value'] for c in parsed if 'name' in c and 'value' in c} + else: + # String format: "name1=value1; name2=value2" + for pair in cookies_str.split(';'): + pair = pair.strip() + if '=' in pair: + name, value = pair.split('=', 1) + cookies_dict[name.strip()] = value.strip() + + if not cookies_dict: + return VideoGenerateResponse( + success=False, + videos=[], + error="No valid cookies provided" + ) + + print(f"[VideoGen] Starting video generation for: '{request.prompt[:50]}...'") + + # Import MetaAI and run video generation in thread pool (it's synchronous) + from metaai_api import MetaAI + + def run_video_gen(): + ai = MetaAI(cookies=cookies_dict) + return ai.generate_video( + prompt=request.prompt, + wait_before_poll=10, + max_attempts=60, # Up to 5 minutes of polling + wait_seconds=5, + verbose=True + ) + + # Run in thread pool since metaai_api is synchronous + loop = asyncio.get_event_loop() + with ThreadPoolExecutor(max_workers=1) as executor: + result = await loop.run_in_executor(executor, run_video_gen) + + if not result.get('success', False): + error_msg = result.get('error', 'Video generation failed') + print(f"[VideoGen] Failed: {error_msg}") + return VideoGenerateResponse( + success=False, + videos=[], + error=error_msg + ) + + video_urls = result.get('video_urls', []) + print(f"[VideoGen] Success! Got {len(video_urls)} video(s)") + + videos = [ + VideoResult( + url=url, + prompt=request.prompt, + model="meta_video" + ) + for url in video_urls + ] + + return VideoGenerateResponse( + success=True, + videos=videos, + conversation_id=result.get('conversation_id') + ) + + except Exception as e: + import traceback + traceback.print_exc() + print(f"[VideoGen] Error: {str(e)}") + return VideoGenerateResponse( + success=False, + videos=[], + error=str(e) + ) diff --git a/services/crawl4ai/app/models.py b/services/crawl4ai/app/models.py index 9726e85..2ba6b42 100644 --- a/services/crawl4ai/app/models.py +++ b/services/crawl4ai/app/models.py @@ -64,3 +64,24 @@ class HealthResponse(BaseModel): status: str = "healthy" version: str = "1.0.0" browser_ready: bool = True + + +class VideoGenerateRequest(BaseModel): + """Request model for video generation""" + prompt: str = Field(..., description="Video generation prompt", min_length=1) + cookies: str = Field(..., description="Meta AI session cookies") + + +class VideoResult(BaseModel): + """Single generated video result""" + url: str + prompt: str + model: str = "meta_video" + + +class VideoGenerateResponse(BaseModel): + """Response model for video generation""" + success: bool + videos: list[VideoResult] = [] + error: Optional[str] = None + conversation_id: Optional[str] = None