import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { GoogleGenAI } from '@google/genai'; export interface GeminiGenerateOptions { model?: string; systemPrompt?: string; temperature?: number; maxTokens?: number; } export interface GeminiChatMessage { role: 'user' | 'model'; content: string; } /** * Gemini AI Service * * Provides AI-powered text generation using Google Gemini API. * This service is globally available when ENABLE_GEMINI=true. * * @example * ```typescript * // Simple text generation * const response = await geminiService.generateText('Write a poem about coding'); * * // With options * const response = await geminiService.generateText('Translate to Turkish', { * temperature: 0.7, * systemPrompt: 'You are a professional translator', * }); * * // Chat conversation * const messages = [ * { role: 'user', content: 'Hello!' }, * { role: 'model', content: 'Hi there!' }, * { role: 'user', content: 'What is 2+2?' }, * ]; * const response = await geminiService.chat(messages); * ``` */ @Injectable() export class GeminiService implements OnModuleInit { private readonly logger = new Logger(GeminiService.name); private client: GoogleGenAI | null = null; private isEnabled = false; private defaultModel: string; constructor(private readonly configService: ConfigService) { this.isEnabled = this.configService.get('gemini.enabled', false); this.defaultModel = this.configService.get( 'gemini.defaultModel', 'gemini-2.5-flash', ); } onModuleInit() { if (!this.isEnabled) { this.logger.log( 'Gemini AI is disabled. Set ENABLE_GEMINI=true to enable.', ); return; } const apiKey = this.configService.get('gemini.apiKey'); if (!apiKey) { this.logger.warn( 'GOOGLE_API_KEY is not set. Gemini features will not work.', ); return; } try { this.client = new GoogleGenAI({ apiKey }); this.logger.log('✅ Gemini AI initialized successfully'); } catch (error) { this.logger.error('Failed to initialize Gemini AI', error); } } /** * Check if Gemini is available and properly configured */ isAvailable(): boolean { return this.isEnabled && this.client !== null; } /** * Generate text content from a prompt * * @param prompt - The text prompt to send to the AI * @param options - Optional configuration for the generation * @returns Generated text response */ async generateText( prompt: string, options: GeminiGenerateOptions = {}, ): Promise<{ text: string; usage?: any }> { if (!this.isAvailable()) { throw new Error('Gemini AI is not available. Check your configuration.'); } const model = options.model || this.defaultModel; try { const contents: any[] = []; // Add system prompt if provided if (options.systemPrompt) { contents.push({ role: 'user', parts: [{ text: options.systemPrompt }], }); contents.push({ role: 'model', parts: [{ text: 'Understood. I will follow these instructions.' }], }); } contents.push({ role: 'user', parts: [{ text: prompt }], }); const response = await this.client!.models.generateContent({ model, contents, config: { temperature: options.temperature, maxOutputTokens: options.maxTokens, }, }); return { text: (response.text || '').trim(), usage: response.usageMetadata, }; } catch (error) { this.logger.error('Gemini generation failed', error); throw error; } } /** * Have a multi-turn chat conversation * * @param messages - Array of chat messages * @param options - Optional configuration for the generation * @returns Generated text response */ async chat( messages: GeminiChatMessage[], options: GeminiGenerateOptions = {}, ): Promise<{ text: string; usage?: any }> { if (!this.isAvailable()) { throw new Error('Gemini AI is not available. Check your configuration.'); } const model = options.model || this.defaultModel; try { const contents = messages.map((msg) => ({ role: msg.role, parts: [{ text: msg.content }], })); // Prepend system prompt if provided if (options.systemPrompt) { contents.unshift( { role: 'user', parts: [{ text: options.systemPrompt }], }, { role: 'model', parts: [{ text: 'Understood. I will follow these instructions.' }], }, ); } const response = await this.client!.models.generateContent({ model, contents, config: { temperature: options.temperature, maxOutputTokens: options.maxTokens, }, }); return { text: (response.text || '').trim(), usage: response.usageMetadata, }; } catch (error) { this.logger.error('Gemini chat failed', error); throw error; } } /** * Generate structured JSON output * * @param prompt - The prompt describing what JSON to generate * @param schema - JSON schema description for the expected output * @param options - Optional configuration for the generation * @returns Parsed JSON object */ async generateJSON( prompt: string, schema: string, options: GeminiGenerateOptions = {}, ): Promise<{ data: T; usage?: any }> { const fullPrompt = `${prompt} Output the result as valid JSON that matches this schema: ${schema} IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; const response = await this.generateText(fullPrompt, options); try { // Try to extract JSON from the response let jsonStr = response.text; // Remove potential markdown code blocks const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/); if (jsonMatch) { jsonStr = jsonMatch[1].trim(); } const data = JSON.parse(jsonStr) as T; return { data, usage: response.usage }; } catch (error) { this.logger.error('Failed to parse JSON response', error); throw new Error('Failed to parse AI response as JSON'); } } // ── Görsel Üretim (Gemini Image Generation) ───────────────────────── /** * Gemini Image Generation API ile görsel üret. * 3 katmanlı fallback mimarisi: * 1) gemini-2.5-flash-image (Nano Banana — hızlı, stabil) * 2) gemini-3.1-flash-image-preview (Nano Banana 2 — en yeni) * 3) Imagen 4 Fast (generateImages API) * * Raspberry Pi 5 bellek koruması için buffer olarak döner. * * @param prompt - İngilizce görsel açıklaması * @param aspectRatio - Görsel en-boy oranı (16:9, 9:16, 1:1) * @returns Base64 decoded image buffer ve mime type */ async generateImage( prompt: string, aspectRatio: '16:9' | '9:16' | '1:1' = '16:9', isIllustration: boolean = false, forceImagen: boolean = false, ): Promise<{ buffer: Buffer; mimeType: string } | null> { if (!this.isAvailable()) { throw new Error('Gemini AI is not available. Check your configuration.'); } // Güncel model sıralaması (Nisan 2026): // - gemini-2.5-flash-image: Nano Banana — stabil, hızlı // - gemini-3.1-flash-image-preview: Nano Banana 2 — en yeni, yüksek kalite const primaryModel = 'gemini-2.5-flash-image'; const fallbackModel = 'gemini-3.1-flash-image-preview'; try { this.logger.log( `🎨 Görsel üretiliyor: "${prompt.substring(0, 100)}..." [${aspectRatio}]`, ); // En-boy oranına göre yönlendirmeyi zorla const orientation = aspectRatio === '9:16' ? '(VERTICAL / PORTRAIT)' : aspectRatio === '16:9' ? '(HORIZONTAL / LANDSCAPE)' : '(SQUARE)'; // Gemini modelleri ana konunun (subject) prompt'un en başında olmasını tercih eder. // Jenerik stil kelimelerini sonuna ekliyoruz ki ana konu (prompt) kaybolmasın. const enhancedPrompt = isIllustration ? `SUBJECT: ${prompt}\n\nINSTRUCTIONS: Generate a premium digital illustration of the EXACT SUBJECT described above. Make it highly detailed and vivid, but NOT photorealistic. Aspect ratio: ${aspectRatio} ${orientation}. DO NOT deviate from the SUBJECT.` : `SUBJECT: ${prompt}\n\nINSTRUCTIONS: Generate a high-quality photorealistic cinematic image of the EXACT SUBJECT described above. Use professional lighting and make it highly detailed. Aspect ratio: ${aspectRatio} ${orientation}. DO NOT deviate from the SUBJECT.`; // ── Katman 1: gemini-2.5-flash-image (Nano Banana) — 2 deneme ── if (!forceImagen) { for (let attempt = 1; attempt <= 2; attempt++) { try { this.logger.log(`🔄 Katman 1 (deneme ${attempt}/2): ${primaryModel}`); const result = await this.tryGenerateContentImage( primaryModel, enhancedPrompt, ); if (result && result.buffer.length > 0) { this.logger.log( `✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`, ); return { buffer: result.buffer, mimeType: result.mimeType }; } const reason = result?.errorReason || 'null response'; this.logger.warn( `⚠️ ${primaryModel} deneme ${attempt}: görsel döndürmedi (${reason})`, ); if ( ['IMAGE_OTHER', 'SAFETY', 'PROHIBITED_CONTENT'].includes(reason) ) { this.logger.warn( `🚫 Güvenlik/Politika filtresi tetiklendi (${reason}). Denemeler iptal ediliyor.`, ); break; // Fail fast for safety blocks } if (attempt < 2) await this.sleep(2000); } catch (err1: any) { this.logger.warn( `⚠️ ${primaryModel} deneme ${attempt} hata: ${err1.message?.substring(0, 200)}`, ); if (attempt < 2) await this.sleep(2000); } } } // ── Katman 2: gemini-3.1-flash-image-preview (Nano Banana 2) ── if (!forceImagen) { try { this.logger.log(`🔄 Katman 2: ${fallbackModel}`); const result = await this.tryGenerateContentImage( fallbackModel, enhancedPrompt, ); if (result && result.buffer.length > 0) { this.logger.log( `✅ Görsel üretildi (${fallbackModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`, ); return { buffer: result.buffer, mimeType: result.mimeType }; } this.logger.warn( `⚠️ ${fallbackModel}: görsel döndürmedi (${result?.errorReason || 'null response'})`, ); if ( ['IMAGE_OTHER', 'SAFETY', 'PROHIBITED_CONTENT'].includes( result?.errorReason || '', ) ) { this.logger.warn( `🚫 Katman 2 Güvenlik/Politika filtresi tetiklendi. Katman 3'e geçiliyor.`, ); } } catch (err2: any) { this.logger.warn( `⚠️ ${fallbackModel} hata: ${err2.message?.substring(0, 200)}`, ); } } // ── Katman 3: Imagen 4 Fast (generateImages API) ── try { this.logger.log(`🔄 Katman 3: Imagen 4 Fast`); const response = await this.client!.models.generateImages({ model: 'imagen-4.0-fast-generate-001', prompt: enhancedPrompt, config: { numberOfImages: 1, aspectRatio: aspectRatio, outputMimeType: 'image/jpeg', }, }); if (response.generatedImages?.[0]?.image?.imageBytes) { const buffer = Buffer.from( response.generatedImages[0].image.imageBytes, 'base64', ); const mimeType = 'image/jpeg'; this.logger.log( `✅ Görsel üretildi (Imagen 4): ${(buffer.length / 1024).toFixed(1)} KB`, ); return { buffer, mimeType }; } this.logger.warn( `⚠️ Imagen 4: görsel döndürmedi. Üretilen görsel sayısı: ${response.generatedImages?.length || 0}`, ); } catch (err3: any) { this.logger.warn( `⚠️ Imagen 4 hata: ${err3.message?.substring(0, 200)}`, ); } this.logger.error('❌ Tüm görsel üretim katmanları başarısız oldu'); return null; } catch (error) { this.logger.error( `Gemini görsel üretim hatası: ${error instanceof Error ? error.message : error}`, ); throw error; } } /** * generateContent API ile görsel üretim denemesi. * responseModalities: ['IMAGE', 'TEXT'] kullanarak inlineData içinden resim çıkarır. */ private async tryGenerateContentImage( model: string, prompt: string, ): Promise<{ buffer: Buffer; mimeType: string; errorReason?: string; } | null> { const response = await this.client!.models.generateContent({ model, contents: prompt, config: { responseModalities: ['IMAGE', 'TEXT'], }, }); const candidate = response.candidates?.[0]; // Safety filter veya boş yanıt kontrolü if (!candidate?.content?.parts || candidate.content.parts.length === 0) { const finishReason = candidate?.finishReason || 'UNKNOWN'; this.logger.warn( `⚠️ ${model}: boş yanıt (finishReason: ${finishReason})`, ); return { buffer: Buffer.from([]), mimeType: '', errorReason: finishReason, }; } const imagePart = candidate.content.parts.find((p: any) => p.inlineData?.mimeType?.startsWith('image/'), ); if (imagePart?.inlineData?.data) { const buffer = Buffer.from(imagePart.inlineData.data, 'base64'); const mimeType = imagePart.inlineData.mimeType || 'image/png'; return { buffer, mimeType }; } // Text-only response geldi (görsel yok) const textParts = candidate.content.parts.filter((p: any) => p.text); if (textParts.length > 0) { this.logger.warn( `⚠️ ${model}: sadece text döndü, görsel yok. Text: "${textParts[0].text?.substring(0, 100)}"`, ); return { buffer: Buffer.from([]), mimeType: '', errorReason: 'TEXT_ONLY', }; } return { buffer: Buffer.from([]), mimeType: '', errorReason: 'NO_IMAGE_DATA', }; } /** Basit uyku fonksiyonu — retry aralarında kullanılır */ private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Sahne bazlı görsel üret — visualPrompt ve video stili kullanarak. * * @param visualPrompt - Sahnenin İngilizce görsel açıklaması * @param style - Video stili (cinematic, documentary, educational vb.) * @param aspectRatio - En-boy oranı * @returns Buffer ve mimeType */ async generateImageForScene( visualPrompt: string, style: string = 'cinematic', aspectRatio: '16:9' | '9:16' | '1:1' = '16:9', ): Promise<{ buffer: Buffer; mimeType: string } | null> { const enhancedPrompt = `${visualPrompt}. Style: ${style}, professional production quality, volumetric lighting, sharp details, 8K resolution.`; return this.generateImage(enhancedPrompt, aspectRatio); } /** * Video için thumbnail görsel üret — proje başlığı ve açıklamasından. * * @param title - Video başlığı * @param description - Video açıklaması * @returns Buffer ve mimeType */ async generateThumbnail( title: string, description: string, ): Promise<{ buffer: Buffer; mimeType: string } | null> { const prompt = `Create a compelling YouTube video thumbnail for a video titled "${title}". ${description}. Make it eye-catching with bold, dynamic composition. No text overlay needed.`; return this.generateImage(prompt, '16:9'); } }