generated from fahricansecer/boilerplate-be
This commit is contained in:
@@ -13,6 +13,7 @@ import { VariationService, VariationSet, VariationOptions } from './services/var
|
||||
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 { ContentType as PrismaContentType, ContentStatus as PrismaContentStatus, MasterContentType as PrismaMasterContentType } from '@prisma/client';
|
||||
|
||||
|
||||
@@ -24,11 +25,15 @@ export interface ContentGenerationRequest {
|
||||
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; };
|
||||
}
|
||||
@@ -56,6 +61,9 @@ export interface GeneratedContentBundle {
|
||||
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,
|
||||
@@ -67,6 +75,7 @@ export class ContentGenerationService {
|
||||
private readonly seoService: SeoService,
|
||||
private readonly neuroService: NeuroMarketingService,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly visualService: VisualGenerationService,
|
||||
) { }
|
||||
|
||||
|
||||
@@ -84,7 +93,10 @@ export class ContentGenerationService {
|
||||
includeHashtags = true,
|
||||
brandVoiceId,
|
||||
variationCount = 3,
|
||||
writingStyle,
|
||||
ctaType,
|
||||
} = request;
|
||||
|
||||
console.log(`[ContentGenerationService] Starting generation for topic: ${topic}, platforms: ${platforms.join(', ')}`);
|
||||
|
||||
// Analyze niche if provided
|
||||
@@ -108,19 +120,29 @@ export class ContentGenerationService {
|
||||
const platformContent: GeneratedContent[] = [];
|
||||
for (const platform of platforms) {
|
||||
try {
|
||||
console.log(`[ContentGenerationService] Generating for platform: ${platform}`);
|
||||
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}`
|
||||
);
|
||||
// Normalize platform to lowercase for consistency
|
||||
const normalizedPlatform = platform.toLowerCase();
|
||||
const aiContent = await this.platformService.generateAIContent(
|
||||
topic,
|
||||
research?.summary || `Everything you need to know about ${topic}`,
|
||||
platform,
|
||||
sanitizedSummary,
|
||||
normalizedPlatform as any, // Cast to any/Platform to resolve type mismatch if Platform is strict union
|
||||
'standard',
|
||||
'tr',
|
||||
writingStyle,
|
||||
ctaType,
|
||||
);
|
||||
console.log(`[ContentGenerationService] AI content length for ${platform}: ${aiContent?.length || 0}`);
|
||||
|
||||
this.logger.log(`AI content length for ${platform}: ${aiContent?.length || 0}`);
|
||||
|
||||
if (!aiContent || aiContent.trim().length === 0) {
|
||||
console.warn(`[ContentGenerationService] AI Content is empty for ${platform}`);
|
||||
this.logger.warn(`AI Content is empty for ${platform}`);
|
||||
}
|
||||
|
||||
const config = this.platformService.getPlatformConfig(platform);
|
||||
@@ -147,15 +169,32 @@ export class ContentGenerationService {
|
||||
content.hashtags = hashtagSet.hashtags.map((h) => h.hashtag);
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
content.imageUrl = image.url;
|
||||
this.logger.log(`Image generated for ${platform}: ${image.url}`);
|
||||
} catch (imgError) {
|
||||
this.logger.warn(`Image generation failed for ${platform}, continuing without image`, imgError);
|
||||
}
|
||||
}
|
||||
|
||||
platformContent.push(content);
|
||||
console.log(`[ContentGenerationService] Successfully pushed content for ${platform}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to generate content for platform ${platform}: ${error.message}`);
|
||||
// Continue to next platform
|
||||
this.logger.error(`Failed to generate for ${platform}`, error);
|
||||
// Continue to next platform even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ContentGenerationService] Generated content for ${platformContent.length} platforms`);
|
||||
this.logger.log(`Generated content for ${platformContent.length} platforms`);
|
||||
|
||||
// Generate variations for primary platform
|
||||
const variations: VariationSet[] = [];
|
||||
@@ -164,29 +203,56 @@ export class ContentGenerationService {
|
||||
const variationSet = this.variationService.generateVariations(primaryContent, {
|
||||
count: variationCount,
|
||||
variationType: 'complete',
|
||||
language: 'tr',
|
||||
});
|
||||
variations.push(variationSet);
|
||||
}
|
||||
|
||||
// SEO Analysis
|
||||
// SEO Analysis (Full)
|
||||
let seoResult: SeoAnalysisResult | undefined;
|
||||
if (platformContent.length > 0) {
|
||||
const primaryContent = platformContent[0].content;
|
||||
const seoScore = this.seoService.quickScore(primaryContent, topic);
|
||||
const lsiKeywords = this.seoService.getLSIKeywords(topic, 10);
|
||||
seoResult = {
|
||||
score: seoScore,
|
||||
keywords: lsiKeywords,
|
||||
suggestions: seoScore < 70 ? [
|
||||
'Add more keyword density',
|
||||
'Include long-tail keywords',
|
||||
'Add meta description',
|
||||
] : ['SEO is optimized'],
|
||||
meta: {
|
||||
title: `${topic} | Content Hunter`,
|
||||
description: research?.summary?.slice(0, 160) || `Learn about ${topic}`,
|
||||
},
|
||||
};
|
||||
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
|
||||
@@ -220,8 +286,30 @@ export class ContentGenerationService {
|
||||
/**
|
||||
* Persist generated content bundle to database
|
||||
*/
|
||||
async saveGeneratedBundle(userId: string, bundle: GeneratedContentBundle): Promise<{ masterContentId: string }> {
|
||||
console.log(`[ContentGenerationService] Saving bundle for user ${userId}, topic: ${bundle.topic}`);
|
||||
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
|
||||
@@ -229,9 +317,9 @@ export class ContentGenerationService {
|
||||
if (bundle.research) {
|
||||
const research = await tx.deepResearch.create({
|
||||
data: {
|
||||
userId,
|
||||
userId: effectiveUserId!,
|
||||
topic: bundle.topic,
|
||||
query: bundle.topic, // Simplified for now
|
||||
query: bundle.topic,
|
||||
summary: bundle.research.summary,
|
||||
sources: JSON.parse(JSON.stringify(bundle.research.sources)),
|
||||
keyFindings: JSON.parse(JSON.stringify(bundle.research.keyFindings)),
|
||||
@@ -245,10 +333,10 @@ export class ContentGenerationService {
|
||||
// 2. Create MasterContent
|
||||
const masterContent = await tx.masterContent.create({
|
||||
data: {
|
||||
userId,
|
||||
userId: effectiveUserId!,
|
||||
title: bundle.topic,
|
||||
body: bundle.platforms[0]?.content || '', // Use first platform as master body for now
|
||||
type: PrismaMasterContentType.BLOG, // Default
|
||||
body: bundle.platforms[0]?.content || '',
|
||||
type: PrismaMasterContentType.BLOG,
|
||||
status: PrismaContentStatus.DRAFT,
|
||||
researchId,
|
||||
summary: bundle.research?.summary,
|
||||
@@ -267,7 +355,7 @@ export class ContentGenerationService {
|
||||
|
||||
const content = await tx.content.create({
|
||||
data: {
|
||||
userId,
|
||||
userId: effectiveUserId!,
|
||||
masterContentId: masterContent.id,
|
||||
type: contentType,
|
||||
title: `${bundle.topic} - ${platformContent.platform}`,
|
||||
@@ -302,7 +390,19 @@ export class ContentGenerationService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
@@ -411,4 +511,117 @@ export class ContentGenerationService {
|
||||
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
|
||||
|
||||
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 Turkish attribution phrases
|
||||
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,
|
||||
/\baccording to [^,.]+/gi,
|
||||
/\bsource:\s*[^,.]+/gi,
|
||||
/\breferans:\s*[^,.]+/gi,
|
||||
/\bkaynak:\s*[^,.]+/gi,
|
||||
];
|
||||
|
||||
// Common Turkish tech/news source brands to strip
|
||||
const sourceNames = [
|
||||
'donanımhaber', 'technopat', 'webtekno', 'shiftdelete',
|
||||
'chip online', 'log.com', 'mediatrend', 'bbc', 'cnn',
|
||||
'reuters', 'anadolu ajansı', 'hürriyet', 'milliyet',
|
||||
'sabah', 'forbes', 'bloomberg', 'techcrunch',
|
||||
];
|
||||
|
||||
for (const pattern of attributionPatterns) {
|
||||
sanitized = sanitized.replace(pattern, '');
|
||||
}
|
||||
|
||||
for (const source of sourceNames) {
|
||||
const regex = new RegExp(`\\b${source}\\b`, 'gi');
|
||||
sanitized = sanitized.replace(regex, '');
|
||||
}
|
||||
|
||||
// Clean up multiple spaces and trailing commas
|
||||
sanitized = sanitized.replace(/\s{2,}/g, ' ').replace(/,\s*,/g, ',').trim();
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user