Spaces:
Sleeping
Sleeping
| """ | |
| Spoiler Detector v2 — Backend Flask | |
| Model: SVM, Random Forest, XGBoost (Pipeline + TF-IDF), BiLSTM (Keras), IndoBERT (HuggingFace) | |
| Struktur folder models/ yang dibutuhkan: | |
| models/ | |
| svm_model.pkl <- best_svm (Pipeline: TF-IDF + LinearSVC) | |
| rf_model.pkl <- best_rf (Pipeline: TF-IDF + RF) | |
| xgb_model.pkl <- best_xgb (Pipeline: TF-IDF + XGBoost) | |
| lstm_model.keras <- model BiLSTM Keras | |
| tokenizer.pkl <- Keras Tokenizer | |
| lstm_threshold.txt <- satu baris angka: best_threshold BiLSTM | |
| indobert/ <- folder hasil trainer.save_model() | |
| config.json | |
| model.safetensors | |
| tokenizer_config.json | |
| ... | |
| bert_threshold.txt <- satu baris angka: best_threshold IndoBERT | |
| metrics.json <- hasil evaluasi semua model dari Colab | |
| CARA EKSPOR -> lihat export_models_v2.py | |
| """ | |
| from flask import Flask, request, jsonify, render_template | |
| import os, re, pickle, warnings | |
| import numpy as np | |
| warnings.filterwarnings("ignore") | |
| app = Flask(__name__) | |
| # CONFIG | |
| MODEL_DIR = os.path.join(os.path.dirname(__file__), "models") | |
| MAXLEN_LSTM = 1773 # p90 review_text_clean | |
| MAX_LEN_BERT = 256 # MAX_LEN notebook IndoBERT | |
| # PREPROCESSING | |
| def clean_text(text: str) -> str: | |
| if not text or not isinstance(text, str): | |
| return "" | |
| text = re.sub(r"http\S+|www\.\S+", " ", text) | |
| text = re.sub(r"@\w+|#\w+", " ", text) | |
| text = re.sub(r"<[^>]+>", " ", text) | |
| text = re.sub(r"[^\w\s.,!?]", " ", text) | |
| text = re.sub(r"\s+", " ", text).strip() | |
| return text.lower() | |
| def stem_text_for_ml(text: str, stemmer, stopwords_set: set) -> str: | |
| if not text: | |
| return "" | |
| WHITELIST = {"ternyata", "akhirnya", "tiba", "tiba-tiba"} | |
| tokens = [t for t in text.split() if t not in stopwords_set or t in WHITELIST] | |
| return stemmer.stem(" ".join(tokens)) | |
| # LAZY CACHE | |
| _models = {} | |
| _stemmer = None | |
| _stopwords = None | |
| def get_stopwords(): | |
| global _stopwords | |
| if _stopwords is None: | |
| try: | |
| import nltk | |
| from nltk.corpus import stopwords as nltk_sw | |
| nltk.download("stopwords", quiet=True) | |
| from Sastrawi.StopWordRemover.StopWordRemoverFactory import StopWordRemoverFactory | |
| WHITELIST = {"ternyata", "akhirnya", "tiba", "tiba-tiba"} | |
| _stopwords = (set(StopWordRemoverFactory().get_stop_words()) | | |
| set(nltk_sw.words("indonesian"))) - WHITELIST | |
| except Exception as e: | |
| print(f"[WARN] stopwords gagal: {e}") | |
| _stopwords = set() | |
| return _stopwords | |
| def get_stemmer(): | |
| global _stemmer | |
| if _stemmer is None: | |
| try: | |
| from Sastrawi.Stemmer.StemmerFactory import StemmerFactory | |
| _stemmer = StemmerFactory().create_stemmer() | |
| except Exception as e: | |
| print(f"[WARN] stemmer gagal: {e}") | |
| return _stemmer | |
| def load_pkl(name: str, filename: str): | |
| if name in _models: | |
| return _models[name] | |
| path = os.path.join(MODEL_DIR, filename) | |
| if not os.path.exists(path): | |
| return None | |
| try: | |
| import joblib | |
| obj = joblib.load(path) | |
| _models[name] = obj | |
| print(f"[OK] {name} dimuat") | |
| return obj | |
| except Exception as e: | |
| print(f"[ERROR] {name}: {e}") | |
| return None | |
| def load_lstm(): | |
| if "lstm" in _models: | |
| return _models["lstm"] | |
| path = os.path.join(MODEL_DIR, "lstm_model.keras") | |
| if not os.path.exists(path): | |
| return None | |
| try: | |
| from tensorflow.keras.models import load_model as keras_load | |
| obj = keras_load(path) | |
| _models["lstm"] = obj | |
| print("[OK] BiLSTM dimuat") | |
| return obj | |
| except Exception as e: | |
| print(f"[ERROR] BiLSTM: {e}") | |
| return None | |
| def load_bert(): | |
| if "bert_model" in _models: | |
| return _models["bert_model"], _models["bert_tokenizer"] | |
| bert_dir = os.path.join(MODEL_DIR, "indobert") | |
| if not os.path.exists(bert_dir): | |
| return None, None | |
| try: | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| import torch | |
| tok = AutoTokenizer.from_pretrained(bert_dir) | |
| model = AutoModelForSequenceClassification.from_pretrained(bert_dir) | |
| model.eval() | |
| _models["bert_model"] = model | |
| _models["bert_tokenizer"] = tok | |
| print("[OK] IndoBERT dimuat") | |
| return model, tok | |
| except Exception as e: | |
| print(f"[ERROR] IndoBERT: {e}") | |
| return None, None | |
| def read_threshold(filename: str, default: float = 0.5) -> float: | |
| path = os.path.join(MODEL_DIR, filename) | |
| if os.path.exists(path): | |
| try: | |
| return float(open(path).read().strip()) | |
| except: | |
| pass | |
| return default | |
| # PREDIKSI | |
| def predict_ml(text_clean: str, model_key: str) -> dict: | |
| filemap = {"svm": "svm_model.pkl", "rf": "rf_model.pkl", "xgb": "xgb_model.pkl"} | |
| model = load_pkl(model_key, filemap[model_key]) | |
| if model is None: | |
| return {"error": f"Model '{model_key.upper()}' belum tersedia di folder models/."} | |
| stemmer = get_stemmer() | |
| stopwords = get_stopwords() | |
| text_stem = stem_text_for_ml(text_clean, stemmer, stopwords) if stemmer else text_clean | |
| label = int(model.predict([text_stem])[0]) | |
| if hasattr(model, "predict_proba"): | |
| proba = model.predict_proba([text_stem])[0] | |
| prob_spoiler = float(proba[1]) | |
| elif hasattr(model, "decision_function"): | |
| score = float(model.decision_function([text_stem])[0]) | |
| prob_spoiler = 1 / (1 + np.exp(-score)) | |
| else: | |
| prob_spoiler = 1.0 if label == 1 else 0.0 | |
| return { | |
| "label" : label, | |
| "is_spoiler" : bool(label == 1), | |
| "prob_spoiler" : round(prob_spoiler * 100, 1), | |
| "prob_nonspoiler" : round((1 - prob_spoiler) * 100, 1), | |
| "text_processed" : text_stem[:200] + "..." if len(text_stem) > 200 else text_stem, | |
| } | |
| def predict_lstm(text_clean: str) -> dict: | |
| try: | |
| from tensorflow.keras.preprocessing.sequence import pad_sequences | |
| except ImportError: | |
| return {"error": "TensorFlow tidak terinstall."} | |
| tokenizer = load_pkl("tokenizer", "tokenizer.pkl") | |
| model = load_lstm() | |
| if tokenizer is None or model is None: | |
| return {"error": "Model BiLSTM atau tokenizer belum tersedia di folder models/."} | |
| seq = tokenizer.texts_to_sequences([text_clean]) | |
| padded = pad_sequences(seq, maxlen=MAXLEN_LSTM, padding="post", truncating="post") | |
| prob_spoiler = float(model.predict(padded, verbose=0)[0][0]) | |
| threshold = read_threshold("lstm_threshold.txt", 0.5) | |
| label = 1 if prob_spoiler >= threshold else 0 | |
| return { | |
| "label" : label, | |
| "is_spoiler" : bool(label == 1), | |
| "prob_spoiler" : round(prob_spoiler * 100, 1), | |
| "prob_nonspoiler" : round((1 - prob_spoiler) * 100, 1), | |
| "text_processed" : text_clean[:200] + "..." if len(text_clean) > 200 else text_clean, | |
| } | |
| import concurrent.futures | |
| def _bert_inference(text_clean: str, model_dir: str, max_len: int) -> dict: | |
| import os, torch | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| bert_dir = os.path.join(model_dir, "indobert") | |
| tok = AutoTokenizer.from_pretrained(bert_dir) | |
| model = AutoModelForSequenceClassification.from_pretrained(bert_dir) | |
| model.eval() | |
| inputs = tok( | |
| text_clean, | |
| max_length=max_len, | |
| truncation=True, | |
| padding="max_length", | |
| return_tensors="pt" | |
| ) | |
| with torch.no_grad(): | |
| logits = model(**inputs).logits | |
| probs = torch.softmax(logits, dim=-1)[0].detach().cpu().numpy() | |
| prob_spoiler = float(probs[1]) | |
| # baca threshold langsung di sini | |
| threshold_path = os.path.join(model_dir, "bert_threshold.txt") | |
| threshold = 0.5 | |
| if os.path.exists(threshold_path): | |
| try: | |
| threshold = float(open(threshold_path).read().strip()) | |
| except: | |
| pass | |
| label = 1 if prob_spoiler >= threshold else 0 | |
| return { | |
| "label": label, | |
| "is_spoiler": bool(label == 1), | |
| "prob_spoiler": round(prob_spoiler * 100, 1), | |
| "prob_nonspoiler": round((1 - prob_spoiler) * 100, 1), | |
| "text_processed": text_clean[:200] + "..." if len(text_clean) > 200 else text_clean, | |
| } | |
| def predict_bert(text_clean: str) -> dict: | |
| try: | |
| import torch | |
| except ImportError: | |
| return {"error": "PyTorch tidak terinstall."} | |
| with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: | |
| future = executor.submit(_bert_inference, text_clean, MODEL_DIR, MAX_LEN_BERT) | |
| try: | |
| return future.result(timeout=60) | |
| except Exception as e: | |
| return {"error": f"IndoBERT inference gagal: {str(e)}"} | |
| # ROUTES | |
| def index(): | |
| return render_template("index.html") | |
| def predict(): | |
| data = request.get_json(silent=True) or {} | |
| text = data.get("text", "").strip() | |
| model_choice = data.get("model", "svm").lower() | |
| if not text: | |
| return jsonify({"error": "Teks tidak boleh kosong."}), 400 | |
| if len(text) < 20: | |
| return jsonify({"error": "Teks terlalu pendek (minimal 20 karakter)."}), 400 | |
| text_clean = clean_text(text) | |
| if model_choice in ("svm", "rf", "xgb"): | |
| result = predict_ml(text_clean, model_choice) | |
| elif model_choice == "lstm": | |
| result = predict_lstm(text_clean) | |
| elif model_choice == "bert": | |
| result = predict_bert(text_clean) | |
| else: | |
| return jsonify({"error": f"Model '{model_choice}' tidak dikenali."}), 400 | |
| if "error" in result: | |
| return jsonify(result), 503 | |
| result["input_length"] = len(text) | |
| result["clean_length"] = len(text_clean) | |
| result["model_used"] = model_choice.upper() | |
| return jsonify(result) | |
| def status(): | |
| checks = { | |
| "svm" : os.path.exists(os.path.join(MODEL_DIR, "svm_model.pkl")), | |
| "rf" : os.path.exists(os.path.join(MODEL_DIR, "rf_model.pkl")), | |
| "xgb" : os.path.exists(os.path.join(MODEL_DIR, "xgb_model.pkl")), | |
| "tokenizer": os.path.exists(os.path.join(MODEL_DIR, "tokenizer.pkl")), | |
| "lstm" : os.path.exists(os.path.join(MODEL_DIR, "lstm_model.keras")), | |
| "bert" : os.path.exists(os.path.join(MODEL_DIR, "indobert", "config.json")), | |
| } | |
| return jsonify({"models": checks}) | |
| def metrics(): | |
| path = os.path.join(MODEL_DIR, "metrics.json") | |
| if os.path.exists(path): | |
| import json | |
| with open(path) as f: | |
| return jsonify(json.load(f)) | |
| return jsonify({"error": "metrics.json belum tersedia."}), 404 | |
| if __name__ == "__main__": | |
| os.makedirs(MODEL_DIR, exist_ok=True) | |
| print("Spoiler Detector v2 — NLP Kelompok 3") | |
| print(f"Folder model : {MODEL_DIR}") | |
| print("Buka browser : http://127.0.0.1:5000") | |
| app.run(debug=False, host="0.0.0.0", port=7860) | |