amarshiv86 commited on
Commit
3b558e2
Β·
verified Β·
1 Parent(s): 62a64e5

ci: deploy app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +169 -0
app/main.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import numpy as np
4
+ from fastapi import FastAPI, Form
5
+ from fastapi.responses import HTMLResponse
6
+ from contextlib import asynccontextmanager
7
+ from huggingface_hub import hf_hub_download
8
+ from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
9
+
10
+ # ── Model loading ─────────────────────────────────────────────
11
+ MODEL = {}
12
+
13
+ HF_REPO = os.getenv("HF_REPO", "amarshiv86/sentiment-analysis-imdb-model")
14
+
15
+ @asynccontextmanager
16
+ async def lifespan(app: FastAPI):
17
+ print(f"Downloading model from HF Hub: {HF_REPO} …")
18
+ files = [
19
+ "model/config.json",
20
+ "model/model.safetensors",
21
+ "model/tokenizer.json",
22
+ "model/tokenizer_config.json",
23
+ ]
24
+ local_dir = "/tmp/sentiment-model"
25
+ os.makedirs(local_dir, exist_ok=True)
26
+
27
+ for f in files:
28
+ hf_hub_download(
29
+ repo_id=HF_REPO,
30
+ filename=f,
31
+ repo_type="model",
32
+ local_dir=local_dir,
33
+ )
34
+
35
+ tokenizer = AutoTokenizer.from_pretrained(f"{local_dir}/model")
36
+ model = AutoModelForSequenceClassification.from_pretrained(f"{local_dir}/model")
37
+ MODEL["clf"] = pipeline(
38
+ "sentiment-analysis",
39
+ model=model,
40
+ tokenizer=tokenizer,
41
+ truncation=True,
42
+ max_length=256,
43
+ )
44
+ print("Model loaded βœ“")
45
+ yield
46
+ MODEL.clear()
47
+
48
+ app = FastAPI(title="Sentiment Analysis API", lifespan=lifespan)
49
+
50
+ # ── HTML ──────────────────────────────────────────────────────
51
+ HTML = """<!DOCTYPE html>
52
+ <html lang="en">
53
+ <head>
54
+ <meta charset="UTF-8"/>
55
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
56
+ <title>Sentiment Analyzer Β· AI Demo</title>
57
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
58
+ <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"/>
59
+ <style>
60
+ :root{
61
+ --bg:#0d1117; --surface:#161b22; --border:rgba(255,255,255,.08);
62
+ --text:#e6edf3; --muted:#7d8590;
63
+ --pos:#2ea043; --neg:#da3633;
64
+ --pos-bg:rgba(46,160,67,.12); --neg-bg:rgba(218,54,51,.12);
65
+ --accent:#58a6ff;
66
+ }
67
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
68
+ 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}
69
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:2.5rem;width:100%;max-width:640px}
70
+ h1{font-family:'Syne',sans-serif;font-size:1.8rem;font-weight:800;letter-spacing:-.02em;margin-bottom:.4rem}
71
+ h1 span{color:var(--accent)}
72
+ .sub{color:var(--muted);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;margin-bottom:2rem}
73
+ label{font-size:.72rem;color:var(--muted);letter-spacing:.06em;display:block;margin-bottom:.4rem}
74
+ textarea{
75
+ width:100%;background:rgba(255,255,255,.04);border:1px solid var(--border);
76
+ border-radius:8px;color:var(--text);font-family:'DM Mono',monospace;
77
+ font-size:.85rem;padding:.75rem 1rem;resize:vertical;min-height:130px;
78
+ outline:none;transition:border-color .2s;
79
+ }
80
+ textarea:focus{border-color:rgba(88,166,255,.5)}
81
+ textarea::placeholder{color:var(--muted)}
82
+ .btn{
83
+ width:100%;margin-top:1rem;padding:.8rem;
84
+ background:var(--accent);border:none;border-radius:8px;
85
+ color:#0d1117;font-family:'Syne',sans-serif;font-size:.9rem;font-weight:700;
86
+ cursor:pointer;transition:opacity .2s,transform .15s;letter-spacing:.03em;
87
+ }
88
+ .btn:hover{opacity:.88;transform:translateY(-1px)}
89
+ .btn:active{transform:translateY(0)}
90
+ .divider{height:1px;background:var(--border);margin:1.5rem 0}
91
+ .result{border-radius:10px;padding:1.2rem 1.5rem;display:flex;align-items:center;gap:1rem;animation:pop .3s ease}
92
+ @keyframes pop{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
93
+ .result.pos{background:var(--pos-bg);border:1px solid rgba(46,160,67,.3)}
94
+ .result.neg{background:var(--neg-bg);border:1px solid rgba(218,54,51,.3)}
95
+ .result-icon{font-size:2rem;line-height:1}
96
+ .result-body h2{font-family:'Syne',sans-serif;font-size:1.1rem;font-weight:700}
97
+ .result-body p{font-size:.75rem;color:var(--muted);margin-top:.2rem}
98
+ .badge{margin-left:auto;padding:.3rem .8rem;border-radius:99px;font-size:.72rem;font-weight:600;font-family:'Syne',sans-serif}
99
+ .pos .badge{background:rgba(46,160,67,.2);color:var(--pos)}
100
+ .neg .badge{background:rgba(218,54,51,.2);color:var(--neg)}
101
+ /* confidence bar */
102
+ .bar-wrap{margin-top:1rem;background:rgba(255,255,255,.06);border-radius:99px;height:6px;overflow:hidden}
103
+ .bar-fill{height:100%;border-radius:99px;transition:width .6s ease}
104
+ .pos .bar-fill{background:var(--pos)}
105
+ .neg .bar-fill{background:var(--neg)}
106
+ .bar-label{display:flex;justify-content:space-between;font-size:.7rem;color:var(--muted);margin-top:.4rem}
107
+ footer{margin-top:1.5rem;font-size:.7rem;color:var(--muted);text-align:center;letter-spacing:.06em}
108
+ footer a{color:var(--accent);text-decoration:none}
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="card">
113
+ <h1>Sentiment <span>Analyzer</span></h1>
114
+ <p class="sub">distilBERT Β· fine-tuned on IMDB Β· MLOps demo</p>
115
+ <form method="post" action="/predict">
116
+ <label>Enter a review or any text</label>
117
+ <textarea name="text" placeholder="e.g. This movie was absolutely fantastic, loved every minute of it!" required>{text_value}</textarea>
118
+ <button class="btn" type="submit">β†’ Analyze Sentiment</button>
119
+ {result_html}
120
+ </form>
121
+ </div>
122
+ <footer>
123
+ Model: <a href="https://huggingface.co/amarshiv86/sentiment-analysis-imdb-model" target="_blank">amarshiv86/sentiment-analysis-imdb-model</a>
124
+ </footer>
125
+ </body>
126
+ </html>
127
+ """
128
+
129
+ @app.get("/", response_class=HTMLResponse)
130
+ async def index():
131
+ return HTML.replace("{result_html}", "").replace("{text_value}", "")
132
+
133
+ @app.post("/predict", response_class=HTMLResponse)
134
+ async def predict(text: str = Form(...)):
135
+ result = MODEL["clf"](text)[0]
136
+ label = result["label"] # POSITIVE or NEGATIVE
137
+ score = result["score"]
138
+ pct = round(score * 100, 1)
139
+ css_class = "pos" if label == "POSITIVE" else "neg"
140
+ icon = "😊" if label == "POSITIVE" else "😞"
141
+ bar_width = round(score * 100)
142
+
143
+ result_html = f"""
144
+ <div class="divider"></div>
145
+ <div class="result {css_class}">
146
+ <div class="result-icon">{icon}</div>
147
+ <div class="result-body">
148
+ <h2>{label}</h2>
149
+ <p>Model confidence Β· {pct}%</p>
150
+ </div>
151
+ <span class="badge">{label}</span>
152
+ </div>
153
+ <div class="bar-wrap">
154
+ <div class="bar-fill" style="width:{bar_width}%"></div>
155
+ </div>
156
+ <div class="bar-label">
157
+ <span>confidence</span>
158
+ <span>{pct}%</span>
159
+ </div>
160
+ """
161
+
162
+ safe_text = text.replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
163
+ return (HTML
164
+ .replace("{result_html}", result_html)
165
+ .replace("{text_value}", safe_text))
166
+
167
+ @app.get("/health")
168
+ async def health():
169
+ return {"status": "ok", "model_loaded": "clf" in MODEL}