Spaces:
Sleeping
Sleeping
Download web/admin.html from Eminakkoc/Scenedrobe: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/spaces/Eminakkoc/Scenedrobe/resolve/main/web/admin.html
- Command line
-
hf download hf://spaces/Eminakkoc/Scenedrobe/web/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/Eminakkoc/Scenedrobe/resolve/main/web/admin.html
17.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Scenedrobe Admin</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| background: #0f0f0f; | |
| color: #e0e0e0; | |
| min-height: 100vh; | |
| padding: 1.5rem; | |
| } | |
| .top-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .top-bar h1 { font-size: 1.5rem; color: #fff; } | |
| .top-bar a { | |
| color: #60a5fa; | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| } | |
| .top-bar a:hover { text-decoration: underline; } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 360px 1fr; | |
| gap: 1.5rem; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| /* ββ Upload panel ββ */ | |
| .upload-panel { | |
| background: #1a1a1a; | |
| border: 1px solid #2a2a2a; | |
| border-radius: 10px; | |
| padding: 1.2rem; | |
| height: fit-content; | |
| position: sticky; | |
| top: 1.5rem; | |
| } | |
| .upload-panel h2 { | |
| font-size: 1rem; | |
| color: #fff; | |
| margin-bottom: 1rem; | |
| } | |
| .form-group { | |
| margin-bottom: 0.8rem; | |
| } | |
| .form-group label { | |
| display: block; | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| margin-bottom: 0.3rem; | |
| } | |
| .form-group input[type="text"] { | |
| width: 100%; | |
| padding: 0.55rem 0.8rem; | |
| border: 1px solid #333; | |
| border-radius: 6px; | |
| background: #111; | |
| color: #e0e0e0; | |
| font-size: 0.85rem; | |
| } | |
| .form-group input[type="text"]::placeholder { color: #555; } | |
| .slider-group label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .slider-group label span { | |
| font-weight: 600; | |
| color: #e0e0e0; | |
| font-size: 0.8rem; | |
| min-width: 2.5em; | |
| text-align: right; | |
| } | |
| .slider-group input[type="range"] { | |
| width: 100%; | |
| margin-top: 0.2rem; | |
| accent-color: #2563eb; | |
| cursor: pointer; | |
| } | |
| .drop-zone { | |
| border: 2px dashed #333; | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: border-color 0.2s, background 0.2s; | |
| margin-bottom: 0.8rem; | |
| } | |
| .drop-zone:hover, .drop-zone.dragover { | |
| border-color: #2563eb; | |
| background: rgba(37, 99, 235, 0.05); | |
| } | |
| .drop-zone.has-file { | |
| border-color: #4ade80; | |
| border-style: solid; | |
| } | |
| .drop-zone input { display: none; } | |
| .drop-zone-text { | |
| color: #666; | |
| font-size: 0.85rem; | |
| } | |
| .drop-zone-text strong { color: #aaa; } | |
| .drop-zone-preview { | |
| display: none; | |
| max-width: 100%; | |
| max-height: 180px; | |
| border-radius: 6px; | |
| margin-top: 0.5rem; | |
| } | |
| .btn { | |
| padding: 0.6rem 1.2rem; | |
| border: 1px solid #333; | |
| border-radius: 6px; | |
| background: #1a1a1a; | |
| color: #e0e0e0; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .btn:hover { background: #2a2a2a; } | |
| .btn-primary { | |
| background: #2563eb; | |
| border-color: #2563eb; | |
| color: #fff; | |
| width: 100%; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| } | |
| .btn-primary:hover { background: #1d4ed8; } | |
| .btn-primary:disabled { | |
| background: #1e3a5f; | |
| cursor: not-allowed; | |
| opacity: 0.6; | |
| } | |
| .btn-danger { | |
| background: transparent; | |
| border-color: #444; | |
| color: #f87171; | |
| padding: 0.35rem 0.6rem; | |
| font-size: 0.75rem; | |
| } | |
| .btn-danger:hover { | |
| background: rgba(248, 113, 113, 0.1); | |
| border-color: #f87171; | |
| } | |
| .upload-status { | |
| margin-top: 0.8rem; | |
| font-size: 0.8rem; | |
| min-height: 1.2em; | |
| text-align: center; | |
| } | |
| .upload-status.success { color: #4ade80; } | |
| .upload-status.error { color: #f87171; } | |
| .extraction-result { | |
| margin-top: 0.8rem; | |
| border: 1px solid #2a2a2a; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| display: none; | |
| } | |
| .extraction-result.visible { display: block; } | |
| .extraction-result-header { | |
| font-size: 0.75rem; | |
| color: #aaa; | |
| padding: 0.5rem 0.7rem 0.3rem; | |
| background: #111; | |
| } | |
| .extraction-result img { | |
| display: block; | |
| width: 100%; | |
| max-height: 180px; | |
| object-fit: contain; | |
| background: #fff; | |
| padding: 0.3rem; | |
| } | |
| /* ββ Catalog grid ββ */ | |
| .catalog-panel h2 { | |
| font-size: 1rem; | |
| color: #fff; | |
| margin-bottom: 0.3rem; | |
| } | |
| .catalog-count { | |
| font-size: 0.8rem; | |
| color: #888; | |
| margin-bottom: 1rem; | |
| } | |
| .catalog-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); | |
| gap: 0.8rem; | |
| } | |
| .catalog-card { | |
| background: #1a1a1a; | |
| border: 1px solid #2a2a2a; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| transition: border-color 0.2s; | |
| } | |
| .catalog-card:hover { border-color: #444; } | |
| .catalog-card-images { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| .catalog-card-images img { | |
| width: 100%; | |
| aspect-ratio: 1; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .catalog-card-images img.original { background: #111; } | |
| .catalog-card-images img.extracted { background: #fff; } | |
| .catalog-card-body { | |
| padding: 0.6rem 0.7rem; | |
| } | |
| .catalog-card-label { | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| color: #e0e0e0; | |
| margin-bottom: 0.15rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .catalog-card-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 0.3rem; | |
| } | |
| .catalog-card-price { | |
| font-size: 0.85rem; | |
| color: #4ade80; | |
| font-weight: 600; | |
| } | |
| .catalog-empty { | |
| grid-column: 1 / -1; | |
| text-align: center; | |
| color: #555; | |
| padding: 3rem 0; | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 768px) { | |
| .layout { | |
| grid-template-columns: 1fr; | |
| } | |
| .upload-panel { position: static; } | |
| .catalog-grid { | |
| grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="top-bar"> | |
| <h1>Catalog Admin</h1> | |
| <a href="/">← Back to Scenedrobe</a> | |
| </div> | |
| <div class="layout"> | |
| <div class="upload-panel"> | |
| <h2>Add Product</h2> | |
| <form id="uploadForm"> | |
| <div class="drop-zone" id="dropZone"> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| <div class="drop-zone-text" id="dropText"> | |
| <strong>Drop image here</strong> or click to browse | |
| </div> | |
| <img class="drop-zone-preview" id="dropPreview"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="labelInput">Label *</label> | |
| <input type="text" id="labelInput" placeholder="e.g. Blue Denim Jacket"> | |
| </div> | |
| <div class="form-group slider-group"> | |
| <label>Mask Erosion <span id="erosionVal">0</span></label> | |
| <input type="range" id="maskErosion" min="0" max="30" step="1" value="0"> | |
| </div> | |
| <button type="button" class="btn btn-primary" id="extractBtn" disabled> | |
| Extract Garment | |
| </button> | |
| <div class="upload-status" id="extractStatus"></div> | |
| <div class="extraction-result" id="extractionResult"> | |
| <div class="extraction-result-header">Extracted garment <span id="extractScore"></span></div> | |
| <img id="extractionImg"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="priceInput">Price</label> | |
| <input type="text" id="priceInput" placeholder="e.g. 49.99"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="urlInput">Product URL</label> | |
| <input type="text" id="urlInput" placeholder="https://..."> | |
| </div> | |
| <button type="submit" class="btn btn-primary" id="saveBtn" disabled style="background:#16a34a;border-color:#16a34a;"> | |
| Save to Catalog | |
| </button> | |
| <div class="upload-status" id="saveStatus"></div> | |
| </form> | |
| </div> | |
| <div class="catalog-panel"> | |
| <h2>Catalog</h2> | |
| <div class="catalog-count" id="catalogCount">Loading...</div> | |
| <div class="catalog-grid" id="catalogGrid"> | |
| <div class="catalog-empty">Loading catalog...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const dropText = document.getElementById('dropText'); | |
| const dropPreview = document.getElementById('dropPreview'); | |
| const uploadForm = document.getElementById('uploadForm'); | |
| const extractBtn = document.getElementById('extractBtn'); | |
| const extractStatus = document.getElementById('extractStatus'); | |
| const extractionResult = document.getElementById('extractionResult'); | |
| const extractionImg = document.getElementById('extractionImg'); | |
| const extractScore = document.getElementById('extractScore'); | |
| const saveBtn = document.getElementById('saveBtn'); | |
| const saveStatus = document.getElementById('saveStatus'); | |
| const labelInput = document.getElementById('labelInput'); | |
| const priceInput = document.getElementById('priceInput'); | |
| const urlInput = document.getElementById('urlInput'); | |
| const catalogGrid = document.getElementById('catalogGrid'); | |
| const catalogCount = document.getElementById('catalogCount'); | |
| const maskErosion = document.getElementById('maskErosion'); | |
| const erosionVal = document.getElementById('erosionVal'); | |
| maskErosion.addEventListener('input', () => { erosionVal.textContent = maskErosion.value; }); | |
| maskErosion.addEventListener('change', () => { if (selectedFile && labelInput.value.trim()) runExtraction(); }); | |
| let selectedFile = null; | |
| let previewData = null; // stores embedding + thumbnails from preview | |
| // ββ Drop zone ββ | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('dragover'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => { | |
| dropZone.classList.remove('dragover'); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length) handleFile(e.target.files[0]); | |
| }); | |
| function handleFile(file) { | |
| if (!file.type.startsWith('image/')) return; | |
| selectedFile = file; | |
| previewData = null; | |
| dropZone.classList.add('has-file'); | |
| dropText.style.display = 'none'; | |
| dropPreview.style.display = 'block'; | |
| dropPreview.src = URL.createObjectURL(file); | |
| clearPreview(); | |
| checkExtractReady(); | |
| // auto-trigger extraction if label is already filled | |
| if (labelInput.value.trim()) runExtraction(); | |
| } | |
| function clearPreview() { | |
| previewData = null; | |
| extractionResult.classList.remove('visible'); | |
| extractStatus.textContent = ''; | |
| extractStatus.className = 'upload-status'; | |
| saveBtn.disabled = true; | |
| saveStatus.textContent = ''; | |
| } | |
| function checkExtractReady() { | |
| extractBtn.disabled = !(selectedFile && labelInput.value.trim()); | |
| } | |
| labelInput.addEventListener('input', () => { | |
| checkExtractReady(); | |
| clearPreview(); | |
| }); | |
| // ββ Step 1: Extract (preview) ββ | |
| extractBtn.addEventListener('click', runExtraction); | |
| async function runExtraction() { | |
| if (!selectedFile || !labelInput.value.trim()) return; | |
| extractBtn.disabled = true; | |
| extractStatus.textContent = 'Detecting & extracting garment...'; | |
| extractStatus.className = 'upload-status'; | |
| clearPreview(); | |
| const formData = new FormData(); | |
| formData.append('image', selectedFile); | |
| formData.append('label', labelInput.value.trim()); | |
| formData.append('mask_erosion', maskErosion.value); | |
| try { | |
| const resp = await fetch('/catalog/preview', { method: 'POST', body: formData }); | |
| const data = await resp.json(); | |
| if (data.status === 'ok') { | |
| previewData = data; | |
| extractionImg.src = `data:image/jpeg;base64,${data.extracted_thumbnail}`; | |
| extractScore.textContent = `(${Math.round(data.detection_score * 100)}% confidence)`; | |
| extractionResult.classList.add('visible'); | |
| extractStatus.textContent = `Extraction successful β review below then save.`; | |
| extractStatus.className = 'upload-status success'; | |
| saveBtn.disabled = false; | |
| } else { | |
| extractStatus.textContent = data.message || 'Extraction failed.'; | |
| extractStatus.className = 'upload-status error'; | |
| } | |
| } catch (err) { | |
| extractStatus.textContent = `Error: ${err.message}`; | |
| extractStatus.className = 'upload-status error'; | |
| } finally { | |
| extractBtn.disabled = false; | |
| checkExtractReady(); | |
| } | |
| } | |
| // ββ Step 2: Save ββ | |
| uploadForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| if (!previewData) return; | |
| saveBtn.disabled = true; | |
| saveStatus.textContent = 'Saving to catalog...'; | |
| saveStatus.className = 'upload-status'; | |
| const formData = new FormData(); | |
| formData.append('label', labelInput.value.trim()); | |
| formData.append('price', priceInput.value.trim()); | |
| formData.append('product_url', urlInput.value.trim()); | |
| formData.append('embedding', JSON.stringify(previewData.embedding)); | |
| formData.append('original_thumbnail', previewData.original_thumbnail); | |
| formData.append('extracted_thumbnail', previewData.extracted_thumbnail); | |
| formData.append('prepared_thumbnail', previewData.prepared_thumbnail || ''); | |
| try { | |
| const resp = await fetch('/catalog/add', { method: 'POST', body: formData }); | |
| const data = await resp.json(); | |
| if (data.status === 'added') { | |
| saveStatus.textContent = `Saved "${data.label}" to catalog`; | |
| saveStatus.className = 'upload-status success'; | |
| resetForm(); | |
| loadCatalog(); | |
| } else { | |
| throw new Error(data.message || 'Unexpected response'); | |
| } | |
| } catch (err) { | |
| saveStatus.textContent = `Error: ${err.message}`; | |
| saveStatus.className = 'upload-status error'; | |
| saveBtn.disabled = false; | |
| } | |
| }); | |
| function resetForm() { | |
| selectedFile = null; | |
| previewData = null; | |
| fileInput.value = ''; | |
| labelInput.value = ''; | |
| priceInput.value = ''; | |
| urlInput.value = ''; | |
| dropZone.classList.remove('has-file'); | |
| dropText.style.display = ''; | |
| dropPreview.style.display = 'none'; | |
| dropPreview.src = ''; | |
| extractBtn.disabled = true; | |
| saveBtn.disabled = true; | |
| extractionResult.classList.remove('visible'); | |
| extractStatus.textContent = ''; | |
| saveStatus.textContent = ''; | |
| } | |
| // ββ Catalog list ββ | |
| async function loadCatalog() { | |
| try { | |
| const resp = await fetch('/catalog/list?limit=200'); | |
| const data = await resp.json(); | |
| catalogCount.textContent = `${data.items.length} item(s)`; | |
| if (data.items.length === 0) { | |
| catalogGrid.innerHTML = '<div class="catalog-empty">No items in catalog yet. Upload some products to get started.</div>'; | |
| return; | |
| } | |
| catalogGrid.innerHTML = ''; | |
| data.items.forEach(item => { | |
| const card = document.createElement('div'); | |
| card.className = 'catalog-card'; | |
| const origSrc = item.original_thumbnail | |
| ? `data:image/jpeg;base64,${item.original_thumbnail}` : ''; | |
| const extSrc = item.extracted_thumbnail | |
| ? `data:image/jpeg;base64,${item.extracted_thumbnail}` : ''; | |
| card.innerHTML = ` | |
| <div class="catalog-card-images"> | |
| ${origSrc | |
| ? `<img class="original" src="${origSrc}" alt="${item.label}" title="Original">` | |
| : `<div style="aspect-ratio:1;background:#222;"></div>` | |
| } | |
| ${extSrc | |
| ? `<img class="extracted" src="${extSrc}" alt="${item.label} (extracted)" title="Extracted">` | |
| : `<div style="aspect-ratio:1;background:#222;"></div>` | |
| } | |
| </div> | |
| <div class="catalog-card-body"> | |
| <div class="catalog-card-label" title="${item.label}">${item.label}</div> | |
| <div class="catalog-card-meta"> | |
| ${item.price ? `<span class="catalog-card-price">$${item.price}</span>` : '<span></span>'} | |
| <button class="btn btn-danger" onclick="deleteItem('${item.id}')">Delete</button> | |
| </div> | |
| </div> | |
| `; | |
| catalogGrid.appendChild(card); | |
| }); | |
| } catch (err) { | |
| catalogGrid.innerHTML = `<div class="catalog-empty">Error loading catalog: ${err.message}</div>`; | |
| } | |
| } | |
| async function deleteItem(id) { | |
| if (!confirm('Delete this item from the catalog?')) return; | |
| try { | |
| await fetch(`/catalog/${id}`, { method: 'DELETE' }); | |
| loadCatalog(); | |
| } catch (err) { | |
| alert(`Error deleting: ${err.message}`); | |
| } | |
| } | |
| // initial load | |
| loadCatalog(); | |
| </script> | |
| </body> | |
| </html> | |