import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
import re
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"):
# Load from Hugging Face URL
state_dict = torch.hub.load_state_dict_from_url(path_or_url, map_location=device)
else:
# Load from local file
state_dict = torch.load(path_or_url, map_location=device)
model.load_state_dict(state_dict)
return model.to(device).eval()
# Initializing the ensemble
model_1 = load_model(model1_path)
model_2 = load_model(model2_url)
model_3 = load_model(model3_url)
label_mapping = {
0: '13B', 1: '30B', 2: '65B', 3: '7B', 4: 'GLM130B', 5: 'bloom_7b',
6: 'bloomz', 7: 'cohere', 8: 'davinci', 9: 'dolly', 10: 'dolly-v2-12b',
11: 'flan_t5_base', 12: 'flan_t5_large', 13: 'flan_t5_small',
14: 'flan_t5_xl', 15: 'flan_t5_xxl', 16: 'gemma-7b-it', 17: 'gemma2-9b-it',
18: 'gpt-3.5-turbo', 19: 'gpt-35', 20: 'gpt4', 21: 'gpt4o',
22: 'gpt_j', 23: 'gpt_neox', 24: 'human', 25: 'llama3-70b', 26: 'llama3-8b',
27: 'mixtral-8x7b', 28: 'opt_1.3b', 29: 'opt_125m', 30: 'opt_13b',
31: 'opt_2.7b', 32: 'opt_30b', 33: 'opt_350m', 34: 'opt_6.7b',
35: 'opt_iml_30b', 36: 'opt_iml_max_1.3b', 37: 't0_11b', 38: 't0_3b',
39: 'text-davinci-002', 40: 'text-davinci-003'
}
# --- 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
# Custom Tokenizer Normalization
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()
])
# --- Core Classification Logic ---
def classify_text(text):
cleaned_text = clean_text(text)
if not cleaned_text.strip():
return "Please enter text to analyze.", None
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
# Soft voting ensemble (averaging probabilities)
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]
# Calculate probabilities for the 2 main categories (Human vs AI)
human_prob = probs[24].item()
# To find the specific LLM, we ignore the 'human' index (24)
ai_probs_only = probs.clone()
ai_probs_only[24] = 0
ai_total_prob = ai_probs_only.sum().item()
# Normalize percentages
total_sum = human_prob + ai_total_prob
human_pct = (human_prob / total_sum) * 100
ai_pct = (ai_total_prob / total_sum) * 100
# Identify the specific AI model with the highest sub-probability
top_ai_idx = torch.argmax(ai_probs_only).item()
predicted_llm = label_mapping[top_ai_idx]
# Construct the result display
if human_pct > ai_pct:
result_message = (
f"### Result: **{human_pct:.2f}% Human written**\n\n"
"The content matches human writing patterns."
)
else:
result_message = (
f"### Result: **{ai_pct:.2f}% AI generated**\n\n"
f"**Specific Model Identified:** `{predicted_llm}`\n\n"
"The structure and syntax are highly characteristic of this LLM."
)
# Visualization
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) # Room for text labels
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')
plt.tight_layout()
return result_message, fig
# --- 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 & LLM Identifier")
gr.Markdown("This tool uses an ensemble of **ModernBERT** models to predict if text is human or AI, and specifies the likely source model.")
with gr.Row():
with gr.Column(scale=2):
text_input = gr.Textbox(
label="Input Text",
placeholder="Paste your English text here...",
lines=10
)
with gr.Column(scale=1):
result_output = gr.Markdown("Analysis results will appear here.", elem_id="output-container")
plot_output = gr.Plot()
# Trigger classification on text change
text_input.change(classify_text, inputs=text_input, outputs=[result_output, plot_output])
gr.Markdown("---")
gr.Markdown("**Developed by SzegedAI**")
if __name__ == "__main__":
# share=True creates a public URL
iface.launch(share=True)