| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> |
| <title>π Virtual Try-On</title> |
| <style> |
| *{box-sizing:border-box;margin:0;padding:0} |
| body{font-family:'Segoe UI',sans-serif;background:#0f0f0f;color:#fff;min-height:100vh} |
| header{background:linear-gradient(135deg,#e879f9,#818cf8);padding:28px;text-align:center} |
| header h1{font-size:2.2rem;font-weight:800;letter-spacing:-1px} |
| header p{opacity:.85;margin-top:8px;font-size:1rem} |
| .container{max-width:1080px;margin:32px auto;padding:0 20px} |
| .model-bar{text-align:center;padding:12px 20px;border-radius:10px;margin-bottom:24px;font-size:.9rem;font-weight:600} |
| .loading{background:#1c1917;color:#fbbf24;border:1px solid #78350f} |
| .ready{background:#052e16;color:#4ade80;border:1px solid #166534} |
| .errored{background:#1c0606;color:#f87171;border:1px solid #7f1d1d} |
| .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px} |
| @media(max-width:720px){.grid{grid-template-columns:1fr}} |
| .card{background:#161616;border-radius:16px;padding:20px;border:1px solid #272727} |
| .card h3{color:#c084fc;font-size:.95rem;margin-bottom:14px;text-transform:uppercase;letter-spacing:.05em} |
| .drop{border:2px dashed #333;border-radius:12px;min-height:240px;display:flex; |
| flex-direction:column;align-items:center;justify-content:center;gap:8px; |
| cursor:pointer;transition:.2s;padding:16px;position:relative} |
| .drop:hover{border-color:#c084fc;background:#1a1a1a} |
| .drop img{max-height:200px;max-width:100%;border-radius:8px;object-fit:contain} |
| .drop input{display:none} |
| .drop .em{font-size:2.8rem} |
| .drop .lbl{color:#555;font-size:.82rem;text-align:center} |
| .btn{width:100%;padding:14px;border:none;border-radius:12px;cursor:pointer; |
| font-size:1rem;font-weight:700;margin-top:14px;letter-spacing:.02em; |
| background:linear-gradient(135deg,#e879f9,#818cf8);color:#fff;transition:.2s} |
| .btn:hover{opacity:.85;transform:translateY(-1px)} |
| .btn:disabled{opacity:.35;cursor:not-allowed;transform:none} |
| .msg{text-align:center;margin-top:10px;font-size:.85rem;color:#888;min-height:18px} |
| .bar-wrap{height:4px;background:#222;border-radius:2px;margin-top:8px;overflow:hidden} |
| .bar{height:100%;width:0%;background:linear-gradient(90deg,#e879f9,#818cf8); |
| border-radius:2px;transition:width .8s ease} |
| .spin{display:none;width:36px;height:36px;border:3px solid #222; |
| border-top-color:#e879f9;border-radius:50%;animation:rot .7s linear infinite;margin:70px auto} |
| @keyframes rot{to{transform:rotate(360deg)}} |
| .res-img{width:100%;border-radius:10px;display:none} |
| .tip{background:#161616;border-radius:12px;padding:18px 20px;margin-top:24px; |
| border-left:4px solid #818cf8} |
| .tip p{color:#666;font-size:.85rem;line-height:1.7} |
| .tip b{color:#a5b4fc} |
| </style> |
| </head> |
| <body> |
| <header> |
| <h1>π Virtual Try-On</h1> |
| <p>Upload your photo + any garment β powered by AI running right here</p> |
| </header> |
| <div class="container"> |
| <div class="model-bar loading" id="modelBar">β³ AI model loading β ready in ~2 minutes on first start...</div> |
| <div class="grid"> |
| |
| <div class="card"> |
| <h3>π€ Your Photo</h3> |
| <div class="drop" onclick="document.getElementById('pIn').click()"> |
| <div class="em" id="pEm">π§</div> |
| <div class="lbl" id="pLbl">Click to upload<br>Full body Β· Front facing</div> |
| <img id="pImg"/> |
| <input type="file" id="pIn" accept="image/*" onchange="preview(this,'pImg','pEm','pLbl','person')"/> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h3>π Garment</h3> |
| <div class="drop" onclick="document.getElementById('gIn').click()"> |
| <div class="em" id="gEm">π</div> |
| <div class="lbl" id="gLbl">Click to upload<br>White bg recommended</div> |
| <img id="gImg"/> |
| <input type="file" id="gIn" accept="image/*" onchange="preview(this,'gImg','gEm','gLbl','garment')"/> |
| </div> |
| <button class="btn" id="btn" onclick="run()" disabled>β¨ Try It On!</button> |
| <div class="msg" id="msg"></div> |
| <div class="bar-wrap"><div class="bar" id="bar"></div></div> |
| </div> |
| |
| <div class="card"> |
| <h3>π Result</h3> |
| <div class="drop" style="cursor:default"> |
| <div class="em" id="rEm">β¨</div> |
| <div class="lbl" id="rLbl">Your try-on result<br>will appear here</div> |
| <div class="spin" id="spin"></div> |
| <img class="res-img" id="rImg"/> |
| </div> |
| </div> |
| </div> |
| <div class="tip"> |
| <p><b>Tips for best results:</b> Use a clear front-facing full-body photo with good lighting. Garment images on plain white background produce the most realistic try-on. The AI will place the garment on your upper body area automatically.</p> |
| </div> |
| </div> |
| <script> |
| let files={person:null,garment:null}, modelReady=false; |
| |
| async function pollModel(){ |
| try{ |
| const d=await(await fetch('/status')).json(); |
| const bar=document.getElementById('modelBar'); |
| if(d.loaded){ |
| bar.className='model-bar ready'; bar.textContent='β
AI Model Ready!'; |
| modelReady=true; updateBtn(); |
| } else if(d.error){ |
| bar.className='model-bar errored'; bar.textContent='β '+d.error; |
| } else { setTimeout(pollModel,4000); } |
| }catch(e){setTimeout(pollModel,4000);} |
| } |
| pollModel(); |
| |
| function preview(input,imgId,emId,lblId,key){ |
| const f=input.files[0]; if(!f)return; |
| files[key]=f; |
| const r=new FileReader(); |
| r.onload=e=>{ |
| document.getElementById(emId).style.display='none'; |
| document.getElementById(lblId).style.display='none'; |
| const i=document.getElementById(imgId); |
| i.src=e.target.result; i.style.display='block'; |
| }; |
| r.readAsDataURL(f); updateBtn(); |
| } |
| |
| function updateBtn(){ |
| document.getElementById('btn').disabled=!(files.person&&files.garment&&modelReady); |
| } |
| |
| async function run(){ |
| const btn=document.getElementById('btn'); |
| const msg=document.getElementById('msg'); |
| const spin=document.getElementById('spin'); |
| const rImg=document.getElementById('rImg'); |
| const rEm=document.getElementById('rEm'); |
| const rLbl=document.getElementById('rLbl'); |
| const bar=document.getElementById('bar'); |
| |
| btn.disabled=true; msg.textContent='β³ Processing (~30-60s)...'; |
| spin.style.display='block'; rImg.style.display='none'; |
| rEm.style.display='none'; rLbl.style.display='none'; |
| bar.style.width='0%'; |
| |
| let p=0; const iv=setInterval(()=>{p=Math.min(p+1.5,88);bar.style.width=p+'%';},1000); |
| |
| const fd=new FormData(); |
| fd.append('person',files.person); |
| fd.append('garment',files.garment); |
| |
| try{ |
| const res=await fetch('/tryon',{method:'POST',body:fd}); |
| const d=await res.json(); |
| clearInterval(iv); bar.style.width='100%'; |
| spin.style.display='none'; |
| if(d.status==='ok'){ |
| rImg.src='data:image/png;base64,'+d.image; |
| rImg.style.display='block'; |
| msg.textContent='β
Done! Download or try another outfit.'; |
| } else if(d.status==='loading'){ |
| msg.textContent='β³ Still warming up, retrying in 20s...'; |
| rEm.style.display='block'; rLbl.style.display='block'; rEm.textContent='β³'; |
| setTimeout(run,20000); return; |
| } else { |
| msg.textContent='β '+d.message; |
| rEm.style.display='block'; rLbl.style.display='block'; |
| } |
| }catch(e){ |
| clearInterval(iv); spin.style.display='none'; |
| msg.textContent='β '+e.message; |
| rEm.style.display='block'; rLbl.style.display='block'; |
| } |
| btn.disabled=false; |
| } |
| </script> |
| </body> |
| </html> |
|
|