feat: enable Subject upload for Meta AI video generation
- Subject button now enabled for Meta AI (Scene/Style still disabled) - Video button appears when Subject reference is uploaded - Uses Subject image for image-to-video generation - Added handleGenerateVideo() in PromptHero - Tooltip: 'Upload image to animate into video'
This commit is contained in:
parent
7aaa4c8166
commit
c2ee01b7b7
1 changed files with 117 additions and 4 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { useRef, useState, useEffect } from "react";
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { useStore, ReferenceCategory } from "@/lib/store";
|
import { useStore, ReferenceCategory } from "@/lib/store";
|
||||||
import { cn } from "@/lib/utils";
|
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];
|
const IMAGE_COUNTS = [1, 2, 4];
|
||||||
|
|
||||||
|
|
@ -19,6 +19,8 @@ export function PromptHero() {
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const [isGenerating, setLocalIsGenerating] = useState(false);
|
const [isGenerating, setLocalIsGenerating] = useState(false);
|
||||||
|
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||||
|
const { addVideo } = useStore();
|
||||||
|
|
||||||
const [uploadingRefs, setUploadingRefs] = useState<Record<string, boolean>>({});
|
const [uploadingRefs, setUploadingRefs] = useState<Record<string, boolean>>({});
|
||||||
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
||||||
|
|
@ -205,6 +207,80 @@ export function PromptHero() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle video generation (Meta AI only) - uses Subject reference image
|
||||||
|
const handleGenerateVideo = async () => {
|
||||||
|
const subjectRefs = references.subject || [];
|
||||||
|
if (subjectRefs.length === 0) {
|
||||||
|
setErrorNotification({ message: '📷 Please upload a Subject image first', type: 'warning' });
|
||||||
|
setTimeout(() => setErrorNotification(null), 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPrompt = prompt.trim() || "Animate this image with natural, cinematic movement";
|
||||||
|
|
||||||
|
if (!settings.metaCookies) {
|
||||||
|
setShowCookieExpired(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGeneratingVideo(true);
|
||||||
|
setIsGenerating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imageBase64 = subjectRefs[0].thumbnail;
|
||||||
|
console.log('[PromptHero] Starting Meta AI image-to-video...');
|
||||||
|
|
||||||
|
const res = await fetch('/api/meta/video', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: finalPrompt,
|
||||||
|
cookies: settings.metaCookies,
|
||||||
|
imageBase64: imageBase64
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
|
if (data.success && data.videos?.length > 0) {
|
||||||
|
for (const video of data.videos) {
|
||||||
|
addVideo({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
url: video.url,
|
||||||
|
prompt: video.prompt || finalPrompt,
|
||||||
|
thumbnail: imageBase64,
|
||||||
|
createdAt: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setErrorNotification({
|
||||||
|
message: `🎬 Video generated! Check the gallery.`,
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
setTimeout(() => setErrorNotification(null), 5000);
|
||||||
|
} else {
|
||||||
|
throw new Error('No videos generated');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[Video Gen]', e);
|
||||||
|
const errorMessage = e.message || 'Video generation failed';
|
||||||
|
|
||||||
|
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) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -488,18 +564,26 @@ export function PromptHero() {
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pt-1">
|
||||||
|
|
||||||
{/* Left Controls: References */}
|
{/* Left Controls: References */}
|
||||||
{/* For Meta AI: References are disabled (generate images first, video from gallery) */}
|
{/* For Meta AI: Only Subject is enabled (for video generation), Scene/Style disabled */}
|
||||||
<div className={cn("flex flex-wrap gap-2", settings.provider === 'meta' && "opacity-30 pointer-events-none grayscale")}>
|
<div className="flex flex-wrap gap-2">
|
||||||
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
{(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
|
||||||
const refs = references[cat] || [];
|
const refs = references[cat] || [];
|
||||||
const hasRefs = refs.length > 0;
|
const hasRefs = refs.length > 0;
|
||||||
const isUploading = uploadingRefs[cat];
|
const isUploading = uploadingRefs[cat];
|
||||||
|
// For Meta AI: only Subject is enabled (for image-to-video), Scene/Style disabled
|
||||||
|
const isDisabledForMeta = settings.provider === 'meta' && cat !== 'subject';
|
||||||
return (
|
return (
|
||||||
<div key={cat} className="relative group">
|
<div key={cat} className={cn(
|
||||||
|
"relative group",
|
||||||
|
isDisabledForMeta && "opacity-30 pointer-events-none grayscale"
|
||||||
|
)}>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleReference(cat)}
|
onClick={() => toggleReference(cat)}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDrop(e, cat)}
|
onDrop={(e) => handleDrop(e, cat)}
|
||||||
|
title={settings.provider === 'meta' && cat === 'subject'
|
||||||
|
? "Upload image to animate into video"
|
||||||
|
: undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[10px] font-medium transition-all border relative overflow-hidden",
|
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[10px] font-medium transition-all border relative overflow-hidden",
|
||||||
hasRefs
|
hasRefs
|
||||||
|
|
@ -645,6 +729,35 @@ export function PromptHero() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Generate Video Button - Only for Meta AI when Subject is uploaded */}
|
||||||
|
{settings.provider === 'meta' && (references.subject?.length ?? 0) > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleGenerateVideo}
|
||||||
|
disabled={isGenerating}
|
||||||
|
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="Animate the subject image into video"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue