Files
change_cloth_server/static/index.html
T
2026-04-06 17:47:45 +08:00

445 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: 960px;
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.optional { border-color: #2a2a3a; }
.upload-card.optional .upload-label { color: #8b80b0; }
.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; }
.badge-optional {
font-size: 0.68rem;
background: #2a2a3a;
color: #777;
border-radius: 6px;
padding: 2px 8px;
letter-spacing: 0.5px;
}
.preview-img {
width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 10px;
display: none;
}
.preview-img.visible { display: block; }
.btn-clear {
position: absolute;
top: 10px;
right: 10px;
background: #3a1a1a;
border: none;
color: #f87171;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 0.8rem;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
z-index: 1;
}
.btn-clear.visible { display: flex; }
.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;
}
.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; }
.raw-resp {
width: 100%;
max-width: 960px;
margin-top: 32px;
background: #1a1a24;
border-radius: 12px;
padding: 16px;
font-size: 0.78rem;
color: #888;
word-break: break-all;
display: none;
}
.raw-resp.visible { display: block; }
.raw-resp pre { white-space: pre-wrap; }
.prompt-panel {
width: 100%;
max-width: 960px;
margin-bottom: 24px;
background: #1a1a24;
border-radius: 16px;
padding: 20px;
border: 1px solid #2a2a3a;
}
.prompt-panel .prompt-title {
font-size: 0.85rem;
font-weight: 600;
color: #a78bfa;
margin-bottom: 12px;
}
.radio-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9rem;
color: #ccc;
}
.radio-row input { margin-top: 3px; accent-color: #7c3aed; flex-shrink: 0; }
.change-desc-input {
width: 100%;
margin-top: 4px;
padding: 12px;
border-radius: 10px;
border: 1px solid #333;
background: #12121a;
color: #e0e0e0;
font-family: inherit;
font-size: 0.88rem;
resize: vertical;
min-height: 88px;
}
.change-desc-input:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 640px) {
.upload-grid { grid-template-columns: 1fr; }
h1 { font-size: 1.5rem; }
}
</style>
</head>
<body>
<h1>AI 换装测试</h1>
<p class="subtitle">上传模特和衣服图片,调用 /change_cloth_base64 接口测试</p>
<div class="upload-grid">
<!-- 模特 -->
<div class="upload-card" id="card-human">
<button class="btn-clear" id="clear-human" title="清除"></button>
<input type="file" accept="image/*" id="input-human" />
<div class="upload-icon">🧍</div>
<div class="upload-label">模特图片</div>
<div class="upload-hint">human_img · 必填</div>
<img class="preview-img" id="preview-human" alt="模特预览" />
</div>
<!-- 上衣 -->
<div class="upload-card" id="card-cloth">
<button class="btn-clear" id="clear-cloth" title="清除"></button>
<input type="file" accept="image/*" id="input-cloth" />
<div class="upload-icon">👕</div>
<div class="upload-label">上衣图片</div>
<div class="upload-hint">cloth_img · 必填</div>
<img class="preview-img" id="preview-cloth" alt="上衣预览" />
</div>
<!-- 裤子(可选) -->
<div class="upload-card optional" id="card-kuzi">
<button class="btn-clear" id="clear-kuzi" title="清除"></button>
<input type="file" accept="image/*" id="input-kuzi" />
<div class="upload-icon">👖</div>
<div class="upload-label">裤子图片</div>
<div class="upload-hint">kuzi_img · 可选</div>
<span class="badge-optional">有则走 8888 服务,无则走 8889</span>
<img class="preview-img" id="preview-kuzi" alt="裤子预览" />
</div>
</div>
<div class="prompt-panel" id="prompt-panel">
<div class="prompt-title">提示词(change_desc</div>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-default" value="default" checked />
<span>使用默认(请求中传空字符串,不覆盖工作流)</span>
</label>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-custom" value="custom" />
<span>自定义提示词</span>
</label>
<textarea id="change-desc" class="change-desc-input" placeholder="输入换装/编辑描述…" rows="4" disabled></textarea>
</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>
<div class="raw-resp" id="raw-resp">
<div style="color:#555;margin-bottom:8px;font-size:0.72rem;">接口原始响应(data 字段已截断)</div>
<pre id="raw-text"></pre>
</div>
<script>
const images = { human: null, cloth: null, kuzi: null };
function setupUpload(inputId, previewId, cardId, clearId, key) {
const input = document.getElementById(inputId);
const preview = document.getElementById(previewId);
const card = document.getElementById(cardId);
const clearBtn= document.getElementById(clearId);
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;
preview.src = ev.target.result;
preview.classList.add('visible');
card.classList.add('has-image');
clearBtn.classList.add('visible');
updateRunButton();
};
reader.readAsDataURL(file);
});
clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
images[key] = null;
input.value = '';
preview.src = '';
preview.classList.remove('visible');
card.classList.remove('has-image');
clearBtn.classList.remove('visible');
updateRunButton();
});
}
setupUpload('input-human', 'preview-human', 'card-human', 'clear-human', 'human');
setupUpload('input-cloth', 'preview-cloth', 'card-cloth', 'clear-cloth', 'cloth');
setupUpload('input-kuzi', 'preview-kuzi', 'card-kuzi', 'clear-kuzi', 'kuzi');
const promptDefault = document.getElementById('prompt-default');
const promptCustom = document.getElementById('prompt-custom');
const changeDescEl = document.getElementById('change-desc');
function syncPromptUi() {
changeDescEl.disabled = !promptCustom.checked;
}
promptDefault.addEventListener('change', syncPromptUi);
promptCustom.addEventListener('change', syncPromptUi);
function getChangeDesc() {
return promptCustom.checked ? changeDescEl.value.trim() : '';
}
function updateRunButton() {
document.getElementById('btn-run').disabled = !(images.human && images.cloth);
}
function setStatus(msg, cls = '') {
const el = document.getElementById('status-bar');
el.className = 'status-bar ' + cls;
el.innerHTML = msg;
}
function elapsed(start) {
return Math.round((Date.now() - start) / 1000) + '秒';
}
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');
const rawResp = document.getElementById('raw-resp');
const rawText = document.getElementById('raw-text');
btn.disabled = true;
resultSection.classList.remove('visible');
rawResp.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');
const body = {
human_img: images.human,
cloth_img: images.cloth,
change_desc: getChangeDesc(),
};
if (images.kuzi) body.kuzi_img = images.kuzi;
try {
const resp = await fetch('/change_cloth_base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
clearInterval(timer);
const data = await resp.json();
// 显示原始响应
rawText.textContent = JSON.stringify(data, null, 2);
rawResp.classList.add('visible');
if (data.ret !== 0 || !data.data) {
throw new Error(data.msg || '接口返回失败');
}
resultImg.src = data.data;
resultSection.classList.add('visible');
setStatus(`✅ 换装完成!耗时 ${elapsed(start)}`, 'success');
document.getElementById('btn-download').onclick = () => {
const a = document.createElement('a');
a.href = data.data;
a.download = `tryon_${Date.now()}.png`;
a.target = '_blank';
a.click();
};
} catch (err) {
clearInterval(timer);
setStatus(`❌ 错误:${err.message}`, 'error');
} finally {
updateRunButton();
}
});
</script>
</body>
</html>