generated from fahricansecer/boilerplate-be
496 lines
18 KiB
TypeScript
496 lines
18 KiB
TypeScript
// 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<string, string[]> = {
|
|
'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<string, string[]> = {
|
|
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<RewriteResult> {
|
|
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<RewriteResult[]> {
|
|
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<string, string> = {
|
|
'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<string, string> = {
|
|
"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);
|
|
}
|
|
}
|