0.30, 'linguistic' => 0.25, 'structural' => 0.20, 'vocabulary' => 0.15, 'repetition' => 0.10, ]; /** * Common AI transition words (EN + FR) * * @var array */ private $transition_words = []; /** * Common AI formal vocabulary (EN + FR) * * @var array */ private $formal_words = []; /** * Common AI hedging phrases (EN + FR) * * @var array */ private $hedging_phrases = []; /** * Common AI formulaic expressions (EN + FR) * * @var array */ private $formulaic_expressions = []; /** * Get singleton instance * * @return AI_Explainer_Pro_Detection_Engine */ public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor */ private function __construct() { $this->load_dictionaries(); } /** * Load detection dictionaries * * @return void */ private function load_dictionaries() { $this->transition_words = [ // English 'however', 'moreover', 'furthermore', 'therefore', 'consequently', 'nevertheless', 'additionally', 'subsequently', 'accordingly', 'hence', 'thus', 'meanwhile', 'nonetheless', 'conversely', 'alternatively', 'specifically', 'notably', 'significantly', 'essentially', 'fundamentally', 'importantly', 'interestingly', 'surprisingly', 'ultimately', 'overall', 'in conclusion', 'in summary', 'to summarize', 'in other words', 'for instance', 'for example', 'in particular', 'as a result', 'on the other hand', 'in addition', 'as mentioned', 'it is worth noting', 'it is important to note', 'it should be noted', // French 'cependant', 'néanmoins', 'toutefois', 'ainsi', 'par conséquent', 'en effet', 'de plus', 'en outre', 'également', 'par ailleurs', 'notamment', 'en revanche', 'de ce fait', 'dès lors', 'en somme', 'en définitive', 'en conclusion', 'il convient de noter', 'il est important de souligner', 'force est de constater', 'il est à noter que', 'dans ce contexte', 'à cet égard', // Spanish 'sin embargo', 'además', 'por lo tanto', 'no obstante', 'en consecuencia', 'asimismo', 'por otra parte', 'en primer lugar', 'en segundo lugar', 'en conclusión', 'en resumen', 'por ejemplo', 'es decir', 'cabe destacar', 'es importante señalar', 'en este sentido', 'dicho esto', // German 'jedoch', 'darüber hinaus', 'folglich', 'nichtsdestotrotz', 'dementsprechend', 'außerdem', 'insbesondere', 'zusammenfassend', 'beispielsweise', 'demzufolge', 'es ist wichtig zu beachten', 'in diesem zusammenhang', 'abschließend', // Italian 'tuttavia', 'inoltre', 'pertanto', 'ciononostante', 'di conseguenza', 'in particolare', 'in conclusione', 'in sintesi', 'ad esempio', 'vale a dire', 'è importante notare', 'in questo contesto', 'a tal proposito', // Portuguese 'no entanto', 'além disso', 'portanto', 'consequentemente', 'não obstante', 'em particular', 'em conclusão', 'em resumo', 'por exemplo', 'ou seja', 'é importante notar', 'neste contexto', 'sendo assim', ]; $this->formal_words = [ // English 'utilize', 'facilitate', 'implement', 'comprehensive', 'significant', 'substantial', 'leverage', 'optimize', 'streamline', 'endeavor', 'aforementioned', 'paramount', 'pivotal', 'multifaceted', 'holistic', 'synergy', 'paradigm', 'methodology', 'framework', 'ecosystem', 'stakeholder', 'scalable', 'robust', 'innovative', 'cutting-edge', 'state-of-the-art', 'groundbreaking', 'transformative', 'dynamic', 'encompass', 'delineate', 'elucidate', 'underscore', 'juxtapose', // French 'optimiser', 'efficacité', 'processus', 'méthodologie', 'transformation', 'intégrer', 'collaboration', 'transversale', 'problématique', 'paradigme', 'synergie', 'globalité', 'pluridisciplinaire', 'écosystème', 'scalable', 'holistique', 'disruptif', 'structurant', 'opérationnel', 'substantiel', // Spanish 'optimizar', 'implementar', 'fundamental', 'significativo', 'integral', 'paradigma', 'metodología', 'sinergia', 'ecosistema', 'innovador', 'transformador', 'sostenible', 'estratégico', 'multifacético', 'holístico', // German 'implementieren', 'optimieren', 'grundlegend', 'umfassend', 'wesentlich', 'paradigma', 'methodik', 'synergie', 'ökosystem', 'innovativ', 'ganzheitlich', 'nachhaltig', 'strategisch', 'vielschichtig', // Italian 'implementare', 'ottimizzare', 'fondamentale', 'significativo', 'integrale', 'paradigma', 'metodologia', 'sinergia', 'ecosistema', 'innovativo', 'trasformativo', 'sostenibile', 'strategico', 'olistico', // Portuguese 'implementar', 'otimizar', 'fundamental', 'significativo', 'abrangente', 'paradigma', 'metodologia', 'sinergia', 'ecossistema', 'inovador', 'transformador', 'sustentável', 'estratégico', 'holístico', ]; $this->hedging_phrases = [ // English 'it is worth noting', 'it is important to', 'one might argue', 'it could be said', 'there are several', 'in many cases', 'it is essential to', 'it is crucial to', 'plays a crucial role', 'plays a significant role', 'plays an important role', 'serves as a', 'can be considered', 'tends to be', 'appears to be', 'seems to be', 'is often considered', 'is widely regarded', 'is generally accepted', 'has the potential to', 'it is widely recognized', 'delve into', 'navigating the', 'landscape of', 'realm of', 'tapestry of', 'in today\'s world', 'in today\'s digital', 'in the modern era', 'in this article', 'in this comprehensive', 'let\'s explore', 'let\'s dive into', 'let\'s take a look', // French 'il convient de', 'il est essentiel de', 'il est primordial de', 'il est fondamental de', 'joue un rôle crucial', 'joue un rôle majeur', 'revêt une importance', 'dans le cadre de', 'au sein de', 'dans un contexte de', 'il apparaît que', 'force est de constater', // Spanish 'es importante destacar', 'juega un papel crucial', 'cabe señalar', 'se puede considerar', 'en el marco de', 'en el ámbito de', 'es fundamental', 'resulta esencial', 'desempeña un papel importante', // German 'es ist wichtig zu betonen', 'spielt eine entscheidende rolle', 'es sei darauf hingewiesen', 'im rahmen von', 'im bereich von', 'kann als .+ betrachtet werden', 'es ist unerlässlich', // Italian 'è importante sottolineare', 'svolge un ruolo cruciale', 'va sottolineato che', 'nell\'ambito di', 'nel contesto di', 'può essere considerato', 'è fondamentale', 'riveste un\'importanza', // Portuguese 'é importante destacar', 'desempenha um papel crucial', 'vale ressaltar', 'no âmbito de', 'no contexto de', 'pode ser considerado', 'é fundamental', 'reveste-se de importância', ]; $this->formulaic_expressions = [ // Common AI opening patterns 'in today\'s rapidly evolving', 'in the ever-changing landscape', 'in an increasingly digital world', 'as technology continues to', 'the importance of .+ cannot be overstated', 'when it comes to', '.+ has become an integral part', '.+ has revolutionized the way', 'whether you\'re a .+ or a', // Common AI closing patterns 'in conclusion,', 'to sum up,', 'by following these', 'by implementing these', 'remember that .+ is a journey', 'embrace the power of', // French patterns 'dans un monde en constante', 'à l\'ère du numérique', 'l\'importance de .+ ne peut être sous-estimée', 'que vous soyez .+ ou', // Spanish patterns 'en el mundo actual', 'en la era digital', 'la importancia de .+ no puede ser subestimada', 'ya seas .+ o', // German patterns 'in der heutigen welt', 'im digitalen zeitalter', 'die bedeutung von .+ kann nicht unterschätzt werden', // Italian patterns 'nel mondo di oggi', 'nell\'era digitale', 'l\'importanza di .+ non può essere sottovalutata', // Portuguese patterns 'no mundo atual', 'na era digital', 'a importância de .+ não pode ser subestimada', ]; $this->transition_words = apply_filters( 'ai_explainer_pro_transition_words', $this->transition_words ); $this->formal_words = apply_filters( 'ai_explainer_pro_formal_words', $this->formal_words ); $this->hedging_phrases = apply_filters( 'ai_explainer_pro_hedging_phrases', $this->hedging_phrases ); } /** * Run full detection on content * * @param string $content The content to analyze. * @param array $options Detection options. * @return array Detection results. */ public function detect( $content, $options = [] ) { if ( empty( $content ) || strlen( $content ) < 50 ) { return [ 'score' => 0, 'confidence' => 'low', 'indicators' => [ __( 'Content too short for reliable analysis', 'ai-explainer-pro' ) ], 'methods' => [], 'word_count' => str_word_count( $content ), ]; } $cache_key = md5( $content . serialize( $options ) ); if ( isset( $this->cache[ $cache_key ] ) ) { return $this->cache[ $cache_key ]; } $content_lower = mb_strtolower( $content, 'UTF-8' ); $word_count = str_word_count( $content ); $sentences = $this->split_sentences( $content ); $words = $this->extract_words( $content_lower ); $methods = []; // 1. Statistical Analysis $methods['statistical'] = $this->analyze_statistical( $sentences, $words, $word_count ); // 2. Linguistic Analysis $methods['linguistic'] = $this->analyze_linguistic( $content_lower, $word_count ); // 3. Structural Analysis $methods['structural'] = $this->analyze_structural( $content, $sentences ); // 4. Vocabulary Analysis $methods['vocabulary'] = $this->analyze_vocabulary( $words, $word_count ); // 5. Repetition Analysis $methods['repetition'] = $this->analyze_repetition( $sentences, $words ); /** * Allow add-ons to inject additional detection methods (e.g. an * external API provider). The Pro add-on uses this to add an * "api" method when the user has configured a provider. */ $extra_methods = apply_filters( 'aiep_detection_methods', [], $content, $options ); if ( is_array( $extra_methods ) ) { foreach ( $extra_methods as $key => $data ) { $methods[ sanitize_key( $key ) ] = $data; } } /** * Method weights used by the ensemble. Add-ons that inject extra * methods through `aiep_detection_methods` can register a weight * for them here; weights are re-normalized internally so the map * does not need to sum to 1. * * @param array $weights Map of method key => weight. * @param array $methods Computed method results. * @param int $word_count Content word count. */ $weights = apply_filters( 'aiep_method_weights', $this->weights, $methods, $word_count ); // Calculate ensemble score. $result = $this->calculate_ensemble_score( $methods, $word_count, $weights ); /** * Allow add-ons to enrich the result (model fingerprint, content * hash, etc.). The Pro add-on hooks in here. */ $result = apply_filters( 'aiep_detection_result', $result, $content, $sentences, $content_lower ); $this->cache[ $cache_key ] = $result; return $result; } /** * Statistical analysis: sentence length variance, burstiness, entropy * * @param array $sentences Array of sentences. * @param array $words Array of words. * @param int $word_count Total word count. * @return array Analysis result with score and indicators. */ private function analyze_statistical( $sentences, $words, $word_count ) { $score = 0; $indicators = []; $count = count( $sentences ); if ( $count < 3 ) { return [ 'score' => 0, 'indicators' => [] ]; } // Sentence length variance (burstiness) $lengths = array_map( 'str_word_count', $sentences ); $mean_len = array_sum( $lengths ) / $count; $variance = 0; foreach ( $lengths as $len ) { $variance += pow( $len - $mean_len, 2 ); } $variance = $variance / $count; $std_dev = sqrt( $variance ); // Coefficient of variation (CV) - lower CV = more uniform = more likely AI $cv = ( $mean_len > 0 ) ? $std_dev / $mean_len : 0; if ( $cv < 0.25 && $count >= 5 ) { $score += 0.35; $indicators[] = sprintf( /* translators: %.2f: coefficient of variation of sentence lengths */ __( 'Very uniform sentence lengths (CV: %.2f) — AI text tends to have consistent rhythm', 'ai-explainer-pro' ), $cv ); } elseif ( $cv < 0.40 && $count >= 5 ) { $score += 0.20; $indicators[] = sprintf( /* translators: %.2f: coefficient of variation of sentence lengths */ __( 'Moderately uniform sentence lengths (CV: %.2f)', 'ai-explainer-pro' ), $cv ); } // Average sentence length if ( $mean_len > 22 ) { $score += 0.15; $indicators[] = sprintf( /* translators: %.0f: mean sentence length in words */ __( 'Long average sentence length (%.0f words) — typical of AI-generated text', 'ai-explainer-pro' ), $mean_len ); } // Perplexity estimation via word frequency distribution if ( count( $words ) >= 50 ) { $freq = array_count_values( $words ); $total = count( $words ); $entropy = 0; foreach ( $freq as $f ) { $p = $f / $total; if ( $p > 0 ) { $entropy -= $p * log( $p, 2 ); } } // Normalized entropy (0-1) $max_entropy = log( count( $freq ), 2 ); $norm_entropy = ( $max_entropy > 0 ) ? $entropy / $max_entropy : 0; // AI text tends to have moderate, consistent entropy if ( $norm_entropy > 0.85 && $norm_entropy < 0.95 ) { $score += 0.15; $indicators[] = sprintf( /* translators: %.2f: normalized Shannon entropy of word distribution */ __( 'Suspiciously uniform word distribution (entropy: %.2f) — AI text tends to use vocabulary very evenly', 'ai-explainer-pro' ), $norm_entropy ); } } return [ 'score' => min( $score, 1.0 ), 'indicators' => $indicators, 'data' => [ 'mean_sentence_length' => round( $mean_len, 1 ), 'sentence_cv' => round( $cv, 3 ), 'sentence_count' => $count, ], ]; } /** * Linguistic analysis: transitions, formality, hedging, pronouns * * @param string $content_lower Lowercased content. * @param int $word_count Total word count. * @return array Analysis result with score and indicators. */ private function analyze_linguistic( $content_lower, $word_count ) { $score = 0; $indicators = []; if ( $word_count < 30 ) { return [ 'score' => 0, 'indicators' => [] ]; } // Transition word density $transition_count = 0; foreach ( $this->transition_words as $word ) { $transition_count += $this->count_term( $content_lower, mb_strtolower( $word, 'UTF-8' ) ); } $transition_density = $transition_count / ( $word_count / 100 ); if ( $transition_density > 4.0 ) { $score += 0.30; $indicators[] = sprintf( /* translators: 1: transition word density per 100 words, 2: total transition word count */ __( 'Very high transition word density (%1$.1f per 100 words, %2$d total) — AI over-uses connectors', 'ai-explainer-pro' ), $transition_density, $transition_count ); } elseif ( $transition_density > 2.5 ) { $score += 0.15; $indicators[] = sprintf( /* translators: %.1f: transition word density per 100 words */ __( 'Elevated transition word usage (%.1f per 100 words)', 'ai-explainer-pro' ), $transition_density ); } // Formal vocabulary density $formal_count = 0; foreach ( $this->formal_words as $word ) { $formal_count += $this->count_term( $content_lower, mb_strtolower( $word, 'UTF-8' ) ); } $formal_density = $formal_count / ( $word_count / 100 ); if ( $formal_density > 3.0 ) { $score += 0.25; $indicators[] = sprintf( /* translators: %.1f: formal vocabulary density per 100 words */ __( 'High formal/corporate vocabulary density (%.1f per 100 words)', 'ai-explainer-pro' ), $formal_density ); } elseif ( $formal_density > 1.5 ) { $score += 0.10; $indicators[] = __( 'Moderate formal vocabulary usage', 'ai-explainer-pro' ); } // Hedging phrases $hedge_count = 0; foreach ( $this->hedging_phrases as $phrase ) { $hedge_count += $this->count_term( $content_lower, mb_strtolower( $phrase, 'UTF-8' ) ); } if ( $hedge_count >= 4 ) { $score += 0.25; $indicators[] = sprintf( /* translators: %d: count of hedging phrase occurrences */ __( 'Heavy use of AI-typical hedging phrases (%d occurrences)', 'ai-explainer-pro' ), $hedge_count ); } elseif ( $hedge_count >= 2 ) { $score += 0.10; $indicators[] = __( 'Some AI-typical hedging phrases detected', 'ai-explainer-pro' ); } // Personal pronoun analysis (multilingual). Word-boundary matched so // pronouns count at sentence start/end and before punctuation too. $personal_patterns = [ // English 'i', 'my', 'me', 'mine', 'myself', 'i\'m', 'i\'ve', 'i\'d', 'i\'ll', // French 'je', 'mon', 'ma', 'mes', 'moi', 'j\'ai', 'j\'étais', 'mien', // Spanish 'yo', 'mi', 'mis', 'mío', 'mía', 'conmigo', // German 'ich', 'mein', 'meine', 'meinem', 'meiner', 'mir', 'mich', // Italian 'io', 'mio', 'mia', 'miei', 'mie', // Portuguese 'eu', 'meu', 'minha', 'meus', 'minhas', 'comigo', ]; $personal_total = 0; foreach ( $personal_patterns as $p ) { $personal_total += $this->count_term( $content_lower, $p ); } $personal_density = $personal_total / ( $word_count / 100 ); if ( $personal_total === 0 && $word_count > 80 ) { $score += 0.20; $indicators[] = __( 'No personal pronouns — impersonal style typical of AI', 'ai-explainer-pro' ); } elseif ( $personal_density < 0.3 && $word_count > 100 ) { $score += 0.10; $indicators[] = __( 'Very few personal pronouns', 'ai-explainer-pro' ); } // Formulaic expression detection $formulaic_count = 0; foreach ( $this->formulaic_expressions as $pattern ) { if ( preg_match( '/' . $pattern . '/i', $content_lower ) ) { $formulaic_count++; } } if ( $formulaic_count >= 3 ) { $score += 0.25; $indicators[] = sprintf( /* translators: %d: count of formulaic expressions */ __( 'Multiple AI-typical formulaic expressions detected (%d)', 'ai-explainer-pro' ), $formulaic_count ); } elseif ( $formulaic_count >= 1 ) { $score += 0.10; $indicators[] = __( 'Some formulaic AI expressions found', 'ai-explainer-pro' ); } return [ 'score' => min( $score, 1.0 ), 'indicators' => $indicators, 'data' => [ 'transition_density' => round( $transition_density, 2 ), 'formal_density' => round( $formal_density, 2 ), 'hedge_count' => $hedge_count, 'personal_density' => round( $personal_density, 2 ), 'formulaic_count' => $formulaic_count, ], ]; } /** * Structural analysis: paragraph uniformity, list patterns, topic sentences * * @param string $content Original content. * @param array $sentences Array of sentences. * @return array Analysis result with score and indicators. */ private function analyze_structural( $content, $sentences ) { $score = 0; $indicators = []; // Paragraph analysis $paragraphs = preg_split( '/\n\s*\n/', trim( $content ) ); $paragraphs = array_filter( $paragraphs, function( $p ) { return str_word_count( trim( $p ) ) > 5; }); $para_count = count( $paragraphs ); if ( $para_count >= 3 ) { $para_lengths = array_map( 'str_word_count', $paragraphs ); $para_mean = array_sum( $para_lengths ) / $para_count; $para_var = 0; foreach ( $para_lengths as $len ) { $para_var += pow( $len - $para_mean, 2 ); } $para_var = $para_var / $para_count; $para_cv = ( $para_mean > 0 ) ? sqrt( $para_var ) / $para_mean : 0; if ( $para_cv < 0.20 ) { $score += 0.30; $indicators[] = sprintf( /* translators: %.2f: coefficient of variation of paragraph lengths */ __( 'Very uniform paragraph lengths (CV: %.2f) — AI generates evenly-sized blocks', 'ai-explainer-pro' ), $para_cv ); } elseif ( $para_cv < 0.35 ) { $score += 0.15; $indicators[] = __( 'Moderately uniform paragraph structure', 'ai-explainer-pro' ); } } // Heading/list pattern detection (AI loves numbered lists and headers) $has_numbered_list = preg_match_all( '/^\s*\d+[\.\)]\s/m', $content ); $has_bullet_list = preg_match_all( '/^\s*[-•*]\s/m', $content ); $has_headers = preg_match_all( '/^#+\s|^[A-Z][^.]{5,50}$/m', $content ); $structural_markers = $has_numbered_list + $has_bullet_list + $has_headers; if ( $structural_markers > 5 ) { $score += 0.20; $indicators[] = sprintf( /* translators: %d: count of structural markers (lists, headers) */ __( 'Heavy structural formatting (%d markers: lists, headers) — common in AI output', 'ai-explainer-pro' ), $structural_markers ); } // Topic sentence pattern (AI often starts paragraphs with a clear topic sentence) if ( $para_count >= 3 ) { $topic_sentence_score = 0; foreach ( $paragraphs as $para ) { $first_sentence = $this->get_first_sentence( trim( $para ) ); // Check for topic-sentence patterns if ( preg_match( '/^(one of|another|the (first|second|third|key|main|primary|most)|additionally|furthermore|moreover|in addition)/i', trim( $first_sentence ) ) ) { $topic_sentence_score++; } } $topic_ratio = $topic_sentence_score / $para_count; if ( $topic_ratio > 0.5 ) { $score += 0.20; $indicators[] = sprintf( /* translators: %.0f: percentage of paragraphs with formulaic topic sentences */ __( 'Formulaic topic sentences in %.0f%% of paragraphs', 'ai-explainer-pro' ), $topic_ratio * 100 ); } } return [ 'score' => min( $score, 1.0 ), 'indicators' => $indicators, 'data' => [ 'paragraph_count' => $para_count, 'paragraph_cv' => isset( $para_cv ) ? round( $para_cv, 3 ) : null, ], ]; } /** * Vocabulary analysis: richness (TTR), hapax legomena, Zipf conformance * * @param array $words Array of words. * @param int $word_count Total word count. * @return array Analysis result with score and indicators. */ private function analyze_vocabulary( $words, $word_count ) { $score = 0; $indicators = []; if ( $word_count < 50 ) { return [ 'score' => 0, 'indicators' => [] ]; } $freq = array_count_values( $words ); $unique_count = count( $freq ); // Type-Token Ratio (TTR) — normalized for text length // Use root TTR (Guiraud's index) which is more stable across lengths $guiraud = $unique_count / sqrt( $word_count ); // AI text tends to have a moderate, predictable Guiraud index // Very high or very low suggests human (either creative or repetitive) // AI lands in a "sweet spot" around 5-8 for English if ( $guiraud > 4.5 && $guiraud < 7.5 && $word_count > 100 ) { $score += 0.15; $indicators[] = sprintf( /* translators: %.1f: Guiraud's index of vocabulary richness */ __( 'Vocabulary richness in typical AI range (Guiraud: %.1f)', 'ai-explainer-pro' ), $guiraud ); } // Hapax legomena ratio (words appearing only once) $hapax_count = count( array_filter( $freq, function( $f ) { return $f === 1; } ) ); $hapax_ratio = $hapax_count / $unique_count; // AI text has fewer hapax legomena (it reuses vocabulary more evenly) if ( $hapax_ratio < 0.45 && $word_count > 100 ) { $score += 0.20; $indicators[] = sprintf( /* translators: %.0f: percentage of unique words used only once (hapax ratio) */ __( 'Low vocabulary novelty rate (%.0f%% unique words used once) — AI reuses words more predictably', 'ai-explainer-pro' ), $hapax_ratio * 100 ); } // Word length distribution (AI tends to use medium-length words consistently) $word_lengths = array_map( 'mb_strlen', $words ); $avg_word_len = array_sum( $word_lengths ) / count( $word_lengths ); $wl_variance = 0; foreach ( $word_lengths as $wl ) { $wl_variance += pow( $wl - $avg_word_len, 2 ); } $wl_variance = $wl_variance / count( $word_lengths ); $wl_cv = ( $avg_word_len > 0 ) ? sqrt( $wl_variance ) / $avg_word_len : 0; if ( $wl_cv < 0.45 && $word_count > 80 ) { $score += 0.15; $indicators[] = sprintf( /* translators: %.2f: coefficient of variation of word lengths */ __( 'Uniform word length distribution (CV: %.2f) — AI selects consistently-sized words', 'ai-explainer-pro' ), $wl_cv ); } return [ 'score' => min( $score, 1.0 ), 'indicators' => $indicators, 'data' => [ 'guiraud_index' => round( $guiraud, 2 ), 'hapax_ratio' => round( $hapax_ratio, 3 ), 'avg_word_length' => round( $avg_word_len, 1 ), 'word_length_cv' => round( $wl_cv, 3 ), ], ]; } /** * Repetition analysis: n-gram repetition, phrase patterns * * @param array $sentences Array of sentences. * @param array $words Array of words. * @return array Analysis result with score and indicators. */ private function analyze_repetition( $sentences, $words ) { $score = 0; $indicators = []; $word_count = count( $words ); if ( $word_count < 50 ) { return [ 'score' => 0, 'indicators' => [] ]; } // Bigram repetition $bigrams = []; for ( $i = 0; $i < $word_count - 1; $i++ ) { $bigram = $words[ $i ] . ' ' . $words[ $i + 1 ]; if ( ! isset( $bigrams[ $bigram ] ) ) { $bigrams[ $bigram ] = 0; } $bigrams[ $bigram ]++; } // Filter out common stop-word bigrams $stop_bigrams = [ 'of the', 'in the', 'to the', 'on the', 'for the', 'and the', 'is a', 'it is', 'de la', 'de le', 'dans le' ]; $repeated_bigrams = array_filter( $bigrams, function( $count, $bigram ) use ( $stop_bigrams ) { return $count >= 3 && ! in_array( $bigram, $stop_bigrams, true ); }, ARRAY_FILTER_USE_BOTH ); if ( count( $repeated_bigrams ) > 3 ) { $score += 0.25; $top_repeated = array_slice( array_keys( $repeated_bigrams ), 0, 3 ); $indicators[] = sprintf( /* translators: 1: number of repeated bigrams, 2: comma-separated list of top repeated bigrams */ __( 'High bigram repetition (%1$d repeated phrases): "%2$s" — AI tends to repeat similar phrasings', 'ai-explainer-pro' ), count( $repeated_bigrams ), implode( '", "', $top_repeated ) ); } elseif ( count( $repeated_bigrams ) > 1 ) { $score += 0.10; $indicators[] = __( 'Some repeated word pairs detected', 'ai-explainer-pro' ); } // Sentence opening repetition $sent_count = count( $sentences ); if ( $sent_count >= 5 ) { $openings = []; foreach ( $sentences as $s ) { $trimmed = trim( $s ); if ( strlen( $trimmed ) > 3 ) { $first_words = implode( ' ', array_slice( explode( ' ', $trimmed ), 0, 3 ) ); $openings[] = mb_strtolower( $first_words, 'UTF-8' ); } } $opening_freq = array_count_values( $openings ); $repeated_openings = array_filter( $opening_freq, function( $c ) { return $c >= 3; } ); if ( ! empty( $repeated_openings ) ) { $score += 0.20; $indicators[] = sprintf( /* translators: %d: number of sentence opening patterns repeated 3 or more times */ __( 'Repetitive sentence openings (%d patterns repeated 3+ times)', 'ai-explainer-pro' ), count( $repeated_openings ) ); } } return [ 'score' => min( $score, 1.0 ), 'indicators' => $indicators, ]; } /** * Calculate ensemble score from all method results * * @param array $methods Method results. * @param int $word_count Content word count. * @param array|null $weights Method weight map (defaults to the core five). * @return array Final detection result. */ private function calculate_ensemble_score( $methods, $word_count, $weights = null ) { $weights = is_array( $weights ) && ! empty( $weights ) ? $weights : $this->weights; $all_indicators = []; $weighted_score = 0; $total_weight = 0; // If API result available, give it high weight if ( isset( $methods['api'] ) ) { $weighted_score += $methods['api']['score'] * 0.50; $total_weight += 0.50; $all_indicators = array_merge( $all_indicators, $methods['api']['indicators'] ); // Reduce other weights proportionally foreach ( $weights as $method => $weight ) { if ( 'api' === $method ) { continue; } if ( isset( $methods[ $method ] ) ) { $adjusted_weight = $weight * 0.50; $weighted_score += $methods[ $method ]['score'] * $adjusted_weight; $total_weight += $adjusted_weight; $all_indicators = array_merge( $all_indicators, $methods[ $method ]['indicators'] ); } } } else { // Local-only ensemble foreach ( $weights as $method => $weight ) { if ( 'api' === $method ) { continue; } if ( isset( $methods[ $method ] ) ) { $weighted_score += $methods[ $method ]['score'] * $weight; $total_weight += $weight; $all_indicators = array_merge( $all_indicators, $methods[ $method ]['indicators'] ); } } } $final_score = ( $total_weight > 0 ) ? $weighted_score / $total_weight : 0; $final_score = min( max( $final_score, 0 ), 1.0 ); // Confidence level based on word count and method agreement $confidence = 'low'; if ( $word_count > 300 ) { $confidence = 'high'; } elseif ( $word_count > 100 ) { $confidence = 'medium'; } // If short content, cap confidence if ( $word_count < 80 ) { $final_score = min( $final_score, 0.7 ); } if ( empty( $all_indicators ) ) { $all_indicators[] = __( 'No strong AI indicators detected — content appears human-written', 'ai-explainer-pro' ); } // Build method summary for detailed view $method_scores = []; foreach ( $methods as $name => $result ) { $method_scores[ $name ] = [ 'score' => isset( $result['score'] ) ? round( $result['score'], 3 ) : 0, 'indicators' => isset( $result['indicators'] ) ? $result['indicators'] : [], 'data' => isset( $result['data'] ) ? $result['data'] : [], ]; } return [ 'score' => round( $final_score, 3 ), 'confidence' => $confidence, 'indicators' => $all_indicators, 'methods' => $method_scores, 'word_count' => $word_count, 'api_used' => isset( $methods['api'] ), ]; } /** * Count whole-word occurrences of a term (single word or phrase). * * Unlike substr_count(), this does not match inside larger words * ("ever" no longer matches "however"). Boundaries are Unicode-aware * so accented dictionaries (FR/ES/DE/IT/PT) match correctly. A leading * apostrophe is allowed (French elision: "l'efficacité" still counts * "efficacité") but a trailing one is not, so "i" does not also match * inside "i'm" and possessives are not double-counted. * * @param string $haystack Lowercased text to search. * @param string $term Lowercased term to count. * @return int Occurrence count. */ private function count_term( $haystack, $term ) { if ( '' === $term ) { return 0; } $pattern = '/(?= 3; } ) ); } /** * Extract words from content * * @param string $content_lower Lowercased content. * @return array Array of word strings. */ private function extract_words( $content_lower ) { $text = wp_strip_all_tags( $content_lower ); preg_match_all( '/[\p{L}\p{N}]+/u', $text, $matches ); return isset( $matches[0] ) ? $matches[0] : []; } /** * Get first sentence from a text block * * @param string $text Text block. * @return string First sentence. */ private function get_first_sentence( $text ) { $parts = preg_split( '/(?<=[.!?])\s/', $text, 2 ); return isset( $parts[0] ) ? $parts[0] : $text; } /** * Quick detection (lightweight, no API) for frontend badge display * * @param string $content Content to analyze. * @return array Simplified result with score and indicators. */ public function quick_detect( $content ) { return $this->detect( $content, [ 'skip_api' => true ] ); } /** * Clear detection cache * * @return void */ public function clear_cache() { $this->cache = []; } }