SMV / index.html
Tyan1988's picture
Subo modelo SMV completo
9885230
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Fashion Classifier</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.44.0/tabler-icons.min.css"/>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#0a0c14;color:#e8eaf0;min-height:100vh;padding:2rem 1rem}
.star{position:fixed;border-radius:50%;background:#fff;opacity:0;animation:twinkle var(--d) var(--delay) infinite;pointer-events:none}
@keyframes twinkle{0%,100%{opacity:0}50%{opacity:var(--op)}}
.layout{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 340px;gap:1.5rem;align-items:start}
header{grid-column:1/-1;margin-bottom:.5rem}
.logo-row{display:flex;align-items:center;gap:12px;margin-bottom:.25rem}
.logo-dot{width:8px;height:8px;border-radius:50%;background:#5b6ee1}
h1{font-size:22px;font-weight:500;color:#e8eaf0;letter-spacing:.02em}
.subtitle{font-size:14px;color:#7a7f9a;line-height:1.6}
.card{background:#10131f;border:0.5px solid #1e2235;border-radius:14px;padding:1.5rem}
.upload-zone{border:1px dashed #2a2f4a;border-radius:10px;min-height:340px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;cursor:pointer;transition:border-color .2s,background .2s;position:relative;overflow:hidden}
.upload-zone:hover,.upload-zone.drag{border-color:#5b6ee1;background:#12152299}
.upload-icon{width:56px;height:56px;border-radius:50%;background:#161929;border:0.5px solid #1e2235;display:flex;align-items:center;justify-content:center;font-size:24px;color:#5b6ee1}
.upload-title{font-size:15px;font-weight:500;color:#c8cbde}
.upload-sub{font-size:13px;color:#5a5f78;text-align:center;line-height:1.6}
.upload-btn{padding:8px 20px;background:transparent;border:0.5px solid #2a2f4a;border-radius:8px;color:#a0a5c0;font-size:13px;cursor:pointer;transition:border-color .2s,color .2s}
.upload-btn:hover{border-color:#5b6ee1;color:#8090e8}
#preview-img{max-width:100%;max-height:280px;border-radius:8px;display:none;object-fit:contain}
.hint-box{margin-top:1rem;background:#0d1020;border:0.5px solid #1a1e30;border-left:2px solid #5b6ee1;border-radius:0 8px 8px 0;padding:.75rem 1rem;font-size:12.5px;color:#6a6f8a;line-height:1.65}
.hint-box strong{color:#8090e8;font-weight:500}
.section-label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#3d4260;margin-bottom:.75rem;font-weight:500}
.predict-btn{width:100%;padding:11px;background:#1c2045;border:0.5px solid #2a3060;border-radius:10px;color:#8090e8;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s,color .2s;margin-top:1rem}
.predict-btn:hover:not(:disabled){background:#232850;color:#aab4f0}
.predict-btn:disabled{opacity:.4;cursor:not-allowed}
.result-area{display:flex;flex-direction:column;gap:1rem}
.class-display{text-align:center;padding:1.25rem 1rem;background:#0d1020;border:0.5px solid #1e2235;border-radius:10px}
.class-label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#3d4260;margin-bottom:.5rem}
.class-name{font-size:28px;font-weight:500;color:#c8cbde;line-height:1.2}
.class-id{font-size:12px;color:#3d4260;margin-top:.25rem}
.confidence-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem}
.conf-label{font-size:13px;color:#7a7f9a}
.conf-value{font-size:15px;font-weight:500;color:#8090e8}
.conf-bar-bg{height:6px;background:#161929;border-radius:3px;overflow:hidden}
.conf-bar-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,#3b4acc,#5b6ee1);transition:width .5s ease}
.probs-list{display:flex;flex-direction:column;gap:6px}
.prob-row{display:flex;align-items:center;gap:8px}
.prob-name{font-size:12px;color:#5a5f78;width:90px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.prob-bar-bg{flex:1;height:4px;background:#161929;border-radius:2px;overflow:hidden}
.prob-bar{height:100%;border-radius:2px;background:#2a3060;transition:width .4s ease}
.prob-bar.top{background:#5b6ee1}
.prob-pct{font-size:11px;color:#3d4260;width:34px;text-align:right;flex-shrink:0}
.model-meta{display:flex;flex-direction:column;gap:0}
.meta-row{display:flex;justify-content:space-between;align-items:center;font-size:12.5px;padding:7px 0;border-bottom:0.5px solid #12152299}
.meta-row:last-child{border-bottom:none}
.meta-key{color:#5a5f78}
.meta-val{color:#8090e8;font-weight:500}
.status-dot{width:6px;height:6px;border-radius:50%;background:#2a5c3a;display:inline-block;margin-right:6px;vertical-align:middle}
.status-dot.active{background:#3d9e5c}
.empty-state{text-align:center;padding:1.5rem 0;color:#3d4260;font-size:13px}
.empty-icon{font-size:28px;margin-bottom:.5rem;display:block}
input[type=file]{display:none}
@media(max-width:700px){.layout{grid-template-columns:1fr}}
</style>
</head>
<body>
<div id="stars"></div>
<div class="layout">
<header>
<div class="logo-row"><div class="logo-dot"></div><h1>Fashion Classifier</h1></div>
<p class="subtitle">Support Vector Machine trained on Fashion-MNIST &mdash; 10 clothing categories, split 80/20 for training and evaluation. Upload an image to classify it.</p>
</header>
<div>
<div class="card">
<div class="section-label">Image input</div>
<div class="upload-zone" id="drop-zone">
<img id="preview-img" alt="Preview"/>
<div id="upload-placeholder">
<div style="display:flex;flex-direction:column;align-items:center;gap:.75rem">
<div class="upload-icon"><i class="ti ti-photo-up" aria-hidden="true"></i></div>
<div>
<div class="upload-title">Drop your image here</div>
<div class="upload-sub">PNG, JPG &mdash; ideally 28x28 px<br/>or larger (resized automatically)</div>
</div>
<button class="upload-btn" onclick="document.getElementById('file-input').click()">
<i class="ti ti-upload" style="font-size:14px;vertical-align:-2px;margin-right:4px" aria-hidden="true"></i>Choose file
</button>
</div>
</div>
</div>
<input type="file" id="file-input" accept="image/*"/>
<div class="hint-box">
<strong>For best results</strong> &mdash; the garment should appear on a <strong>plain white or light gray background</strong>, centered in the frame, with no other objects visible. Dark, patterned, or busy backgrounds significantly reduce classification accuracy. The model was trained on clean studio-style images.
</div>
<button class="predict-btn" id="predict-btn" disabled onclick="runPredict()">
<i class="ti ti-cpu" style="font-size:15px;vertical-align:-2px;margin-right:6px" aria-hidden="true"></i>Run classification
</button>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:1.25rem">
<div class="card">
<div class="section-label">Prediction</div>
<div class="result-area" id="result-area">
<div class="empty-state">
<span class="empty-icon"><i class="ti ti-satellite" aria-hidden="true"></i></span>
Upload an image to see the classification result
</div>
</div>
</div>
<div class="card">
<div class="section-label">Model details</div>
<div class="model-meta">
<div class="meta-row"><span class="meta-key">Algorithm</span><span class="meta-val">SVM &mdash; RBF kernel</span></div>
<div class="meta-row"><span class="meta-key">Dimensionality reduction</span><span class="meta-val">PCA (95 % variance)</span></div>
<div class="meta-row"><span class="meta-key">Train / test split</span><span class="meta-val">80 % / 20 %</span></div>
<div class="meta-row"><span class="meta-key">Test accuracy</span><span class="meta-val" id="accuracy-val">90 %+</span></div>
<div class="meta-row"><span class="meta-key">Classes</span><span class="meta-val">10 categories</span></div>
<div class="meta-row">
<span class="meta-key">API status</span>
<span class="meta-val"><span class="status-dot" id="status-dot"></span><span id="status-text">Checking...</span></span>
</div>
</div>
</div>
<div class="card">
<div class="section-label">Categories</div>
<div id="classes-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:4px 12px"></div>
</div>
</div>
</div>
<script>
const CLASS_NAMES=["T-shirt / top","Trouser","Pullover","Dress","Coat","Sandal","Shirt","Sneaker","Bag","Ankle boot"];
const API_BASE=window.location.origin;
const grid=document.getElementById("classes-grid");
CLASS_NAMES.forEach((name,i)=>{
const d=document.createElement("div");
d.style.cssText="font-size:12px;color:#5a5f78;padding:4px 0;display:flex;align-items:center;gap:6px;border-bottom:0.5px solid #12152299";
d.innerHTML=`<span style="font-size:11px;color:#3d4260;width:14px;flex-shrink:0">${i}</span>${name}`;
grid.appendChild(d);
});
(function makeStars(){
const c=document.getElementById("stars");
for(let i=0;i<80;i++){
const s=document.createElement("div"),sz=Math.random()*2+1;
s.className="star";
s.style.cssText=`width:${sz}px;height:${sz}px;left:${Math.random()*100}vw;top:${Math.random()*100}vh;--d:${(Math.random()*4+2).toFixed(1)}s;--delay:${(Math.random()*5).toFixed(1)}s;--op:${(Math.random()*.6+.1).toFixed(2)}`;
c.appendChild(s);
}
})();
async function checkHealth(){
const dot=document.getElementById("status-dot"),txt=document.getElementById("status-text");
try{
const r=await fetch(`${API_BASE}/health`,{signal:AbortSignal.timeout(4000)});
const d=await r.json();
dot.classList.toggle("active",!!d.model_loaded);
txt.textContent=d.model_loaded?"Online":"Model not loaded";
}catch(e){txt.textContent="Offline";}
}
checkHealth();
const dropZone=document.getElementById("drop-zone");
const fileInput=document.getElementById("file-input");
const previewImg=document.getElementById("preview-img");
const placeholder=document.getElementById("upload-placeholder");
const predictBtn=document.getElementById("predict-btn");
let currentPixels=null;
dropZone.addEventListener("dragover",e=>{e.preventDefault();dropZone.classList.add("drag")});
dropZone.addEventListener("dragleave",()=>dropZone.classList.remove("drag"));
dropZone.addEventListener("drop",e=>{e.preventDefault();dropZone.classList.remove("drag");if(e.dataTransfer.files[0])handleFile(e.dataTransfer.files[0])});
fileInput.addEventListener("change",()=>{if(fileInput.files[0])handleFile(fileInput.files[0])});
dropZone.addEventListener("click",e=>{if(e.target===dropZone||e.target===previewImg)fileInput.click()});
function handleFile(file){
const reader=new FileReader();
reader.onload=ev=>{
previewImg.src=ev.target.result;previewImg.style.display="block";placeholder.style.display="none";
extractPixels(ev.target.result);
};
reader.readAsDataURL(file);
}
function extractPixels(dataUrl){
const img=new Image();
img.onload=()=>{
const canvas=document.createElement("canvas");canvas.width=28;canvas.height=28;
const ctx=canvas.getContext("2d");ctx.drawImage(img,0,0,28,28);
const data=ctx.getImageData(0,0,28,28).data,pixels=[];
for(let i=0;i<data.length;i+=4)pixels.push(Math.round((data[i]+data[i+1]+data[i+2])/3));
currentPixels=pixels;predictBtn.disabled=false;
};
img.src=dataUrl;
}
async function runPredict(){
if(!currentPixels)return;
predictBtn.disabled=true;
predictBtn.innerHTML='<i class="ti ti-loader-2" style="font-size:15px;vertical-align:-2px;margin-right:6px"></i>Classifying...';
const resultArea=document.getElementById("result-area");
try{
const r=await fetch(`${API_BASE}/predict`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pixels:currentPixels})});
const d=await r.json();
if(d.error)throw new Error(d.error);
showResult(d);
}catch(e){
resultArea.innerHTML=`<div class="empty-state" style="color:#7a3030"><span class="empty-icon"><i class="ti ti-alert-circle"></i></span>${e.message}</div>`;
}finally{
predictBtn.disabled=false;
predictBtn.innerHTML='<i class="ti ti-cpu" style="font-size:15px;vertical-align:-2px;margin-right:6px"></i>Run classification';
}
}
function showResult(d){
const conf=Math.round(d.confidence*100);
const sorted=Object.entries(d.probabilities).sort((a,b)=>b[1]-a[1]);
const rows=sorted.map(([name,p])=>{
const pct=Math.round(p*100),top=name===d.class_name;
return `<div class="prob-row"><span class="prob-name" title="${name}">${name}</span><div class="prob-bar-bg"><div class="prob-bar${top?" top":""}" style="width:${pct}%"></div></div><span class="prob-pct">${pct}%</span></div>`;
}).join("");
document.getElementById("result-area").innerHTML=`
<div class="class-display"><div class="class-label">Classified as</div><div class="class-name">${d.class_name}</div><div class="class-id">Category ID: ${d.class_id}</div></div>
<div><div class="confidence-row"><span class="conf-label">Confidence</span><span class="conf-value">${conf}%</span></div><div class="conf-bar-bg"><div class="conf-bar-fill" style="width:${conf}%"></div></div></div>
<div><div class="section-label" style="margin-bottom:.6rem">All probabilities</div><div class="probs-list">${rows}</div></div>
${d.inference_ms?`<div style="font-size:11px;color:#3d4260;text-align:right">Inference: ${d.inference_ms} ms</div>`:""}
`;
}
</script>
</body>
</html>