TIF_E41253329/be-nlp/api/index.py

190 lines
6.3 KiB
Python

import os
import re
import string
import joblib
import requests
import nltk
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from nltk.tokenize import word_tokenize
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
# ---------------------------------------------------------
# 1. KONFIGURASI ENVIRONMENT & VERCEL BLOB
# ---------------------------------------------------------
BLOB_TOKEN = os.environ.get("BLOB_READ_WRITE_TOKEN", "vercel_blob_rw_rtoKzdsaGXwvNykn_6VpBFIALjlA8NPCMtwJmUVTOzisxfe")
MODEL_FILENAME = "model_sentiment.pkl"
MODEL_PATH = f"/tmp/{MODEL_FILENAME}"
# ---------------------------------------------------------
# 2. KONFIGURASI NLTK UNTUK VERCEL (SERVERLESS)
# ---------------------------------------------------------
nltk_data_path = '/tmp/nltk_data'
os.makedirs(nltk_data_path, exist_ok=True)
nltk.data.path.append(nltk_data_path)
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt', download_dir=nltk_data_path)
try:
nltk.data.find('tokenizers/punkt_tab')
except LookupError:
nltk.download('punkt_tab', download_dir=nltk_data_path)
# ---------------------------------------------------------
# 3. INISIALISASI SASTRAWI GLOBAL (PENCEGAH TIMEOUT VERCEL)
# ---------------------------------------------------------
factory = StemmerFactory()
global_stemmer = factory.create_stemmer()
app = FastAPI(title="Sentiment Analysis API")
# ---------------------------------------------------------
# 4. STATE MODEL GLOBAL
# ---------------------------------------------------------
model_state = {
"is_loaded": False,
"vectorizer": None,
"classifier": None,
"metrics": None,
"slangwords": {},
"stopwords": set(),
"data": None
}
class PredictRequest(BaseModel):
texts: list[str]
# ---------------------------------------------------------
# 5. FUNGSI UNDUH & MUAT MODEL DARI VERCEL BLOB
# ---------------------------------------------------------
def load_model_from_blob():
try:
headers = {"Authorization": f"Bearer {BLOB_TOKEN}"}
list_url = "https://blob.vercel-storage.com"
response = requests.get(list_url, headers=headers)
response.raise_for_status()
blobs = response.json().get('blobs', [])
pkl_url = next((b['url'] for b in blobs if b['pathname'].endswith(MODEL_FILENAME)), None)
if not pkl_url:
raise Exception(f"File {MODEL_FILENAME} tidak ditemukan di Vercel Blob.")
pkl_response = requests.get(pkl_url)
pkl_response.raise_for_status()
with open(MODEL_PATH, 'wb') as f:
f.write(pkl_response.content)
loaded_data = joblib.load(MODEL_PATH)
model_state["vectorizer"] = loaded_data['vectorizer']
model_state["classifier"] = loaded_data['classifier']
model_state["metrics"] = loaded_data['metrics']
model_state["slangwords"] = loaded_data['preprocessing_assets']['slangwords']
model_state["stopwords"] = loaded_data['preprocessing_assets']['stopwords']
model_state["data"] = loaded_data.get('data')
model_state["is_loaded"] = True
print("Model berhasil dimuat dari Vercel Blob!")
except Exception as e:
print(f"Gagal memuat model: {e}")
raise e
# ---------------------------------------------------------
# 6. PIPELINE PRAPEMROSESAN
# ---------------------------------------------------------
def cleaningText(text):
text = re.sub(r'@[A-Za-z0-9]+', ' ', text)
text = re.sub(r'#[A-Za-z0-9]+', ' ', text)
text = re.sub(r'RT[\s]', ' ', text)
text = re.sub(r"http\S+", ' ', text)
text = re.sub(r'[0-9]+', ' ', text)
text = re.sub(r'[^\w\s]', ' ', text)
text = text.replace('\n', ' ')
text = text.translate(str.maketrans('', '', string.punctuation))
return text.strip(' ')
def casefoldingText(text):
return text.lower()
def fast_fix_slangwords(text):
pattern = re.compile(r'\b\w+\b')
return pattern.sub(lambda x: model_state['slangwords'].get(x.group(), x.group()), text)
def fast_filteringText(text):
return [txt for txt in text if txt not in model_state['stopwords']]
def stemmingText(text_list):
return global_stemmer.stem(' '.join(text_list))
def fast_preprocess_pipeline(text):
text = cleaningText(text)
text = casefoldingText(text)
text = fast_fix_slangwords(text)
text = word_tokenize(text)
text = fast_filteringText(text)
text = stemmingText(text)
return text
# ---------------------------------------------------------
# 7. ENDPOINTS
# ---------------------------------------------------------
@app.get("/")
def read_root():
return {
"status": "Online",
"message": "API Sentimen Fast API siap digunakan.",
"model_loaded": model_state["is_loaded"]
}
@app.get("/info")
def get_info():
if not model_state["is_loaded"]:
raise HTTPException(status_code=503, detail="Model belum dimuat. Silakan akses POST /reload-model.")
return {
"status": "success",
"data": model_state["data"]
}
@app.get("/metrics")
def get_metrics():
if not model_state["is_loaded"]:
raise HTTPException(status_code=503, detail="Model belum dimuat. Silakan akses POST /reload-model.")
return {
"status": "success",
"metrics": model_state["metrics"]
}
@app.post("/reload-model")
def reload_model():
try:
load_model_from_blob()
return {"status": "success", "message": "Model berhasil diunduh ulang dan dimuat."}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Gagal memuat model: {str(e)}")
@app.post("/predict")
def predict(req: PredictRequest):
if not model_state["is_loaded"]:
raise HTTPException(status_code=503, detail="Model belum dimuat. Silakan akses POST /reload-model.")
results = []
try:
clean_texts = [fast_preprocess_pipeline(t) for t in req.texts]
vectorized_texts = model_state["vectorizer"].transform(clean_texts)
predictions = model_state["classifier"].predict(vectorized_texts)
for pred in predictions:
results.append({
"sentiment": pred.item()
})
return results
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error saat prediksi: {str(e)}")