nanasmuda's picture
Update app.py
b4aef7e verified
Raw
History Blame Contribute Delete
11 kB
"""
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
@app.route("/")
def index():
return render_template("index.html")
@app.route("/predict", methods=["POST"])
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)
@app.route("/status")
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})
@app.route("/metrics")
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)