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

This commit is contained in:
Harun CAN
2026-03-14 14:01:01 +03:00
parent 0fefbe6859
commit dee6e29cfd
37 changed files with 1925 additions and 157 deletions

View File

@@ -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;
}
}