| import time |
| import joblib |
| import numpy as np |
| import torch |
| from fastapi import FastAPI |
| from pydantic import BaseModel |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification, RobertaConfig, RobertaForSequenceClassification |
| from huggingface_hub import hf_hub_download |
| from safetensors.torch import load_file |
|
|
| SENTIMENT_MODEL_ID = "vanhai123/phobert-vi-comment-4class" |
| HATE_MODEL_ID = "visolex/visobert-hsd" |
| MAX_LENGTH = 128 |
|
|
| torch.set_num_threads(2) |
|
|
| app = FastAPI(title="Vietnamese Hate Speech Detection API") |
|
|
| print("Loading models... (chỉ chạy 1 lần khi Space khởi động)") |
|
|
| |
| sent_tok = AutoTokenizer.from_pretrained(SENTIMENT_MODEL_ID) |
| sent_model = AutoModelForSequenceClassification.from_pretrained(SENTIMENT_MODEL_ID).eval() |
| sent_labels = [sent_model.config.id2label[i] for i in range(sent_model.config.num_labels)] |
|
|
| |
| hate_tok = AutoTokenizer.from_pretrained(HATE_MODEL_ID) |
|
|
| hate_config = RobertaConfig( |
| vocab_size=15004, |
| hidden_size=768, |
| num_hidden_layers=12, |
| num_attention_heads=12, |
| intermediate_size=3072, |
| max_position_embeddings=514, |
| type_vocab_size=2, |
| pad_token_id=1, |
| bos_token_id=0, |
| eos_token_id=2, |
| num_labels=3, |
| id2label={0: "CLEAN", 1: "OFFENSIVE", 2: "HATE"}, |
| label2id={"CLEAN": 0, "OFFENSIVE": 1, "HATE": 2} |
| ) |
|
|
| hate_model = RobertaForSequenceClassification(hate_config) |
|
|
| |
| weights_path = hf_hub_download(repo_id=HATE_MODEL_ID, filename="model.safetensors") |
| state_dict = load_file(weights_path) |
|
|
| |
| fixed_state_dict = {} |
| for key, value in state_dict.items(): |
| new_key = key |
| if key.startswith("encoder."): |
| new_key = key.replace("encoder.", "roberta.", 1) |
| fixed_state_dict[new_key] = value |
|
|
| hate_model.load_state_dict(fixed_state_dict, strict=False) |
| hate_model.eval() |
| hate_labels = [hate_model.config.id2label[i] for i in range(hate_model.config.num_labels)] |
|
|
| |
| meta_bundle = joblib.load("meta_classifier.joblib") |
| meta_clf = meta_bundle["model"] |
|
|
| print("Models loaded successfully. Ready to serve.") |
|
|
|
|
| @torch.no_grad() |
| def get_probs(text, tok, model): |
| inputs = tok(text, truncation=True, max_length=MAX_LENGTH, return_tensors="pt") |
| logits = model(**inputs).logits |
| probs = torch.softmax(logits, dim=-1).squeeze(0).numpy() |
| return probs |
|
|
|
|
| class PredictRequest(BaseModel): |
| text: str |
|
|
|
|
| class PredictResponse(BaseModel): |
| final_label: str |
| confidence: float |
| sentiment: dict |
| hate_speech: dict |
| latency_ms: float |
|
|
|
|
| @app.get("/") |
| def health(): |
| return {"status": "ok"} |
|
|
|
|
| @app.post("/predict", response_model=PredictResponse) |
| def predict(req: PredictRequest): |
| start = time.time() |
|
|
| |
| p_sent = get_probs(req.text, sent_tok, sent_model) |
| p_hate = get_probs(req.text, hate_tok, hate_model) |
|
|
| |
| sent_label = sent_labels[int(np.argmax(p_sent))] |
| hate_label = hate_labels[int(np.argmax(p_hate))] |
|
|
| |
| features = np.concatenate([p_sent, p_hate]).reshape(1, -1) |
| final_pred = meta_clf.predict(features)[0] |
| final_proba = meta_clf.predict_proba(features)[0] |
| confidence = float(np.max(final_proba)) |
|
|
| elapsed_ms = (time.time() - start) * 1000 |
|
|
| return { |
| "final_label": final_pred, |
| "confidence": round(confidence, 4), |
| "sentiment": { |
| "label": sent_label, |
| "scores": {l: round(float(s), 4) for l, s in zip(sent_labels, p_sent)}, |
| }, |
| "hate_speech": { |
| "label": hate_label, |
| "scores": {l: round(float(s), 4) for l, s in zip(hate_labels, p_hate)}, |
| }, |
| "latency_ms": round(elapsed_ms, 1), |
| } |