generated from fahricansecer/boilerplate-be
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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 ──
|
||||||
try {
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
this.logger.debug(`🔄 Katman 1: ${primaryModel} deneniyor...`);
|
try {
|
||||||
const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt);
|
this.logger.log(`🔄 Katman 1 (deneme ${attempt}/2): ${primaryModel}`);
|
||||||
if (result) {
|
const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt);
|
||||||
this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`);
|
if (result) {
|
||||||
return 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) ──
|
// ── 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.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user