AI_Detector_llm / app.py
jay123jay's picture
Update app.py
0c1d05c verified
Raw
History Blame Contribute Delete
9.32 kB
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
import re
import numpy as np
from tokenizers.normalizers import Sequence, Replace, Strip
from tokenizers import Regex
import matplotlib.pyplot as plt
# --- Setup and Model Loading ---
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Model paths and URLs
model1_path = "modernbert.bin"
model2_url = "https://huggingface.co/mihalykiss/modernbert_2/resolve/main/Model_groups_3class_seed12"
model3_url = "https://huggingface.co/mihalykiss/modernbert_2/resolve/main/Model_groups_3class_seed22"
tokenizer = AutoTokenizer.from_pretrained("answerdotai/ModernBERT-base")
def load_model(path_or_url, num_labels=41):
model = AutoModelForSequenceClassification.from_pretrained("answerdotai/ModernBERT-base", num_labels=num_labels)
if path_or_url.startswith("http"):
state_dict = torch.hub.load_state_dict_from_url(path_or_url, map_location=device)
else:
state_dict = torch.load(path_or_url, map_location=device)
model.load_state_dict(state_dict)
return model.to(device).eval()
# Initializing the ensemble
print("Loading Ensemble Models...")
model_1 = load_model(model1_path)
model_2 = load_model(model2_url)
model_3 = load_model(model3_url)
print("Models Loaded Successfully.")
# --- Text Preprocessing ---
def clean_text(text: str) -> str:
text = re.sub(r'\s{2,}', ' ', text)
text = re.sub(r'\s+([,.;:?!])', r'\1', text)
return text
newline_to_space = Replace(Regex(r'\s*\n\s*'), " ")
join_hyphen_break = Replace(Regex(r'(\w+)[--]\s*\n\s*(\w+)'), r"\1\2")
tokenizer.backend_tokenizer.normalizer = Sequence([
tokenizer.backend_tokenizer.normalizer,
join_hyphen_break,
newline_to_space,
Strip()
])
# --- BRAND NAME MAPPING ---
# Mapping the architectural traces directly to consumer brands
openai_indices = [8, 18, 19, 20, 21, 39, 40]
# Updated Mapping:
modern_grouping = {
"ChatGPT (OpenAI)": openai_indices,
"Meta AI (Llama)": [0, 1, 2, 3, 25, 26],
"Gemini (Google)": [11, 12, 13, 14, 15, 16, 17],
"Grok (xAI)": [4, 22, 23], # Removed 27 from here
"Claude / Other AI": [5, 6, 7, 9, 10, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38] # Added 27 here
}
# --- STYLOMETRIC ANALYSIS ---
def calculate_stylometrics(text):
sentences = re.split(r'[.!?]+', text)
words = re.findall(r'\b\w+\b', text.lower())
if not words or len(sentences) < 2:
return 0, 0, 0, 0
lengths = [len(s.split()) for s in sentences if len(s.strip()) > 0]
variance = np.var(lengths) if lengths else 0
unique_words = set(words)
ttr = len(unique_words) / len(words)
human_boost = 0
ai_boost = 0
if variance > 50:
human_boost += 0.15
elif variance < 20:
ai_boost += 0.15
if ttr > 0.65:
human_boost += 0.05
elif ttr < 0.45:
ai_boost += 0.05
return human_boost, ai_boost, variance, ttr
# --- Core Classification Logic ---
def classify_text(text):
cleaned_text = clean_text(text)
word_count = len(cleaned_text.split())
if word_count < 30:
return "Please enter at least 30 words for an accurate analysis.", None, "Insufficient data."
inputs = tokenizer(cleaned_text, return_tensors="pt", truncation=True, padding=True).to(device)
with torch.no_grad():
logits_1 = model_1(**inputs).logits
logits_2 = model_2(**inputs).logits
logits_3 = model_3(**inputs).logits
s1, s2, s3 = torch.softmax(logits_1, 1), torch.softmax(logits_2, 1), torch.softmax(logits_3, 1)
avg_probs = (s1 + s2 + s3) / 3
probs = avg_probs[0]
# --- BALANCED RLHF SUPPRESSOR ---
pre_suppression_openai = sum([probs[i].item() for i in openai_indices])
# Soften the OpenAI dominance just enough to let Gemini and Grok traces show through
for idx in openai_indices:
probs[idx] = probs[idx] * 0.15
total_prob = probs.sum().item()
probs = probs / total_prob
# -------------------------------------
base_human_prob = probs[24].item()
ai_probs_only = probs.clone()
ai_probs_only[24] = 0
base_ai_total_prob = ai_probs_only.sum().item()
# Apply Stylometrics
human_modifier, ai_modifier, variance, ttr = calculate_stylometrics(cleaned_text)
adjusted_human_prob = max(0, base_human_prob + human_modifier)
adjusted_ai_prob = max(0, base_ai_total_prob + ai_modifier)
total_sum = adjusted_human_prob + adjusted_ai_prob
human_pct = (adjusted_human_prob / total_sum) * 100
ai_pct = (adjusted_ai_prob / total_sum) * 100
# Calculate Brand Scores
modern_scores = {}
for modern_name, indices in modern_grouping.items():
family_sum = sum([probs[i].item() for i in indices])
modern_scores[modern_name] = family_sum
total_ai_modern_score = sum(modern_scores.values())
if total_ai_modern_score > 0:
modern_scores_pct = {k: (v / total_ai_modern_score) * 100 for k, v in modern_scores.items()}
else:
modern_scores_pct = {k: 0 for k in modern_scores.keys()}
sorted_families = sorted(modern_scores_pct.items(), key=lambda item: item[1], reverse=True)
top_family, top_score = sorted_families[0]
second_family, second_score = sorted_families[1]
# --- Generate Diagnostics Report ---
diagnostics = f"""
**Lexical Analysis:**
* Word Count: `{word_count}`
* Sentence Variance (Burstiness): `{variance:.2f}` *(>50 favors Human, <20 favors AI)*
* Type-Token Ratio (Richness): `{ttr:.2f}` *(>0.65 favors Human, <0.45 favors AI)*
**Algorithmic Adjustments:**
* Stylometric Modifiers Applied: `Human +{human_modifier:.2f} | AI +{ai_modifier:.2f}`
* Raw RLHF Signal Detected (Pre-Suppression): `{pre_suppression_openai * 100:.1f}%`
"""
# --- Construct Output ---
if human_pct > ai_pct:
result_message = (
f"### Result: <span class='highlight-human'>**{human_pct:.2f}% Human written**</span>\n\n"
"The content matches human writing patterns in both semantic structure and lexical variance."
)
fig, ax = plt.subplots(figsize=(8, 4))
bars = ax.bar(['Human', 'AI'], [human_pct, ai_pct], color=['#4CAF50', '#FF5733'], alpha=0.8)
ax.set_ylabel('Probability (%)')
ax.set_title('Detection Probability')
ax.set_ylim(0, 110)
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + 2, f'{height:.1f}%', ha='center', fontweight='bold')
else:
result_message = (
f"### Result: <span class='highlight-ai'>**{ai_pct:.2f}% AI generated**</span>\n\n"
f"**Likely Source:** `{top_family}` ({top_score:.1f}% Match)\n"
f"**Secondary Match:** `{second_family}` ({second_score:.1f}% Match)\n"
)
# Clean up names for the graph (e.g., "ChatGPT (OpenAI)" -> "ChatGPT")
fig, ax = plt.subplots(figsize=(10, 5))
families = [f[0].split('(')[0].strip() if '(' in f[0] else f[0] for f in sorted_families]
scores = [f[1] for f in sorted_families]
bars = ax.bar(families, scores, color=['#10a37f', '#4285f4', '#000000', '#0668E1', '#D3D3D3'], alpha=0.8)
ax.set_ylabel('Signal Strength within AI (%)')
ax.set_title('AI Brand Attribution Breakdown')
ax.set_ylim(0, max(scores) + 15 if max(scores) > 0 else 100)
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + 1, f'{height:.1f}%', ha='center', fontweight='bold')
plt.tight_layout()
return result_message, fig, diagnostics
# --- Gradio UI Layout ---
with gr.Blocks(css="""
.highlight-human { color: #4CAF50; font-weight: bold; background: rgba(76, 175, 80, 0.1); padding: 5px; border-radius: 5px; }
.highlight-ai { color: #FF5733; font-weight: bold; background: rgba(255, 87, 51, 0.1); padding: 5px; border-radius: 5px; }
#output-container { text-align: center; padding: 20px; }
""") as iface:
gr.Markdown("# AI Text Detector & Brand Identifier")
gr.Markdown("Detects AI-generated text and maps structural traces to major consumer models like **ChatGPT, Gemini, Grok, and Meta AI**.")
with gr.Row():
with gr.Column(scale=2):
text_input = gr.Textbox(
label="Input Text (Min 30 Words required for mathematical accuracy)",
placeholder="Paste your English text here to begin analysis...",
lines=12
)
with gr.Column(scale=1):
result_output = gr.Markdown("Analysis results will appear here.", elem_id="output-container")
plot_output = gr.Plot()
with gr.Accordion("Technical Diagnostics (Under the Hood)", open=False):
diagnostics_output = gr.Markdown("Awaiting input to generate metrics...")
text_input.change(
classify_text,
inputs=text_input,
outputs=[result_output, plot_output, diagnostics_output]
)
gr.Markdown("---")
gr.Markdown("**Engineered by SzegedAI**")
if __name__ == "__main__":
iface.launch(share=True)