Files
cardex/src/ai/flows/generate-card-video.ts
T

192 lines
6.6 KiB
TypeScript

// This file handles video generation for Pokemon cards using Veo 3 model
'use server';
/**
* @fileOverview Uses Google Veo 3 to generate animated videos of Pokemon cards.
* Note: This is a placeholder implementation until Veo 3 API is fully available
*/
import { GoogleGenAI } from '@google/genai';
import { getUserApiKeys } from '@/lib/firestore';
export interface GenerateCardVideoInput {
cardImageUrl: string;
pokemonName: string;
pokemonType: string;
customPrompt?: string;
userId?: string;
}
export interface GenerateCardVideoOutput {
videoBase64?: string;
prompt?: string;
error?: string;
}
export async function generateCardVideo(input: GenerateCardVideoInput): Promise<GenerateCardVideoOutput> {
try {
console.log('Starting video generation for:', input.pokemonName);
// Get user API keys first if userId is provided
let apiKey: string | undefined;
if (input.userId) {
try {
const userApiKeys = await getUserApiKeys(input.userId);
apiKey = userApiKeys?.geminiApiKey;
} catch (error) {
console.warn('Failed to get user API keys:', error);
}
}
// Fallback to environment variables if no user API key
if (!apiKey) {
apiKey = process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY;
}
if (!apiKey) {
return { error: 'Google AI API key is required. Please set your Gemini API key in settings or contact support.' };
}
// Create the animation prompt
const animationPrompt = input.customPrompt ||
`Take this existing Pokemon card image of ${input.pokemonName}, a ${input.pokemonType}-type Pokemon, and create a magical animated video where:
- The Pokemon comes to life and moves naturally within the card frame
- Add sparkles, glowing effects, and ${input.pokemonType}-type elemental effects
- The background has subtle movement and atmospheric effects
- The card itself has a gentle holographic shimmer
- Keep the card frame intact and readable
- Make it feel like the Pokemon is truly alive and magical
- Use the provided card image as the base for animation`;
console.log('Generated animation prompt:', animationPrompt);
// Fetch and convert image to base64
console.log('Fetching image from URL:', input.cardImageUrl);
const imageResponse = await fetch(input.cardImageUrl);
if (!imageResponse.ok) {
return { error: `Failed to fetch image: ${imageResponse.statusText}` };
}
const imageBuffer = await imageResponse.arrayBuffer();
const base64Image = Buffer.from(imageBuffer).toString('base64');
const mimeType = imageResponse.headers.get('content-type') || 'image/jpeg';
console.log('Image converted to base64, mime type:', mimeType);
// Initialize Google GenAI client
const ai = new GoogleGenAI({ apiKey });
// Start video generation operation
console.log('Starting Veo video generation...');
let operation = await ai.models.generateVideos({
model: 'veo-2.0-generate-001',
prompt: animationPrompt,
image: {
mimeType: mimeType,
imageBytes: base64Image
},
config: {
numberOfVideos: 1,
aspectRatio: '9:16',
durationSeconds: 5, // Set duration to 5 seconds
}
});
// Poll for completion
console.log('Waiting for video generation to complete...');
let pollCount = 0;
const maxPolls = 60; // Maximum 10 minutes (60 * 10 seconds)
while (!operation.done && pollCount < maxPolls) {
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
try {
operation = await ai.operations.getVideosOperation({operation: operation});
pollCount++;
console.log(`Video generation status: ${operation.done ? 'completed' : 'in progress'} (poll ${pollCount}/${maxPolls})`);
} catch (pollError) {
console.error('Error polling for video status:', pollError);
break;
}
}
if (!operation.done) {
return {
error: 'Video generation timed out after 10 minutes. The process is taking longer than expected. Please try again later.'
};
}
// Check if video was generated successfully
if (!operation.response?.generatedVideos?.[0]?.video) {
console.error('No video in operation response:', operation.response);
return {
error: 'No video generated by Veo model'
};
}
const generatedVideo = operation.response.generatedVideos[0].video;
console.log('Video generation completed. Video data available:', Object.keys(generatedVideo));
// Handle video data - we need to download it if it's a URI since HTML video can't handle authenticated URLs
if (generatedVideo.uri) {
console.log('Video generated with URI, downloading video data...');
try {
// Download the video using the correct API key parameter format
const downloadUrl = `${generatedVideo.uri}&key=${apiKey}`;
console.log('Downloading from URL:', downloadUrl);
const videoResponse = await fetch(downloadUrl);
if (!videoResponse.ok) {
console.error('Failed to download video:', videoResponse.status, videoResponse.statusText);
return {
error: `Failed to download generated video: ${videoResponse.status} ${videoResponse.statusText}`
};
}
// Convert to base64 for storage/transmission
const videoBuffer = await videoResponse.arrayBuffer();
const videoBase64 = Buffer.from(videoBuffer).toString('base64');
console.log('Video successfully downloaded and converted to base64');
return {
videoBase64: videoBase64,
prompt: animationPrompt,
};
} catch (downloadError) {
console.error('Error downloading video:', downloadError);
return {
error: `Failed to download video: ${downloadError instanceof Error ? downloadError.message : 'Unknown error'}`
};
}
} else if (generatedVideo.videoBytes) {
console.log('Video generated with direct bytes');
return {
videoBase64: generatedVideo.videoBytes,
prompt: animationPrompt,
};
} else {
console.error('No usable video data in response');
return {
error: 'Video was generated but no accessible video data was provided'
};
}
} catch (error) {
console.error('Error in video generation setup:', error);
let errorMessage = 'Failed to set up video generation';
if (error instanceof Error) {
errorMessage = error.message;
}
return {
error: errorMessage,
};
}
}