ai-text-detector
Version:
A lightweight, fast JavaScript/TypeScript library for detecting AI-generated text using advanced linguistic analysis. Works in Node.js, React, and browser environments with zero dependencies.
1,078 lines • 50.8 kB
JavaScript
class AITextDetector {
constructor() {
// Common words for frequency analysis
this.commonWords = new Set([
"the",
"be",
"to",
"of",
"and",
"a",
"in",
"that",
"have",
"i",
"it",
"for",
"not",
"on",
"with",
"he",
"as",
"you",
"do",
"at",
"this",
"but",
"his",
"by",
"from",
"they",
"she",
"or",
"an",
"will",
"my",
"one",
"all",
"would",
"there",
"their",
]);
// AI-typical phrases and patterns (expanded)
this.aiPatterns = [
/\b(it is important to note|it should be noted|it is worth mentioning|it is crucial to understand)\b/gi,
/\b(furthermore|moreover|additionally|consequently|therefore|thus|hence|nonetheless|nevertheless)\b/gi,
/\b(in conclusion|to summarize|in summary|overall|ultimately|essentially)\b/gi,
/\b(various|numerous|several|multiple|different|diverse|wide range of)\b/gi,
/\b(enhance|optimize|facilitate|utilize|implement|establish|maintain|ensure)\b/gi,
/\b(significant|substantial|considerable|notable|remarkable|extensive)\b/gi,
/\b(comprehensive|thorough|detailed|in-depth|multifaceted)\b/gi,
/\b(approach|strategy|methodology|framework|process|procedure)\b/gi,
// New AI patterns based on recent models
/\b(it's worth noting|it's important to understand|it's crucial to consider)\b/gi,
/\b(as we delve into|let's explore|let's examine|it's clear that)\b/gi,
/\b(in today's|in our modern|in the current|in this digital age)\b/gi,
/\b(revolutionize|transform|streamline|cutting-edge|state-of-the-art)\b/gi,
/\b(stakeholders|end-users|best practices|value proposition|synergistic)\b/gi,
/\b(leverage the power of|harness the potential|unlock the benefits)\b/gi,
];
// Human-like patterns and indicators
this.humanPatterns = [
/\b(lol|lmao|omg|wtf|btw|tbh|imho|imo)\b/gi,
/\b(gonna|wanna|gotta|kinda|sorta|dunno)\b/gi,
/\b(yeah|yep|nah|nope|meh|ugh|hmm)\b/gi,
/\b(super|really|pretty|kinda|totally|absolutely)\b/gi,
/\b(awesome|amazing|terrible|awful|weird|crazy)\b/gi,
];
// Emotional expressions
this.emotionalMarkers = [
/\b(love|hate|excited|frustrated|angry|happy|sad|worried|anxious)\b/gi,
/\b(feel|felt|feeling|emotions|emotional|mood)\b/gi,
/(!{2,}|\?{2,}|\.{3,})/g, // Multiple punctuation marks
/[A-Z]{2,}/g, // CAPS for emphasis
];
// Discourse markers for sophisticated analysis
this.discourseMarkers = [
"first",
"second",
"third",
"finally",
"lastly",
"initially",
"subsequently",
"meanwhile",
"simultaneously",
"on the other hand",
"in contrast",
"however",
"nevertheless",
"for instance",
"for example",
"such as",
"namely",
"in fact",
"indeed",
"actually",
"certainly",
"admittedly",
"granted",
"of course",
"naturally",
];
// Function words for stylometric analysis
this.functionWords = [
"the",
"be",
"to",
"of",
"and",
"a",
"in",
"that",
"have",
"i",
"it",
"for",
"not",
"on",
"with",
"he",
"as",
"you",
"do",
"at",
"this",
"but",
"his",
"by",
"from",
"they",
"she",
"or",
"an",
"will",
"my",
"one",
"all",
"would",
"there",
"their",
"what",
"so",
"up",
"out",
"if",
"about",
"who",
"get",
"which",
"go",
"me",
"when",
"make",
"can",
"like",
"time",
"no",
"just",
"him",
"know",
"take",
"people",
"into",
"year",
"your",
"good",
"some",
"could",
"them",
"see",
"other",
"than",
"then",
"now",
"look",
"only",
"come",
"its",
"over",
"think",
"also",
"back",
"after",
"use",
"two",
"how",
"our",
"work",
"first",
"well",
"way",
"even",
"new",
"want",
"because",
"any",
"these",
"give",
"day",
"most",
"us",
];
// Transition words for density analysis
this.transitionWords = [
"however",
"furthermore",
"moreover",
"additionally",
"consequently",
"therefore",
"thus",
"hence",
"nevertheless",
"nonetheless",
"meanwhile",
"subsequently",
"ultimately",
"essentially",
"specifically",
"particularly",
"notably",
"importantly",
"significantly",
"interestingly",
"surprisingly",
"accordingly",
"alternatively",
"comparatively",
"conversely",
"similarly",
"likewise",
"meanwhile",
"simultaneously",
];
// Sophisticated vocabulary often used by AI
this.sophisticatedWords = [
"utilize",
"facilitate",
"demonstrate",
"implement",
"establish",
"maintain",
"require",
"appropriate",
"significant",
"considerable",
"substantial",
"comprehensive",
"extensive",
"innovative",
"strategic",
"optimize",
"enhance",
"leverage",
"paradigm",
"methodology",
"framework",
"initiative",
"synergy",
];
}
detectAIText(text) {
if (!text || text.trim().length === 0) {
throw new Error("Text cannot be empty");
}
if (text.trim().length < 50) {
throw new Error("Text too short for reliable analysis (minimum 50 characters)");
}
const metrics = this.analyzeText(text);
const score = this.calculateAdvancedAIScore(metrics);
// More balanced threshold - conservative but not too much
let threshold = 0.58; // Slightly lower base threshold
// Adjust threshold based on text length
const wordCount = this.tokenizeWords(text).length;
if (wordCount < 100) {
threshold += 0.04; // Less adjustment for shorter texts
}
else if (wordCount > 300) {
threshold -= 0.02; // Slightly more sensitive for longer texts
}
// Detect narrative/literary writing patterns - ONLY for highly narrative text
const narrativeScore = this.calculateNarrativeScore(text);
if (narrativeScore > 0.5) {
threshold += 0.15; // Much more conservative for clearly narrative text
}
else if (narrativeScore > 0.35) {
threshold += 0.08; // Moderate adjustment for somewhat narrative text
}
// Adjust based on detected human patterns - be more selective
if (metrics.humanLikenessIndicators > 0.6) {
threshold += 0.2; // Strong adjustment for very human text
}
else if (metrics.humanLikenessIndicators > 0.4) {
threshold += 0.12; // Moderate adjustment for clearly human text
}
else if (metrics.humanLikenessIndicators > 0.2) {
threshold += 0.05; // Small adjustment for somewhat human text
}
if (metrics.informalnessScore > 0.5) {
threshold += 0.15; // Strong adjustment for very informal text
}
else if (metrics.informalnessScore > 0.3) {
threshold += 0.08; // Moderate adjustment for informal text
}
else if (metrics.informalnessScore > 0.15) {
threshold += 0.03; // Small adjustment for somewhat informal text
}
if (metrics.emotionalToneVariability > 0.4) {
threshold += 0.12; // Adjustment for very emotional text
}
else if (metrics.emotionalToneVariability > 0.2) {
threshold += 0.06; // Small adjustment for somewhat emotional text
}
// Check for creative writing indicators - be more selective
const creativityScore = this.calculateCreativityScore(text);
if (creativityScore > 0.5) {
threshold += 0.2; // Strong boost for very creative writing
}
else if (creativityScore > 0.35) {
threshold += 0.1; // Moderate boost for creative writing
}
const isAIGenerated = score > threshold;
const confidence = Math.round(score * 100) / 100;
const reasons = this.generateDetailedReasons(metrics, score);
return {
isAIGenerated,
confidence,
reasons,
score,
perplexityScore: metrics.perplexity,
burstinessScore: metrics.burstiness,
};
}
analyzeText(text) {
const sentences = this.splitIntoSentences(text);
const words = this.tokenizeWords(text);
const cleanWords = words.filter((word) => word.length > 2);
return {
perplexity: this.calculatePerplexity(words),
burstiness: this.calculateBurstiness(sentences),
averageWordsPerSentence: this.calculateAverageWordsPerSentence(sentences),
sentenceVariability: this.calculateSentenceVariability(sentences),
lexicalDiversity: this.calculateLexicalDiversity(cleanWords),
readabilityScore: this.calculateReadabilityScore(text, sentences, words),
syntacticComplexity: this.calculateSyntacticComplexity(sentences),
semanticCoherence: this.calculateSemanticCoherence(sentences),
nGramRepetition: this.calculateNGramRepetition(words),
punctuationPatterns: this.analyzePunctuationPatterns(text),
wordFrequencyDistribution: this.analyzeWordFrequencyDistribution(cleanWords),
transitionDensity: this.calculateTransitionDensity(cleanWords),
formalityIndex: this.calculateFormalityIndex(cleanWords),
vocabularyRichness: this.calculateVocabularyRichness(cleanWords),
contextualConsistency: this.calculateContextualConsistency(sentences),
// Enhanced metrics
entropyScore: this.calculateEntropyScore(words),
humanLikenessIndicators: this.calculateHumanLikenessIndicators(text),
emotionalToneVariability: this.calculateEmotionalToneVariability(text),
discourseMarkerPatterns: this.calculateDiscourseMarkerPatterns(cleanWords),
functionWordAnalysis: this.calculateFunctionWordAnalysis(cleanWords),
informalnessScore: this.calculateInformalnessScore(text),
sentenceStructureEntropy: this.calculateSentenceStructureEntropy(sentences),
topicCoherenceScore: this.calculateTopicCoherenceScore(sentences),
bigramUnusualness: this.calculateBigramUnusualness(words),
stylometricSignature: this.calculateStylometricSignature(text, sentences, words),
};
}
tokenizeWords(text) {
return text
.toLowerCase()
.replace(/[^\w\s'-]/g, " ")
.split(/\s+/)
.filter((word) => word.length > 0);
}
splitIntoSentences(text) {
return text
.split(/[.!?]+/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
// Enhanced perplexity calculation with improved statistical modeling
calculatePerplexity(words) {
if (words.length < 3)
return 10; // Default high perplexity for very short texts
const wordCounts = new Map();
const bigramCounts = new Map();
const trigramCounts = new Map();
// Count unigrams
words.forEach((word) => {
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
});
// Count bigrams
for (let i = 0; i < words.length - 1; i++) {
const bigram = `${words[i]} ${words[i + 1]}`;
bigramCounts.set(bigram, (bigramCounts.get(bigram) || 0) + 1);
}
// Count trigrams
for (let i = 0; i < words.length - 2; i++) {
const trigram = `${words[i]} ${words[i + 1]} ${words[i + 2]}`;
trigramCounts.set(trigram, (trigramCounts.get(trigram) || 0) + 1);
}
let totalLogProb = 0;
let totalPredictions = 0;
// Calculate log probability using interpolated n-gram model
for (let i = 2; i < words.length; i++) {
const currentWord = words[i];
const prevWord = words[i - 1];
const prevPrevWord = words[i - 2];
const trigram = `${prevPrevWord} ${prevWord} ${currentWord}`;
const bigram = `${prevWord} ${currentWord}`;
const prevBigram = `${prevPrevWord} ${prevWord}`;
const trigramFreq = trigramCounts.get(trigram) || 0;
const bigramFreq = bigramCounts.get(bigram) || 0;
const prevBigramFreq = bigramCounts.get(prevBigram) || 0;
const wordFreq = wordCounts.get(currentWord) || 0;
// Interpolated probability with smoothing
let probability = 0;
// Trigram probability
if (prevBigramFreq > 0) {
probability +=
(0.6 * (trigramFreq + 0.1)) /
(prevBigramFreq + 0.1 * trigramCounts.size);
}
// Bigram probability
const prevWordFreq = wordCounts.get(prevWord) || 0;
if (prevWordFreq > 0) {
probability +=
(0.3 * (bigramFreq + 0.1)) / (prevWordFreq + 0.1 * bigramCounts.size);
}
// Unigram probability
probability +=
(0.1 * (wordFreq + 0.1)) / (words.length + 0.1 * wordCounts.size);
// Ensure minimum probability
probability = Math.max(probability, 0.0001);
totalLogProb += Math.log2(probability);
totalPredictions++;
}
// Calculate perplexity
const averageLogProb = totalLogProb / Math.max(totalPredictions, 1);
return Math.pow(2, -averageLogProb);
}
// Burstiness measures variation in sentence lengths
calculateBurstiness(sentences) {
if (sentences.length < 2)
return 0;
const lengths = sentences.map((s) => s.split(/\s+/).length);
const mean = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - mean, 2), 0) /
lengths.length;
const stdDev = Math.sqrt(variance);
// Burstiness formula: (σ - μ) / (σ + μ)
return (stdDev - mean) / (stdDev + mean);
}
calculateLexicalDiversity(words) {
const uniqueWords = new Set(words);
return uniqueWords.size / words.length;
}
calculateReadabilityScore(text, sentences, words) {
const avgWordsPerSentence = words.length / sentences.length;
const complexWords = words.filter((word) => word.length > 6).length;
const complexWordRatio = complexWords / words.length;
// Simplified Flesch-Kincaid-like formula
return 206.835 - 1.015 * avgWordsPerSentence - 84.6 * complexWordRatio;
}
calculateSyntacticComplexity(sentences) {
let totalComplexity = 0;
sentences.forEach((sentence) => {
const words = sentence.split(/\s+/);
let complexity = 0;
// Count subordinate clauses (simplified)
complexity += (sentence.match(/\b(that|which|who|whom|whose|when|where|while|although|because|since|if|unless|until)\b/gi) || []).length;
// Count conjunctions
complexity += (sentence.match(/\b(and|but|or|yet|so|for|nor)\b/gi) || [])
.length;
// Penalize very long sentences
if (words.length > 30)
complexity += 2;
if (words.length > 40)
complexity += 3;
totalComplexity += complexity / Math.max(words.length, 1);
});
return totalComplexity / sentences.length;
}
calculateSemanticCoherence(sentences) {
if (sentences.length < 2)
return 1;
let coherenceScore = 0;
for (let i = 1; i < sentences.length; i++) {
const prevWords = new Set(this.tokenizeWords(sentences[i - 1]));
const currWords = new Set(this.tokenizeWords(sentences[i]));
// Calculate word overlap between consecutive sentences
const intersection = new Set([...prevWords].filter((x) => currWords.has(x)));
const union = new Set([...prevWords, ...currWords]);
coherenceScore += intersection.size / union.size;
}
return coherenceScore / (sentences.length - 1);
}
calculateNGramRepetition(words) {
const trigrams = new Map();
for (let i = 0; i < words.length - 2; i++) {
const trigram = `${words[i]} ${words[i + 1]} ${words[i + 2]}`;
trigrams.set(trigram, (trigrams.get(trigram) || 0) + 1);
}
const repeatedTrigrams = Array.from(trigrams.values()).filter((count) => count > 1);
return repeatedTrigrams.length / Math.max(trigrams.size, 1);
}
analyzePunctuationPatterns(text) {
const punctuation = text.match(/[.!?;:,]/g) || [];
const words = this.tokenizeWords(text);
if (words.length === 0)
return 0;
// AI often has consistent punctuation patterns
const punctuationRatio = punctuation.length / words.length;
const commaRatio = (text.match(/,/g) || []).length / words.length;
const semicolonRatio = (text.match(/;/g) || []).length / words.length;
// AI tends to use moderate punctuation
let score = 0;
if (punctuationRatio > 0.05 && punctuationRatio < 0.15)
score += 0.3;
if (commaRatio > 0.02 && commaRatio < 0.08)
score += 0.3;
if (semicolonRatio > 0.001 && semicolonRatio < 0.01)
score += 0.2;
return score;
}
analyzeWordFrequencyDistribution(words) {
const wordCounts = new Map();
words.forEach((word) => {
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
});
const frequencies = Array.from(wordCounts.values()).sort((a, b) => b - a);
// Zipf's law analysis - natural text follows specific distribution
let zipfScore = 0;
for (let i = 1; i < Math.min(frequencies.length, 10); i++) {
const expected = frequencies[0] / (i + 1);
const actual = frequencies[i];
const ratio = Math.min(actual, expected) / Math.max(actual, expected);
zipfScore += ratio;
}
return zipfScore / Math.min(frequencies.length - 1, 9);
}
calculateTransitionDensity(words) {
const transitionCount = words.filter((word) => this.transitionWords.some((tw) => word.includes(tw))).length;
return (transitionCount / words.length) * 100;
}
calculateFormalityIndex(words) {
const sophisticatedCount = words.filter((word) => this.sophisticatedWords.includes(word)).length;
const commonWordCount = words.filter((word) => this.commonWords.has(word)).length;
return (sophisticatedCount /
words.length /
Math.max(commonWordCount / words.length, 0.1));
}
calculateVocabularyRichness(words) {
const uniqueWords = new Set(words);
const hapaxLegomena = Array.from(uniqueWords).filter((word) => words.filter((w) => w === word).length === 1);
return hapaxLegomena.length / uniqueWords.size;
}
calculateContextualConsistency(sentences) {
if (sentences.length < 3)
return 1;
let consistencyScore = 0;
const topics = sentences.map((sentence) => this.extractTopicWords(sentence));
for (let i = 1; i < topics.length - 1; i++) {
const prevTopics = new Set(topics[i - 1]);
const currTopics = new Set(topics[i]);
const nextTopics = new Set(topics[i + 1]);
const allTopics = new Set([...prevTopics, ...currTopics, ...nextTopics]);
const commonTopics = [...allTopics].filter((topic) => [prevTopics, currTopics, nextTopics].filter((set) => set.has(topic))
.length >= 2);
consistencyScore += commonTopics.length / Math.max(allTopics.size, 1);
}
return consistencyScore / Math.max(sentences.length - 2, 1);
}
extractTopicWords(sentence) {
const words = this.tokenizeWords(sentence);
return words.filter((word) => word.length > 4 &&
!this.commonWords.has(word) &&
!this.transitionWords.includes(word));
}
calculateAverageWordsPerSentence(sentences) {
if (sentences.length === 0)
return 0;
const totalWords = sentences.reduce((sum, sentence) => sum + sentence.split(/\s+/).length, 0);
return totalWords / sentences.length;
}
calculateSentenceVariability(sentences) {
if (sentences.length < 2)
return 0;
const lengths = sentences.map((s) => s.split(/\s+/).length);
const average = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - average, 2), 0) /
lengths.length;
return Math.sqrt(variance);
}
// Enhanced calculation methods for improved detection
calculateEntropyScore(words) {
const wordCounts = new Map();
words.forEach((word) => {
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
});
const totalWords = words.length;
let entropy = 0;
for (const count of wordCounts.values()) {
const probability = count / totalWords;
entropy -= probability * Math.log2(probability);
}
// Normalize entropy (higher entropy = more human-like)
return entropy / Math.log2(Math.min(wordCounts.size, totalWords));
}
calculateHumanLikenessIndicators(text) {
let score = 0;
let totalIndicators = 0;
// Check for informal language (enhanced patterns)
const informalMatches = this.humanPatterns.reduce((count, pattern) => {
return count + (text.match(pattern) || []).length;
}, 0);
score += Math.min(informalMatches / 3, 1); // Increased sensitivity
totalIndicators++;
// Check for contractions (very human-like)
const contractions = (text.match(/\b\w+[''](?:t|re|ve|ll|d|s|m)\b/gi) || [])
.length;
score += Math.min(contractions / 5, 1); // More sensitive
totalIndicators++;
// Check for typos and misspellings
const potentialTypos = text.match(/\b[a-z]*[aeiou]{3,}[a-z]*\b/gi) || [];
const doubleLetters = text.match(/\b\w*([a-z])\1{2,}\w*\b/gi) || [];
const inconsistentSpacing = text.match(/\s{2,}/g) || [];
score += Math.min((potentialTypos.length +
doubleLetters.length +
inconsistentSpacing.length) /
5, // More sensitive
1);
totalIndicators++;
// Check for personal pronouns and narrative style
const personalPronouns = (text.match(/\b(I|me|my|mine|myself|we|us|our|ours)\b/gi) || []).length;
const words = this.tokenizeWords(text);
score += Math.min(personalPronouns / Math.max(words.length * 0.05, 1), 1); // More sensitive
totalIndicators++;
// Check for emotional punctuation (very human)
const emotionalPunct = (text.match(/[!]{2,}|[?]{2,}|[.]{3,}/g) || [])
.length;
score += Math.min(emotionalPunct / 3, 1); // More sensitive
totalIndicators++;
// Check for ALL CAPS words (emphasis) - very human
const capsWords = (text.match(/\b[A-Z]{2,}\b/g) || []).length;
score += Math.min(capsWords / 5, 1); // More sensitive
totalIndicators++;
// Check for internet slang and abbreviations (very human)
const internetSlang = (text.match(/\b(lol|lmao|omg|wtf|btw|tbh|imho|imo|ngl|smh|fml|irl|rn|af|fr|periodt|idk|ikr|brb|ttyl|dm|pm|sus|lit|fam|bae|goat|facts|no cap|bet|vibe|mood|periodt)\b/gi) || []).length;
score += Math.min(internetSlang / 2, 1); // Very sensitive to slang
totalIndicators++;
// Check for incomplete sentences or fragments (human-like)
const sentences = this.splitIntoSentences(text);
const fragments = sentences.filter((s) => {
const words = s.trim().split(/\s+/);
return (words.length < 4 &&
!words.some((w) => w.match(/^(yes|no|ok|okay|yeah|nah|sure|maybe|absolutely|definitely)$/i)));
}).length;
score += Math.min(fragments / Math.max(sentences.length * 0.3, 1), 0.8);
totalIndicators++; // Check for conversational markers (very human)
const conversationalMarkers = (text.match(/\b(like|you know|I mean|right|so|well|um|uh|actually|basically|literally|honestly|seriously|obviously|apparently|supposedly|kinda|sorta|maybe|probably|definitely|absolutely|totally|completely|exactly|precisely)\b/gi) || []).length;
score += Math.min(conversationalMarkers / Math.max(words.length * 0.1, 1), 1);
totalIndicators++;
// NEW: Check for creative/descriptive language (narrative human writing)
const creativeDescriptions = (text.match(/\b(nearly twice|hardly any|very large|came in very useful|no finer|big beefy|which made|although he did|spent so much|craning over|spying on)\b/gi) || []).length;
score += Math.min(creativeDescriptions / 3, 1);
totalIndicators++;
// NEW: Check for character names and storytelling elements
const narrativeElements = (text.match(/\b(Mr\.|Mrs\.|called|named|director|firm|son|opinion|neighbors|mustache|blonde)\b/gi) || []).length;
score += Math.min(narrativeElements / 5, 0.8);
totalIndicators++;
// NEW: Check for narrative pronouns (third person storytelling)
const narrativePronouns = (text.match(/\b(he|she|they|him|her|them|his|hers|their|theirs)\b/gi) || []).length;
score += Math.min(narrativePronouns / Math.max(words.length * 0.08, 1), 0.7);
totalIndicators++;
return score / totalIndicators;
}
calculateEmotionalToneVariability(text) {
let emotionalMarkers = 0;
this.emotionalMarkers.forEach((pattern) => {
emotionalMarkers += (text.match(pattern) || []).length;
});
// Additional emotional indicators
const exclamations = (text.match(/!/g) || []).length;
const questions = (text.match(/\?/g) || []).length;
const emotionalWords = (text.match(/\b(love|hate|excited|frustrated|angry|happy|sad|worried|anxious|amazing|terrible|awesome|awful|horrible|wonderful|fantastic|disgusting|annoying|brilliant|stupid|crazy|insane|wild|mad|furious|thrilled|devastated|shocked|surprised|confused|overwhelmed)\b/gi) || []).length;
const words = this.tokenizeWords(text);
const totalEmotionalSignals = emotionalMarkers + exclamations + questions + emotionalWords;
return Math.min(totalEmotionalSignals / Math.max(words.length * 0.1, 1), 1);
}
calculateDiscourseMarkerPatterns(words) {
const discourseMarkerCount = words.filter((word) => this.discourseMarkers.some((marker) => marker.toLowerCase().includes(word.toLowerCase()))).length;
// AI tends to overuse discourse markers
const density = discourseMarkerCount / words.length;
return Math.min(density * 50, 1);
}
calculateFunctionWordAnalysis(words) {
const functionWordCount = words.filter((word) => this.functionWords.includes(word.toLowerCase())).length;
const ratio = functionWordCount / words.length;
// Natural human text typically has 40-60% function words
// AI often deviates from this pattern
if (ratio >= 0.4 && ratio <= 0.6) {
return 0.2; // Lower score for human-like ratio
}
else {
return Math.min(Math.abs(ratio - 0.5) * 2, 1);
}
}
calculateInformalnessScore(text) {
let informalityScore = 0;
let totalFeatures = 0;
// Contractions (very informal)
const contractions = (text.match(/\b\w+[''](?:t|re|ve|ll|d|s|m)\b/gi) || [])
.length;
const words = this.tokenizeWords(text);
informalityScore += Math.min(contractions / Math.max(words.length * 0.1, 1), 1);
totalFeatures++;
// Slang and colloquialisms (very informal)
const slangCount = this.humanPatterns.reduce((count, pattern) => {
return count + (text.match(pattern) || []).length;
}, 0);
informalityScore += Math.min(slangCount / 5, 1); // More sensitive
totalFeatures++;
// Sentence fragments (informal)
const sentences = this.splitIntoSentences(text);
const fragments = sentences.filter((s) => s.split(/\s+/).length < 4).length;
informalityScore += Math.min(fragments / Math.max(sentences.length * 0.4, 1), 1);
totalFeatures++;
// Ellipses and multiple punctuation (informal)
const multiplePunct = (text.match(/[.!?]{2,}/g) || []).length;
informalityScore += Math.min(multiplePunct / 5, 1); // More sensitive
totalFeatures++;
// Conversational words (informal)
const conversationalWords = (text.match(/\b(like|you know|I mean|right|so|well|um|uh|actually|basically|literally|honestly|seriously|obviously|apparently|kinda|sorta|gonna|wanna|gotta)\b/gi) || []).length;
informalityScore += Math.min(conversationalWords / Math.max(words.length * 0.05, 1), 1);
totalFeatures++;
// Lowercase sentence beginnings (very informal)
const lowercaseStarts = sentences.filter((s) => {
const trimmed = s.trim();
return (trimmed.length > 0 &&
trimmed[0] === trimmed[0].toLowerCase() &&
trimmed[0].match(/[a-z]/));
}).length;
informalityScore += Math.min(lowercaseStarts / Math.max(sentences.length * 0.3, 1), 1);
totalFeatures++;
// Run-on sentences with "and" (informal)
const runOnSentences = sentences.filter((s) => {
const andCount = (s.match(/\band\b/gi) || []).length;
const wordCount = s.split(/\s+/).length;
return andCount > 2 && wordCount > 20;
}).length;
informalityScore += Math.min(runOnSentences / Math.max(sentences.length * 0.5, 1), 0.8);
totalFeatures++;
return informalityScore / totalFeatures;
}
calculateSentenceStructureEntropy(sentences) {
const structures = sentences.map((sentence) => {
const words = sentence.split(/\s+/);
const length = words.length;
// Classify sentence structure
if (length <= 5)
return "short";
if (length <= 15)
return "medium";
if (length <= 25)
return "long";
return "very_long";
});
const structureCounts = new Map();
structures.forEach((structure) => {
structureCounts.set(structure, (structureCounts.get(structure) || 0) + 1);
});
let entropy = 0;
const totalSentences = sentences.length;
for (const count of structureCounts.values()) {
const probability = count / totalSentences;
entropy -= probability * Math.log2(probability);
}
return entropy / Math.log2(Math.min(structureCounts.size, totalSentences));
}
calculateTopicCoherenceScore(sentences) {
if (sentences.length < 2)
return 1;
const topicWords = sentences.map((sentence) => this.extractTopicWords(sentence));
let coherenceSum = 0;
let comparisons = 0;
for (let i = 0; i < topicWords.length - 1; i++) {
for (let j = i + 1; j < Math.min(i + 4, topicWords.length); j++) {
const words1 = new Set(topicWords[i]);
const words2 = new Set(topicWords[j]);
const intersection = new Set([...words1].filter((x) => words2.has(x)));
const union = new Set([...words1, ...words2]);
const similarity = intersection.size / Math.max(union.size, 1);
coherenceSum += similarity;
comparisons++;
}
}
return comparisons > 0 ? coherenceSum / comparisons : 0;
}
calculateBigramUnusualness(words) {
const bigramCounts = new Map();
const totalBigrams = words.length - 1;
// Count bigrams
for (let i = 0; i < words.length - 1; i++) {
const bigram = `${words[i]} ${words[i + 1]}`;
bigramCounts.set(bigram, (bigramCounts.get(bigram) || 0) + 1);
}
// Calculate unusualness based on expected frequency
let unusualness = 0;
bigramCounts.forEach((count, bigram) => {
const [word1, word2] = bigram.split(" ");
const word1Count = words.filter((w) => w === word1).length;
const word2Count = words.filter((w) => w === word2).length;
// Expected frequency based on individual word frequencies
const expectedFreq = (word1Count * word2Count) / words.length;
const actualFreq = count;
if (actualFreq > expectedFreq * 2) {
unusualness += actualFreq / totalBigrams;
}
});
return Math.min(unusualness, 1);
}
calculateStylometricSignature(text, sentences, words) {
let signature = 0;
let components = 0;
// Average sentence length variability
const sentenceLengths = sentences.map((s) => s.split(/\s+/).length);
const avgLength = sentenceLengths.reduce((a, b) => a + b, 0) / sentenceLengths.length;
const lengthVariance = sentenceLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / sentenceLengths.length;
signature += Math.min(Math.sqrt(lengthVariance) / avgLength, 1);
components++;
// Word length distribution
const wordLengths = words.map((w) => w.length);
const avgWordLength = wordLengths.reduce((a, b) => a + b, 0) / wordLengths.length;
const wordLengthVariance = wordLengths.reduce((sum, len) => sum + Math.pow(len - avgWordLength, 2), 0) / wordLengths.length;
signature += Math.min(Math.sqrt(wordLengthVariance) / avgWordLength, 1);
components++;
// Punctuation variety
const punctuationTypes = new Set(text.match(/[.!?;:,\-()]/g) || []);
signature += Math.min(punctuationTypes.size / 8, 1);
components++;
// Sentence beginning variety
const sentenceBeginnings = sentences
.map((s) => {
const firstWord = s.trim().split(/\s+/)[0];
return firstWord ? firstWord.toLowerCase() : "";
})
.filter((w) => w.length > 0);
const uniqueBeginnings = new Set(sentenceBeginnings);
signature += Math.min(uniqueBeginnings.size / sentenceBeginnings.length, 1);
components++;
return signature / components;
}
calculateAdvancedAIScore(metrics) {
let score = 0;
// Focus on the most discriminative metrics with proper weighting
// 1. Human-likeness indicators (MOST IMPORTANT - inverse scoring)
const humanScore = 1 - metrics.humanLikenessIndicators;
score += humanScore * 0.25; // Reduced from 0.30
// 2. Informality score (VERY IMPORTANT - inverse scoring)
const formalityScore = 1 - metrics.informalnessScore;
score += formalityScore * 0.2; // Reduced from 0.25
// 3. Emotional tone variability (IMPORTANT - inverse scoring)
const emotionalScore = 1 - Math.min(metrics.emotionalToneVariability, 1);
score += emotionalScore * 0.15; // Reduced from 0.20 // 4. Perplexity analysis (more balanced)
let perplexityScore = 0;
if (metrics.perplexity < 2) {
perplexityScore = 1; // Very AI-like
}
else if (metrics.perplexity < 4) {
perplexityScore = 0.8; // Likely AI
}
else if (metrics.perplexity < 7) {
perplexityScore = 0.5; // Uncertain - could be formal human writing
}
else if (metrics.perplexity < 12) {
perplexityScore = 0.2; // Likely human
}
else {
perplexityScore = 0.05; // Very likely human
}
score += perplexityScore * 0.18; // Reduced from 0.20
// 5. Burstiness analysis (increased weight)
let burstinessScore = 0;
if (metrics.burstiness < -0.5) {
burstinessScore = 0.9; // Very consistent = AI-like
}
else if (metrics.burstiness < 0) {
burstinessScore = 0.6; // Somewhat consistent = possibly AI
}
else if (metrics.burstiness < 0.3) {
burstinessScore = 0.3; // Some variation = possibly human
}
else {
burstinessScore = 0.1; // High variation = likely human
}
score += burstinessScore * 0.15; // Increased from 0.10
// 6. Add some additional AI indicators with smaller weights
// Transition density (AI overuses transitions)
if (metrics.transitionDensity > 2) {
score += Math.min(metrics.transitionDensity / 10, 0.1) * 0.05;
}
// Apply adaptive adjustments based on strong human indicators
score = this.applyAdaptiveThresholding(score, metrics);
// Ensure score is between 0 and 1
return Math.max(0, Math.min(1, score));
}
applyAdaptiveThresholding(baseScore, metrics) {
let adjustedScore = baseScore;
// Strong human indicators should significantly reduce AI probability
if (metrics.humanLikenessIndicators > 0.6) {
adjustedScore *= 0.2; // Very strong reduction for very human text
}
else if (metrics.humanLikenessIndicators > 0.4) {
adjustedScore *= 0.4; // Strong reduction for clearly human text
}
else if (metrics.humanLikenessIndicators > 0.2) {
adjustedScore *= 0.7; // Moderate reduction for somewhat human text
}
// High informality should reduce AI probability
if (metrics.informalnessScore > 0.6) {
adjustedScore *= 0.3; // Strong reduction for very informal text
}
else if (metrics.informalnessScore > 0.4) {
adjustedScore *= 0.5; // Moderate reduction for informal text
}
else if (metrics.informalnessScore > 0.2) {
adjustedScore *= 0.8; // Light reduction for somewhat informal text
}
// High emotional variability should reduce AI probability
if (metrics.emotionalToneVariability > 0.5) {
adjustedScore *= 0.4; // Strong reduction for very emotional text
}
else if (metrics.emotionalToneVariability > 0.3) {
adjustedScore *= 0.6; // Moderate reduction for emotional text
}
// Multiple strong human indicators compound the effect
const strongHumanIndicators = [
metrics.humanLikenessIndicators > 0.3,
metrics.informalnessScore > 0.3,
metrics.emotionalToneVariability > 0.2,
metrics.entropyScore > 0.8,
].filter(Boolean).length;
if (strongHumanIndicators >= 3) {
adjustedScore *= 0.1; // Very strong reduction for clearly human text
}
else if (strongHumanIndicators >= 2) {
adjustedScore *= 0.3; // Strong reduction for likely human text
}
return Math.max(0, Math.min(1, adjustedScore));
}
generateDetailedReasons(metrics, score) {
const reasons = [];
if (metrics.perplexity < 8) {
reasons.push(`Low perplexity (${metrics.perplexity.toFixed(2)}) suggests predictable word patterns typical of AI`);
}
if (metrics.burstiness < 0.1) {
reasons.push(`Low burstiness (${metrics.burstiness.toFixed(2)}) indicates consistent sentence structure characteristic of AI`);
}
if (metrics.humanLikenessIndicators < 0.3) {
reasons.push(`Low human-likeness indicators (${metrics.humanLikenessIndicators.toFixed(2)}) suggest absence of typical human writing patterns`);
}
if (metrics.entropyScore < 0.7) {
reasons.push(`Low entropy score (${metrics.entropyScore.toFixed(2)}) indicates predictable word choice patterns typical of AI`);
}
if (metrics.informalnessScore < 0.2) {
reasons.push(`Low informality score (${metrics.informalnessScore.toFixed(2)}) suggests formal, AI-like writing style`);
}
if (metrics.lexicalDiversity > 0.4 && metrics.lexicalDiversity < 0.7) {
reasons.push(`Lexical diversity (${metrics.lexicalDiversity.toFixed(2)}) falls within AI-typical range`);
}
if (metrics.transitionDensity > 2) {
reasons.push(`High transition word density (${metrics.transitionDensity.toFixed(1)}%) characteristic of AI writing`);
}
if (metrics.discourseMarkerPatterns > 0.3) {
reasons.push(`Elevated discourse marker usage (${metrics.discourseMarkerPatterns.toFixed(2)}) typical of AI text structure`);
}
if (metrics.formalityIndex > 0.5) {
reasons.push(`Elevated formality index (${metrics.formalityIndex.toFixed(2)}) suggests AI-generated content`);
}
if (metrics.semanticCoherence > 0.6) {
reasons.push(`High semantic coherence (${metrics.semanticCoherence.toFixed(2)}) typical of AI optimization`);
}
if (metrics.functionWordAnalysis > 0.5) {
reasons.push(`Function word distribution (${metrics.functionWordAnalysis.toFixed(2)}) deviates from natural human patterns`);
}
if (metrics.emotionalToneVariability < 0.2) {
reasons.push(`Low emotional tone variability (${metrics.emotionalToneVariability.toFixed(2)}) suggests limited emotional expression typical of AI`);
}
if (metrics.stylometricSignature < 0.6) {
reasons.push(`Low stylometric variation (${metrics.stylometricSignature.toFixed(2)}) indicates consistent AI writing patterns`);
}
if (metrics.sentenceStructureEntropy < 0.8) {
reasons.push(`Low sentence structure entropy (${metrics.sentenceStructureEntropy.toFixed(2)}) suggests uniform AI sentence construction`);
}
if (metrics.nGramRepetition > 0.1) {
reasons.push(`Repetitive n-gram patterns (${(metrics.nGramRepetition * 100).toFixed(1)}%) detected`);
}
if (metrics.bigramUnusualness > 0.2) {
reasons.push(`Unusual bigram patterns (${(metrics.bigramUnusualness * 100).toFixed(1)}%) may indicate AI generation`);
}
// Positive indicators for human text
if (score <= 0.4) {
reasons.push("Natural linguistic variation suggests human authorship");
reasons.push("Irregular patterns inconsistent with AI generation");
if (metrics.humanLikenessIndicators > 0.5) {
reasons.push("Strong human-like writing patterns detected");
}
if (metrics.informalnessScore > 0.4) {
reasons.push("Informal language patterns suggest human authorship");
}
if (metrics.emotionalToneVariability > 0.3) {
reasons.push("Varied emotional expression typical of human writing");
}
}
if (metrics.entropyScore > 0.8) {
reasons.push("High entropy indicates natural human unpredictability in word choice");
}
if (metrics.vocabularyRichness < 0.3) {
reasons.push("Limited vocabulary richness may indicate AI limitations");
}
return reasons;
}
// New method to detect narrative/literary writing patterns
calculateNarrativeScore(text) {
let narrativeScore = 0;
let totalIndicators = 0;
// Check for character names and proper nouns (common in narrative)
const properNouns = (text.match(/\b[A-Z][a-z]+\b/g) || []).length;
const words = this.tokenizeWords(text);
narrativeScore += Math.min(properNouns / Math.max(words.length * 0.1, 1), 1);
totalIndicators++;
// Check for past tense narrative patterns
const pastTenseVerbs = (text.match(/\b\w+(ed|was|were|had|did|said|went|came|saw|looked|thought|felt|knew|told|asked|answered|walked|turned|opened|closed)\b/gi) || []).length;
narrativeScore += Math.min(pastTenseVerbs / Math.max(words.length * 0.1, 1), 1);
totalIndicators++;
// Check for descriptive language
const descriptiveWords = (text.match(/\b(big|small|large|tiny|huge|enormous|beautiful|ugly|old|young|tall|short|fat|thin|thick|wide|narrow|bright|dark|loud|quiet|soft|hard|smooth|rough|hot|cold|warm|cool|dry|wet|clean|dirty|new|old|fresh|stale|sweet|sour|bitter|salty|spicy|mild|strong|weak|heavy|light|fast|slow|quick|careful|gentle|rough|kind|mean|nice|bad|good|excellent|terrible|wonderful|awful|amazing|boring|interesting|exciting|scary|funny|sad|happy|angry|surprised|confused|tired|energetic)\b/gi) || []).length;
narrativeScore += Math.min(descriptiveWords / Math.max(words.length * 0.08, 1), 1);
totalIndicators++;
// Check for dialogue indicators
const dialogueIndicators = (text.match(/["'"]/g) || []).length;
narrativeScore += Math.min(dialogueIndicators / 10, 0.8);
totalIndicators++;
// Check for third-person narrative pronouns
const thirdPersonPronouns = (text.match(/\b(he|she|they|him|her|them|his|hers|their|theirs)\b/gi) || []).length;
narrativeScore += Math.min(thirdPersonPronouns / Math.max(words.length * 0.05, 1), 1);
totalIndicators++;
return narrativeScore / totalIndicators;
}
// New method to detect creative writing patterns
calculateCreativityScore(text) {
let creativityScore = 0;
let totalIndicators = 0;
// Check for metaphors and similes
const metaphorPatterns = (text.match(/\b(like|as|seemed|appeared|looked like|sounded like|felt like|was like|were like)\b/gi) || []).length;
const words = this.tokenizeWords(text);
creativityScore += Math.min(metaphorPatterns / Math.max(words.length * 0.05, 1), 1);
totalIndicators++;
// Check for unique/creative descriptions (unusual adjective-noun combinations)
const creativeDescriptions = (text.match(/\b(nearly twice|hardly any|very large|came in very useful|no finer|so much of|which made|although he did)\b/gi) || []).length;
creativityScore += Math.min(creativeDescriptions / 5, 1);
totalIndicators++;
// Check for vivid imagery words
const imageryWords = (text.match(/\b(craning|spying|mustache|beefy|blonde|drilling|garden fences|neighbors|opinion|director|firm)\b/gi) || []).length;
creativityScore += Math.min(imageryWords / Math.max(words.length * 0.1, 1), 1);
totalIndicators++;
// Check for specific, concrete details rather than abstract concepts
const concreteNouns = (text.match(/\b(drill|mustache|neck|fence|garden|neighbor|son|boy|director|firm|company|house|car|door|window|street|road|tree|flower|table|chair|book|phone|computer|cat|dog|bird|food|water|coffee|tea|money|time|day|night|morning|evening|sun|moon|star|cloud|rain|snow|wind|fire|ice|rock|sand|grass|leaf|branch|root|seed)\b/gi) || []).length;
creativityScore += Math.min(concreteNouns / Math.max(words.length * 0.08, 1), 1);
totalIndicators++;
// Check for character-focused writing
const characterFocus = (text.match(/\b(Mr\.|Mrs\.|Dursley|Dudley|Grunnings|called|named|known as)\b/gi) || []).length;
creativityScore += Math.min(characterFocus / 8, 1);
totalIndicators++;
return creativityScore / totalIndicators;
}
}
// Export for different environme