Filamsimg
fix : change to false
5a9b39d
Raw
History Blame Contribute Delete
20.3 kB
# 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"""
<div style="background-color: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; color: #f87171; border-radius: 12px; padding: 16px; font-family: sans-serif;">
<h4 style="margin-top: 0;">Error during prediction:</h4>
<p style="margin-bottom: 8px; font-family: monospace;">{str(e)}</p>
<details>
<summary style="cursor: pointer; font-size: 0.85rem; color: #f87171; font-weight: 600;">Lihat Detail Stacktrace</summary>
<pre style="margin-top: 8px; font-size: 0.8rem; font-family: monospace; white-space: pre-wrap; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px; border: 1px solid rgba(239, 68, 68, 0.2); overflow-x: auto; color: #fca5a5;">{tb}</pre>
</details>
</div>
"""
return error_html, {"error": str(e), "traceback": tb}
def generate_result_html(text, cleaned_text, label, confidence, sentiment, sentiment_score):
if not text:
return """
<div style='text-align: center; padding: 40px; color: #6b7280; font-family: "Outfit", sans-serif;'>
<svg style="margin: 0 auto 16px auto; width: 48px; height: 48px; stroke: currentColor; fill: none;" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<p style='font-size: 1.1rem; font-weight: 500; color: #9ca3af;'>Siap Menganalisis</p>
<p style='font-size: 0.9rem; color: #6b7280;'>Masukkan teks di sebelah kiri lalu klik tombol <b>Analyze Text 🛡️</b></p>
</div>
"""
conf_pct = f"{confidence * 100:.2f}%"
sent_pct = f"{sentiment_score * 100:.2f}%"
if label == "Spam":
class_badge = '<span class="result-badge badge-spam">🚫 SPAM / JUDI ONLINE</span>'
class_color = '#ef4444'
class_bg = 'rgba(239, 68, 68, 0.08)'
else:
class_badge = '<span class="result-badge badge-normal">✅ NORMAL / AMAN</span>'
class_color = '#10b981'
class_bg = 'rgba(16, 185, 129, 0.08)'
if sentiment == 'positive':
sent_badge = '<span class="result-badge badge-positive">😊 POSITIVE</span>'
sent_color = '#10b981'
elif sentiment == 'negative':
sent_badge = '<span class="result-badge badge-negative">😡 NEGATIVE</span>'
sent_color = '#ef4444'
else:
sent_badge = '<span class="result-badge badge-neutral">😐 NEUTRAL</span>'
sent_color = '#6b7280'
# Highlight perbedaan jika ada text cleansing
cleansing_info = ""
if text.strip().lower() != cleaned_text.strip().lower():
cleansing_info = f"""
<div style="margin-bottom: 20px; background: rgba(245, 158, 11, 0.05); border-radius: 12px; padding: 16px; border: 1px solid rgba(245, 158, 11, 0.2);">
<div style="font-size: 0.8rem; text-transform: uppercase; color: #f59e0b; letter-spacing: 0.05em; margin-bottom: 8px; font-weight: 600;">Text Cleansing Preview (Train-Serving Skew Fix)</div>
<div style="display: grid; grid-template-columns: 1fr; gap: 12px;">
<div>
<span style="font-size: 0.75rem; color: #9ca3af; font-weight: 500;">Original:</span>
<p style="margin: 4px 0 0 0; font-size: 0.95rem; color: #d1d5db; font-style: italic;">"{text}"</p>
</div>
<hr style="border: 0; border-top: 1px solid rgba(255, 255, 255, 0.08); margin: 4px 0;">
<div>
<span style="font-size: 0.75rem; color: #34d399; font-weight: 500;">Cleaned Text:</span>
<p style="margin: 4px 0 0 0; font-size: 0.95rem; color: #34d399; font-family: 'JetBrains Mono', monospace; font-weight: 600;">"{cleaned_text}"</p>
</div>
</div>
</div>
"""
else:
cleansing_info = """
<div style="margin-bottom: 20px; background: rgba(255, 255, 255, 0.02); border-radius: 12px; padding: 16px; border: 1px solid rgba(255, 255, 255, 0.05);">
<div style="font-size: 0.8rem; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; margin-bottom: 4px; font-weight: 600;">Text Cleansing Preview</div>
<span style="font-size: 0.8rem; color: #6b7280; font-style: italic;">(Teks sudah bersih. Tidak ada perubahan leet-speak atau spasi berlebih)</span>
</div>
"""
html = f"""
<div class="card" style="font-family: 'Outfit', sans-serif;">
<!-- Header -->
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255, 255, 255, 0.08); padding-bottom: 16px; margin-bottom: 20px;">
<h3 style="margin: 0; font-size: 1.25rem; font-weight: 700; color: #ffffff; display: flex; align-items: center; gap: 8px;">
Hasil Analisis 🛡️
</h3>
{class_badge}
</div>
<!-- Cleansing Info -->
{cleansing_info}
<!-- Metrics Row -->
<div style="display: grid; grid-template-columns: 1fr; gap: 16px;">
<!-- Spam Confidence -->
<div style="background: {class_bg}; border: 1px solid {class_color}33; border-radius: 12px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<div style="font-size: 0.8rem; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; font-weight: 600;">Spam Classifier Confidence</div>
<div style="font-size: 0.8rem; font-weight: 600; color: {class_color};">{label}</div>
</div>
<div style="font-size: 1.75rem; font-weight: 800; color: #ffffff; margin-bottom: 8px;">{conf_pct}</div>
<!-- Progress Bar -->
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.08); border-radius: 999px; overflow: hidden;">
<div style="width: {confidence * 100}%; height: 100%; background: {class_color}; border-radius: 999px;"></div>
</div>
</div>
<!-- Sentiment Confidence -->
<div style="background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 16px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
<div style="font-size: 0.8rem; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; font-weight: 600;">Sentiment Score</div>
{sent_badge}
</div>
<div style="font-size: 1.75rem; font-weight: 800; color: #ffffff; margin-bottom: 8px;">{sent_pct}</div>
<!-- Progress Bar -->
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.08); border-radius: 999px; overflow: hidden;">
<div style="width: {sentiment_score * 100}%; height: 100%; background: {sent_color}; border-radius: 999px;"></div>
</div>
</div>
</div>
</div>
"""
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("""
<div style="padding-top: 15px;">
<h1 class="dashboard-title">🛡️ ATHENA SHIELD MODEL</h1>
<p class="dashboard-subtitle">Indonesian Text Spam & Sentiment Analyzer Dashboard (Thesis Demo)</p>
</div>
""")
with gr.Row():
# Kolom Input
with gr.Column(scale=1):
gr.HTML("""
<div style="margin-bottom: 10px;">
<h4 style="margin: 0; font-size: 1.1rem; color: #ffffff;">Input Teks</h4>
<p style="margin: 4px 0 0 0; font-size: 0.85rem; color: #6b7280;">Masukkan teks SMS, chat, atau postingan sosial media berbahasa Indonesia.</p>
</div>
""")
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("""
<div style="margin-top: 30px; padding: 15px; border-top: 1px solid rgba(255, 255, 255, 0.08); text-align: center; font-size: 0.85rem; color: #6b7280;">
<p style="margin: 0 0 4px 0; font-weight: 500; color: #9ca3af;">Aplikasi Pendukung Skripsi / Tugas Akhir</p>
<p style="margin: 0;">Model Klasifikasi: <b>BERT (filamss/bert-judol-indonesia)</b> | Analisis Sentimen: <b>IndoBERT Sentiment</b></p>
</div>
""")
# 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()