mirror of
https://github.com/xavidop/cardex.git
synced 2026-08-28 23:01:22 +00:00
184 lines
7.3 KiB
TypeScript
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.`;
|
|
}
|