feat(调试): swapHair webui步数暴露到测试页可调

参数透传链路: 测试页→debug接口→generate_hairline_redraw→_grow_core
  →_call_swap→change_hair swapHair→webui_img2img→build_body_v2

- app.py: 调试接口新增 webui_steps Form 参数
- hairline_grow.py: _call_swap/_grow_core/generate_hairline_grow/generate_hairline_redraw 透传 webui_steps
- debug_grow_timing.html: 新增 swap步数 下拉(默认/10/15/20/25/30)

实测 steps 10→25 swap 耗时 4.1s→4.4s,参数确实生效
This commit is contained in:
xsl
2026-07-26 16:49:21 +08:00
parent ae5a3b8f1d
commit e4d03a782d
3 changed files with 25 additions and 8 deletions
+2 -1
View File
@@ -827,6 +827,7 @@ async def debug_grow_timing(
image_url: Optional[str] = Form(default=None), image_url: Optional[str] = Form(default=None),
image_base64: Optional[str] = Form(default=None), image_base64: Optional[str] = Form(default=None),
hair_style: str = Form(default="2", description="发型序号(花瓣=2),逗号分隔多选"), hair_style: str = Form(default="2", description="发型序号(花瓣=2),逗号分隔多选"),
webui_steps: Optional[int] = Form(default=None, description="swapHair webui img2img 采样步数,None=服务端默认(15),可填10/15/20/25对比"),
): ):
"""单图跑接口2女性生发,返回每个步骤的耗时 + 结果图,用于定位性能瓶颈。 """单图跑接口2女性生发,返回每个步骤的耗时 + 结果图,用于定位性能瓶颈。
@@ -903,7 +904,7 @@ async def debug_grow_timing(
t0 = _time.perf_counter() t0 = _time.perf_counter()
data = await run_in_threadpool( data = await run_in_threadpool(
generate_hairline_redraw, redraw_img, chang_id, generate_hairline_redraw, redraw_img, chang_id,
hair_mask=hair_mask_redraw, **_V2_FINAL_DEFAULTS) hair_mask=hair_mask_redraw, webui_steps=webui_steps, **_V2_FINAL_DEFAULTS)
t_redraw_pipeline = _time.perf_counter() - t0 t_redraw_pipeline = _time.perf_counter() - t0
_tm = data.get("timings_ms") or {} _tm = data.get("timings_ms") or {}
entry["mask_ms"] = _tm.get("mask", 0) entry["mask_ms"] = _tm.get("mask", 0)
+10 -7
View File
@@ -508,13 +508,14 @@ def _segment_hair(image_bgr, seg_model, landmarks, w, h):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength, def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0): inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0, webui_steps=None):
"""调 change_hair /api/swapHair/v1,返回与输入同分辨率同对齐的换发型结果(BGR)。 """调 change_hair /api/swapHair/v1,返回与输入同分辨率同对齐的换发型结果(BGR)。
ext_mask_bool 非 None 时作为 ext_mask 传入(swap_mode=ext_mask)。 ext_mask_bool 非 None 时作为 ext_mask 传入(swap_mode=ext_mask)。
denoising_strengthwebui img2img 重绘强度(越大生发越激进),透传给换发型。 denoising_strengthwebui img2img 重绘强度(越大生发越激进),透传给换发型。
inpainting_fill / mask_blur / mask_dilate_scale:服务端重绘参数(透传给 change_hair inpainting_fill / mask_blur / mask_dilate_scale:服务端重绘参数(透传给 change_hair
默认值=服务端原始硬编码值,未传时行为不变)。详见 change_hair 文档。 默认值=服务端原始硬编码值,未传时行为不变)。详见 change_hair 文档。
webui_stepswebui img2img 采样步数,None 用服务端默认(15)。
""" """
import requests import requests
@@ -530,6 +531,8 @@ def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
"mask_blur": int(mask_blur), "mask_blur": int(mask_blur),
"mask_dilate_scale": float(mask_dilate_scale), "mask_dilate_scale": float(mask_dilate_scale),
} }
if webui_steps is not None:
payload["webui_steps"] = int(webui_steps)
if ext_mask_bool is not None: if ext_mask_bool is not None:
mbuf = cv2.imencode(".png", (ext_mask_bool.astype(np.uint8)) * 255)[1] mbuf = cv2.imencode(".png", (ext_mask_bool.astype(np.uint8)) * 255)[1]
payload["ext_mask"] = "data:image/png;base64," + base64.b64encode(mbuf.tobytes()).decode() payload["ext_mask"] = "data:image/png;base64," + base64.b64encode(mbuf.tobytes()).decode()
@@ -855,7 +858,7 @@ def _grow_core(image_bgr, hairline_id, *, is_hr, seg_model, erode_cm, swap_mode,
mb_levels, hairline_push_cm, hairline_edge, blend_method, color_match, mb_levels, hairline_push_cm, hairline_edge, blend_method, color_match,
color_match_strength, mb_feather_px, transition_band_px, color_match_strength, mb_feather_px, transition_band_px,
inpainting_fill, mask_blur, mask_dilate_scale, rid, render_viz=True, inpainting_fill, mask_blur, mask_dilate_scale, rid, render_viz=True,
hair_mask=None): hair_mask=None, webui_steps=None):
"""接口11 共享核心:遮罩(pushed)→生成→硬贴回→接缝融合,产出 ④ final。 """接口11 共享核心:遮罩(pushed)→生成→硬贴回→接缝融合,产出 ④ final。
不做任何重绘。返回中间产物 dict(供接口11 构造响应、接口12 取 final+重绘带用): 不做任何重绘。返回中间产物 dict(供接口11 构造响应、接口12 取 final+重绘带用):
@@ -897,7 +900,7 @@ def _grow_core(image_bgr, hairline_id, *, is_hr, seg_model, erode_cm, swap_mode,
ext_mask = mask_bool if swap_mode == "ext_mask" else None ext_mask = mask_bool if swap_mode == "ext_mask" else None
swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength, swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength,
inpainting_fill=inpainting_fill, mask_blur=mask_blur, inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale) mask_dilate_scale=mask_dilate_scale, webui_steps=webui_steps)
t_swap = time.time() - t0 t_swap = time.time() - t0
# 步骤3:严格按遮罩硬贴回(无融合,用于对比) # 步骤3:严格按遮罩硬贴回(无融合,用于对比)
@@ -935,7 +938,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
color_match_strength=1.0, mb_feather_px=1, color_match_strength=1.0, mb_feather_px=1,
transition_band_px=-1, transition_band_px=-1,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0, inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
rid=None): rid=None, webui_steps=None):
"""接口11 完整管线(**不含重绘**,重绘见接口12 generate_hairline_redraw)。 """接口11 完整管线(**不含重绘**,重绘见接口12 generate_hairline_redraw)。
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。 返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
@@ -957,7 +960,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
color_match=color_match, color_match_strength=color_match_strength, color_match=color_match, color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px, mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur, inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, rid=rid) mask_dilate_scale=mask_dilate_scale, rid=rid, webui_steps=webui_steps)
mask_viz = core["mask_viz"] mask_viz = core["mask_viz"]
alpha = core["alpha"] alpha = core["alpha"]
w, h = core["w"], core["h"] w, h = core["w"], core["h"]
@@ -1035,7 +1038,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0, inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
comfyui_prompt=None, beauty_alpha=0.6, comfyui_prompt=None, beauty_alpha=0.6,
band_lo_mult=0.5, band_hi_mult=1.5, rid=None, band_lo_mult=0.5, band_hi_mult=1.5, rid=None,
hair_mask=None): hair_mask=None, webui_steps=None):
"""接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带 """接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带
(外推↔内推之间、经 baseline 截断只留上部)作遮罩。 (外推↔内推之间、经 baseline 截断只留上部)作遮罩。
@@ -1064,7 +1067,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px, mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur, inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, rid=rid, render_viz=False, mask_dilate_scale=mask_dilate_scale, rid=rid, render_viz=False,
hair_mask=hair_mask) hair_mask=hair_mask, webui_steps=webui_steps)
final = core["final"] final = core["final"]
mask_viz = core["mask_viz"] mask_viz = core["mask_viz"]
w, h = core["w"], core["h"] w, h = core["w"], core["h"]
+13
View File
@@ -80,6 +80,17 @@
<option value="5">5 波浪</option> <option value="5">5 波浪</option>
</select> </select>
</div> </div>
<div class="form-group">
<label>swap步数</label>
<select id="webuiSteps">
<option value="">默认(15)</option>
<option value="10">10 (最快)</option>
<option value="15">15</option>
<option value="20">20 (原版)</option>
<option value="25">25 (高画质)</option>
<option value="30">30</option>
</select>
</div>
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 开始分析</button> <button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 开始分析</button>
</div> </div>
<div id="statusBar" class="status hidden"></div> <div id="statusBar" class="status hidden"></div>
@@ -115,6 +126,8 @@ async function submitTest(){
const fd = new FormData(); const fd = new FormData();
fd.append('image_file', f); fd.append('image_file', f);
fd.append('hair_style', $('hairStyle').value); fd.append('hair_style', $('hairStyle').value);
const _steps = $('webuiSteps').value;
if(_steps !== '') fd.append('webui_steps', _steps);
try{ try{
const r = await fetch(API_BASE+'/api/v1/debug/grow-timing',{ const r = await fetch(API_BASE+'/api/v1/debug/grow-timing',{