// Content Rewriter Service - Plagiarism-free content rewriting // Path: src/modules/source-accounts/services/content-rewriter.service.ts import { Injectable, Logger } from '@nestjs/common'; export interface RewriteOptions { preserveTone: boolean; preserveStructure: boolean; targetPlatform?: string; style?: 'professional' | 'casual' | 'humorous' | 'educational'; length?: 'shorter' | 'same' | 'longer'; addPersonalization?: boolean; } export interface RewriteResult { original: string; rewritten: string; similarity: number; // 0-100, lower is more unique changes: ChangeDetail[]; suggestions: string[]; } export interface ChangeDetail { type: 'synonym' | 'restructure' | 'paraphrase' | 'expand' | 'condense'; original: string; replacement: string; reason: string; } @Injectable() export class ContentRewriterService { private readonly logger = new Logger(ContentRewriterService.name); // Synonym database for variety private readonly synonyms: Record = { 'important': ['crucial', 'essential', 'vital', 'significant', 'key'], 'great': ['excellent', 'outstanding', 'remarkable', 'exceptional', 'superb'], 'good': ['solid', 'strong', 'effective', 'valuable', 'beneficial'], 'bad': ['poor', 'weak', 'ineffective', 'problematic', 'flawed'], 'big': ['substantial', 'significant', 'major', 'considerable', 'massive'], 'small': ['minor', 'modest', 'limited', 'slight', 'minimal'], 'fast': ['quick', 'rapid', 'swift', 'speedy', 'prompt'], 'slow': ['gradual', 'steady', 'measured', 'deliberate', 'unhurried'], 'easy': ['simple', 'straightforward', 'effortless', 'seamless', 'accessible'], 'hard': ['challenging', 'difficult', 'demanding', 'complex', 'tough'], 'think': ['believe', 'consider', 'feel', 'reckon', 'suspect'], 'know': ['understand', 'recognize', 'realize', 'grasp', 'comprehend'], 'show': ['demonstrate', 'reveal', 'illustrate', 'display', 'exhibit'], 'make': ['create', 'build', 'develop', 'craft', 'produce'], 'get': ['obtain', 'acquire', 'gain', 'secure', 'achieve'], 'use': ['utilize', 'employ', 'leverage', 'apply', 'implement'], 'help': ['assist', 'support', 'aid', 'enable', 'facilitate'], 'need': ['require', 'must have', 'demand', 'call for', 'necessitate'], 'want': ['desire', 'wish', 'aim', 'seek', 'aspire'], 'start': ['begin', 'launch', 'initiate', 'commence', 'kick off'], 'stop': ['cease', 'halt', 'end', 'discontinue', 'pause'], 'change': ['transform', 'modify', 'alter', 'shift', 'evolve'], 'improve': ['enhance', 'boost', 'elevate', 'upgrade', 'optimize'], 'increase': ['grow', 'expand', 'rise', 'surge', 'escalate'], 'decrease': ['reduce', 'lower', 'diminish', 'drop', 'decline'], }; // Sentence starters for variety private readonly sentenceStarters: Record = { cause: ['Because', 'Since', 'As', 'Given that', 'Due to the fact'], contrast: ['However', 'On the other hand', 'Conversely', 'Yet', 'Despite this'], addition: ['Additionally', 'Furthermore', 'Moreover', 'Also', 'In addition'], example: ['For instance', 'For example', 'As an illustration', 'Consider', 'Take'], conclusion: ['Therefore', 'Thus', 'Consequently', 'As a result', 'In conclusion'], emphasis: ['Importantly', 'Notably', 'Significantly', 'Crucially', 'Essentially'], }; /** * Rewrite content to be unique while preserving meaning */ async rewrite( content: string, options: RewriteOptions = { preserveTone: true, preserveStructure: true, }, ): Promise { const changes: ChangeDetail[] = []; let rewritten = content; // Step 1: Replace common words with synonyms rewritten = this.applySynonyms(rewritten, changes); // Step 2: Restructure sentences if (!options.preserveStructure) { rewritten = this.restructureSentences(rewritten, changes); } // Step 3: Vary sentence starters rewritten = this.varySentenceStarters(rewritten, changes); // Step 4: Adjust length if requested if (options.length === 'shorter') { rewritten = this.condenseContent(rewritten, changes); } else if (options.length === 'longer') { rewritten = this.expandContent(rewritten, changes); } // Step 5: Platform adaptation if (options.targetPlatform) { rewritten = this.adaptForPlatform(rewritten, options.targetPlatform, changes); } // Step 6: Style adjustment if (options.style) { rewritten = this.adjustStyle(rewritten, options.style, changes); } // Calculate similarity const similarity = this.calculateSimilarity(content, rewritten); // Generate suggestions for further improvement const suggestions = this.generateSuggestions(similarity, changes); return { original: content, rewritten, similarity, changes, suggestions, }; } /** * Generate multiple variations */ async generateVariations( content: string, count: number = 3, ): Promise { const variations: RewriteResult[] = []; const styles: RewriteOptions['style'][] = ['professional', 'casual', 'educational']; for (let i = 0; i < count; i++) { const result = await this.rewrite(content, { preserveTone: false, preserveStructure: Math.random() > 0.5, style: styles[i % styles.length], }); variations.push(result); } return variations; } /** * Check plagiarism risk */ checkPlagiarismRisk( original: string, rewritten: string, ): { risk: 'low' | 'medium' | 'high'; similarity: number; flaggedPhrases: string[]; recommendations: string[]; } { const similarity = this.calculateSimilarity(original, rewritten); const flaggedPhrases = this.findMatchingPhrases(original, rewritten); let risk: 'low' | 'medium' | 'high'; if (similarity <= 30) risk = 'low'; else if (similarity <= 60) risk = 'medium'; else risk = 'high'; const recommendations: string[] = []; if (risk !== 'low') { recommendations.push('Consider rewording the flagged phrases'); recommendations.push('Add personal examples or anecdotes'); recommendations.push('Change the structure of key paragraphs'); if (flaggedPhrases.length > 3) { recommendations.push('The content needs more significant rewriting'); } } return { risk, similarity, flaggedPhrases, recommendations }; } /** * Generate AI prompt for advanced rewriting */ generateRewritePrompt( content: string, options: RewriteOptions, ): string { const styleGuide = { professional: 'Use formal, business-appropriate language', casual: 'Use conversational, friendly tone with contractions', humorous: 'Add wit and playful language', educational: 'Be clear, instructive, and methodical', }; return `Rewrite the following content to be completely unique while preserving the core message. ORIGINAL CONTENT: ${content} REQUIREMENTS: 1. ${options.preserveTone ? 'Maintain the original tone' : 'Adjust tone as needed'} 2. ${options.preserveStructure ? 'Keep similar structure' : 'Feel free to restructure'} 3. ${options.style ? styleGuide[options.style] : 'Match the original style'} 4. ${options.length === 'shorter' ? 'Make it 30% shorter' : options.length === 'longer' ? 'Expand with additional detail' : 'Similar length'} 5. ${options.targetPlatform ? `Optimize for ${options.targetPlatform}` : ''} 6. ${options.addPersonalization ? 'Add personal touches and [PERSONALIZATION_PLACEHOLDER] where relevant' : ''} IMPORTANT: - Do NOT copy phrases directly - Change sentence structures - Use different words with same meaning - Add value where possible - Make it feel original and authentic`; } // Private methods private applySynonyms(text: string, changes: ChangeDetail[]): string { let result = text; for (const [word, synonymList] of Object.entries(this.synonyms)) { const regex = new RegExp(`\\b${word}\\b`, 'gi'); const matches = text.match(regex); if (matches) { // Replace first occurrence with a synonym const synonym = synonymList[Math.floor(Math.random() * synonymList.length)]; result = result.replace(regex, (match) => { // Preserve case if (match[0] === match[0].toUpperCase()) { return synonym.charAt(0).toUpperCase() + synonym.slice(1); } return synonym; }); changes.push({ type: 'synonym', original: word, replacement: synonym, reason: 'Word variety for uniqueness', }); } } return result; } private restructureSentences(text: string, changes: ChangeDetail[]): string { const sentences = text.split(/(?<=[.!?])\s+/); const restructured: string[] = []; for (const sentence of sentences) { // Randomly restructure some sentences if (Math.random() > 0.6 && sentence.includes(',')) { // Move clause const clauses = sentence.split(','); if (clauses.length >= 2) { const reordered = [...clauses.slice(1), clauses[0]].join(', ').trim(); restructured.push(reordered); changes.push({ type: 'restructure', original: sentence, replacement: reordered, reason: 'Clause reordering for uniqueness', }); continue; } } restructured.push(sentence); } return restructured.join(' '); } private varySentenceStarters(text: string, changes: ChangeDetail[]): string { let result = text; // Find and vary consecutive sentence starters const starterPatterns = Object.entries(this.sentenceStarters); for (const [type, starters] of starterPatterns) { for (const starter of starters) { const regex = new RegExp(`^${starter}\\b`, 'gim'); if (result.match(regex)) { const alternatives = starters.filter((s) => s !== starter); const newStarter = alternatives[Math.floor(Math.random() * alternatives.length)]; result = result.replace(regex, newStarter); changes.push({ type: 'paraphrase', original: starter, replacement: newStarter, reason: 'Sentence starter variety', }); break; // Only change one per type } } } return result; } private condenseContent(text: string, changes: ChangeDetail[]): string { // Remove filler words const fillers = ['basically', 'actually', 'essentially', 'really', 'very', 'just']; let result = text; for (const filler of fillers) { const regex = new RegExp(`\\b${filler}\\b\\s*`, 'gi'); if (result.match(regex)) { result = result.replace(regex, ''); changes.push({ type: 'condense', original: filler, replacement: '', reason: 'Removed filler word', }); } } return result; } private expandContent(text: string, changes: ChangeDetail[]): string { // Add transitional phrases const sentences = text.split(/(?<=[.!?])\s+/); const expanded: string[] = []; for (let i = 0; i < sentences.length; i++) { if (i > 0 && Math.random() > 0.7) { const transitions = ['Moreover,', 'Additionally,', 'Furthermore,', 'In fact,']; const transition = transitions[Math.floor(Math.random() * transitions.length)]; expanded.push(transition + ' ' + sentences[i].toLowerCase()); changes.push({ type: 'expand', original: sentences[i], replacement: transition + ' ' + sentences[i].toLowerCase(), reason: 'Added transition for flow', }); } else { expanded.push(sentences[i]); } } return expanded.join(' '); } private adaptForPlatform( text: string, platform: string, changes: ChangeDetail[], ): string { let result = text; switch (platform.toLowerCase()) { case 'twitter': // Shorten if too long if (result.length > 280) { result = result.substring(0, 270) + '...'; changes.push({ type: 'condense', original: text, replacement: result, reason: 'Truncated for Twitter character limit', }); } break; case 'linkedin': // Add professional opening if missing if (!/^(I |We |The |In |At |As )/i.test(result)) { result = 'Here\'s something worth considering: ' + result; changes.push({ type: 'expand', original: '', replacement: 'Here\'s something worth considering:', reason: 'Added LinkedIn-style opener', }); } break; case 'instagram': // Add emoji breaks const sentences = result.split('. '); if (sentences.length > 3) { result = sentences.join('. \n\n'); changes.push({ type: 'restructure', original: text, replacement: result, reason: 'Added line breaks for Instagram readability', }); } break; } return result; } private adjustStyle( text: string, style: RewriteOptions['style'], changes: ChangeDetail[], ): string { let result = text; switch (style) { case 'casual': result = result.replace(/\b(is not|are not|do not|does not|cannot)\b/g, (match) => { const contractions: Record = { 'is not': "isn't", 'are not': "aren't", 'do not': "don't", 'does not': "doesn't", 'cannot': "can't", }; return contractions[match] || match; }); break; case 'professional': result = result.replace(/\b(isn't|aren't|don't|doesn't|can't)\b/g, (match) => { const expanded: Record = { "isn't": 'is not', "aren't": 'are not', "don't": 'do not', "doesn't": 'does not', "can't": 'cannot', }; return expanded[match] || match; }); break; } return result; } private calculateSimilarity(original: string, rewritten: string): number { const originalWords = original.toLowerCase().split(/\s+/); const rewrittenWords = rewritten.toLowerCase().split(/\s+/); const originalSet = new Set(originalWords); const rewrittenSet = new Set(rewrittenWords); let matching = 0; for (const word of rewrittenWords) { if (originalSet.has(word)) matching++; } const similarity = (matching / Math.max(originalWords.length, rewrittenWords.length)) * 100; return Math.round(similarity); } private findMatchingPhrases(original: string, rewritten: string): string[] { const flagged: string[] = []; const phrases = original.split(/[.!?]+/).filter((p) => p.trim().length > 0); for (const phrase of phrases) { const cleanPhrase = phrase.trim().toLowerCase(); if (cleanPhrase.length > 20 && rewritten.toLowerCase().includes(cleanPhrase)) { flagged.push(phrase.trim()); } } return flagged; } private generateSuggestions( similarity: number, changes: ChangeDetail[], ): string[] { const suggestions: string[] = []; if (similarity > 50) { suggestions.push('Consider rewriting longer phrases, not just individual words'); suggestions.push('Try restructuring the overall flow of arguments'); } if (changes.filter((c) => c.type === 'synonym').length < 5) { suggestions.push('More synonym replacements would increase uniqueness'); } if (!changes.some((c) => c.type === 'restructure')) { suggestions.push('Consider restructuring some sentences for variety'); } suggestions.push('Adding personal examples or data points increases originality'); suggestions.push('Consider adding your unique perspective or angle'); return suggestions.slice(0, 4); } }