2026-09-10 23:28:59 +02:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > unwrap-clothes | Garment Photo Cleaner< / title >
< style >
:root {
--bg-base: #0a0d14;
--bg-surface: #121722;
--bg-surface-elevated: #182030;
--bg-input: #0e131d;
--border-subtle: #202b3f;
--border-focus: #3b82f6;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--text-dim: #64748b;
--accent-primary: #2563eb;
--accent-primary-hover: #1d4ed8;
--accent-success: #059669;
--accent-success-hover: #047857;
--accent-preset-active: #1e3a8a;
--danger-bg: #450a0a;
--danger-border: #7f1d1d;
--danger-text: #fecaca;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-main);
font-family: var(--font-sans);
line-height: 1.5;
min-height: 100vh;
padding: 2rem 1rem;
}
.container {
max-width: 1120px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.75rem;
}
header {
display: flex;
flex-direction: column;
gap: 0.25rem;
border-bottom: 1px solid var(--border-subtle);
padding-bottom: 1.25rem;
}
.brand-title {
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.025em;
color: #ffffff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.brand-tag {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
background: #1e293b;
color: #93c5fd;
border: 1px solid #334155;
}
.brand-subtitle {
font-size: 0.95rem;
color: var(--text-muted);
}
.card {
background-color: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 1.5rem;
}
.grid-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 840px) {
.grid-form {
grid-template-columns: 1fr;
}
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Drop Zone */
.drop-zone {
border: 2px dashed var(--border-subtle);
background: var(--bg-input);
border-radius: var(--radius-md);
padding: 2rem 1.25rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 260px;
position: relative;
}
.drop-zone:hover,
.drop-zone.dragover {
border-color: var(--border-focus);
background: #131c2d;
}
.drop-zone-icon {
font-size: 2.25rem;
margin-bottom: 0.75rem;
color: var(--text-dim);
}
.drop-zone-prompt h4 {
font-size: 1rem;
font-weight: 600;
color: var(--text-main);
margin-bottom: 0.25rem;
}
.drop-zone-prompt p {
font-size: 0.85rem;
color: var(--text-muted);
}
.file-input {
display: none;
}
.before-preview-thumb {
max-width: 100%;
max-height: 220px;
object-fit: contain;
border-radius: var(--radius-sm);
margin-bottom: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.file-info {
font-size: 0.85rem;
color: #38bdf8;
font-family: var(--font-mono);
background: #0f172a;
padding: 0.35rem 0.75rem;
border-radius: var(--radius-sm);
border: 1px solid #1e293b;
margin-top: 0.5rem;
word-break: break-all;
}
/* Form Controls */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-muted);
}
.preset-group {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.preset-btn {
flex: 1;
padding: 0.45rem 0.75rem;
font-size: 0.825rem;
font-weight: 500;
background: var(--bg-surface-elevated);
color: var(--text-muted);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease;
}
.preset-btn:hover {
border-color: #3b82f6;
color: #ffffff;
}
.preset-btn.active {
background: var(--accent-preset-active);
border-color: #3b82f6;
color: #93c5fd;
font-weight: 600;
}
textarea, input[type="text"], input[type="password"] {
width: 100%;
background: var(--bg-input);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm);
color: var(--text-main);
padding: 0.65rem 0.85rem;
font-size: 0.9rem;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
textarea:focus, input[type="text"]:focus, input[type="password"]:focus {
border-color: var(--border-focus);
}
textarea {
resize: vertical;
min-height: 120px;
line-height: 1.45;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.6rem;
cursor: pointer;
user-select: none;
margin-top: 0.25rem;
}
.checkbox-group input[type="checkbox"] {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--accent-primary);
cursor: pointer;
}
.checkbox-label {
font-size: 0.875rem;
color: var(--text-main);
}
.checkbox-hint {
font-size: 0.75rem;
color: var(--text-dim);
margin-left: 1.7rem;
}
/* Actions & Feedback */
.action-panel {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 0.5rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.8rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
border-radius: var(--radius-sm);
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary {
background: var(--accent-primary);
color: #ffffff;
width: 100%;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-primary-hover);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-download {
background: var(--accent-success);
color: #ffffff;
}
.btn-download:hover {
background: var(--accent-success-hover);
}
.error-banner {
background-color: var(--danger-bg);
border: 1px solid var(--danger-border);
color: var(--danger-text);
padding: 0.85rem 1.15rem;
border-radius: var(--radius-sm);
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
gap: 0.75rem;
}
.spinner {
width: 28px;
height: 28px;
border: 3px solid #1e293b;
border-top-color: #38bdf8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 0.9rem;
color: #93c5fd;
font-weight: 500;
}
.elapsed-timer {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-dim);
}
/* Results section */
.results-container {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.metrics-bar {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
justify-content: space-between;
background: var(--bg-input);
padding: 0.75rem 1rem;
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
}
.metrics-left {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
.metric-badge {
font-family: var(--font-mono);
font-size: 0.825rem;
padding: 0.25rem 0.6rem;
border-radius: var(--radius-sm);
background: #1e293b;
color: #e2e8f0;
border: 1px solid #334155;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
.comparison-card {
background: var(--bg-surface-elevated);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
display: flex;
flex-direction: column;
}
.comparison-header {
padding: 0.65rem 1rem;
background: #0f1420;
border-bottom: 1px solid var(--border-subtle);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
display: flex;
justify-content: space-between;
align-items: center;
}
.comparison-header .tag {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
background: #1e293b;
color: #94a3b8;
}
.comparison-view {
padding: 1rem;
min-height: 380px;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle, #1a2233 10%, #0e131d 90%);
}
.comparison-img {
max-width: 100%;
max-height: 480px;
object-fit: contain;
border-radius: var(--radius-sm);
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
< / style >
< / head >
< body >
< div class = "container" >
< header >
< div class = "brand-title" >
< span > unwrap-clothes< / span >
< span class = "brand-tag" > Studio v1.0< / span >
< / div >
< p class = "brand-subtitle" > Wrinkle-free garment photo cleaning and catalog backdrop standardization< / p >
< / header >
< form id = "clean-form" >
< div class = "grid-form" >
<!-- Upload Card -->
< div class = "card" >
< div class = "section-title" >
< span > Garment Photo< / span >
< / div >
< div id = "drop-zone" class = "drop-zone" >
< input
type="file"
id="file-input"
name="image"
class="file-input"
accept="image/png,image/jpeg,image/webp"
/>
< img
id="before-preview"
class="before-preview-thumb"
alt="Selected garment thumbnail"
style="display: none;"
/>
< div class = "drop-zone-prompt" >
< div class = "drop-zone-icon" > 📷< / div >
< h4 > Drag & drop garment photo here< / h4 >
< p > or click to browse from your computer (PNG, JPEG, WebP)< / p >
< / div >
< div id = "file-info" class = "file-info" style = "display: none;" > < / div >
< / div >
< / div >
<!-- Prompt & Options Card -->
< div class = "card" >
< div class = "section-title" >
< span > Editing Prompt & Model Options< / span >
< / div >
< div class = "form-group" >
< label > Backdrop Presets< / label >
< div class = "preset-group" >
< button type = "button" id = "preset-beige" class = "preset-btn active" data-preset = "beige" >
Warm beige
< / button >
< button type = "button" id = "preset-white" class = "preset-btn" data-preset = "white" >
Bright white
< / button >
< / div >
< / div >
< div class = "form-group" >
< label for = "prompt" > Prompt< / label >
< textarea id = "prompt" name = "prompt" rows = "5" > Product photography edit of this garment photo. Keep the garments pixel-faithful: identical shape, colors, prints, labels, buttons, and every existing flaw unchanged. Remove ALL wrinkles and creases completely: the fabric must look perfectly smooth and freshly ironed, flat like a new catalog product photo. Soft warm beige studio background, evenly lit, no shadows, no props.< / textarea >
< / div >
< div class = "form-group" >
< label for = "model" > Model< / label >
< input type = "text" id = "model" name = "model" value = "meta/muse-image" / >
< / div >
< div class = "form-group" >
< label for = "api-key" > OpenRouter API Key (Optional)< / label >
< input
type="password"
id="api-key"
name="api_key"
placeholder="Using server OPENROUTER_API_KEY if unset"
/>
< / div >
< div class = "form-group" >
< label class = "checkbox-group" >
< input type = "checkbox" id = "restore-resolution" name = "restore_resolution" checked / >
< span class = "checkbox-label" > Restore original resolution< / span >
< / label >
< span class = "checkbox-hint" > Upscales Muse output back to original dimensions using Pillow Lanczos resampling< / span >
< / div >
< / div >
< / div >
< div class = "action-panel" >
< button type = "submit" id = "submit-btn" class = "btn btn-primary" >
Clean Garment
< / button >
< div id = "error-banner" class = "error-banner" style = "display: none;" > < / div >
< div id = "loading-container" class = "loading-container" style = "display: none;" >
< div class = "spinner" > < / div >
< div class = "loading-text" > Editing with Muse, usually takes ~15-20s...< / div >
< div id = "elapsed-timer" class = "elapsed-timer" > 0.0s< / div >
< / div >
< / div >
< / form >
<!-- Results Container -->
< div id = "results-container" class = "results-container" style = "display: none;" >
< div class = "card" >
< div class = "section-title" >
< span > Cleaned Garment Comparison< / span >
< button type = "button" id = "download-btn" class = "btn btn-download" >
Download Clean Image
< / button >
< / div >
< div class = "metrics-bar" >
< div class = "metrics-left" >
< div id = "cost-display" class = "metric-badge cost-display" > Cost: -< / div >
< div id = "resolution-display" class = "metric-badge resolution-display" > Resolution: -< / div >
< / div >
< / div >
< div class = "comparison-grid" style = "margin-top: 1rem;" >
< div class = "comparison-card" >
< div class = "comparison-header" >
< span > Original Photo< / span >
< span class = "tag" > Before< / span >
< / div >
< div class = "comparison-view" >
< img id = "before-image" class = "comparison-img" alt = "Original garment photo" / >
< / div >
< / div >
< div class = "comparison-card" >
< div class = "comparison-header" >
< span > Cleaned Catalog Edit< / span >
< span class = "tag" style = "background:#065f46;color:#a7f3d0;" > After< / span >
< / div >
< div class = "comparison-view" >
< img id = "after-image" class = "comparison-img" alt = "Cleaned catalog garment" / >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< script >
(function () {
const BACKGROUND_PRESETS = {
beige: "Soft warm beige studio background, evenly lit, no shadows, no props.",
white: "Plain bright white studio backdrop, evenly lit, no shadows, no gradients, no props."
};
function buildPrompt(bgChoice) {
const bgDesc = BACKGROUND_PRESETS[bgChoice.toLowerCase()] || bgChoice;
return (
"Product photography edit of this garment photo. Keep the garments pixel-faithful: identical " +
"shape, colors, prints, labels, buttons, and every existing flaw unchanged. Remove ALL wrinkles " +
"and creases completely: the fabric must look perfectly smooth and freshly ironed, flat like a " +
"new catalog product photo. " +
bgDesc
);
}
// Elements
const cleanForm = document.getElementById("clean-form");
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
const beforePreview = document.getElementById("before-preview");
const fileInfo = document.getElementById("file-info");
const promptTextarea = document.getElementById("prompt");
const presetBeige = document.getElementById("preset-beige");
const presetWhite = document.getElementById("preset-white");
const modelInput = document.getElementById("model");
const apiKeyInput = document.getElementById("api-key");
const restoreResolutionCheckbox = document.getElementById("restore-resolution");
const submitBtn = document.getElementById("submit-btn");
const errorBanner = document.getElementById("error-banner");
const loadingContainer = document.getElementById("loading-container");
const elapsedTimer = document.getElementById("elapsed-timer");
const resultsContainer = document.getElementById("results-container");
const beforeImage = document.getElementById("before-image");
const afterImage = document.getElementById("after-image");
const costDisplay = document.getElementById("cost-display");
const resolutionDisplay = document.getElementById("resolution-display");
const downloadBtn = document.getElementById("download-btn");
let selectedFile = null;
let originalFileName = "";
let originalDimensions = null;
let cleanResultData = null;
let timerInterval = null;
let startTime = 0;
function showError(message) {
errorBanner.textContent = message;
errorBanner.style.display = "flex";
}
function hideError() {
errorBanner.textContent = "";
errorBanner.style.display = "none";
}
function startTimer() {
startTime = Date.now();
elapsedTimer.textContent = "0.0s";
timerInterval = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
elapsedTimer.textContent = `${elapsed}s`;
}, 100);
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}
// Presets
presetBeige.addEventListener("click", () => {
promptTextarea.value = buildPrompt("beige");
presetBeige.classList.add("active");
presetWhite.classList.remove("active");
});
presetWhite.addEventListener("click", () => {
promptTextarea.value = buildPrompt("white");
presetWhite.classList.add("active");
presetBeige.classList.remove("active");
});
// Drag & Drop
["dragenter", "dragover"].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add("dragover");
});
});
["dragleave", "drop"].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove("dragover");
});
});
dropZone.addEventListener("drop", (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files & & files.length > 0) {
handleFile(files[0]);
}
});
dropZone.addEventListener("click", (e) => {
if (e.target !== fileInput) {
fileInput.click();
}
});
fileInput.addEventListener("change", () => {
if (fileInput.files & & fileInput.files.length > 0) {
handleFile(fileInput.files[0]);
}
});
function handleFile(file) {
if (!file.type.startsWith("image/")) {
showError("Please select a valid image file (PNG, JPEG, or WebP).");
return;
}
selectedFile = file;
originalFileName = file.name;
hideError();
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target.result;
const img = new Image();
img.onload = () => {
originalDimensions = { width: img.naturalWidth, height: img.naturalHeight };
beforePreview.src = dataUrl;
beforePreview.style.display = "block";
beforeImage.src = dataUrl;
fileInfo.textContent = `${file.name} — ${img.naturalWidth} × ${img.naturalHeight} px (${formatBytes(file.size)})`;
fileInfo.style.display = "block";
const promptEl = dropZone.querySelector(".drop-zone-prompt");
if (promptEl) {
promptEl.style.display = "none";
}
};
img.src = dataUrl;
};
reader.readAsDataURL(file);
}
// Form Submit
cleanForm.addEventListener("submit", async (e) => {
e.preventDefault();
if (!selectedFile) {
showError("Please select or drop a garment photo first.");
return;
}
hideError();
submitBtn.disabled = true;
submitBtn.textContent = "Processing with Muse...";
loadingContainer.style.display = "flex";
startTimer();
try {
const formData = new FormData();
2026-09-10 23:32:40 +02:00
formData.append("file", selectedFile);
2026-09-10 23:28:59 +02:00
formData.append("image", selectedFile);
formData.append("prompt", promptTextarea.value);
formData.append("model", modelInput.value.trim() || "meta/muse-image");
const apiKey = apiKeyInput.value.trim();
if (apiKey) {
formData.append("api_key", apiKey);
}
2026-09-10 23:32:40 +02:00
const shouldRestore = restoreResolutionCheckbox.checked ? "true" : "false";
formData.append("restore_res", shouldRestore);
formData.append("restore_resolution", shouldRestore);
2026-09-10 23:28:59 +02:00
const response = await fetch("/api/clean", {
method: "POST",
body: formData,
});
if (!response.ok) {
let errorMsg = `Server error (${response.status})`;
try {
const errJson = await response.json();
if (typeof errJson.detail === "string") {
errorMsg = errJson.detail;
} else if (Array.isArray(errJson.detail)) {
errorMsg = errJson.detail.map((d) => d.msg || JSON.stringify(d)).join("; ");
} else if (errJson.message) {
errorMsg = errJson.message;
}
} catch (_) {
const text = await response.text();
if (text) errorMsg = text;
}
throw new Error(errorMsg);
}
const data = await response.json();
cleanResultData = data;
displayResults(data);
} catch (err) {
showError(err.message || "Failed to clean garment image.");
} finally {
stopTimer();
loadingContainer.style.display = "none";
submitBtn.disabled = false;
submitBtn.textContent = "Clean Garment";
}
});
function displayResults(data) {
afterImage.src = data.image;
if (data.cost !== null & & data.cost !== undefined) {
costDisplay.textContent = `Cost: $${Number(data.cost).toFixed(4)}`;
} else {
costDisplay.textContent = "Cost: Included / Free";
}
const origDimsStr = data.original_dimensions
? `${data.original_dimensions[0]}× ${data.original_dimensions[1]}`
: originalDimensions
? `${originalDimensions.width}× ${originalDimensions.height}`
: "N/A";
const outDimsStr = `${data.width}× ${data.height}`;
const rescaledNote = data.was_rescaled ? " (Restored via Lanczos)" : "";
resolutionDisplay.textContent = `Resolution: ${origDimsStr} → ${outDimsStr}${rescaledNote}`;
resultsContainer.style.display = "flex";
resultsContainer.scrollIntoView({ behavior: "smooth" });
}
// Download
downloadBtn.addEventListener("click", () => {
if (!cleanResultData || !cleanResultData.image) return;
const baseName = (originalFileName || "garment").replace(/\.[^/.]+$/, "");
const ext = cleanResultData.image.startsWith("data:image/png") ? ".png" : ".jpg";
const downloadName = `${baseName}_clean${ext}`;
const link = document.createElement("a");
link.href = cleanResultData.image;
link.download = downloadName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
})();
< / script >
< / body >
< / html >