891 lines
25 KiB
HTML
891 lines
25 KiB
HTML
<!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();
|
||
formData.append("file", selectedFile);
|
||
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);
|
||
}
|
||
const shouldRestore = restoreResolutionCheckbox.checked ? "true" : "false";
|
||
formData.append("restore_res", shouldRestore);
|
||
formData.append("restore_resolution", shouldRestore);
|
||
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>
|