import { GoogleGenAI } from "@google/genai"; // eslint-disable-next-line @typescript-eslint/no-var-requires const mime = require("mime") as any; import path from "path"; import { ImageGenerationOptions, ImageGenerationResult, ReferenceImage, } from "../types/api"; import { StorageFactory } from "./StorageFactory"; export class ImageGenService { private ai: GoogleGenAI; private primaryModel = "gemini-2.5-flash-image-preview"; private fallbackModel = "imagen-4.0-generate-001"; constructor(apiKey: string) { if (!apiKey) { throw new Error("Gemini API key is required"); } this.ai = new GoogleGenAI({ apiKey }); } /** * Generate an image from text prompt with optional reference images */ async generateImage( options: ImageGenerationOptions, ): Promise { const { prompt, filename, referenceImages, orgId, projectId, userId } = options; const timestamp = new Date().toISOString(); // Use default values if not provided const finalOrgId = orgId || process.env['DEFAULT_ORG_ID'] || 'default'; const finalProjectId = projectId || process.env['DEFAULT_PROJECT_ID'] || 'main'; const finalUserId = userId || process.env['DEFAULT_USER_ID'] || 'system'; console.log( `[${timestamp}] Starting image generation: "${prompt.substring(0, 50)}..." for ${finalOrgId}/${finalProjectId}`, ); try { // First try the primary model (Nano Banana) const result = await this.tryGeneration({ model: this.primaryModel, config: { responseModalities: ["IMAGE", "TEXT"] }, prompt, filename, orgId: finalOrgId, projectId: finalProjectId, userId: finalUserId, ...(referenceImages && { referenceImages }), modelName: "Nano Banana", }); if (result.success) { return result; } // Fallback to Imagen 4 console.log( `[${new Date().toISOString()}] Primary model failed, trying fallback (Imagen 4)...`, ); return await this.tryGeneration({ model: this.fallbackModel, config: { responseModalities: ["IMAGE"] }, prompt, filename: `${filename}_fallback`, orgId: finalOrgId, projectId: finalProjectId, userId: finalUserId, ...(referenceImages && { referenceImages }), modelName: "Imagen 4", }); } catch (error) { console.error( `[${new Date().toISOString()}] Image generation failed:`, error, ); return { success: false, model: "none", error: error instanceof Error ? error.message : "Unknown error occurred", }; } } /** * Try generation with a specific model */ private async tryGeneration(params: { model: string; config: { responseModalities: string[] }; prompt: string; filename: string; orgId: string; projectId: string; userId: string; referenceImages?: ReferenceImage[]; modelName: string; }): Promise { const { model, config, prompt, filename, orgId, projectId, userId, referenceImages, modelName } = params; try { // Build content parts for the API request const contentParts: any[] = []; // Add reference images if provided if (referenceImages && referenceImages.length > 0) { console.log( `[${new Date().toISOString()}] Adding ${referenceImages.length} reference image(s)`, ); for (const refImage of referenceImages) { contentParts.push({ inlineData: { mimeType: refImage.mimetype, data: refImage.buffer.toString("base64"), }, }); } } // Add the text prompt contentParts.push({ text: prompt, }); const contents = [ { role: "user" as const, parts: contentParts, }, ]; console.log( `[${new Date().toISOString()}] Making API request to ${modelName} (${model})...`, ); const response = await this.ai.models.generateContent({ model, config, contents, }); console.log( `[${new Date().toISOString()}] Response received from ${modelName}`, ); if ( response.candidates && response.candidates[0] && response.candidates[0].content ) { const content = response.candidates[0].content; let generatedDescription = ""; let uploadResult = null; for (let index = 0; index < (content.parts?.length || 0); index++) { const part = content.parts?.[index]; if (!part) continue; if (part.inlineData) { const fileExtension = mime.getExtension( part.inlineData.mimeType || "", ); const finalFilename = `${filename}.${fileExtension}`; const contentType = part.inlineData.mimeType || `image/${fileExtension}`; console.log( `[${new Date().toISOString()}] Uploading image to MinIO: ${finalFilename}`, ); const buffer = Buffer.from(part.inlineData.data || "", "base64"); // Upload to MinIO storage const storageService = StorageFactory.getInstance(); uploadResult = await storageService.uploadFile( orgId, projectId, 'generated', finalFilename, buffer, contentType ); console.log( `[${new Date().toISOString()}] Image uploaded successfully: ${uploadResult.path}`, ); } else if (part.text) { generatedDescription = part.text; console.log( `[${new Date().toISOString()}] Generated description: ${part.text.substring(0, 100)}...`, ); } } if (uploadResult && uploadResult.success) { return { success: true, filename: uploadResult.filename, filepath: uploadResult.path, url: uploadResult.url, description: generatedDescription, model: modelName, }; } } return { success: false, model: modelName, error: "No image data received from API", }; } catch (error) { console.error( `[${new Date().toISOString()}] ${modelName} generation failed:`, error, ); return { success: false, model: modelName, error: error instanceof Error ? error.message : "Generation failed", }; } } /** * Validate reference images */ static validateReferenceImages(files: Express.Multer.File[]): { valid: boolean; error?: string; } { if (files.length > 3) { return { valid: false, error: "Maximum 3 reference images allowed" }; } const allowedTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"]; const maxSize = 5 * 1024 * 1024; // 5MB for (const file of files) { if (!allowedTypes.includes(file.mimetype)) { return { valid: false, error: `Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`, }; } if (file.size > maxSize) { return { valid: false, error: `File ${file.originalname} is too large. Maximum size: 5MB`, }; } } return { valid: true }; } /** * Convert Express.Multer.File[] to ReferenceImage[] */ static convertFilesToReferenceImages( files: Express.Multer.File[], ): ReferenceImage[] { return files.map((file) => ({ buffer: file.buffer, mimetype: file.mimetype, originalname: file.originalname, })); } }