techresearchspace commited on
Commit
97390c6
·
verified ·
1 Parent(s): 105bf90

Upload 2 files

Browse files
Files changed (2) hide show
  1. README.md +31 -5
  2. index.html +388 -56
README.md CHANGED
@@ -1,10 +1,36 @@
1
  ---
2
- title: Sentiment Demo
3
- emoji: 🖼️
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: static
7
  pinned: false
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Sentiment Analysis Demo
3
+ emoji: 🤗
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ license: mit
9
+ models:
10
+ - distilbert-base-uncased-finetuned-sst-2-english
11
  ---
12
 
13
+ # Sentiment Analysis Demo (in-browser)
14
+
15
+ A static, client-side sentiment analysis demo powered by
16
+ [🤗 Transformers.js](https://huggingface.co/docs/transformers.js). The model
17
+ ([`Xenova/distilbert-base-uncased-finetuned-sst-2-english`](https://huggingface.co/Xenova/distilbert-base-uncased-finetuned-sst-2-english),
18
+ an ONNX-converted version of
19
+ [`distilbert-base-uncased-finetuned-sst-2-english`](https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english))
20
+ downloads and runs entirely in the visitor's browser via WebAssembly — no server,
21
+ no backend, no API calls, and no subscription required to host.
22
+
23
+ ## How it works
24
+
25
+ Open `index.html` — nothing to build or install. The page loads the model on
26
+ first visit (cached by the browser afterward), then classifies whatever text
27
+ you type as POSITIVE or NEGATIVE with a confidence score.
28
+
29
+ ## Running locally
30
+
31
+ Just open `index.html` directly in a browser, or serve the folder with any
32
+ static file server, e.g.:
33
+
34
+ ```bash
35
+ python -m http.server 8000
36
+ ```
index.html CHANGED
@@ -1,57 +1,389 @@
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Gradio-Lite: Serverless Gradio Running Entirely in Your Browser</title>
7
- <meta name="description" content="Gradio-Lite: Serverless Gradio Running Entirely in Your Browser">
8
-
9
- <script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
10
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.css" />
11
-
12
- <style>
13
- html, body {
14
- margin: 0;
15
- padding: 0;
16
- height: 100%;
17
- }
18
- </style>
19
- </head>
20
- <body>
21
- <gradio-lite>
22
- <gradio-file name="app.py" entrypoint>
23
- import gradio as gr
24
-
25
- from filters import as_gray
26
-
27
- def process(input_image):
28
- output_image = as_gray(input_image)
29
- return output_image
30
-
31
- demo = gr.Interface(
32
- process,
33
- "image",
34
- "image",
35
- examples=["lion.jpg", "logo.png"],
36
- )
37
-
38
- demo.launch()
39
- </gradio-file>
40
-
41
- <gradio-file name="filters.py">
42
- from skimage.color import rgb2gray
43
-
44
- def as_gray(image):
45
- return rgb2gray(image)
46
- </gradio-file>
47
-
48
- <gradio-file name="lion.jpg" url="https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/test_data/lion.jpg" />
49
- <gradio-file name="logo.png" url="https://raw.githubusercontent.com/gradio-app/gradio/main/guides/assets/logo.png" />
50
-
51
- <gradio-requirements>
52
- # Same syntax as requirements.txt
53
- scikit-image
54
- </gradio-requirements>
55
- </gradio-lite>
56
- </body>
57
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sentiment Analysis In-Browser Demo</title>
7
+ <style>
8
+ :root {
9
+ --ink: #1b1b1f;
10
+ --paper: #faf9f6;
11
+ --line: #dcd8ce;
12
+ --pos: #2f6d4f;
13
+ --pos-bg: #e6f2ea;
14
+ --neg: #9a3b3b;
15
+ --neg-bg: #f6e9e7;
16
+ --accent: #3d5a80;
17
+ --mono: "SF Mono", "Roboto Mono", ui-monospace, Menlo, Consolas, monospace;
18
+ }
19
+
20
+ * { box-sizing: border-box; }
21
+
22
+ body {
23
+ margin: 0;
24
+ background: var(--paper);
25
+ color: var(--ink);
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
27
+ line-height: 1.5;
28
+ }
29
+
30
+ .wrap {
31
+ max-width: 720px;
32
+ margin: 0 auto;
33
+ padding: 48px 24px 80px;
34
+ }
35
+
36
+ header {
37
+ border-bottom: 1px solid var(--line);
38
+ padding-bottom: 24px;
39
+ margin-bottom: 32px;
40
+ }
41
+
42
+ .eyebrow {
43
+ font-family: var(--mono);
44
+ font-size: 12px;
45
+ letter-spacing: 0.08em;
46
+ text-transform: uppercase;
47
+ color: var(--accent);
48
+ margin-bottom: 8px;
49
+ }
50
+
51
+ h1 {
52
+ font-size: 28px;
53
+ font-weight: 650;
54
+ margin: 0 0 8px;
55
+ letter-spacing: -0.01em;
56
+ }
57
+
58
+ .sub {
59
+ color: #55555c;
60
+ font-size: 15px;
61
+ max-width: 56ch;
62
+ }
63
+
64
+ .sub code {
65
+ font-family: var(--mono);
66
+ background: #f0eee6;
67
+ padding: 1px 5px;
68
+ border-radius: 3px;
69
+ font-size: 13px;
70
+ }
71
+
72
+ .status-row {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ font-family: var(--mono);
77
+ font-size: 13px;
78
+ color: #77777d;
79
+ margin-top: 16px;
80
+ }
81
+
82
+ .dot {
83
+ width: 8px;
84
+ height: 8px;
85
+ border-radius: 50%;
86
+ background: #c9c4b6;
87
+ flex-shrink: 0;
88
+ transition: background 0.3s;
89
+ }
90
+ .dot.loading { background: #d9a441; animation: pulse 1.2s infinite ease-in-out; }
91
+ .dot.ready { background: var(--pos); }
92
+ .dot.error { background: var(--neg); }
93
+
94
+ @keyframes pulse {
95
+ 0%, 100% { opacity: 1; }
96
+ 50% { opacity: 0.35; }
97
+ }
98
+
99
+ textarea {
100
+ width: 100%;
101
+ min-height: 110px;
102
+ padding: 14px 16px;
103
+ border: 1px solid var(--line);
104
+ border-radius: 8px;
105
+ background: #fff;
106
+ font-family: inherit;
107
+ font-size: 15px;
108
+ color: var(--ink);
109
+ resize: vertical;
110
+ outline: none;
111
+ transition: border-color 0.15s;
112
+ }
113
+ textarea:focus { border-color: var(--accent); }
114
+ textarea::placeholder { color: #a8a49a; }
115
+
116
+ .btn-row {
117
+ display: flex;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ margin-top: 12px;
121
+ }
122
+
123
+ button {
124
+ font-family: var(--mono);
125
+ font-size: 13px;
126
+ letter-spacing: 0.02em;
127
+ text-transform: uppercase;
128
+ background: var(--ink);
129
+ color: var(--paper);
130
+ border: none;
131
+ padding: 11px 20px;
132
+ border-radius: 6px;
133
+ cursor: pointer;
134
+ transition: opacity 0.15s, transform 0.1s;
135
+ }
136
+ button:hover:not(:disabled) { opacity: 0.85; }
137
+ button:active:not(:disabled) { transform: scale(0.98); }
138
+ button:disabled { opacity: 0.35; cursor: not-allowed; }
139
+
140
+ .hint {
141
+ font-size: 12px;
142
+ color: #93908a;
143
+ }
144
+
145
+ #result {
146
+ margin-top: 28px;
147
+ }
148
+
149
+ .result-card {
150
+ border: 1px solid var(--line);
151
+ border-radius: 10px;
152
+ padding: 20px 22px;
153
+ background: #fff;
154
+ display: none;
155
+ }
156
+ .result-card.show { display: block; }
157
+
158
+ .result-top {
159
+ display: flex;
160
+ justify-content: space-between;
161
+ align-items: baseline;
162
+ margin-bottom: 14px;
163
+ }
164
+
165
+ .label-pill {
166
+ font-family: var(--mono);
167
+ font-size: 13px;
168
+ font-weight: 600;
169
+ letter-spacing: 0.02em;
170
+ padding: 5px 12px;
171
+ border-radius: 100px;
172
+ }
173
+ .label-pill.POSITIVE { color: var(--pos); background: var(--pos-bg); }
174
+ .label-pill.NEGATIVE { color: var(--neg); background: var(--neg-bg); }
175
+
176
+ .score {
177
+ font-family: var(--mono);
178
+ font-size: 13px;
179
+ color: #77777d;
180
+ }
181
+
182
+ .bar-track {
183
+ height: 6px;
184
+ background: #eeece5;
185
+ border-radius: 100px;
186
+ overflow: hidden;
187
+ }
188
+
189
+ .bar-fill {
190
+ height: 100%;
191
+ border-radius: 100px;
192
+ transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
193
+ width: 0%;
194
+ }
195
+ .bar-fill.POSITIVE { background: var(--pos); }
196
+ .bar-fill.NEGATIVE { background: var(--neg); }
197
+
198
+ .history {
199
+ margin-top: 36px;
200
+ }
201
+
202
+ .history h2 {
203
+ font-size: 13px;
204
+ font-family: var(--mono);
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.06em;
207
+ color: #93908a;
208
+ font-weight: 600;
209
+ margin-bottom: 12px;
210
+ }
211
+
212
+ .history-item {
213
+ display: flex;
214
+ justify-content: space-between;
215
+ gap: 16px;
216
+ padding: 12px 0;
217
+ border-bottom: 1px solid var(--line);
218
+ font-size: 14px;
219
+ }
220
+ .history-item:last-child { border-bottom: none; }
221
+
222
+ .history-text {
223
+ color: #3a3a3f;
224
+ overflow: hidden;
225
+ text-overflow: ellipsis;
226
+ white-space: nowrap;
227
+ flex: 1;
228
+ }
229
+
230
+ .history-label {
231
+ font-family: var(--mono);
232
+ font-size: 11px;
233
+ font-weight: 600;
234
+ letter-spacing: 0.02em;
235
+ flex-shrink: 0;
236
+ }
237
+ .history-label.POSITIVE { color: var(--pos); }
238
+ .history-label.NEGATIVE { color: var(--neg); }
239
+
240
+ footer {
241
+ margin-top: 56px;
242
+ padding-top: 20px;
243
+ border-top: 1px solid var(--line);
244
+ font-size: 12px;
245
+ color: #a8a49a;
246
+ }
247
+ footer a { color: var(--accent); text-decoration: none; }
248
+ footer a:hover { text-decoration: underline; }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <div class="wrap">
253
+ <header>
254
+ <div class="eyebrow">In-browser inference · no server</div>
255
+ <h1>Sentiment Analysis</h1>
256
+ <p class="sub">
257
+ Runs <code>distilbert-base-uncased-finetuned-sst-2-english</code> entirely in your browser
258
+ via WebAssembly. Nothing you type is sent anywhere.
259
+ </p>
260
+ <div class="status-row">
261
+ <span class="dot loading" id="statusDot"></span>
262
+ <span id="statusText">Loading model…</span>
263
+ </div>
264
+ </header>
265
+
266
+ <textarea id="input" placeholder="I really loved this movie, the acting was fantastic!" disabled></textarea>
267
+ <div class="btn-row">
268
+ <button id="analyzeBtn" disabled>Analyze</button>
269
+ <span class="hint">⌘/Ctrl + Enter to run</span>
270
+ </div>
271
+
272
+ <div id="result">
273
+ <div class="result-card" id="resultCard">
274
+ <div class="result-top">
275
+ <span class="label-pill" id="labelPill"></span>
276
+ <span class="score" id="scoreText"></span>
277
+ </div>
278
+ <div class="bar-track">
279
+ <div class="bar-fill" id="barFill"></div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+
284
+ <div class="history" id="historySection" style="display:none;">
285
+ <h2>Recent</h2>
286
+ <div id="historyList"></div>
287
+ </div>
288
+
289
+ <footer>
290
+ Model card: <a href="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english" target="_blank" rel="noopener">distilbert-base-uncased-finetuned-sst-2-english</a>
291
+ · Powered by <a href="https://huggingface.co/docs/transformers.js" target="_blank" rel="noopener">🤗 Transformers.js</a>
292
+ </footer>
293
+ </div>
294
+
295
+ <script type="module">
296
+ import { pipeline, env } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0";
297
+
298
+ env.allowLocalModels = false;
299
+
300
+ const statusDot = document.getElementById("statusDot");
301
+ const statusText = document.getElementById("statusText");
302
+ const input = document.getElementById("input");
303
+ const analyzeBtn = document.getElementById("analyzeBtn");
304
+ const resultCard = document.getElementById("resultCard");
305
+ const labelPill = document.getElementById("labelPill");
306
+ const scoreText = document.getElementById("scoreText");
307
+ const barFill = document.getElementById("barFill");
308
+ const historySection = document.getElementById("historySection");
309
+ const historyList = document.getElementById("historyList");
310
+
311
+ let classifier = null;
312
+ const history = [];
313
+
314
+ async function loadModel() {
315
+ try {
316
+ classifier = await pipeline(
317
+ "sentiment-analysis",
318
+ "Xenova/distilbert-base-uncased-finetuned-sst-2-english"
319
+ );
320
+ statusDot.className = "dot ready";
321
+ statusText.textContent = "Model ready";
322
+ input.disabled = false;
323
+ analyzeBtn.disabled = false;
324
+ input.focus();
325
+ } catch (err) {
326
+ statusDot.className = "dot error";
327
+ statusText.textContent = "Failed to load model — check console";
328
+ console.error(err);
329
+ }
330
+ }
331
+
332
+ function renderResult(label, score) {
333
+ resultCard.classList.add("show");
334
+ labelPill.textContent = label;
335
+ labelPill.className = "label-pill " + label;
336
+ scoreText.textContent = (score * 100).toFixed(1) + "% confidence";
337
+ barFill.className = "bar-fill " + label;
338
+ requestAnimationFrame(() => {
339
+ barFill.style.width = (score * 100).toFixed(1) + "%";
340
+ });
341
+ }
342
+
343
+ function addToHistory(text, label, score) {
344
+ history.unshift({ text, label, score });
345
+ if (history.length > 6) history.pop();
346
+ historySection.style.display = "block";
347
+ historyList.innerHTML = history
348
+ .map(
349
+ (h) => `
350
+ <div class="history-item">
351
+ <span class="history-text">${escapeHtml(h.text)}</span>
352
+ <span class="history-label ${h.label}">${h.label} ${(h.score * 100).toFixed(0)}%</span>
353
+ </div>`
354
+ )
355
+ .join("");
356
+ }
357
+
358
+ function escapeHtml(str) {
359
+ const div = document.createElement("div");
360
+ div.textContent = str;
361
+ return div.innerHTML;
362
+ }
363
+
364
+ async function analyze() {
365
+ const text = input.value.trim();
366
+ if (!text || !classifier) return;
367
+
368
+ analyzeBtn.disabled = true;
369
+ analyzeBtn.textContent = "Analyzing…";
370
+
371
+ const output = await classifier(text);
372
+ const { label, score } = output[0];
373
+
374
+ renderResult(label, score);
375
+ addToHistory(text, label, score);
376
+
377
+ analyzeBtn.disabled = false;
378
+ analyzeBtn.textContent = "Analyze";
379
+ }
380
+
381
+ analyzeBtn.addEventListener("click", analyze);
382
+ input.addEventListener("keydown", (e) => {
383
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") analyze();
384
+ });
385
+
386
+ loadModel();
387
+ </script>
388
+ </body>
389
+ </html>