Spaces:
Sleeping
Sleeping
Fix Level 2 thresholds: remove word_count restrictions for better human text detection
ede39dc verified | """ | |
| AI Text Detector - Standalone RoBERTa Model + 10-Level Analysis | |
| Trained on 123,653 samples achieving 99.18% test accuracy. | |
| No API dependencies - fully standalone detection. | |
| """ | |
| import gradio as gr | |
| import torch | |
| import torch.nn.functional as F | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModelForSeq2SeqLM | |
| import boto3 | |
| from botocore.config import Config | |
| from pathlib import Path | |
| import os | |
| import re | |
| import math | |
| from collections import Counter | |
| from typing import Dict, List, Any | |
| # iDrive e2 credentials (set via environment variables) | |
| E2_ENDPOINT = "https://s3.us-west-1.idrivee2.com" | |
| E2_BUCKET = "crop-spray-uploads" | |
| E2_ACCESS_KEY = os.environ.get("E2_ACCESS_KEY") | |
| E2_SECRET_KEY = os.environ.get("E2_SECRET_KEY") | |
| # Model path on e2 - New trained model with 99.18% accuracy | |
| ROBERTA_123K_PREFIX = "ai-detector/models/roberta_123k_trained/" | |
| # Local model directory | |
| MODEL_DIR = Path("/tmp/roberta_123k_model") | |
| # Label mapping (binary classification) | |
| LABELS = { | |
| 0: "Human-Written", | |
| 1: "AI-Generated" | |
| } | |
| LABEL_COLORS = { | |
| 0: "#22c55e", # Green for human | |
| 1: "#ef4444", # Red for AI | |
| } | |
| # ============== 10-LEVEL DETECTION SYSTEM ============== | |
| class Level2_StatisticalFingerprinting: | |
| """AI has statistical patterns humans don't - IMPROVED for casual human text""" | |
| def detect(self, text: str) -> float: | |
| """ | |
| IMPROVED: Recognizes casual human writing patterns including personal anecdotes. | |
| Returns AI score (0.0 = definitely human, 1.0 = definitely AI) | |
| """ | |
| if not text or not text.strip(): | |
| return 0.5 | |
| words = text.split() | |
| if not words: | |
| return 0.5 | |
| text_lower = text.lower() | |
| words_lower = [w.lower().strip('.,!?;:"\'-') for w in words] | |
| # ===== CASUAL WORD MARKERS ===== | |
| casual_word_markers = { | |
| "lol", "lmao", "rofl", "smh", "tbh", "ngl", "imo", | |
| "wtf", "omg", "omfg", "fml", "btw", "idk", | |
| "dude", "bro", "brah", "bruh", "yo", | |
| "hey", "yeah", "yep", "nope", "yup", | |
| "gonna", "wanna", "gotta", "kinda", "sorta", | |
| "dunno", "lemme", "gimme", "ok", "okay", | |
| "haha", "hahaha", "lmfao" | |
| } | |
| casual_count = sum(1 for w in words_lower if w in casual_word_markers) | |
| # ===== PERSONAL ANECDOTE INDICATORS ===== | |
| anecdote_patterns = [ | |
| r'\bi was\b', r'\bi am\b', r'\bi\'m\b', r'\bi swear\b', r'\bi think\b', | |
| r'\bi guess\b', r'\bi mean\b', r'\bi got\b', r'\bi had\b', r'\bi have\b', | |
| r'\bmy \w+\b', r'\bme \w+\b', | |
| r'\byesterday\b', r'\btoday\b', r'\bthis morning\b', r'\blast night\b', | |
| r'\bthe guy\b', r'\bthe dude\b', r'\bsome guy\b', | |
| r'\blike \d+\b', | |
| ] | |
| anecdote_count = sum(1 for p in anecdote_patterns if re.search(p, text_lower)) | |
| # ===== CONTRACTIONS ===== | |
| contractions = len(re.findall(r"n't|'re|'ve|'ll|'d|'m|'s", text)) | |
| contraction_ratio = contractions / len(words) if words else 0 | |
| # ===== PUNCTUATION PATTERNS ===== | |
| multiple_punct = len(re.findall(r'[!?]{2,}', text)) | |
| exclamation_marks = text.count('!') | |
| question_marks = text.count('?') | |
| # ===== PRONOUNS ===== | |
| pronoun_words = {"i", "me", "we", "you", "my", "your", "our", "us", "myself"} | |
| pronouns = sum(1 for w in words_lower if w in pronoun_words) | |
| pronoun_ratio = pronouns / len(words) if words else 0 | |
| # ===== INFORMAL PATTERNS ===== | |
| informal_starters = ["so ", "and ", "but ", "like ", "well ", "anyway "] | |
| starts_informal = any(text_lower.startswith(s) for s in informal_starters) | |
| # Short sentence fragments | |
| sentences = re.split(r'[.!?]+', text) | |
| short_sentences = sum(1 for s in sentences if s.strip() and len(s.split()) <= 5) | |
| # ===== CALCULATE HUMAN SCORE ===== | |
| human_score = 0 | |
| # Casual markers (strong) | |
| if casual_count >= 3: | |
| human_score += 0.45 | |
| elif casual_count >= 2: | |
| human_score += 0.35 | |
| elif casual_count >= 1: | |
| human_score += 0.25 | |
| # Personal anecdotes (strong indicator) | |
| if anecdote_count >= 4: | |
| human_score += 0.40 | |
| elif anecdote_count >= 3: | |
| human_score += 0.30 | |
| elif anecdote_count >= 2: | |
| human_score += 0.20 | |
| elif anecdote_count >= 1: | |
| human_score += 0.10 | |
| # Contractions (important) | |
| if contraction_ratio > 0.08: | |
| human_score += 0.30 | |
| elif contraction_ratio > 0.04: | |
| human_score += 0.20 | |
| elif contractions >= 2: | |
| human_score += 0.15 | |
| elif contractions >= 1: | |
| human_score += 0.10 | |
| # Multiple punctuation (strong for casual) | |
| if multiple_punct > 0: | |
| human_score += 0.30 | |
| # High pronoun usage | |
| if pronoun_ratio > 0.12: | |
| human_score += 0.25 | |
| elif pronoun_ratio > 0.08: | |
| human_score += 0.18 | |
| elif pronoun_ratio > 0.05: | |
| human_score += 0.12 | |
| # Exclamation/question marks | |
| if (exclamation_marks + question_marks) > 3: | |
| human_score += 0.15 | |
| elif (exclamation_marks + question_marks) > 1: | |
| human_score += 0.08 | |
| # Informal start | |
| if starts_informal: | |
| human_score += 0.10 | |
| # Short sentence fragments (conversational) | |
| if short_sentences >= 2: | |
| human_score += 0.15 | |
| # ===== SPECIAL CASES ===== | |
| # Short text with any human indicator | |
| if len(words) < 20 and (casual_count > 0 or anecdote_count > 0 or contractions > 0): | |
| human_score += 0.15 | |
| # Very short responses | |
| if len(words) <= 3: | |
| short_human = {'k', 'ok', 'lol', 'no', 'yes', 'yeah', 'nope', | |
| 'yep', 'sure', 'nah', 'haha', 'omg', 'wow', 'nice', 'cool'} | |
| if any(w in short_human for w in words_lower): | |
| human_score = 0.95 | |
| # Just punctuation (confused/expressive human) | |
| if re.match(r'^[!?]+$', text.strip()): | |
| human_score = 0.95 | |
| # Text with lots of question marks + words = confused human | |
| if question_marks >= 4 and len(words) < 15: | |
| human_score += 0.30 | |
| # ===== AI SCORE ===== | |
| ai_score = 0 | |
| if human_score < 0.30: | |
| formal_words = { | |
| "furthermore", "moreover", "however", "thus", "therefore", | |
| "consequently", "notwithstanding", "subsequently", | |
| "additionally", "nevertheless", "henceforth", | |
| "leverage", "transformative", "organizations", "comprehensive", | |
| "implementation", "facilitate", "utilize" | |
| } | |
| formal_count = sum(1 for w in words_lower if w in formal_words) | |
| if formal_count >= 3: | |
| ai_score += 0.30 | |
| elif formal_count >= 2: | |
| ai_score += 0.20 | |
| elif formal_count >= 1: | |
| ai_score += 0.10 | |
| if contractions == 0 and len(words) > 25: | |
| ai_score += 0.15 | |
| if pronoun_ratio < 0.02 and len(words) > 25: | |
| ai_score += 0.12 | |
| # ===== FINAL SCORE ===== | |
| final_score = max(0, min(1 - human_score + ai_score, 1.0)) | |
| return final_score | |
| class Level3_SyntacticPatterns: | |
| """Detect AI by sentence structure patterns""" | |
| def detect(self, text: str) -> float: | |
| sentences = re.split(r'[.!?]+', text) | |
| ai_score = 0 | |
| complex_count = 0 | |
| simple_count = 0 | |
| passive_count = 0 | |
| question_count = 0 | |
| for sent in sentences: | |
| if not sent.strip(): | |
| continue | |
| sent_lower = sent.lower() | |
| if re.search(r'\b(is|are|was|were)\b.*\b(by|that|which)\b', sent_lower): | |
| passive_count += 1 | |
| if '?' in sent: | |
| question_count += 1 | |
| clause_count = len(re.findall(r',|\bthat\b|\bwhich\b|\bbecause\b', sent_lower)) | |
| if clause_count > 2: | |
| complex_count += 1 | |
| else: | |
| simple_count += 1 | |
| total = complex_count + simple_count + question_count | |
| if total == 0: | |
| return 0.5 | |
| if (complex_count / total) > 0.5: | |
| ai_score += 0.15 | |
| if passive_count > 0 and (passive_count / total) > 0.3: | |
| ai_score += 0.15 | |
| if (question_count / total) > 0.2: | |
| ai_score -= 0.1 | |
| return max(0, min(ai_score, 1.0)) | |
| class Level4_NGramEntropy: | |
| """AI has LOW entropy (repetitive), humans have HIGH entropy (varied)""" | |
| def extract_ngrams(self, text: str, n: int = 3) -> List[str]: | |
| words = text.lower().split() | |
| return [' '.join(words[i:i+n]) for i in range(len(words) - n + 1)] | |
| def calculate_entropy(self, ngrams: List[str]) -> float: | |
| if not ngrams: | |
| return 0 | |
| counter = Counter(ngrams) | |
| total = len(ngrams) | |
| entropy = 0 | |
| for count in counter.values(): | |
| prob = count / total | |
| entropy -= prob * math.log2(prob + 1e-10) | |
| return entropy | |
| def detect(self, text: str) -> float: | |
| words = text.lower().split() | |
| if len(words) < 10: | |
| return 0.5 | |
| bigrams = self.extract_ngrams(text, 2) | |
| trigrams = self.extract_ngrams(text, 3) | |
| fourgrams = self.extract_ngrams(text, 4) | |
| entropy_2 = self.calculate_entropy(bigrams) | |
| entropy_3 = self.calculate_entropy(trigrams) | |
| entropy_4 = self.calculate_entropy(fourgrams) | |
| avg_entropy = (entropy_2 + entropy_3 + entropy_4) / 3 | |
| normalized = min(avg_entropy / 8, 1.0) | |
| return 1 - normalized | |
| class Level6_LexicalDiversity: | |
| """Humans use MORE diverse vocabulary, AI tends to repeat""" | |
| def detect(self, text: str) -> float: | |
| words = text.lower().split() | |
| if not words or len(words) < 10: | |
| return 0.5 | |
| unique_words = set(words) | |
| ai_score = 0 | |
| ttr = len(unique_words) / len(words) | |
| if ttr < 0.4: | |
| ai_score += 0.2 | |
| elif ttr < 0.5: | |
| ai_score += 0.1 | |
| stopwords = ["the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for"] | |
| stopword_ratio = sum(1 for w in words if w in stopwords) / len(words) | |
| if stopword_ratio > 0.25: | |
| ai_score += 0.15 | |
| hapax = sum(1 for w in unique_words if words.count(w) == 1) | |
| hapax_ratio = hapax / len(unique_words) if unique_words else 0 | |
| if hapax_ratio < 0.5: | |
| ai_score += 0.1 | |
| return min(ai_score, 1.0) | |
| class Level7_NamedEntities: | |
| """AI mentions FEWER specific people/places/dates""" | |
| def detect(self, text: str) -> float: | |
| sentences = re.split(r'[.!?]+', text) | |
| proper_nouns = 0 | |
| for sent in sentences: | |
| words = sent.strip().split() | |
| for i, word in enumerate(words[1:], 1): | |
| if word and word[0].isupper() and word.isalpha(): | |
| proper_nouns += 1 | |
| dates = len(re.findall(r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b', text)) | |
| dates += len(re.findall(r'\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}', text, re.I)) | |
| specific_numbers = len(re.findall(r'\b\d+(?:\.\d+)?%?\b', text)) | |
| total_specifics = proper_nouns + dates + specific_numbers | |
| word_count = len(text.split()) | |
| specifics_ratio = total_specifics / word_count if word_count > 0 else 0 | |
| if specifics_ratio < 0.02: | |
| return 0.2 | |
| elif specifics_ratio < 0.05: | |
| return 0.1 | |
| else: | |
| return 0.0 | |
| class Level8_TemporalContextual: | |
| """AI uses generic temporal references, humans use specific ones""" | |
| def detect(self, text: str) -> float: | |
| ai_score = 0 | |
| words = text.split() | |
| if len(words) < 10: | |
| return 0.5 | |
| generic_temp = len(re.findall( | |
| r'\b(?:previously|furthermore|consequently|subsequently|moreover|additionally|therefore|hence)\b', | |
| text, re.I | |
| )) | |
| if generic_temp > 3: | |
| ai_score += 0.15 | |
| elif generic_temp > 1: | |
| ai_score += 0.1 | |
| personal = sum(1 for w in words if w.lower() in ["i", "me", "my", "we", "our", "us"]) | |
| if personal < 2: | |
| ai_score += 0.1 | |
| specific_time = len(re.findall( | |
| r'\b(?:yesterday|today|tomorrow|last\s+(?:week|month|year)|this\s+(?:morning|afternoon|evening))\b', | |
| text, re.I | |
| )) | |
| if specific_time == 0: | |
| ai_score += 0.1 | |
| return min(ai_score, 1.0) | |
| class TenLevelAnalyzer: | |
| """10-Level Analysis System for supplementary detection insights""" | |
| def __init__(self): | |
| self.level2 = Level2_StatisticalFingerprinting() | |
| self.level3 = Level3_SyntacticPatterns() | |
| self.level4 = Level4_NGramEntropy() | |
| self.level6 = Level6_LexicalDiversity() | |
| self.level7 = Level7_NamedEntities() | |
| self.level8 = Level8_TemporalContextual() | |
| self.weights = { | |
| "statistical": 0.20, | |
| "syntactic": 0.15, | |
| "entropy": 0.20, | |
| "lexical": 0.15, | |
| "entities": 0.15, | |
| "temporal": 0.15, | |
| } | |
| def analyze(self, text: str) -> Dict[str, Any]: | |
| """Run all analysis levels""" | |
| if not text or len(text.strip()) < 50: | |
| return {"error": "Text too short for detailed analysis"} | |
| scores = { | |
| "statistical": self.level2.detect(text), | |
| "syntactic": self.level3.detect(text), | |
| "entropy": self.level4.detect(text), | |
| "lexical": self.level6.detect(text), | |
| "entities": self.level7.detect(text), | |
| "temporal": self.level8.detect(text), | |
| } | |
| total_weight = sum(self.weights.values()) | |
| weighted_score = sum(scores[k] * self.weights[k] for k in scores) / total_weight | |
| votes_for_ai = sum(1 for s in scores.values() if s > 0.3) | |
| insights = [] | |
| if scores["statistical"] > 0.3: | |
| insights.append("Formal language patterns detected") | |
| if scores["syntactic"] > 0.3: | |
| insights.append("Complex sentence structures typical of AI") | |
| if scores["entropy"] > 0.5: | |
| insights.append("Low vocabulary entropy (repetitive patterns)") | |
| if scores["lexical"] > 0.3: | |
| insights.append("Limited lexical diversity") | |
| if scores["entities"] > 0.15: | |
| insights.append("Few specific names/dates/numbers") | |
| if scores["temporal"] > 0.2: | |
| insights.append("Generic temporal references") | |
| return { | |
| "scores": scores, | |
| "weighted_score": weighted_score, | |
| "votes_for_ai": votes_for_ai, | |
| "total_levels": len(scores), | |
| "insights": insights | |
| } | |
| # ============== MODEL LOADING ============== | |
| def download_model(): | |
| """Download trained RoBERTa model from iDrive e2.""" | |
| s3 = boto3.client( | |
| 's3', | |
| endpoint_url=E2_ENDPOINT, | |
| aws_access_key_id=E2_ACCESS_KEY, | |
| aws_secret_access_key=E2_SECRET_KEY, | |
| config=Config(signature_version='s3v4') | |
| ) | |
| if not MODEL_DIR.exists() or not (MODEL_DIR / "model.safetensors").exists(): | |
| print("Downloading RoBERTa 123K model from iDrive e2...") | |
| MODEL_DIR.mkdir(parents=True, exist_ok=True) | |
| # Download the zip file | |
| zip_path = MODEL_DIR / "model.zip" | |
| s3.download_file(E2_BUCKET, "ai-detector/models/roberta_123k_trained.zip", str(zip_path)) | |
| # Extract | |
| import zipfile | |
| with zipfile.ZipFile(zip_path, 'r') as zip_ref: | |
| zip_ref.extractall(MODEL_DIR) | |
| # Clean up zip | |
| zip_path.unlink() | |
| print("Model download complete!") | |
| else: | |
| print("Model already downloaded") | |
| # Download model on startup | |
| download_model() | |
| # Load model and tokenizer | |
| print("Loading RoBERTa 123K model (99.18% accuracy)...") | |
| tokenizer = AutoTokenizer.from_pretrained(str(MODEL_DIR)) | |
| model = AutoModelForSequenceClassification.from_pretrained(str(MODEL_DIR)) | |
| model.eval() | |
| # Move to GPU if available | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model.to(device) | |
| print(f"Model loaded on {device}") | |
| # Initialize 10-level analyzer | |
| ten_level_analyzer = TenLevelAnalyzer() | |
| # Load Humanizer model | |
| print("Loading Ateeqq Text-Rewriter-Paraphraser model...") | |
| try: | |
| humanizer_tokenizer = AutoTokenizer.from_pretrained("Ateeqq/Text-Rewriter-Paraphraser") | |
| humanizer_model = AutoModelForSeq2SeqLM.from_pretrained("Ateeqq/Text-Rewriter-Paraphraser") | |
| humanizer_model.to(device) | |
| humanizer_model.eval() | |
| HUMANIZER_AVAILABLE = True | |
| print("Humanizer model loaded successfully!") | |
| except Exception as e: | |
| print(f"Warning: Could not load humanizer model: {e}") | |
| humanizer_tokenizer = None | |
| humanizer_model = None | |
| HUMANIZER_AVAILABLE = False | |
| # ============== PREDICTION FUNCTIONS ============== | |
| def predict(text: str) -> Dict[str, Any]: | |
| """ | |
| Get prediction from trained RoBERTa model with casual human text correction. | |
| The RoBERTa model was trained on formal human text, so it may incorrectly | |
| flag casual/informal human text as AI. This function uses the improved | |
| Level 2 statistical analysis to correct such cases. | |
| """ | |
| inputs = tokenizer( | |
| text, return_tensors="pt", truncation=True, max_length=512, padding=True | |
| ) | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| probs = F.softmax(outputs.logits, dim=-1) | |
| pred_class = torch.argmax(probs, dim=-1).item() | |
| confidence = probs[0][pred_class].item() | |
| human_prob = probs[0][0].item() | |
| ai_prob = probs[0][1].item() | |
| label = LABELS[pred_class] | |
| # ===== HUMAN TEXT CORRECTION ===== | |
| # If RoBERTa says AI, check if Level 2 indicates human text | |
| # Level 2 returns AI score (0.0 = definitely human, 1.0 = definitely AI) | |
| # AI text typically has Level 2 scores > 0.85, human text < 0.60 | |
| if pred_class == 1: # RoBERTa says AI | |
| level2 = Level2_StatisticalFingerprinting() | |
| level2_score = level2.detect(text) | |
| word_count = len(text.split()) | |
| # Override when Level 2 indicates human (score < 0.70) | |
| # AI text has scores > 0.85, so this won't trigger for AI | |
| if level2_score < 0.35: | |
| # Very strong human indicators - definitely human | |
| label = "Human-Written" | |
| human_prob = max(human_prob, 1 - level2_score) | |
| ai_prob = min(ai_prob, level2_score) | |
| confidence = human_prob | |
| elif level2_score < 0.55: | |
| # Strong human indicators - likely human | |
| label = "Human-Written" | |
| human_prob = max(human_prob, 0.75) | |
| ai_prob = min(ai_prob, 0.25) | |
| confidence = human_prob | |
| elif level2_score < 0.70: | |
| # Moderate human indicators - probably human | |
| label = "Human-Written" | |
| human_prob = max(human_prob, 0.65) | |
| ai_prob = min(ai_prob, 0.35) | |
| confidence = human_prob | |
| # For high Level 2 scores (> 0.70), trust RoBERTa - it's likely AI | |
| return { | |
| "label": label, | |
| "confidence": confidence, | |
| "probabilities": { | |
| "Human-Written": human_prob, | |
| "AI-Generated": ai_prob | |
| } | |
| } | |
| def humanize_text(text: str, num_beams: int = 5, max_length: int = 512) -> str: | |
| """Humanize AI-generated text using Ateeqq Text-Rewriter-Paraphraser model.""" | |
| if not HUMANIZER_AVAILABLE: | |
| return "Error: Humanizer model not available" | |
| try: | |
| input_text = f"paraphrase: {text}" | |
| inputs = humanizer_tokenizer( | |
| input_text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=max_length, | |
| padding=True | |
| ) | |
| inputs = {k: v.to(device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = humanizer_model.generate( | |
| **inputs, | |
| max_length=max_length, | |
| num_beams=num_beams, | |
| early_stopping=True, | |
| do_sample=True, | |
| temperature=0.7, | |
| top_p=0.9 | |
| ) | |
| humanized = humanizer_tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| return humanized | |
| except Exception as e: | |
| return f"Error during humanization: {str(e)}" | |
| # ============== UI FUNCTIONS ============== | |
| def create_result_html(prediction: Dict, analysis: Dict) -> str: | |
| """Create HTML for displaying results.""" | |
| label = prediction["label"] | |
| confidence = prediction["confidence"] | |
| probs = prediction["probabilities"] | |
| label_idx = 0 if label == "Human-Written" else 1 | |
| color = LABEL_COLORS[label_idx] | |
| # Main result | |
| html = f""" | |
| <div style='padding: 20px; border-radius: 10px; background: linear-gradient(135deg, {color}22, {color}11);'> | |
| <h2 style='margin: 0 0 10px 0; color: {color}; text-align: center;'>{label}</h2> | |
| <p style='margin: 0; text-align: center; font-size: 24px; font-weight: bold;'>{confidence*100:.1f}% confidence</p> | |
| <hr style='margin: 15px 0; border: none; border-top: 1px solid {color}44;'> | |
| <h4 style='margin: 10px 0 5px 0;'>Probability Distribution:</h4> | |
| <div style='display: flex; gap: 10px; margin-bottom: 15px;'> | |
| <div style='flex: 1; padding: 10px; background: #22c55e22; border-radius: 4px; text-align: center;'> | |
| <div style='font-weight: bold; color: #22c55e;'>Human-Written</div> | |
| <div style='font-size: 18px;'>{probs["Human-Written"]*100:.1f}%</div> | |
| </div> | |
| <div style='flex: 1; padding: 10px; background: #ef444422; border-radius: 4px; text-align: center;'> | |
| <div style='font-weight: bold; color: #ef4444;'>AI-Generated</div> | |
| <div style='font-size: 18px;'>{probs["AI-Generated"]*100:.1f}%</div> | |
| </div> | |
| </div> | |
| """ | |
| # 10-Level Analysis | |
| if "error" not in analysis: | |
| html += f""" | |
| <hr style='margin: 15px 0; border: none; border-top: 1px solid {color}44;'> | |
| <h4 style='margin: 10px 0 5px 0;'>10-Level Analysis ({analysis['votes_for_ai']}/{analysis['total_levels']} indicators suggest AI):</h4> | |
| <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;'> | |
| """ | |
| level_names = { | |
| "statistical": "Statistical", | |
| "syntactic": "Syntactic", | |
| "entropy": "Entropy", | |
| "lexical": "Lexical", | |
| "entities": "Entities", | |
| "temporal": "Temporal" | |
| } | |
| for key, score in analysis["scores"].items(): | |
| score_color = "#ef4444" if score > 0.3 else "#22c55e" | |
| html += f""" | |
| <div style='padding: 8px; background: {score_color}11; border-radius: 4px; text-align: center;'> | |
| <div style='font-size: 12px; color: #666;'>{level_names.get(key, key)}</div> | |
| <div style='font-weight: bold; color: {score_color};'>{score*100:.0f}%</div> | |
| </div> | |
| """ | |
| html += "</div>" | |
| if analysis["insights"]: | |
| html += "<div style='margin-top: 10px; padding: 10px; background: #f3f4f6; border-radius: 4px;'>" | |
| html += "<strong>Key Insights:</strong><ul style='margin: 5px 0 0 0; padding-left: 20px;'>" | |
| for insight in analysis["insights"]: | |
| html += f"<li style='font-size: 13px; color: #666;'>{insight}</li>" | |
| html += "</ul></div>" | |
| html += "</div>" | |
| # Model info | |
| html += """ | |
| <div style='margin-top: 10px; padding: 10px; background: #f0f9ff; border-radius: 4px; font-size: 12px; color: #0369a1;'> | |
| <strong>Model:</strong> RoBERTa-base trained on 123,653 samples | <strong>Test Accuracy:</strong> 99.18% | <strong>F1 Score:</strong> 99.31% | |
| </div> | |
| """ | |
| return html | |
| def analyze_text(text: str) -> str: | |
| """Main analysis function for Gradio.""" | |
| if not text or len(text.strip()) < 10: | |
| return "<div style='padding: 20px; text-align: center;'>Please enter at least 10 characters of text.</div>" | |
| prediction = predict(text) | |
| analysis = ten_level_analyzer.analyze(text) | |
| return create_result_html(prediction, analysis) | |
| def humanize_and_analyze(text: str) -> tuple: | |
| """Humanize text and analyze both versions.""" | |
| if not text or len(text.strip()) < 10: | |
| error_msg = "<div style='padding: 20px; text-align: center;'>Please enter at least 10 characters of text.</div>" | |
| return error_msg, "", error_msg | |
| if not HUMANIZER_AVAILABLE: | |
| error_msg = "<div style='padding: 20px; text-align: center; color: red;'>Humanizer model not available.</div>" | |
| return error_msg, "", error_msg | |
| # Analyze original | |
| original_pred = predict(text) | |
| original_analysis = ten_level_analyzer.analyze(text) | |
| original_html = create_result_html(original_pred, original_analysis) | |
| # Humanize | |
| humanized_text = humanize_text(text) | |
| if humanized_text.startswith("Error"): | |
| return original_html, humanized_text, "<div style='padding: 20px; text-align: center; color: red;'>" + humanized_text + "</div>" | |
| # Analyze humanized | |
| humanized_pred = predict(humanized_text) | |
| humanized_analysis = ten_level_analyzer.analyze(humanized_text) | |
| humanized_html = create_result_html(humanized_pred, humanized_analysis) | |
| return original_html, humanized_text, humanized_html | |
| def just_humanize(text: str) -> str: | |
| """Just humanize text without analysis.""" | |
| if not text or len(text.strip()) < 10: | |
| return "Please enter at least 10 characters of text." | |
| return humanize_text(text) | |
| # ============== GRADIO INTERFACE ============== | |
| with gr.Blocks(title="AI Text Detector - 99.18% Accuracy", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(""" | |
| # AI Text Detector | |
| ### Standalone RoBERTa Model + 10-Level Analysis | |
| **Model Performance:** 99.18% accuracy on 12,366 test samples | F1: 99.31% | Precision: 98.75% | Recall: 99.88% | |
| **Training Data:** 123,653 samples (50,000 human + 58,570 AI + 15,083 humanized AI) | |
| **No API Dependencies** - Fully standalone detection using trained transformer model. | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("Detect AI Text"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| input_text = gr.Textbox( | |
| label="Enter text to analyze", | |
| placeholder="Paste the text you want to check for AI generation...", | |
| lines=10 | |
| ) | |
| analyze_btn = gr.Button("Analyze Text", variant="primary") | |
| with gr.Column(scale=1): | |
| result_html = gr.HTML(label="Detection Result") | |
| analyze_btn.click( | |
| fn=analyze_text, | |
| inputs=[input_text], | |
| outputs=[result_html] | |
| ) | |
| with gr.Tab("Humanize & Detect"): | |
| gr.Markdown(""" | |
| This tab humanizes AI-generated text and shows detection results for both versions. | |
| Uses the Ateeqq Text-Rewriter-Paraphraser model for humanization. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| humanize_input = gr.Textbox( | |
| label="Enter AI-generated text to humanize", | |
| placeholder="Paste AI-generated text here...", | |
| lines=8 | |
| ) | |
| humanize_btn = gr.Button("Humanize & Analyze", variant="primary") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Original Text Analysis") | |
| original_result = gr.HTML() | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Humanized Text") | |
| humanized_output = gr.Textbox(label="Humanized Version", lines=6) | |
| gr.Markdown("### Humanized Text Analysis") | |
| humanized_result = gr.HTML() | |
| humanize_btn.click( | |
| fn=humanize_and_analyze, | |
| inputs=[humanize_input], | |
| outputs=[original_result, humanized_output, humanized_result] | |
| ) | |
| with gr.Tab("Just Humanize"): | |
| gr.Markdown("Humanize text without detection analysis.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| just_humanize_input = gr.Textbox( | |
| label="Enter text to humanize", | |
| placeholder="Paste text here...", | |
| lines=8 | |
| ) | |
| just_humanize_btn = gr.Button("Humanize", variant="primary") | |
| with gr.Column(scale=1): | |
| just_humanize_output = gr.Textbox( | |
| label="Humanized Text", | |
| lines=8 | |
| ) | |
| just_humanize_btn.click( | |
| fn=just_humanize, | |
| inputs=[just_humanize_input], | |
| outputs=[just_humanize_output] | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### About This Detector | |
| This AI text detector uses a RoBERTa-base model fine-tuned on a diverse dataset of 123,653 text samples: | |
| - **50,000 human-written** samples from Wikipedia, Reddit, and web sources | |
| - **58,570 AI-generated** samples from GPT-4, Claude, Gemini, Llama, and other models | |
| - **15,083 humanized AI** samples processed through various paraphrasers | |
| The model achieves **99.18% test accuracy** with excellent precision (98.75%) and recall (99.88%). | |
| The 10-level analysis provides additional insights using statistical fingerprinting, syntactic patterns, | |
| n-gram entropy, lexical diversity, named entity analysis, and temporal reference detection. | |
| **Fully Standalone** - No external API calls required for detection. | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch() | |