generated from fahricansecer/boilerplate-be
726 lines
30 KiB
TypeScript
726 lines
30 KiB
TypeScript
// Content Generation Service - Main orchestration
|
||
// Path: src/modules/content-generation/content-generation.service.ts
|
||
|
||
import { Injectable, Logger } from '@nestjs/common';
|
||
import { randomUUID } from 'crypto';
|
||
import { PrismaService } from '../../database/prisma.service';
|
||
import { NicheService, Niche, NicheAnalysis } from './services/niche.service';
|
||
import { DeepResearchService, ResearchResult, ResearchQuery } from './services/deep-research.service';
|
||
import { PlatformGeneratorService, Platform, GeneratedContent, MultiPlatformContent } from './services/platform-generator.service';
|
||
import { HashtagService, HashtagSet } from './services/hashtag.service';
|
||
import { BrandVoiceService, BrandVoice, VoiceApplication } from './services/brand-voice.service';
|
||
import { VariationService, VariationSet, VariationOptions } from './services/variation.service';
|
||
import { SeoService, FullSeoAnalysis as SeoDTO } from '../seo/seo.service';
|
||
import { NeuroMarketingService } from '../neuro-marketing/neuro-marketing.service';
|
||
import { StorageService } from '../visual-generation/services/storage.service';
|
||
import { VisualGenerationService } from '../visual-generation/visual-generation.service';
|
||
import { WebScraperService, ScrapedContent } from '../trends/services/web-scraper.service';
|
||
import { ContentType as PrismaContentType, ContentStatus as PrismaContentStatus, MasterContentType as PrismaMasterContentType } from '@prisma/client';
|
||
|
||
|
||
export interface ContentGenerationRequest {
|
||
topic: string;
|
||
sourceUrl?: string;
|
||
niche?: string;
|
||
platforms: Platform[];
|
||
includeResearch?: boolean;
|
||
includeHashtags?: boolean;
|
||
brandVoiceId?: string;
|
||
variationCount?: number;
|
||
writingStyle?: string;
|
||
ctaType?: string;
|
||
}
|
||
|
||
export interface SeoAnalysisResult {
|
||
score: number;
|
||
keywords: string[];
|
||
questions: string[];
|
||
longTail: { keyword: string; estimatedVolume: number; competitionLevel: string }[];
|
||
suggestions: string[];
|
||
meta: { title: string; description: string; };
|
||
}
|
||
|
||
export interface NeuroAnalysisResult {
|
||
score: number;
|
||
triggersUsed: string[];
|
||
emotionProfile: string[];
|
||
improvements: string[];
|
||
}
|
||
|
||
export interface GeneratedContentBundle {
|
||
id: string;
|
||
topic: string;
|
||
niche?: NicheAnalysis;
|
||
research?: ResearchResult;
|
||
platforms: GeneratedContent[];
|
||
variations: VariationSet[];
|
||
seo?: SeoAnalysisResult;
|
||
neuro?: NeuroAnalysisResult;
|
||
createdAt: Date;
|
||
}
|
||
|
||
@Injectable()
|
||
export class ContentGenerationService {
|
||
private readonly logger = new Logger(ContentGenerationService.name);
|
||
|
||
// Visual platforms that should get auto-generated images
|
||
private readonly VISUAL_PLATFORMS: Platform[] = ['instagram', 'twitter', 'medium'];
|
||
|
||
constructor(
|
||
private readonly prisma: PrismaService,
|
||
private readonly nicheService: NicheService,
|
||
private readonly researchService: DeepResearchService,
|
||
private readonly platformService: PlatformGeneratorService,
|
||
private readonly hashtagService: HashtagService,
|
||
private readonly brandVoiceService: BrandVoiceService,
|
||
private readonly variationService: VariationService,
|
||
private readonly seoService: SeoService,
|
||
private readonly neuroService: NeuroMarketingService,
|
||
private readonly storageService: StorageService,
|
||
private readonly visualService: VisualGenerationService,
|
||
private readonly webScraperService: WebScraperService,
|
||
) { }
|
||
|
||
|
||
// ========== FULL GENERATION WORKFLOW ==========
|
||
|
||
/**
|
||
* Complete content generation workflow
|
||
*/
|
||
async generateContent(request: ContentGenerationRequest): Promise<GeneratedContentBundle> {
|
||
const {
|
||
topic,
|
||
sourceUrl,
|
||
niche,
|
||
platforms,
|
||
includeResearch = true,
|
||
includeHashtags = true,
|
||
brandVoiceId,
|
||
variationCount = 3,
|
||
writingStyle,
|
||
ctaType,
|
||
} = request;
|
||
|
||
console.log(`[ContentGenerationService] Starting generation for topic: ${topic}, platforms: ${platforms.join(', ')}`);
|
||
|
||
// ========== STEP 1: Scrape source article if URL provided ==========
|
||
let scrapedSource: ScrapedContent | null = null;
|
||
if (sourceUrl) {
|
||
this.logger.log(`Scraping source article: ${sourceUrl}`);
|
||
try {
|
||
scrapedSource = await this.webScraperService.scrapeUrl(sourceUrl, {
|
||
extractImages: true,
|
||
extractLinks: true,
|
||
timeout: 15000,
|
||
}, topic);
|
||
if (scrapedSource) {
|
||
this.logger.log(`Scraped source: ${scrapedSource.wordCount} words, ${scrapedSource.images.length} images, ${scrapedSource.videoLinks.length} videos`);
|
||
} else {
|
||
this.logger.warn(`Failed to scrape source URL: ${sourceUrl}`);
|
||
}
|
||
} catch (err) {
|
||
this.logger.warn(`Source scraping error: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
// Analyze niche if provided
|
||
let nicheAnalysis: NicheAnalysis | undefined;
|
||
if (niche) {
|
||
nicheAnalysis = this.nicheService.analyzeNiche(niche) || undefined;
|
||
}
|
||
|
||
// Perform research if requested
|
||
let research: ResearchResult | undefined;
|
||
if (includeResearch) {
|
||
research = await this.researchService.research({
|
||
topic,
|
||
depth: 'standard',
|
||
includeStats: true,
|
||
includeQuotes: true,
|
||
});
|
||
}
|
||
|
||
// ========== Build enriched context from scraped source ==========
|
||
let sourceContext = '';
|
||
if (scrapedSource) {
|
||
const articleText = scrapedSource.content.substring(0, 3000);
|
||
const videoInfo = scrapedSource.videoLinks.length > 0
|
||
? `\nVİDEO LİNKLERİ: ${scrapedSource.videoLinks.join(', ')}`
|
||
: '';
|
||
const importantLinks = scrapedSource.links
|
||
.filter(l => l.isExternal && !l.href.includes('facebook') && !l.href.includes('twitter'))
|
||
.slice(0, 5)
|
||
.map(l => `${l.text}: ${l.href}`)
|
||
.join('\n');
|
||
const linkInfo = importantLinks ? `\nÖNEMLİ LİNKLER:\n${importantLinks}` : '';
|
||
|
||
sourceContext = `\n\n📰 KAYNAK MAKALE İÇERİĞİ (ZORUNLU REFERANS):\n${articleText}${videoInfo}${linkInfo}\n\n⚠️ ÖNEMLİ: Yukarıdaki kaynak makaledeki TÜM özneleri (kişi, ürün, oyun adları, tarihler, fiyatlar, markalar) habere dahil et. Hiçbir önemli bilgiyi atlama. Video linkleri ve önemli dış linkler varsa bunları da içerikte paylaş.`;
|
||
}
|
||
|
||
// Generate content for each platform using AI
|
||
const platformContent: GeneratedContent[] = [];
|
||
for (const platform of platforms) {
|
||
try {
|
||
this.logger.log(`Generating for platform: ${platform}`);
|
||
|
||
// Use AI generation when available
|
||
// Sanitize research summary to remove source names/URLs
|
||
const sanitizedSummary = this.sanitizeResearchSummary(
|
||
research?.summary || `Everything you need to know about ${topic}`
|
||
);
|
||
// Append scraped source context to give AI the full article details
|
||
const enrichedSummary = sanitizedSummary + sourceContext;
|
||
// Normalize platform to lowercase for consistency
|
||
const normalizedPlatform = platform.toLowerCase();
|
||
const aiContent = await this.platformService.generateAIContent(
|
||
topic,
|
||
enrichedSummary,
|
||
normalizedPlatform as any, // Cast to any/Platform to resolve type mismatch if Platform is strict union
|
||
'standard',
|
||
'tr',
|
||
writingStyle,
|
||
ctaType,
|
||
);
|
||
|
||
this.logger.log(`AI content length for ${platform}: ${aiContent?.length || 0}`);
|
||
|
||
if (!aiContent || aiContent.trim().length === 0) {
|
||
this.logger.warn(`AI Content is empty for ${platform}`);
|
||
}
|
||
|
||
// Use scraped image from source if available
|
||
const sourceImageUrl = scrapedSource?.images?.[0]?.src || undefined;
|
||
|
||
const config = this.platformService.getPlatformConfig(platform);
|
||
let content: GeneratedContent = {
|
||
platform,
|
||
format: 'AI Generated',
|
||
content: aiContent,
|
||
hashtags: [],
|
||
mediaRecommendations: [],
|
||
postingRecommendation: `Best times: ${config.bestPostingTimes.join(', ')}`,
|
||
characterCount: aiContent.length,
|
||
isWithinLimit: aiContent.length <= config.maxCharacters,
|
||
};
|
||
|
||
// Apply brand voice if specified
|
||
if (brandVoiceId) {
|
||
const voiceApplied = this.brandVoiceService.applyVoice(content.content, brandVoiceId);
|
||
content.content = voiceApplied.branded;
|
||
}
|
||
|
||
// Add hashtags using AI (based on actual generated content)
|
||
if (includeHashtags) {
|
||
try {
|
||
content.hashtags = await this.platformService.generateAIHashtags(
|
||
content.content,
|
||
topic,
|
||
platform as any,
|
||
'tr',
|
||
);
|
||
} catch (hashErr) {
|
||
this.logger.warn(`AI hashtag generation failed, skipping: ${hashErr.message}`);
|
||
content.hashtags = [];
|
||
}
|
||
}
|
||
|
||
// Generate image for visual platforms
|
||
if (this.VISUAL_PLATFORMS.includes(platform)) {
|
||
try {
|
||
this.logger.log(`Generating image for visual platform: ${platform}`);
|
||
const platformKey = platform === 'instagram' ? 'instagram_feed' : platform;
|
||
const imagePrompt = `${topic} - Professional, high-quality ${platform} visual. Detailed, engaging, and relevant to the topic: ${topic}.`;
|
||
const image = await this.visualService.generateImage({
|
||
prompt: imagePrompt,
|
||
platform: platformKey,
|
||
enhancePrompt: true,
|
||
});
|
||
|
||
// Check if image is a real image or just a placeholder
|
||
const isPlaceholder = image.url?.includes('placehold.co') || image.url?.includes('placeholder');
|
||
if (!isPlaceholder) {
|
||
content.imageUrl = image.url;
|
||
this.logger.log(`Image generated for ${platform}: ${image.url}`);
|
||
} else if (sourceImageUrl) {
|
||
// Use scraped source image instead of placeholder
|
||
content.imageUrl = sourceImageUrl;
|
||
this.logger.log(`Using scraped source image instead of placeholder: ${sourceImageUrl}`);
|
||
} else {
|
||
content.imageUrl = image.url;
|
||
this.logger.log(`Image generated for ${platform}: ${image.url} (placeholder, no source image available)`);
|
||
}
|
||
} catch (imgError) {
|
||
this.logger.warn(`Image generation failed for ${platform}, continuing without image`, imgError);
|
||
// Fallback to scraped source image
|
||
if (sourceImageUrl) {
|
||
content.imageUrl = sourceImageUrl;
|
||
this.logger.log(`Using scraped source image as fallback: ${sourceImageUrl}`);
|
||
}
|
||
}
|
||
} else if (sourceImageUrl && !content.imageUrl) {
|
||
// For non-visual platforms, still attach source image if available
|
||
content.imageUrl = sourceImageUrl;
|
||
}
|
||
|
||
platformContent.push(content);
|
||
} catch (error) {
|
||
this.logger.error(`Failed to generate for ${platform}`, error);
|
||
// Continue to next platform even if one fails
|
||
}
|
||
}
|
||
|
||
this.logger.log(`Generated content for ${platformContent.length} platforms`);
|
||
|
||
// Generate variations for primary platform
|
||
const variations: VariationSet[] = [];
|
||
if (variationCount > 0 && platformContent.length > 0) {
|
||
const primaryContent = platformContent[0].content;
|
||
const variationSet = this.variationService.generateVariations(primaryContent, {
|
||
count: variationCount,
|
||
variationType: 'complete',
|
||
language: 'tr',
|
||
});
|
||
variations.push(variationSet);
|
||
}
|
||
|
||
// SEO Analysis (Full)
|
||
let seoResult: SeoAnalysisResult | undefined;
|
||
if (platformContent.length > 0) {
|
||
try {
|
||
const primaryContent = platformContent[0].content;
|
||
const fullSeo = await this.seoService.analyzeFull(primaryContent, topic, { language: 'tr' });
|
||
const keywordTerms = fullSeo.keywords.related.map(k => k.term);
|
||
const questions = fullSeo.keywords.main.questions || [];
|
||
const longTail = fullSeo.keywords.longTail.map(lt => ({
|
||
keyword: lt.keyword,
|
||
estimatedVolume: lt.estimatedVolume,
|
||
competitionLevel: lt.competitionLevel,
|
||
}));
|
||
seoResult = {
|
||
score: fullSeo.content.score.overall,
|
||
keywords: [fullSeo.keywords.main.term, ...keywordTerms].slice(0, 15),
|
||
questions,
|
||
longTail: longTail.slice(0, 10),
|
||
suggestions: fullSeo.content.score.overall < 70 ? [
|
||
'Add more keyword density',
|
||
'Include long-tail keywords',
|
||
'Add meta description',
|
||
'Improve content structure with headings',
|
||
] : ['SEO is well optimized', 'Content structure is strong'],
|
||
meta: {
|
||
title: fullSeo.content.meta.title || `${topic} | Content Hunter`,
|
||
description: fullSeo.content.meta.description || research?.summary?.slice(0, 160) || `Learn about ${topic}`,
|
||
},
|
||
};
|
||
} catch (seoError) {
|
||
this.logger.warn(`Full SEO analysis failed, falling back to basic`, seoError);
|
||
const seoScore = this.seoService.quickScore(platformContent[0].content, topic);
|
||
const lsiKeywords = this.seoService.getLSIKeywords(topic, 10);
|
||
seoResult = {
|
||
score: seoScore,
|
||
keywords: lsiKeywords,
|
||
questions: [],
|
||
longTail: [],
|
||
suggestions: seoScore < 70 ? ['Add more keyword density', 'Include long-tail keywords'] : ['SEO is optimized'],
|
||
meta: {
|
||
title: `${topic} | Content Hunter`,
|
||
description: research?.summary?.slice(0, 160) || `Learn about ${topic}`,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
// Neuro Marketing Analysis
|
||
let neuroResult: NeuroAnalysisResult | undefined;
|
||
if (platformContent.length > 0) {
|
||
const primaryContent = platformContent[0].content;
|
||
const analysis = this.neuroService.analyze(primaryContent, platforms[0]);
|
||
neuroResult = {
|
||
score: analysis.prediction.overallScore,
|
||
triggersUsed: analysis.triggerAnalysis.used.map(t => t.name),
|
||
emotionProfile: Object.keys(analysis.prediction.categories).filter(
|
||
k => analysis.prediction.categories[k as keyof typeof analysis.prediction.categories] > 50
|
||
),
|
||
improvements: analysis.prediction.improvements,
|
||
};
|
||
}
|
||
|
||
return {
|
||
id: randomUUID(),
|
||
topic,
|
||
niche: nicheAnalysis,
|
||
research,
|
||
platforms: platformContent,
|
||
variations,
|
||
seo: seoResult,
|
||
neuro: neuroResult,
|
||
createdAt: new Date(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Persist generated content bundle to database
|
||
*/
|
||
async saveGeneratedBundle(userId: string | null, bundle: GeneratedContentBundle): Promise<{ masterContentId: string }> {
|
||
// If no userId, try to find or create a system anonymous user
|
||
let effectiveUserId = userId;
|
||
if (!effectiveUserId) {
|
||
try {
|
||
const anonUser = await this.prisma.user.findFirst({ where: { email: 'anonymous@contenthunter.system' } });
|
||
if (anonUser) {
|
||
effectiveUserId = anonUser.id;
|
||
} else {
|
||
const newAnon = await this.prisma.user.create({
|
||
data: {
|
||
email: 'anonymous@contenthunter.system',
|
||
password: 'system-anonymous-no-login',
|
||
firstName: 'Anonymous',
|
||
},
|
||
});
|
||
effectiveUserId = newAnon.id;
|
||
}
|
||
} catch (anonError) {
|
||
this.logger.warn(`Could not create anonymous user, content will not be saved: ${anonError.message}`);
|
||
return { masterContentId: 'not-saved' };
|
||
}
|
||
}
|
||
console.log(`[ContentGenerationService] Saving bundle for user ${effectiveUserId}, topic: ${bundle.topic}`);
|
||
try {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
// 1. Create DeepResearch if it exists in bundle
|
||
let researchId: string | undefined;
|
||
if (bundle.research) {
|
||
const research = await tx.deepResearch.create({
|
||
data: {
|
||
userId: effectiveUserId!,
|
||
topic: bundle.topic,
|
||
query: bundle.topic,
|
||
summary: bundle.research.summary,
|
||
sources: JSON.parse(JSON.stringify(bundle.research.sources)),
|
||
keyFindings: JSON.parse(JSON.stringify(bundle.research.keyFindings)),
|
||
status: 'completed',
|
||
completedAt: new Date(),
|
||
}
|
||
});
|
||
researchId = research.id;
|
||
}
|
||
|
||
// 2. Create MasterContent
|
||
const masterContent = await tx.masterContent.create({
|
||
data: {
|
||
userId: effectiveUserId!,
|
||
title: bundle.topic,
|
||
body: bundle.platforms[0]?.content || '',
|
||
type: PrismaMasterContentType.BLOG,
|
||
status: PrismaContentStatus.DRAFT,
|
||
researchId,
|
||
summary: bundle.research?.summary,
|
||
}
|
||
});
|
||
|
||
// 3. Create platform-specific content
|
||
for (const platformContent of bundle.platforms) {
|
||
// Map SocialPlatform/Platform to ContentType enum
|
||
let contentType: PrismaContentType = PrismaContentType.BLOG;
|
||
if (platformContent.platform === 'twitter') contentType = PrismaContentType.TWITTER;
|
||
else if (platformContent.platform === 'instagram') contentType = PrismaContentType.INSTAGRAM;
|
||
else if (platformContent.platform === 'linkedin') contentType = PrismaContentType.LINKEDIN;
|
||
else if (platformContent.platform === 'facebook') contentType = PrismaContentType.FACEBOOK;
|
||
else if (platformContent.platform === 'tiktok') contentType = PrismaContentType.TIKTOK;
|
||
|
||
const content = await tx.content.create({
|
||
data: {
|
||
userId: effectiveUserId!,
|
||
masterContentId: masterContent.id,
|
||
type: contentType,
|
||
title: this.sanitizeResearchSummary(`${bundle.topic}`) + ` - ${platformContent.platform}`,
|
||
body: platformContent.content,
|
||
hashtags: platformContent.hashtags,
|
||
status: PrismaContentStatus.DRAFT,
|
||
researchId,
|
||
}
|
||
});
|
||
|
||
// Save SEO data if available
|
||
if (bundle.seo) {
|
||
await tx.contentSeo.create({
|
||
data: {
|
||
contentId: content.id,
|
||
metaTitle: bundle.seo.meta.title,
|
||
metaDescription: bundle.seo.meta.description,
|
||
seoScore: bundle.seo.score,
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save Psychology data if available
|
||
if (bundle.neuro) {
|
||
await tx.contentPsychology.create({
|
||
data: {
|
||
contentId: content.id,
|
||
triggersUsed: bundle.neuro.triggersUsed,
|
||
engagementScore: bundle.neuro.score,
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
console.log(`[ContentGenerationService] Bundle saved successfully. MasterContentId: ${masterContent.id}`);
|
||
|
||
// Robust Verification
|
||
const savedCount = await tx.content.count({
|
||
where: { masterContentId: masterContent.id }
|
||
});
|
||
|
||
if (savedCount !== bundle.platforms.length) {
|
||
this.logger.error(`[CRITICAL] Save mismatch! Expected ${bundle.platforms.length} items, found ${savedCount}. MasterID: ${masterContent.id}`);
|
||
// Ensure we at least have the master content
|
||
}
|
||
|
||
return { masterContentId: masterContent.id };
|
||
});
|
||
} catch (error) {
|
||
console.error(`[ContentGenerationService] Failed to save bundle:`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
|
||
// ========== NICHE OPERATIONS ==========
|
||
|
||
getNiches(): Niche[] {
|
||
return this.nicheService.getAllNiches();
|
||
}
|
||
|
||
analyzeNiche(nicheId: string): NicheAnalysis | null {
|
||
return this.nicheService.analyzeNiche(nicheId);
|
||
}
|
||
|
||
recommendNiches(interests: string[]): Niche[] {
|
||
return this.nicheService.recommendNiches(interests);
|
||
}
|
||
|
||
getContentIdeas(nicheId: string, count?: number): string[] {
|
||
return this.nicheService.getContentIdeas(nicheId, count);
|
||
}
|
||
|
||
// ========== RESEARCH OPERATIONS ==========
|
||
|
||
async research(query: ResearchQuery): Promise<ResearchResult> {
|
||
return this.researchService.research(query);
|
||
}
|
||
|
||
async factCheck(claim: string) {
|
||
return this.researchService.factCheck(claim);
|
||
}
|
||
|
||
async getContentResearch(topic: string) {
|
||
return this.researchService.research({ topic, depth: 'standard', includeStats: true, includeQuotes: true });
|
||
}
|
||
|
||
// ========== PLATFORM OPERATIONS ==========
|
||
|
||
getPlatforms() {
|
||
return this.platformService.getAllPlatforms();
|
||
}
|
||
|
||
getPlatformConfig(platform: Platform) {
|
||
return this.platformService.getPlatformConfig(platform);
|
||
}
|
||
|
||
generateForPlatform(platform: Platform, input: { topic: string; mainMessage: string }) {
|
||
return this.platformService.generateForPlatform(platform, input);
|
||
}
|
||
|
||
generateMultiPlatform(input: { topic: string; mainMessage: string; platforms: Platform[] }) {
|
||
return this.platformService.generateMultiPlatform(input);
|
||
}
|
||
|
||
adaptContent(content: string, from: Platform, to: Platform) {
|
||
return this.platformService.adaptContent(content, from, to);
|
||
}
|
||
|
||
// ========== HASHTAG OPERATIONS ==========
|
||
|
||
generateHashtags(topic: string, platform: string): HashtagSet {
|
||
return this.hashtagService.generateHashtags(topic, platform);
|
||
}
|
||
|
||
analyzeHashtag(hashtag: string) {
|
||
return this.hashtagService.analyzeHashtag(hashtag);
|
||
}
|
||
|
||
getTrendingHashtags(category?: string) {
|
||
return this.hashtagService.getTrendingHashtags(category);
|
||
}
|
||
|
||
// ========== BRAND VOICE OPERATIONS ==========
|
||
|
||
createBrandVoice(input: Partial<BrandVoice> & { name: string }): BrandVoice {
|
||
return this.brandVoiceService.createBrandVoice(input);
|
||
}
|
||
|
||
getBrandVoice(id: string): BrandVoice | null {
|
||
return this.brandVoiceService.getBrandVoice(id);
|
||
}
|
||
|
||
listBrandVoicePresets() {
|
||
return this.brandVoiceService.listPresets();
|
||
}
|
||
|
||
applyBrandVoice(content: string, voiceId: string): VoiceApplication {
|
||
return this.brandVoiceService.applyVoice(content, voiceId);
|
||
}
|
||
|
||
generateVoicePrompt(voiceId: string): string {
|
||
return this.brandVoiceService.generateVoicePrompt(voiceId);
|
||
}
|
||
|
||
// ========== VARIATION OPERATIONS ==========
|
||
|
||
generateVariations(content: string, options?: VariationOptions): VariationSet {
|
||
return this.variationService.generateVariations(content, options);
|
||
}
|
||
|
||
createABTest(content: string) {
|
||
return this.variationService.createABTest(content);
|
||
}
|
||
|
||
// ========== NEURO REGENERATION ==========
|
||
|
||
async regenerateForNeuro(request: {
|
||
content: string;
|
||
platform: string;
|
||
currentScore: number;
|
||
improvements: string[];
|
||
}): Promise<{ content: string; score: number; improvements: string[] }> {
|
||
const { content, platform, currentScore, improvements } = request;
|
||
|
||
// Use platform service to regenerate with neuro optimization
|
||
const platformEnum = platform as Platform;
|
||
const improvementList = improvements.join('\n- ');
|
||
|
||
const neuroPrompt = `Sen nöro-pazarlama uzmanı bir sosyal medya içerik yazarısın.
|
||
|
||
MEVCUT İÇERİK:
|
||
${content}
|
||
|
||
MEVCUT NÖRO SKORU: ${currentScore}/100
|
||
|
||
İYİLEŞTİRME ÖNERİLERİ:
|
||
- ${improvementList}
|
||
|
||
GÖREV: Yukarıdaki içeriği nöro-pazarlama ilkeleri kullanarak yeniden yaz.
|
||
Mevcut mesajı koru ama psikolojik etkiyi artır.
|
||
|
||
KURALLAR:
|
||
1. Güçlü bir hook ile başla (merak, şok, soru)
|
||
2. Duygusal tetikleyiciler kullan (korku, heyecan, aidiyet)
|
||
3. Sosyal kanıt ekle
|
||
4. Aciliyet hissi yarat
|
||
5. Güçlü bir CTA ile bitir
|
||
6. Karakter limitini koru
|
||
7. Platformun tonuna uygun yaz
|
||
8. SADECE yayınlanacak metni yaz
|
||
9. Hiçbir haber sitesi, kaynak, ajans veya web sitesi adı kullanma
|
||
10. "...göre", "...haberlere göre", "...kaynağına göre" gibi atıf ifadeleri ASLA kullanma
|
||
|
||
SADECE yeniden yazılmış metni döndür, açıklama ekleme.`;
|
||
|
||
try {
|
||
const response = await this.platformService.generateAIContent(
|
||
neuroPrompt,
|
||
content,
|
||
platformEnum,
|
||
'standard',
|
||
'tr',
|
||
);
|
||
|
||
// Re-analyze with neuro service
|
||
const analysis = this.neuroService.analyze(response, platformEnum);
|
||
|
||
return {
|
||
content: response,
|
||
score: analysis.prediction.overallScore,
|
||
improvements: analysis.prediction.improvements,
|
||
};
|
||
} catch (error) {
|
||
this.logger.error(`Neuro regeneration failed: ${error.message}`);
|
||
return {
|
||
content,
|
||
score: currentScore,
|
||
improvements: ['Regeneration failed, try again'],
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Strip source names, URLs, and attribution phrases from research summary
|
||
* to prevent them from leaking into generated content.
|
||
*/
|
||
private sanitizeResearchSummary(summary: string): string {
|
||
let sanitized = summary;
|
||
|
||
// Remove URLs
|
||
sanitized = sanitized.replace(/https?:\/\/[^\s]+/gi, '');
|
||
sanitized = sanitized.replace(/www\.[^\s]+/gi, '');
|
||
|
||
// Remove common attribution phrases (Turkish and English)
|
||
const attributionPatterns = [
|
||
/\b\w+\.com(\.tr)?\b/gi,
|
||
/\b\w+\.org(\.tr)?\b/gi,
|
||
/\b\w+\.net(\.tr)?\b/gi,
|
||
/\bkaynağına göre\b/gi,
|
||
/\b'e göre\b/gi,
|
||
/\b'(i|a|e|u|ü|\u0131)n(da|de) (yayınlanan|yer alan|çıkan)\b/gi,
|
||
/\b(da|de) (çıkan|yayınlanan|yer alan) (haberlere|habere|bilgilere) göre\b/gi,
|
||
/\bhaberlere göre\b/gi,
|
||
/\braporuna göre\b/gi,
|
||
/\bsitesinde yer alan\b/gi,
|
||
/\baçıklamasına göre\b/gi,
|
||
/\byazısına göre\b/gi,
|
||
/\bhaberine göre\b/gi,
|
||
/\btarafından yapılan\b/gi,
|
||
/\baccording to [^,.]+/gi,
|
||
/\breported by [^,.]+/gi,
|
||
/\bas reported in [^,.]+/gi,
|
||
/\bsource:\s*[^,.]+/gi,
|
||
/\breferans:\s*[^,.]+/gi,
|
||
/\bkaynak:\s*[^,.]+/gi,
|
||
];
|
||
|
||
// Comprehensive list of Turkish tech/news source brands to strip
|
||
const sourceNames = [
|
||
'tamindir', 'donanımhaber', 'technopat', 'webtekno', 'shiftdelete',
|
||
'chip online', 'log.com', 'mediatrend', 'bbc', 'cnn',
|
||
'reuters', 'anadolu ajansı', 'hürriyet', 'milliyet',
|
||
'sabah', 'forbes', 'bloomberg', 'techcrunch',
|
||
'the verge', 'engadget', 'ars technica', 'wired',
|
||
'mashable', 'gizmodo', 'tom\'s hardware', 'tom\'s guide',
|
||
'ntv', 'habertürk', 'sozcu', 'sözcü', 'cumhuriyet', 'star',
|
||
'posta', 'aksam', 'yeni safak', 'yeni şafak', 'takvim',
|
||
'mynet', 'ensonhaber', 'haber7', 'internethaber',
|
||
'ad hoc news', 'finanzen.net', 'der aktionär', 'aktionar',
|
||
'business insider', 'cnbc', 'financial times', 'wall street journal',
|
||
];
|
||
|
||
for (const pattern of attributionPatterns) {
|
||
sanitized = sanitized.replace(pattern, '');
|
||
}
|
||
|
||
for (const source of sourceNames) {
|
||
const regex = new RegExp(`\\b${source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
|
||
sanitized = sanitized.replace(regex, '');
|
||
}
|
||
|
||
// Also remove "- site_name" patterns from titles (e.g. "Great News - Tamindir")
|
||
sanitized = sanitized.replace(/\s*-\s*$/gm, '');
|
||
|
||
// Clean up multiple spaces, trailing commas, and orphaned punctuation
|
||
sanitized = sanitized.replace(/\s{2,}/g, ' ').replace(/,\s*,/g, ',').replace(/\s+([.,;:!?])/g, '$1').trim();
|
||
|
||
return sanitized;
|
||
}
|
||
}
|