generated from fahricansecer/boilerplate-be
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 425 KiB |
@@ -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";
|
||||||
+4
-12
@@ -189,16 +189,8 @@ enum AspectRatio {
|
|||||||
LANDSCAPE_16_9
|
LANDSCAPE_16_9
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VideoStyle {
|
// NOT: VideoStyle artık serbest String — enum kaldırıldı.
|
||||||
CINEMATIC
|
// Frontend 50+ stil destekliyor, DB enum kısıtlaması kaldırıldı.
|
||||||
DOCUMENTARY
|
|
||||||
EDUCATIONAL
|
|
||||||
STORYTELLING
|
|
||||||
NEWS
|
|
||||||
PROMOTIONAL
|
|
||||||
ARTISTIC
|
|
||||||
MINIMALIST
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
VIDEO_CLIP
|
VIDEO_CLIP
|
||||||
@@ -263,7 +255,7 @@ model Project {
|
|||||||
// Configuration
|
// Configuration
|
||||||
language String @default("tr") @db.VarChar(5) // ISO 639-1
|
language String @default("tr") @db.VarChar(5) // ISO 639-1
|
||||||
aspectRatio AspectRatio @default(PORTRAIT_9_16)
|
aspectRatio AspectRatio @default(PORTRAIT_9_16)
|
||||||
videoStyle VideoStyle @default(CINEMATIC)
|
videoStyle String @default("CINEMATIC") @db.VarChar(50)
|
||||||
cinematicReference String? @db.VarChar(200)
|
cinematicReference String? @db.VarChar(200)
|
||||||
targetDuration Int @default(60) // saniye
|
targetDuration Int @default(60) // saniye
|
||||||
|
|
||||||
@@ -599,7 +591,7 @@ model UserPreference {
|
|||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
defaultLanguage String @default("tr") @db.VarChar(5)
|
defaultLanguage String @default("tr") @db.VarChar(5)
|
||||||
defaultVideoStyle VideoStyle @default(CINEMATIC)
|
defaultVideoStyle String @default("CINEMATIC") @db.VarChar(50)
|
||||||
defaultDuration Int @default(60)
|
defaultDuration Int @default(60)
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
|
|||||||
@@ -264,39 +264,58 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
|
|||||||
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}.`;
|
||||||
|
|
||||||
|
// 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({
|
const response = await this.client!.models.generateContent({
|
||||||
model: imageModel,
|
model: imageModel,
|
||||||
contents: [
|
contents: enhancedPrompt,
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
text: `Generate a high-quality image for this description: ${prompt}. Aspect ratio: ${aspectRatio}. Style: photorealistic, cinematic lighting, detailed.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
config: {
|
config: {
|
||||||
responseModalities: ['TEXT', 'IMAGE'] as any,
|
responseModalities: ['IMAGE', 'TEXT'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gemini image response'dan image part'ı çıkar
|
// Gemini image generation modeli, inlineData olarak görsel döner
|
||||||
const parts = (response as any).candidates?.[0]?.content?.parts || [];
|
const candidate = response.candidates?.[0];
|
||||||
for (const part of parts) {
|
const imagePart = candidate?.content?.parts?.find(
|
||||||
if (part.inlineData?.data) {
|
(p: any) => p.inlineData?.mimeType?.startsWith('image/'),
|
||||||
const buffer = Buffer.from(part.inlineData.data, 'base64');
|
);
|
||||||
const mimeType = part.inlineData.mimeType || 'image/png';
|
|
||||||
|
|
||||||
this.logger.log(`✅ Görsel üretildi: ${(buffer.length / 1024).toFixed(1)} KB`);
|
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 };
|
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;
|
return null;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,11 @@ export enum AspectRatioDto {
|
|||||||
LANDSCAPE_16_9 = 'LANDSCAPE_16_9',
|
LANDSCAPE_16_9 = 'LANDSCAPE_16_9',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VideoStyleDto {
|
/**
|
||||||
CINEMATIC = 'CINEMATIC',
|
* Video stili artık serbest string olarak kabul ediliyor.
|
||||||
DOCUMENTARY = 'DOCUMENTARY',
|
* Frontend'de 50+ stil tanımlı (CINEMATIC, ANIME, NOIR, CYBERPUNK vb.).
|
||||||
EDUCATIONAL = 'EDUCATIONAL',
|
* Enum kısıtlaması kaldırıldı — yeni stiller eklendiğinde backend güncellemesi gerekmez.
|
||||||
STORYTELLING = 'STORYTELLING',
|
*/
|
||||||
NEWS = 'NEWS',
|
|
||||||
PROMOTIONAL = 'PROMOTIONAL',
|
|
||||||
ARTISTIC = 'ARTISTIC',
|
|
||||||
MINIMALIST = 'MINIMALIST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CreateProjectDto {
|
export class CreateProjectDto {
|
||||||
@ApiProperty({ description: 'Proje başlığı', example: 'Boötes Boşluğu' })
|
@ApiProperty({ description: 'Proje başlığı', example: 'Boötes Boşluğu' })
|
||||||
@@ -69,13 +64,14 @@ export class CreateProjectDto {
|
|||||||
aspectRatio?: AspectRatioDto;
|
aspectRatio?: AspectRatioDto;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: 'Video stili',
|
description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)',
|
||||||
enum: VideoStyleDto,
|
example: 'CINEMATIC',
|
||||||
default: VideoStyleDto.CINEMATIC,
|
default: 'CINEMATIC',
|
||||||
})
|
})
|
||||||
@IsEnum(VideoStyleDto)
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
videoStyle?: VideoStyleDto;
|
@MaxLength(50)
|
||||||
|
videoStyle?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -139,10 +135,13 @@ export class UpdateProjectDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
aspectRatio?: AspectRatioDto;
|
aspectRatio?: AspectRatioDto;
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: VideoStyleDto })
|
@ApiPropertyOptional({
|
||||||
@IsEnum(VideoStyleDto)
|
description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
videoStyle?: VideoStyleDto;
|
@MaxLength(50)
|
||||||
|
videoStyle?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -196,10 +195,14 @@ export class CreateFromTweetDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
aspectRatio?: AspectRatioDto;
|
aspectRatio?: AspectRatioDto;
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: VideoStyleDto, default: VideoStyleDto.CINEMATIC })
|
@ApiPropertyOptional({
|
||||||
@IsEnum(VideoStyleDto)
|
description: 'Video stili (CINEMATIC, DOCUMENTARY, ANIME, NOIR, CYBERPUNK vb.)',
|
||||||
|
default: 'CINEMATIC',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
videoStyle?: VideoStyleDto;
|
@MaxLength(50)
|
||||||
|
videoStyle?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -163,14 +163,63 @@ export class ProjectsService {
|
|||||||
...(dto.language && { language: dto.language }),
|
...(dto.language && { language: dto.language }),
|
||||||
...(dto.aspectRatio && { aspectRatio: dto.aspectRatio }),
|
...(dto.aspectRatio && { aspectRatio: dto.aspectRatio }),
|
||||||
...(dto.videoStyle && { videoStyle: dto.videoStyle }),
|
...(dto.videoStyle && { videoStyle: dto.videoStyle }),
|
||||||
|
...(dto.cinematicReference !== undefined && { cinematicReference: dto.cinematicReference }),
|
||||||
...(dto.targetDuration && { targetDuration: dto.targetDuration }),
|
...(dto.targetDuration && { targetDuration: dto.targetDuration }),
|
||||||
},
|
} as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Proje güncellendi: ${projectId}`);
|
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;
|
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) {
|
async remove(userId: string, projectId: string) {
|
||||||
await this.findOne(userId, projectId);
|
await this.findOne(userId, projectId);
|
||||||
await this.db.project.update({
|
await this.db.project.update({
|
||||||
@@ -238,6 +287,7 @@ export class ProjectsService {
|
|||||||
data: {
|
data: {
|
||||||
scriptJson: scriptJson as object,
|
scriptJson: scriptJson as object,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
|
errorMessage: null,
|
||||||
scriptVersion: { increment: 1 },
|
scriptVersion: { increment: 1 },
|
||||||
// SEO & Social metadata (skill-enhanced)
|
// SEO & Social metadata (skill-enhanced)
|
||||||
seoTitle: scriptJson.seo?.title || scriptJson.metadata.title,
|
seoTitle: scriptJson.seo?.title || scriptJson.metadata.title,
|
||||||
@@ -446,6 +496,7 @@ export class ProjectsService {
|
|||||||
data: {
|
data: {
|
||||||
scriptJson: scriptJson as object,
|
scriptJson: scriptJson as object,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
|
errorMessage: null,
|
||||||
scriptVersion: 1,
|
scriptVersion: 1,
|
||||||
seoTitle: scriptJson.seo?.title || scriptJson.metadata.title,
|
seoTitle: scriptJson.seo?.title || scriptJson.metadata.title,
|
||||||
seoDescription: scriptJson.seo?.description || scriptJson.metadata.description,
|
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 prevScene = project.scenes.find((s) => s.order === scene.order - 1);
|
||||||
const nextScene = 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 = `
|
const contextPrompt = `
|
||||||
Bir video senaryosunun ${scene.order}. sahnesini yeniden üret.
|
Bir video senaryosunun ${scene.order}. sahnesini yeniden üret.
|
||||||
Proje konusu: ${project.prompt}
|
Proje konusu: ${project.prompt}
|
||||||
Proje dili: ${project.language}
|
Proje dili: ${project.language}
|
||||||
Video stili: ${project.videoStyle}
|
Video stili: ${project.videoStyle}
|
||||||
${prevScene ? `Önceki sahne: "${prevScene.narrationText}"` : ''}
|
${cinematicRef ? `Sinematik referans: ${cinematicRef}` : ''}
|
||||||
${nextScene ? `Sonraki sahne: "${nextScene.narrationText}"` : ''}
|
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:
|
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);
|
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({
|
const updated = await this.db.scene.update({
|
||||||
where: { id: sceneId },
|
where: { id: sceneId },
|
||||||
data: {
|
data: {
|
||||||
narrationText: result.narrationText,
|
narrationText: result.narrationText,
|
||||||
visualPrompt: result.visualPrompt,
|
visualPrompt: enrichedVisualPrompt,
|
||||||
subtitleText: result.subtitleText || result.narrationText,
|
subtitleText: result.subtitleText || result.narrationText,
|
||||||
duration: result.durationSeconds || scene.duration,
|
duration: result.durationSeconds || scene.duration,
|
||||||
},
|
},
|
||||||
include: { mediaAssets: true },
|
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;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,9 +683,13 @@ Sadece bu tek sahneyi üret. JSON formatında:
|
|||||||
};
|
};
|
||||||
const mappedRatio = aspectRatioMap[project.aspectRatio] || '9:16';
|
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(
|
const imageResult = await this.geminiService.generateImage(
|
||||||
`${scene.visualPrompt}. Style: ${project.videoStyle}`,
|
`${scene.visualPrompt}. ${styleLabel}`,
|
||||||
mappedRatio,
|
mappedRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -220,18 +220,17 @@ Every image belongs to a visual universe. Define that universe with specific ref
|
|||||||
|
|
||||||
❌ BAD: "dark and moody"
|
❌ BAD: "dark and moody"
|
||||||
❌ BAD: "cinematic look"
|
❌ BAD: "cinematic look"
|
||||||
✅ GOOD: "Blade Runner 2049 color palette with heavy teal-and-amber contrast, Denis Villeneuve visual language, desolate monumental scale"
|
✅ GOOD: "[Detailed color palette from the chosen style], [Director/Cinematographer name] 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: "[Famous Director] symmetry with pastel color palette, 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: "National Geographic documentary realism, intimate close-up photography, true-to-life rendering"
|
||||||
✅ GOOD: "Studio Ghibli hand-painted watercolor backgrounds with lush green landscapes, Hayao Miyazaki cloudscapes, magical realism atmosphere"
|
|
||||||
|
|
||||||
Mood reference options (use 2-3 per scene combined):
|
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"
|
• 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"
|
• 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"
|
• 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) ━━━
|
━━━ LAYER 3: LIGHTING (Source, Direction, Quality) ━━━
|
||||||
Lighting is the single most important factor in image quality. Never leave it to chance.
|
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.
|
* 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<string, StyleDNA> = {
|
const dnaMap: Record<string, StyleDNA> = {
|
||||||
CINEMATIC: {
|
CINEMATIC: {
|
||||||
reference: cinematicReference ? `${cinematicReference} visual style and cinematography` : 'Denis Villeneuve and Roger Deakins cinematography',
|
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 a single strong motivated source casting deep sculpted shadows.',
|
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 shallow depth of field f/2.0 and characteristic oval bokeh.',
|
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}` : 'Teal-and-orange blockbuster color grade with desaturated midtones and crushed blacks.',
|
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 Kodak Vision3 film grain, anamorphic horizontal lens flare, slight vignette darkening corners.',
|
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: {
|
DOCUMENTARY: {
|
||||||
reference: 'National Geographic and Planet Earth II',
|
reference: 'National Geographic and Planet Earth II',
|
||||||
@@ -1227,15 +1226,45 @@ export class VideoAiService {
|
|||||||
let parsed: GeneratedScript;
|
let parsed: GeneratedScript;
|
||||||
try {
|
try {
|
||||||
let cleanText = rawText.trim();
|
let cleanText = rawText.trim();
|
||||||
if (cleanText.startsWith('```json')) cleanText = cleanText.slice(7);
|
|
||||||
if (cleanText.startsWith('```')) cleanText = cleanText.slice(3);
|
// Pass 1: Markdown code fence temizliği (başta, sonda, ortada)
|
||||||
if (cleanText.endsWith('```')) cleanText = cleanText.slice(0, -3);
|
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();
|
cleanText = cleanText.trim();
|
||||||
|
|
||||||
|
// İlk deneme: Direkt parse
|
||||||
|
try {
|
||||||
parsed = JSON.parse(cleanText);
|
parsed = JSON.parse(cleanText);
|
||||||
} catch {
|
} catch {
|
||||||
this.logger.error(`JSON parse hatası: ${rawText.substring(0, 500)}`);
|
// 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(
|
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;
|
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.
|
* 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.');
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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());
|
||||||
@@ -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());
|
||||||
Reference in New Issue
Block a user