# Deteksi apakah berjalan di Hugging Face Spaces ZeroGPU (Harus di-import paling pertama sebelum torch/transformers) try: import spaces HAS_SPACES = True except ImportError: HAS_SPACES = False # Jika tidak berjalan di ZeroGPU (misal lokal), buat mock decorator if not HAS_SPACES: class spaces: @staticmethod def GPU(duration=None): def decorator(fn): return fn return decorator import gradio as gr from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch import torch.nn.functional as F import os import random import numpy as np import re import unicodedata import string # ========================================== # PENGATURAN EFISIENSI KUOTA HARIANS (ZeroGPU) # ========================================== # Set ke False jika ingin kuota 100% UNLIMITED (Bebas Batasan Harian) # Model BERT berukuran kecil sangat cepat berjalan di CPU (~50-100ms per prediksi). USE_GPU = False # ========================================== # Set seed untuk hasil yang konsisten (Deterministik) SEED = 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) # Load Model & Tokenizer dari Hugging Face Hub secara global MODEL_PATH = "filamss/bert-judol-indonesia" print(f"Loading classification model: {MODEL_PATH}") tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH) model = AutoModelForSequenceClassification.from_pretrained(MODEL_PATH) # Pola Resmi ZeroGPU: Pindahkan model ke 'cuda' di level global (module level) # ZeroGPU akan meng-intercept pemanggilan ini, menyimpannya di CPU, dan memindahkannya # ke GPU secara otomatis hanya saat masuk ke dalam fungsi yang di-decorate @spaces.GPU. if HAS_SPACES and USE_GPU: print("ZeroGPU detected: Registering model to CUDA (intercepted by ZeroGPU)") model = model.to("cuda") else: print("Running on CPU mode") model = model.to("cpu") model.eval() # Load Sentiment Model & Tokenizer secara manual (Untuk menghindari CUDA init tersembunyi dari pipeline) SENTIMENT_MODEL_PATH = "mdhugol/indonesia-bert-sentiment-classification" print(f"Loading sentiment model (CPU): {SENTIMENT_MODEL_PATH}") sentiment_tokenizer = AutoTokenizer.from_pretrained(SENTIMENT_MODEL_PATH) sentiment_model = AutoModelForSequenceClassification.from_pretrained(SENTIMENT_MODEL_PATH) sentiment_model = sentiment_model.to("cpu") sentiment_model.eval() LABEL_MAP = { 0: "Normal", 1: "Spam" } SENTIMENT_MAP = { 0: "positive", 1: "neutral", 2: "negative" } # --- TEXT CLEANSING UTILITIES FROM COLAB --- LEET_DICT = {"4":"a", "7":"t", "0":"o", "1":"i", "3":"e", "5":"s", "@":"a"} url_re = re.compile(r"(https?://\S+|www\.\S+)", re.IGNORECASE) DOMAIN_EXCLUDE = re.compile(r"\.(com|net|org|id|xyz|biz|info|io|gov|edu)\b", re.IGNORECASE) def fold_spaced_words(text): return re.sub(r"\b(?:[a-z]\s+){2,}[a-z]\b", lambda m: m.group(0).replace(" ", ""), text) def selective_leet_fix(match): word = match.group(0) if DOMAIN_EXCLUDE.search(word): return word if re.search(r'[a-z]', word): suffix_digits = re.search(r'\d+\b$', word) safe_index = suffix_digits.start() if suffix_digits else len(word) new_word = "" for i, char in enumerate(word): if char in LEET_DICT and i < safe_index: new_word += LEET_DICT[char] else: new_word += char return new_word return word def clean_text(text): if not isinstance(text, str): return "" t = unicodedata.normalize("NFKC", text).lower() t = re.sub(r'\b[a-z0-9@.]+\b', selective_leet_fix, t) t = fold_spaced_words(t) t = re.sub(r'([a-z])\1{2,}', r'\1\1', t) t = t.encode('ascii', 'ignore').decode('ascii') t = t.translate(str.maketrans('', '', string.punctuation)) t = re.sub(r"\s+", " ", t).strip() return t # ------------------------------------------- # FUNGSI INFERENSI KHUSUS GPU (Hanya memakan kuota GPU selama proses kalkulasi model matematika) # Dibatasi maksimum 2 detik per panggilan untuk efisiensi ekstrim @spaces.GPU(duration=2) def predict_spam_gpu(input_ids, attention_mask): # Set seed GPU di dalam container GPU ZeroGPU if torch.cuda.is_available(): torch.cuda.manual_seed_all(SEED) # Model secara otomatis dipindahkan ke CUDA oleh ZeroGPU. # Kita hanya perlu memindahkan input tensor ke CUDA. input_ids = input_ids.to("cuda") attention_mask = attention_mask.to("cuda") with torch.no_grad(): outputs = model(input_ids=input_ids, attention_mask=attention_mask) return outputs.logits.cpu() # FUNGSI INFERENSI KHUSUS CPU (Tidak memakan kuota GPU sama sekali) def predict_spam_cpu(input_ids, attention_mask): with torch.no_grad(): outputs = model(input_ids=input_ids, attention_mask=attention_mask) return outputs.logits def predict(text): if not text or not text.strip(): return generate_result_html("", "", "", 0, "", 0), {} try: # 1. Text Cleansing (Dijalankan di CPU - Hemat Kuota) cleaned_text = clean_text(text) # 2. Tokenization (Dijalankan di CPU - Hemat Kuota) inputs = tokenizer(cleaned_text, return_tensors="pt", truncation=True, padding=True, max_length=128) input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] # 3. Model Inference (Pilih GPU atau CPU) if USE_GPU and HAS_SPACES: # Menggunakan GPU dinamis, hanya memakan kuota beberapa milidetik saja logits = predict_spam_gpu(input_ids, attention_mask) else: # Berjalan di CPU secara lokal atau Cloud CPU (Kuota GPU terpakai = 0%) logits = predict_spam_cpu(input_ids, attention_mask) probs = F.softmax(logits, dim=1) confidence, predicted_class_id = torch.max(probs, dim=1) label_id = predicted_class_id.item() label_name = LABEL_MAP.get(label_id, f"LABEL_{label_id}") conf_score = confidence.item() # 4. Sentiment Analysis (Dijalankan di CPU secara manual - 100% Bebas Kuota GPU) sentiment_inputs = sentiment_tokenizer(cleaned_text, return_tensors="pt", truncation=True, padding=True, max_length=128) sentiment_inputs = {k: v.to("cpu") for k, v in sentiment_inputs.items()} with torch.no_grad(): sentiment_outputs = sentiment_model(**sentiment_inputs) sentiment_probs = F.softmax(sentiment_outputs.logits, dim=1) sentiment_confidence, sentiment_predicted_class_id = torch.max(sentiment_probs, dim=1) sentiment_id = sentiment_predicted_class_id.item() sentiment_label = SENTIMENT_MAP.get(sentiment_id, 'neutral') sentiment_score = sentiment_confidence.item() # Generate HTML Report & JSON result_html = generate_result_html(text, cleaned_text, label_name, conf_score, sentiment_label, sentiment_score) raw_json = { "input_original": text, "input_cleaned": cleaned_text, "classification": { "label": label_name, "label_id": label_id, "confidence": conf_score }, "sentiment": { "label": sentiment_label, "score": sentiment_score } } return result_html, raw_json except Exception as e: import traceback tb = traceback.format_exc() error_html = f"""

Error during prediction:

{str(e)}

Lihat Detail Stacktrace
{tb}
""" return error_html, {"error": str(e), "traceback": tb} def generate_result_html(text, cleaned_text, label, confidence, sentiment, sentiment_score): if not text: return """

Siap Menganalisis

Masukkan teks di sebelah kiri lalu klik tombol Analyze Text ๐Ÿ›ก๏ธ

""" conf_pct = f"{confidence * 100:.2f}%" sent_pct = f"{sentiment_score * 100:.2f}%" if label == "Spam": class_badge = '๐Ÿšซ SPAM / JUDI ONLINE' class_color = '#ef4444' class_bg = 'rgba(239, 68, 68, 0.08)' else: class_badge = 'โœ… NORMAL / AMAN' class_color = '#10b981' class_bg = 'rgba(16, 185, 129, 0.08)' if sentiment == 'positive': sent_badge = '๐Ÿ˜Š POSITIVE' sent_color = '#10b981' elif sentiment == 'negative': sent_badge = '๐Ÿ˜ก NEGATIVE' sent_color = '#ef4444' else: sent_badge = '๐Ÿ˜ NEUTRAL' sent_color = '#6b7280' # Highlight perbedaan jika ada text cleansing cleansing_info = "" if text.strip().lower() != cleaned_text.strip().lower(): cleansing_info = f"""
Text Cleansing Preview (Train-Serving Skew Fix)
Original:

"{text}"


Cleaned Text:

"{cleaned_text}"

""" else: cleansing_info = """
Text Cleansing Preview
(Teks sudah bersih. Tidak ada perubahan leet-speak atau spasi berlebih)
""" html = f"""

Hasil Analisis ๐Ÿ›ก๏ธ

{class_badge}
{cleansing_info}
Spam Classifier Confidence
{label}
{conf_pct}
Sentiment Score
{sent_badge}
{sent_pct}
""" return html # Custom CSS untuk Gradio Dashboard custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); /* Global Fonts */ body, .gradio-container { font-family: 'Outfit', sans-serif !important; } .dashboard-title { font-family: 'Outfit', sans-serif; font-weight: 800; background: linear-gradient(135deg, #10b981, #3b82f6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; margin-bottom: 0.2rem; font-size: 2.2rem; letter-spacing: -0.03em; } .dashboard-subtitle { text-align: center; color: #9ca3af; font-size: 1rem; margin-bottom: 1.5rem; } .card { background: rgba(17, 24, 39, 0.6) !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 16px !important; padding: 20px !important; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4) !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; } .result-badge { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 9999px; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; } .badge-spam { background-color: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); } .badge-normal { background-color: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); } .badge-positive { background-color: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); } .badge-neutral { background-color: rgba(107, 114, 128, 0.15); color: #9ca3af; border: 1px solid rgba(107, 114, 128, 0.3); } .badge-negative { background-color: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); } /* Tombol Kustom */ .analyze-btn { background: linear-gradient(135deg, #10b981, #059669) !important; color: white !important; border: none !important; font-weight: 600 !important; } .analyze-btn:hover { box-shadow: 0 0 15px rgba(16, 185, 129, 0.4) !important; transform: translateY(-1px) !important; } """ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="indigo", neutral_hue="slate")) as demo: gr.HTML("""

๐Ÿ›ก๏ธ ATHENA SHIELD MODEL

Indonesian Text Spam & Sentiment Analyzer Dashboard (Thesis Demo)

""") with gr.Row(): # Kolom Input with gr.Column(scale=1): gr.HTML("""

Input Teks

Masukkan teks SMS, chat, atau postingan sosial media berbahasa Indonesia.

""") input_text = gr.Textbox( placeholder="Tulis atau tempel teks di sini...", lines=5, label="", elem_id="input-text" ) with gr.Row(): clear_btn = gr.Button("Clear ๐Ÿงน", variant="secondary") analyze_btn = gr.Button("Analyze Text ๐Ÿ›ก๏ธ", elem_classes=["analyze-btn"]) gr.Examples( examples=[ ["Halo mas, besok kuliah jam berapa ya?"], ["PROMO HADlAH Rp.100jt! d4p4tk4n b0nus m3n4r1k h4r1 1n1. Klik l1nk k4m1: http://judislot77.xyz"], ["Wah, layanannya sangat memuaskan dan adminnya ramah sekali!"], ["Aku kecewa bgt beli barang di toko ini, barangnya rusak pas nyampe."], ["Info d0ng g43s, cara d4ft4r m3mb3r b4ru g1m4n4 y4?"] ], inputs=input_text, label="Contoh Teks untuk Demo:" ) # Kolom Output with gr.Column(scale=1): output_html = gr.HTML(value=generate_result_html("", "", "", 0, "", 0)) with gr.Accordion("Raw JSON Output โš™๏ธ", open=False): output_json = gr.JSON(label="JSON Response") # Metadata Skripsi gr.HTML("""

Aplikasi Pendukung Skripsi / Tugas Akhir

Model Klasifikasi: BERT (filamss/bert-judol-indonesia) | Analisis Sentimen: IndoBERT Sentiment

""") # Events analyze_btn.click( fn=predict, inputs=input_text, outputs=[output_html, output_json] ) # Event untuk tombol Clear def clear_fields(): return "", generate_result_html("", "", "", 0, "", 0), {} clear_btn.click( fn=clear_fields, inputs=None, outputs=[input_text, output_html, output_json] ) if __name__ == "__main__": demo.launch()