Files
cardex-xavidop/src/ai/flows/generate-pokemon-card.ts
T
2025-11-20 18:09:06 +01:00

184 lines
7.3 KiB
TypeScript

// This file is automatically generated - edits will be lost!
'use server';
/**
* @fileOverview Uses Google Imagen4 to generate custom Pokemon cards based on user parameters.
*
* - generatePokemonCard - A function that handles the card generation process.
* - GeneratePokemonCardInput - The input type for the generatePokemonCard function.
* - GeneratePokemonCardOutput - The return type for the generatePokemonCard function.
*/
import { z } from 'genkit';
import { GoogleGenAI } from '@google/genai';
import { getUserApiKeys } from '@/lib/firestore';
const GeneratePokemonCardInputSchema = z.object({
userId: z.string().min(1, "User ID is required"),
pokemonName: z.string().min(1, "Pokemon name is required"),
pokemonType: z.string().min(1, "Pokemon type is required"),
isIllustrationRare: z.boolean(),
isHolo: z.boolean(),
backgroundDescription: z.string().min(1, "Background description is required"),
pokemonDescription: z.string().min(1, "Pokemon description is required"),
language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']),
model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']).default('imagen-4.0-ultra-generate-001'),
hp: z.number().optional(),
attackName1: z.string().optional(),
attackDamage1: z.number().optional(),
attackName2: z.string().optional(),
attackDamage2: z.number().optional(),
weakness: z.string().optional(),
resistance: z.string().optional(),
retreatCost: z.number().optional(),
});
export type GeneratePokemonCardInput = z.infer<typeof GeneratePokemonCardInputSchema>;
const GeneratePokemonCardOutputSchema = z.object({
imageBase64: z.string().describe("The generated card image as base64 string").optional(),
prompt: z.string().describe("The prompt used to generate the card").optional(),
error: z.string().describe("Error message if the card could not be generated").optional()
});
export type GeneratePokemonCardOutput = z.infer<typeof GeneratePokemonCardOutputSchema>;
export async function generatePokemonCard(input: GeneratePokemonCardInput): Promise<GeneratePokemonCardOutput> {
try {
// Get user's API keys from Firestore
const userApiKeys = await getUserApiKeys(input.userId);
const apiKey = userApiKeys?.geminiApiKey || process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY;
if (!apiKey) {
return {
error: 'Gemini API key is required. Please configure your API key in Settings or set GOOGLE_GENAI_API_KEY environment variable.'
};
}
const genAI = new GoogleGenAI({
apiKey: apiKey,
});
// Generate the detailed prompt for the Pokemon card
const prompt = generateCardPrompt(input);
console.log("Generated prompt:", prompt);
// Check if using Gemini model (for generateContentStream) or Imagen model (for generateImages)
const isGeminiModel = input.model.startsWith('gemini-');
if (isGeminiModel) {
// Use generateContentStream for Gemini models
const config = {
responseModalities: ['IMAGE', 'TEXT'],
imageConfig: {
aspectRatio: '3:4',
imageSize: '1K',
},
};
const contents = [
{
role: 'user' as const,
parts: [
{
text: prompt,
},
],
},
];
const response = await genAI.models.generateContentStream({
model: input.model,
config,
contents,
});
let imageBase64: string | undefined;
for await (const chunk of response) {
if (!chunk.candidates || !chunk.candidates[0].content || !chunk.candidates[0].content.parts) {
continue;
}
if (chunk.candidates?.[0]?.content?.parts?.[0]?.inlineData) {
const inlineData = chunk.candidates[0].content.parts[0].inlineData;
imageBase64 = inlineData.data || '';
break; // We only need the first image
}
}
if (!imageBase64) {
return { error: 'No image generated from Gemini model.' };
}
return {
imageBase64,
prompt,
};
} else {
// Use generateImages for Imagen models
const response = await genAI.models.generateImages({
model: input.model,
prompt: prompt,
config: {
numberOfImages: 1,
outputMimeType: 'image/jpeg',
aspectRatio: '3:4' // Pokemon cards are roughly 3:4 aspect ratio
},
});
if (!response?.generatedImages || response.generatedImages.length === 0) {
return { error: 'No images generated.' };
}
const generatedImage = response.generatedImages[0];
if (!generatedImage?.image?.imageBytes) {
return { error: 'Generated image data is missing.' };
}
return {
imageBase64: generatedImage.image.imageBytes,
prompt: prompt,
};
}
} catch (error: any) {
console.error('Error generating Pokemon card:', error);
return { error: `Failed to generate Pokemon card: ${error?.message || 'Unknown error'}` };
}
}
function generateCardPrompt(params: GeneratePokemonCardInput): string {
const {
pokemonName,
pokemonType,
isIllustrationRare,
isHolo,
backgroundDescription,
pokemonDescription,
language,
hp = 130,
attackName1 = "Quick Attack",
attackDamage1 = 60,
attackName2 = "Special Move",
attackDamage2 = 90,
weakness = "Fighting",
resistance = "Psychic",
retreatCost = 2
} = params;
const cardType = isIllustrationRare ? "full art (ART)" : "Regular classic and not full art";
const holoEffect = isHolo ? "The card's surface has a \"holo\" effect, creating a shimmering, rainbow-like reflection that highlights the Pokémon and key elements." : "";
const languageInstruction = language !== 'english' ? `Write all card information in ${language}. The Pokemon name, attacks and description should be translated accordingly.` : '';
return `A ${cardType} collectible Pokémon trading card, featuring the Pokémon "${pokemonName}". ${isIllustrationRare ? "The illustration covers the entire card" : "The card has a standard vertical layout with a detailed illustration in the main area."} ${pokemonName}, a ${pokemonType} type, is depicted ${pokemonDescription}. The background of the illustration shows ${backgroundDescription}. ${holoEffect}
The card layout includes: The top left corner displays "${pokemonName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${pokemonType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1} and should be written in ${language}, deals ${attackDamage1} damage. The second attack is ${attackName2} and should be written in ${language}, deals ${attackDamage2} damage.
Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information.
${languageInstruction}
The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork.
Make sure the information is displayed clearly.
The output should just be the card.`;
}