generated from fahricansecer/boilerplate-be
This commit is contained in:
547
src/modules/content-generation/services/brand-voice.service.ts
Normal file
547
src/modules/content-generation/services/brand-voice.service.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
// Brand Voice Service - Brand voice training and application
|
||||
// Path: src/modules/content-generation/services/brand-voice.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface BrandVoice {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
personality: BrandPersonality;
|
||||
toneAttributes: ToneAttribute[];
|
||||
vocabulary: VocabularyRules;
|
||||
examples: BrandExample[];
|
||||
doList: string[];
|
||||
dontList: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface BrandPersonality {
|
||||
primary: PersonalityTrait;
|
||||
secondary: PersonalityTrait;
|
||||
tertiary?: PersonalityTrait;
|
||||
archetypes: string[];
|
||||
}
|
||||
|
||||
export type PersonalityTrait =
|
||||
| 'friendly'
|
||||
| 'professional'
|
||||
| 'playful'
|
||||
| 'authoritative'
|
||||
| 'empathetic'
|
||||
| 'innovative'
|
||||
| 'traditional'
|
||||
| 'bold'
|
||||
| 'calm'
|
||||
| 'energetic';
|
||||
|
||||
export interface ToneAttribute {
|
||||
attribute: string;
|
||||
level: number; // 1-10
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface VocabularyRules {
|
||||
preferredWords: string[];
|
||||
avoidWords: string[];
|
||||
industryTerms: string[];
|
||||
brandSpecificTerms: Record<string, string>; // term -> replacement
|
||||
emojiUsage: 'heavy' | 'moderate' | 'minimal' | 'none';
|
||||
formality: 'formal' | 'semi-formal' | 'casual' | 'very-casual';
|
||||
}
|
||||
|
||||
export interface BrandExample {
|
||||
type: 'good' | 'bad';
|
||||
original?: string;
|
||||
branded: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
export interface VoiceApplication {
|
||||
original: string;
|
||||
branded: string;
|
||||
changes: VoiceChange[];
|
||||
voiceScore: number;
|
||||
}
|
||||
|
||||
export interface VoiceChange {
|
||||
type: 'vocabulary' | 'tone' | 'structure' | 'emoji';
|
||||
before: string;
|
||||
after: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BrandVoiceService {
|
||||
private readonly logger = new Logger(BrandVoiceService.name);
|
||||
|
||||
// In-memory storage for demo
|
||||
private brandVoices: Map<string, BrandVoice> = new Map();
|
||||
|
||||
// Preset brand voice templates
|
||||
private readonly presets: Record<string, Partial<BrandVoice>> = {
|
||||
'startup-founder': {
|
||||
name: 'Startup Founder',
|
||||
description: 'Energetic, innovative, and transparent communication style',
|
||||
personality: {
|
||||
primary: 'bold',
|
||||
secondary: 'innovative',
|
||||
tertiary: 'friendly',
|
||||
archetypes: ['Innovator', 'Maverick', 'Pioneer'],
|
||||
},
|
||||
toneAttributes: [
|
||||
{ attribute: 'Confidence', level: 9, description: 'Speak with conviction' },
|
||||
{ attribute: 'Transparency', level: 8, description: 'Be open about challenges' },
|
||||
{ attribute: 'Excitement', level: 8, description: 'Show passion for the mission' },
|
||||
],
|
||||
vocabulary: {
|
||||
preferredWords: ['building', 'shipping', 'scaling', 'disrupting', 'journey', 'hustle'],
|
||||
avoidWords: ['corporate', 'synergy', 'leverage', 'circle back'],
|
||||
industryTerms: ['MVP', 'product-market fit', 'runway', 'growth hack'],
|
||||
brandSpecificTerms: { 'company': 'our team', 'customers': 'community' },
|
||||
emojiUsage: 'moderate',
|
||||
formality: 'casual',
|
||||
},
|
||||
doList: [
|
||||
'Share behind-the-scenes moments',
|
||||
'Admit mistakes openly',
|
||||
'Celebrate team wins',
|
||||
'Use "we" instead of "I"',
|
||||
],
|
||||
dontList: [
|
||||
'Sound corporate or stiff',
|
||||
'Overpromise',
|
||||
'Ignore feedback',
|
||||
'Use jargon without explanation',
|
||||
],
|
||||
},
|
||||
|
||||
'thought-leader': {
|
||||
name: 'Thought Leader',
|
||||
description: 'Authoritative yet approachable expert positioning',
|
||||
personality: {
|
||||
primary: 'authoritative',
|
||||
secondary: 'empathetic',
|
||||
tertiary: 'calm',
|
||||
archetypes: ['Sage', 'Teacher', 'Guide'],
|
||||
},
|
||||
toneAttributes: [
|
||||
{ attribute: 'Authority', level: 9, description: 'Speak from experience' },
|
||||
{ attribute: 'Wisdom', level: 8, description: 'Share insights, not just information' },
|
||||
{ attribute: 'Humility', level: 7, description: 'Credit sources, acknowledge limits' },
|
||||
],
|
||||
vocabulary: {
|
||||
preferredWords: ['insight', 'perspective', 'framework', 'principle', 'observation'],
|
||||
avoidWords: ['obviously', 'simply', 'just', 'everyone knows'],
|
||||
industryTerms: [],
|
||||
brandSpecificTerms: {},
|
||||
emojiUsage: 'minimal',
|
||||
formality: 'semi-formal',
|
||||
},
|
||||
doList: [
|
||||
'Share original frameworks',
|
||||
'Reference data and research',
|
||||
'Tell stories with lessons',
|
||||
'Ask thought-provoking questions',
|
||||
],
|
||||
dontList: [
|
||||
'Be preachy or condescending',
|
||||
'Claim to have all answers',
|
||||
'Dismiss other perspectives',
|
||||
'Overuse "I"',
|
||||
],
|
||||
},
|
||||
|
||||
'friendly-expert': {
|
||||
name: 'Friendly Expert',
|
||||
description: 'Warm, helpful, and knowledgeable without being intimidating',
|
||||
personality: {
|
||||
primary: 'friendly',
|
||||
secondary: 'professional',
|
||||
tertiary: 'empathetic',
|
||||
archetypes: ['Guide', 'Helper', 'Friend'],
|
||||
},
|
||||
toneAttributes: [
|
||||
{ attribute: 'Warmth', level: 9, description: 'Make people feel welcome' },
|
||||
{ attribute: 'Helpfulness', level: 9, description: 'Always provide value' },
|
||||
{ attribute: 'Clarity', level: 8, description: 'Explain simply' },
|
||||
],
|
||||
vocabulary: {
|
||||
preferredWords: ['let\'s', 'together', 'you', 'try', 'discover', 'learn'],
|
||||
avoidWords: ['actually', 'basically', 'obviously', 'you should'],
|
||||
industryTerms: [],
|
||||
brandSpecificTerms: {},
|
||||
emojiUsage: 'moderate',
|
||||
formality: 'casual',
|
||||
},
|
||||
doList: [
|
||||
'Use "you" frequently',
|
||||
'Acknowledge struggles',
|
||||
'Celebrate wins, even small ones',
|
||||
'End with encouragement',
|
||||
],
|
||||
dontList: [
|
||||
'Talk down to audience',
|
||||
'Use complex jargon',
|
||||
'Be negative or discouraging',
|
||||
'Ignore questions',
|
||||
],
|
||||
},
|
||||
|
||||
'bold-provocateur': {
|
||||
name: 'Bold Provocateur',
|
||||
description: 'Challenging conventional wisdom with strong opinions',
|
||||
personality: {
|
||||
primary: 'bold',
|
||||
secondary: 'energetic',
|
||||
tertiary: 'innovative',
|
||||
archetypes: ['Rebel', 'Provocateur', 'Truth-teller'],
|
||||
},
|
||||
toneAttributes: [
|
||||
{ attribute: 'Boldness', level: 10, description: 'Don\'t hold back' },
|
||||
{ attribute: 'Controversy', level: 7, description: 'Challenge status quo' },
|
||||
{ attribute: 'Conviction', level: 9, description: 'Stand by your views' },
|
||||
],
|
||||
vocabulary: {
|
||||
preferredWords: ['truth', 'reality', 'myth', 'wake up', 'stop', 'wrong'],
|
||||
avoidWords: ['maybe', 'perhaps', 'I think', 'in my humble opinion'],
|
||||
industryTerms: [],
|
||||
brandSpecificTerms: {},
|
||||
emojiUsage: 'minimal',
|
||||
formality: 'casual',
|
||||
},
|
||||
doList: [
|
||||
'Take strong positions',
|
||||
'Back claims with evidence',
|
||||
'Name problems directly',
|
||||
'Offer real solutions',
|
||||
],
|
||||
dontList: [
|
||||
'Be mean-spirited',
|
||||
'Attack individuals',
|
||||
'Claim controversy for its own sake',
|
||||
'Refuse to engage with disagreement',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a custom brand voice
|
||||
*/
|
||||
createBrandVoice(input: Partial<BrandVoice> & { name: string }): BrandVoice {
|
||||
const brandVoice: BrandVoice = {
|
||||
id: `voice-${Date.now()}`,
|
||||
name: input.name,
|
||||
description: input.description || '',
|
||||
personality: input.personality || {
|
||||
primary: 'friendly',
|
||||
secondary: 'professional',
|
||||
archetypes: [],
|
||||
},
|
||||
toneAttributes: input.toneAttributes || [],
|
||||
vocabulary: input.vocabulary || {
|
||||
preferredWords: [],
|
||||
avoidWords: [],
|
||||
industryTerms: [],
|
||||
brandSpecificTerms: {},
|
||||
emojiUsage: 'moderate',
|
||||
formality: 'semi-formal',
|
||||
},
|
||||
examples: input.examples || [],
|
||||
doList: input.doList || [],
|
||||
dontList: input.dontList || [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.brandVoices.set(brandVoice.id, brandVoice);
|
||||
return brandVoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brand voice by ID
|
||||
*/
|
||||
getBrandVoice(id: string): BrandVoice | null {
|
||||
return this.brandVoices.get(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preset brand voice
|
||||
*/
|
||||
getPreset(presetName: string): Partial<BrandVoice> | null {
|
||||
return this.presets[presetName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all presets
|
||||
*/
|
||||
listPresets(): { name: string; description: string }[] {
|
||||
return Object.entries(this.presets).map(([key, preset]) => ({
|
||||
name: key,
|
||||
description: preset.description || '',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply brand voice to content
|
||||
*/
|
||||
applyVoice(content: string, voiceId: string): VoiceApplication {
|
||||
const voice = this.brandVoices.get(voiceId);
|
||||
if (!voice) {
|
||||
return {
|
||||
original: content,
|
||||
branded: content,
|
||||
changes: [],
|
||||
voiceScore: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let branded = content;
|
||||
const changes: VoiceChange[] = [];
|
||||
|
||||
// Apply vocabulary substitutions
|
||||
for (const [term, replacement] of Object.entries(voice.vocabulary.brandSpecificTerms)) {
|
||||
if (branded.toLowerCase().includes(term.toLowerCase())) {
|
||||
const regex = new RegExp(term, 'gi');
|
||||
branded = branded.replace(regex, replacement);
|
||||
changes.push({
|
||||
type: 'vocabulary',
|
||||
before: term,
|
||||
after: replacement,
|
||||
reason: 'Brand-specific terminology',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove avoided words
|
||||
for (const avoidWord of voice.vocabulary.avoidWords) {
|
||||
if (branded.toLowerCase().includes(avoidWord.toLowerCase())) {
|
||||
const regex = new RegExp(`\\b${avoidWord}\\b`, 'gi');
|
||||
branded = branded.replace(regex, '');
|
||||
changes.push({
|
||||
type: 'vocabulary',
|
||||
before: avoidWord,
|
||||
after: '[removed]',
|
||||
reason: 'Avoid word per brand guidelines',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust formality
|
||||
branded = this.adjustFormality(branded, voice.vocabulary.formality, changes);
|
||||
|
||||
// Handle emoji usage
|
||||
branded = this.adjustEmojis(branded, voice.vocabulary.emojiUsage, changes);
|
||||
|
||||
// Calculate voice score
|
||||
const voiceScore = this.calculateVoiceScore(branded, voice);
|
||||
|
||||
return {
|
||||
original: content,
|
||||
branded: branded.replace(/\s+/g, ' ').trim(),
|
||||
changes,
|
||||
voiceScore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze content against brand voice
|
||||
*/
|
||||
analyzeAgainstVoice(content: string, voiceId: string): {
|
||||
overallScore: number;
|
||||
vocabularyScore: number;
|
||||
toneScore: number;
|
||||
suggestions: string[];
|
||||
} {
|
||||
const voice = this.brandVoices.get(voiceId);
|
||||
if (!voice) {
|
||||
return { overallScore: 0, vocabularyScore: 0, toneScore: 0, suggestions: [] };
|
||||
}
|
||||
|
||||
const vocabularyScore = this.calculateVocabularyScore(content, voice);
|
||||
const toneScore = this.calculateToneScore(content, voice);
|
||||
const overallScore = Math.round((vocabularyScore + toneScore) / 2);
|
||||
|
||||
const suggestions = this.generateSuggestions(content, voice);
|
||||
|
||||
return {
|
||||
overallScore,
|
||||
vocabularyScore,
|
||||
toneScore,
|
||||
suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI prompt for brand voice
|
||||
*/
|
||||
generateVoicePrompt(voiceId: string): string {
|
||||
const voice = this.brandVoices.get(voiceId);
|
||||
if (!voice) return '';
|
||||
|
||||
return `
|
||||
Write in the following brand voice:
|
||||
|
||||
PERSONALITY: ${voice.personality.primary}, ${voice.personality.secondary}
|
||||
ARCHETYPES: ${voice.personality.archetypes.join(', ')}
|
||||
FORMALITY: ${voice.vocabulary.formality}
|
||||
EMOJI USAGE: ${voice.vocabulary.emojiUsage}
|
||||
|
||||
TONE ATTRIBUTES:
|
||||
${voice.toneAttributes.map((t) => `- ${t.attribute} (${t.level}/10): ${t.description}`).join('\n')}
|
||||
|
||||
VOCABULARY DO'S:
|
||||
${voice.vocabulary.preferredWords.join(', ')}
|
||||
|
||||
VOCABULARY DON'TS:
|
||||
${voice.vocabulary.avoidWords.join(', ')}
|
||||
|
||||
DO:
|
||||
${voice.doList.map((d) => `- ${d}`).join('\n')}
|
||||
|
||||
DON'T:
|
||||
${voice.dontList.map((d) => `- ${d}`).join('\n')}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
private adjustFormality(
|
||||
content: string,
|
||||
formality: string,
|
||||
changes: VoiceChange[],
|
||||
): string {
|
||||
let adjusted = content;
|
||||
|
||||
if (formality === 'casual' || formality === 'very-casual') {
|
||||
// Add contractions
|
||||
const contractions: Record<string, string> = {
|
||||
'do not': "don't",
|
||||
'cannot': "can't",
|
||||
'will not': "won't",
|
||||
'is not': "isn't",
|
||||
'are not': "aren't",
|
||||
'I am': "I'm",
|
||||
'you are': "you're",
|
||||
'we are': "we're",
|
||||
'it is': "it's",
|
||||
};
|
||||
|
||||
for (const [formal, informal] of Object.entries(contractions)) {
|
||||
const regex = new RegExp(formal, 'gi');
|
||||
if (adjusted.match(regex)) {
|
||||
adjusted = adjusted.replace(regex, informal);
|
||||
changes.push({
|
||||
type: 'tone',
|
||||
before: formal,
|
||||
after: informal,
|
||||
reason: 'Casual tone adjustment',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
private adjustEmojis(
|
||||
content: string,
|
||||
emojiUsage: string,
|
||||
changes: VoiceChange[],
|
||||
): string {
|
||||
if (emojiUsage === 'none') {
|
||||
const withoutEmojis = content.replace(/[\u{1F300}-\u{1F9FF}]/gu, '');
|
||||
if (withoutEmojis !== content) {
|
||||
changes.push({
|
||||
type: 'emoji',
|
||||
before: 'emojis present',
|
||||
after: 'emojis removed',
|
||||
reason: 'Brand voice prohibits emojis',
|
||||
});
|
||||
}
|
||||
return withoutEmojis;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private calculateVoiceScore(content: string, voice: BrandVoice): number {
|
||||
let score = 60;
|
||||
|
||||
// Check preferred words
|
||||
const hasPreferred = voice.vocabulary.preferredWords.some((w) =>
|
||||
content.toLowerCase().includes(w.toLowerCase())
|
||||
);
|
||||
if (hasPreferred) score += 15;
|
||||
|
||||
// Check avoided words
|
||||
const hasAvoided = voice.vocabulary.avoidWords.some((w) =>
|
||||
content.toLowerCase().includes(w.toLowerCase())
|
||||
);
|
||||
if (hasAvoided) score -= 15;
|
||||
|
||||
return Math.min(100, Math.max(0, score));
|
||||
}
|
||||
|
||||
private calculateVocabularyScore(content: string, voice: BrandVoice): number {
|
||||
let score = 50;
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
// Preferred words bonus
|
||||
voice.vocabulary.preferredWords.forEach((word) => {
|
||||
if (contentLower.includes(word.toLowerCase())) score += 5;
|
||||
});
|
||||
|
||||
// Avoided words penalty
|
||||
voice.vocabulary.avoidWords.forEach((word) => {
|
||||
if (contentLower.includes(word.toLowerCase())) score -= 10;
|
||||
});
|
||||
|
||||
return Math.min(100, Math.max(0, score));
|
||||
}
|
||||
|
||||
private calculateToneScore(content: string, voice: BrandVoice): number {
|
||||
let score = 50;
|
||||
|
||||
// Simple heuristics for tone
|
||||
const hasExclamation = content.includes('!');
|
||||
const hasQuestion = content.includes('?');
|
||||
const wordCount = content.split(/\s+/).length;
|
||||
const avgWordLength = content.replace(/\s+/g, '').length / wordCount;
|
||||
|
||||
// Adjust based on personality
|
||||
if (voice.personality.primary === 'energetic' && hasExclamation) score += 10;
|
||||
if (voice.personality.primary === 'calm' && !hasExclamation) score += 10;
|
||||
if (voice.personality.secondary === 'empathetic' && hasQuestion) score += 10;
|
||||
|
||||
return Math.min(100, Math.max(0, score));
|
||||
}
|
||||
|
||||
private generateSuggestions(content: string, voice: BrandVoice): string[] {
|
||||
const suggestions: string[] = [];
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
// Check for avoided words
|
||||
voice.vocabulary.avoidWords.forEach((word) => {
|
||||
if (contentLower.includes(word.toLowerCase())) {
|
||||
suggestions.push(`Consider removing or replacing "${word}"`);
|
||||
}
|
||||
});
|
||||
|
||||
// Suggest preferred words if missing
|
||||
const hasAnyPreferred = voice.vocabulary.preferredWords.some((w) =>
|
||||
contentLower.includes(w.toLowerCase())
|
||||
);
|
||||
if (!hasAnyPreferred && voice.vocabulary.preferredWords.length > 0) {
|
||||
suggestions.push(`Try incorporating brand words like: ${voice.vocabulary.preferredWords.slice(0, 3).join(', ')}`);
|
||||
}
|
||||
|
||||
// General suggestions from do's list
|
||||
if (voice.doList.length > 0 && suggestions.length < 3) {
|
||||
suggestions.push(`Remember: ${voice.doList[0]}`);
|
||||
}
|
||||
|
||||
return suggestions.slice(0, 5);
|
||||
}
|
||||
}
|
||||
301
src/modules/content-generation/services/deep-research.service.ts
Normal file
301
src/modules/content-generation/services/deep-research.service.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
// Deep Research Service - Real implementation using Gemini AI
|
||||
// Path: src/modules/content-generation/services/deep-research.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { GeminiService } from '../../gemini/gemini.service';
|
||||
|
||||
export interface ResearchQuery {
|
||||
topic: string;
|
||||
depth: 'quick' | 'standard' | 'deep';
|
||||
sources?: SourceType[];
|
||||
maxSources?: number;
|
||||
includeStats?: boolean;
|
||||
includeQuotes?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export type SourceType =
|
||||
| 'academic'
|
||||
| 'news'
|
||||
| 'industry'
|
||||
| 'social'
|
||||
| 'government'
|
||||
| 'blog';
|
||||
|
||||
export interface ResearchResult {
|
||||
query: string;
|
||||
summary: string;
|
||||
keyFindings: KeyFinding[];
|
||||
statistics: Statistic[];
|
||||
quotes: Quote[];
|
||||
sources: Source[];
|
||||
relatedTopics: string[];
|
||||
contentAngles: string[];
|
||||
generatedAt: Date;
|
||||
}
|
||||
|
||||
export interface KeyFinding {
|
||||
finding: string;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
value: string;
|
||||
context: string;
|
||||
sourceId: string;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
text: string;
|
||||
author: string;
|
||||
role?: string;
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
id: string;
|
||||
type: SourceType;
|
||||
title: string;
|
||||
url: string;
|
||||
author?: string;
|
||||
publishedDate?: string;
|
||||
credibilityScore: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DeepResearchService {
|
||||
private readonly logger = new Logger(DeepResearchService.name);
|
||||
|
||||
constructor(private readonly gemini: GeminiService) { }
|
||||
|
||||
/**
|
||||
* Perform deep research on a topic using Gemini AI
|
||||
*/
|
||||
async research(query: ResearchQuery): Promise<ResearchResult> {
|
||||
const { topic, depth, includeStats = true, includeQuotes = true, language = 'tr' } = query;
|
||||
|
||||
this.logger.log(`Performing ${depth} research on: ${topic}`);
|
||||
|
||||
if (!this.gemini.isAvailable()) {
|
||||
this.logger.warn('Gemini not available, returning basic research');
|
||||
return this.getFallbackResearch(topic);
|
||||
}
|
||||
|
||||
try {
|
||||
const researchPrompt = this.buildResearchPrompt(topic, depth, includeStats, includeQuotes, language);
|
||||
|
||||
const schema = `{
|
||||
"summary": "string - comprehensive summary of the topic (2-3 paragraphs)",
|
||||
"keyFindings": [{ "finding": "string", "confidence": "high|medium|low" }],
|
||||
"statistics": [{ "value": "string", "context": "string", "year": number }],
|
||||
"quotes": [{ "text": "string", "author": "string", "role": "string" }],
|
||||
"relatedTopics": ["string"],
|
||||
"contentAngles": ["string - unique content angle ideas"],
|
||||
"sources": [{ "title": "string", "url": "string", "type": "news|industry|blog|academic" }]
|
||||
}`;
|
||||
|
||||
const response = await this.gemini.generateJSON<any>(researchPrompt, schema, {
|
||||
temperature: 0.7,
|
||||
maxTokens: 4000,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
query: topic,
|
||||
summary: data.summary || `Research summary for ${topic}`,
|
||||
keyFindings: (data.keyFindings || []).map((f: any, i: number) => ({
|
||||
finding: f.finding,
|
||||
confidence: f.confidence || 'medium',
|
||||
sourceId: `src-${i}`,
|
||||
})),
|
||||
statistics: (data.statistics || []).map((s: any, i: number) => ({
|
||||
value: s.value,
|
||||
context: s.context,
|
||||
sourceId: `src-${i}`,
|
||||
year: s.year,
|
||||
})),
|
||||
quotes: (data.quotes || []).map((q: any, i: number) => ({
|
||||
text: q.text,
|
||||
author: q.author,
|
||||
role: q.role,
|
||||
sourceId: `src-${i}`,
|
||||
})),
|
||||
sources: (data.sources || []).map((s: any, i: number) => ({
|
||||
id: `src-${i}`,
|
||||
type: s.type || 'news',
|
||||
title: s.title,
|
||||
url: s.url || `https://google.com/search?q=${encodeURIComponent(topic)}`,
|
||||
credibilityScore: 80,
|
||||
})),
|
||||
relatedTopics: data.relatedTopics || [],
|
||||
contentAngles: data.contentAngles || [],
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Research failed: ${error.message}`);
|
||||
return this.getFallbackResearch(topic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the research prompt based on depth
|
||||
*/
|
||||
private buildResearchPrompt(
|
||||
topic: string,
|
||||
depth: string,
|
||||
includeStats: boolean,
|
||||
includeQuotes: boolean,
|
||||
language: string
|
||||
): string {
|
||||
const depthInstructions = {
|
||||
quick: 'Provide a brief overview with 3 key findings.',
|
||||
standard: 'Provide detailed research with 5-7 key findings, statistics, and expert quotes.',
|
||||
deep: 'Provide comprehensive research with 10+ key findings, extensive statistics, multiple expert quotes, and diverse content angles.',
|
||||
};
|
||||
|
||||
return `You are a professional research analyst. Research the following topic thoroughly and provide accurate, up-to-date information.
|
||||
|
||||
TOPIC: ${topic}
|
||||
|
||||
DEPTH: ${depthInstructions[depth] || depthInstructions.standard}
|
||||
|
||||
REQUIREMENTS:
|
||||
- Provide a comprehensive summary (2-3 paragraphs)
|
||||
- List key findings with confidence levels
|
||||
${includeStats ? '- Include relevant statistics with context and year' : ''}
|
||||
${includeQuotes ? '- Include quotes from industry experts or thought leaders' : ''}
|
||||
- Suggest related topics for further exploration
|
||||
- Provide unique content angles for creating engaging content
|
||||
- List credible sources (real URLs when possible)
|
||||
|
||||
LANGUAGE: Respond in ${language === 'tr' ? 'Turkish' : 'English'}
|
||||
|
||||
Be factual, avoid speculation, and cite sources where possible.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback when Gemini is not available
|
||||
*/
|
||||
private getFallbackResearch(topic: string): ResearchResult {
|
||||
return {
|
||||
query: topic,
|
||||
summary: `Research on "${topic}" requires AI service. Please ensure Gemini API is configured.`,
|
||||
keyFindings: [{
|
||||
finding: 'AI research service is currently unavailable',
|
||||
confidence: 'low',
|
||||
sourceId: 'fallback',
|
||||
}],
|
||||
statistics: [],
|
||||
quotes: [],
|
||||
sources: [{
|
||||
id: 'fallback',
|
||||
type: 'news',
|
||||
title: `Google Search: ${topic}`,
|
||||
url: `https://google.com/search?q=${encodeURIComponent(topic)}`,
|
||||
credibilityScore: 50,
|
||||
}],
|
||||
relatedTopics: [],
|
||||
contentAngles: [],
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick fact check using Gemini
|
||||
*/
|
||||
async factCheck(claim: string): Promise<{
|
||||
claim: string;
|
||||
isAccurate: boolean;
|
||||
confidence: number;
|
||||
explanation: string;
|
||||
corrections?: string[];
|
||||
}> {
|
||||
if (!this.gemini.isAvailable()) {
|
||||
return {
|
||||
claim,
|
||||
isAccurate: false,
|
||||
confidence: 0,
|
||||
explanation: 'Fact-checking service unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
const prompt = `Fact check this claim: "${claim}"
|
||||
|
||||
Respond with:
|
||||
1. Is it accurate? (true/false)
|
||||
2. Confidence level (0-100)
|
||||
3. Brief explanation
|
||||
4. Any corrections needed`;
|
||||
|
||||
const schema = `{
|
||||
"isAccurate": boolean,
|
||||
"confidence": number,
|
||||
"explanation": "string",
|
||||
"corrections": ["string"]
|
||||
}`;
|
||||
|
||||
try {
|
||||
const response = await this.gemini.generateJSON<any>(prompt, schema);
|
||||
return {
|
||||
claim,
|
||||
isAccurate: response.data.isAccurate,
|
||||
confidence: response.data.confidence,
|
||||
explanation: response.data.explanation,
|
||||
corrections: response.data.corrections,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Fact check failed: ${error.message}`);
|
||||
return {
|
||||
claim,
|
||||
isAccurate: false,
|
||||
confidence: 0,
|
||||
explanation: 'Unable to verify claim',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze competitors/sources for a topic
|
||||
*/
|
||||
async analyzeCompetitors(topic: string, niche?: string): Promise<{
|
||||
topCreators: { name: string; platform: string; approach: string }[];
|
||||
contentGaps: string[];
|
||||
opportunities: string[];
|
||||
}> {
|
||||
if (!this.gemini.isAvailable()) {
|
||||
return {
|
||||
topCreators: [],
|
||||
contentGaps: ['Enable Gemini for competitor analysis'],
|
||||
opportunities: [],
|
||||
};
|
||||
}
|
||||
|
||||
const prompt = `Analyze the content landscape for "${topic}"${niche ? ` in the ${niche} niche` : ''}.
|
||||
|
||||
Identify:
|
||||
1. Top content creators covering this topic (with their platform and approach)
|
||||
2. Content gaps that are underserved
|
||||
3. Opportunities for unique content`;
|
||||
|
||||
const schema = `{
|
||||
"topCreators": [{ "name": "string", "platform": "string", "approach": "string" }],
|
||||
"contentGaps": ["string"],
|
||||
"opportunities": ["string"]
|
||||
}`;
|
||||
|
||||
try {
|
||||
const response = await this.gemini.generateJSON<any>(prompt, schema);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Competitor analysis failed: ${error.message}`);
|
||||
return {
|
||||
topCreators: [],
|
||||
contentGaps: [],
|
||||
opportunities: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
276
src/modules/content-generation/services/hashtag.service.ts
Normal file
276
src/modules/content-generation/services/hashtag.service.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
// Hashtag Service - Intelligent hashtag generation and management
|
||||
// Path: src/modules/content-generation/services/hashtag.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface HashtagSuggestion {
|
||||
hashtag: string;
|
||||
type: HashtagType;
|
||||
popularity: 'low' | 'medium' | 'high' | 'trending';
|
||||
competition: 'low' | 'medium' | 'high';
|
||||
reachPotential: number; // 1-100
|
||||
recommended: boolean;
|
||||
}
|
||||
|
||||
export type HashtagType =
|
||||
| 'niche'
|
||||
| 'trending'
|
||||
| 'community'
|
||||
| 'branded'
|
||||
| 'location'
|
||||
| 'broad'
|
||||
| 'long_tail';
|
||||
|
||||
export interface HashtagSet {
|
||||
topic: string;
|
||||
platform: string;
|
||||
hashtags: HashtagSuggestion[];
|
||||
strategy: string;
|
||||
recommendedCount: number;
|
||||
}
|
||||
|
||||
export interface HashtagAnalysis {
|
||||
hashtag: string;
|
||||
totalPosts: number;
|
||||
avgEngagement: number;
|
||||
topRelated: string[];
|
||||
bestTimeToUse: string;
|
||||
overused: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HashtagService {
|
||||
private readonly logger = new Logger(HashtagService.name);
|
||||
|
||||
// Platform-specific hashtag limits
|
||||
private readonly platformLimits: Record<string, number> = {
|
||||
twitter: 3,
|
||||
instagram: 30,
|
||||
linkedin: 5,
|
||||
tiktok: 5,
|
||||
facebook: 3,
|
||||
youtube: 15,
|
||||
threads: 0,
|
||||
};
|
||||
|
||||
// Popular hashtag database (mock)
|
||||
private readonly hashtagDatabase: Record<string, { posts: number; engagement: number }> = {
|
||||
'contentcreator': { posts: 15000000, engagement: 2.5 },
|
||||
'entrepreneur': { posts: 25000000, engagement: 2.1 },
|
||||
'marketing': { posts: 20000000, engagement: 1.8 },
|
||||
'business': { posts: 30000000, engagement: 1.5 },
|
||||
'motivation': { posts: 35000000, engagement: 3.2 },
|
||||
'productivity': { posts: 8000000, engagement: 2.8 },
|
||||
'ai': { posts: 5000000, engagement: 4.5 },
|
||||
'growthhacking': { posts: 2000000, engagement: 3.1 },
|
||||
'personalbranding': { posts: 3000000, engagement: 3.4 },
|
||||
'socialmedia': { posts: 12000000, engagement: 2.0 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate hashtags for a topic
|
||||
*/
|
||||
generateHashtags(
|
||||
topic: string,
|
||||
platform: string,
|
||||
options?: {
|
||||
count?: number;
|
||||
includeNiche?: boolean;
|
||||
includeTrending?: boolean;
|
||||
includeBranded?: boolean;
|
||||
},
|
||||
): HashtagSet {
|
||||
const maxCount = this.platformLimits[platform] || 10;
|
||||
const count = Math.min(options?.count || maxCount, maxCount);
|
||||
|
||||
const hashtags: HashtagSuggestion[] = [];
|
||||
const topicWords = topic.toLowerCase().split(/\s+/);
|
||||
|
||||
// Generate niche hashtags (specific to topic)
|
||||
if (options?.includeNiche !== false) {
|
||||
const nicheHashtags = this.generateNicheHashtags(topicWords, Math.ceil(count * 0.4));
|
||||
hashtags.push(...nicheHashtags);
|
||||
}
|
||||
|
||||
// Generate trending/popular hashtags
|
||||
if (options?.includeTrending !== false) {
|
||||
const trendingHashtags = this.generateTrendingHashtags(topicWords, Math.ceil(count * 0.3));
|
||||
hashtags.push(...trendingHashtags);
|
||||
}
|
||||
|
||||
// Generate broad/community hashtags
|
||||
const communityHashtags = this.generateCommunityHashtags(topicWords, Math.ceil(count * 0.3));
|
||||
hashtags.push(...communityHashtags);
|
||||
|
||||
// Sort by reach potential and take top N
|
||||
const sortedHashtags = hashtags
|
||||
.sort((a, b) => b.reachPotential - a.reachPotential)
|
||||
.slice(0, count);
|
||||
|
||||
return {
|
||||
topic,
|
||||
platform,
|
||||
hashtags: sortedHashtags,
|
||||
strategy: this.generateStrategy(platform, sortedHashtags),
|
||||
recommendedCount: Math.min(count, maxCount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a hashtag
|
||||
*/
|
||||
analyzeHashtag(hashtag: string): HashtagAnalysis {
|
||||
const cleanHashtag = hashtag.replace('#', '').toLowerCase();
|
||||
const data = this.hashtagDatabase[cleanHashtag] || { posts: 100000, engagement: 2.0 };
|
||||
|
||||
return {
|
||||
hashtag: `#${cleanHashtag}`,
|
||||
totalPosts: data.posts,
|
||||
avgEngagement: data.engagement,
|
||||
topRelated: this.findRelatedHashtags(cleanHashtag),
|
||||
bestTimeToUse: this.getBestTimeToUse(data.engagement),
|
||||
overused: data.posts > 20000000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find related hashtags
|
||||
*/
|
||||
findRelatedHashtags(hashtag: string, count: number = 5): string[] {
|
||||
// Mock related hashtags based on common patterns
|
||||
const related: string[] = [];
|
||||
const base = hashtag.replace('#', '').toLowerCase();
|
||||
|
||||
// Add common variations
|
||||
related.push(`#${base}tips`);
|
||||
related.push(`#${base}life`);
|
||||
related.push(`#${base}community`);
|
||||
related.push(`#${base}daily`);
|
||||
related.push(`#${base}goals`);
|
||||
|
||||
return related.slice(0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check hashtag performance
|
||||
*/
|
||||
checkPerformance(hashtags: string[]): {
|
||||
hashtag: string;
|
||||
score: number;
|
||||
recommendation: string;
|
||||
}[] {
|
||||
return hashtags.map((hashtag) => {
|
||||
const analysis = this.analyzeHashtag(hashtag);
|
||||
const score = this.calculateHashtagScore(analysis);
|
||||
|
||||
let recommendation = '';
|
||||
if (score >= 80) recommendation = 'Excellent choice';
|
||||
else if (score >= 60) recommendation = 'Good hashtag';
|
||||
else if (score >= 40) recommendation = 'Consider alternatives';
|
||||
else recommendation = 'Not recommended';
|
||||
|
||||
return { hashtag, score, recommendation };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate optimal hashtag strategy
|
||||
*/
|
||||
generateStrategy(platform: string, hashtags: HashtagSuggestion[]): string {
|
||||
const strategies: Record<string, string> = {
|
||||
instagram: `Use ${this.platformLimits.instagram} hashtags: Mix 10 niche, 10 medium, 10 broad. Place in first comment for cleaner look.`,
|
||||
twitter: `Use 1-3 relevant hashtags. Focus on trending topics for visibility.`,
|
||||
linkedin: `Use 3-5 professional hashtags. Industry-specific performs best.`,
|
||||
tiktok: `Use 3-5 hashtags including trending sounds/challenges. Niche > broad.`,
|
||||
youtube: `Use 3-5 hashtags in description. Include 1 branded hashtag.`,
|
||||
facebook: `Minimal hashtags (1-2). Focus on groups and direct engagement.`,
|
||||
threads: `Hashtags have limited impact. Focus on content quality.`,
|
||||
};
|
||||
|
||||
return strategies[platform] || 'Use relevant hashtags based on your content.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trending hashtags
|
||||
*/
|
||||
getTrendingHashtags(
|
||||
category?: string,
|
||||
count: number = 10,
|
||||
): { hashtag: string; growth: number; posts24h: number }[] {
|
||||
// Mock trending hashtags
|
||||
const trending = [
|
||||
{ hashtag: '#AI', growth: 150, posts24h: 50000 },
|
||||
{ hashtag: '#ChatGPT', growth: 120, posts24h: 45000 },
|
||||
{ hashtag: '#ContentCreation', growth: 80, posts24h: 30000 },
|
||||
{ hashtag: '#RemoteWork', growth: 60, posts24h: 25000 },
|
||||
{ hashtag: '#PersonalBranding', growth: 55, posts24h: 20000 },
|
||||
{ hashtag: '#Productivity', growth: 45, posts24h: 18000 },
|
||||
{ hashtag: '#SideHustle', growth: 40, posts24h: 15000 },
|
||||
{ hashtag: '#GrowthMindset', growth: 35, posts24h: 12000 },
|
||||
{ hashtag: '#Entrepreneurship', growth: 30, posts24h: 10000 },
|
||||
{ hashtag: '#DigitalMarketing', growth: 25, posts24h: 8000 },
|
||||
];
|
||||
|
||||
return trending.slice(0, count);
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
private generateNicheHashtags(words: string[], count: number): HashtagSuggestion[] {
|
||||
return words.slice(0, count).map((word) => ({
|
||||
hashtag: `#${word}`,
|
||||
type: 'niche' as HashtagType,
|
||||
popularity: 'medium' as const,
|
||||
competition: 'low' as const,
|
||||
reachPotential: 70 + Math.random() * 20,
|
||||
recommended: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private generateTrendingHashtags(words: string[], count: number): HashtagSuggestion[] {
|
||||
const trending = ['tips', 'howto', '2024', 'hacks', 'growth'];
|
||||
return trending.slice(0, count).map((suffix) => ({
|
||||
hashtag: `#${words[0]}${suffix}`,
|
||||
type: 'trending' as HashtagType,
|
||||
popularity: 'high' as const,
|
||||
competition: 'high' as const,
|
||||
reachPotential: 60 + Math.random() * 30,
|
||||
recommended: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private generateCommunityHashtags(words: string[], count: number): HashtagSuggestion[] {
|
||||
const community = ['community', 'life', 'motivation', 'success', 'goals'];
|
||||
return community.slice(0, count).map((suffix) => ({
|
||||
hashtag: `#${words[0]}${suffix}`,
|
||||
type: 'community' as HashtagType,
|
||||
popularity: 'high' as const,
|
||||
competition: 'medium' as const,
|
||||
reachPotential: 50 + Math.random() * 25,
|
||||
recommended: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private getBestTimeToUse(engagement: number): string {
|
||||
if (engagement > 3) return 'Peak hours (9AM-11AM, 6PM-9PM)';
|
||||
if (engagement > 2) return 'Business hours (9AM-5PM)';
|
||||
return 'Anytime';
|
||||
}
|
||||
|
||||
private calculateHashtagScore(analysis: HashtagAnalysis): number {
|
||||
let score = 50;
|
||||
|
||||
// Engagement factor
|
||||
score += analysis.avgEngagement * 10;
|
||||
|
||||
// Overused penalty
|
||||
if (analysis.overused) score -= 20;
|
||||
|
||||
// Sweet spot for posts (not too few, not too many)
|
||||
if (analysis.totalPosts >= 100000 && analysis.totalPosts <= 10000000) {
|
||||
score += 15;
|
||||
}
|
||||
|
||||
return Math.min(100, Math.max(0, Math.round(score)));
|
||||
}
|
||||
}
|
||||
330
src/modules/content-generation/services/niche.service.ts
Normal file
330
src/modules/content-generation/services/niche.service.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
// Niche Service - Niche selection and management
|
||||
// Path: src/modules/content-generation/services/niche.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface Niche {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
category: NicheCategory;
|
||||
description: string;
|
||||
targetAudience: string[];
|
||||
painPoints: string[];
|
||||
keywords: string[];
|
||||
contentAngles: string[];
|
||||
competitors: string[];
|
||||
monetization: string[];
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced';
|
||||
growthPotential: number; // 1-100
|
||||
engagement: {
|
||||
avgLikes: number;
|
||||
avgComments: number;
|
||||
avgShares: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type NicheCategory =
|
||||
| 'business'
|
||||
| 'finance'
|
||||
| 'health'
|
||||
| 'tech'
|
||||
| 'lifestyle'
|
||||
| 'education'
|
||||
| 'entertainment'
|
||||
| 'creative'
|
||||
| 'personal_development'
|
||||
| 'relationships'
|
||||
| 'career'
|
||||
| 'parenting';
|
||||
|
||||
export interface NicheAnalysis {
|
||||
niche: Niche;
|
||||
saturation: 'low' | 'medium' | 'high';
|
||||
competition: number; // 1-100
|
||||
opportunity: number; // 1-100
|
||||
trendingTopics: string[];
|
||||
contentGaps: string[];
|
||||
recommendedPlatforms: string[];
|
||||
monetizationPotential: number; // 1-100
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NicheService {
|
||||
private readonly logger = new Logger(NicheService.name);
|
||||
|
||||
// Popular niches database
|
||||
private readonly niches: Niche[] = [
|
||||
{
|
||||
id: 'personal-finance',
|
||||
name: 'Personal Finance',
|
||||
slug: 'personal-finance',
|
||||
category: 'finance',
|
||||
description: 'Money management, investing, budgeting, and financial freedom',
|
||||
targetAudience: ['millennials', 'young professionals', 'families', 'retirees'],
|
||||
painPoints: ['debt', 'saving money', 'investing confusion', 'retirement anxiety'],
|
||||
keywords: ['budgeting', 'investing', 'passive income', 'financial freedom', 'side hustle'],
|
||||
contentAngles: ['beginner guides', 'investment strategies', 'debt payoff stories', 'money mistakes'],
|
||||
competitors: ['The Financial Diet', 'Graham Stephan', 'Dave Ramsey'],
|
||||
monetization: ['affiliate marketing', 'courses', 'coaching', 'sponsored content'],
|
||||
difficulty: 'intermediate',
|
||||
growthPotential: 85,
|
||||
engagement: { avgLikes: 500, avgComments: 50, avgShares: 100 },
|
||||
},
|
||||
{
|
||||
id: 'productivity',
|
||||
name: 'Productivity & Time Management',
|
||||
slug: 'productivity',
|
||||
category: 'personal_development',
|
||||
description: 'Work efficiency, habits, systems, and getting more done',
|
||||
targetAudience: ['entrepreneurs', 'remote workers', 'students', 'executives'],
|
||||
painPoints: ['procrastination', 'overwhelm', 'work-life balance', 'focus issues'],
|
||||
keywords: ['productivity tips', 'time management', 'habits', 'morning routine', 'deep work'],
|
||||
contentAngles: ['system breakdowns', 'tool reviews', 'habit building', 'workflow optimization'],
|
||||
competitors: ['Ali Abdaal', 'Thomas Frank', 'Cal Newport'],
|
||||
monetization: ['digital products', 'consulting', 'affiliate marketing', 'memberships'],
|
||||
difficulty: 'intermediate',
|
||||
growthPotential: 80,
|
||||
engagement: { avgLikes: 800, avgComments: 80, avgShares: 200 },
|
||||
},
|
||||
{
|
||||
id: 'ai-tech',
|
||||
name: 'AI & Technology',
|
||||
slug: 'ai-tech',
|
||||
category: 'tech',
|
||||
description: 'Artificial intelligence, automation, and emerging technology',
|
||||
targetAudience: ['tech enthusiasts', 'developers', 'entrepreneurs', 'business owners'],
|
||||
painPoints: ['keeping up with change', 'implementation', 'job automation fears', 'tool overload'],
|
||||
keywords: ['AI tools', 'ChatGPT', 'automation', 'machine learning', 'future of work'],
|
||||
contentAngles: ['tool tutorials', 'trend analysis', 'use cases', 'predictions'],
|
||||
competitors: ['Matt Wolfe', 'Linus Tech Tips', 'Fireship'],
|
||||
monetization: ['sponsorships', 'affiliate marketing', 'consulting', 'SaaS products'],
|
||||
difficulty: 'advanced',
|
||||
growthPotential: 95,
|
||||
engagement: { avgLikes: 1200, avgComments: 150, avgShares: 400 },
|
||||
},
|
||||
{
|
||||
id: 'content-creation',
|
||||
name: 'Content Creation & Social Media',
|
||||
slug: 'content-creation',
|
||||
category: 'creative',
|
||||
description: 'Growing on social media, creating content, and building an audience',
|
||||
targetAudience: ['aspiring creators', 'small businesses', 'influencers', 'marketers'],
|
||||
painPoints: ['algorithm changes', 'consistency', 'monetization', 'growth plateaus'],
|
||||
keywords: ['grow on Instagram', 'content strategy', 'viral content', 'engagement tips'],
|
||||
contentAngles: ['platform strategies', 'case studies', 'tool recommendations', 'growth hacks'],
|
||||
competitors: ['Vanessa Lau', 'Jade Darmawangsa', 'Roberto Blake'],
|
||||
monetization: ['courses', 'coaching', 'agency services', 'brand deals'],
|
||||
difficulty: 'intermediate',
|
||||
growthPotential: 75,
|
||||
engagement: { avgLikes: 600, avgComments: 100, avgShares: 150 },
|
||||
},
|
||||
{
|
||||
id: 'mental-health',
|
||||
name: 'Mental Health & Wellness',
|
||||
slug: 'mental-health',
|
||||
category: 'health',
|
||||
description: 'Mental wellness, anxiety, stress management, and self-care',
|
||||
targetAudience: ['young adults', 'stressed professionals', 'students', 'parents'],
|
||||
painPoints: ['anxiety', 'burnout', 'depression', 'overwhelm', 'self-doubt'],
|
||||
keywords: ['mental health tips', 'anxiety relief', 'self-care', 'therapy', 'mindfulness'],
|
||||
contentAngles: ['personal stories', 'coping strategies', 'professional insights', 'resources'],
|
||||
competitors: ['Therapy in a Nutshell', 'Dr. Julie Smith', 'The Holistic Psychologist'],
|
||||
monetization: ['books', 'courses', 'therapy referrals', 'speaking'],
|
||||
difficulty: 'advanced',
|
||||
growthPotential: 90,
|
||||
engagement: { avgLikes: 2000, avgComments: 300, avgShares: 500 },
|
||||
},
|
||||
{
|
||||
id: 'entrepreneurship',
|
||||
name: 'Entrepreneurship & Startups',
|
||||
slug: 'entrepreneurship',
|
||||
category: 'business',
|
||||
description: 'Starting and growing businesses, startup culture, and business strategy',
|
||||
targetAudience: ['founders', 'aspiring entrepreneurs', 'small business owners', 'investors'],
|
||||
painPoints: ['funding', 'scaling', 'finding customers', 'team building', 'failure fear'],
|
||||
keywords: ['startup tips', 'business ideas', 'entrepreneurship', 'funding', 'growth strategies'],
|
||||
contentAngles: ['founder stories', 'business frameworks', 'failure lessons', 'growth strategies'],
|
||||
competitors: ['GaryVee', 'Alex Hormozi', 'My First Million'],
|
||||
monetization: ['coaching', 'events', 'investments', 'courses'],
|
||||
difficulty: 'advanced',
|
||||
growthPotential: 85,
|
||||
engagement: { avgLikes: 1500, avgComments: 200, avgShares: 350 },
|
||||
},
|
||||
{
|
||||
id: 'fitness',
|
||||
name: 'Fitness & Exercise',
|
||||
slug: 'fitness',
|
||||
category: 'health',
|
||||
description: 'Workouts, gym routines, home fitness, and physical health',
|
||||
targetAudience: ['beginners', 'fitness enthusiasts', 'athletes', 'busy professionals'],
|
||||
painPoints: ['motivation', 'time constraints', 'plateau', 'injuries', 'gym intimidation'],
|
||||
keywords: ['workout routine', 'home workout', 'weight loss', 'muscle building', 'fitness tips'],
|
||||
contentAngles: ['workout demos', 'transformation stories', 'nutrition tips', 'myth busting'],
|
||||
competitors: ['Jeff Nippard', 'Athlean-X', 'Whitney Simmons'],
|
||||
monetization: ['programs', 'supplements', 'apparel', 'coaching'],
|
||||
difficulty: 'beginner',
|
||||
growthPotential: 70,
|
||||
engagement: { avgLikes: 3000, avgComments: 200, avgShares: 400 },
|
||||
},
|
||||
{
|
||||
id: 'parenting',
|
||||
name: 'Parenting & Family',
|
||||
slug: 'parenting',
|
||||
category: 'parenting',
|
||||
description: 'Raising children, family life, and parenting strategies',
|
||||
targetAudience: ['new parents', 'expecting parents', 'parents of teens', 'grandparents'],
|
||||
painPoints: ['sleep deprivation', 'discipline', 'education', 'work-life balance', 'screen time'],
|
||||
keywords: ['parenting tips', 'baby care', 'toddler', 'teen parenting', 'family activities'],
|
||||
contentAngles: ['age-specific tips', 'product reviews', 'real-life stories', 'expert advice'],
|
||||
competitors: ['Janet Lansbury', 'Dr. Becky', 'Big Little Feelings'],
|
||||
monetization: ['affiliate marketing', 'books', 'courses', 'brand partnerships'],
|
||||
difficulty: 'beginner',
|
||||
growthPotential: 75,
|
||||
engagement: { avgLikes: 1000, avgComments: 250, avgShares: 300 },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all niches
|
||||
*/
|
||||
getAllNiches(): Niche[] {
|
||||
return this.niches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get niche by ID or slug
|
||||
*/
|
||||
getNiche(idOrSlug: string): Niche | null {
|
||||
return this.niches.find((n) => n.id === idOrSlug || n.slug === idOrSlug) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get niches by category
|
||||
*/
|
||||
getNichesByCategory(category: NicheCategory): Niche[] {
|
||||
return this.niches.filter((n) => n.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a niche
|
||||
*/
|
||||
analyzeNiche(nicheId: string): NicheAnalysis | null {
|
||||
const niche = this.getNiche(nicheId);
|
||||
if (!niche) return null;
|
||||
|
||||
const saturation = niche.growthPotential < 60 ? 'high' : niche.growthPotential < 80 ? 'medium' : 'low';
|
||||
const competition = 100 - niche.growthPotential + Math.random() * 20;
|
||||
|
||||
return {
|
||||
niche,
|
||||
saturation,
|
||||
competition: Math.min(100, Math.round(competition)),
|
||||
opportunity: niche.growthPotential,
|
||||
trendingTopics: this.getTrendingTopics(niche),
|
||||
contentGaps: this.findContentGaps(niche),
|
||||
recommendedPlatforms: this.recommendPlatforms(niche),
|
||||
monetizationPotential: this.calculateMonetizationPotential(niche),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommend niches based on interests
|
||||
*/
|
||||
recommendNiches(interests: string[]): Niche[] {
|
||||
const scored = this.niches.map((niche) => {
|
||||
let score = 0;
|
||||
const nicheText = [
|
||||
niche.name,
|
||||
niche.description,
|
||||
...niche.keywords,
|
||||
...niche.contentAngles,
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
for (const interest of interests) {
|
||||
if (nicheText.includes(interest.toLowerCase())) {
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
score += niche.growthPotential / 10;
|
||||
|
||||
return { niche, score };
|
||||
});
|
||||
|
||||
return scored
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((s) => s.niche);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content ideas for a niche
|
||||
*/
|
||||
getContentIdeas(nicheId: string, count: number = 10): string[] {
|
||||
const niche = this.getNiche(nicheId);
|
||||
if (!niche) return [];
|
||||
|
||||
const ideas: string[] = [];
|
||||
const templates = [
|
||||
`${count} things nobody tells you about {keyword}`,
|
||||
`How to {keyword} without {pain_point}`,
|
||||
`Why most people fail at {keyword}`,
|
||||
`The complete guide to {keyword} for beginners`,
|
||||
`{keyword} mistakes I made (so you don't have to)`,
|
||||
`How I {keyword} in {time_period}`,
|
||||
`Stop doing this if you want to {keyword}`,
|
||||
`The truth about {keyword} that experts won't tell you`,
|
||||
`{keyword} tips that actually work in 2024`,
|
||||
`My {keyword} system that changed everything`,
|
||||
];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const template = templates[i % templates.length];
|
||||
const keyword = niche.keywords[i % niche.keywords.length];
|
||||
const painPoint = niche.painPoints[i % niche.painPoints.length];
|
||||
|
||||
let idea = template
|
||||
.replace('{keyword}', keyword)
|
||||
.replace('{pain_point}', painPoint)
|
||||
.replace('{time_period}', '30 days')
|
||||
.replace('{count}', String(Math.floor(Math.random() * 7 + 3)));
|
||||
|
||||
ideas.push(idea);
|
||||
}
|
||||
|
||||
return ideas;
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
private getTrendingTopics(niche: Niche): string[] {
|
||||
// Mock trending topics
|
||||
return niche.keywords.slice(0, 3).map((k) => `${k} 2024`);
|
||||
}
|
||||
|
||||
private findContentGaps(niche: Niche): string[] {
|
||||
return [
|
||||
`Advanced ${niche.keywords[0]} strategies`,
|
||||
`${niche.name} for complete beginners`,
|
||||
`Common ${niche.keywords[1]} mistakes`,
|
||||
`${niche.name} case studies`,
|
||||
];
|
||||
}
|
||||
|
||||
private recommendPlatforms(niche: Niche): string[] {
|
||||
const platforms: string[] = [];
|
||||
|
||||
if (niche.engagement.avgLikes > 1000) platforms.push('instagram', 'tiktok');
|
||||
if (niche.category === 'business' || niche.category === 'career') platforms.push('linkedin');
|
||||
if (niche.engagement.avgShares > 200) platforms.push('twitter');
|
||||
if (niche.difficulty === 'advanced') platforms.push('youtube');
|
||||
|
||||
return platforms.length > 0 ? platforms : ['instagram', 'twitter'];
|
||||
}
|
||||
|
||||
private calculateMonetizationPotential(niche: Niche): number {
|
||||
const monetizationScore = niche.monetization.length * 15;
|
||||
const difficultyBonus = niche.difficulty === 'advanced' ? 20 : niche.difficulty === 'intermediate' ? 10 : 0;
|
||||
return Math.min(100, monetizationScore + difficultyBonus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
// Platform Generator Service - Platform-specific content generation with AI
|
||||
// Path: src/modules/content-generation/services/platform-generator.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { GeminiService } from '../../gemini/gemini.service';
|
||||
|
||||
export type Platform =
|
||||
| 'twitter'
|
||||
| 'instagram'
|
||||
| 'linkedin'
|
||||
| 'facebook'
|
||||
| 'tiktok'
|
||||
| 'youtube'
|
||||
| 'threads'
|
||||
| 'medium';
|
||||
|
||||
export interface PlatformConfig {
|
||||
platform: Platform;
|
||||
name: string;
|
||||
icon: string;
|
||||
maxCharacters: number;
|
||||
maxHashtags: number;
|
||||
supportsMedia: boolean;
|
||||
mediaTypes: string[];
|
||||
bestPostingTimes: string[];
|
||||
contentFormats: ContentFormat[];
|
||||
tone: string;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export interface ContentFormat {
|
||||
name: string;
|
||||
description: string;
|
||||
template: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
export interface GeneratedContent {
|
||||
platform: Platform;
|
||||
format: string;
|
||||
content: string;
|
||||
hashtags: string[];
|
||||
mediaRecommendations: string[];
|
||||
postingRecommendation: string;
|
||||
characterCount: number;
|
||||
isWithinLimit: boolean;
|
||||
variations?: string[];
|
||||
}
|
||||
|
||||
export interface MultiPlatformContent {
|
||||
original: {
|
||||
topic: string;
|
||||
mainMessage: string;
|
||||
};
|
||||
platforms: GeneratedContent[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PlatformGeneratorService {
|
||||
private readonly logger = new Logger(PlatformGeneratorService.name);
|
||||
|
||||
constructor(private readonly gemini: GeminiService) { }
|
||||
|
||||
// Platform configurations
|
||||
private readonly platforms: Record<Platform, PlatformConfig> = {
|
||||
twitter: {
|
||||
platform: 'twitter',
|
||||
name: 'Twitter/X',
|
||||
icon: '𝕏',
|
||||
maxCharacters: 280,
|
||||
maxHashtags: 3,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'video', 'gif', 'poll'],
|
||||
bestPostingTimes: ['9:00 AM', '12:00 PM', '5:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Single Tweet',
|
||||
description: 'Concise, impactful single post',
|
||||
template: '[Hook]\n\n[Main point]\n\n[CTA]',
|
||||
example: 'The best content creators don\'t just post.\n\nThey build systems.\n\nHere\'s how ↓',
|
||||
},
|
||||
{
|
||||
name: 'Thread',
|
||||
description: 'Long-form content in connected tweets',
|
||||
template: '🧵 [Hook - create curiosity]\n\n1/ [First point]\n2/ [Second point]\n...\n\n[Summary + CTA]',
|
||||
example: '🧵 I studied 100 viral threads.\n\nHere\'s what they all have in common:',
|
||||
},
|
||||
{
|
||||
name: 'Quote Tweet',
|
||||
description: 'Commentary on existing content',
|
||||
template: '[Your perspective]\n\n[Quote/retweet]',
|
||||
example: 'This is exactly why I always say:\n\nConsistency beats talent every time.',
|
||||
},
|
||||
],
|
||||
tone: 'conversational, witty, direct',
|
||||
features: ['retweets', 'quote tweets', 'polls', 'spaces'],
|
||||
},
|
||||
|
||||
instagram: {
|
||||
platform: 'instagram',
|
||||
name: 'Instagram',
|
||||
icon: '📸',
|
||||
maxCharacters: 2200,
|
||||
maxHashtags: 30,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'carousel', 'reel', 'story'],
|
||||
bestPostingTimes: ['11:00 AM', '2:00 PM', '7:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Carousel',
|
||||
description: 'Educational multi-slide content',
|
||||
template: 'Slide 1: [Hook headline]\nSlide 2-9: [Value points]\nSlide 10: [CTA]\n\nCaption: [Hook] + [Context] + [CTA] + [Hashtags]',
|
||||
example: 'Slide 1: 7 Things I Wish I Knew Before Starting\nSlide 2: 1. Your first content will suck...',
|
||||
},
|
||||
{
|
||||
name: 'Reel',
|
||||
description: 'Short-form video content',
|
||||
template: '[Hook - 3 sec] → [Content - 15-30 sec] → [CTA/Loop - 3 sec]',
|
||||
example: 'Hook: "Stop doing this if you want to grow"\nContent: Show the mistake and fix\nCTA: "Follow for more tips"',
|
||||
},
|
||||
{
|
||||
name: 'Single Post',
|
||||
description: 'Static image with caption',
|
||||
template: '[Visual hook]\n\nCaption:\n[Hook line]\n\n[Value/Story]\n\n[CTA]\n\n[Hashtags]',
|
||||
example: 'The difference between struggle and success:\n\nIt\'s not about working harder.\nIt\'s about working smarter.\n\nSave this for later 💾',
|
||||
},
|
||||
],
|
||||
tone: 'visual, aspirational, authentic',
|
||||
features: ['stories', 'reels', 'guides', 'collabs', 'live'],
|
||||
},
|
||||
|
||||
linkedin: {
|
||||
platform: 'linkedin',
|
||||
name: 'LinkedIn',
|
||||
icon: '💼',
|
||||
maxCharacters: 3000,
|
||||
maxHashtags: 5,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'video', 'document', 'poll'],
|
||||
bestPostingTimes: ['7:00 AM', '12:00 PM', '5:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Story Post',
|
||||
description: 'Personal story with lesson',
|
||||
template: '[Hook - short, powerful]\n\n↓\n\n[Story with struggle]\n[Turning point]\n[Result]\n\n[Key lessons]\n\n[Question for engagement]',
|
||||
example: 'I got fired 6 months ago.\n\n↓\n\nBest thing that ever happened to me.\n\nHere\'s why...',
|
||||
},
|
||||
{
|
||||
name: 'Listicle',
|
||||
description: 'Numbered tips or insights',
|
||||
template: '[Hook]\n\n1. [Point 1]\n2. [Point 2]\n3. [Point 3]\n...\n\n[Summary]\n[CTA]',
|
||||
example: '5 things successful leaders do differently:\n\n1. They listen more than they talk...',
|
||||
},
|
||||
{
|
||||
name: 'Contrarian Take',
|
||||
description: 'Against-the-grain perspective',
|
||||
template: '[Unpopular opinion]\n\n[Your reasoning]\n\n[Evidence/experience]\n\n[Conclusion]\n\n[Discussion prompt]',
|
||||
example: 'Controversial take:\n\nHustle culture is killing creativity.\n\nHere\'s what I mean...',
|
||||
},
|
||||
],
|
||||
tone: 'professional, thoughtful, authentic',
|
||||
features: ['articles', 'newsletters', 'polls', 'events'],
|
||||
},
|
||||
|
||||
facebook: {
|
||||
platform: 'facebook',
|
||||
name: 'Facebook',
|
||||
icon: '📘',
|
||||
maxCharacters: 63206,
|
||||
maxHashtags: 3,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'video', 'live', 'story', 'reel'],
|
||||
bestPostingTimes: ['1:00 PM', '4:00 PM', '8:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Community Post',
|
||||
description: 'Engagement-focused discussion',
|
||||
template: '[Question or discussion starter]\n\n[Context]\n\n[Call for opinions]',
|
||||
example: 'Quick question for parents:\n\nHow do you handle screen time with your kids?\n\nI\'m curious what works for you 👇',
|
||||
},
|
||||
{
|
||||
name: 'Story Share',
|
||||
description: 'Personal or customer story',
|
||||
template: '[Hook]\n\n[Background]\n[Challenge]\n[Solution/Outcome]\n\n[Takeaway]',
|
||||
example: 'This message made my day ❤️\n\nA customer just sent me this...',
|
||||
},
|
||||
],
|
||||
tone: 'friendly, conversational, community-focused',
|
||||
features: ['groups', 'events', 'marketplace', 'reels'],
|
||||
},
|
||||
|
||||
tiktok: {
|
||||
platform: 'tiktok',
|
||||
name: 'TikTok',
|
||||
icon: '🎵',
|
||||
maxCharacters: 2200,
|
||||
maxHashtags: 5,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['video', 'live', 'story'],
|
||||
bestPostingTimes: ['7:00 AM', '12:00 PM', '7:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Tutorial',
|
||||
description: 'How-to video content',
|
||||
template: '[Hook - 1-3 sec] → [Steps - fast paced] → [Result/CTA]',
|
||||
example: 'POV: You finally learn this trick\n*shows quick tutorial*\nFollow for more!',
|
||||
},
|
||||
{
|
||||
name: 'Storytime',
|
||||
description: 'Narrative content',
|
||||
template: '[Hook that creates curiosity] → [Story with pacing] → [Payoff]',
|
||||
example: 'So this happened at work today...\n*tells story with dramatic pauses*',
|
||||
},
|
||||
{
|
||||
name: 'Trend',
|
||||
description: 'Trend participation',
|
||||
template: '[Trending sound/format] + [Your niche twist]',
|
||||
example: '*Uses trending sound but applies it to your industry*',
|
||||
},
|
||||
],
|
||||
tone: 'casual, entertaining, authentic',
|
||||
features: ['duets', 'stitches', 'effects', 'live'],
|
||||
},
|
||||
|
||||
youtube: {
|
||||
platform: 'youtube',
|
||||
name: 'YouTube',
|
||||
icon: '▶️',
|
||||
maxCharacters: 5000,
|
||||
maxHashtags: 15,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['video', 'short', 'live', 'community'],
|
||||
bestPostingTimes: ['2:00 PM', '4:00 PM', '6:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Long-form Video',
|
||||
description: 'In-depth content (8-20 min)',
|
||||
template: 'Hook (0-30s) → Intro (30s-1m) → Content (main body) → CTA (last 30s)',
|
||||
example: 'Title: How I Grew From 0 to 100K Subscribers\nIntro: "In this video, I\'ll show you exactly..."',
|
||||
},
|
||||
{
|
||||
name: 'Short',
|
||||
description: 'Vertical short-form (< 60s)',
|
||||
template: '[Hook - 1s] → [Value - 50s] → [CTA - 9s]',
|
||||
example: '*Quick tip or fact* → "Subscribe for more!"',
|
||||
},
|
||||
],
|
||||
tone: 'educational, entertaining, personality-driven',
|
||||
features: ['community posts', 'playlists', 'premieres', 'cards'],
|
||||
},
|
||||
|
||||
threads: {
|
||||
platform: 'threads',
|
||||
name: 'Threads',
|
||||
icon: '🧵',
|
||||
maxCharacters: 500,
|
||||
maxHashtags: 0,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'video', 'carousel'],
|
||||
bestPostingTimes: ['9:00 AM', '1:00 PM', '6:00 PM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Hot Take',
|
||||
description: 'Opinion or observation',
|
||||
template: '[Strong opinion]\n\n[Brief explanation]',
|
||||
example: 'Hot take: Most productivity advice is just procrastination in disguise.',
|
||||
},
|
||||
{
|
||||
name: 'Conversation Starter',
|
||||
description: 'Discussion prompt',
|
||||
template: '[Relatable observation or question]\n\n[Optional context]',
|
||||
example: 'What\'s one thing you wish you learned earlier in your career?',
|
||||
},
|
||||
],
|
||||
tone: 'casual, conversational, text-first',
|
||||
features: ['reposts', 'quotes', 'follows from Instagram'],
|
||||
},
|
||||
|
||||
medium: {
|
||||
platform: 'medium', // Note: You might need to add 'medium' to Platform type definition if strict
|
||||
name: 'Medium',
|
||||
icon: '📝',
|
||||
maxCharacters: 20000,
|
||||
maxHashtags: 5,
|
||||
supportsMedia: true,
|
||||
mediaTypes: ['image', 'embed'],
|
||||
bestPostingTimes: ['8:00 AM', '10:00 AM'],
|
||||
contentFormats: [
|
||||
{
|
||||
name: 'Blog Post',
|
||||
description: 'Long-form article',
|
||||
template: '# [Title]\n\n## Introduction\n[Hook]\n\n## Main Point 1\n[Content]\n\n## Main Point 2\n[Content]\n\n## Conclusion\n[Summary + CTA]',
|
||||
example: '# The Future of AI\n\n## Introduction\nAI is changing everything...',
|
||||
}
|
||||
],
|
||||
tone: 'professional, informative, storytelling',
|
||||
features: ['publications', 'newsletters'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
getPlatformConfig(platform: Platform): PlatformConfig {
|
||||
return this.platforms[platform];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all platform configurations
|
||||
*/
|
||||
getAllPlatforms(): PlatformConfig[] {
|
||||
return Object.values(this.platforms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content for a specific platform
|
||||
*/
|
||||
generateForPlatform(
|
||||
platform: Platform,
|
||||
input: {
|
||||
topic: string;
|
||||
mainMessage: string;
|
||||
format?: string;
|
||||
includeHashtags?: boolean;
|
||||
},
|
||||
): GeneratedContent {
|
||||
const config = this.platforms[platform];
|
||||
const format = input.format || config.contentFormats[0].name;
|
||||
const formatConfig = config.contentFormats.find((f) => f.name === format) || config.contentFormats[0];
|
||||
|
||||
// Generate content based on template
|
||||
const content = this.generateContent(input.topic, input.mainMessage, formatConfig, config);
|
||||
const hashtags = input.includeHashtags !== false ? this.generateHashtags(input.topic, config.maxHashtags) : [];
|
||||
|
||||
return {
|
||||
platform,
|
||||
format: formatConfig.name,
|
||||
content,
|
||||
hashtags,
|
||||
mediaRecommendations: this.getMediaRecommendations(platform, formatConfig.name),
|
||||
postingRecommendation: `Best times: ${config.bestPostingTimes.join(', ')}`,
|
||||
characterCount: content.length,
|
||||
isWithinLimit: content.length <= config.maxCharacters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content for multiple platforms
|
||||
*/
|
||||
generateMultiPlatform(input: {
|
||||
topic: string;
|
||||
mainMessage: string;
|
||||
platforms: Platform[];
|
||||
}): MultiPlatformContent {
|
||||
const platforms = input.platforms.map((p) =>
|
||||
this.generateForPlatform(p, {
|
||||
topic: input.topic,
|
||||
mainMessage: input.mainMessage,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
original: {
|
||||
topic: input.topic,
|
||||
mainMessage: input.mainMessage,
|
||||
},
|
||||
platforms,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt content from one platform to another
|
||||
*/
|
||||
adaptContent(
|
||||
content: string,
|
||||
fromPlatform: Platform,
|
||||
toPlatform: Platform,
|
||||
): GeneratedContent {
|
||||
const toConfig = this.platforms[toPlatform];
|
||||
let adapted = content;
|
||||
|
||||
// Shorten if needed
|
||||
if (adapted.length > toConfig.maxCharacters) {
|
||||
adapted = adapted.substring(0, toConfig.maxCharacters - 3) + '...';
|
||||
}
|
||||
|
||||
// Adjust tone/style based on platform
|
||||
adapted = this.adjustTone(adapted, toPlatform);
|
||||
|
||||
return {
|
||||
platform: toPlatform,
|
||||
format: 'adapted',
|
||||
content: adapted,
|
||||
hashtags: [],
|
||||
mediaRecommendations: this.getMediaRecommendations(toPlatform, 'adapted'),
|
||||
postingRecommendation: `Best times: ${toConfig.bestPostingTimes.join(', ')}`,
|
||||
characterCount: adapted.length,
|
||||
isWithinLimit: adapted.length <= toConfig.maxCharacters,
|
||||
};
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
private generateContent(
|
||||
topic: string,
|
||||
mainMessage: string,
|
||||
format: ContentFormat,
|
||||
config: PlatformConfig,
|
||||
): string {
|
||||
// Fallback template-based content (used when AI is not available)
|
||||
return this.generateTemplateContent(topic, mainMessage, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI-powered content using Gemini
|
||||
*/
|
||||
async generateAIContent(
|
||||
topic: string,
|
||||
mainMessage: string,
|
||||
platform: Platform,
|
||||
format: string,
|
||||
language: string = 'tr',
|
||||
): Promise<string> {
|
||||
const config = this.platforms[platform];
|
||||
|
||||
if (!this.gemini.isAvailable()) {
|
||||
this.logger.warn('Gemini not available, using template fallback');
|
||||
return this.generateTemplateContent(topic, mainMessage, config);
|
||||
}
|
||||
|
||||
const prompt = `Sen profesyonel bir sosyal medya içerik uzmanısın.
|
||||
|
||||
PLATFORM: ${config.name}
|
||||
KONU: ${topic}
|
||||
ANA MESAJ: ${mainMessage}
|
||||
FORMAT: ${format}
|
||||
KARAKTER LİMİTİ: ${config.maxCharacters}
|
||||
MAX HASHTAG: ${config.maxHashtags}
|
||||
TON: ${config.tone}
|
||||
|
||||
Bu platform için özgün, ilgi çekici ve viral potansiyeli yüksek bir içerik oluştur.
|
||||
|
||||
KURALLAR:
|
||||
1. Karakter limitine uy
|
||||
2. Platformun tonuna uygun yaz
|
||||
3. Hook (dikkat çeken giriş) ile başla
|
||||
4. CTA (harekete geçirici) ile bitir
|
||||
5. Emoji kullan ama aşırıya kaçma
|
||||
6. ${language === 'tr' ? 'Türkçe' : 'İngilizce'} yaz
|
||||
|
||||
SADECE içeriği yaz, açıklama veya başlık ekleme.`;
|
||||
|
||||
try {
|
||||
const response = await this.gemini.generateText(prompt, {
|
||||
temperature: 0.8,
|
||||
maxTokens: 1000,
|
||||
});
|
||||
return response.text;
|
||||
} catch (error) {
|
||||
this.logger.error(`AI content generation failed: ${error.message}`);
|
||||
return this.generateTemplateContent(topic, mainMessage, config);
|
||||
}
|
||||
}
|
||||
|
||||
private generateTemplateContent(
|
||||
topic: string,
|
||||
mainMessage: string,
|
||||
config: PlatformConfig,
|
||||
): string {
|
||||
let content = '';
|
||||
|
||||
switch (config.platform) {
|
||||
case 'twitter':
|
||||
content = `${mainMessage}\n\nLearn more about ${topic} 👇`;
|
||||
break;
|
||||
case 'instagram':
|
||||
content = `${mainMessage}\n\n·\n·\n·\n\n💡 Save this for later!\n\nWant more ${topic} tips? Follow @yourhandle`;
|
||||
break;
|
||||
case 'linkedin':
|
||||
content = `${mainMessage}\n\n↓\n\nHere's what I've learned about ${topic}:\n\n1. Start small\n2. Be consistent\n3. Learn from feedback\n\nWhat's your experience with ${topic}?\n\nShare in the comments 👇`;
|
||||
break;
|
||||
case 'tiktok':
|
||||
content = `POV: You finally understand ${topic}\n\n${mainMessage}\n\nFollow for more tips! 🎯`;
|
||||
break;
|
||||
case 'youtube':
|
||||
content = `📺 ${topic.toUpperCase()}\n\n${mainMessage}\n\n⏱️ Timestamps:\n0:00 Intro\n1:00 Main content\n\n🔔 Subscribe for more!`;
|
||||
break;
|
||||
case 'threads':
|
||||
content = `${mainMessage}`;
|
||||
break;
|
||||
default:
|
||||
content = mainMessage;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private generateHashtags(topic: string, maxCount: number): string[] {
|
||||
const words = topic.toLowerCase().split(' ').filter((w) => w.length > 3);
|
||||
const hashtags = words.slice(0, maxCount).map((w) => `#${w}`);
|
||||
return hashtags;
|
||||
}
|
||||
|
||||
private getMediaRecommendations(platform: Platform, format: string): string[] {
|
||||
const recommendations: Record<Platform, string[]> = {
|
||||
twitter: ['Add an image for 2x engagement', 'Consider a poll for interaction'],
|
||||
instagram: ['Use high-quality visuals', 'Add text overlays for accessibility', 'Use trending audio for Reels'],
|
||||
linkedin: ['Add a relevant image', 'Consider a carousel document', 'Include data visualizations'],
|
||||
facebook: ['Include a video if possible', 'Use native video over links'],
|
||||
tiktok: ['Use trending sounds', 'Add captions for accessibility', 'Film vertically'],
|
||||
youtube: ['Create an attention-grabbing thumbnail', 'Add end screens', 'Include cards for related videos'],
|
||||
threads: ['Keep it text-focused', 'Add one image max'],
|
||||
medium: ['Use high-quality header image', 'Break text with images/embeds', 'Use proper H1/H2 tags'],
|
||||
};
|
||||
|
||||
return recommendations[platform] || [];
|
||||
}
|
||||
|
||||
private adjustTone(content: string, platform: Platform): string {
|
||||
// Simple tone adjustments
|
||||
switch (platform) {
|
||||
case 'linkedin':
|
||||
return content.replace(/lol|haha|😂/gi, '');
|
||||
case 'tiktok':
|
||||
return content.replace(/Furthermore|Moreover|Additionally/gi, 'Also');
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
399
src/modules/content-generation/services/variation.service.ts
Normal file
399
src/modules/content-generation/services/variation.service.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
// Variation Service - Generate content variations
|
||||
// Path: src/modules/content-generation/services/variation.service.ts
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface VariationOptions {
|
||||
count?: number;
|
||||
variationType?: VariationType;
|
||||
preserveCore?: boolean;
|
||||
targetLength?: 'shorter' | 'same' | 'longer';
|
||||
toneShift?: string;
|
||||
}
|
||||
|
||||
export type VariationType =
|
||||
| 'hook' // Different hooks/openings
|
||||
| 'angle' // Different perspective/angle
|
||||
| 'tone' // Same content, different tone
|
||||
| 'format' // Different structure
|
||||
| 'length' // Shorter/longer versions
|
||||
| 'platform' // Platform-adapted versions
|
||||
| 'complete'; // Full rewrites
|
||||
|
||||
export interface ContentVariation {
|
||||
id: string;
|
||||
type: VariationType;
|
||||
content: string;
|
||||
changes: string[];
|
||||
similarity: number; // 0-100 (0 = completely different)
|
||||
}
|
||||
|
||||
export interface VariationSet {
|
||||
original: string;
|
||||
variations: ContentVariation[];
|
||||
recommendation: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class VariationService {
|
||||
private readonly logger = new Logger(VariationService.name);
|
||||
|
||||
// Hook variations
|
||||
private readonly hookTemplates = {
|
||||
question: (topic: string) => `Ever wondered why ${topic}?`,
|
||||
bold: (topic: string) => `Here's the truth about ${topic}:`,
|
||||
story: (topic: string) => `Last year, I learned something about ${topic} that changed everything.`,
|
||||
statistic: (topic: string) => `73% of people get ${topic} wrong. Here's what I discovered:`,
|
||||
contrarian: (topic: string) => `Everything you've been told about ${topic} is wrong.`,
|
||||
pain: (topic: string) => `Struggling with ${topic}? You're not alone.`,
|
||||
promise: (topic: string) => `What if you could master ${topic} in just 30 days?`,
|
||||
curiosity: (topic: string) => `The ${topic} secret nobody talks about:`,
|
||||
};
|
||||
|
||||
// Tone modifiers
|
||||
private readonly toneModifiers: Record<string, (text: string) => string> = {
|
||||
professional: (text) => text.replace(/!/g, '.').replace(/lol|haha|😂/gi, ''),
|
||||
casual: (text) => text.replace(/Furthermore,/g, 'Also,').replace(/However,/g, 'But'),
|
||||
enthusiastic: (text) => text.replace(/\./g, '!').replace(/good/gi, 'amazing'),
|
||||
empathetic: (text) => `I understand how challenging this can be. ${text}`,
|
||||
urgent: (text) => `Don't wait. ${text} Time is running out.`,
|
||||
humorous: (text) => text + ' (And yes, I learned this the hard way 😅)',
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate variations of content
|
||||
*/
|
||||
generateVariations(
|
||||
content: string,
|
||||
options: VariationOptions = {},
|
||||
): VariationSet {
|
||||
const { count = 3, variationType = 'complete', preserveCore = true } = options;
|
||||
const variations: ContentVariation[] = [];
|
||||
|
||||
switch (variationType) {
|
||||
case 'hook':
|
||||
variations.push(...this.generateHookVariations(content, count));
|
||||
break;
|
||||
case 'angle':
|
||||
variations.push(...this.generateAngleVariations(content, count));
|
||||
break;
|
||||
case 'tone':
|
||||
variations.push(...this.generateToneVariations(content, count, options.toneShift));
|
||||
break;
|
||||
case 'format':
|
||||
variations.push(...this.generateFormatVariations(content, count));
|
||||
break;
|
||||
case 'length':
|
||||
variations.push(...this.generateLengthVariations(content, options.targetLength || 'same'));
|
||||
break;
|
||||
case 'complete':
|
||||
default:
|
||||
variations.push(...this.generateCompleteVariations(content, count, preserveCore));
|
||||
}
|
||||
|
||||
return {
|
||||
original: content,
|
||||
variations: variations.slice(0, count),
|
||||
recommendation: this.getRecommendation(variations),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A/B test variations
|
||||
*/
|
||||
createABTest(content: string): {
|
||||
original: string;
|
||||
variationA: ContentVariation;
|
||||
variationB: ContentVariation;
|
||||
testingTips: string[];
|
||||
} {
|
||||
const hookVar = this.generateHookVariations(content, 1)[0];
|
||||
const formatVar = this.generateFormatVariations(content, 1)[0];
|
||||
|
||||
return {
|
||||
original: content,
|
||||
variationA: hookVar,
|
||||
variationB: formatVar,
|
||||
testingTips: [
|
||||
'Test at the same time of day',
|
||||
'Use similar audience targeting',
|
||||
'Run for at least 24-48 hours',
|
||||
'Look at engagement rate, not just likes',
|
||||
'Track save rate as quality indicator',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hook-only variations
|
||||
*/
|
||||
private generateHookVariations(content: string, count: number): ContentVariation[] {
|
||||
const topic = this.extractTopic(content);
|
||||
const hookTypes = Object.keys(this.hookTemplates);
|
||||
const variations: ContentVariation[] = [];
|
||||
|
||||
for (let i = 0; i < Math.min(count, hookTypes.length); i++) {
|
||||
const hookType = hookTypes[i] as keyof typeof this.hookTemplates;
|
||||
const newHook = this.hookTemplates[hookType](topic);
|
||||
|
||||
// Replace first line with new hook
|
||||
const lines = content.split('\n');
|
||||
const newContent = [newHook, ...lines.slice(1)].join('\n');
|
||||
|
||||
variations.push({
|
||||
id: `hook-${i + 1}`,
|
||||
type: 'hook',
|
||||
content: newContent,
|
||||
changes: [`Changed hook to ${hookType} style`],
|
||||
similarity: 85,
|
||||
});
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate angle variations
|
||||
*/
|
||||
private generateAngleVariations(content: string, count: number): ContentVariation[] {
|
||||
const angles = [
|
||||
{ name: 'how-to', prefix: 'Here\'s how to', focus: 'actionable steps' },
|
||||
{ name: 'why', prefix: 'This is why', focus: 'reasoning and benefits' },
|
||||
{ name: 'what-if', prefix: 'What if you could', focus: 'possibilities' },
|
||||
{ name: 'mistake', prefix: 'The biggest mistake people make with', focus: 'what not to do' },
|
||||
{ name: 'story', prefix: 'When I first started with', focus: 'personal experience' },
|
||||
];
|
||||
|
||||
const topic = this.extractTopic(content);
|
||||
const variations: ContentVariation[] = [];
|
||||
|
||||
for (let i = 0; i < Math.min(count, angles.length); i++) {
|
||||
const angle = angles[i];
|
||||
variations.push({
|
||||
id: `angle-${i + 1}`,
|
||||
type: 'angle',
|
||||
content: `${angle.prefix} ${topic}:\n\n${this.keepCore(content)}\n\nFocusing on: ${angle.focus}`,
|
||||
changes: [`Shifted to ${angle.name} angle`, `Focus: ${angle.focus}`],
|
||||
similarity: 70,
|
||||
});
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tone variations
|
||||
*/
|
||||
private generateToneVariations(
|
||||
content: string,
|
||||
count: number,
|
||||
specificTone?: string,
|
||||
): ContentVariation[] {
|
||||
const tones = specificTone
|
||||
? [specificTone]
|
||||
: Object.keys(this.toneModifiers).slice(0, count);
|
||||
|
||||
return tones.map((tone, i) => {
|
||||
const modifier = this.toneModifiers[tone];
|
||||
const modified = modifier ? modifier(content) : content;
|
||||
|
||||
return {
|
||||
id: `tone-${i + 1}`,
|
||||
type: 'tone',
|
||||
content: modified,
|
||||
changes: [`Applied ${tone} tone`],
|
||||
similarity: 90,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate format variations
|
||||
*/
|
||||
private generateFormatVariations(content: string, count: number): ContentVariation[] {
|
||||
const variations: ContentVariation[] = [];
|
||||
const points = this.extractPoints(content);
|
||||
|
||||
// Listicle format
|
||||
if (count >= 1) {
|
||||
variations.push({
|
||||
id: 'format-list',
|
||||
type: 'format',
|
||||
content: this.toListFormat(points),
|
||||
changes: ['Converted to numbered list'],
|
||||
similarity: 80,
|
||||
});
|
||||
}
|
||||
|
||||
// Bullet format
|
||||
if (count >= 2) {
|
||||
variations.push({
|
||||
id: 'format-bullets',
|
||||
type: 'format',
|
||||
content: this.toBulletFormat(points),
|
||||
changes: ['Converted to bullet points'],
|
||||
similarity: 80,
|
||||
});
|
||||
}
|
||||
|
||||
// Thread format
|
||||
if (count >= 3) {
|
||||
variations.push({
|
||||
id: 'format-thread',
|
||||
type: 'format',
|
||||
content: this.toThreadFormat(points),
|
||||
changes: ['Converted to thread format'],
|
||||
similarity: 75,
|
||||
});
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate length variations
|
||||
*/
|
||||
private generateLengthVariations(
|
||||
content: string,
|
||||
target: 'shorter' | 'same' | 'longer',
|
||||
): ContentVariation[] {
|
||||
const variations: ContentVariation[] = [];
|
||||
|
||||
if (target === 'shorter' || target === 'same') {
|
||||
variations.push({
|
||||
id: 'length-short',
|
||||
type: 'length',
|
||||
content: this.shorten(content),
|
||||
changes: ['Condensed to key points'],
|
||||
similarity: 75,
|
||||
});
|
||||
}
|
||||
|
||||
if (target === 'longer' || target === 'same') {
|
||||
variations.push({
|
||||
id: 'length-long',
|
||||
type: 'length',
|
||||
content: this.lengthen(content),
|
||||
changes: ['Expanded with more detail'],
|
||||
similarity: 70,
|
||||
});
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complete rewrites
|
||||
*/
|
||||
private generateCompleteVariations(
|
||||
content: string,
|
||||
count: number,
|
||||
preserveCore: boolean,
|
||||
): ContentVariation[] {
|
||||
const variations: ContentVariation[] = [];
|
||||
const topic = this.extractTopic(content);
|
||||
const core = this.keepCore(content);
|
||||
|
||||
// Variation 1: Different angle + hook
|
||||
variations.push({
|
||||
id: 'complete-1',
|
||||
type: 'complete',
|
||||
content: `The truth about ${topic}:\n\n${preserveCore ? core : this.rewriteCore(core)}\n\nSave this for later.`,
|
||||
changes: ['New hook', 'New CTA', preserveCore ? 'Preserved core' : 'Rewrote core'],
|
||||
similarity: preserveCore ? 60 : 40,
|
||||
});
|
||||
|
||||
// Variation 2: Story-based
|
||||
if (count >= 2) {
|
||||
variations.push({
|
||||
id: 'complete-2',
|
||||
type: 'complete',
|
||||
content: `I wish someone told me this about ${topic} earlier:\n\n${preserveCore ? core : this.rewriteCore(core)}\n\nShare if this helped.`,
|
||||
changes: ['Story-based approach', 'Personal angle'],
|
||||
similarity: preserveCore ? 55 : 35,
|
||||
});
|
||||
}
|
||||
|
||||
// Variation 3: Direct/Bold
|
||||
if (count >= 3) {
|
||||
variations.push({
|
||||
id: 'complete-3',
|
||||
type: 'complete',
|
||||
content: `Stop what you're doing.\n\nThis ${topic} insight is too important:\n\n${preserveCore ? core : this.rewriteCore(core)}\n\nYou're welcome.`,
|
||||
changes: ['Bold/direct style', 'Urgency added'],
|
||||
similarity: preserveCore ? 50 : 30,
|
||||
});
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private extractTopic(content: string): string {
|
||||
const firstLine = content.split('\n')[0];
|
||||
// Simple topic extraction from first line
|
||||
return firstLine.replace(/[#:?!.]/g, '').trim().substring(0, 50);
|
||||
}
|
||||
|
||||
private extractPoints(content: string): string[] {
|
||||
const lines = content.split('\n').filter((line) => line.trim().length > 0);
|
||||
return lines.slice(1, -1); // Remove first (hook) and last (CTA)
|
||||
}
|
||||
|
||||
private keepCore(content: string): string {
|
||||
const lines = content.split('\n').filter((line) => line.trim().length > 0);
|
||||
return lines.slice(1, -1).join('\n'); // Middle content
|
||||
}
|
||||
|
||||
private rewriteCore(core: string): string {
|
||||
// Simple word substitutions for demo
|
||||
return core
|
||||
.replace(/important/gi, 'crucial')
|
||||
.replace(/great/gi, 'excellent')
|
||||
.replace(/good/gi, 'solid')
|
||||
.replace(/need to/gi, 'must')
|
||||
.replace(/should/gi, 'need to');
|
||||
}
|
||||
|
||||
private toListFormat(points: string[]): string {
|
||||
return points.map((p, i) => `${i + 1}. ${p.replace(/^[-•*]\s*/, '')}`).join('\n');
|
||||
}
|
||||
|
||||
private toBulletFormat(points: string[]): string {
|
||||
return points.map((p) => `• ${p.replace(/^[-•*\d.]\s*/, '')}`).join('\n');
|
||||
}
|
||||
|
||||
private toThreadFormat(points: string[]): string {
|
||||
return points.map((p, i) => `${i + 1}/ ${p.replace(/^[-•*\d.]\s*/, '')}`).join('\n\n');
|
||||
}
|
||||
|
||||
private shorten(content: string): string {
|
||||
const lines = content.split('\n').filter((l) => l.trim());
|
||||
// Keep first, last, and every other middle line
|
||||
const shortened = [
|
||||
lines[0],
|
||||
...lines.slice(1, -1).filter((_, i) => i % 2 === 0),
|
||||
lines[lines.length - 1],
|
||||
];
|
||||
return shortened.join('\n\n');
|
||||
}
|
||||
|
||||
private lengthen(content: string): string {
|
||||
return content + '\n\n' +
|
||||
'📌 Key takeaway: Apply these insights consistently for best results.\n\n' +
|
||||
'💡 Pro tip: Start small and build momentum over time.\n\n' +
|
||||
'🎯 Action item: Pick one thing from this post and implement it today.';
|
||||
}
|
||||
|
||||
private getRecommendation(variations: ContentVariation[]): string {
|
||||
if (variations.length === 0) return 'No variations generated';
|
||||
|
||||
const highVariety = variations.filter((v) => v.similarity < 60);
|
||||
if (highVariety.length > 0) {
|
||||
return `Variation ${highVariety[0].id} offers the most differentiation (${100 - highVariety[0].similarity}% unique)`;
|
||||
}
|
||||
|
||||
return `All variations maintain strong similarity to original. Consider testing ${variations[0].id}.`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user