TruthLens-Backend / services /analysis /clickbait_classifier.py
Gargi Monga
Deploy backend
c8b1fd7
Raw
History Blame Contribute Delete
4.69 kB
import os
from typing import Dict, Any
BASE_MODEL_NAME = "distilbert-base-uncased"
# Path to the LoRA adapter produced by training/train_clickbait_classifier.py
_MODEL_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"models",
"clickbait-lora",
)
_DEFAULT_RESULT: Dict[str, Any] = {
"clickbait": False,
"clickbait_score": 0.0,
"model_status": "not_loaded",
"explanation": "Finetuned clickbait model not found. Run "
"backend/training/train_clickbait_classifier.py and copy "
"the output into backend/models/clickbait-lora/ to enable "
"this feature.",
}
# Lazy-loaded globals - we only import torch/transformers and load
# weights the first time analyze_clickbait() is actually called, so a
# missing/broken model never slows down or crashes app startup.
_model = None
_tokenizer = None
_load_attempted = False
_load_error = None
def _try_load_model():
global _model, _tokenizer, _load_attempted, _load_error
if _load_attempted:
return
_load_attempted = True
if not os.path.isdir(_MODEL_DIR):
_load_error = f"Model directory not found at {_MODEL_DIR}"
return
try:
import torch # noqa: F401
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
tokenizer = AutoTokenizer.from_pretrained(_MODEL_DIR)
base_model = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL_NAME,
num_labels=2,
id2label={0: "not_clickbait", 1: "clickbait"},
label2id={"not_clickbait": 0, "clickbait": 1},
)
model = PeftModel.from_pretrained(base_model, _MODEL_DIR)
model.eval()
_model = model
_tokenizer = tokenizer
except Exception as e: # noqa: BLE001 - intentionally broad, this must never crash the app
_load_error = str(e)
_model = None
_tokenizer = None
def _get_headline(text: str) -> str:
"""Use the first sentence as a headline proxy if no explicit title exists."""
if not text:
return ""
first_sentence = text.strip().split(".")[0]
# Keep it short - headlines/titles are short by nature
return first_sentence[:200]
def analyze_clickbait(text: str) -> Dict[str, Any]:
"""
Returns a dict:
{
"clickbait": bool,
"clickbait_score": float (0.0 - 1.0),
"model_status": "loaded" | "not_loaded" | "error",
"explanation": str
}
This function NEVER raises. Any failure path returns a safe default
so callers (routes/analyze.py) don't need special-case error handling
beyond what they already do for the other analyzers.
"""
if not text or not text.strip():
return {
**_DEFAULT_RESULT,
"model_status": "not_loaded" if _model is None else "loaded",
"explanation": "Empty input text.",
}
_try_load_model()
if _model is None or _tokenizer is None:
result = dict(_DEFAULT_RESULT)
if _load_error:
result["explanation"] = (
"Clickbait model unavailable, using neutral default. "
f"(Reason: {_load_error})"
)
return result
try:
import torch
headline = _get_headline(text)
inputs = _tokenizer(
headline,
return_tensors="pt",
truncation=True,
max_length=64,
)
with torch.no_grad():
outputs = _model(**inputs)
probs = torch.softmax(outputs.logits, dim=-1)[0]
clickbait_score = float(probs[1].item())
return {
"clickbait": clickbait_score >= 0.5,
"clickbait_score": round(clickbait_score, 4),
"model_status": "loaded",
"explanation": (
f"Finetuned model classified this as "
f"{'likely' if clickbait_score >= 0.5 else 'unlikely'} clickbait "
f"(confidence {round(clickbait_score, 2)})."
),
}
except Exception as e: # noqa: BLE001 - never let inference errors reach the caller
return {
**_DEFAULT_RESULT,
"model_status": "error",
"explanation": f"Clickbait model inference failed, using neutral default. ({e})",
}
if __name__ == "__main__":
samples = [
"You Won't Believe What Happened Next!",
"Central Bank Raises Interest Rates by 0.25 Percent",
]
for s in samples:
print(s, "->", analyze_clickbait(s))