diff --git a/app.py b/app.py index 5e90b21..b5424ce 100644 --- a/app.py +++ b/app.py @@ -813,6 +813,143 @@ async def hair_grow( return err(1007, f"处理失败:{ex}") +# --------------------------------------------------------------------------- +# 调试接口:接口2 女性生发 分步计时 +# --------------------------------------------------------------------------- +@app.post( + "/api/v1/debug/grow-timing", + summary="调试-接口2女性生发分步计时", + tags=["调试"], + include_in_schema=False, +) +async def debug_grow_timing( + image_file: Optional[UploadFile] = File(default=None), + image_url: Optional[str] = Form(default=None), + image_base64: Optional[str] = Form(default=None), + hair_style: str = Form(default="2", description="发型序号(花瓣=2),逗号分隔多选"), +): + """单图跑接口2女性生发,返回每个步骤的耗时 + 结果图,用于定位性能瓶颈。 + + 步骤拆分: + 1. extract_context:人脸关键点检测 + 头发分割 + 发际线几何 + 2. [每个发型] generate_hairline_redraw: + 2a. compute_mask:发际线遮罩计算 + 2b. _call_swap:调 change_hair 换发型(内含 webui SD1.5 推理,远程或本机) + 2c. _composite:接缝融合(多频段/羽化) + 3. [每个发型] _call_local_redraw:调本机 ComfyUI 用 Flux.2 重绘 + """ + import time as _time + from fastapi.concurrency import run_in_threadpool + + raw, e = await resolve_image_bytes(image_file, image_url, image_base64) + if e is not None: + return e + image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) + if image is None: + return err(1008, "图片格式不支持(仅 JPG / PNG)") + + try: + max_styles = 5 + hair_styles = _parse_hair_styles(hair_style, max_styles) + if hair_styles is None: + return err(1007, f"hair_style 必须为 1..{max_styles}") + + t_total0 = _time.perf_counter() + timings = {"total_ms": 0, "extract_context_ms": 0, "per_hairstyle": []} + + # 步骤1: extract_context + t0 = _time.perf_counter() + from hairline.service import extract_context as _ec, _call_local_redraw, _REDRAW_MAX_SIDE # noqa + from face_analysis.hairline_grow import generate_hairline_redraw, NoFaceError # noqa + from face_analysis.head_mask import SEGFORMER_HAIR # noqa + ctx = await run_in_threadpool(_ec, image) + timings["extract_context_ms"] = int((_time.perf_counter() - t0) * 1000) + if ctx is None: + return err(1001, "无法识别人像") + + hair_mask_reuse = (ctx["parse_map"] == SEGFORMER_HAIR) + h, w = image.shape[:2] + eff_side = _REDRAW_MAX_SIDE + redraw_img, hair_mask_redraw = image, hair_mask_reuse + downscale_info = None + if eff_side > 0 and max(h, w) > eff_side: + from hairline.service import _downscale_max_side + redraw_img, _rs = _downscale_max_side(image, eff_side) + _nh, _nw = redraw_img.shape[:2] + if hair_mask_redraw is not None: + hair_mask_redraw = cv2.resize(hair_mask_reuse.astype(np.uint8), (_nw, _nh), + interpolation=cv2.INTER_NEAREST).astype(bool) + downscale_info = {"from": f"{w}x{h}", "to": f"{_nw}x{_nh}", "max_side": eff_side} + + textures_map = None + from hairline.service import get_texture_map, _FEMALE_KEY_TO_CHANG, load_texture_rgba, build_overlay_layer, load_ext_mesh + textures = get_texture_map()["female"] + items = [(s, textures[s - 1]) for s in hair_styles] + + for order, (key, white_path) in items: + hs_t0 = _time.perf_counter() + entry = {"hairline_type": key, "order": order} + chang_id = _FEMALE_KEY_TO_CHANG.get(key) + entry["chang_id"] = chang_id + entry["ok"] = False + entry["error"] = None + entry["grown_b64"] = None + if chang_id is None: + entry["error"] = f"无对应 chang_id" + timings["per_hairstyle"].append(entry) + continue + try: + # 2a/2b/2c: generate_hairline_redraw (内部含 mask+swap+blend) + t0 = _time.perf_counter() + data = await run_in_threadpool( + generate_hairline_redraw, redraw_img, chang_id, + hair_mask=hair_mask_redraw, **_V2_FINAL_DEFAULTS) + t_redraw_pipeline = _time.perf_counter() - t0 + _tm = data.get("timings_ms") or {} + entry["mask_ms"] = _tm.get("mask", 0) + entry["swap_ms"] = _tm.get("swap", 0) + entry["blend_ms"] = _tm.get("blend", 0) + entry["redraw_pipeline_ms"] = int(t_redraw_pipeline * 1000) + + steps = data.get("steps") or {} + final_b64 = steps.get("final_base64") or "" + mask_b64 = steps.get("redraw_band_mask_base64") or "" + if not final_b64 or not mask_b64: + entry["error"] = f"final/遮罩缺失(final={len(final_b64)} mask={len(mask_b64)})" + timings["per_hairstyle"].append(entry) + continue + if final_b64.startswith("data:"): + final_b64 = final_b64.split(",", 1)[1] + if mask_b64.startswith("data:"): + mask_b64 = mask_b64.split(",", 1)[1] + + # 3: ComfyUI 重绘 + t0 = _time.perf_counter() + grown_png = await run_in_threadpool( + _call_local_redraw, + base64.b64decode(final_b64), base64.b64decode(mask_b64)) + entry["comfyui_redraw_ms"] = int((_time.perf_counter() - t0) * 1000) + if grown_png: + entry["grown_b64"] = "data:image/jpeg;base64," + _png_to_jpg_b64(grown_png) + entry["ok"] = True + else: + entry["error"] = "ComfyUI 重绘返回空" + except NoFaceError: + entry["error"] = "未检出人脸" + except Exception as ex: # noqa: BLE001 + entry["error"] = str(ex)[:150] + entry["hairstyle_total_ms"] = int((_time.perf_counter() - hs_t0) * 1000) + timings["per_hairstyle"].append(entry) + + timings["total_ms"] = int((_time.perf_counter() - t_total0) * 1000) + timings["downscale"] = downscale_info + timings["image_size"] = f"{w}x{h}" + return ok(timings) + except Exception as ex: # noqa: BLE001 + logger.exception("debug/grow-timing 异常") + return err(1007, f"处理失败:{ex}") + + # --------------------------------------------------------------------------- # 接口 7:C 端生发 v2 —— 已弃用(add_hair2.json 用 Klein-9b 大模型,会把常驻的 # Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。 diff --git a/static/debug_grow_timing.html b/static/debug_grow_timing.html new file mode 100644 index 0000000..1342627 --- /dev/null +++ b/static/debug_grow_timing.html @@ -0,0 +1,203 @@ + + +
+ + +上传图片 → 跑一次女性生发 → 展示每步耗时,定位性能瓶颈
+ +