Spaces:
Sleeping
Sleeping
| 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)) | |