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

This commit is contained in:
Harun CAN
2026-04-12 15:14:49 +02:00
parent 23eed2982c
commit 5a52370fe2
12 changed files with 152 additions and 64 deletions
+1
View File
@@ -36,3 +36,4 @@ junit.xml
dist dist
cli-tool cli-tool
.pnpm-store
+4 -2
View File
@@ -13,8 +13,10 @@ RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine AS runtime FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine AS runtime
WORKDIR /app WORKDIR /app
# FFmpeg kurulumu (ARM64 native Alpine paketi) # FFmpeg ve Globalization kurulumu (ARM64 native Alpine paketi)
RUN apk add --no-cache ffmpeg font-dejavu RUN apk add --no-cache ffmpeg font-dejavu icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
# Temp dizin oluştur # Temp dizin oluştur
RUN mkdir -p /tmp/contgen-render RUN mkdir -p /tmp/contgen-render
+1 -3
View File
@@ -33,9 +33,7 @@ public class S3StorageService
var config = new AmazonS3Config var config = new AmazonS3Config
{ {
ServiceURL = _settings.Endpoint, ServiceURL = _settings.Endpoint,
ForcePathStyle = true, ForcePathStyle = true
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED,
}; };
_s3Client = new AmazonS3Client( _s3Client = new AmazonS3Client(
+5
View File
@@ -39,12 +39,15 @@
"@nestjs/websockets": "^11.1.17", "@nestjs/websockets": "^11.1.17",
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
"@types/sharp": "^0.32.0", "@types/sharp": "^0.32.0",
"axios": "^1.15.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bullmq": "^5.66.4", "bullmq": "^5.66.4",
"cache-manager": "^7.2.7", "cache-manager": "^7.2.7",
"cache-manager-redis-yet": "^5.1.5", "cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.3", "class-validator": "^0.14.3",
"express": "^5.2.1",
"form-data": "^4.0.5",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"ioredis": "^5.9.0", "ioredis": "^5.9.0",
"nestjs-i18n": "^10.6.0", "nestjs-i18n": "^10.6.0",
@@ -70,7 +73,9 @@
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/form-data": "^2.5.2",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/multer": "^2.1.0",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
+58
View File
@@ -59,6 +59,9 @@ importers:
'@types/sharp': '@types/sharp':
specifier: ^0.32.0 specifier: ^0.32.0
version: 0.32.0 version: 0.32.0
axios:
specifier: ^1.15.0
version: 1.15.0
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
@@ -77,6 +80,12 @@ importers:
class-validator: class-validator:
specifier: ^0.14.3 specifier: ^0.14.3
version: 0.14.4 version: 0.14.4
express:
specifier: ^5.2.1
version: 5.2.1
form-data:
specifier: ^4.0.5
version: 4.0.5
helmet: helmet:
specifier: ^8.1.0 specifier: ^8.1.0
version: 8.1.0 version: 8.1.0
@@ -147,9 +156,15 @@ importers:
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.6 version: 5.0.6
'@types/form-data':
specifier: ^2.5.2
version: 2.5.2
'@types/jest': '@types/jest':
specifier: ^30.0.0 specifier: ^30.0.0
version: 30.0.0 version: 30.0.0
'@types/multer':
specifier: ^2.1.0
version: 2.1.0
'@types/node': '@types/node':
specifier: ^22.10.7 specifier: ^22.10.7
version: 22.19.15 version: 22.19.15
@@ -1721,6 +1736,10 @@ packages:
'@types/express@5.0.6': '@types/express@5.0.6':
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
'@types/form-data@2.5.2':
resolution: {integrity: sha512-tfmcyHn1Pp9YHAO5r40+UuZUPAZbUEgqTel3EuEKpmF9hPkXgR4l41853raliXnb4gwyPNoQOfvgGGlHN5WSog==}
deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.
'@types/http-errors@2.0.5': '@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
@@ -1748,6 +1767,9 @@ packages:
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/multer@2.1.0':
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
'@types/node@22.19.15': '@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
@@ -2153,6 +2175,9 @@ packages:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
axios@1.15.0:
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
babel-jest@30.3.0: babel-jest@30.3.0:
resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -2809,6 +2834,15 @@ packages:
flatted@3.4.2: flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
foreground-child@3.3.1: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -3780,6 +3814,10 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
pump@3.0.4: pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -6449,6 +6487,10 @@ snapshots:
'@types/express-serve-static-core': 5.1.1 '@types/express-serve-static-core': 5.1.1
'@types/serve-static': 2.2.0 '@types/serve-static': 2.2.0
'@types/form-data@2.5.2':
dependencies:
form-data: 4.0.5
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
'@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-coverage@2.0.6': {}
@@ -6477,6 +6519,10 @@ snapshots:
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/multer@2.1.0':
dependencies:
'@types/express': 5.0.6
'@types/node@22.19.15': '@types/node@22.19.15':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@@ -6889,6 +6935,14 @@ snapshots:
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
axios@1.15.0:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 2.1.0
transitivePeerDependencies:
- debug
babel-jest@30.3.0(@babel/core@7.29.0): babel-jest@30.3.0(@babel/core@7.29.0):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
@@ -7601,6 +7655,8 @@ snapshots:
flatted@3.4.2: {} flatted@3.4.2: {}
follow-redirects@1.15.11: {}
foreground-child@3.3.1: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
@@ -8753,6 +8809,8 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
proxy-from-env@2.1.0: {}
pump@3.0.4: pump@3.0.4:
dependencies: dependencies:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
+1
View File
@@ -236,6 +236,7 @@ enum SourceType {
MANUAL MANUAL
X_TWEET X_TWEET
YOUTUBE YOUTUBE
DOCUMENT
} }
// ============================================ // ============================================
+1 -1
View File
@@ -118,7 +118,7 @@ import {
useFactory: (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({
fallbackLanguage: configService.get('i18n.fallbackLanguage', 'en'), fallbackLanguage: configService.get('i18n.fallbackLanguage', 'en'),
loaderOptions: { loaderOptions: {
path: path.join(__dirname, '..', 'i18n'), path: path.join(__dirname, 'i18n'),
watch: configService.get('app.isDevelopment', true), watch: configService.get('app.isDevelopment', true),
}, },
}), }),
+60 -22
View File
@@ -242,6 +242,11 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
/** /**
* Gemini Image Generation API ile görsel üret. * 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. * Raspberry Pi 5 bellek koruması için buffer olarak döner.
* *
* @param prompt - İngilizce görsel açıklaması * @param prompt - İngilizce görsel açıklaması
@@ -256,18 +261,44 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
throw new Error('Gemini AI is not available. Check your configuration.'); throw new Error('Gemini AI is not available. Check your configuration.');
} }
const imageModel = this.configService.get<string>( // Güncel model sıralaması (Nisan 2026):
'gemini.imageModel', // - gemini-2.5-flash-image: Nano Banana — stabil, hızlı
'gemini-2.0-flash-preview-image-generation', // - 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 { try {
this.logger.debug(`🎨 Görsel üretiliyor: "${prompt.substring(0, 80)}..." [${aspectRatio}]`); this.logger.debug(`🎨 Görsel üretiliyor: "${prompt.substring(0, 80)}..." [${aspectRatio}]`);
const enhancedPrompt = `Generate a high-quality image for this description: ${prompt}. Style: photorealistic, cinematic lighting, detailed. Aspect ratio: ${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.`;
// 1) First try the stable Imagen-4 Fast API // ── Katman 1: gemini-2.5-flash-image (Nano Banana) ──
try { 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;
}
} 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...`);
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;
}
} catch (err2: any) {
this.logger.warn(`⚠️ ${fallbackModel} başarısız: ${err2.message?.substring(0, 120)}`);
}
// ── Katman 3: Imagen 4 Fast (generateImages API) ──
try {
this.logger.debug(`🔄 Katman 3: Imagen 4 Fast deneniyor...`);
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,
@@ -275,30 +306,44 @@ 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 personGeneration: 'ALLOW_ALL' as any,
} },
}); });
if (response.generatedImages?.[0]?.image?.imageBytes) { if (response.generatedImages?.[0]?.image?.imageBytes) {
const buffer = Buffer.from(response.generatedImages[0].image.imageBytes, 'base64'); const buffer = Buffer.from(response.generatedImages[0].image.imageBytes, 'base64');
const mimeType = 'image/jpeg'; const mimeType = 'image/jpeg';
this.logger.log(`✅ Görsel üretildi (Imagen): ${(buffer.length / 1024).toFixed(1)} KB [${mimeType}]`); this.logger.log(`✅ Görsel üretildi (Imagen 4): ${(buffer.length / 1024).toFixed(1)} KB`);
return { buffer, mimeType }; return { buffer, mimeType };
} }
} catch (imagenError: any) { } catch (err3: any) {
this.logger.warn(`Imagen API error, falling back to generateContent... ${imagenError.message}`); this.logger.warn(`⚠️ Imagen 4 başarısız: ${err3.message?.substring(0, 120)}`);
} }
// 2) Fallback to Gemini Flash image modalities (experimental feature) 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 } | null> {
const response = await this.client!.models.generateContent({ const response = await this.client!.models.generateContent({
model: imageModel, model,
contents: enhancedPrompt, contents: prompt,
config: { config: {
responseModalities: ['IMAGE', 'TEXT'], responseModalities: ['IMAGE', 'TEXT'],
}, },
}); });
// Gemini image generation modeli, inlineData olarak görsel döner
const candidate = response.candidates?.[0]; const candidate = response.candidates?.[0];
const imagePart = candidate?.content?.parts?.find( const imagePart = candidate?.content?.parts?.find(
(p: any) => p.inlineData?.mimeType?.startsWith('image/'), (p: any) => p.inlineData?.mimeType?.startsWith('image/'),
@@ -307,17 +352,10 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
if (imagePart?.inlineData?.data) { if (imagePart?.inlineData?.data) {
const buffer = Buffer.from(imagePart.inlineData.data, 'base64'); const buffer = Buffer.from(imagePart.inlineData.data, 'base64');
const mimeType = imagePart.inlineData.mimeType || 'image/png'; const mimeType = imagePart.inlineData.mimeType || 'image/png';
this.logger.log(`✅ Görsel üretildi (Flash): ${(buffer.length / 1024).toFixed(1)} KB [${mimeType}]`);
return { buffer, mimeType }; return { buffer, mimeType };
} }
this.logger.warn('Gemini görsel üretemedi — response içinde image part bulunamadı');
return null; return null;
} catch (error) {
this.logger.error(`Gemini görsel üretim hatası: ${error instanceof Error ? error.message : error}`);
throw error;
}
} }
/** /**
+1 -1
View File
@@ -8,7 +8,7 @@ import {
ApiResponse, ApiResponse,
createSuccessResponse, createSuccessResponse,
} from '../../common/types/api-response.type'; } from '../../common/types/api-response.type';
import { User } from '@prisma/client/wasm'; import { User } from '@prisma/client';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { UserResponseDto } from './dto/user.dto'; import { UserResponseDto } from './dto/user.dto';
+1 -1
View File
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../database/prisma.service'; import { PrismaService } from '../../database/prisma.service';
import { BaseService } from '../../common/base'; import { BaseService } from '../../common/base';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto'; import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
import { User } from '@prisma/client/wasm'; import { User } from '@prisma/client';
@Injectable() @Injectable()
export class UsersService extends BaseService< export class UsersService extends BaseService<
-15
View File
@@ -1,15 +0,0 @@
import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
async function run() {
const adminUser = await db.user.findUnique({ where: { email: 'admin@contentgen.ai' } });
console.log("Admin ID:", adminUser?.id);
const projects = await db.project.findMany({
orderBy: { createdAt: 'desc' },
take: 5,
select: { id: true, title: true, createdAt: true, userId: true }
});
console.log("Last 5 projects:", projects);
await db.$disconnect();
}
run();
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"]
} }