From ad5a97a4fdba6d7c8e87aff41e3d410378acb262 Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Thu, 16 Apr 2026 14:17:03 +0200 Subject: [PATCH] main --- Dockerfile | 3 ++ src/main.ts | 16 ++++++++ src/modules/gemini/gemini.service.ts | 61 ++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf56882..e314c53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,9 @@ ENV NODE_ENV=production # Portu aç EXPOSE 3000 +# Medya depolama dizinini oluştur ve node kullanıcısına sahipliğini ver +RUN mkdir -p /data/media && chown -R node:node /data/media + # Güvenlik: Root yerine 'node' kullanıcısına geç USER node diff --git a/src/main.ts b/src/main.ts index 0c7b99d..264b3fc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,12 @@ import { Logger, LoggerErrorInterceptor } from 'nestjs-pino'; import * as express from 'express'; import * as path from 'path'; +// Prisma BigInt alanları JSON'a serialize edilemiyor — global polyfill +// MediaAsset.sizeBytes gibi alanlar BigInt tipinde +(BigInt.prototype as any).toJSON = function () { + return Number(this); +}; + async function bootstrap() { const logger = new NestLogger('Bootstrap'); @@ -22,10 +28,12 @@ async function bootstrap() { app.useLogger(app.get(Logger)); app.useGlobalInterceptors(new LoggerErrorInterceptor()); + // Security Headers app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, + crossOriginResourcePolicy: { policy: 'cross-origin' }, })); // Graceful Shutdown (Prisma & Docker) @@ -39,6 +47,14 @@ async function bootstrap() { // ── Static File Serving — Medya dosyalarına HTTP erişim ── const mediaPath = configService.get('STORAGE_LOCAL_PATH', './data/media'); const absoluteMediaPath = path.resolve(mediaPath); + + // Medya dosyaları için CORS header'ları (Frontend farklı port'ta çalışıyor) + app.use('/media', (req: any, res: any, next: any) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + next(); + }); + app.use('/media', express.static(absoluteMediaPath, { maxAge: '1d', etag: true, diff --git a/src/modules/gemini/gemini.service.ts b/src/modules/gemini/gemini.service.ts index 9b9cab2..8361571 100644 --- a/src/modules/gemini/gemini.service.ts +++ b/src/modules/gemini/gemini.service.ts @@ -268,37 +268,45 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; const fallbackModel = 'gemini-3.1-flash-image-preview'; try { - this.logger.debug(`🎨 Görsel üretiliyor: "${prompt.substring(0, 80)}..." [${aspectRatio}]`); + this.logger.log(`🎨 Görsel üretiliyor: "${prompt.substring(0, 100)}..." [${aspectRatio}]`); - const enhancedPrompt = `Generate a high-quality image for this description: ${prompt}. Style: photorealistic, cinematic lighting, detailed. Aspect ratio: ${aspectRatio}. IMPORTANT: Generate only the image, no text response needed.`; + // En-boy oranına göre yönlendirmeyi zorla + const orientation = aspectRatio === '9:16' ? '(VERTICAL / PORTRAIT)' : aspectRatio === '16:9' ? '(HORIZONTAL / LANDSCAPE)' : '(SQUARE)'; + const enhancedPrompt = `Generate a high-quality, photorealistic image. Description: ${prompt}. Aspect ratio and framing: EXACTLY ${aspectRatio} ${orientation}. Style: cinematic lighting, detailed, professional. IMPORTANT: Return the generated image only, no text.`; - // ── Katman 1: gemini-2.5-flash-image (Nano Banana) ── - try { - this.logger.debug(`🔄 Katman 1: ${primaryModel} deneniyor...`); - const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt); - if (result) { - this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`); - return result; + // ── Katman 1: gemini-2.5-flash-image (Nano Banana) — 2 deneme ── + 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) { + this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`); + return result; + } + this.logger.warn(`⚠️ ${primaryModel} deneme ${attempt}: görsel döndürmedi (null response)`); + 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); } - } catch (err1: any) { - this.logger.warn(`⚠️ ${primaryModel} başarısız: ${err1.message?.substring(0, 120)}`); } // ── Katman 2: gemini-3.1-flash-image-preview (Nano Banana 2) ── try { - this.logger.debug(`🔄 Katman 2: ${fallbackModel} deneniyor...`); + this.logger.log(`🔄 Katman 2: ${fallbackModel}`); const result = await this.tryGenerateContentImage(fallbackModel, enhancedPrompt); if (result) { this.logger.log(`✅ Görsel üretildi (${fallbackModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`); return result; } + this.logger.warn(`⚠️ ${fallbackModel}: görsel döndürmedi (null response)`); } catch (err2: any) { - this.logger.warn(`⚠️ ${fallbackModel} başarısız: ${err2.message?.substring(0, 120)}`); + this.logger.warn(`⚠️ ${fallbackModel} hata: ${err2.message?.substring(0, 200)}`); } // ── Katman 3: Imagen 4 Fast (generateImages API) ── try { - this.logger.debug(`🔄 Katman 3: Imagen 4 Fast deneniyor...`); + this.logger.log(`🔄 Katman 3: Imagen 4 Fast`); const response = await this.client!.models.generateImages({ model: 'imagen-4.0-fast-generate-001', prompt: enhancedPrompt, @@ -306,7 +314,6 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; numberOfImages: 1, aspectRatio: aspectRatio, outputMimeType: 'image/jpeg', - personGeneration: 'ALLOW_ALL' as any, }, }); @@ -316,8 +323,9 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; 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'); } catch (err3: any) { - this.logger.warn(`⚠️ Imagen 4 başarısız: ${err3.message?.substring(0, 120)}`); + 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'); @@ -345,7 +353,15 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; }); const candidate = response.candidates?.[0]; - const imagePart = candidate?.content?.parts?.find( + + // 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 null; + } + + const imagePart = candidate.content.parts.find( (p: any) => p.inlineData?.mimeType?.startsWith('image/'), ); @@ -355,9 +371,20 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; 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 null; } + /** 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. *