main
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s

This commit is contained in:
Harun CAN
2026-02-10 12:27:14 +03:00
parent 80f53511d8
commit fc88faddb9
141 changed files with 35961 additions and 101 deletions

View 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);
}
}

View 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: [],
};
}
}
}

View 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)));
}
}

View 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);
}
}

View File

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

View 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}.`;
}
}