import gradio as gr
import torch
import numpy as np
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import os
import time
MODEL_PATH = os.getenv("MODEL_PATH", "./model")
MODEL_NAME = "distilbert-base-multilingual-cased"
MAX_LENGTH = 512
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
TEMPERATURE = 2.0
print(f"Loading model dari: {MODEL_PATH}")
try:
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH)
model.to(DEVICE)
model.eval()
print(f"Model berhasil dimuat! Device: {DEVICE} | Temperature: {TEMPERATURE}")
except Exception as e:
print(f"Model lokal tidak ditemukan, fallback ke HuggingFace: {e}")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2)
model.to(DEVICE)
model.eval()
def detect_text(text: str):
text = text.strip()
if not text:
return None, None, "⚠️ Teks tidak boleh kosong.", ""
if len(text) < 20:
return None, None, "⚠️ Teks terlalu pendek, minimal 20 karakter.", ""
if len(text) > 10000:
return None, None, "⚠️ Teks terlalu panjang, maksimal 10.000 karakter.", ""
start_time = time.time()
inputs = tokenizer(
text,
max_length = MAX_LENGTH,
padding = "max_length",
truncation = True,
return_tensors = "pt"
)
inputs = {k: v.to(DEVICE) for k, v in inputs.items()
if k in ["input_ids", "attention_mask"]}
with torch.no_grad():
logits = model(**inputs).logits
scaled_logits = logits / TEMPERATURE
probs = torch.softmax(scaled_logits, dim=-1).cpu().numpy()[0]
prob_human = float(probs[0])
prob_ai = float(probs[1])
predicted = "AI" if prob_ai > prob_human else "Human"
confidence = max(prob_ai, prob_human)
process_time = round(time.time() - start_time, 3)
word_count = len(text.split())
char_count = len(text)
label_html = f"""
{'🤖' if predicted == 'AI' else '👤'}
{'Teks AI' if predicted == 'AI' else 'Teks Manusia'}
{'Kemungkinan besar ditulis oleh AI' if predicted == 'AI' else 'Kemungkinan besar ditulis oleh manusia'}
{confidence*100:.1f}% yakin
Kemungkinan AI
{prob_ai*100:.1f}%
Kemungkinan Manusia
{prob_human*100:.1f}%
{process_time}s
Waktu Proses
"""
return label_html, None, "", ""
# ── Custom CSS
css = """
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap');
body, .gradio-container {
background: #0a0a0f !important;
font-family: 'DM Sans', sans-serif !important;
}
.gradio-container {
max-width: 860px !important;
margin: 0 auto !important;
}
/* Header */
#header {
text-align: center;
padding: 48px 0 32px;
border-bottom: 1px solid rgba(255,255,255,0.06);
margin-bottom: 32px;
}
#header h1 {
font-family: 'Syne', sans-serif !important;
font-size: clamp(32px, 5vw, 52px) !important;
font-weight: 800 !important;
letter-spacing: -2px !important;
color: #e8e8f0 !important;
line-height: 1.05 !important;
margin: 0 !important;
}
#header p {
color: #6b6b80 !important;
font-size: 15px !important;
margin-top: 12px !important;
font-weight: 300 !important;
}
/* Pills */
#pills {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
margin: 20px 0 0;
}
/* Textarea */
textarea {
background: #12121a !important;
border: 1px solid rgba(255,255,255,0.07) !important;
border-radius: 14px !important;
color: #e8e8f0 !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 15px !important;
line-height: 1.7 !important;
padding: 20px !important;
resize: none !important;
}
textarea:focus {
border-color: rgba(255,255,255,0.15) !important;
outline: none !important;
box-shadow: none !important;
}
textarea::placeholder { color: #4a4a5a !important; }
/* Button */
#scan-btn {
background: #00e5b0 !important;
color: #0a0a0f !important;
border: none !important;
border-radius: 10px !important;
font-family: 'Syne', sans-serif !important;
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: 0.3px !important;
padding: 12px 28px !important;
cursor: pointer !important;
transition: all 0.2s !important;
width: 100% !important;
}
#scan-btn:hover {
background: #00ffbf !important;
transform: translateY(-1px) !important;
}
/* Hide default Gradio labels */
.gr-label { display: none !important; }
label > span { display: none !important; }
/* Error message */
#error-box textarea {
color: #ff4d6d !important;
background: rgba(255,77,109,0.06) !important;
border-color: rgba(255,77,109,0.2) !important;
font-family: 'DM Mono', monospace !important;
font-size: 13px !important;
}
/* Footer */
#footer {
text-align: center;
padding: 32px 0;
border-top: 1px solid rgba(255,255,255,0.06);
margin-top: 40px;
color: #3a3a4a;
font-family: 'DM Mono', monospace;
font-size: 11px;
}
"""
# ── Build UI
with gr.Blocks(css=css, title="TextScan AI — Deteksi Teks AI") as demo:
gr.HTML("""
""")
with gr.Row():
with gr.Column():
text_input = gr.Textbox(
placeholder="Paste atau ketik teks di sini untuk dianalisis...\n\nMinimal 20 karakter, maksimal 10.000 karakter.",
lines=8,
max_lines=20,
label="",
show_label=False,
)
scan_btn = gr.Button("Analisis Teks →", elem_id="scan-btn", variant="primary")
error_box = gr.Textbox(visible=False, elem_id="error-box", show_label=False)
result_html = gr.HTML(label="")
dummy = gr.HTML(visible=False)
def run_detection(text):
label_html, _, error, _ = detect_text(text)
if error:
return gr.update(value=error, visible=True), ""
return gr.update(visible=False), label_html
scan_btn.click(
fn=run_detection,
inputs=text_input,
outputs=[error_box, result_html]
)
text_input.submit(
fn=run_detection,
inputs=text_input,
outputs=[error_box, result_html]
)
gr.HTML("""
""")
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)