205 lines
10 KiB
HTML
205 lines
10 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>接口12 final — 发际线带重绘(精简版)</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; }
|
||
.container { max-width: 1240px; margin: 0 auto; padding: 24px; }
|
||
h1 { font-size: 22px; margin-bottom: 4px; }
|
||
h2 { font-size: 16px; margin: 20px 0 12px; }
|
||
.subtitle { color: #888; font-size: 13px; margin-bottom: 16px; line-height: 1.6; }
|
||
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
|
||
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||
input[type=file] { flex: 1; min-width: 200px; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
|
||
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; font-weight: 600; }
|
||
.btn-primary { background: #7c3aed; color: #fff; }
|
||
.btn-primary:disabled { background: #c4b5fd; cursor: not-allowed; }
|
||
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
|
||
.btn-green { background: #059669; color: #fff; }
|
||
.params { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; margin-top: 16px; }
|
||
.pf { display: flex; flex-direction: column; gap: 4px; }
|
||
.pf label { font-size: 13px; font-weight: 600; }
|
||
.pf select { padding: 8px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
|
||
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; font-family: monospace; }
|
||
.status.info { background: #ede9fe; color: #5b21b6; display: block; }
|
||
.status.error { background: #fee2e2; color: #991b1b; display: block; }
|
||
.status.success { background: #d1fae5; color: #065f46; display: block; }
|
||
.steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
||
.step { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||
.step .cap { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; }
|
||
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; }
|
||
.step img { width: 100%; display: block; background: #eee; cursor: zoom-in; }
|
||
.big img { max-height: 520px; object-fit: contain; }
|
||
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; z-index: 50; cursor: zoom-out; }
|
||
.lightbox img { max-width: 95%; max-height: 95%; }
|
||
.hidden { display: none; }
|
||
</style>
|
||
<script src="/static/img_downscale.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>接口12 final — 发际线带重绘 <span style="font-size:13px;color:#888">(精简版:final + 纯红遮罩 → ComfyUI 重绘)</span></h1>
|
||
<div class="subtitle">
|
||
只需上传图片 + 选择发型,其余参数全部用当前调优默认值(<code>/api/v1/hairline/grow_v2_final</code>)。<br>
|
||
后端产出 <b>④ final(接缝融合基底)</b> + <b>⑤-② 纯红遮罩 PNG</b>,再调 <b>后端重绘接口(/api/v1/redraw)</b>完成发际线带重绘。⚠️ 需 ComfyUI(:8188) 在跑。
|
||
局部加发版见 <a href="/static/test_interface12_final_v2.html">接口12 final v2</a>;完整参数调试见 <a href="/static/test_interface12.html">接口12 调试页</a>。
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="upload-row">
|
||
<input type="file" id="imageFile" accept="image/*">
|
||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🎨 生成 final+遮罩</button>
|
||
<button class="btn btn-green" id="redrawBtn" onclick="runLocalRedraw()" disabled>🧪 重绘</button>
|
||
</div>
|
||
<div class="upload-row" style="margin-top:10px">
|
||
<label style="font-size:13px;font-weight:600;white-space:nowrap">重绘提示词</label>
|
||
<input type="text" id="localTestPrompt" value="填充遮罩区域的头发,皮肤加一点磨皮" style="flex:1;min-width:200px;padding:8px;border:1px solid #ddd;border-radius:8px">
|
||
</div>
|
||
<div class="params">
|
||
<div class="pf">
|
||
<label>发际线类型 ID</label>
|
||
<select id="hairlineId">
|
||
<option value="chang_bolang" selected>chang_bolang(波浪)</option>
|
||
<option value="chang_zhixian">chang_zhixian(直线)</option>
|
||
<option value="chang_tuoyuan">chang_tuoyuan(椭圆)</option>
|
||
<option value="chang_xinxing">chang_xinxing(心形)</option>
|
||
<option value="chang_huaban">chang_huaban(花瓣)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status" id="statusBar"></div>
|
||
|
||
<div id="resultsArea" class="hidden">
|
||
<div class="card">
|
||
<h2 style="margin-top:0">🎯 final + 纯红遮罩 → 重绘结果</h2>
|
||
<div class="steps" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
||
<div class="step big"><div class="cap">接口11 final <small>④ 接缝融合(重绘输入基底,无美颜)</small></div><img id="finalBase"></div>
|
||
<div class="step big"><div class="cap">⑤-① 发际线重绘带遮罩 <small>纯红 alpha PNG(遮罩区=红+不透明,其余全透明)</small></div><img id="maskPng"></div>
|
||
<div class="step big"><div class="cap">🧪 重绘结果 <small>final + 遮罩 → ComfyUI(0716add-hair)</small></div><img id="localResult"></div>
|
||
</div>
|
||
<div style="font-size:12px;color:#888;margin-top:10px">流程:后端产出 final(接缝融合基底)+ 纯红遮罩 PNG,再调后端 /api/v1/redraw 完成发际线补发。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lightbox" id="lightbox" onclick="this.style.display='none'"><img id="lightboxImg" alt=""></div>
|
||
|
||
<script>
|
||
const API_BASE = window.location.origin;
|
||
const ENDPOINT = '/api/v1/hairline/grow_v2_final';
|
||
const TOKEN = 'dev-shared-secret-2026';
|
||
|
||
// 缓存最近一次后端返回的 final(data URI)和纯红遮罩(data URI),供重绘使用
|
||
let _finalDataUri = '';
|
||
let _maskDataUri = '';
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
function setStatus(text, type) { const b = $('statusBar'); b.textContent = text; b.className = 'status ' + type; }
|
||
function pick(obj, name) { if (!obj) return null; return obj[name + '_url'] || obj[name + '_base64'] || null; }
|
||
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
|
||
|
||
function renderResult(d) {
|
||
const s = d.steps || {};
|
||
const finalSrc = pick(s, 'final') || '';
|
||
const maskSrc = pick(s, 'redraw_band_mask') || '';
|
||
_finalDataUri = finalSrc;
|
||
_maskDataUri = maskSrc;
|
||
$('finalBase').src = finalSrc;
|
||
$('finalBase').onclick = function(){ zoom(this.src); };
|
||
$('maskPng').src = maskSrc;
|
||
$('maskPng').onclick = function(){ zoom(this.src); };
|
||
const canRedraw = !!(finalSrc && maskSrc);
|
||
$('redrawBtn').disabled = !canRedraw;
|
||
}
|
||
|
||
// data URI → Blob,用于把后端返回的 final/遮罩图作为文件 POST 给后端重绘
|
||
function dataUriToBlob(dataUri) {
|
||
const [meta, b64] = dataUri.split(',');
|
||
const mime = (meta.match(/data:([^;]+)/) || [, 'application/octet-stream'])[1];
|
||
const bin = atob(b64);
|
||
const arr = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
||
return new Blob([arr], { type: mime });
|
||
}
|
||
|
||
async function submitTest() {
|
||
let file = $('imageFile').files[0];
|
||
if (!file) { setStatus('请先选择图片', 'error'); return; }
|
||
file = await window.downscaleImageFile(file);
|
||
|
||
const btn = $('submitBtn');
|
||
btn.disabled = true; btn.textContent = '⏳ 生成中...';
|
||
setStatus('正在请求(内部跑接口11 生成 final + 纯红遮罩,约 10~15s)...', 'info');
|
||
$('resultsArea').classList.remove('hidden');
|
||
|
||
const form = new FormData();
|
||
form.append('image_file', file);
|
||
form.append('hairline_id', $('hairlineId').value);
|
||
|
||
const t0 = performance.now();
|
||
try {
|
||
const resp = await fetch(API_BASE + ENDPOINT, {
|
||
method: 'POST',
|
||
headers: { 'X-Internal-Token': TOKEN },
|
||
body: form,
|
||
});
|
||
const json = await resp.json();
|
||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||
|
||
if (json.code === 0) {
|
||
const d = json.data;
|
||
const hasFinal = !!(pick(d.steps, 'final'));
|
||
const hasMask = !!(pick(d.steps, 'redraw_band_mask'));
|
||
setStatus((hasFinal && hasMask ? '✅ 已生成 final + 纯红遮罩' : '⚠️ 已返回(final/遮罩缺失,见日志)') + ' (' + dt + 's)', (hasFinal && hasMask) ? 'success' : 'error');
|
||
renderResult(d);
|
||
} else {
|
||
setStatus('❌ 业务错误 code=' + json.code + ':' + json.message, 'error');
|
||
}
|
||
} catch (err) {
|
||
setStatus('❌ 网络错误: ' + err.message, 'error');
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = '🎨 生成 final+遮罩';
|
||
}
|
||
}
|
||
|
||
async function runLocalRedraw() {
|
||
if (!_finalDataUri || !_maskDataUri) { setStatus('缺少 final 或遮罩,请先生成', 'error'); return; }
|
||
const prompt = $('localTestPrompt').value.trim() || '填充遮罩区域的头发,皮肤加一点磨皮';
|
||
const btn = $('redrawBtn');
|
||
btn.disabled = true; btn.textContent = '⏳ 重绘中...';
|
||
|
||
const form = new FormData();
|
||
form.append('image_file', dataUriToBlob(_finalDataUri), 'final.jpg');
|
||
form.append('mask_file', dataUriToBlob(_maskDataUri), 'mask.png');
|
||
form.append('prompt', prompt);
|
||
|
||
const t0 = performance.now();
|
||
try {
|
||
const resp = await fetch(API_BASE + '/api/v1/redraw', {
|
||
method: 'POST',
|
||
body: form,
|
||
});
|
||
const dt = ((performance.now() - t0) / 1000).toFixed(2);
|
||
const json = await resp.json();
|
||
if (json.code === 0 && json.data && json.data.image_base64) {
|
||
$('localResult').src = json.data.image_base64;
|
||
$('localResult').onclick = function(){ zoom(this.src); };
|
||
setStatus('✅ 重绘完成 (' + dt + 's)', 'success');
|
||
} else {
|
||
setStatus('❌ 重绘失败:' + (json.message || JSON.stringify(json)), 'error');
|
||
}
|
||
} catch (err) {
|
||
setStatus('❌ 重绘网络错误:' + err.message, 'error');
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = '🧪 重绘';
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|