feat: add Meta AI video generation
- Add /video/generate endpoint to crawl4ai Python service - Add VideoGenerateRequest and VideoGenerateResponse models - Add generateVideo method to MetaCrawlClient TypeScript client - Add /api/meta/video Next.js API route - Add 'Video' button in PromptHero UI (visible only for Meta AI provider) - Blue/cyan gradient styling for Video button to differentiate from Generate
This commit is contained in:
parent
2a4bf8b58b
commit
0f87b8ef99
7 changed files with 345 additions and 4 deletions
|
|
@ -7,3 +7,4 @@ README.md
|
|||
.git
|
||||
.env*
|
||||
! .env.example
|
||||
.venv
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -45,3 +45,4 @@ next-env.d.ts
|
|||
.venv
|
||||
error.log
|
||||
__pycache__
|
||||
*.log
|
||||
|
|
|
|||
69
app/api/meta/video/route.ts
Normal file
69
app/api/meta/video/route.ts
Normal file
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Record<string, boolean>>({});
|
||||
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(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() {
|
|||
)}
|
||||
>
|
||||
<div className="relative z-10 flex items-center gap-1.5">
|
||||
{isGenerating ? (
|
||||
{isGenerating && !isGeneratingVideo ? (
|
||||
<>
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
<span className="animate-pulse">Dreaming...</span>
|
||||
|
|
@ -643,6 +714,35 @@ export function PromptHero() {
|
|||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Generate Video Button - Only for Meta AI */}
|
||||
{settings.provider === 'meta' && (
|
||||
<button
|
||||
onClick={handleGenerateVideo}
|
||||
disabled={isGenerating || !prompt.trim()}
|
||||
className={cn(
|
||||
"relative overflow-hidden px-4 py-1.5 rounded-lg font-bold text-sm text-white shadow-lg transition-all active:scale-95 group border border-white/10",
|
||||
isGenerating
|
||||
? "bg-gray-700 cursor-not-allowed"
|
||||
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 hover:shadow-cyan-500/25"
|
||||
)}
|
||||
title="Generate video from prompt (30-60+ seconds)"
|
||||
>
|
||||
<div className="relative z-10 flex items-center gap-1.5">
|
||||
{isGeneratingVideo ? (
|
||||
<>
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
<span className="animate-pulse">Creating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Video className="h-3 w-3 group-hover:scale-110 transition-transform" />
|
||||
<span>Video</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MetaCrawlVideoResponse> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue