From 5a52370fe2d321ae65dba1154d203c6a78fd8433 Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Sun, 12 Apr 2026 15:14:49 +0200 Subject: [PATCH] main --- .gitignore | 3 +- media-worker/Dockerfile | 6 +- media-worker/Services/S3StorageService.cs | 4 +- package.json | 5 + pnpm-lock.yaml | 58 +++++++++++ prisma/schema.prisma | 1 + src/app.module.ts | 2 +- src/modules/gemini/gemini.service.ts | 116 ++++++++++++++-------- src/modules/users/users.controller.ts | 2 +- src/modules/users/users.service.ts | 2 +- test-db.ts | 15 --- tsconfig.build.json | 2 +- 12 files changed, 152 insertions(+), 64 deletions(-) delete mode 100644 test-db.ts diff --git a/.gitignore b/.gitignore index b5647cb..5c31c14 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ junit.xml dist -cli-tool \ No newline at end of file +cli-tool +.pnpm-store \ No newline at end of file diff --git a/media-worker/Dockerfile b/media-worker/Dockerfile index 4258748..796dfa6 100644 --- a/media-worker/Dockerfile +++ b/media-worker/Dockerfile @@ -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 diff --git a/media-worker/Services/S3StorageService.cs b/media-worker/Services/S3StorageService.cs index 9352cdb..e223231 100644 --- a/media-worker/Services/S3StorageService.cs +++ b/media-worker/Services/S3StorageService.cs @@ -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( diff --git a/package.json b/package.json index 29bf9b6..635a0cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 272e1c9..3d3ea8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 66b063b..4c96961 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -236,6 +236,7 @@ enum SourceType { MANUAL X_TWEET YOUTUBE + DOCUMENT } // ============================================ diff --git a/src/app.module.ts b/src/app.module.ts index 4fb5d26..b517f68 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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), }, }), diff --git a/src/modules/gemini/gemini.service.ts b/src/modules/gemini/gemini.service.ts index a65d2ea..9b9cab2 100644 --- a/src/modules/gemini/gemini.service.ts +++ b/src/modules/gemini/gemini.service.ts @@ -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( - '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. * diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index da1ccf5..6900a5b 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -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'; diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 6271491..2bb544e 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -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< diff --git a/test-db.ts b/test-db.ts deleted file mode 100644 index da6d55a..0000000 --- a/test-db.ts +++ /dev/null @@ -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(); diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..2c8f61f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"] }