Files
Content-Hunter_BE/src/modules/content/services/writing-styles.service.ts
Harun CAN fc88faddb9
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s
main
2026-02-10 12:27:14 +03:00

425 lines
15 KiB
TypeScript

// Writing Styles Service - Extended with 15+ personality tones
// Path: src/modules/content/services/writing-styles.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { ContentLanguage } from '@prisma/client';
export interface WritingStyleConfig {
name: string;
description?: string;
tone: WritingTone;
voice: 'first_person' | 'second_person' | 'third_person';
vocabulary: 'simple' | 'intermediate' | 'advanced' | 'technical';
sentenceLength: 'short' | 'medium' | 'long' | 'varied';
emojiUsage: 'none' | 'minimal' | 'moderate' | 'heavy';
hashtagStyle: 'none' | 'minimal' | 'topic_based' | 'trending';
structurePreference: 'paragraphs' | 'bullets' | 'numbered' | 'mixed';
engagementStyle: 'educational' | 'storytelling' | 'data_driven' | 'conversational' | 'provocative';
signatureElements?: string[];
avoidPhrases?: string[];
preferredPhrases?: string[];
language?: ContentLanguage;
}
// 15+ Writing Tones
export type WritingTone =
| 'storyteller' // Narrative-driven, emotionally engaging
| 'narrator' // Documentary style, observational
| 'sarcastic' // Witty, ironic, sharp humor
| 'inspirational' // Motivational, uplifting
| 'professional' // Business-like, polished
| 'casual' // Relaxed, conversational
| 'friendly' // Warm, approachable
| 'authoritative' // Expert, commanding
| 'playful' // Fun, light-hearted
| 'provocative' // Controversial, challenging
| 'empathetic' // Understanding, supportive
| 'analytical' // Data-focused, logical
| 'humorous' // Comedy-driven, entertaining
| 'minimalist' // Concise, direct
| 'dramatic' // Intense, emotional
| 'educational'; // Teaching, informative
export const WRITING_TONES: Record<WritingTone, { emoji: string; description: string; promptHint: string }> = {
storyteller: {
emoji: '📖',
description: 'Narrative-driven, weaves stories to make points memorable',
promptHint: 'Write like a master storyteller, using narrative techniques, character arcs, and emotional hooks',
},
narrator: {
emoji: '🎙️',
description: 'Documentary-style, observational and descriptive',
promptHint: 'Write like a documentary narrator, observing and describing with clarity and depth',
},
sarcastic: {
emoji: '😏',
description: 'Witty, ironic, with sharp humor',
promptHint: 'Write with sarcastic wit, using irony and clever observations that make readers think',
},
inspirational: {
emoji: '✨',
description: 'Motivational and uplifting content',
promptHint: 'Write to inspire and motivate, using powerful language that uplifts the reader',
},
professional: {
emoji: '💼',
description: 'Business-like, polished and authoritative',
promptHint: 'Write in a professional, business-appropriate tone with credibility and expertise',
},
casual: {
emoji: '😊',
description: 'Relaxed and conversational',
promptHint: 'Write casually, like talking to a friend over coffee',
},
friendly: {
emoji: '🤝',
description: 'Warm, approachable and welcoming',
promptHint: 'Write in a warm, friendly manner that makes readers feel comfortable and welcomed',
},
authoritative: {
emoji: '👔',
description: 'Expert voice with commanding presence',
promptHint: 'Write with authority and expertise, establishing credibility and trust',
},
playful: {
emoji: '🎉',
description: 'Fun, light-hearted and entertaining',
promptHint: 'Write playfully with humor, wordplay, and a light touch',
},
provocative: {
emoji: '🔥',
description: 'Controversial, thought-provoking',
promptHint: 'Write to challenge assumptions and provoke thought, with bold statements',
},
empathetic: {
emoji: '💙',
description: 'Understanding and supportive',
promptHint: 'Write with empathy, acknowledging struggles and offering understanding',
},
analytical: {
emoji: '📊',
description: 'Data-focused and logical',
promptHint: 'Write analytically, using data, logic, and structured arguments',
},
humorous: {
emoji: '😂',
description: 'Comedy-driven, entertaining',
promptHint: 'Write with humor, jokes, and entertainment value',
},
minimalist: {
emoji: '🎯',
description: 'Concise, direct, no fluff',
promptHint: 'Write minimally, every word counts, eliminate all unnecessary words',
},
dramatic: {
emoji: '🎭',
description: 'Intense, emotional, theatrical',
promptHint: 'Write dramatically with intensity, building tension and emotional impact',
},
educational: {
emoji: '📚',
description: 'Teaching-focused, informative',
promptHint: 'Write to educate, explain concepts clearly with examples and structure',
},
};
// Preset Writing Styles (combining tone + other settings)
export const PRESET_STYLES: Record<string, WritingStyleConfig> = {
master_storyteller: {
name: 'Master Storyteller',
description: 'Narrative-driven, emotionally engaging content with story arcs',
tone: 'storyteller',
voice: 'first_person',
vocabulary: 'intermediate',
sentenceLength: 'varied',
emojiUsage: 'minimal',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'storytelling',
preferredPhrases: ['Let me tell you...', 'Picture this:', 'Here\'s what happened:'],
},
sharp_sarcastic: {
name: 'Sharp & Sarcastic',
description: 'Witty observations with ironic humor',
tone: 'sarcastic',
voice: 'first_person',
vocabulary: 'intermediate',
sentenceLength: 'short',
emojiUsage: 'minimal',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'provocative',
preferredPhrases: ['Oh sure,', 'Because obviously,', 'Shocking, I know.'],
avoidPhrases: ['To be honest', 'Actually'],
},
documentary_narrator: {
name: 'Documentary Narrator',
description: 'Observational, descriptive, cinematic',
tone: 'narrator',
voice: 'third_person',
vocabulary: 'advanced',
sentenceLength: 'long',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'storytelling',
},
motivational_coach: {
name: 'Motivational Coach',
description: 'Inspiring, action-oriented, empowering',
tone: 'inspirational',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'moderate',
hashtagStyle: 'minimal',
structurePreference: 'bullets',
engagementStyle: 'conversational',
preferredPhrases: ['You can do this!', 'Here\'s the truth:', 'Your time is now.'],
},
data_analyst: {
name: 'Data Analyst',
description: 'Facts, figures, and logical conclusions',
tone: 'analytical',
voice: 'first_person',
vocabulary: 'technical',
sentenceLength: 'medium',
emojiUsage: 'none',
hashtagStyle: 'topic_based',
structurePreference: 'numbered',
engagementStyle: 'data_driven',
preferredPhrases: ['The data shows:', 'Research indicates:', 'Here are the numbers:'],
},
friendly_teacher: {
name: 'Friendly Teacher',
description: 'Educational, patient, encouraging',
tone: 'educational',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'moderate',
hashtagStyle: 'topic_based',
structurePreference: 'numbered',
engagementStyle: 'educational',
preferredPhrases: ['Let me explain:', 'Think of it this way:', 'Here\'s a simple example:'],
},
corporate_executive: {
name: 'Corporate Executive',
description: 'Professional, strategic, leadership-focused',
tone: 'professional',
voice: 'first_person',
vocabulary: 'advanced',
sentenceLength: 'medium',
emojiUsage: 'none',
hashtagStyle: 'topic_based',
structurePreference: 'mixed',
engagementStyle: 'data_driven',
},
stand_up_comedian: {
name: 'Stand-up Comedian',
description: 'Funny, self-deprecating, observational humor',
tone: 'humorous',
voice: 'first_person',
vocabulary: 'simple',
sentenceLength: 'varied',
emojiUsage: 'moderate',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'conversational',
},
thought_provocateur: {
name: 'Thought Provocateur',
description: 'Bold statements, contrarian views, challenges assumptions',
tone: 'provocative',
voice: 'first_person',
vocabulary: 'advanced',
sentenceLength: 'varied',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'provocative',
preferredPhrases: ['Unpopular opinion:', 'Hot take:', 'Everyone is wrong about:'],
},
zen_minimalist: {
name: 'Zen Minimalist',
description: 'Every word matters, no fluff, pure clarity',
tone: 'minimalist',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'bullets',
engagementStyle: 'educational',
},
};
@Injectable()
export class WritingStylesService {
private readonly logger = new Logger(WritingStylesService.name);
constructor(private readonly prisma: PrismaService) { }
/**
* Get all available tones
*/
getTones(): typeof WRITING_TONES {
return WRITING_TONES;
}
/**
* Get all preset styles
*/
getPresets(): typeof PRESET_STYLES {
return PRESET_STYLES;
}
/**
* Create a custom writing style
*/
async create(userId: string, config: WritingStyleConfig) {
return this.prisma.writingStyle.create({
data: {
userId,
name: config.name,
type: 'CUSTOM',
tone: config.tone,
vocabulary: Array.isArray(config.vocabulary) ? config.vocabulary : [config.vocabulary],
sentenceLength: config.sentenceLength,
emojiUsage: config.emojiUsage,
hashtagStyle: config.hashtagStyle,
structurePreference: config.structurePreference,
engagementStyle: config.engagementStyle,
signatureElements: config.signatureElements || [],
avoidWords: config.avoidPhrases || [],
preferredPhrases: config.preferredPhrases || [],
isDefault: false,
},
});
}
/**
* Get writing style by ID
*/
async getById(id: string) {
// Check if it's a preset
if (id.startsWith('preset-')) {
const presetKey = id.replace('preset-', '');
return PRESET_STYLES[presetKey] ? { id, ...PRESET_STYLES[presetKey] } : null;
}
return this.prisma.writingStyle.findUnique({
where: { id },
});
}
/**
* Get user's default writing style
*/
async getDefault(userId: string) {
const userDefault = await this.prisma.writingStyle.findFirst({
where: { userId, isDefault: true },
});
return userDefault || { id: 'preset-master_storyteller', ...PRESET_STYLES.master_storyteller };
}
/**
* Get all user's writing styles (custom + presets)
*/
async getAll(userId: string) {
const customStyles = await this.prisma.writingStyle.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
const presets = Object.entries(PRESET_STYLES).map(([key, style]) => ({
id: `preset-${key}`,
...style,
isPreset: true,
}));
return [...customStyles, ...presets];
}
/**
* Set default writing style
*/
async setDefault(userId: string, styleId: string) {
await this.prisma.writingStyle.updateMany({
where: { userId, isDefault: true },
data: { isDefault: false },
});
return this.prisma.writingStyle.update({
where: { id: styleId },
data: { isDefault: true },
});
}
/**
* Generate AI prompt for style
*/
generatePrompt(style: WritingStyleConfig, language?: ContentLanguage): string {
const toneInfo = WRITING_TONES[style.tone];
let prompt = `
WRITING STYLE INSTRUCTIONS:
${toneInfo.promptHint}
STYLE PARAMETERS:
- Tone: ${style.tone} (${toneInfo.description})
- Voice: ${style.voice.replace('_', ' ')}
- Vocabulary: ${style.vocabulary}
- Sentence length: ${style.sentenceLength}
- Emoji usage: ${style.emojiUsage}
- Structure: ${style.structurePreference}
- Engagement: ${style.engagementStyle}
`;
if (style.preferredPhrases?.length) {
prompt += `\n- Use phrases like: ${style.preferredPhrases.join(', ')}`;
}
if (style.avoidPhrases?.length) {
prompt += `\n- Avoid phrases: ${style.avoidPhrases.join(', ')}`;
}
if (language) {
prompt += `\n\nWRITE CONTENT IN: ${language}`;
}
return prompt.trim();
}
/**
* Apply writing style transformations
*/
applyStyle(content: string, style: WritingStyleConfig): string {
let styledContent = content;
if (style.emojiUsage === 'none') {
styledContent = styledContent.replace(/[\u{1F300}-\u{1F9FF}]/gu, '');
}
if (style.structurePreference === 'bullets') {
styledContent = this.convertToBullets(styledContent);
} else if (style.structurePreference === 'numbered') {
styledContent = this.convertToNumbered(styledContent);
}
return styledContent;
}
private convertToBullets(content: string): string {
const sentences = content.split(/(?<=[.!?])\s+/);
return sentences.map((s) => `${s.trim()}`).join('\n');
}
private convertToNumbered(content: string): string {
const sentences = content.split(/(?<=[.!?])\s+/);
return sentences.map((s, i) => `${i + 1}. ${s.trim()}`).join('\n');
}
}