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
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
WORKDIR /app
# FFmpeg kurulumu (ARM64 native Alpine paketi)
RUN apk add --no-cache ffmpeg font-dejavu
# FFmpeg ve Globalization kurulumu (ARM64 native Alpine paketi)
RUN apk add --no-cache ffmpeg font-dejavu icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
# Temp dizin oluştur
RUN mkdir -p /tmp/contgen-render
+1 -3
View File
@@ -33,9 +33,7 @@ public class S3StorageService
var config = new AmazonS3Config
{
ServiceURL = _settings.Endpoint,
ForcePathStyle = true,
RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED,
ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED,
ForcePathStyle = true
};
_s3Client = new AmazonS3Client(
+5
View File
@@ -39,12 +39,15 @@
"@nestjs/websockets": "^11.1.17",
"@prisma/client": "^5.22.0",
"@types/sharp": "^0.32.0",
"axios": "^1.15.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
"cache-manager": "^7.2.7",
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"express": "^5.2.1",
"form-data": "^4.0.5",
"helmet": "^8.1.0",
"ioredis": "^5.9.0",
"nestjs-i18n": "^10.6.0",
@@ -70,7 +73,9 @@
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/form-data": "^2.5.2",
"@types/jest": "^30.0.0",
"@types/multer": "^2.1.0",
"@types/node": "^22.10.7",
"@types/nodemailer": "^7.0.4",
"@types/passport-jwt": "^4.0.1",
+58
View File
@@ -59,6 +59,9 @@ importers:
'@types/sharp':
specifier: ^0.32.0
version: 0.32.0
axios:
specifier: ^1.15.0
version: 1.15.0
bcrypt:
specifier: ^6.0.0
version: 6.0.0
@@ -77,6 +80,12 @@ importers:
class-validator:
specifier: ^0.14.3
version: 0.14.4
express:
specifier: ^5.2.1
version: 5.2.1
form-data:
specifier: ^4.0.5
version: 4.0.5
helmet:
specifier: ^8.1.0
version: 8.1.0
@@ -147,9 +156,15 @@ importers:
'@types/express':
specifier: ^5.0.0
version: 5.0.6
'@types/form-data':
specifier: ^2.5.2
version: 2.5.2
'@types/jest':
specifier: ^30.0.0
version: 30.0.0
'@types/multer':
specifier: ^2.1.0
version: 2.1.0
'@types/node':
specifier: ^22.10.7
version: 22.19.15
@@ -1721,6 +1736,10 @@ packages:
'@types/express@5.0.6':
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':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
@@ -1748,6 +1767,9 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/multer@2.1.0':
resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==}
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
@@ -2153,6 +2175,9 @@ packages:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
axios@1.15.0:
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
babel-jest@30.3.0:
resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -2809,6 +2834,15 @@ packages:
flatted@3.4.2:
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:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -3780,6 +3814,10 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
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:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -6449,6 +6487,10 @@ snapshots:
'@types/express-serve-static-core': 5.1.1
'@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/istanbul-lib-coverage@2.0.6': {}
@@ -6477,6 +6519,10 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/multer@2.1.0':
dependencies:
'@types/express': 5.0.6
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
@@ -6889,6 +6935,14 @@ snapshots:
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):
dependencies:
'@babel/core': 7.29.0
@@ -7601,6 +7655,8 @@ snapshots:
flatted@3.4.2: {}
follow-redirects@1.15.11: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -8753,6 +8809,8 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
proxy-from-env@2.1.0: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
+1
View File
@@ -236,6 +236,7 @@ enum SourceType {
MANUAL
X_TWEET
YOUTUBE
DOCUMENT
}
// ============================================
+1 -1
View File
@@ -118,7 +118,7 @@ import {
useFactory: (configService: ConfigService) => ({
fallbackLanguage: configService.get('i18n.fallbackLanguage', 'en'),
loaderOptions: {
path: path.join(__dirname, '..', 'i18n'),
path: path.join(__dirname, 'i18n'),
watch: configService.get('app.isDevelopment', true),
},
}),
+76 -38
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.
* 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.
*
* @param prompt - İngilizce görsel açıklaması
@@ -256,63 +261,66 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
throw new Error('Gemini AI is not available. Check your configuration.');
}
const imageModel = this.configService.get<string>(
'gemini.imageModel',
'gemini-2.0-flash-preview-image-generation',
);
// Güncel model sıralaması (Nisan 2026):
// - gemini-2.5-flash-image: Nano Banana — stabil, hızlı
// - 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 {
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 {
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({
model: 'imagen-4.0-fast-generate-001',
prompt: enhancedPrompt,
config: {
numberOfImages: 1,
aspectRatio: aspectRatio,
outputMimeType: 'image/jpeg',
personGeneration: 'ALLOW_ALL' as any
}
numberOfImages: 1,
aspectRatio: aspectRatio,
outputMimeType: 'image/jpeg',
personGeneration: 'ALLOW_ALL' as any,
},
});
if (response.generatedImages?.[0]?.image?.imageBytes) {
const buffer = Buffer.from(response.generatedImages[0].image.imageBytes, 'base64');
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 };
}
} catch (imagenError: any) {
this.logger.warn(`Imagen API error, falling back to generateContent... ${imagenError.message}`);
} catch (err3: any) {
this.logger.warn(`⚠️ Imagen 4 başarısız: ${err3.message?.substring(0, 120)}`);
}
// 2) Fallback to Gemini Flash image modalities (experimental feature)
const response = await this.client!.models.generateContent({
model: imageModel,
contents: enhancedPrompt,
config: {
responseModalities: ['IMAGE', 'TEXT'],
},
});
// Gemini image generation modeli, inlineData olarak görsel döner
const candidate = response.candidates?.[0];
const imagePart = candidate?.content?.parts?.find(
(p: any) => p.inlineData?.mimeType?.startsWith('image/'),
);
if (imagePart?.inlineData?.data) {
const buffer = Buffer.from(imagePart.inlineData.data, 'base64');
const mimeType = imagePart.inlineData.mimeType || 'image/png';
this.logger.log(`✅ Görsel üretildi (Flash): ${(buffer.length / 1024).toFixed(1)} KB [${mimeType}]`);
return { buffer, mimeType };
}
this.logger.warn('Gemini görsel üretemedi — response içinde image part bulunamadı');
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}`);
@@ -320,6 +328,36 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
}
}
/**
* 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({
model,
contents: prompt,
config: {
responseModalities: ['IMAGE', 'TEXT'],
},
});
const candidate = response.candidates?.[0];
const imagePart = candidate?.content?.parts?.find(
(p: any) => p.inlineData?.mimeType?.startsWith('image/'),
);
if (imagePart?.inlineData?.data) {
const buffer = Buffer.from(imagePart.inlineData.data, 'base64');
const mimeType = imagePart.inlineData.mimeType || 'image/png';
return { buffer, mimeType };
}
return null;
}
/**
* Sahne bazlı görsel üret — visualPrompt ve video stili kullanarak.
*
+1 -1
View File
@@ -8,7 +8,7 @@ import {
ApiResponse,
createSuccessResponse,
} from '../../common/types/api-response.type';
import { User } from '@prisma/client/wasm';
import { User } from '@prisma/client';
import { plainToInstance } from 'class-transformer';
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 { BaseService } from '../../common/base';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
import { User } from '@prisma/client/wasm';
import { User } from '@prisma/client';
@Injectable()
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",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"]
}