345 lines
9.1 KiB
HTML
345 lines
9.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>AI 换装系统</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
background: #0f0f13;
|
|
color: #e0e0e0;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 32px 16px 64px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #a78bfa, #60a5fa, #34d399);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 8px;
|
|
text-align: center;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 0.9rem;
|
|
color: #888;
|
|
margin-bottom: 40px;
|
|
text-align: center;
|
|
}
|
|
|
|
.upload-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 20px;
|
|
width: 100%;
|
|
max-width: 900px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.upload-card {
|
|
background: #1a1a24;
|
|
border: 2px dashed #333;
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
cursor: pointer;
|
|
transition: border-color 0.2s, background 0.2s;
|
|
position: relative;
|
|
min-height: 280px;
|
|
}
|
|
|
|
.upload-card:hover {
|
|
border-color: #7c3aed;
|
|
background: #1e1e2e;
|
|
}
|
|
|
|
.upload-card.has-image {
|
|
border-style: solid;
|
|
border-color: #6d28d9;
|
|
}
|
|
|
|
.upload-card input[type="file"] {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.upload-icon {
|
|
font-size: 2.5rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.upload-label {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #c4b5fd;
|
|
}
|
|
|
|
.upload-hint {
|
|
font-size: 0.78rem;
|
|
color: #666;
|
|
text-align: center;
|
|
}
|
|
|
|
.preview-img {
|
|
width: 100%;
|
|
max-height: 200px;
|
|
object-fit: contain;
|
|
border-radius: 10px;
|
|
display: none;
|
|
}
|
|
|
|
.preview-img.visible {
|
|
display: block;
|
|
}
|
|
|
|
.btn-run {
|
|
padding: 14px 56px;
|
|
font-size: 1.05rem;
|
|
font-weight: 700;
|
|
border: none;
|
|
border-radius: 50px;
|
|
background: linear-gradient(135deg, #7c3aed, #2563eb);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s, transform 0.1s;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.btn-run:hover:not(:disabled) {
|
|
opacity: 0.9;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-run:disabled {
|
|
opacity: 0.45;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status-bar {
|
|
margin-top: 20px;
|
|
font-size: 0.9rem;
|
|
color: #888;
|
|
min-height: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.status-bar.running { color: #60a5fa; }
|
|
.status-bar.success { color: #34d399; }
|
|
.status-bar.error { color: #f87171; }
|
|
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid #60a5fa44;
|
|
border-top-color: #60a5fa;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
vertical-align: middle;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
.result-section {
|
|
margin-top: 40px;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
display: none;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.result-section.visible { display: flex; }
|
|
|
|
.result-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: #a78bfa;
|
|
}
|
|
|
|
.result-img {
|
|
width: 100%;
|
|
border-radius: 16px;
|
|
box-shadow: 0 0 40px #7c3aed44;
|
|
}
|
|
|
|
.btn-download {
|
|
padding: 10px 36px;
|
|
border: 2px solid #7c3aed;
|
|
border-radius: 50px;
|
|
background: transparent;
|
|
color: #a78bfa;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
background: #7c3aed22;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.upload-grid { grid-template-columns: 1fr; }
|
|
h1 { font-size: 1.5rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>AI 换装系统</h1>
|
|
<p class="subtitle">上传模特、上衣、裤子三张图片,AI 自动完成换装</p>
|
|
|
|
<div class="upload-grid">
|
|
<!-- 模特 -->
|
|
<div class="upload-card" id="card-model">
|
|
<input type="file" accept="image/*" id="input-model" />
|
|
<div class="upload-icon">🧍</div>
|
|
<div class="upload-label">模特图片</div>
|
|
<div class="upload-hint">节点 11 · 人物全身照</div>
|
|
<img class="preview-img" id="preview-model" alt="模特预览" />
|
|
</div>
|
|
|
|
<!-- 上衣 -->
|
|
<div class="upload-card" id="card-shirt">
|
|
<input type="file" accept="image/*" id="input-shirt" />
|
|
<div class="upload-icon">👕</div>
|
|
<div class="upload-label">上衣图片</div>
|
|
<div class="upload-hint">节点 10 · 上身服装</div>
|
|
<img class="preview-img" id="preview-shirt" alt="上衣预览" />
|
|
</div>
|
|
|
|
<!-- 裤子 -->
|
|
<div class="upload-card" id="card-pants">
|
|
<input type="file" accept="image/*" id="input-pants" />
|
|
<div class="upload-icon">👖</div>
|
|
<div class="upload-label">裤子图片</div>
|
|
<div class="upload-hint">节点 4 · 下身服装</div>
|
|
<img class="preview-img" id="preview-pants" alt="裤子预览" />
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn-run" id="btn-run" disabled>开始换装</button>
|
|
<div class="status-bar" id="status-bar"></div>
|
|
|
|
<div class="result-section" id="result-section">
|
|
<div class="result-title">换装结果</div>
|
|
<img class="result-img" id="result-img" alt="换装结果" />
|
|
<button class="btn-download" id="btn-download">下载图片</button>
|
|
</div>
|
|
|
|
<script>
|
|
const images = { model: null, shirt: null, pants: null };
|
|
|
|
function setupUpload(inputId, previewId, cardId, key) {
|
|
const input = document.getElementById(inputId);
|
|
const preview = document.getElementById(previewId);
|
|
const card = document.getElementById(cardId);
|
|
|
|
input.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
images[key] = ev.target.result; // data:image/...;base64,...
|
|
preview.src = ev.target.result;
|
|
preview.classList.add('visible');
|
|
card.classList.add('has-image');
|
|
updateRunButton();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
setupUpload('input-model', 'preview-model', 'card-model', 'model');
|
|
setupUpload('input-shirt', 'preview-shirt', 'card-shirt', 'shirt');
|
|
setupUpload('input-pants', 'preview-pants', 'card-pants', 'pants');
|
|
|
|
function updateRunButton() {
|
|
const btn = document.getElementById('btn-run');
|
|
btn.disabled = !(images.model && images.shirt && images.pants);
|
|
}
|
|
|
|
function setStatus(msg, cls = '') {
|
|
const el = document.getElementById('status-bar');
|
|
el.className = 'status-bar ' + cls;
|
|
el.innerHTML = msg;
|
|
}
|
|
|
|
function elapsed(start) {
|
|
const s = Math.round((Date.now() - start) / 1000);
|
|
return `${s}秒`;
|
|
}
|
|
|
|
document.getElementById('btn-run').addEventListener('click', async () => {
|
|
const btn = document.getElementById('btn-run');
|
|
const resultSection = document.getElementById('result-section');
|
|
const resultImg = document.getElementById('result-img');
|
|
|
|
btn.disabled = true;
|
|
resultSection.classList.remove('visible');
|
|
|
|
const start = Date.now();
|
|
let timer = setInterval(() => {
|
|
setStatus(`<span class="spinner"></span>正在换装中,请稍候… 已等待 ${elapsed(start)}`, 'running');
|
|
}, 1000);
|
|
setStatus('<span class="spinner"></span>正在上传图片并提交工作流…', 'running');
|
|
|
|
try {
|
|
const resp = await fetch('/try-on', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model_image: images.model,
|
|
shirt_image: images.shirt,
|
|
pants_image: images.pants,
|
|
}),
|
|
});
|
|
|
|
clearInterval(timer);
|
|
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
|
throw new Error(err.detail || '请求失败');
|
|
}
|
|
|
|
const data = await resp.json();
|
|
resultImg.src = data.result_url;
|
|
resultSection.classList.add('visible');
|
|
setStatus(`✅ 换装完成!耗时 ${elapsed(start)}`, 'success');
|
|
|
|
// 下载按钮
|
|
document.getElementById('btn-download').onclick = () => {
|
|
const a = document.createElement('a');
|
|
a.href = data.result_url;
|
|
a.download = `tryon_${Date.now()}.png`;
|
|
a.target = '_blank';
|
|
a.click();
|
|
};
|
|
} catch (err) {
|
|
clearInterval(timer);
|
|
setStatus(`❌ 错误:${err.message}`, 'error');
|
|
} finally {
|
|
updateRunButton();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|