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}%
{word_count:,}
Kata
{char_count:,}
Karakter
{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)