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

This commit is contained in:
Harun CAN
2026-04-25 14:37:46 +02:00
parent ad5a97a4fd
commit 9d8c34b39d
34 changed files with 5853 additions and 164 deletions
+7 -5
View File
@@ -253,9 +253,14 @@ export class AuthService {
tenantId: user.tenantId || undefined,
};
const isAdmin = roles.includes('admin');
const accessExpiration = isAdmin
? '7d'
: this.configService.get('JWT_ACCESS_EXPIRATION', '15m');
// Generate access token
const accessToken = this.jwtService.sign(payload, {
expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
expiresIn: accessExpiration as any,
});
// Generate refresh token
@@ -276,10 +281,7 @@ export class AuthService {
return {
accessToken,
refreshToken: refreshTokenValue,
expiresIn:
this.parseExpiration(
this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
) / 1000, // Convert to seconds
expiresIn: this.parseExpiration(accessExpiration) / 1000, // Convert to seconds
user: {
id: user.id,
email: user.email,
+23 -10
View File
@@ -279,11 +279,19 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
try {
this.logger.log(`🔄 Katman 1 (deneme ${attempt}/2): ${primaryModel}`);
const result = await this.tryGenerateContentImage(primaryModel, enhancedPrompt);
if (result) {
if (result && result.buffer.length > 0) {
this.logger.log(`✅ Görsel üretildi (${primaryModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`);
return result;
return { buffer: result.buffer, mimeType: result.mimeType };
}
this.logger.warn(`⚠️ ${primaryModel} deneme ${attempt}: görsel döndürmedi (null response)`);
const reason = result?.errorReason || 'null response';
this.logger.warn(`⚠️ ${primaryModel} deneme ${attempt}: görsel döndürmedi (${reason})`);
if (['IMAGE_OTHER', 'SAFETY', 'PROHIBITED_CONTENT'].includes(reason)) {
this.logger.warn(`🚫 Güvenlik/Politika filtresi tetiklendi (${reason}). Denemeler iptal ediliyor.`);
break; // Fail fast for safety blocks
}
if (attempt < 2) await this.sleep(2000);
} catch (err1: any) {
this.logger.warn(`⚠️ ${primaryModel} deneme ${attempt} hata: ${err1.message?.substring(0, 200)}`);
@@ -295,11 +303,15 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
try {
this.logger.log(`🔄 Katman 2: ${fallbackModel}`);
const result = await this.tryGenerateContentImage(fallbackModel, enhancedPrompt);
if (result) {
if (result && result.buffer.length > 0) {
this.logger.log(`✅ Görsel üretildi (${fallbackModel}): ${(result.buffer.length / 1024).toFixed(1)} KB`);
return result;
return { buffer: result.buffer, mimeType: result.mimeType };
}
this.logger.warn(`⚠️ ${fallbackModel}: görsel döndürmedi (${result?.errorReason || 'null response'})`);
if (['IMAGE_OTHER', 'SAFETY', 'PROHIBITED_CONTENT'].includes(result?.errorReason || '')) {
this.logger.warn(`🚫 Katman 2 Güvenlik/Politika filtresi tetiklendi. Katman 3'e geçiliyor.`);
}
this.logger.warn(`⚠️ ${fallbackModel}: görsel döndürmedi (null response)`);
} catch (err2: any) {
this.logger.warn(`⚠️ ${fallbackModel} hata: ${err2.message?.substring(0, 200)}`);
}
@@ -323,7 +335,7 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
this.logger.log(`✅ Görsel üretildi (Imagen 4): ${(buffer.length / 1024).toFixed(1)} KB`);
return { buffer, mimeType };
}
this.logger.warn('⚠️ Imagen 4: görsel döndürmedi');
this.logger.warn(`⚠️ Imagen 4: görsel döndürmedi. Üretilen görsel sayısı: ${response.generatedImages?.length || 0}`);
} catch (err3: any) {
this.logger.warn(`⚠️ Imagen 4 hata: ${err3.message?.substring(0, 200)}`);
}
@@ -343,7 +355,7 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
private async tryGenerateContentImage(
model: string,
prompt: string,
): Promise<{ buffer: Buffer; mimeType: string } | null> {
): Promise<{ buffer: Buffer; mimeType: string; errorReason?: string } | null> {
const response = await this.client!.models.generateContent({
model,
contents: prompt,
@@ -358,7 +370,7 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
if (!candidate?.content?.parts || candidate.content.parts.length === 0) {
const finishReason = candidate?.finishReason || 'UNKNOWN';
this.logger.warn(`⚠️ ${model}: boş yanıt (finishReason: ${finishReason})`);
return null;
return { buffer: Buffer.from([]), mimeType: '', errorReason: finishReason };
}
const imagePart = candidate.content.parts.find(
@@ -375,9 +387,10 @@ IMPORTANT: Only output valid JSON, no markdown code blocks or other text.`;
const textParts = candidate.content.parts.filter((p: any) => p.text);
if (textParts.length > 0) {
this.logger.warn(`⚠️ ${model}: sadece text döndü, görsel yok. Text: "${textParts[0].text?.substring(0, 100)}"`);
return { buffer: Buffer.from([]), mimeType: '', errorReason: 'TEXT_ONLY' };
}
return null;
return { buffer: Buffer.from([]), mimeType: '', errorReason: 'NO_IMAGE_DATA' };
}
/** Basit uyku fonksiyonu — retry aralarında kullanılır */
+69 -3
View File
@@ -214,7 +214,7 @@ export class CreateFromTweetDto {
@IsInt()
@IsOptional()
@Min(15)
@Max(90)
@Max(180)
targetDuration?: number;
}
@@ -261,11 +261,17 @@ export class CreateFromYoutubeDto {
@MaxLength(50)
videoStyle?: string;
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
@IsString()
@IsOptional()
@MaxLength(200)
cinematicReference?: string;
@ApiPropertyOptional({ description: 'Hedef video süresi (saniye)', default: 60 })
@IsInt()
@IsOptional()
@Min(15)
@Max(90)
@Max(180)
targetDuration?: number;
}
@@ -300,10 +306,70 @@ export class CreateFromDocumentDto {
@MaxLength(50)
videoStyle?: string;
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
@IsString()
@IsOptional()
@MaxLength(200)
cinematicReference?: string;
@ApiPropertyOptional({ description: 'Hedef video süresi (saniye)', default: 60 })
@IsInt()
@IsOptional()
@Min(15)
@Max(90)
@Max(180)
targetDuration?: number;
}
export class CreateFromExtractedTextDto {
@ApiProperty({ description: 'Çıkarılan metin' })
@IsString()
@IsNotEmpty()
text: string;
@ApiProperty({ description: 'Seçilen video konusu' })
@IsString()
@IsNotEmpty()
topic: string;
@ApiPropertyOptional({ description: 'Orijinal dosya adı' })
@IsString()
@IsOptional()
originalFilename?: string;
@ApiPropertyOptional({ description: 'Video dili (ISO 639-1)', default: 'tr' })
@IsString()
@IsOptional()
@MaxLength(5)
language?: string;
@ApiPropertyOptional({
description: 'En-boy oranı (PORTRAIT_9_16, LANDSCAPE_16_9, SQUARE_1_1)',
default: 'PORTRAIT_9_16',
})
@IsString()
@IsOptional()
@MaxLength(20)
aspectRatio?: string;
@ApiPropertyOptional({
description: 'Video stili (CINEMATIC, DOCUMENTARY, vb.)',
default: 'CINEMATIC',
})
@IsString()
@IsOptional()
@MaxLength(50)
videoStyle?: string;
@ApiPropertyOptional({ description: 'Sinematik stil referansı (örn: Wes Anderson, Matrix)' })
@IsString()
@IsOptional()
@MaxLength(200)
cinematicReference?: string;
@ApiPropertyOptional({ description: 'Hedef video süresi (saniye)', default: 60 })
@IsInt()
@IsOptional()
@Min(15)
@Max(180)
targetDuration?: number;
}
+34 -1
View File
@@ -26,7 +26,7 @@ import {
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { ProjectsService } from './projects.service';
import { CreateProjectDto, UpdateProjectDto, CreateFromTweetDto, CreateFromYoutubeDto, CreateFromDocumentDto } from './dto/project.dto';
import { CreateProjectDto, UpdateProjectDto, CreateFromTweetDto, CreateFromYoutubeDto, CreateFromDocumentDto, CreateFromExtractedTextDto } from './dto/project.dto';
@ApiTags('projects')
@ApiBearerAuth()
@@ -191,6 +191,39 @@ export class ProjectsController {
return this.projectsService.createFromDocument(userId, file, dto);
}
/**
* Doküman yüklenip metni çıkarılır ve video konu önerileri üretilir.
*/
@Post('extract-document-topics')
@HttpCode(HttpStatus.OK)
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@ApiOperation({ summary: 'Dosyadan metin çıkar ve konu önerileri al' })
@ApiResponse({ status: 200, description: 'Metin ve konular başarıyla çıkarıldı' })
async extractDocumentTopics(
@UploadedFile() file: Express.Multer.File,
@Req() req: any,
) {
this.logger.log(`Dosyadan metin ve konular çıkarılıyor: ${file?.originalname}`);
if (!file) {
throw new BadRequestException('Dosya yüklenmedi');
}
return this.projectsService.extractDocumentTopics(file);
}
/**
* Extracted text ve seçilen konu üzerinden doğrudan proje oluşturur.
*/
@Post('document-from-topic')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Seçilen konu ve metin ile proje oluştur' })
@ApiResponse({ status: 201, description: 'Seçilen konu baz alınarak proje oluşturuldu' })
async createFromTopic(@Body() dto: CreateFromExtractedTextDto, @Req() req: any) {
const userId = req.user?.id || req.user?.sub;
this.logger.log(`Metin ve konu üzerinden proje oluşturuluyor. Konu: ${dto.topic}`);
return this.projectsService.createFromExtractedText(userId, dto);
}
/**
* Tekil sahne güncelleme (narrasyon, görsel prompt, süre).
*/
+170 -24
View File
@@ -4,7 +4,7 @@ import {
BadRequestException,
Logger,
} from '@nestjs/common';
import { TransitionType } from '@prisma/client';
import { TransitionType, AspectRatio } from '@prisma/client';
import { PrismaService } from '../../database/prisma.service';
import { VideoAiService } from '../video-ai/video-ai.service';
import { VideoQueueModule } from '../video-queue/video-queue.module';
@@ -13,9 +13,11 @@ import { XTwitterService } from '../x-twitter/x-twitter.service';
import { GeminiService } from '../gemini/gemini.service';
import { StorageService } from '../storage/storage.service';
import { ExtractorService } from '../extractor/extractor.service';
import { CreateProjectDto, UpdateProjectDto, CreateFromTweetDto, CreateFromYoutubeDto, CreateFromDocumentDto } from './dto/project.dto';
import { CreateProjectDto, UpdateProjectDto, CreateFromTweetDto, CreateFromYoutubeDto, CreateFromDocumentDto, CreateFromExtractedTextDto } from './dto/project.dto';
import sharp from 'sharp';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
interface FindAllOptions {
page: number;
@@ -360,15 +362,21 @@ export class ProjectsService {
aspectRatio: project.aspectRatio,
videoStyle: project.videoStyle,
targetDuration: project.targetDuration,
scenes: project.scenes.map((s) => ({
id: s.id,
order: s.order,
narrationText: s.narrationText,
visualPrompt: s.visualPrompt,
subtitleText: s.subtitleText || s.narrationText,
duration: s.duration,
transitionType: s.transitionType,
})),
scenes: project.scenes.map((s) => {
const thumbnail = s.mediaAssets?.find(m => m.type === 'THUMBNAIL');
const imagePath = thumbnail && thumbnail.s3Key ? this.storageService.getAbsolutePath(thumbnail.s3Key) : undefined;
return {
id: s.id,
order: s.order,
narrationText: s.narrationText,
visualPrompt: s.visualPrompt,
subtitleText: s.subtitleText || s.narrationText,
duration: s.duration,
transitionType: s.transitionType,
imagePath,
ttsProvider: 'openai', // TODO: Make configurable from frontend or project settings
};
}),
});
await this.db.renderJob.update({
@@ -622,31 +630,87 @@ export class ProjectsService {
}
}
/**
* PDF, Word vb. dokümandan metin çıkarır ve konu önerileri üretir.
*/
async extractDocumentTopics(file: Express.Multer.File) {
this.logger.log(`Belgeden konu önerileri çıkarılıyor: ${file.originalname}`);
let tempFilePath: string | null = null;
let extractedText = '';
try {
if (file.path) {
tempFilePath = file.path;
} else if (file.buffer) {
tempFilePath = path.join(os.tmpdir(), `${Date.now()}-${file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')}`);
await fs.writeFile(tempFilePath, file.buffer);
} else {
throw new Error("Dosya içeriği okunamadı (Buffer veya Path yok).");
}
extractedText = await this.extractorService.extractFromFile(tempFilePath, file.originalname, file.mimetype);
} finally {
if (tempFilePath && !file.path) {
await fs.unlink(tempFilePath).catch(e => this.logger.warn(`Temp dosya silinemedi: ${e.message}`));
}
}
if (!extractedText || extractedText.trim().length === 0) {
throw new BadRequestException("Belgeden okunabilir metin çıkarılamadı.");
}
// Kısa metinse doğrudan 1 konu öner (kendi başlığı gibi), uzunsa çoklu konu
let topics: string[] = [];
if (extractedText.length < 5000) {
topics = [file.originalname.split('.')[0] || "Belge Özeti"];
} else {
topics = await this.videoAiService.suggestDocumentTopics(extractedText, 4);
}
return {
text: extractedText,
topics,
originalFilename: file.originalname
};
}
/**
* PDF, Word vb. dokümandan proje oluşturur.
*/
async createFromDocument(userId: string, file: Express.Multer.File, dto: CreateFromDocumentDto) {
this.logger.log(`Belgeden proje oluşturuluyor: ${file.originalname}`);
// Gelen dosyanın geçici path'i
// Not: multer ile yüklendiğinde `file.path` üzerinden geçici dosya adresini alabiliyoruz
if (!file.path) {
// Eğer memoryStorage kullanılıyorsa temp dizine yazılarak paslanabilir,
// bu örnekte form-data ile Python extractor'a gönderileceği varsayılıyor
throw new Error("Multer destPath bulunamadı, diskStorage kullanılmalıdır.");
}
let tempFilePath: string | null = null;
let extractedText = '';
const extractedText = await this.extractorService.extractFromFile(file.path, file.originalname, file.mimetype);
try {
if (file.path) {
tempFilePath = file.path;
} else if (file.buffer) {
tempFilePath = path.join(os.tmpdir(), `${Date.now()}-${file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')}`);
await fs.writeFile(tempFilePath, file.buffer);
} else {
throw new Error("Dosya içeriği okunamadı (Buffer veya Path yok).");
}
extractedText = await this.extractorService.extractFromFile(tempFilePath, file.originalname, file.mimetype);
} finally {
if (tempFilePath && !file.path) {
await fs.unlink(tempFilePath).catch(e => this.logger.warn(`Temp dosya silinemedi: ${e.message}`));
}
}
// Başlık ve prompt belirlenmesi
const title = dto.title || `${file.originalname} Özeti`;
const prompt = `Aşağıda içeriği verilen dökümandan çarpıcı bir video senaryosu üret:\n\n${extractedText.substring(0, 15000)}`;
const fullAiPrompt = `Aşağıda içeriği verilen dökümandan çarpıcı bir video senaryosu üret:\n\n${extractedText.substring(0, 15000)}`;
const shortDbPrompt = `Belge üzerinden oluşturuldu: ${file.originalname}`;
const project = await this.db.project.create({
data: {
title,
description: `Belge üzerinden üretildi: ${file.originalname}`,
prompt,
prompt: shortDbPrompt,
language: dto.language || 'tr',
aspectRatio: dto.aspectRatio || 'PORTRAIT_9_16',
videoStyle: dto.videoStyle || 'CINEMATIC',
@@ -659,7 +723,7 @@ export class ProjectsService {
try {
const scriptJson = await this.videoAiService.generateVideoScript({
topic: prompt,
topic: fullAiPrompt,
targetDurationSeconds: project.targetDuration,
language: project.language,
videoStyle: project.videoStyle,
@@ -704,6 +768,82 @@ export class ProjectsService {
}
}
/**
* Çıkarılmış metin ve kullanıcının seçtiği bir "topic" üzerinden proje oluşturur.
*/
async createFromExtractedText(userId: string, dto: CreateFromExtractedTextDto) {
this.logger.log(`Metin ve konu üzerinden proje oluşturuluyor: ${dto.topic}`);
const title = dto.topic;
// Tam prompt metni (AI'a gönderilecek)
const fullAiPrompt = `Aşağıda içeriği verilen metinden, özellikle "${dto.topic}" konusuna odaklanan çarpıcı bir video senaryosu üret:\n\n${dto.text.substring(0, 15000)}`;
// Veritabanına kaydedilecek kısa prompt metni (VarChar 2000 limitine takılmaması için)
const shortDbPrompt = `Belge/Metin üzerinden "${dto.topic}" konusu hedeflenerek oluşturuldu.`;
const project = await this.db.project.create({
data: {
title,
description: dto.originalFilename ? `Belgeden üretildi: ${dto.originalFilename} (Konu: ${dto.topic})` : `Metinden üretildi (Konu: ${dto.topic})`,
prompt: shortDbPrompt,
language: dto.language || 'tr',
aspectRatio: (dto.aspectRatio as AspectRatio) || AspectRatio.PORTRAIT_9_16,
videoStyle: dto.videoStyle || 'CINEMATIC',
cinematicReference: dto.cinematicReference,
targetDuration: dto.targetDuration || 60,
status: 'GENERATING_SCRIPT',
userId,
sourceType: 'DOCUMENT',
},
});
try {
const scriptJson = await this.videoAiService.generateVideoScript({
topic: fullAiPrompt,
targetDurationSeconds: project.targetDuration,
language: project.language,
videoStyle: project.videoStyle,
cinematicReference: project.cinematicReference ?? undefined,
});
const scenesData = scriptJson.scenes.map((scene: any) => ({
projectId: project.id,
order: scene.order,
title: scene.title || `Sahne ${scene.order}`,
narrationText: scene.narrationText,
visualPrompt: scene.visualPrompt,
subtitleText: scene.subtitleText,
duration: scene.durationSeconds,
transitionType: this.mapTransitionType(scene.transitionType),
}));
await this.db.scene.createMany({ data: scenesData });
const updatedProject = await this.db.project.update({
where: { id: project.id },
data: {
scriptJson: scriptJson as object,
status: 'DRAFT',
errorMessage: null,
scriptVersion: 1,
},
include: {
scenes: { orderBy: { order: 'asc' } },
},
});
return updatedProject;
} catch (error) {
await this.db.project.update({
where: { id: project.id },
data: {
status: 'DRAFT',
errorMessage: error instanceof Error ? error.message : 'Konu bazlı senaryo üretimi sırasında hata',
},
});
throw error;
}
}
/**
* Tekil sahne güncelleme — narrasyon, görsel prompt, altyazı veya süre.
*/
@@ -856,13 +996,19 @@ Sadece bu tek sahneyi üret. JSON formatında:
const styleLabel = cinematicRef
? `Style: ${project.videoStyle}, Cinematic reference: ${cinematicRef}`
: `Style: ${project.videoStyle}`;
const imageResult = await this.geminiService.generateImage(
let imageResult = await this.geminiService.generateImage(
`${scene.visualPrompt}. ${styleLabel}`,
mappedRatio,
);
if (!imageResult) {
throw new BadRequestException('Görsel üretilemedi, servis yanıt vermedi');
this.logger.warn(`⚠️ Orijinal prompt ile görsel üretilemedi. Güvenli fallback deneniyor...`);
const safePrompt = `A cinematic, highly detailed abstract visualization matching the mood of: ${project.videoStyle}. Ensure professional quality, 8k resolution. Do not include specific people, recognizable faces, or real-world public figures.`;
imageResult = await this.geminiService.generateImage(safePrompt, mappedRatio);
}
if (!imageResult) {
throw new BadRequestException('Görsel üretilemedi, güvenlik filtreleri veya servis hatası nedeniyle işlem başarısız oldu.');
}
// Storage'a kaydet
+57 -8
View File
@@ -298,11 +298,11 @@ This is CRITICAL. All scenes in one project must feel like they belong to the sa
Match the "videoStyle" to its corresponding visual DNA. These are your default creative parameters per style:
CINEMATIC:
Reference: Denis Villeneuve, Roger Deakins cinematography, Christopher Nolan IMAX
Reference: High-end cinematic production with professional cinematography techniques
Lighting: Dramatic key-and-fill, single strong motivated source, deep shadows
Lens: 35mm anamorphic or 65mm IMAX, shallow DOF
Color: Teal-orange grade, desaturated midtones, crushed blacks
Texture: Film grain, anamorphic lens flare, subtle vignette
Color: Professional cinematic color grading, balanced contrast, atmospheric depth
Texture: Subtle organic film grain, anamorphic lens flare, cinematic light bloom
DOCUMENTARY:
Reference: National Geographic, Planet Earth II, David Attenborough
@@ -616,6 +616,51 @@ export class VideoAiService {
}
}
/**
* Uzun metinlerden (kitap, uzun makale vb.) potansiyel video konuları çıkarır.
* Gemini 1.5 Flash kullanarak 3-4 çarpıcı YouTube video başlığı önerir.
*/
async suggestDocumentTopics(text: string, count: number = 4): Promise<string[]> {
this.logger.log(`Dokümandan konu önerileri çıkarılıyor... (Metin uzunluğu: ${text.length})`);
const systemPrompt = `You are an elite YouTube producer and content strategist.
Your task is to analyze the provided book/document extract and suggest exactly ${count} highly engaging, distinct video topics or angles that could be made into successful YouTube Shorts or videos.
REQUIREMENTS:
- Return ONLY a JSON array of strings. No markdown, no explanations, no wrapping object.
- Example: ["The Hidden Psychology of Habits", "Why Discipline Beats Motivation", "The 5-Second Rule Explained"]
- Each topic should be punchy, curiosity-driven, and clearly related to the core themes of the text.
- Language: Turkish.`;
const userPrompt = `Extract ${count} engaging video topics from this text:\n\n${text.substring(0, 20000)}`;
try {
const response = await this.genAI.models.generateContent({
model: this.modelName,
contents: userPrompt,
config: {
systemInstruction: systemPrompt,
temperature: 0.7,
topP: 0.9,
responseMimeType: 'application/json',
},
});
const rawText = response.text ?? '[]';
const topics: string[] = JSON.parse(rawText);
this.logger.log(`${topics.length} adet konu önerisi çıkarıldı.`);
return topics;
} catch (error) {
this.logger.error(
`Konu çıkarma hatası: ${error instanceof Error ? error.message : 'Bilinmeyen'}`,
);
throw new InternalServerErrorException(
`Video konuları çıkarılamadı: ${error instanceof Error ? error.message : 'API hatası'}`,
);
}
}
private buildUserPrompt(input: ScriptGenerationInput): string {
const langMap: Record<string, string> = {
tr: 'Turkish', en: 'English', es: 'Spanish', de: 'German',
@@ -640,6 +685,8 @@ export class VideoAiService {
`- Make it viral-worthy, visually stunning, and intellectually captivating\n` +
`- The first 2 seconds must hook the viewer immediately\n` +
`- Write narration that sounds HUMAN — avoid AI writing patterns\n` +
`- WHITE-LABELING (CRITICAL): NEVER mention the original source, creator, author, URL, channel name, or @username. Present all content as if YOU are the original creator.\n` +
`- DO NOT include logos, handles, or mentions of the original source in your visual prompts.\n` +
`- Include SEO-optimized metadata with keywords and schema markup\n` +
`- Generate social media captions for YouTube, TikTok, Instagram, Twitter\n`;
@@ -684,11 +731,13 @@ export class VideoAiService {
}
}
prompt += `\nIMPORTANT:\n`;
prompt += `- Analyze WHY this tweet went viral and capture that energy\n`;
prompt += `- The narration should feel like a reaction/commentary on the tweet content\n`;
prompt += `- Mention the original tweet author @${tw.authorUsername} naturally in narration\n`;
prompt += `- Use both the tweet's images as reference AND generate new AI visuals\n`;
prompt += `\nIMPORTANT WHITE-LABELING RULES (CRITICAL):\n`;
prompt += `- Analyze the core message of the tweet and capture its energy.\n`;
prompt += `- You are creating ORIGINAL content. Do NOT act like you are reacting to or commenting on someone else's post.\n`;
prompt += `- ABSOLUTELY DO NOT mention the original author (@${tw.authorUsername}), their real name, or the fact that this is from a tweet/X.\n`;
prompt += `- DO NOT include any logos, usernames, or references to the original source in your visual prompts (e.g. no "@${tw.authorUsername} logo").\n`;
prompt += `- Present the facts, stories, or insights as if YOU are the original expert creator.\n`;
prompt += `- Use the tweet's images as reference for the visuals, but describe them generally without mentioning any source brands or handles.\n`;
prompt += `═══════════════════════════════\n`;
}
@@ -21,6 +21,8 @@ export interface VideoGenerationJobPayload {
duration: number;
transitionType: string;
ambientSoundPrompt?: string; // AudioGen: sahne bazlı ortam sesi
imagePath?: string; // Gemini'den üretilen görselin yerel yolu
ttsProvider?: string; // openai veya elevenlabs
}>;
}