diff --git a/data/media/3cbc524a-c536-4706-b531-39053c0cd7a1/images/scene-001.png b/data/media/3cbc524a-c536-4706-b531-39053c0cd7a1/images/scene-001.png new file mode 100644 index 0000000..aa3d0e7 Binary files /dev/null and b/data/media/3cbc524a-c536-4706-b531-39053c0cd7a1/images/scene-001.png differ diff --git a/data/media/4444460d-09db-4ae2-a62d-f594bea00603/images/scene-001.png b/data/media/4444460d-09db-4ae2-a62d-f594bea00603/images/scene-001.png new file mode 100644 index 0000000..77eecd3 Binary files /dev/null and b/data/media/4444460d-09db-4ae2-a62d-f594bea00603/images/scene-001.png differ diff --git a/prisma/migrations/20260405203800_video_style_enum_to_string/migration.sql b/prisma/migrations/20260405203800_video_style_enum_to_string/migration.sql new file mode 100644 index 0000000..6c8e3d4 --- /dev/null +++ b/prisma/migrations/20260405203800_video_style_enum_to_string/migration.sql @@ -0,0 +1,12 @@ +-- AlterTable: Convert videoStyle from enum to text (data-safe) +-- Step 1: Alter columns to TEXT, casting existing enum values to strings +ALTER TABLE "Project" ALTER COLUMN "videoStyle" DROP DEFAULT; +ALTER TABLE "Project" ALTER COLUMN "videoStyle" TYPE TEXT USING "videoStyle"::TEXT; +ALTER TABLE "Project" ALTER COLUMN "videoStyle" SET DEFAULT 'CINEMATIC'; + +ALTER TABLE "UserPreference" ALTER COLUMN "defaultVideoStyle" DROP DEFAULT; +ALTER TABLE "UserPreference" ALTER COLUMN "defaultVideoStyle" TYPE TEXT USING "defaultVideoStyle"::TEXT; +ALTER TABLE "UserPreference" ALTER COLUMN "defaultVideoStyle" SET DEFAULT 'CINEMATIC'; + +-- Step 2: Drop the enum type (no longer needed) +DROP TYPE IF EXISTS "VideoStyle"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ab5ab6..66b063b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -189,16 +189,8 @@ enum AspectRatio { LANDSCAPE_16_9 } -enum VideoStyle { - CINEMATIC - DOCUMENTARY - EDUCATIONAL - STORYTELLING - NEWS - PROMOTIONAL - ARTISTIC - MINIMALIST -} +// NOT: VideoStyle artık serbest String — enum kaldırıldı. +// Frontend 50+ stil destekliyor, DB enum kısıtlaması kaldırıldı. enum MediaType { VIDEO_CLIP @@ -263,7 +255,7 @@ model Project { // Configuration language String @default("tr") @db.VarChar(5) // ISO 639-1 aspectRatio AspectRatio @default(PORTRAIT_9_16) - videoStyle VideoStyle @default(CINEMATIC) + videoStyle String @default("CINEMATIC") @db.VarChar(50) cinematicReference String? @db.VarChar(200) targetDuration Int @default(60) // saniye @@ -599,7 +591,7 @@ model UserPreference { // Defaults defaultLanguage String @default("tr") @db.VarChar(5) - defaultVideoStyle VideoStyle @default(CINEMATIC) + defaultVideoStyle String @default("CINEMATIC") @db.VarChar(50) defaultDuration Int @default(60) // UI diff --git a/src/modules/gemini/gemini.service.ts b/src/modules/gemini/gemini.service.ts index 7f9c02d..a65d2ea 100644 --- a/src/modules/gemini/gemini.service.ts +++ b/src/modules/gemini/gemini.service.ts @@ -264,39 +264,58 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`; 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}.`; + + // 1) First try the stable Imagen-4 Fast API + try { + 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 + } + }); + + 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}]`); + return { buffer, mimeType }; + } + } catch (imagenError: any) { + this.logger.warn(`Imagen API error, falling back to generateContent... ${imagenError.message}`); + } + + // 2) Fallback to Gemini Flash image modalities (experimental feature) const response = await this.client!.models.generateContent({ model: imageModel, - contents: [ - { - role: 'user', - parts: [ - { - text: `Generate a high-quality image for this description: ${prompt}. Aspect ratio: ${aspectRatio}. Style: photorealistic, cinematic lighting, detailed.`, - }, - ], - }, - ], + contents: enhancedPrompt, config: { - responseModalities: ['TEXT', 'IMAGE'] as any, + responseModalities: ['IMAGE', 'TEXT'], }, }); - // Gemini image response'dan image part'ı çıkar - const parts = (response as any).candidates?.[0]?.content?.parts || []; - for (const part of parts) { - if (part.inlineData?.data) { - const buffer = Buffer.from(part.inlineData.data, 'base64'); - const mimeType = part.inlineData.mimeType || 'image/png'; + // 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/'), + ); - this.logger.log(`✅ Görsel üretildi: ${(buffer.length / 1024).toFixed(1)} KB`); - return { buffer, mimeType }; - } + 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 — boş yanıt'); + this.logger.warn('Gemini görsel üretemedi — response içinde image part bulunamadı'); return null; } catch (error) { - this.logger.error(`Gemini görsel üretim hatası: ${error}`); + this.logger.error(`Gemini görsel üretim hatası: ${error instanceof Error ? error.message : error}`); throw error; } } diff --git a/src/modules/projects/dto/project.dto.ts b/src/modules/projects/dto/project.dto.ts index e93ef31..ef881b7 100644 --- a/src/modules/projects/dto/project.dto.ts +++ b/src/modules/projects/dto/project.dto.ts @@ -17,16 +17,11 @@ export enum AspectRatioDto { LANDSCAPE_16_9 = 'LANDSCAPE_16_9', } -export enum VideoStyleDto { - CINEMATIC = 'CINEMATIC', - DOCUMENTARY = 'DOCUMENTARY', - EDUCATIONAL = 'EDUCATIONAL', - STORYTELLING = 'STORYTELLING', - NEWS = 'NEWS', - PROMOTIONAL = 'PROMOTIONAL', - ARTISTIC = 'ARTISTIC', - MINIMALIST = 'MINIMALIST', -} +/** + * Video stili artık serbest string olarak kabul ediliyor. + * Frontend'de 50+ stil tanımlı (CINEMATIC, ANIME, NOIR, CYBERPUNK vb.). + * Enum kısıtlaması kaldırıldı — yeni stiller eklendiğinde backend güncellemesi gerekmez. + */ export class CreateProjectDto { @ApiProperty({ description: 'Proje başlığı', example: 'Boötes Boşluğu' }) @@ -69,13 +64,14 @@ export class CreateProjectDto { aspectRatio?: AspectRatioDto; @ApiPropertyOptional({ - description: 'Video stili', - enum: VideoStyleDto, - default: VideoStyleDto.CINEMATIC, + description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)', + example: 'CINEMATIC', + default: 'CINEMATIC', }) - @IsEnum(VideoStyleDto) + @IsString() @IsOptional() - videoStyle?: VideoStyleDto; + @MaxLength(50) + videoStyle?: string; @ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' }) @IsString() @@ -139,10 +135,13 @@ export class UpdateProjectDto { @IsOptional() aspectRatio?: AspectRatioDto; - @ApiPropertyOptional({ enum: VideoStyleDto }) - @IsEnum(VideoStyleDto) + @ApiPropertyOptional({ + description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)', + }) + @IsString() @IsOptional() - videoStyle?: VideoStyleDto; + @MaxLength(50) + videoStyle?: string; @ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' }) @IsString() @@ -196,10 +195,14 @@ export class CreateFromTweetDto { @IsOptional() aspectRatio?: AspectRatioDto; - @ApiPropertyOptional({ enum: VideoStyleDto, default: VideoStyleDto.CINEMATIC }) - @IsEnum(VideoStyleDto) + @ApiPropertyOptional({ + description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)', + default: 'CINEMATIC', + }) + @IsString() @IsOptional() - videoStyle?: VideoStyleDto; + @MaxLength(50) + videoStyle?: string; @ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' }) @IsString() diff --git a/src/modules/projects/projects.service.ts b/src/modules/projects/projects.service.ts index 18c3451..60b5e3a 100644 --- a/src/modules/projects/projects.service.ts +++ b/src/modules/projects/projects.service.ts @@ -163,14 +163,63 @@ export class ProjectsService { ...(dto.language && { language: dto.language }), ...(dto.aspectRatio && { aspectRatio: dto.aspectRatio }), ...(dto.videoStyle && { videoStyle: dto.videoStyle }), + ...(dto.cinematicReference !== undefined && { cinematicReference: dto.cinematicReference }), ...(dto.targetDuration && { targetDuration: dto.targetDuration }), - }, + } as any, }); this.logger.log(`Proje güncellendi: ${projectId}`); + + // Stil ya da görsel parametre değiştiyse ve senaryo/sahneler varsa otomatik rewrite başlat + const styleChanged = + (dto.videoStyle && dto.videoStyle !== project.videoStyle) || + (dto.cinematicReference !== undefined && dto.cinematicReference !== (project as any).cinematicReference) || + (dto.aspectRatio && dto.aspectRatio !== project.aspectRatio); + + if (styleChanged && project.scenes && project.scenes.length > 0) { + this.logger.log(`Stil değişikliği tespit edildi (${projectId}), visual promptlar arka planda yenileniyor...`); + this.rewriteVisualPromptsBackground(userId, projectId, updated).catch((err) => { + this.logger.error(`Arka plan rewriteVisualPrompts hatası: ${err}`); + }); + } + return updated; } + // Arka planda tüm promptları güncelleyen metod + private async rewriteVisualPromptsBackground(userId: string, projectId: string, updatedProject: any) { + const targetProject = await this.findOne(userId, projectId); + if (!targetProject || !targetProject.scenes || targetProject.scenes.length === 0) return; + + try { + // Sahnelerin visual prompt'larını yenile (ID'leri ve narration'ları gönderiyoruz) + const mappedScenes = targetProject.scenes.map(s => ({ + id: s.id, + order: s.order, + narrationText: s.narrationText, + visualPrompt: s.visualPrompt + })); + + const rewritten = await this.videoAiService.rewriteAllVisualPrompts( + mappedScenes, + updatedProject.videoStyle, + (updatedProject as any).cinematicReference, + updatedProject.aspectRatio + ); + + // Veritabanına kaydet + for (const newScene of rewritten) { + await this.db.scene.update({ + where: { id: newScene.id }, + data: { visualPrompt: newScene.visualPrompt } + }); + } + this.logger.log(`Görsel promptlar ${projectId} için başarıyla yenilendi.`); + } catch (err) { + this.logger.error(`Visual promptları yenileme işlemi başarısız: ${err}`); + } + } + async remove(userId: string, projectId: string) { await this.findOne(userId, projectId); await this.db.project.update({ @@ -238,6 +287,7 @@ export class ProjectsService { data: { scriptJson: scriptJson as object, status: 'DRAFT', + errorMessage: null, scriptVersion: { increment: 1 }, // SEO & Social metadata (skill-enhanced) seoTitle: scriptJson.seo?.title || scriptJson.metadata.title, @@ -446,6 +496,7 @@ export class ProjectsService { data: { scriptJson: scriptJson as object, status: 'DRAFT', + errorMessage: null, scriptVersion: 1, seoTitle: scriptJson.seo?.title || scriptJson.metadata.title, seoDescription: scriptJson.seo?.description || scriptJson.metadata.description, @@ -548,13 +599,32 @@ export class ProjectsService { const prevScene = project.scenes.find((s) => s.order === scene.order - 1); const nextScene = project.scenes.find((s) => s.order === scene.order + 1); + // Stil DNA bilgilerini prompt'a dahil et + const cinematicRef = (project as any).cinematicReference || ''; + const styleDNA = this.videoAiService.getStyleDNA(project.videoStyle, cinematicRef || undefined); + const contextPrompt = ` Bir video senaryosunun ${scene.order}. sahnesini yeniden üret. Proje konusu: ${project.prompt} Proje dili: ${project.language} Video stili: ${project.videoStyle} -${prevScene ? `Önceki sahne: "${prevScene.narrationText}"` : ''} -${nextScene ? `Sonraki sahne: "${nextScene.narrationText}"` : ''} +${cinematicRef ? `Sinematik referans: ${cinematicRef}` : ''} +En-boy oranı: ${project.aspectRatio || 'PORTRAIT_9_16'} + +═══ GÖRSEL STİL DNA (Bu kurallara KESİNLİKLE uy) ═══ +Referans: ${styleDNA.reference} +Işık: ${styleDNA.lighting} +Lens: ${styleDNA.lens} +Renk: ${styleDNA.color} +Doku: ${styleDNA.texture} +═══════════════════════════════════════════ + +${prevScene ? `Önceki sahne narasyonu: "${prevScene.narrationText}"` : ''} +${prevScene ? `Önceki sahne visual prompt: "${prevScene.visualPrompt}"` : ''} +${nextScene ? `Sonraki sahne narasyonu: "${nextScene.narrationText}"` : ''} + +ÖNEMLİ: visualPrompt İNGİLİZCE olmalı ve yukarıdaki Stil DNA bilgilerini yansıtmalı. +Narration ve subtitle ise ${project.language} dilinde olmalı. Sadece bu tek sahneyi üret. JSON formatında: { @@ -566,18 +636,28 @@ Sadece bu tek sahneyi üret. JSON formatında: const result = await this.videoAiService.generateSingleScene(contextPrompt); + // Stil DNA ile visual prompt'u zenginleştir (5-Layer Architecture™) + const isHookScene = scene.order === 1; + const enrichedVisualPrompt = this.videoAiService.enrichSingleVisualPrompt( + result.visualPrompt, + project.videoStyle, + cinematicRef || undefined, + project.aspectRatio, + isHookScene, + ); + const updated = await this.db.scene.update({ where: { id: sceneId }, data: { narrationText: result.narrationText, - visualPrompt: result.visualPrompt, + visualPrompt: enrichedVisualPrompt, subtitleText: result.subtitleText || result.narrationText, duration: result.durationSeconds || scene.duration, }, include: { mediaAssets: true }, }); - this.logger.log(`Sahne yeniden üretildi: ${sceneId} (proje: ${projectId})`); + this.logger.log(`Sahne yeniden üretildi: ${sceneId} (proje: ${projectId}) — Stil: ${project.videoStyle}${cinematicRef ? ', Ref: ' + cinematicRef : ''}`); return updated; } @@ -603,9 +683,13 @@ Sadece bu tek sahneyi üret. JSON formatında: }; const mappedRatio = aspectRatioMap[project.aspectRatio] || '9:16'; - // Görüntüyü üret + // Görüntüyü üret — stil + sinematik referans bilgisini dahil et + const cinematicRef = (project as any).cinematicReference || ''; + const styleLabel = cinematicRef + ? `Style: ${project.videoStyle}, Cinematic reference: ${cinematicRef}` + : `Style: ${project.videoStyle}`; const imageResult = await this.geminiService.generateImage( - `${scene.visualPrompt}. Style: ${project.videoStyle}`, + `${scene.visualPrompt}. ${styleLabel}`, mappedRatio, ); diff --git a/src/modules/video-ai/video-ai.service.ts b/src/modules/video-ai/video-ai.service.ts index 7ef7f09..c31f634 100644 --- a/src/modules/video-ai/video-ai.service.ts +++ b/src/modules/video-ai/video-ai.service.ts @@ -220,18 +220,17 @@ Every image belongs to a visual universe. Define that universe with specific ref ❌ BAD: "dark and moody" ❌ BAD: "cinematic look" -✅ GOOD: "Blade Runner 2049 color palette with heavy teal-and-amber contrast, Denis Villeneuve visual language, desolate monumental scale" -✅ GOOD: "Wes Anderson symmetry with pastel Easter-egg color palette, The Grand Budapest Hotel framing, whimsical yet melancholic tone, perfectly centered subjects" -✅ GOOD: "National Geographic documentary realism, Planet Earth II visual texture, intimate close-up wildlife photography, David Attenborough's visual signature" -✅ GOOD: "Studio Ghibli hand-painted watercolor backgrounds with lush green landscapes, Hayao Miyazaki cloudscapes, magical realism atmosphere" +✅ GOOD: "[Detailed color palette from the chosen style], [Director/Cinematographer name] visual language, desolate monumental scale" +✅ GOOD: "[Famous Director] symmetry with pastel color palette, whimsical yet melancholic tone, perfectly centered subjects" +✅ GOOD: "National Geographic documentary realism, intimate close-up photography, true-to-life rendering" Mood reference options (use 2-3 per scene combined): -• Film references: "Blade Runner", "Interstellar", "Mad Max: Fury Road", "Spirited Away", "2001: A Space Odyssey", "The Grand Budapest Hotel", "Tenet", "Dune" +• Film references: Refer to the provided Cinematic Style DNA. • Photography references: "Annie Leibovitz portrait lighting", "National Geographic close-up", "Sebastião Salgado black-and-white photojournalism", "Steve McCurry color richness" • Art movements: "Renaissance chiaroscuro", "Impressionist broken color", "Art Deco geometry", "Japanese ukiyo-e woodblock", "Bauhaus minimalism", "Surrealist Dalí dreamscapes" • Color systems: "70s Kodachrome warm tones", "Fujifilm Velvia saturated", "muted Scandinavian palette", "cyberpunk neon (magenta/teal/violet)", "earthy terracotta and sage" -CRITICAL RULE: Once you establish a visual universe in Scene 1, ALL subsequent scenes MUST stay within that same visual world. If Scene 1 is Blade Runner, Scene 5 cannot suddenly look like Wes Anderson. +CRITICAL RULE: Once you establish a visual universe in Scene 1 (based on the provided Style DNA), ALL subsequent scenes MUST stay within that EXACT same visual world. Do not randomly switch styles between scenes. ━━━ LAYER 3: LIGHTING (Source, Direction, Quality) ━━━ Lighting is the single most important factor in image quality. Never leave it to chance. @@ -887,14 +886,14 @@ export class VideoAiService { /** * Video stiline göre varsayılan görsel DNA değerlerini döndürür. */ - private getStyleDNA(videoStyle: string, cinematicReference?: string): StyleDNA { + public getStyleDNA(videoStyle: string, cinematicReference?: string): StyleDNA { const dnaMap: Record = { CINEMATIC: { - reference: cinematicReference ? `${cinematicReference} visual style and cinematography` : 'Denis Villeneuve and Roger Deakins cinematography', - lighting: cinematicReference ? `Iconic lighting setup matching the ${cinematicReference} cinematic style` : 'Dramatic key-and-fill lighting with a single strong motivated source casting deep sculpted shadows.', - lens: cinematicReference ? `Signature camera lens choice and depth of field suitable for ${cinematicReference}` : 'Shot on 35mm anamorphic lens with shallow depth of field f/2.0 and characteristic oval bokeh.', - color: cinematicReference ? `Color grading and palette uniquely associated with ${cinematicReference}` : 'Teal-and-orange blockbuster color grade with desaturated midtones and crushed blacks.', - texture: cinematicReference ? `Film grain and visual texture evoking the ${cinematicReference} experience` : 'Subtle Kodak Vision3 film grain, anamorphic horizontal lens flare, slight vignette darkening corners.', + reference: cinematicReference ? `${cinematicReference} visual style and cinematography` : 'High-end cinematic production with professional cinematography techniques', + lighting: cinematicReference ? `Iconic lighting setup matching the ${cinematicReference} cinematic style` : 'Dramatic key-and-fill lighting with motivated light sources, sculpted shadows, and cinematic depth.', + lens: cinematicReference ? `Signature camera lens choice and depth of field suitable for ${cinematicReference}` : 'Shot on 35mm anamorphic lens with cinematic depth of field and professional framing.', + color: cinematicReference ? `Color grading and palette uniquely associated with ${cinematicReference}` : 'Professional cinematic color grading with balanced contrast, natural skin tones, and atmospheric depth.', + texture: cinematicReference ? `Film grain and visual texture evoking the ${cinematicReference} experience` : 'Subtle organic film grain, natural lens characteristics, cinematic light bloom on practical sources.', }, DOCUMENTARY: { reference: 'National Geographic and Planet Earth II', @@ -1227,15 +1226,45 @@ export class VideoAiService { let parsed: GeneratedScript; try { let cleanText = rawText.trim(); - if (cleanText.startsWith('```json')) cleanText = cleanText.slice(7); - if (cleanText.startsWith('```')) cleanText = cleanText.slice(3); - if (cleanText.endsWith('```')) cleanText = cleanText.slice(0, -3); + + // Pass 1: Markdown code fence temizliği (başta, sonda, ortada) + cleanText = cleanText.replace(/^```(?:json)?\s*/i, ''); + cleanText = cleanText.replace(/\s*```\s*$/i, ''); + // Ortada kalan fence'leri de temizle + cleanText = cleanText.replace(/```(?:json)?\s*/gi, ''); + + // Pass 2: BOM ve kontrol karakterleri temizliği + cleanText = cleanText.replace(/^\uFEFF/, ''); + // JSON-dışı kontrol karakterleri kaldır (tab, newline, CR hariç) + cleanText = cleanText.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); + + // Pass 3: Trailing comma — JSON'da yasak ama Gemini bazen koyuyor + cleanText = cleanText.replace(/,\s*([}\]])/g, '$1'); + cleanText = cleanText.trim(); - parsed = JSON.parse(cleanText); - } catch { - this.logger.error(`JSON parse hatası: ${rawText.substring(0, 500)}`); + + // İlk deneme: Direkt parse + try { + parsed = JSON.parse(cleanText); + } catch { + // Pass 4: Kesilmiş JSON kurtarma — son geçerli } veya ] bul + const lastBrace = cleanText.lastIndexOf('}'); + const lastBracket = cleanText.lastIndexOf(']'); + const cutPoint = Math.max(lastBrace, lastBracket); + + if (cutPoint > 0) { + const truncated = cleanText.substring(0, cutPoint + 1); + this.logger.warn(`JSON kesilmiş olabilir — son ${cleanText.length - cutPoint - 1} karakter atıldı, kurtarma deneniyor...`); + parsed = JSON.parse(truncated); + } else { + throw new Error('Kurtarılabilir JSON bulunamadı'); + } + } + } catch (parseError) { + this.logger.error(`JSON parse hatası — İlk 1000 karakter:\n${rawText.substring(0, 1000)}`); + this.logger.error(`Parse error: ${parseError instanceof Error ? parseError.message : parseError}`); throw new InternalServerErrorException( - 'AI yanıtı geçerli JSON formatında değil.', + 'AI yanıtı geçerli JSON formatında değil. Lütfen "Yeniden Üret" butonuyla tekrar deneyin.', ); } @@ -1282,6 +1311,55 @@ export class VideoAiService { return parsed; } + /** + * Tekil görsel prompt'u stil DNA ile zenginleştirir. + * regenerateScene gibi metotlardan dönen ham visualPrompt'a + * 5-Layer Architecture™ özelliklerini uygular. + */ + enrichSingleVisualPrompt( + visualPrompt: string, + videoStyle: string, + cinematicReference?: string, + aspectRatio?: string, + isHookScene: boolean = false, + ): string { + const styleDNA = this.getStyleDNA(videoStyle, cinematicReference); + const defaultNegative = 'Avoid: text overlays, watermarks, brand logos, recognizable celebrity faces, distorted anatomy, extra fingers, blurry faces, stock photo aesthetic, oversaturated CGI plastic look, generic clip art, UI elements'; + let vp = visualPrompt; + const wordCount = vp.split(/\s+/).length; + const minWords = isHookScene ? 80 : 50; + + // 1. Minimum kelime kontrolü — eksikse stil DNA'sından zenginleştir + if (wordCount < minWords) { + vp = this.padVisualPrompt(vp, styleDNA, minWords, isHookScene); + } + + // 2. Visual continuity anchor + // Önceki prefix'i temizle ki eski stiller yapışıp kalmasın (örn: Blade Runner) + vp = vp.replace(/^Continuing the .*? visual language (established in previous scenes|from previous scenes)?:\s*/i, ''); + + if (!isHookScene) { + vp = `Continuing the ${styleDNA.reference} visual language established in previous scenes: ${vp}`; + } + + // 3. Aspect ratio hint + if (aspectRatio && !vp.toLowerCase().includes('framing') && !vp.toLowerCase().includes('composition')) { + const arHint = aspectRatio === 'PORTRAIT_9_16' + ? 'Vertical framing optimized for mobile viewing.' + : aspectRatio === 'LANDSCAPE_16_9' + ? 'Wide cinematic horizontal composition.' + : 'Square centered symmetrical framing.'; + vp = `${vp} ${arHint}`; + } + + // 4. Negative prompt + if (!vp.toLowerCase().includes('avoid:')) { + vp = `${vp} ${defaultNegative}`; + } + + return vp; + } + /** * Tekil sahne yeniden üretimi — sınırlı bağlam ile sadece 1 sahne üretir. */ @@ -1321,4 +1399,100 @@ export class VideoAiService { throw new InternalServerErrorException('Sahne yeniden üretilemedi.'); } } + + /** + * Projenin tüm görsel promptlarını yeni bir stil DNA'sıyla yeniden yazar. + * Narration ve ID'leri korur, sadece görsel betimlemeleri AI ile baştan yazar. + */ + async rewriteAllVisualPrompts( + scenes: { id: string; order: number; narrationText: string; visualPrompt: string }[], + videoStyle: string, + cinematicReference?: string, + aspectRatio?: string, + ): Promise<{ id: string; visualPrompt: string }[]> { + if (!this.genAI) { + throw new InternalServerErrorException('AI servisi etkin değil.'); + } + + const styleDNA = this.getStyleDNA(videoStyle, cinematicReference); + + const contextPrompt = ` +You are an expert cinematographer. I have a video script with ${scenes.length} scenes. +I changed the project's visual style. I need you to REWRITE ONLY the "visualPrompt" for each scene so they perfectly match the new style. +DO NOT change the scene IDs. + +═══ NEW STYLE DNA ═══ +Reference: ${styleDNA.reference} +Lighting: ${styleDNA.lighting} +Lens: ${styleDNA.lens} +Color: ${styleDNA.color} +Texture: ${styleDNA.texture} +═════════════════════ + +SCENES: +${JSON.stringify( + scenes.map((s) => ({ + id: s.id, + order: s.order, + narrationText: s.narrationText, + oldVisualPrompt: s.visualPrompt, + })), + null, + 2, +)} + +CRITICAL: +- Return valid JSON only. +- The visual prompts must be extremely descriptive, 50+ words, containing lighting, mood, color, and lens info. +- Write purely in English. +- Do NOT include any text overlays or watermarks in the prompts. + +OUTPUT FORMAT (JSON ONLY, NO MARKDOWN FENCES): +{ + "scenes": [ + { + "id": "SCENE-ID-HERE", + "visualPrompt": "new highly detailed cinematic visual prompt here..." + } + ] +} +`; + + try { + const response = await this.genAI.models.generateContent({ + model: this.modelName, + contents: contextPrompt, + config: { + responseMimeType: 'application/json', + temperature: 0.7, + }, + }); + + const rawText = response.text || ''; + const cleaned = rawText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); + const parsed = JSON.parse(cleaned); + + if (!parsed.scenes || !Array.isArray(parsed.scenes)) { + throw new Error('Geçersiz yanıt yapısı.'); + } + + return parsed.scenes.map((s: any) => { + // Zenginleştir (continuity anchor + aspect ratio padding vs) + const enrichedVp = this.enrichSingleVisualPrompt( + s.visualPrompt, + videoStyle, + cinematicReference, + aspectRatio, + false, + ); + return { + id: s.id, + visualPrompt: enrichedVp, + }; + }); + } catch (error) { + this.logger.error(`Toplu prompt yenileme hatası: ${error}`); + throw new InternalServerErrorException('Visual promptlar yenilenemedi.'); + } + } } diff --git a/test-gemini-image.ts b/test-gemini-image.ts deleted file mode 100644 index c1caf79..0000000 --- a/test-gemini-image.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GoogleGenAI } from '@google/genai'; -import * as dotenv from 'dotenv'; - -dotenv.config(); - -const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); - -async function listModels() { - console.log('Listing models...'); - try { - let hasImagen = false; - let hasPreview = false; - // Iterate over pagination using valid SDK method if possible, but let's just use REST if not found. - // However, the new `@google/genai` has ai.models.list() - const response = await ai.models.list(); - for await (const m of response) { - if (m.name.includes('image') || m.name.includes('imagen')) { - console.log(m.name); - } - } - } catch (error) { - console.error('Error listing models:', error); - } -} - -listModels(); diff --git a/test.js b/test.js new file mode 100644 index 0000000..020bd0c --- /dev/null +++ b/test.js @@ -0,0 +1,9 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +async function main() { + const proj = await prisma.project.findFirst({ + where: { id: "3cbc524a-c536-4706-b531-39053c0cd7a1" } + }); + console.log(proj); +} +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/test2.js b/test2.js new file mode 100644 index 0000000..c7c75e1 --- /dev/null +++ b/test2.js @@ -0,0 +1,7 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +async function main() { + const projects = await prisma.project.findMany(); + console.log(projects.map(p => ({id: p.id, title: p.title}))); +} +main().catch(console.error).finally(() => prisma.$disconnect());