mirror of
https://github.com/xavidop/cardex.git
synced 2026-08-29 01:08:52 +00:00
192 lines
6.6 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|