save code
This commit is contained in:
@@ -595,7 +595,7 @@ async def hair_grow_b(
|
|||||||
marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB)"),
|
marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB)"),
|
||||||
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
||||||
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
||||||
use_mask: bool = Form(default=True, description="是否启用自动检测遮罩(测试对比用)。false 时跳过检测、直接送划线图(空遮罩)"),
|
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
||||||
):
|
):
|
||||||
# 划线图三选一取图(只需这一张)
|
# 划线图三选一取图(只需这一张)
|
||||||
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
||||||
|
|||||||
+1
-1
@@ -244,7 +244,7 @@
|
|||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| marked_image_* | file / string | 是 | 已用马克笔标注发际线的图片,三选一 |
|
| marked_image_* | file / string | 是 | 已用马克笔标注发际线的图片,三选一 |
|
||||||
| use_mask | bool | 否 | 是否启用自动检测遮罩,默认 `true`。`false` 时跳过划线检测、直接送划线图(空遮罩),模型仅凭手绘黑线生发,供测试对比 |
|
| use_mask | bool | 否 | 是否画发际线,默认 `true`。`false` 时跳过划线检测、直接送划线图,模型仅凭手绘黑线生发,供测试对比 |
|
||||||
|
|
||||||
### 输出(data)
|
### 输出(data)
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,13 @@
|
|||||||
<label>性别</label>
|
<label>性别</label>
|
||||||
<select id="gender"><option value="female" selected>👩 Female</option><option value="male">👨 Male</option></select>
|
<select id="gender"><option value="female" selected>👩 Female</option><option value="male">👨 Male</option></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>inpaint 遮罩</label>
|
||||||
|
<select id="useMask">
|
||||||
|
<option value="true" selected>✅ 启用(默认)</option>
|
||||||
|
<option value="false">⬜ 关闭 — 空遮罩,不烧模板线</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +125,7 @@ async function submitTest() {
|
|||||||
setStatus('⏳ 请求中(生发图生成较慢,请耐心)...', 'info'); $('resultsArea').classList.add('hidden');
|
setStatus('⏳ 请求中(生发图生成较慢,请耐心)...', 'info'); $('resultsArea').classList.add('hidden');
|
||||||
|
|
||||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value);
|
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value);
|
||||||
|
fd.append('use_mask', $('useMask').value);
|
||||||
try {
|
try {
|
||||||
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', body:fd });
|
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', body:fd });
|
||||||
const json = await r.json();
|
const json = await r.json();
|
||||||
|
|||||||
+65
-26
@@ -52,6 +52,17 @@
|
|||||||
.result-field .name { font-size: 12px; font-weight: 600; color: #6b7280; margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
|
.result-field .name { font-size: 12px; font-weight: 600; color: #6b7280; margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
|
||||||
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; background: #dbeafe; color: #1e40af; }
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; background: #dbeafe; color: #1e40af; }
|
||||||
|
|
||||||
|
/* blend */
|
||||||
|
.blend-container { position: relative; width: 100%; background: #222; border-radius: 8px; overflow: hidden; line-height: 0; }
|
||||||
|
.blend-container img { display: block; width: 100%; max-height: 500px; object-fit: contain; }
|
||||||
|
.blend-bottom { position: relative; z-index: 1; }
|
||||||
|
.blend-top { position: absolute; top: 0; left: 0; z-index: 2; transition: opacity 0.06s; }
|
||||||
|
.blend-placeholder { color: #9ca3af; font-size: 12px; padding: 80px 20px; background: #f9fafb; line-height: 1.5; }
|
||||||
|
.blend-controls { display: flex; align-items: center; gap: 10px; margin-top: 14px; justify-content: center; }
|
||||||
|
.blend-controls input[type=range] { width: 220px; accent-color: #2563eb; }
|
||||||
|
.blend-label { font-size: 12px; color: #6b7280; font-weight: 600; }
|
||||||
|
.blend-value { font-size: 13px; font-weight: 600; color: #374151; min-width: 42px; text-align: center; background: #f3f4f6; border-radius: 6px; padding: 2px 8px; }
|
||||||
|
|
||||||
.json-panel { max-height: 550px; overflow: auto; }
|
.json-panel { max-height: 550px; overflow: auto; }
|
||||||
.json-content { padding: 14px 16px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
.json-content { padding: 14px 16px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
|
||||||
@@ -72,6 +83,14 @@
|
|||||||
<div class="label">✏️ 划线图(marked_image)<span class="tag required">必填</span></div>
|
<div class="label">✏️ 划线图(marked_image)<span class="tag required">必填</span></div>
|
||||||
<input type="file" id="markedFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
<input type="file" id="markedFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="upload-group" style="margin-top:14px">
|
||||||
|
<div class="label">🪄 画发际线(use_mask)<span class="tag-new">NEW</span></div>
|
||||||
|
<select id="useMask" style="padding:8px 12px;border:2px solid #ddd;border-radius:8px;font-size:14px;background:#fff;min-width:280px">
|
||||||
|
<option value="true" selected>✅ 画发际线(默认)— 检测划线并生成遮罩</option>
|
||||||
|
<option value="false">⬜ 不画发际线 — 跳过划线检测,直接送划线图(测试对比用)</option>
|
||||||
|
</select>
|
||||||
|
<div class="hint">false 时跳过划线检测、直接送划线图,用于对比测试</div>
|
||||||
|
</div>
|
||||||
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
<div style="margin-top:14px;display:flex;gap:12px;align-items:center">
|
||||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||||
@@ -83,24 +102,19 @@
|
|||||||
|
|
||||||
<div class="results-layout hidden" id="resultsArea">
|
<div class="results-layout hidden" id="resultsArea">
|
||||||
<div class="col-main">
|
<div class="col-main">
|
||||||
<!-- 输入预览 -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><span>📸 划线图预览</span></div>
|
<div class="card-header"><span>🔄 原图 / 生发图 叠加对比 </span><span class="badge" id="hairlineType" style="display:none"></span></div>
|
||||||
<div class="card-body" style="text-align:center">
|
<div class="card-body" style="text-align:center">
|
||||||
<div class="thumb" id="markedThumb"><span style="color:#9ca3af;padding:40px">—</span></div>
|
<div class="blend-container" id="blendContainer">
|
||||||
</div>
|
<img id="blendBottom" class="blend-bottom" alt="原图(划线图)" style="display:none">
|
||||||
</div>
|
<img id="blendTop" class="blend-top" alt="生发效果图" style="display:none;opacity:0.5">
|
||||||
<!-- 结果 -->
|
<div class="blend-placeholder" id="blendPlaceholder">—</div>
|
||||||
<div class="card">
|
|
||||||
<div class="card-header"><span>🎯 生发效果</span></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="result-field">
|
|
||||||
<div class="name">发际线类型</div>
|
|
||||||
<span class="badge" id="hairlineType">—</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="result-field">
|
<div class="blend-controls hidden" id="blendControls">
|
||||||
<div class="name">生发效果图(hair_growth_image)<span class="tag-new">ComfyUI</span></div>
|
<span class="blend-label">原图</span>
|
||||||
<div class="thumb" id="growthImg"><span style="color:#9ca3af;padding:40px">—</span></div>
|
<input type="range" id="blendSlider" min="0" max="1" step="0.01" value="0.5">
|
||||||
|
<span class="blend-label">生发图</span>
|
||||||
|
<span class="blend-value" id="blendValue">0.50</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,14 +139,20 @@ async function submitTest() {
|
|||||||
if (!mf) { setStatus('请选择划线图', 'error'); return; }
|
if (!mf) { setStatus('请选择划线图', 'error'); return; }
|
||||||
if (mf.size > 1_000_000) { setStatus('文件超过 1 MB', 'error'); return; }
|
if (mf.size > 1_000_000) { setStatus('文件超过 1 MB', 'error'); return; }
|
||||||
|
|
||||||
// 预览划线图
|
const markedUrl = URL.createObjectURL(mf);
|
||||||
$('markedThumb').innerHTML = '<img src="'+URL.createObjectURL(mf)+'" alt="划线图">';
|
// 先显示原图
|
||||||
|
$('blendPlaceholder').style.display = 'none';
|
||||||
|
$('blendBottom').src = markedUrl;
|
||||||
|
$('blendBottom').style.display = 'block';
|
||||||
|
$('blendTop').style.display = 'none';
|
||||||
|
$('blendControls').classList.add('hidden');
|
||||||
|
|
||||||
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
|
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
|
||||||
setStatus('请求中(生发需要数十秒)...', 'info'); $('resultsArea').classList.add('hidden');
|
setStatus('请求中(生发需要数十秒)...', 'info'); $('resultsArea').classList.remove('hidden');
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('marked_image_file', mf);
|
fd.append('marked_image_file', mf);
|
||||||
|
fd.append('use_mask', $('useMask').value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(API_BASE + '/api/v1/hair/grow-b', { method:'POST', body:fd });
|
const r = await fetch(API_BASE + '/api/v1/hair/grow-b', { method:'POST', body:fd });
|
||||||
@@ -142,18 +162,29 @@ async function submitTest() {
|
|||||||
|
|
||||||
if (json.code === 0) {
|
if (json.code === 0) {
|
||||||
const d = json.data;
|
const d = json.data;
|
||||||
$('hairlineType').textContent = d.hairline_type || '—';
|
if (d.hairline_type) {
|
||||||
|
$('hairlineType').textContent = '发际线: ' + d.hairline_type;
|
||||||
|
$('hairlineType').style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
if (d.hair_growth_image_url) {
|
const grownSrc = d.hair_growth_image_url || d.hair_growth_image_base64;
|
||||||
$('growthImg').innerHTML = '<img src="'+d.hair_growth_image_url+'" alt="生发效果">';
|
if (grownSrc) {
|
||||||
|
$('blendTop').src = grownSrc;
|
||||||
|
$('blendTop').style.display = 'block';
|
||||||
|
$('blendTop').style.opacity = $('blendSlider').value;
|
||||||
|
$('blendControls').classList.remove('hidden');
|
||||||
|
$('blendValue').textContent = parseFloat($('blendSlider').value).toFixed(2);
|
||||||
} else {
|
} else {
|
||||||
$('growthImg').innerHTML = '<div class="na">⚠ 生发图未返回<br><span style="font-size:10px;color:#9ca3af">ComfyUI 未就绪或生成失败</span></div>';
|
$('blendTop').style.display = 'none';
|
||||||
|
$('blendControls').classList.add('hidden');
|
||||||
|
setStatus('⚠ 生发图未返回 — ComfyUI 未就绪或生成失败', 'info');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('✅ 成功 | 发际线类型: ' + (d.hairline_type || '—'), 'success');
|
setStatus('✅ 成功 | 发际线类型: ' + (d.hairline_type || '—'), 'success');
|
||||||
} else {
|
} else {
|
||||||
setStatus('❌ code=' + json.code + ' ' + json.message, 'error');
|
setStatus('❌ code=' + json.code + ' ' + json.message, 'error');
|
||||||
$('growthImg').innerHTML = '<div class="na">—</div>';
|
$('blendTop').style.display = 'none';
|
||||||
}
|
}
|
||||||
} catch(e) { setStatus('❌ ' + e.message, 'error'); $('resultsArea').classList.remove('hidden'); }
|
} catch(e) { setStatus('❌ ' + e.message, 'error'); $('resultsArea').classList.remove('hidden'); }
|
||||||
finally { $('submitBtn').disabled = false; $('submitBtn').textContent = '🚀 提交'; }
|
finally { $('submitBtn').disabled = false; $('submitBtn').textContent = '🚀 提交'; }
|
||||||
@@ -162,11 +193,19 @@ async function submitTest() {
|
|||||||
function clearResults() {
|
function clearResults() {
|
||||||
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
|
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
|
||||||
$('markedFile').value=''; $('jsonContent').textContent='';
|
$('markedFile').value=''; $('jsonContent').textContent='';
|
||||||
$('markedThumb').innerHTML='<span style="color:#9ca3af;padding:40px">—</span>';
|
$('blendBottom').src = ''; $('blendBottom').style.display = 'none';
|
||||||
$('growthImg').innerHTML='<span style="color:#9ca3af;padding:40px">—</span>';
|
$('blendTop').src = ''; $('blendTop').style.display = 'none';
|
||||||
$('hairlineType').textContent='—';
|
$('blendPlaceholder').style.display = 'block';
|
||||||
|
$('blendControls').classList.add('hidden');
|
||||||
|
$('blendSlider').value = 0.5; $('blendValue').textContent = '0.50';
|
||||||
|
$('hairlineType').textContent = ''; $('hairlineType').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('blendSlider').addEventListener('input', function() {
|
||||||
|
$('blendTop').style.opacity = this.value;
|
||||||
|
$('blendValue').textContent = parseFloat(this.value).toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
function copyJson() {
|
function copyJson() {
|
||||||
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
|
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
|
||||||
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
|
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
|
||||||
|
|||||||
Reference in New Issue
Block a user