feat: add Meta AI video generation
Some checks are pending
CI / build (18.x) (push) Waiting to run
CI / build (20.x) (push) Waiting to run

- 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:
Khoa.vo 2026-01-06 13:52:31 +07:00
parent 2a4bf8b58b
commit 0f87b8ef99
7 changed files with 345 additions and 4 deletions

View file

@ -7,3 +7,4 @@ README.md
.git
.env*
! .env.example
.venv

1
.gitignore vendored
View file

@ -45,3 +45,4 @@ next-env.d.ts
.venv
error.log
__pycache__
*.log

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

View file

@ -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>

View file

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

View file

@ -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)
)

View file

@ -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