Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Virtual Fashion Try-On</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Cropper.js CSS --> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet"> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen flex flex-col items-center py-10"> | |
| <h1 class="text-4xl font-bold text-blue-400 mb-10">Virtual Fashion Try-On</h1> | |
| <div class="flex flex-col md:flex-row gap-10 w-full max-w-6xl"> | |
| <!-- LEFT: Input Form --> | |
| <form id="tryon-form" action="/" method="post" enctype="multipart/form-data" class="w-full md:w-1/2 bg-gray-800 rounded-2xl shadow-lg p-8 space-y-6"> | |
| <div class="grid grid-cols-1 gap-8"> | |
| <!-- Person Image Upload --> | |
| <div> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h2 class="text-lg font-semibold">Upload your photo</h2> | |
| {% if cached_person %} | |
| <button type="button" onclick="changePerson()" class="bg-yellow-500 hover:bg-yellow-600 text-white text-sm px-3 py-1 rounded-lg transition"> | |
| Change Person | |
| </button> | |
| {% endif %} | |
| </div> | |
| {% if cached_person %} | |
| <!-- Show cached person image --> | |
| <div class="border-2 border-green-500 rounded-xl p-4 bg-green-900/20"> | |
| <div class="flex flex-col items-center"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| <p class="text-green-400 text-sm font-medium">Person image cached</p> | |
| </div> | |
| <img src="/uploads/person.jpg" alt="Cached Person" class="max-h-32 rounded-lg border border-gray-600"> | |
| <p class="text-gray-400 text-xs mt-2">Coordinates saved - no need to re-upload!</p> | |
| </div> | |
| </div> | |
| {% else %} | |
| <label for="person_image" class="flex flex-col items-center justify-center border-2 border-dashed border-gray-600 rounded-xl p-6 hover:bg-gray-700 cursor-pointer"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none" | |
| viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" /> | |
| </svg> | |
| <p class="text-gray-400">Drag & drop or click to upload</p> | |
| <input id="person_image" type="file" name="person_image" class="hidden" | |
| onchange="showFileName('person_image', 'person_filename', 'person_preview')"> | |
| </label> | |
| <p id="person_filename" class="text-green-400 text-sm mt-2 text-center"></p> | |
| <div class="mt-3 flex justify-center"> | |
| <img id="person_preview" class="hidden max-h-32 rounded-lg border border-gray-600"> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <!-- Gender Selection --> | |
| <div> | |
| <h2 class="text-lg font-semibold mb-3">Select Gender</h2> | |
| <div class="flex gap-4"> | |
| <label class="flex items-center cursor-pointer"> | |
| <input type="radio" name="gender" value="male" checked class="sr-only"> | |
| <div class="gender-option flex items-center gap-2 px-4 py-2 rounded-lg border-2 border-blue-500 bg-blue-500/20 text-blue-400"> | |
| <!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> | |
| </svg> --> | |
| <span>Male</span> | |
| </div> | |
| </label> | |
| <label class="flex items-center cursor-pointer"> | |
| <input type="radio" name="gender" value="female" class="sr-only"> | |
| <div class="gender-option flex items-center gap-2 px-4 py-2 rounded-lg border-2 border-gray-600 text-gray-400"> | |
| <!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> | |
| </svg> --> | |
| <span>Female</span> | |
| </div> | |
| </label> | |
| </div> | |
| <!-- <p class="text-xs text-gray-400 mt-2"> | |
| <span class="text-pink-400">Female mode:</span> Uses chest-to-hip detection to avoid hair on shoulders | |
| </p> --> | |
| </div> | |
| <!-- Garment Image Upload with Cropper --> | |
| <div> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h2 class="text-lg font-semibold">Upload garment image</h2> | |
| {% if cached_person %} | |
| <div class="flex items-center gap-1 text-xs text-green-400"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> | |
| </svg> | |
| <span>Fast mode</span> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <label for="tshirt_image" class="flex flex-col items-center justify-center border-2 border-dashed border-gray-600 rounded-xl p-6 hover:bg-gray-700 cursor-pointer"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mb-2" fill="none" | |
| viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M7 16v4m0 0h10m-10 0v-4m0 0h10m-10 0V5m0 0h10m-10 0H5m14 0h-2" /> | |
| </svg> | |
| <p class="text-gray-400">Drag & drop or click to upload</p> | |
| <input id="tshirt_image" type="file" name="tshirt_image" class="hidden" required> | |
| </label> | |
| <p id="tshirt_filename" class="text-green-400 text-sm mt-2 text-center"></p> | |
| <!-- Cropping Container --> | |
| <div class="mt-3 flex justify-center"> | |
| <img id="tshirt_preview" class="hidden max-h-64 rounded-lg border border-gray-600"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Submit Button --> | |
| <div class="flex justify-center"> | |
| <button type="submit" class="bg-pink-500 hover:bg-pink-600 text-white font-semibold py-3 px-8 rounded-xl shadow-md transition"> | |
| 🚀 Perform Virtual Try-On | |
| </button> | |
| </div> | |
| </form> | |
| <!-- RIGHT: Output --> | |
| <div class="w-full md:w-1/2 bg-gray-800 rounded-2xl shadow-lg p-8 flex items-center justify-center text-center"> | |
| {% if result_img %} | |
| <div> | |
| <h2 class="text-2xl font-bold mb-6 text-center">🎉 Your Virtual Try-On Result</h2> | |
| {% if processing_time %} | |
| <div class="text-center mb-4"> | |
| <span class="bg-blue-500 text-white px-3 py-1 rounded-full text-sm"> | |
| ⚡ Processed in {{ processing_time }} | |
| </span> | |
| </div> | |
| {% endif %} | |
| <div class="flex justify-center mb-6"> | |
| <img id="result-image" src="{{ result_img }}" alt="Result Image" class="rounded-xl" | |
| onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> | |
| <div style="display:none;" class="text-red-500 p-4 border border-red-500 rounded-xl"> | |
| <p>❌ Image failed to load</p> | |
| <p class="text-sm">Please try again or check the server logs</p> | |
| <p class="text-xs">Image URL: {{ result_img }}</p> | |
| </div> | |
| </div> | |
| <div class="flex justify-center"> | |
| <button onclick="downloadImage()" class="bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-6 rounded-xl shadow-md transition flex items-center gap-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" | |
| stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
| d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
| </svg> | |
| Download Image | |
| </button> | |
| </div> | |
| </div> | |
| {% else %} | |
| <div id="output-container"> | |
| <div id="loading-spinner" style="display: none;" class="flex flex-col items-center"> | |
| <svg class="animate-spin h-16 w-16 text-pink-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"> | |
| </circle> | |
| <path class="opacity-75" fill="currentColor" | |
| d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 | |
| 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| <p class="mt-4 text-lg">Processing... Please wait.</p> | |
| </div> | |
| <div id="placeholder-text"> | |
| <h2 class="text-2xl font-bold text-center text-gray-500">Your result will appear here</h2> | |
| </div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| <!-- Global Full-screen Loader Overlay --> | |
| <div id="global-loader" style="display:none;" class="fixed inset-0 z-50 bg-black/70 flex items-center justify-center"> | |
| <div class="flex flex-col items-center"> | |
| <svg class="animate-spin h-16 w-16 text-pink-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| <p class="mt-4 text-lg">Processing... Please wait.</p> | |
| </div> | |
| </div> | |
| <!-- Cropper.js --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script> | |
| <script> | |
| let cropper; | |
| // Global loader on every submit | |
| document.getElementById('tryon-form').addEventListener('submit', function(e) { | |
| const overlay = document.getElementById('global-loader'); | |
| if (overlay) overlay.style.display = 'flex'; | |
| // If garment cropper is active → replace original file with cropped version | |
| if (cropper) { | |
| e.preventDefault(); | |
| cropper.getCroppedCanvas().toBlob((blob) => { | |
| const file = new File([blob], "cropped_garment.png", { type: "image/png" }); | |
| // Replace original input | |
| const dataTransfer = new DataTransfer(); | |
| dataTransfer.items.add(file); | |
| document.getElementById('tshirt_image').files = dataTransfer.files; | |
| // Now submit form | |
| e.target.submit(); | |
| }); | |
| return; | |
| } | |
| }); | |
| // Change Person: full refresh via POST + server redirect | |
| function changePerson() { | |
| const form = document.createElement('form'); | |
| form.method = 'POST'; | |
| form.action = '/change_person'; | |
| document.body.appendChild(form); | |
| form.submit(); | |
| } | |
| // Gender selection handling | |
| document.querySelectorAll('input[name="gender"]').forEach(radio => { | |
| radio.addEventListener('change', function() { | |
| // Update visual styling | |
| document.querySelectorAll('.gender-option').forEach(option => { | |
| option.classList.remove('border-blue-500', 'bg-blue-500/20', 'text-blue-400', 'border-pink-500', 'bg-pink-500/20', 'text-pink-400'); | |
| option.classList.add('border-gray-600', 'text-gray-400'); | |
| }); | |
| const selectedOption = this.parentElement.querySelector('.gender-option'); | |
| if (this.value === 'male') { | |
| selectedOption.classList.remove('border-gray-600', 'text-gray-400'); | |
| selectedOption.classList.add('border-blue-500', 'bg-blue-500/20', 'text-blue-400'); | |
| } else { | |
| selectedOption.classList.remove('border-gray-600', 'text-gray-400'); | |
| selectedOption.classList.add('border-pink-500', 'bg-pink-500/20', 'text-pink-400'); | |
| } | |
| }); | |
| }); | |
| // Show file name + preview (person only) | |
| function showFileName(inputId, filenameId, previewId) { | |
| const input = document.getElementById(inputId); | |
| const filename = document.getElementById(filenameId); | |
| const preview = document.getElementById(previewId); | |
| if (input.files.length > 0) { | |
| const file = input.files[0]; | |
| filename.textContent = "✔️ " + file.name + " uploaded"; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| preview.src = e.target.result; | |
| preview.classList.remove("hidden"); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| filename.textContent = ""; | |
| preview.classList.add("hidden"); | |
| } | |
| } | |
| // Garment upload with Cropper | |
| document.getElementById('tshirt_image').addEventListener('change', function() { | |
| const input = this; | |
| const filename = document.getElementById('tshirt_filename'); | |
| const preview = document.getElementById('tshirt_preview'); | |
| if (input.files.length > 0) { | |
| const file = input.files[0]; | |
| filename.textContent = "✔️ " + file.name + " uploaded"; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| preview.src = e.target.result; | |
| preview.classList.remove("hidden"); | |
| // Destroy old cropper if exists | |
| if (cropper) { | |
| cropper.destroy(); | |
| } | |
| // Initialize Cropper.js with square ratio | |
| cropper = new Cropper(preview, { | |
| aspectRatio: 1, | |
| viewMode: 1, | |
| autoCropArea: 1, | |
| }); | |
| }; | |
| reader.readAsDataURL(file); | |
| } else { | |
| filename.textContent = ""; | |
| preview.classList.add("hidden"); | |
| if (cropper) { | |
| cropper.destroy(); | |
| cropper = null; | |
| } | |
| } | |
| }); | |
| // Download result | |
| function downloadImage() { | |
| const img = document.getElementById('result-image'); | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = img.naturalWidth; | |
| canvas.height = img.naturalHeight; | |
| ctx.drawImage(img, 0, 0); | |
| canvas.toBlob(function(blob) { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'virtual-try-on-result.png'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 'image/png'); | |
| } | |
| </script> | |
| </body> | |
| </html> |