添加单张图片变换

This commit is contained in:
Your Name
2026-05-06 00:12:55 +08:00
parent 0b9b5d21ff
commit 47ff49dc19
7 changed files with 1119 additions and 39 deletions
+390
View File
@@ -0,0 +1,390 @@
<!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: 1fr;
gap: 20px;
width: 100%;
max-width: 460px;
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;
}
.prompt-panel {
width: 100%;
max-width: 460px;
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) {
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>
<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>使用默认(传空,不覆盖工作流节点 31)</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>
<script>
const images = { model: 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');
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() {
const btn = document.getElementById('btn-run');
btn.disabled = !images.model;
}
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,
change_desc: getChangeDesc(),
}),
});
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>