generated from fahricansecer/boilerplate-be
This commit is contained in:
424
src/modules/content/services/writing-styles.service.ts
Normal file
424
src/modules/content/services/writing-styles.service.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user