generated from fahricansecer/boilerplate-be
+16
@@ -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<string>('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,
|
||||
|
||||
@@ -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<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sahne bazlı görsel üret — visualPrompt ve video stili kullanarak.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user