generated from fahricansecer/boilerplate-be
@@ -0,0 +1,48 @@
|
||||
export class UploadedFileDto {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class AnalyzeContentDto {
|
||||
transcripts: UploadedFileDto[];
|
||||
comments: UploadedFileDto[];
|
||||
tone: string;
|
||||
duration: string;
|
||||
speakerName: string;
|
||||
topicFocus: string;
|
||||
targetAudience: string;
|
||||
}
|
||||
|
||||
export class StrategyResultDto {
|
||||
title: string;
|
||||
psychologicalTheme?: string;
|
||||
inspiredByGap?: string;
|
||||
hook?: string;
|
||||
thumbnailConcept?: string;
|
||||
segments: {
|
||||
type: string;
|
||||
duration: string;
|
||||
description: string;
|
||||
keyPoints: string[];
|
||||
neuroObjective?: string;
|
||||
}[];
|
||||
interviewQuestions: string[];
|
||||
selectedComments: {
|
||||
username?: string;
|
||||
text: string;
|
||||
insightValue?: string;
|
||||
sourceFile?: string;
|
||||
}[];
|
||||
commercialAnalysis: {
|
||||
suitableIndustries: string[];
|
||||
brandSafetyScore: number;
|
||||
suggestedBrands: string[];
|
||||
monetizationPotential: string;
|
||||
};
|
||||
chartData?: { topic: string; emotionalArousal: number }[];
|
||||
}
|
||||
|
||||
export class CommercialAnalysisDto {
|
||||
title: string;
|
||||
industries: string[];
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { GoogleGenAI, Type } from '@google/genai';
|
||||
import { AnalyzeContentDto, StrategyResultDto, CommercialAnalysisDto } from './dto/tube-strategist.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TubeStrategistService {
|
||||
private readonly logger = new Logger(TubeStrategistService.name);
|
||||
private ai: GoogleGenAI;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const apiKey =
|
||||
this.configService.get<string>('gemini.apiKey') ||
|
||||
process.env.GOOGLE_API_KEY;
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
}
|
||||
|
||||
private async withRetry<T>(fn: () => Promise<T>, retries = 3, delay = 2000): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
retries > 0 &&
|
||||
(error.status === 429 || error.toString().includes('500'))
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.withRetry(fn, retries - 1, delay * 1.5);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeContent(dto: AnalyzeContentDto): Promise<StrategyResultDto> {
|
||||
const transcriptText = dto.transcripts
|
||||
.map((t) => `[DOSYA: ${t.name}]\n${t.content.substring(0, 10000)}`)
|
||||
.join('\n\n');
|
||||
const commentText = dto.comments
|
||||
.map((c) => `[DOSYA: ${c.name}]\n${c.content.substring(0, 5000)}`)
|
||||
.join('\n\n');
|
||||
|
||||
const schema: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
title: { type: Type.STRING },
|
||||
psychologicalTheme: { type: Type.STRING },
|
||||
inspiredByGap: { type: Type.STRING },
|
||||
hook: { type: Type.STRING },
|
||||
thumbnailConcept: { type: Type.STRING },
|
||||
segments: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
type: { type: Type.STRING },
|
||||
duration: {
|
||||
type: Type.STRING,
|
||||
description: 'Dakika damgası. Örn: 0-5. dk, 5-12. dk',
|
||||
},
|
||||
description: { type: Type.STRING },
|
||||
keyPoints: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
neuroObjective: { type: Type.STRING },
|
||||
},
|
||||
},
|
||||
},
|
||||
interviewQuestions: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
selectedComments: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
username: { type: Type.STRING },
|
||||
text: { type: Type.STRING },
|
||||
insightValue: { type: Type.STRING },
|
||||
sourceFile: {
|
||||
type: Type.STRING,
|
||||
description: 'Yorumun alındığı dosya adı',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
commercialAnalysis: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
suitableIndustries: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
brandSafetyScore: { type: Type.NUMBER },
|
||||
suggestedBrands: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
monetizationPotential: { type: Type.STRING },
|
||||
},
|
||||
},
|
||||
chartData: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
topic: { type: Type.STRING },
|
||||
emotionalArousal: { type: Type.NUMBER },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'title',
|
||||
'segments',
|
||||
'interviewQuestions',
|
||||
'commercialAnalysis',
|
||||
'selectedComments',
|
||||
],
|
||||
};
|
||||
|
||||
const prompt = `
|
||||
ROL: YouTube Strateji Uzmanı.
|
||||
KREATİF TALİMAT: Mevcut verileri sadece kopyalama. Verilerdeki boşlukları kullanarak DAHA ÖNCE YAPILMAMIŞ, özgün ve sıradışı bir video fikri üret.
|
||||
|
||||
KURALLAR:
|
||||
1. ÜSLUP VE RUH: Tüm içerik "${dto.tone}" tonunda olmalı.
|
||||
2. ZAMANLAMA: Videonun süresi ${dto.duration}. Segmentleri matematiksel olarak böl (Örn: 0-5. dk, 5-12. dk, 12-20. dk vb.). Her segment 3-8 dk arası olsun.
|
||||
3. SORULAR: Tam 20 tane derin, sarsıcı mülakat sorusu üret.
|
||||
4. YORUMLAR: Seçtiğin her yorumun hangi dosyadan geldiğini 'sourceFile' alanına yaz.
|
||||
|
||||
VERİLER:
|
||||
TRANSKRİPTLER: ${transcriptText}
|
||||
YORUMLAR: ${commentText}
|
||||
|
||||
Detaylar: Kitle: ${dto.targetAudience}, Sunucu: ${dto.speakerName}, Konu: ${dto.topicFocus}
|
||||
`;
|
||||
|
||||
return await this.withRetry(async () => {
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: prompt,
|
||||
config: {
|
||||
responseMimeType: 'application/json',
|
||||
responseSchema: schema,
|
||||
thinkingConfig: { thinkingBudget: 16000 },
|
||||
},
|
||||
});
|
||||
return JSON.parse(response.text || '{}');
|
||||
});
|
||||
}
|
||||
|
||||
async generateSeoReport(strategy: StrategyResultDto): Promise<any> {
|
||||
const schema: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
mainKeywords: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
tags: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
description: 'Sadece kelimeler, başında # olmadan',
|
||||
},
|
||||
optimizedTitle: { type: Type.STRING },
|
||||
metaDescription: { type: Type.STRING },
|
||||
competitorGap: { type: Type.STRING },
|
||||
alternativeTitles: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
title: { type: Type.STRING },
|
||||
neuroScore: { type: Type.NUMBER },
|
||||
psychologicalAngle: { type: Type.STRING },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = `
|
||||
Video: "${strategy.title}".
|
||||
Görev: Profesyonel YouTube SEO analizi yap.
|
||||
ÖNEMLİ: 'tags' dizisine sadece virgülle ayrılacak saf kelimeleri yaz, başında # olmasın.
|
||||
5 tane alternatif başlık üret ve her birine 0-100 arası başarı (neuro) puanı ver.
|
||||
`;
|
||||
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
config: { responseMimeType: 'application/json', responseSchema: schema },
|
||||
});
|
||||
return JSON.parse(response.text || '{}');
|
||||
}
|
||||
|
||||
async generateNeuroReport(strategy: StrategyResultDto): Promise<any> {
|
||||
const schema: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
eyeTrackingFocus: { type: Type.STRING },
|
||||
colorPsychology: { type: Type.STRING },
|
||||
dopamineTriggers: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
limbicSystemGoal: { type: Type.STRING },
|
||||
attentionSpans: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: { phase: { type: Type.STRING }, score: { type: Type.NUMBER } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: `Video Konsepti: "${strategy.title}". Bu video için aşırı detaylı nöro-pazarlama analizi yap. İzleyicinin beyninde oluşacak dopamin döngüsünü kurgula.`,
|
||||
config: { responseMimeType: 'application/json', responseSchema: schema },
|
||||
});
|
||||
return JSON.parse(response.text || '{}');
|
||||
}
|
||||
|
||||
async generateMarketingReport(strategy: StrategyResultDto): Promise<any> {
|
||||
const schema: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
targetPersonas: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
socialMediaHooks: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: { platform: { type: Type.STRING }, text: { type: Type.STRING } },
|
||||
},
|
||||
},
|
||||
emailSubjectLines: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
viralHooks: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
},
|
||||
};
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: `Video Konsepti: "${strategy.title}". Bu videoyu viral yapmak için pazarlama stratejisi ve persona analizi üret.`,
|
||||
config: { responseMimeType: 'application/json', responseSchema: schema },
|
||||
});
|
||||
return JSON.parse(response.text || '{}');
|
||||
}
|
||||
|
||||
async generateDeepCommercialAnalysis(dto: CommercialAnalysisDto): Promise<any> {
|
||||
const schema: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
targetBrands: { type: Type.ARRAY, items: { type: Type.STRING } },
|
||||
emailDraft: { type: Type.STRING },
|
||||
estimatedRevenue: { type: Type.STRING },
|
||||
negotiationTip: { type: Type.STRING },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: `Video: "${dto.title}". Sektörler: ${dto.industries.join(
|
||||
', ',
|
||||
)}. Türkiye'den 5 gerçek marka seç ve her birine özel mail taslağı oluştur.`,
|
||||
config: {
|
||||
responseMimeType: 'application/json',
|
||||
responseSchema: schema,
|
||||
},
|
||||
});
|
||||
return JSON.parse(response.text || '{}');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Controller, Post, Body, Get, Param, UseGuards, HttpCode, HttpStatus, Req } from '@nestjs/common';
|
||||
import { YoutubeToolsService } from './youtube-tools.service';
|
||||
import { TubeStrategistService } from './tube-strategist.service';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/auth.guards';
|
||||
import { AnalyzeContentDto, CommercialAnalysisDto, StrategyResultDto } from './dto/tube-strategist.dto';
|
||||
|
||||
@ApiTags('youtube-tools')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('youtube-tools')
|
||||
export class YoutubeToolsController {
|
||||
constructor(private readonly youtubeToolsService: YoutubeToolsService) {}
|
||||
constructor(
|
||||
private readonly youtubeToolsService: YoutubeToolsService,
|
||||
private readonly tubeStrategistService: TubeStrategistService
|
||||
) {}
|
||||
|
||||
@Post('analyze')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -65,4 +70,43 @@ export class YoutubeToolsController {
|
||||
async generateSeoImage(@Body('prompt') prompt: string) {
|
||||
return this.youtubeToolsService.generateSeoImage(prompt);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// TUBE STRATEGIST ENDPOINTS
|
||||
// ==========================================
|
||||
|
||||
@Post('strategist/analyze')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Tube Strategist: Ana İçerik Stratejisi Analizi' })
|
||||
async strategistAnalyze(@Body() dto: AnalyzeContentDto) {
|
||||
return this.tubeStrategistService.analyzeContent(dto);
|
||||
}
|
||||
|
||||
@Post('strategist/seo')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Tube Strategist: SEO Raporu Üretimi' })
|
||||
async strategistSeo(@Body() dto: StrategyResultDto) {
|
||||
return this.tubeStrategistService.generateSeoReport(dto);
|
||||
}
|
||||
|
||||
@Post('strategist/neuro')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Tube Strategist: Nöro-Pazarlama Raporu Üretimi' })
|
||||
async strategistNeuro(@Body() dto: StrategyResultDto) {
|
||||
return this.tubeStrategistService.generateNeuroReport(dto);
|
||||
}
|
||||
|
||||
@Post('strategist/marketing')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Tube Strategist: Pazarlama & Viral Raporu Üretimi' })
|
||||
async strategistMarketing(@Body() dto: StrategyResultDto) {
|
||||
return this.tubeStrategistService.generateMarketingReport(dto);
|
||||
}
|
||||
|
||||
@Post('strategist/commercial')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Tube Strategist: Ticari Sponsorluk Taslağı Üretimi' })
|
||||
async strategistCommercial(@Body() dto: CommercialAnalysisDto) {
|
||||
return this.tubeStrategistService.generateDeepCommercialAnalysis(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { YoutubeToolsController } from './youtube-tools.controller';
|
||||
import { YoutubeToolsService } from './youtube-tools.service';
|
||||
import { TubeStrategistService } from './tube-strategist.service';
|
||||
import { GeminiModule } from '../gemini/gemini.module';
|
||||
|
||||
@Module({
|
||||
imports: [GeminiModule],
|
||||
controllers: [YoutubeToolsController],
|
||||
providers: [YoutubeToolsService],
|
||||
exports: [YoutubeToolsService],
|
||||
providers: [YoutubeToolsService, TubeStrategistService],
|
||||
exports: [YoutubeToolsService, TubeStrategistService],
|
||||
})
|
||||
export class YoutubeToolsModule {}
|
||||
|
||||
Reference in New Issue
Block a user