Spaces:
Sleeping
Sleeping
File size: 6,848 Bytes
3b558e2 5c09346 3b558e2 5c09346 3b558e2 5c09346 3b558e2 5c09346 3b558e2 5c09346 3b558e2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | 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")
@asynccontextmanager
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>
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML.replace("{result_html}", "").replace("{text_value}", "")
@app.post("/predict", response_class=HTMLResponse)
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))
@app.get("/health")
async def health():
return {"status": "ok", "model_loaded": "clf" in MODEL}
|