""" DFK Content Classification โ HuggingFace Spaces (CPU Basic โ Gratis) ===================================================================== Model : ggapar/Ministral-3-8B-Base-2512-DFK (LoRA adapter) Base : mistralai/Ministral-3-8B-Base-2512 (float32, CPU) GPU : CPU Basic (gratis, tanpa GPU) Catatan: Inference lebih lambat (~2-5 menit/request) karena CPU only """ import os import re import gc import torch import numpy as np import gradio as gr from collections import Counter from transformers import AutoModelForCausalLM, AutoTokenizer, Mistral3ForConditionalGeneration from peft import PeftModel # ================================================================ # KONFIGURASI # ================================================================ BASE_MODEL = "mistralai/Ministral-3-8B-Base-2512" ADAPTER_REPO = "ggapar/Ministral-3-8B-Base-2512-DFK" HF_TOKEN = os.environ.get("HF_TOKEN", "") SYSTEM_PROMPT = ( "Anda adalah sistem deteksi konten DFK (Disinformasi, Fitnah, Kebencian). " "Klasifikasikan teks ke dalam: Fakta, Disinformasi, Fitnah, atau Ujaran Kebencian. " "Berikan label dan penjelasan yang jelas." ) LABEL_INFO = { "fakta" : ("๐ข", "#dcfce7", "#166534", "Konten yang sesuai dengan fakta"), "disinformasi" : ("๐ด", "#fee2e2", "#991b1b", "Informasi yang menyesatkan"), "fitnah" : ("๐ ", "#ffedd5", "#9a3412", "Tuduhan tanpa bukti"), "ujaran_kebencian": ("โซ", "#f1f5f9", "#1e293b", "Konten menyerang kelompok tertentu"), "unknown" : ("โช", "#f8fafc", "#64748b", "Label tidak terdeteksi"), } # ================================================================ # LOAD MODEL โ di CPU dulu, GPU dialokasikan saat inference # Dengan ZeroGPU, model di-load ke CPU saat startup # GPU baru dialokasikan saat fungsi @spaces.GPU dipanggil # ================================================================ print("Loading tokenizer...") tokenizer = AutoTokenizer.from_pretrained( ADAPTER_REPO, trust_remote_code = True, token = HF_TOKEN or None, ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token print("Loading base model (CPU, float32)...") # Ministral-3-8B menggunakan Mistral3 architecture (VLM) # Harus pakai Mistral3ForConditionalGeneration, bukan AutoModelForCausalLM base_model = Mistral3ForConditionalGeneration.from_pretrained( BASE_MODEL, dtype = torch.float32, # โ CPU butuh float32 device_map = "cpu", trust_remote_code = True, token = HF_TOKEN or None, low_cpu_mem_usage = True, ) print("Loading LoRA adapter...") model = PeftModel.from_pretrained( base_model, ADAPTER_REPO, token = HF_TOKEN or None, ) model.eval() print("โ Model loaded ke CPU โ siap inference (estimasi 2-5 menit/request)") # ================================================================ # HELPER FUNCTIONS # ================================================================ def extract_label(text: str) -> str: t = text.lower().strip() if "ujaran kebencian" in t[:80] or "ujaran_kebencian" in t[:80]: return "ujaran_kebencian" m = re.search(r'label\s*:\s*\*{0,2}([\w\s]+?)\*{0,2}[.,]', t) if m: lbl = m.group(1).strip() for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]: if kw in lbl: return kw.replace(" ", "_") for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]: if kw in t[:80]: return kw.replace(" ", "_") for kw in ["ujaran kebencian", "disinformasi", "fitnah", "fakta"]: if kw in t: return kw.replace(" ", "_") return "unknown" def extract_reasoning(text: str) -> str: m = re.search(r'penjelasan\s*:\s*(.*)', text, re.DOTALL | re.IGNORECASE) if m: return m.group(1).strip() lines = text.strip().split('\n') return ' '.join(lines[1:]).strip() if len(lines) > 1 else text.strip() def compute_mtla_confidence(scores_list, gen_ids, K: int = 10) -> float: K_act = min(K, len(scores_list), len(gen_ids)) log_probs = [] for t in range(K_act): probs = torch.softmax(scores_list[t], dim=-1) tok_prob = probs[0, gen_ids[t].item()].item() log_probs.append(np.log(max(tok_prob, 1e-10))) avg_lp = float(np.mean(log_probs)) return round(float(1.0 / (1.0 + np.exp(-(avg_lp + 2.5) * 1.5))), 4) # ================================================================ # FUNGSI INFERENCE โ decorator @spaces.GPU wajib untuk ZeroGPU # GPU dialokasikan hanya saat fungsi ini dipanggil # ================================================================ def classify_dfk(text: str, num_trials: int, temperature: float): if not text or not text.strip(): return ("โ", "0%", "โ", "โ", "โ", "Masukkan teks yang ingin diklasifikasi.", [], "") device = "cpu" messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"Klasifikasikan konten berikut:\n{text}"}, ] prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer( [prompt] * int(num_trials), return_tensors = "pt", padding = True, truncation = True, max_length = 1900, ).to(device) with torch.inference_mode(): out = model.generate( **inputs, max_new_tokens = 256, temperature = float(temperature), do_sample = True, return_dict_in_generate = True, output_scores = True, use_cache = True, ) # Kumpulkan hasil per trial trials = [] for i in range(int(num_trials)): gen_ids = out.sequences[i][inputs.input_ids.shape[1]:] gen_text = tokenizer.decode(gen_ids, skip_special_tokens=True) scores_i = [s[i:i+1] for s in out.scores] conf = compute_mtla_confidence(scores_i, gen_ids, K=10) trials.append({ "label" : extract_label(gen_text), "reasoning": extract_reasoning(gen_text), "confidence": conf, }) # Voting vote = Counter(t["label"] for t in trials) best_label, count = vote.most_common(1)[0] winners = [t for t in trials if t["label"] == best_label] avg_conf = float(np.mean([t["confidence"] for t in winners])) best_reason = max(winners, key=lambda x: x["confidence"])["reasoning"] is_ambiguous = count == 1 or avg_conf < 0.45 emoji, bg, fg, desc = LABEL_INFO.get(best_label, LABEL_INFO["unknown"]) label_display = f"{emoji} {best_label.upper().replace('_', ' ')}" conf_pct = f"{avg_conf * 100:.1f}%" consistency = f"{count}/{int(num_trials)}" ambig_status = "โ ๏ธ Ambigu โ model ragu-ragu" if is_ambiguous else "โ Model yakin" label_html = f"""
Deteksi Disinformasi ยท Fitnah ยท Ujaran Kebencian ยท Fakta
Model: Ministral-3-8B + LoRA Fine-tuning ยท Bahasa Indonesia