Files
Content-Hunter_BE/src/modules/source-accounts/services/content-rewriter.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

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