main
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-16 14:17:03 +02:00
parent 5a52370fe2
commit ad5a97a4fd
3 changed files with 63 additions and 17 deletions
+3
View File
@@ -55,6 +55,9 @@ ENV NODE_ENV=production
# Portu aç # Portu aç
EXPOSE 3000 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ç # Güvenlik: Root yerine 'node' kullanıcısına geç
USER node USER node
+16
View File
@@ -8,6 +8,12 @@ import { Logger, LoggerErrorInterceptor } from 'nestjs-pino';
import * as express from 'express'; import * as express from 'express';
import * as path from 'path'; 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() { async function bootstrap() {
const logger = new NestLogger('Bootstrap'); const logger = new NestLogger('Bootstrap');
@@ -22,10 +28,12 @@ async function bootstrap() {
app.useLogger(app.get(Logger)); app.useLogger(app.get(Logger));
app.useGlobalInterceptors(new LoggerErrorInterceptor()); app.useGlobalInterceptors(new LoggerErrorInterceptor());
// Security Headers // Security Headers
app.use(helmet({ app.use(helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false, crossOriginEmbedderPolicy: false,
crossOriginResourcePolicy: { policy: 'cross-origin' },
})); }));
// Graceful Shutdown (Prisma & Docker) // Graceful Shutdown (Prisma & Docker)
@@ -39,6 +47,14 @@ async function bootstrap() {
// ── Static File Serving — Medya dosyalarına HTTP erişim ── // ── Static File Serving — Medya dosyalarına HTTP erişim ──
const mediaPath = configService.get<string>('STORAGE_LOCAL_PATH', './data/media'); const mediaPath = configService.get<string>('STORAGE_LOCAL_PATH', './data/media');
const absoluteMediaPath = path.resolve(mediaPath); 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, { app.use('/media', express.static(absoluteMediaPath, {
maxAge: '1d', maxAge: '1d',
etag: true, etag: true,
+38 -11
View File
@@ -268,37 +268,45 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
const fallbackModel = 'gemini-3.1-flash-image-preview'; const fallbackModel = 'gemini-3.1-flash-image-preview';
try { 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) ── // ── Katman 1: gemini-2.5-flash-image (Nano Banana) — 2 deneme ──
for (let attempt = 1; attempt <= 2; attempt++) {
try { try {
this.logger.debug(`🔄 Katman 1: ${primaryModel} deneniyor...`); this.logger.log(`🔄 Katman 1 (deneme ${attempt}/2): ${primaryModel}`);
const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt); const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt);
if (result) { if (result) {
this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`); this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`);
return result; 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) { } catch (err1: any) {
this.logger.warn(`⚠️ ${primaryModel} başarısız: ${err1.message?.substring(0, 120)}`); 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) ── // ── Katman 2: gemini-3.1-flash-image-preview (Nano Banana 2) ──
try { try {
this.logger.debug(`🔄 Katman 2: ${fallbackModel} deneniyor...`); this.logger.log(`🔄 Katman 2: ${fallbackModel}`);
const result = await this.tryGenerateContentImage(fallbackModel, enhancedPrompt); const result = await this.tryGenerateContentImage(fallbackModel, enhancedPrompt);
if (result) { if (result) {
this.logger.log(`✅ Görsel üretildi (${fallbackModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`); this.logger.log(`✅ Görsel üretildi (${fallbackModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`);
return result; return result;
} }
this.logger.warn(`⚠️ ${fallbackModel}: görsel döndürmedi (null response)`);
} catch (err2: any) { } 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) ── // ── Katman 3: Imagen 4 Fast (generateImages API) ──
try { 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({ const response = await this.client!.models.generateImages({
model: 'imagen-4.0-fast-generate-001', model: 'imagen-4.0-fast-generate-001',
prompt: enhancedPrompt, prompt: enhancedPrompt,
@@ -306,7 +314,6 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
numberOfImages: 1, numberOfImages: 1,
aspectRatio: aspectRatio, aspectRatio: aspectRatio,
outputMimeType: 'image/jpeg', 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`); this.logger.log(`✅ Görsel üretildi (Imagen 4): ${(buffer.length / 1024).toFixed(1)} KB`);
return { buffer, mimeType }; return { buffer, mimeType };
} }
this.logger.warn('⚠️ Imagen 4: görsel döndürmedi');
} catch (err3: any) { } 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'); 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 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/'), (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 }; 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; return null;
} }
/** Basit uyku fonksiyonu — retry aralarında kullanılır */
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/** /**
* Sahne bazlı görsel üret — visualPrompt ve video stili kullanarak. * Sahne bazlı görsel üret — visualPrompt ve video stili kullanarak.
* *