lodestones commited on
Commit
4e7ac21
·
1 Parent(s): bc0d1c8

Upload tagger_ui/templates/index.html

Browse files
Files changed (1) hide show
  1. tagger_ui/templates/index.html +475 -0
tagger_ui/templates/index.html ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>DINOv3 Tagger</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #0f0f11;
12
+ --surface: #1a1a1f;
13
+ --border: #2e2e38;
14
+ --accent: #7c6af7;
15
+ --accent2: #a78bfa;
16
+ --text: #e2e2e8;
17
+ --muted: #6b6b7e;
18
+ --green: #4ade80;
19
+ --radius: 10px;
20
+ }
21
+
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: 'Inter', system-ui, sans-serif;
26
+ min-height: 100vh;
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ padding: 2rem 1rem 4rem;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 1.6rem;
35
+ font-weight: 700;
36
+ letter-spacing: -0.02em;
37
+ margin-bottom: 0.25rem;
38
+ }
39
+ h1 span { color: var(--accent2); }
40
+
41
+ .subtitle {
42
+ color: var(--muted);
43
+ font-size: 0.85rem;
44
+ margin-bottom: 2rem;
45
+ }
46
+
47
+ .card {
48
+ background: var(--surface);
49
+ border: 1px solid var(--border);
50
+ border-radius: var(--radius);
51
+ padding: 1.5rem;
52
+ width: 100%;
53
+ max-width: 780px;
54
+ }
55
+
56
+ /* ---- input area ---- */
57
+ .input-row {
58
+ display: flex;
59
+ gap: 0.5rem;
60
+ margin-bottom: 1rem;
61
+ }
62
+
63
+ .input-row input[type="text"] {
64
+ flex: 1;
65
+ background: var(--bg);
66
+ border: 1px solid var(--border);
67
+ border-radius: var(--radius);
68
+ color: var(--text);
69
+ font-size: 0.9rem;
70
+ padding: 0.6rem 0.9rem;
71
+ outline: none;
72
+ transition: border-color 0.15s;
73
+ }
74
+ .input-row input[type="text"]:focus { border-color: var(--accent); }
75
+ .input-row input[type="text"]::placeholder { color: var(--muted); }
76
+
77
+ .btn {
78
+ background: var(--accent);
79
+ border: none;
80
+ border-radius: var(--radius);
81
+ color: #fff;
82
+ cursor: pointer;
83
+ font-size: 0.9rem;
84
+ font-weight: 600;
85
+ padding: 0.6rem 1.2rem;
86
+ transition: opacity 0.15s;
87
+ white-space: nowrap;
88
+ }
89
+ .btn:hover { opacity: 0.85; }
90
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
91
+
92
+ /* ---- drop zone ---- */
93
+ #drop-zone {
94
+ border: 2px dashed var(--border);
95
+ border-radius: var(--radius);
96
+ color: var(--muted);
97
+ cursor: pointer;
98
+ font-size: 0.85rem;
99
+ padding: 1.4rem;
100
+ text-align: center;
101
+ transition: border-color 0.15s, background 0.15s;
102
+ margin-bottom: 1rem;
103
+ }
104
+ #drop-zone.drag-over {
105
+ border-color: var(--accent);
106
+ background: rgba(124,106,247,0.06);
107
+ }
108
+ #drop-zone input[type="file"] { display: none; }
109
+
110
+ /* ---- options row ---- */
111
+ .options-row {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 1rem;
115
+ flex-wrap: wrap;
116
+ margin-bottom: 1.2rem;
117
+ font-size: 0.85rem;
118
+ color: var(--muted);
119
+ }
120
+ .options-row label { display: flex; align-items: center; gap: 0.4rem; }
121
+ .options-row input[type="number"],
122
+ .options-row input[type="range"] {
123
+ background: var(--bg);
124
+ border: 1px solid var(--border);
125
+ border-radius: 6px;
126
+ color: var(--text);
127
+ padding: 0.3rem 0.5rem;
128
+ width: 70px;
129
+ font-size: 0.85rem;
130
+ }
131
+ .options-row input[type="range"] {
132
+ width: 110px;
133
+ padding: 0;
134
+ cursor: pointer;
135
+ accent-color: var(--accent);
136
+ }
137
+ #threshold-val { color: var(--text); min-width: 2.5ch; }
138
+
139
+ /* ---- mode toggle ---- */
140
+ .mode-toggle {
141
+ display: flex;
142
+ background: var(--bg);
143
+ border: 1px solid var(--border);
144
+ border-radius: 8px;
145
+ overflow: hidden;
146
+ }
147
+ .mode-toggle button {
148
+ background: none;
149
+ border: none;
150
+ color: var(--muted);
151
+ cursor: pointer;
152
+ font-size: 0.8rem;
153
+ padding: 0.3rem 0.7rem;
154
+ transition: background 0.15s, color 0.15s;
155
+ }
156
+ .mode-toggle button.active {
157
+ background: var(--accent);
158
+ color: #fff;
159
+ }
160
+
161
+ /* ---- preview + results ---- */
162
+ #results-area { display: none; }
163
+
164
+ .result-block {
165
+ display: grid;
166
+ grid-template-columns: auto 1fr;
167
+ gap: 1.2rem;
168
+ margin-top: 1rem;
169
+ }
170
+
171
+ .preview-wrap {
172
+ width: 200px;
173
+ flex-shrink: 0;
174
+ }
175
+ .preview-wrap img {
176
+ border-radius: var(--radius);
177
+ max-width: 200px;
178
+ max-height: 200px;
179
+ object-fit: contain;
180
+ border: 1px solid var(--border);
181
+ display: block;
182
+ }
183
+ .preview-wrap .img-meta {
184
+ color: var(--muted);
185
+ font-size: 0.72rem;
186
+ margin-top: 0.4rem;
187
+ word-break: break-all;
188
+ }
189
+
190
+ .tags-wrap { min-width: 0; }
191
+
192
+ .tag-copy-row {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 0.5rem;
196
+ margin-bottom: 0.7rem;
197
+ }
198
+ .tag-string {
199
+ background: var(--bg);
200
+ border: 1px solid var(--border);
201
+ border-radius: 6px;
202
+ color: var(--muted);
203
+ font-size: 0.78rem;
204
+ flex: 1;
205
+ padding: 0.4rem 0.6rem;
206
+ white-space: nowrap;
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ cursor: pointer;
210
+ transition: border-color 0.15s;
211
+ }
212
+ .tag-string:hover { border-color: var(--accent); color: var(--text); }
213
+ .copy-btn {
214
+ background: var(--bg);
215
+ border: 1px solid var(--border);
216
+ border-radius: 6px;
217
+ color: var(--muted);
218
+ cursor: pointer;
219
+ font-size: 0.78rem;
220
+ padding: 0.35rem 0.6rem;
221
+ transition: border-color 0.15s, color 0.15s;
222
+ white-space: nowrap;
223
+ }
224
+ .copy-btn:hover { border-color: var(--accent); color: var(--accent2); }
225
+ .copy-btn.copied { color: var(--green); border-color: var(--green); }
226
+
227
+ .tag-list {
228
+ display: flex;
229
+ flex-wrap: wrap;
230
+ gap: 0.4rem;
231
+ }
232
+
233
+ .tag-pill {
234
+ align-items: center;
235
+ border-radius: 20px;
236
+ display: inline-flex;
237
+ font-size: 0.78rem;
238
+ gap: 0.35rem;
239
+ padding: 0.25rem 0.65rem;
240
+ cursor: default;
241
+ transition: opacity 0.1s;
242
+ }
243
+ .tag-pill:hover { opacity: 0.8; }
244
+ .tag-pill .score {
245
+ font-size: 0.68rem;
246
+ opacity: 0.75;
247
+ }
248
+
249
+ /* ---- spinner ---- */
250
+ .spinner {
251
+ display: none;
252
+ width: 22px; height: 22px;
253
+ border: 3px solid var(--border);
254
+ border-top-color: var(--accent);
255
+ border-radius: 50%;
256
+ animation: spin 0.7s linear infinite;
257
+ margin: 1rem auto;
258
+ }
259
+ @keyframes spin { to { transform: rotate(360deg); } }
260
+
261
+ .error-msg {
262
+ color: #f87171;
263
+ font-size: 0.85rem;
264
+ margin-top: 0.5rem;
265
+ display: none;
266
+ }
267
+
268
+ @media (max-width: 520px) {
269
+ .result-block { grid-template-columns: 1fr; }
270
+ .preview-wrap { width: 100%; }
271
+ .preview-wrap img { max-width: 100%; }
272
+ }
273
+ </style>
274
+ </head>
275
+ <body>
276
+
277
+ <h1>DINOv3 <span>Tagger</span></h1>
278
+ <p class="subtitle">ViT-H/16+ · {{ num_tags | format_number }} tags · {{ vocab_path }}</p>
279
+
280
+ <div class="card">
281
+
282
+ <!-- URL input -->
283
+ <div class="input-row">
284
+ <input type="text" id="url-input" placeholder="Paste image URL or drop a file below…" />
285
+ <button class="btn" id="tag-btn" onclick="runFromUrl()">Tag</button>
286
+ </div>
287
+
288
+ <!-- Drop zone -->
289
+ <div id="drop-zone" onclick="document.getElementById('file-input').click()">
290
+ <input type="file" id="file-input" accept="image/*" onchange="runFromFile(this)" />
291
+ Drop image here or <strong>click to browse</strong>
292
+ </div>
293
+
294
+ <!-- Options -->
295
+ <div class="options-row">
296
+ <div class="mode-toggle">
297
+ <button id="mode-topk" class="active" onclick="setMode('topk')">Top-K</button>
298
+ <button id="mode-thresh" onclick="setMode('threshold')">Threshold</button>
299
+ </div>
300
+
301
+ <label id="topk-label">
302
+ K =
303
+ <input type="number" id="topk-input" value="40" min="1" max="500" />
304
+ </label>
305
+
306
+ <label id="thresh-label" style="display:none">
307
+
308
+ <input type="range" id="thresh-input" min="0.01" max="0.99" step="0.01" value="0.35"
309
+ oninput="document.getElementById('threshold-val').textContent=parseFloat(this.value).toFixed(2)" />
310
+ <span id="threshold-val">0.35</span>
311
+ </label>
312
+
313
+ <label>
314
+ Max px
315
+ <input type="number" id="maxsize-input" value="1024" min="64" max="4096" step="16" />
316
+ </label>
317
+ </div>
318
+
319
+ <div class="spinner" id="spinner"></div>
320
+ <div class="error-msg" id="error-msg"></div>
321
+
322
+ <!-- Results -->
323
+ <div id="results-area">
324
+ <div class="result-block">
325
+ <div class="preview-wrap">
326
+ <img id="preview-img" src="" alt="preview" />
327
+ <div class="img-meta" id="img-meta"></div>
328
+ </div>
329
+ <div class="tags-wrap">
330
+ <div class="tag-copy-row">
331
+ <div class="tag-string" id="tag-string" onclick="copyTags()" title="Click to copy"></div>
332
+ <button class="copy-btn" id="copy-btn" onclick="copyTags()">Copy</button>
333
+ </div>
334
+ <div class="tag-list" id="tag-list"></div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ </div>
340
+
341
+ <script>
342
+ let currentMode = 'topk';
343
+
344
+ function setMode(m) {
345
+ currentMode = m;
346
+ document.getElementById('mode-topk').classList.toggle('active', m === 'topk');
347
+ document.getElementById('mode-thresh').classList.toggle('active', m === 'threshold');
348
+ document.getElementById('topk-label').style.display = m === 'topk' ? '' : 'none';
349
+ document.getElementById('thresh-label').style.display = m === 'threshold' ? '' : 'none';
350
+ }
351
+
352
+ // ---- drag & drop ----
353
+ const dz = document.getElementById('drop-zone');
354
+ dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
355
+ dz.addEventListener('dragleave', () => dz.classList.remove('drag-over'));
356
+ dz.addEventListener('drop', e => {
357
+ e.preventDefault();
358
+ dz.classList.remove('drag-over');
359
+ const file = e.dataTransfer.files[0];
360
+ if (file) submitFile(file);
361
+ });
362
+
363
+ function runFromFile(input) {
364
+ if (input.files[0]) submitFile(input.files[0]);
365
+ }
366
+
367
+ function runFromUrl() {
368
+ const url = document.getElementById('url-input').value.trim();
369
+ if (!url) return;
370
+ const params = buildParams();
371
+ params.append('url', url);
372
+ submit('/tag/url', params, url);
373
+ }
374
+
375
+ function buildParams() {
376
+ const p = new URLSearchParams();
377
+ p.append('max_size', document.getElementById('maxsize-input').value);
378
+ if (currentMode === 'topk') {
379
+ p.append('topk', document.getElementById('topk-input').value);
380
+ } else {
381
+ p.append('threshold', document.getElementById('thresh-input').value);
382
+ }
383
+ return p;
384
+ }
385
+
386
+ function submitFile(file) {
387
+ const params = buildParams();
388
+ const fd = new FormData();
389
+ fd.append('file', file);
390
+ for (const [k, v] of params) fd.append(k, v);
391
+
392
+ // local preview
393
+ const reader = new FileReader();
394
+ reader.onload = e => setPreview(e.target.result, file.name);
395
+ reader.readAsDataURL(file);
396
+
397
+ submitFetch('/tag/upload', { method: 'POST', body: fd });
398
+ }
399
+
400
+ function submit(endpoint, params, previewUrl) {
401
+ setPreview(previewUrl, previewUrl);
402
+ submitFetch(`${endpoint}?${params}`, { method: 'POST' });
403
+ }
404
+
405
+ function submitFetch(url, opts) {
406
+ setLoading(true);
407
+ fetch(url, opts)
408
+ .then(r => r.ok ? r.json() : r.json().then(e => Promise.reject(e.detail || 'Server error')))
409
+ .then(renderResults)
410
+ .catch(err => showError(String(err)))
411
+ .finally(() => setLoading(false));
412
+ }
413
+
414
+ function setPreview(src, label) {
415
+ document.getElementById('preview-img').src = src;
416
+ document.getElementById('img-meta').textContent = label.length > 60
417
+ ? '…' + label.slice(-57) : label;
418
+ }
419
+
420
+ function renderResults(data) {
421
+ hideError();
422
+ const list = document.getElementById('tag-list');
423
+ list.innerHTML = '';
424
+
425
+ const tagString = data.tags.map(t => t.tag).join(', ');
426
+ document.getElementById('tag-string').textContent = tagString;
427
+
428
+ data.tags.forEach(({ tag, score }) => {
429
+ const hue = Math.round(260 - score * 80); // purple → teal gradient
430
+ const pill = document.createElement('span');
431
+ pill.className = 'tag-pill';
432
+ pill.style.background = `hsla(${hue},60%,55%,0.18)`;
433
+ pill.style.border = `1px solid hsla(${hue},60%,55%,0.35)`;
434
+ pill.style.color = `hsl(${hue},70%,78%)`;
435
+ pill.innerHTML = `${tag}<span class="score">${(score * 100).toFixed(0)}%</span>`;
436
+ pill.title = `${tag}: ${score.toFixed(4)}`;
437
+ list.appendChild(pill);
438
+ });
439
+
440
+ document.getElementById('results-area').style.display = 'block';
441
+ document.getElementById('copy-btn').classList.remove('copied');
442
+ }
443
+
444
+ function copyTags() {
445
+ const text = document.getElementById('tag-string').textContent;
446
+ navigator.clipboard.writeText(text).then(() => {
447
+ const btn = document.getElementById('copy-btn');
448
+ btn.textContent = 'Copied!';
449
+ btn.classList.add('copied');
450
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
451
+ });
452
+ }
453
+
454
+ function setLoading(on) {
455
+ document.getElementById('spinner').style.display = on ? 'block' : 'none';
456
+ document.getElementById('tag-btn').disabled = on;
457
+ if (on) document.getElementById('results-area').style.display = 'none';
458
+ }
459
+
460
+ function showError(msg) {
461
+ const el = document.getElementById('error-msg');
462
+ el.textContent = msg;
463
+ el.style.display = 'block';
464
+ }
465
+ function hideError() {
466
+ document.getElementById('error-msg').style.display = 'none';
467
+ }
468
+
469
+ // allow pressing Enter in URL box
470
+ document.getElementById('url-input').addEventListener('keydown', e => {
471
+ if (e.key === 'Enter') runFromUrl();
472
+ });
473
+ </script>
474
+ </body>
475
+ </html>