Spaces:
Sleeping
Sleeping
| import os | |
| from fastapi import FastAPI, Form | |
| from fastapi.responses import HTMLResponse | |
| from contextlib import asynccontextmanager | |
| from huggingface_hub import hf_hub_download | |
| from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification | |
| MODEL = {} | |
| HF_REPO = os.getenv("HF_REPO", "amarshiv86/sentiment-analysis-imdb-model") | |
| async def lifespan(app: FastAPI): | |
| print(f"Downloading model from HF Hub: {HF_REPO} …") | |
| files = [ | |
| "model/config.json", | |
| "model/model.safetensors", | |
| "model/tokenizer.json", | |
| "model/tokenizer_config.json", | |
| ] | |
| local_dir = "/tmp/sentiment-model" | |
| os.makedirs(local_dir, exist_ok=True) | |
| for f in files: | |
| hf_hub_download( | |
| repo_id=HF_REPO, | |
| filename=f, | |
| repo_type="model", | |
| local_dir=local_dir, | |
| ) | |
| tokenizer = AutoTokenizer.from_pretrained(f"{local_dir}/model") | |
| # DistilBERT does not use token_type_ids — strip them to avoid TypeError | |
| tokenizer.model_input_names = ["input_ids", "attention_mask"] | |
| model = AutoModelForSequenceClassification.from_pretrained(f"{local_dir}/model") | |
| MODEL["clf"] = pipeline( | |
| "sentiment-analysis", | |
| model=model, | |
| tokenizer=tokenizer, | |
| truncation=True, | |
| max_length=256, | |
| ) | |
| print("Model loaded ✓") | |
| yield | |
| MODEL.clear() | |
| app = FastAPI(title="Sentiment Analysis API", lifespan=lifespan) | |
| HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>Sentiment Analyzer · AI Demo</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:wght@300;400;500&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root{ | |
| --bg:#0d1117;--surface:#161b22;--border:rgba(255,255,255,.08); | |
| --text:#e6edf3;--muted:#7d8590; | |
| --pos:#2ea043;--neg:#da3633; | |
| --pos-bg:rgba(46,160,67,.12);--neg-bg:rgba(218,54,51,.12); | |
| --accent:#58a6ff; | |
| } | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| body{background:var(--bg);color:var(--text);font-family:'DM Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem} | |
| .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:2.5rem;width:100%;max-width:640px} | |
| h1{font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;letter-spacing:-.02em;margin-bottom:.4rem} | |
| h1 span{color:var(--accent)} | |
| .sub{color:var(--muted);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;margin-bottom:2rem} | |
| label{font-size:.72rem;color:var(--muted);letter-spacing:.06em;display:block;margin-bottom:.4rem} | |
| textarea{width:100%;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:8px;color:var(--text);font-family:'DM Mono',monospace;font-size:.85rem;padding:.75rem 1rem;resize:vertical;min-height:130px;outline:none;transition:border-color .2s} | |
| textarea:focus{border-color:rgba(88,166,255,.5)} | |
| textarea::placeholder{color:var(--muted)} | |
| .btn{width:100%;margin-top:1rem;padding:.8rem;background:var(--accent);border:none;border-radius:8px;color:#0d1117;font-family:'Syne',sans-serif;font-size:.9rem;font-weight:700;cursor:pointer;transition:opacity .2s,transform .15s;letter-spacing:.03em} | |
| .btn:hover{opacity:.88;transform:translateY(-1px)} | |
| .btn:active{transform:translateY(0)} | |
| .divider{height:1px;background:var(--border);margin:1.5rem 0} | |
| .result{border-radius:10px;padding:1.2rem 1.5rem;display:flex;align-items:center;gap:1rem;animation:pop .3s ease} | |
| @keyframes pop{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}} | |
| .result.pos{background:var(--pos-bg);border:1px solid rgba(46,160,67,.3)} | |
| .result.neg{background:var(--neg-bg);border:1px solid rgba(218,54,51,.3)} | |
| .result-icon{font-size:2rem;line-height:1} | |
| .result-body h2{font-family:'Syne',sans-serif;font-size:1.1rem;font-weight:700} | |
| .result-body p{font-size:.75rem;color:var(--muted);margin-top:.2rem} | |
| .badge{margin-left:auto;padding:.3rem .8rem;border-radius:99px;font-size:.72rem;font-weight:600;font-family:'Syne',sans-serif} | |
| .pos .badge{background:rgba(46,160,67,.2);color:var(--pos)} | |
| .neg .badge{background:rgba(218,54,51,.2);color:var(--neg)} | |
| .bar-wrap{margin-top:1rem;background:rgba(255,255,255,.06);border-radius:99px;height:6px;overflow:hidden} | |
| .bar-fill{height:100%;border-radius:99px;transition:width .6s ease} | |
| .pos .bar-fill{background:var(--pos)} | |
| .neg .bar-fill{background:var(--neg)} | |
| .bar-label{display:flex;justify-content:space-between;font-size:.7rem;color:var(--muted);margin-top:.4rem} | |
| footer{margin-top:1.5rem;font-size:.7rem;color:var(--muted);text-align:center;letter-spacing:.06em} | |
| footer a{color:var(--accent);text-decoration:none} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>Sentiment <span>Analyzer</span></h1> | |
| <p class="sub">distilBERT · fine-tuned on IMDB · MLOps demo</p> | |
| <form method="post" action="/predict"> | |
| <label>Enter a review or any text</label> | |
| <textarea name="text" placeholder="e.g. This movie was absolutely fantastic, loved every minute of it!" required>{text_value}</textarea> | |
| <button class="btn" type="submit">→ Analyze Sentiment</button> | |
| {result_html} | |
| </form> | |
| </div> | |
| <footer> | |
| Model: <a href="https://huggingface.co/amarshiv86/sentiment-analysis-imdb-model" target="_blank">amarshiv86/sentiment-analysis-imdb-model</a> | |
| </footer> | |
| </body> | |
| </html> | |
| """ | |
| async def index(): | |
| return HTML.replace("{result_html}", "").replace("{text_value}", "") | |
| async def predict(text: str = Form(...)): | |
| result = MODEL["clf"](text)[0] | |
| label = result["label"] | |
| score = result["score"] | |
| pct = round(score * 100, 1) | |
| css_class = "pos" if label == "POSITIVE" else "neg" | |
| icon = "😊" if label == "POSITIVE" else "😞" | |
| bar_width = round(score * 100) | |
| result_html = f""" | |
| <div class="divider"></div> | |
| <div class="result {css_class}"> | |
| <div class="result-icon">{icon}</div> | |
| <div class="result-body"> | |
| <h2>{label}</h2> | |
| <p>Model confidence · {pct}%</p> | |
| </div> | |
| <span class="badge">{label}</span> | |
| </div> | |
| <div class="bar-wrap"> | |
| <div class="bar-fill" style="width:{bar_width}%"></div> | |
| </div> | |
| <div class="bar-label"> | |
| <span>confidence</span> | |
| <span>{pct}%</span> | |
| </div> | |
| """ | |
| safe_text = text.replace('"', """).replace("<", "<").replace(">", ">") | |
| return (HTML | |
| .replace("{result_html}", result_html) | |
| .replace("{text_value}", safe_text)) | |
| async def health(): | |
| return {"status": "ok", "model_loaded": "clf" in MODEL} | |