save
This commit is contained in:
@@ -714,10 +714,11 @@ async def face_measure_v2(
|
||||
- 发际线类型 `hairline_type`(英文 key)
|
||||
- 顺序 `order`(本期固定 `1..N`,不排序)
|
||||
|
||||
> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair)
|
||||
> **female 走「换发型」模式**:1..5 的生发图 `grown_image_base64` 由换发型(change_hair)
|
||||
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
||||
> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
||||
> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
||||
> **6/7(bigflower/clasicalflower)与 male 一样走原生发(ComfyUI add_hair)管线**。
|
||||
> **male 全部走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
||||
> female 1..5 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
||||
|
||||
{_image_fields_desc}
|
||||
|
||||
@@ -725,15 +726,15 @@ async def face_measure_v2(
|
||||
|
||||
---
|
||||
|
||||
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
|
||||
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 7 张 / male 6 张)。
|
||||
非法或缺失返回 `1004`。
|
||||
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
|
||||
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;
|
||||
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007`。
|
||||
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;
|
||||
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal。越界/非法返回 `1007`。
|
||||
- **beauty_enabled**:本期保留但不生效。
|
||||
|
||||
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`(female),
|
||||
`ellipse` / `m` / `straight` / `inverse_arc`(male)。
|
||||
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave` / `bigflower` / `clasicalflower`(female),
|
||||
`ellipse` / `m` / `straight` / `inverse_arc` / `heart` / `Softpetal`(male)。
|
||||
""",
|
||||
responses={
|
||||
200: {
|
||||
@@ -772,7 +773,7 @@ async def hair_grow(
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-7 male:1-6"),
|
||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||
@@ -782,7 +783,7 @@ async def hair_grow(
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
|
||||
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
||||
max_styles = {"female": 5, "male": 4}[gender]
|
||||
max_styles = {"female": 7, "male": 6}[gender]
|
||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||
if hair_styles is None:
|
||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||
@@ -804,7 +805,8 @@ async def hair_grow(
|
||||
if gender == "female":
|
||||
from hairline.service import generate_grow_results_swap
|
||||
items = await run_in_threadpool(
|
||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS)
|
||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS,
|
||||
prompt=prompt)
|
||||
else:
|
||||
from hairline.service import generate_grow_results
|
||||
items = await run_in_threadpool(
|
||||
@@ -827,6 +829,149 @@ 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),逗号分隔多选"),
|
||||
webui_steps: Optional[int] = Form(default=None, description="swapHair webui img2img 采样步数,None=服务端默认(15),可填10/15/20/25对比"),
|
||||
redraw_max_side: Optional[int] = Form(default=None, description="ComfyUI重绘分辨率(长边像素)。None=默认896;0=原图不缩;其他如640/768/1024"),
|
||||
redraw_prompt: Optional[str] = Form(default=None, description="ComfyUI重绘提示词,None=默认'填充遮罩区域的头发'"),
|
||||
):
|
||||
"""单图跑接口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 = 7
|
||||
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 if redraw_max_side is None else 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, webui_steps=webui_steps, **_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()
|
||||
# max_side: 0 或 None 都让 _call_local_redraw 用默认逻辑(外层已控制分辨率)
|
||||
_ms = redraw_max_side if redraw_max_side is not None and redraw_max_side > 0 else None
|
||||
grown_png = await run_in_threadpool(
|
||||
_call_local_redraw,
|
||||
base64.b64decode(final_b64), base64.b64decode(mask_b64),
|
||||
max_side=_ms, prompt=redraw_prompt)
|
||||
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 耗时抖动;且业务已不再调用)。
|
||||
@@ -1012,9 +1157,10 @@ async def face_features(
|
||||
---
|
||||
|
||||
**入参**(同接口2:先选性别,再多选发型):
|
||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。
|
||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 7 / male 6)。
|
||||
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave,6=bigflower,7=clasicalflower;
|
||||
`male`:1=ellipse,2=inverse_arc,3=m,4=straight,5=heart,6=Softpetal。
|
||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
||||
@@ -1094,7 +1240,7 @@ async def hairline_generate(
|
||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-7 male:1-6"),
|
||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||
@@ -1103,7 +1249,7 @@ async def hairline_generate(
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
|
||||
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
|
||||
max_styles = {"female": 5, "male": 4}[gender]
|
||||
max_styles = {"female": 7, "male": 6}[gender]
|
||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||
if hair_styles is None:
|
||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||
|
||||
Reference in New Issue
Block a user