Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff4019c570 | ||
|
|
5226e23989 | ||
|
|
c462fd3634 | ||
|
|
ab6b0cb0bb | ||
|
|
94cdfd6de5 |
@@ -53,3 +53,15 @@ static/report_hairline_v2.zip
|
|||||||
# local_test 运行期日志 / pid(不入 git)
|
# local_test 运行期日志 / pid(不入 git)
|
||||||
local_test/hair_service.log
|
local_test/hair_service.log
|
||||||
local_test/hair_service.pid
|
local_test/hair_service.pid
|
||||||
|
|
||||||
|
# benchmark 原始输出(含结果图+原图,体积大,不入 git)
|
||||||
|
benchmark_out/
|
||||||
|
|
||||||
|
# benchmark 部署的 HTML 报告(图片 base64 内嵌,体积大,不入 git)
|
||||||
|
static/hairstyle_thumbs/
|
||||||
|
|
||||||
|
# 网关运行期日志(不入 git)
|
||||||
|
gateway.log
|
||||||
|
|
||||||
|
# 工作流备份文件(不入 git)
|
||||||
|
*.json.bak.*
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"16": {
|
"16": {
|
||||||
"class_type": "UNETLoader",
|
"class_type": "UnetLoaderGGUF",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
"unet_name": "flux-2-klein-9b-Q4_K_M.gguf",
|
||||||
"weight_dtype": "fp8_e4m3fn_fast"
|
"weight_dtype": "fp8_e4m3fn_fast"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"61": {
|
"61": {
|
||||||
"class_type": "CLIPLoader",
|
"class_type": "CLIPLoader",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip_name": "qwen_3_4b.safetensors",
|
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
||||||
"type": "flux2",
|
"type": "flux2",
|
||||||
"device": "cpu"
|
"device": "cpu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,10 +170,10 @@
|
|||||||
},
|
},
|
||||||
"16": {
|
"16": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
"unet_name": "flux-2-klein-9b-Q4_K_M.gguf",
|
||||||
"weight_dtype": "fp8_e4m3fn_fast"
|
"weight_dtype": "fp8_e4m3fn_fast"
|
||||||
},
|
},
|
||||||
"class_type": "UNETLoader",
|
"class_type": "UnetLoaderGGUF",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"title": "UNet加载器"
|
"title": "UNet加载器"
|
||||||
}
|
}
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
},
|
},
|
||||||
"61": {
|
"61": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip_name": "qwen_3_4b.safetensors",
|
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
||||||
"type": "flux2",
|
"type": "flux2",
|
||||||
"device": "cpu"
|
"device": "cpu"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ _AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static",
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# change_hair 代理路由(解决 CORS 问题)
|
# change_hair 代理路由(解决 CORS 问题)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_CHANGE_HAIR_BASE = "http://127.0.0.1:8801"
|
_CHANGE_HAIR_BASE = os.getenv("CHANGE_HAIR_BASE", "http://127.0.0.1:8801")
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/swapHair/v1", tags=["change_hair"])
|
@app.post("/api/swapHair/v1", tags=["change_hair"])
|
||||||
@@ -401,24 +401,9 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
||||||
|
|
||||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||||
discarded = result.hairline_discarded
|
|
||||||
data = result.to_response()
|
data = result.to_response()
|
||||||
vd = result.vertical
|
|
||||||
if variant == "v6":
|
if variant == "v6":
|
||||||
if discarded:
|
vd = result.vertical
|
||||||
# 发际线弃用:接口6 的上庭也依赖发际线,一并置 null;只保留中/下庭。
|
|
||||||
base_px = vd["middle_court_px"] + vd["lower_court_px"]
|
|
||||||
data["four_courts"]["upper_court_cm"] = None
|
|
||||||
data["four_courts"]["ratios"] = {
|
|
||||||
"upper_court": None,
|
|
||||||
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
|
||||||
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
|
||||||
}
|
|
||||||
data["four_courts"].pop("top_court_cm", None)
|
|
||||||
data["face_total_height_cm"] = round(
|
|
||||||
result.middle_cm + result.lower_cm, 2)
|
|
||||||
data["landmarks"]["hairline"] = None
|
|
||||||
else:
|
|
||||||
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
||||||
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
||||||
data["four_courts"]["ratios"] = {
|
data["four_courts"]["ratios"] = {
|
||||||
@@ -445,14 +430,11 @@ def _run_face_measure_data(image, variant="v1"):
|
|||||||
data["seven_eyes"][f"eye{i + 2}"] = (
|
data["seven_eyes"][f"eye{i + 2}"] = (
|
||||||
None if (a is None or b is None) else round((b - a) / pc, 2))
|
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||||
if variant != "v6":
|
if variant != "v6":
|
||||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线
|
||||||
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
|
|
||||||
from face_analysis.annotation import _ear_edges_from_mask
|
from face_analysis.annotation import _ear_edges_from_mask
|
||||||
top_y = (vd["brow_center"][1] if discarded
|
|
||||||
else vd["hair_top"][1])
|
|
||||||
head_l, head_r = _ear_edges_from_mask(
|
head_l, head_r = _ear_edges_from_mask(
|
||||||
ear_mask, hair_mask,
|
ear_mask, hair_mask,
|
||||||
top_y, vd["chin_tip"][1],
|
result.vertical["hair_top"][1], result.vertical["chin_tip"][1],
|
||||||
lcx, rcx, (lcx + rcx) / 2)
|
lcx, rcx, (lcx + rcx) / 2)
|
||||||
data["seven_eyes"]["eye1"] = (
|
data["seven_eyes"]["eye1"] = (
|
||||||
None if (head_l is None) else round((lcx - head_l) / pc, 2))
|
None if (head_l is None) else round((lcx - head_l) / pc, 2))
|
||||||
@@ -714,11 +696,10 @@ async def face_measure_v2(
|
|||||||
- 发际线类型 `hairline_type`(英文 key)
|
- 发际线类型 `hairline_type`(英文 key)
|
||||||
- 顺序 `order`(本期固定 `1..N`,不排序)
|
- 顺序 `order`(本期固定 `1..N`,不排序)
|
||||||
|
|
||||||
> **female 走「换发型」模式**:1..5 的生发图 `grown_image_base64` 由换发型(change_hair)
|
> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair)
|
||||||
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
|
||||||
> **6/7(bigflower/clasicalflower)与 male 一样走原生发(ComfyUI add_hair)管线**。
|
> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
||||||
> **male 全部走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
|
> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
||||||
> female 1..5 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
|
|
||||||
|
|
||||||
{_image_fields_desc}
|
{_image_fields_desc}
|
||||||
|
|
||||||
@@ -726,15 +707,15 @@ async def face_measure_v2(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 7 张 / male 6 张)。
|
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
|
||||||
非法或缺失返回 `1004`。
|
非法或缺失返回 `1004`。
|
||||||
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
|
- **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。
|
||||||
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;
|
`female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;
|
||||||
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal。越界/非法返回 `1007`。
|
`male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007`。
|
||||||
- **beauty_enabled**:本期保留但不生效。
|
- **beauty_enabled**:本期保留但不生效。
|
||||||
|
|
||||||
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave` / `bigflower` / `clasicalflower`(female),
|
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`(female),
|
||||||
`ellipse` / `m` / `straight` / `inverse_arc` / `heart` / `Softpetal`(male)。
|
`ellipse` / `m` / `straight` / `inverse_arc`(male)。
|
||||||
""",
|
""",
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
@@ -773,17 +754,19 @@ async def hair_grow(
|
|||||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-7 male:1-6"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
||||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||||
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||||
|
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(切换模型用)。None=工作流默认;如 flux-2-klein-9b-Q5_K_M.gguf / flux-2-klein-9b-Q4_K_M.gguf / flux2.0/flux-2-klein-9b-fp8.safetensors"),
|
||||||
|
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
|
||||||
):
|
):
|
||||||
# 1. gender 必填校验(非法/缺失 → 1004)
|
# 1. gender 必填校验(非法/缺失 → 1004)
|
||||||
if gender not in ("male", "female"):
|
if gender not in ("male", "female"):
|
||||||
return err(1004, "gender 必填且只能为 male / female")
|
return err(1004, "gender 必填且只能为 male / female")
|
||||||
|
|
||||||
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
|
||||||
max_styles = {"female": 7, "male": 6}[gender]
|
max_styles = {"female": 5, "male": 4}[gender]
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||||
if hair_styles is None:
|
if hair_styles is None:
|
||||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||||
@@ -806,11 +789,12 @@ async def hair_grow(
|
|||||||
from hairline.service import generate_grow_results_swap
|
from hairline.service import generate_grow_results_swap
|
||||||
items = await run_in_threadpool(
|
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)
|
redraw_max_side=redraw_max_side, unet_name=flux_model)
|
||||||
else:
|
else:
|
||||||
from hairline.service import generate_grow_results
|
from hairline.service import generate_grow_results
|
||||||
items = await run_in_threadpool(
|
items = await run_in_threadpool(
|
||||||
generate_grow_results, image, gender, use_mask, prompt, hair_styles)
|
generate_grow_results, image, gender, use_mask, prompt, hair_styles,
|
||||||
|
unet_name=flux_model)
|
||||||
if items is None:
|
if items is None:
|
||||||
return err(1001, "无法识别人像")
|
return err(1001, "无法识别人像")
|
||||||
|
|
||||||
@@ -829,149 +813,6 @@ async def hair_grow(
|
|||||||
return err(1007, f"处理失败:{ex}")
|
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 大模型,会把常驻的
|
# 接口 7:C 端生发 v2 —— 已弃用(add_hair2.json 用 Klein-9b 大模型,会把常驻的
|
||||||
# Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。
|
# Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。
|
||||||
@@ -1157,10 +998,9 @@ async def face_features(
|
|||||||
---
|
---
|
||||||
|
|
||||||
**入参**(同接口2:先选性别,再多选发型):
|
**入参**(同接口2:先选性别,再多选发型):
|
||||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 7 / male 6)。
|
- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。
|
||||||
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
||||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave,6=bigflower,7=clasicalflower;
|
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||||
`male`:1=ellipse,2=inverse_arc,3=m,4=straight,5=heart,6=Softpetal。
|
|
||||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
||||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
||||||
@@ -1240,7 +1080,7 @@ async def hairline_generate(
|
|||||||
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
image_url: Optional[str] = Form(default=None, description="图片 URL"),
|
||||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-7 male:1-6"),
|
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||||
@@ -1249,7 +1089,7 @@ async def hairline_generate(
|
|||||||
return err(1004, "gender 必填且只能为 male / female")
|
return err(1004, "gender 必填且只能为 male / female")
|
||||||
|
|
||||||
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
|
# hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007
|
||||||
max_styles = {"female": 7, "male": 6}[gender]
|
max_styles = {"female": 5, "male": 4}[gender]
|
||||||
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
hair_styles = _parse_hair_styles(hair_style, max_styles)
|
||||||
if hair_styles is None:
|
if hair_styles is None:
|
||||||
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""发型对比矩阵测试:3图×5发型=15行,每行10张图(4b@896×1 + 9b三模型×三分辨率×9)。
|
||||||
|
按模型分组跑(减少模型切换次数、降低OOM风险),结果重组为15行存JSON+生成报告。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/hairstyle")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
HAIRSTYLES = [
|
||||||
|
(1, "ellipse", "椭圆"), (2, "flower", "花瓣"), (3, "heart", "心形"),
|
||||||
|
(4, "straight", "直线"), (5, "wave", "波浪"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 按模型分组:每个模型对应其要跑的(分辨率,列标题)
|
||||||
|
MODEL_GROUPS = [
|
||||||
|
("flux-2-klein-4b-fp8.safetensors", [("896", "4B@896")]),
|
||||||
|
("flux2.0/flux-2-klein-9b-fp8.safetensors",
|
||||||
|
[("0", "9B-fp8@原图"), ("896", "9B-fp8@896"), ("640", "9B-fp8@640")]),
|
||||||
|
("flux-2-klein-9b-Q5_K_M.gguf",
|
||||||
|
[("0", "9B-Q5@原图"), ("896", "9B-Q5@896"), ("640", "9B-Q5@640")]),
|
||||||
|
("flux-2-klein-9b-Q4_K_M.gguf",
|
||||||
|
[("0", "9B-Q4@原图"), ("896", "9B-Q4@896"), ("640", "9B-Q4@640")]),
|
||||||
|
]
|
||||||
|
# 列顺序(4b在前,然后9b三模型)
|
||||||
|
COLUMN_TITLES = ["4B@896", "9B-fp8@原图", "9B-fp8@896", "9B-fp8@640",
|
||||||
|
"9B-Q5@原图", "9B-Q5@896", "9B-Q5@640",
|
||||||
|
"9B-Q4@原图", "9B-Q4@896", "9B-Q4@640"]
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_used():
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"], timeout=10)
|
||||||
|
return int(out.decode().strip())
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def call(img_path, hair_num, model_file, res_val):
|
||||||
|
fd = {"gender": "female", "hair_style": str(hair_num), "use_mask": "true",
|
||||||
|
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"}
|
||||||
|
if model_file:
|
||||||
|
fd["flux_model"] = model_file
|
||||||
|
if res_val != "":
|
||||||
|
fd["redraw_max_side"] = res_val
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
peak = gpu_used()
|
||||||
|
err = None
|
||||||
|
grown_b64 = None
|
||||||
|
try:
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=fd, timeout=300)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
peak = max(peak, gpu_used())
|
||||||
|
j = r.json()
|
||||||
|
if j.get("code") != 0:
|
||||||
|
err = f"code={j.get('code')} {j.get('message', '')[:60]}"
|
||||||
|
else:
|
||||||
|
res = j.get("data", {}).get("results", [])
|
||||||
|
if res and res[0].get("grown_image_base64"):
|
||||||
|
grown_b64 = res[0]["grown_image_base64"]
|
||||||
|
elif res:
|
||||||
|
err = "grown_image空"
|
||||||
|
else:
|
||||||
|
err = "无results"
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
err = str(e)[:150]
|
||||||
|
return {"elapsed": elapsed, "gpu_peak": peak, "grown_b64": grown_b64, "error": err}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 结果字典: results[(img, hair_num, column_title)] = {grown_path, elapsed, gpu_peak, error}
|
||||||
|
results = {}
|
||||||
|
total = len(IMGS) * len(HAIRSTYLES) * len(COLUMN_TITLES)
|
||||||
|
idx = 0
|
||||||
|
for mfile, res_list in MODEL_GROUPS:
|
||||||
|
mname = os.path.basename(mfile)
|
||||||
|
print(f"\n===== 切换到模型: {mname} =====", flush=True)
|
||||||
|
# 等模型切换稳定
|
||||||
|
time.sleep(2)
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
for rval, ctitle in res_list:
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total}] {ilabel}|{hname}|{ctitle}", flush=True)
|
||||||
|
r = call(ipath, hnum, mfile, rval)
|
||||||
|
status = f"{r['elapsed']:.1f}s" if not r["error"] else r["error"][:40]
|
||||||
|
print(f" -> {status} peak={r['gpu_peak']}M", flush=True)
|
||||||
|
if r["grown_b64"]:
|
||||||
|
fname = f"{ilabel}_{hkey}_{ctitle.replace('@','_').replace('-','')}.jpg"
|
||||||
|
with open(OUT / fname, "wb") as gf:
|
||||||
|
gf.write(base64.b64decode(r["grown_b64"]))
|
||||||
|
r["grown_path"] = str(OUT / fname)
|
||||||
|
results[(ilabel, hnum, ctitle)] = r
|
||||||
|
|
||||||
|
# 重组为15行
|
||||||
|
rows = []
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
for hnum, hkey, hname in HAIRSTYLES:
|
||||||
|
cells = []
|
||||||
|
for ct in COLUMN_TITLES:
|
||||||
|
r = results.get((ilabel, hnum, ct), {"error": "未跑"})
|
||||||
|
cells.append({"title": ct, **{k: v for k, v in r.items() if k != "grown_b64"}})
|
||||||
|
rows.append({"img": ilabel, "img_path": ipath,
|
||||||
|
"hair_num": hnum, "hair_key": hkey, "hair_name": hname,
|
||||||
|
"cells": cells})
|
||||||
|
with open(OUT / "results.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"columns": COLUMN_TITLES, "rows": rows}, f, ensure_ascii=False, indent=2)
|
||||||
|
ok = sum(1 for row in rows for c in row["cells"] if not c.get("error"))
|
||||||
|
print(f"\n✓ 完成: {ok}/{total} 成功 -> {OUT/'results.json'}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""把发型对比测试结果生成 HTML 报告。
|
||||||
|
15行(3图×5发型) × 10列(4b@896 + 9b三模型×三分辨率),每行首列=原图。
|
||||||
|
图片 base64 内嵌,自包含单文件。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/hairstyle")
|
||||||
|
RESULTS = OUT / "results.json"
|
||||||
|
HTML = OUT / "report.html"
|
||||||
|
|
||||||
|
|
||||||
|
def img_src(path):
|
||||||
|
"""把绝对路径转成报告里的相对 URL(报告在 static/,图片在 static/bench/)。"""
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
p = str(path)
|
||||||
|
if "benchmark_out/hairstyle/" in p:
|
||||||
|
return "bench/hairstyle/" + os.path.basename(p)
|
||||||
|
if "benchmark_out/matrix/" in p:
|
||||||
|
return "bench/matrix/" + os.path.basename(p)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
d = json.load(open(RESULTS, encoding="utf-8"))
|
||||||
|
columns = d["columns"]
|
||||||
|
rows = d["rows"]
|
||||||
|
|
||||||
|
# 统计每列的平均耗时、峰值显存
|
||||||
|
col_stats = {}
|
||||||
|
for ct in columns:
|
||||||
|
times, peaks = [], []
|
||||||
|
for r in rows:
|
||||||
|
for c in r["cells"]:
|
||||||
|
if c.get("title") == ct and not c.get("error"):
|
||||||
|
times.append(c["elapsed"])
|
||||||
|
peaks.append(c["gpu_peak"])
|
||||||
|
col_stats[ct] = {
|
||||||
|
"avg_t": sum(times) / len(times) if times else 0,
|
||||||
|
"max_p": max(peaks) / 1024 if peaks else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 表头:原图 + 10列
|
||||||
|
headers = ['<th class="col-label">原图</th>']
|
||||||
|
for ct in columns:
|
||||||
|
s = col_stats[ct]
|
||||||
|
headers.append(
|
||||||
|
f'<th class="col-label"><div class="col-title">{ct}</div>'
|
||||||
|
f'<div class="col-stat">{s["avg_t"]:.0f}s · {s["max_p"]:.0f}G</div></th>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 表体:15行
|
||||||
|
body_rows = []
|
||||||
|
for r in rows:
|
||||||
|
# 发型+图标签
|
||||||
|
label = f'<div class="row-label">{r["img"]}<br><b>{r["hair_name"]}</b></div>'
|
||||||
|
# 原图
|
||||||
|
ORIG_SRC = {"asdf": "bench/orig/asdf.jpg", "qwer": "bench/orig/qwer.jpg", "girl5": "bench/orig/girl5.jpg"}
|
||||||
|
orig = ORIG_SRC.get(r["img"])
|
||||||
|
cells = [f'<td class="cell-orig"><div class="row-label-cell">{label}</div>'
|
||||||
|
f'<img class="orig-img" src="{orig}"></td>']
|
||||||
|
# 10个结果列
|
||||||
|
for ct in columns:
|
||||||
|
c = next((x for x in r["cells"] if x.get("title") == ct), {})
|
||||||
|
src = img_src(c.get("grown_path")) if not c.get("error") else None
|
||||||
|
if src:
|
||||||
|
cells.append(
|
||||||
|
f'<td class="cell-result"><img class="result-img" src="{src}" loading="lazy">'
|
||||||
|
f'<div class="cell-time">{c["elapsed"]:.1f}s</div></td>')
|
||||||
|
else:
|
||||||
|
cells.append(f'<td class="cell-result"><div class="na">⚠</div></td>')
|
||||||
|
body_rows.append(f'<tr>{"".join(cells)}</tr>')
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>发型对比测试报告 — 4模型×3分辨率</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, "Segoe UI", sans-serif; background: #f5f5f5; padding: 16px; }}
|
||||||
|
h1 {{ font-size: 20px; margin-bottom: 4px; }}
|
||||||
|
.subtitle {{ color: #888; font-size: 12px; margin-bottom: 12px; }}
|
||||||
|
.legend {{ background: #fff; border-radius: 8px; padding: 10px 16px; margin-bottom: 12px; font-size: 12px; color: #555; }}
|
||||||
|
.scroll-wrap {{ overflow-x: auto; }}
|
||||||
|
table {{ border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.06); }}
|
||||||
|
th, td {{ border: 1px solid #eee; padding: 6px; vertical-align: top; text-align: center; }}
|
||||||
|
th {{ background: #f9fafb; position: sticky; top: 0; }}
|
||||||
|
.col-label {{ min-width: 110px; max-width: 130px; }}
|
||||||
|
.col-title {{ font-size: 12px; font-weight: 700; color: #374151; }}
|
||||||
|
.col-stat {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.row-label {{ font-size: 11px; color: #6b7280; }}
|
||||||
|
.row-label b {{ color: #1f2937; }}
|
||||||
|
.row-label-cell {{ font-size: 11px; color: #6b7280; margin-bottom: 4px; }}
|
||||||
|
.row-label-cell b {{ color: #1f2937; font-size: 13px; }}
|
||||||
|
img {{ border-radius: 4px; max-width: 120px; max-height: 150px; object-fit: contain; background: #f3f4f6; }}
|
||||||
|
.orig-img {{ border: 2px solid #d1d5db; max-height: 130px; }}
|
||||||
|
.cell-time {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||||
|
.na {{ color: #d1d5db; font-size: 16px; padding: 40px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>💇 发型对比测试报告</h1>
|
||||||
|
<p class="subtitle">接口2女性 · 3图×5发型=15行 · 每行: 4B@896(1) + 9B(fp8/Q5/Q4)×(原图/896/640)(9) · 150/150成功 · RTX3090</p>
|
||||||
|
<div class="legend">列标题下显示<b>平均耗时 · 峰值显存</b>。横向滚动查看更多列。原图列含图片名+发型名。</div>
|
||||||
|
<div class="scroll-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>{"".join(headers)}</tr>
|
||||||
|
{"".join(body_rows)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with open(HTML, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
print(f"✓ 报告: {HTML} ({HTML.stat().st_size//1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""接口2女性 花瓣形 单发型 4模型×3分辨率×3图×3次 矩阵测试。
|
||||||
|
|
||||||
|
调用本机 hair-worker (:8187) 的 /api/v1/hair/grow,gender=female, hair_style=2(花瓣形)。
|
||||||
|
每次记录:生发图、耗时、显存峰值。结果图存到 benchmark_out/matrix/,最后生成 HTML 报告。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||||
|
TOKEN = "dev-shared-secret-2026"
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/matrix")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 4 模型 × 3 分辨率 × 3 图 × 3 次
|
||||||
|
MODELS = [
|
||||||
|
("4b-fp8", "flux-2-klein-4b-fp8.safetensors"),
|
||||||
|
("9b-fp8", "flux2.0/flux-2-klein-9b-fp8.safetensors"),
|
||||||
|
("9b-Q5", "flux-2-klein-9b-Q5_K_M.gguf"),
|
||||||
|
("9b-Q4", "flux-2-klein-9b-Q4_K_M.gguf"),
|
||||||
|
]
|
||||||
|
RES = [("orig", "0"), ("640", "640"), ("896", "896")]
|
||||||
|
IMGS = [
|
||||||
|
("asdf", "/home/ubuntu/hair/image/asdf.jpg"),
|
||||||
|
("qwer", "/home/ubuntu/hair/image/qwer.jpg"),
|
||||||
|
("girl5", "/home/ubuntu/hair/image/girl_img/girl5.jpg"),
|
||||||
|
]
|
||||||
|
REPEAT = 3
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_used():
|
||||||
|
"""返回当前显存已用 MiB。"""
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
return int(out.decode().strip())
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def call(img_path, model_file, res_val):
|
||||||
|
"""调一次接口2。返回 dict: ok/elapsed/grown_path/gpu_peak/error。"""
|
||||||
|
fd = {
|
||||||
|
"gender": "female",
|
||||||
|
"hair_style": "2", # 花瓣形
|
||||||
|
"use_mask": "true",
|
||||||
|
"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
||||||
|
}
|
||||||
|
if model_file:
|
||||||
|
fd["flux_model"] = model_file
|
||||||
|
if res_val != "":
|
||||||
|
fd["redraw_max_side"] = res_val
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
peak = gpu_used()
|
||||||
|
err = None
|
||||||
|
grown_path = None
|
||||||
|
try:
|
||||||
|
with open(img_path, "rb") as f:
|
||||||
|
r = requests.post(
|
||||||
|
API, headers={"X-Internal-Token": TOKEN},
|
||||||
|
files={"image_file": (os.path.basename(img_path), f, "image/jpeg")},
|
||||||
|
data=fd, timeout=300,
|
||||||
|
)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
# 采样峰值(推理刚结束)
|
||||||
|
peak = max(peak, gpu_used())
|
||||||
|
j = r.json()
|
||||||
|
if j.get("code") != 0:
|
||||||
|
err = f"code={j.get('code')} {j.get('message','')}"
|
||||||
|
else:
|
||||||
|
res = j.get("data", {}).get("results", [])
|
||||||
|
if res and res[0].get("grown_image_base64"):
|
||||||
|
grown_path = OUT / f"tmp_grown.jpg"
|
||||||
|
with open(grown_path, "wb") as gf:
|
||||||
|
gf.write(base64.b64decode(res[0]["grown_image_base64"]))
|
||||||
|
elif res:
|
||||||
|
err = "grown_image_base64 为空"
|
||||||
|
else:
|
||||||
|
err = "无 results"
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
err = str(e)[:200]
|
||||||
|
return {"elapsed": elapsed, "gpu_peak": peak, "grown_path": str(grown_path) if grown_path else None, "error": err}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
results = [] # 每元素一个组合
|
||||||
|
total = len(MODELS) * len(RES) * len(IMGS) * REPEAT
|
||||||
|
idx = 0
|
||||||
|
for mlabel, mfile in MODELS:
|
||||||
|
for rlabel, rval in RES:
|
||||||
|
for ilabel, ipath in IMGS:
|
||||||
|
# 一个组合:3 次
|
||||||
|
runs = []
|
||||||
|
for rep in range(REPEAT):
|
||||||
|
idx += 1
|
||||||
|
print(f"[{idx}/{total}] {mlabel} | res={rlabel} | {ilabel} | rep{rep+1}", flush=True)
|
||||||
|
r = call(ipath, mfile, rval)
|
||||||
|
print(f" -> {r['elapsed']:.1f}s peak={r['gpu_peak']}MiB err={r['error']}", flush=True)
|
||||||
|
# 存每次的生发图
|
||||||
|
if r["grown_path"]:
|
||||||
|
save_to = OUT / f"{mlabel}_{rlabel}_{ilabel}_r{rep+1}.jpg"
|
||||||
|
os.replace(r["grown_path"], save_to)
|
||||||
|
r["grown_path"] = str(save_to)
|
||||||
|
runs.append(r)
|
||||||
|
results.append({
|
||||||
|
"model": mlabel, "model_file": mfile,
|
||||||
|
"res": rlabel, "res_val": rval,
|
||||||
|
"img": ilabel, "img_path": ipath,
|
||||||
|
"runs": runs,
|
||||||
|
})
|
||||||
|
# 存原始数据
|
||||||
|
with open(OUT / "results.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"\n✓ 全部完成,原始数据 -> {OUT/'results.json'}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""把 benchmark_out/matrix/results.json 生成 HTML 报告。
|
||||||
|
每个组合一行:原图 + 3次生发图 + 耗时/显存。
|
||||||
|
图片用 base64 内嵌(自包含单文件,便于部署)。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OUT = Path("/home/ubuntu/hair/benchmark_out/matrix")
|
||||||
|
RESULTS = OUT / "results.json"
|
||||||
|
HTML = OUT / "report.html"
|
||||||
|
|
||||||
|
RES_LABEL = {"orig": "原图", "640": "640", "896": "896(默认)"}
|
||||||
|
MODEL_LABEL = {
|
||||||
|
"4b-fp8": "4B fp8 (3.8G)",
|
||||||
|
"9b-fp8": "9B fp8 (8.8G)",
|
||||||
|
"9b-Q5": "9B Q5_K_M (6.6G)",
|
||||||
|
"9b-Q4": "9B Q4_K_M (5.6G)",
|
||||||
|
}
|
||||||
|
MODEL_ORDER = ["4b-fp8", "9b-Q4", "9b-Q5", "9b-fp8"]
|
||||||
|
|
||||||
|
|
||||||
|
def img_src(path):
|
||||||
|
"""把绝对路径转成报告里的相对 URL(报告在 static/,图片在 static/bench/)。"""
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
p = str(path)
|
||||||
|
# benchmark_out/matrix/xxx.jpg -> bench/matrix/xxx.jpg
|
||||||
|
if "benchmark_out/matrix/" in p:
|
||||||
|
return "bench/matrix/" + os.path.basename(p)
|
||||||
|
if "benchmark_out/hairstyle/" in p:
|
||||||
|
return "bench/hairstyle/" + os.path.basename(p)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def thumb(src, alt="", cls=""):
|
||||||
|
if not src:
|
||||||
|
return f'<div class="na {cls}">⚠ 失败</div>'
|
||||||
|
return f'<img class="{cls}" src="{src}" alt="{alt}" loading="lazy">'
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = json.load(open(RESULTS, encoding="utf-8"))
|
||||||
|
# 原图相对路径映射(图片在 static/bench/orig/)
|
||||||
|
ORIG_SRC = {"asdf": "bench/orig/asdf.jpg", "qwer": "bench/orig/qwer.jpg", "girl5": "bench/orig/girl5.jpg"}
|
||||||
|
|
||||||
|
# 统计:每个模型的平均耗时、平均峰值显存
|
||||||
|
stats = {}
|
||||||
|
for c in data:
|
||||||
|
m = c["model"]
|
||||||
|
stats.setdefault(m, {"times": [], "peaks": []})
|
||||||
|
for r in c["runs"]:
|
||||||
|
if not r["error"]:
|
||||||
|
stats[m]["times"].append(r["elapsed"])
|
||||||
|
stats[m]["peaks"].append(r["gpu_peak"])
|
||||||
|
|
||||||
|
rows_html = []
|
||||||
|
# 按模型顺序、分辨率顺序、图片顺序排列
|
||||||
|
for m in MODEL_ORDER:
|
||||||
|
mdata = [c for c in data if c["model"] == m]
|
||||||
|
for rlabel in ["orig", "640", "896"]:
|
||||||
|
for ilabel in ["asdf", "qwer", "girl5"]:
|
||||||
|
c = next((x for x in mdata if x["res"] == rlabel and x["img"] == ilabel), None)
|
||||||
|
if not c:
|
||||||
|
continue
|
||||||
|
# 3 次结果图
|
||||||
|
run_cells = []
|
||||||
|
for i, r in enumerate(c["runs"]):
|
||||||
|
src = img_src(r["grown_path"]) if not r["error"] else None
|
||||||
|
if src:
|
||||||
|
run_cells.append(
|
||||||
|
f'<div class="run-cell"><div class="run-label">第{i+1}次 · {r["elapsed"]:.1f}s</div>'
|
||||||
|
f'{thumb(src, f"r{i+1}", "result-img")}</div>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_cells.append(
|
||||||
|
f'<div class="run-cell"><div class="run-label">第{i+1}次 · 失败</div>'
|
||||||
|
f'<div class="na">⚠ {r["error"][:30] if r["error"] else ""}</div></div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
orig = ORIG_SRC.get(c["img"])
|
||||||
|
rows_html.append(f'''
|
||||||
|
<div class="combo-row">
|
||||||
|
<div class="cell-model">{MODEL_LABEL.get(m, m)}<div class="cell-sub">res={RES_LABEL.get(rlabel, rlabel)}</div></div>
|
||||||
|
<div class="cell-img">{thumb(orig, "原图", "orig-img")}<div class="run-label">{ilabel}</div></div>
|
||||||
|
<div class="cell-runs">{"".join(run_cells)}</div>
|
||||||
|
</div>''')
|
||||||
|
|
||||||
|
# 模型对比汇总
|
||||||
|
summary_rows = []
|
||||||
|
for m in MODEL_ORDER:
|
||||||
|
s = stats.get(m, {"times": [], "peaks": []})
|
||||||
|
if s["times"]:
|
||||||
|
avg_t = sum(s["times"]) / len(s["times"])
|
||||||
|
max_p = max(s["peaks"]) / 1024
|
||||||
|
summary_rows.append(
|
||||||
|
f"<tr><td>{MODEL_LABEL.get(m,m)}</td><td>{avg_t:.1f}s</td>"
|
||||||
|
f"<td>{max_p:.1f} GB</td><td>{len(s['times'])} 成功</td></tr>"
|
||||||
|
)
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Flux 模型矩阵测试报告 — 接口2女性花瓣形</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; padding: 20px; }}
|
||||||
|
h1 {{ font-size: 22px; margin-bottom: 4px; }}
|
||||||
|
.subtitle {{ color: #888; font-size: 13px; margin-bottom: 16px; }}
|
||||||
|
.summary {{ background: #fff; border-radius: 10px; padding: 16px 20px; margin-bottom: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }}
|
||||||
|
.summary h2 {{ font-size: 16px; margin-bottom: 10px; }}
|
||||||
|
.summary table {{ border-collapse: collapse; width: 100%; font-size: 14px; }}
|
||||||
|
.summary th, .summary td {{ border: 1px solid #e5e7eb; padding: 8px 12px; text-align: left; }}
|
||||||
|
.summary th {{ background: #f9fafb; font-weight: 600; }}
|
||||||
|
.combo-row {{ display: flex; align-items: flex-start; gap: 12px; background: #fff; border-radius: 10px;
|
||||||
|
padding: 12px 16px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.05); }}
|
||||||
|
.cell-model {{ min-width: 130px; font-weight: 700; font-size: 14px; padding-top: 6px; }}
|
||||||
|
.cell-sub {{ font-weight: 400; font-size: 12px; color: #6b7280; margin-top: 2px; }}
|
||||||
|
.cell-img {{ min-width: 160px; text-align: center; }}
|
||||||
|
.cell-runs {{ display: flex; gap: 10px; flex: 1; }}
|
||||||
|
.run-cell {{ text-align: center; }}
|
||||||
|
.run-label {{ font-size: 11px; color: #6b7280; margin-bottom: 4px; }}
|
||||||
|
img {{ border-radius: 6px; max-height: 200px; max-width: 100%; object-fit: contain; background: #f9fafb; }}
|
||||||
|
.orig-img {{ max-height: 180px; border: 2px solid #e5e7eb; }}
|
||||||
|
.result-img {{ max-height: 200px; }}
|
||||||
|
.na {{ color: #d1d5db; font-size: 12px; padding: 40px 20px; background: #f9fafb; border-radius: 6px; width: 150px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>💇 Flux 模型矩阵测试报告</h1>
|
||||||
|
<p class="subtitle">接口2女性 · 花瓣形发型 · 4模型 × 3分辨率 × 3图 × 3次 = 108 次 · RTX 3090 24GB</p>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>📊 模型对比汇总</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>模型</th><th>平均耗时</th><th>峰值显存</th><th>成功次数</th></tr>
|
||||||
|
{"".join(summary_rows)}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="font-size:16px;margin:24px 0 12px">🖼️ 各组合对比(每行:原图 + 3次生发结果)</h2>
|
||||||
|
{"".join(rows_html)}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
with open(HTML, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
print(f"✓ 报告已生成: {HTML} ({HTML.stat().st_size//1024} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的
|
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的
|
||||||
`add_hair.json`(Flux-2) 出图。**关键坑**:obj 是重排序,需 `INDEX_MAP_468` 把 MP 序→OBJ 序。
|
`add_hair.json`(Flux-2) 出图。**关键坑**:obj 是重排序,需 `INDEX_MAP_468` 把 MP 序→OBJ 序。
|
||||||
返回 `results[].image_base64` + `grown_image_base64`。
|
返回 `results[].image_base64` + `grown_image_base64`。
|
||||||
- `hair_style` 映射: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。female 1..5 走「换发型(change_hair)」+Flux-2 重绘管线;female 6/7 与 male 全部走原生发(ComfyUI add_hair)管线。
|
- `hair_style` 映射:female 1=ellipse 2=flower 3=heart 4=straight 5=wave;male 1=ellipse 2=inverse_arc 3=m 4=straight。
|
||||||
|
|
||||||
### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流
|
### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流
|
||||||
- **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。
|
- **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。
|
||||||
|
|||||||
@@ -265,7 +265,7 @@
|
|||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
||||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数。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` |
|
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `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` |
|
||||||
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
||||||
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| image_url | string | 发际线曲线**透明 PNG** URL(仅白色发际线曲线,透明底,**不含人物**,需前端叠加原图显示) |
|
| image_url | string | 发际线曲线**透明 PNG** URL(仅白色发际线曲线,透明底,**不含人物**,需前端叠加原图显示) |
|
||||||
| grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) |
|
| grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) |
|
||||||
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) |
|
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) |
|
||||||
| order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) |
|
| order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) |
|
||||||
|
|
||||||
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。
|
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。
|
||||||
@@ -405,8 +405,8 @@
|
|||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 7 / male 6)。缺失/非法返回 `1004` |
|
| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 5 / male 4)。缺失/非法返回 `1004` |
|
||||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。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` |
|
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `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` |
|
||||||
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||||
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 |
|
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 |
|
||||||
@@ -427,7 +427,7 @@
|
|||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) |
|
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) |
|
||||||
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
||||||
| image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) |
|
| image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) |
|
||||||
| image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) |
|
| image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) |
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
|
|
||||||
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
||||||
s = min(w, h)
|
s = min(w, h)
|
||||||
font_size = max(9, round(s * 0.020)) # 字号上调一档
|
font_size = max(8, round(s * 0.017)) # 字体更小
|
||||||
line_w = max(1, round(s * 0.0022))
|
line_w = max(1, round(s * 0.0022))
|
||||||
dash_len = max(4, round(s * 0.008))
|
dash_len = max(4, round(s * 0.008))
|
||||||
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
||||||
@@ -212,11 +212,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
|
|
||||||
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
||||||
|
|
||||||
# 发际线弃用(hairline_discarded):保留头顶横线,去掉发际线横线,
|
if variant == "v6":
|
||||||
# 也不标顶/上庭(缺发际线作边界,算不出)。横线 = 头顶/眉心/鼻翼下缘/下巴尖。
|
|
||||||
if getattr(measure_result, "hairline_discarded", False):
|
|
||||||
order = ["hair_top", "brow_center", "nose_bottom", "chin_tip"]
|
|
||||||
elif variant == "v6":
|
|
||||||
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
else:
|
else:
|
||||||
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||||
@@ -243,7 +239,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
face_cx = (fx0 + fx1) / 2
|
face_cx = (fx0 + fx1) / 2
|
||||||
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
||||||
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
||||||
# 竖线纵向范围:v6 = 发际线→下巴尖(不超出);v1(含发际线弃用)= 头顶→下巴尖并两端超出一点
|
# v6 竖线纵向范围 = 发际线→下巴尖(不超出);v1 = 头顶→下巴尖并两端超出一点
|
||||||
v_top = fy0 if variant == "v6" else fy0 - over
|
v_top = fy0 if variant == "v6" else fy0 - over
|
||||||
v_bot = fy1 if variant == "v6" else fy1 + over
|
v_bot = fy1 if variant == "v6" else fy1 + over
|
||||||
|
|
||||||
@@ -259,18 +255,6 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
font = _load_font(font_size)
|
font = _load_font(font_size)
|
||||||
|
|
||||||
# --- 2b. 每条横线在「中间线段」(两内眼角之间)中点画原点突出 ---
|
|
||||||
# 注意:原点不在整条线的中点 face_cx,而在被竖线切出的中间段(左内眼角↔右内眼角)
|
|
||||||
# 的正中,即脸的竖直中轴附近、两内眼角连线中点。
|
|
||||||
li_x = pts["left_inner"][0]
|
|
||||||
ri_x = pts["right_inner"][0]
|
|
||||||
dot_cx = (li_x + ri_x) / 2
|
|
||||||
dot_r = max(2, round(s * 0.0045)) # 原点半径,与 arrow_size 同档自适应
|
|
||||||
for cy in ys:
|
|
||||||
x0, y0 = dot_cx - dot_r, cy - dot_r
|
|
||||||
x1, y1 = dot_cx + dot_r, cy + dot_r
|
|
||||||
draw.ellipse((x0, y0, x1, y1), fill=LINE_COLOR)
|
|
||||||
|
|
||||||
# --- 3a. 横线右侧:线名(头顶/发际线/眉心/鼻翼下缘/下巴尖),文字纵向居中对齐到线 ---
|
# --- 3a. 横线右侧:线名(头顶/发际线/眉心/鼻翼下缘/下巴尖),文字纵向居中对齐到线 ---
|
||||||
name_x = fx1 + over + pad # 移到横线右端外侧一点(往右)
|
name_x = fx1 + over + pad # 移到横线右端外侧一点(往右)
|
||||||
for i, name in enumerate(order):
|
for i, name in enumerate(order):
|
||||||
@@ -281,28 +265,19 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
|||||||
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
||||||
|
|
||||||
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
||||||
# court_start:庭段在 order 里的起始索引。发际线弃用时 order 首位是头顶(无下界发际线,
|
if variant == "v6":
|
||||||
# 顶/上庭不标),中庭从眉心开始 → 跳过 order[0]。
|
|
||||||
if getattr(measure_result, "hairline_discarded", False):
|
|
||||||
court_cm = [measure_result.middle_cm, measure_result.lower_cm]
|
|
||||||
court_name = ["中庭", "下庭"]
|
|
||||||
n_court = 2
|
|
||||||
court_start = 1
|
|
||||||
elif variant == "v6":
|
|
||||||
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
||||||
court_name = ["上庭", "中庭", "下庭"]
|
court_name = ["上庭", "中庭", "下庭"]
|
||||||
n_court = 3
|
n_court = 3
|
||||||
court_start = 0
|
|
||||||
else:
|
else:
|
||||||
court_cm = [measure_result.top_cm, measure_result.upper_cm,
|
court_cm = [measure_result.top_cm, measure_result.upper_cm,
|
||||||
measure_result.middle_cm, measure_result.lower_cm]
|
measure_result.middle_cm, measure_result.lower_cm]
|
||||||
court_name = ["顶庭", "上庭", "中庭", "下庭"]
|
court_name = ["顶庭", "上庭", "中庭", "下庭"]
|
||||||
n_court = 4
|
n_court = 4
|
||||||
court_start = 0
|
|
||||||
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
|
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
|
||||||
court_total = sum(court_cm) or 1.0 # 各庭占比分母 = 四庭(v6 三庭)之和
|
court_total = sum(court_cm) or 1.0 # 各庭占比分母 = 四庭(v6 三庭)之和
|
||||||
for i in range(n_court):
|
for i in range(n_court):
|
||||||
y_a, y_b = ys[court_start + i], ys[court_start + i + 1]
|
y_a, y_b = ys[i], ys[i + 1]
|
||||||
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
|
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
|
||||||
inset = min(arrow_size, (y_b - y_a) * 0.12)
|
inset = min(arrow_size, (y_b - y_a) * 0.12)
|
||||||
draw_dashed_line_with_arrows(
|
draw_dashed_line_with_arrows(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from face_analysis.calibration import (
|
|||||||
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
||||||
)
|
)
|
||||||
from face_analysis.face_mesh_landmarks import (
|
from face_analysis.face_mesh_landmarks import (
|
||||||
GLABELLA_9, NOSE_BOTTOM, CHIN_TIP,
|
GLABELLA_9, GLABELLA_151, NOSE_BOTTOM, CHIN_TIP,
|
||||||
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
||||||
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
||||||
)
|
)
|
||||||
@@ -24,8 +24,10 @@ _TOP_RATIO = 0.22 / 0.28 # 顶庭 ÷ 中庭(≈ 0.786)
|
|||||||
|
|
||||||
|
|
||||||
def _brow_center(lm, w, h):
|
def _brow_center(lm, w, h):
|
||||||
"""眉心 = 索引 9(眉间上点)。"""
|
"""眉心 = 索引 9 / 151 中点。"""
|
||||||
return normalized_to_pixel(lm[GLABELLA_9], w, h)
|
g9 = normalized_to_pixel(lm[GLABELLA_9], w, h)
|
||||||
|
g151 = normalized_to_pixel(lm[GLABELLA_151], w, h)
|
||||||
|
return (g9[0] + g151[0]) / 2, (g9[1] + g151[1]) / 2
|
||||||
|
|
||||||
|
|
||||||
def estimate_vertical_landmarks(landmarks, image_width, image_height):
|
def estimate_vertical_landmarks(landmarks, image_width, image_height):
|
||||||
@@ -142,22 +144,9 @@ def measure_seven_eyes(landmarks, image_width, image_height):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def pt_or_none(vertical, name):
|
|
||||||
"""vertical dict 的点 → {"x","y"},值为 None 时返回 None。"""
|
|
||||||
v = vertical.get(name)
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
return {"x": int(round(v[0])), "y": int(round(v[1]))}
|
|
||||||
|
|
||||||
|
|
||||||
class MeasureResult:
|
class MeasureResult:
|
||||||
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
||||||
|
|
||||||
# 发际线弃用阈值:发际线离头顶(顶庭)< 此值时判定分割不可靠,弃用发际线。
|
|
||||||
# hairline 与 hair_top 几乎重合(如稀疏头发中轴漏检只剩一小撮),说明发际线
|
|
||||||
# 定位无意义 → 顶/上庭置 null、标注图不画头顶/发际线。
|
|
||||||
HAIRLINE_DISCARD_TOP_CM = 0.7
|
|
||||||
|
|
||||||
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose,
|
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose,
|
||||||
landmarks=None, image_width=None, image_height=None):
|
landmarks=None, image_width=None, image_height=None):
|
||||||
self.vertical = vertical
|
self.vertical = vertical
|
||||||
@@ -175,13 +164,6 @@ class MeasureResult:
|
|||||||
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
||||||
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
||||||
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
||||||
# 发际线弃用判定:顶庭(头顶→发际线)过小视为发际线贴近头顶、不可靠。
|
|
||||||
# 弃用时 hairline_source 改为 "discarded",face_total 只算中庭+下庭。
|
|
||||||
self.hairline_discarded = self.top_cm < self.HAIRLINE_DISCARD_TOP_CM
|
|
||||||
if self.hairline_discarded:
|
|
||||||
self.hairline_source = "discarded"
|
|
||||||
self.face_total_cm = self.middle_cm + self.lower_cm
|
|
||||||
else:
|
|
||||||
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
||||||
|
|
||||||
# 七眼厘米
|
# 七眼厘米
|
||||||
@@ -190,45 +172,14 @@ class MeasureResult:
|
|||||||
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
||||||
|
|
||||||
def to_response(self):
|
def to_response(self):
|
||||||
# 发际线弃用:顶/上庭相关字段置 null(保留键),ratio 分母只算中下庭;
|
|
||||||
# landmarks.hair_top/hairline 置 null。否则按四庭正常输出。
|
|
||||||
if self.hairline_discarded:
|
|
||||||
base_px = (self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
|
||||||
data = {
|
|
||||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
|
||||||
"four_courts": {
|
|
||||||
"top_court_cm": None,
|
|
||||||
"upper_court_cm": None,
|
|
||||||
"middle_court_cm": round(self.middle_cm, 2),
|
|
||||||
"lower_court_cm": round(self.lower_cm, 2),
|
|
||||||
"ratios": {
|
|
||||||
"top_court": None,
|
|
||||||
"upper_court": None,
|
|
||||||
"middle_court": round(self.vertical["middle_court_px"] / base_px, 3),
|
|
||||||
"lower_court": round(self.vertical["lower_court_px"] / base_px, 3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"seven_eyes": {
|
|
||||||
"eye_width_cm": round(self.eye_width_cm, 2),
|
|
||||||
"face_width_cm": round(self.face_width_cm, 2),
|
|
||||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
|
||||||
"ratios": {
|
|
||||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
|
||||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"landmarks": {
|
|
||||||
"hair_top": None,
|
|
||||||
"hairline": None,
|
|
||||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
|
||||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
|
||||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
|
||||||
},
|
|
||||||
"hairline_source": self.hairline_source,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
||||||
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||||
|
fw_px = self.eyes["face_width_px"]
|
||||||
|
|
||||||
|
def pt(name):
|
||||||
|
x, y = self.vertical[name]
|
||||||
|
return {"x": int(round(x)), "y": int(round(y))}
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||||
"four_courts": {
|
"four_courts": {
|
||||||
@@ -248,16 +199,16 @@ class MeasureResult:
|
|||||||
"face_width_cm": round(self.face_width_cm, 2),
|
"face_width_cm": round(self.face_width_cm, 2),
|
||||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||||
"ratios": {
|
"ratios": {
|
||||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
"eye_width": round(self.eyes["eye_width_px"] / fw_px, 3),
|
||||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / fw_px, 3),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"landmarks": {
|
"landmarks": {
|
||||||
"hair_top": pt_or_none(self.vertical, "hair_top"),
|
"hair_top": pt("hair_top"),
|
||||||
"hairline": pt_or_none(self.vertical, "hairline"),
|
"hairline": pt("hairline"),
|
||||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
"brow_center": pt("brow_center"),
|
||||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
"nose_bottom": pt("nose_bottom"),
|
||||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
"chin_tip": pt("chin_tip"),
|
||||||
},
|
},
|
||||||
"hairline_source": self.hairline_source,
|
"hairline_source": self.hairline_source,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
INFO: Started server process [26440]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
2026-07-01 23:35:15 [INFO] gateway.config: 配置加载完成 | workers=['http://127.0.0.1:8187'] | public_base_url=http://127.0.0.1:8080 | hc_interval=8s | dispatch_timeout=600s | queue_wait=30s
|
|
||||||
2026-07-01 23:35:15 [INFO] gateway: 网关启动中... workers=['http://127.0.0.1:8187']
|
|
||||||
2026-07-01 23:35:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:35:16 [INFO] gateway.pool: Worker 池初始化完成 | 总数=1 | 在线=1
|
|
||||||
2026-07-01 23:35:16 [INFO] gateway: 标注图目录: /home/ubuntu/hair/static/annotations
|
|
||||||
2026-07-01 23:35:16 [INFO] gateway.pool: 健康检查循环启动 | 间隔=8s | 下线阈值=2 | 上线阈值=1 | workers=1
|
|
||||||
2026-07-01 23:35:16 [INFO] gateway: 清理任务启动 | 间隔=60min | 保留=24h | 目录=/home/ubuntu/hair/static/annotations
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
|
|
||||||
2026-07-01 23:35:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:35872 - "GET /gateway-health HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:35:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:35:27 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/face/measure "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:35:27 [INFO] gateway.forward: base64→URL: annotated_image_base64 → http://127.0.0.1:8080/static/annotations/422b7786adc4486989d7f8770df40693.png (21864 bytes)
|
|
||||||
INFO: 127.0.0.1:57220 - "POST /api/v1/face/measure HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:35:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:57232 - "GET /static/annotations/422b7786adc4486989d7f8770df40693.png HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:57240 - "GET /gateway-health HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:35:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:35:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:35:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:36:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:37:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:38:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:39:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:40:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:41:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:42:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:43:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:44:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:45:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:46:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:47:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:48:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:49:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:50:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:51:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:44470 - "GET /gateway-health HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:52:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:52:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:53:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:6017 - "GET / HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:6017 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
|
||||||
2026-07-01 23:53:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:54:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:39090 - "GET /docs HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:39092 - "GET / HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:55:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:56172 - "GET /static/test_interface1.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56174 - "GET /static/test_interface1.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56184 - "GET /static/test_interface2.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56196 - "GET /static/test_interface2.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56202 - "GET /static/test_interface3.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56206 - "GET /static/test_interface3.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56212 - "GET /static/test_interface4.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56214 - "GET /static/test_interface4.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56218 - "GET /static/test_interface5.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56230 - "GET /static/test_interface5.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56242 - "GET /static/test_interface6.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56256 - "GET /static/test_interface6.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56272 - "GET /static/test_interface7.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56278 - "GET /static/test_interface7.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56282 - "GET /static/integration.html HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:56284 - "GET /static/integration.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:55:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:55:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:6434 - "GET /static/test_interface1.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:56:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:08 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/face/measure "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:08 [INFO] gateway.forward: base64→URL: annotated_image_base64 → http://127.0.0.1:8080/static/annotations/8a70c12c19ef4868af555ef84eaeafae.png (35965 bytes)
|
|
||||||
INFO: 111.192.98.24:6435 - "POST /api/v1/face/measure HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:56:12 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:20 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:28 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:36 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:44 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:56:52 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:00 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:08 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:16 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:24 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:32 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:40 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:48 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:57:56 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:04 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: Shutting down
|
|
||||||
INFO: Waiting for application shutdown.
|
|
||||||
2026-07-01 23:58:08 [INFO] gateway: 网关关闭中...
|
|
||||||
2026-07-01 23:58:08 [INFO] gateway: 清理任务已停止
|
|
||||||
2026-07-01 23:58:08 [INFO] gateway.pool: 健康检查循环已停止
|
|
||||||
2026-07-01 23:58:08 [INFO] gateway.pool: Worker 池已关闭
|
|
||||||
2026-07-01 23:58:08 [INFO] gateway: 网关已关闭
|
|
||||||
INFO: Application shutdown complete.
|
|
||||||
INFO: Finished server process [26440]
|
|
||||||
INFO: Started server process [31898]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway.config: 配置加载完成 | workers=['http://127.0.0.1:8187'] | public_base_url=http://117.50.213.111:8080 | hc_interval=8s | dispatch_timeout=600s | queue_wait=30s
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway: 网关启动中... workers=['http://127.0.0.1:8187']
|
|
||||||
2026-07-01 23:58:10 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway.pool: Worker 池初始化完成 | 总数=1 | 在线=1
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway: 标注图目录: /home/ubuntu/hair/static/annotations
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway.pool: 健康检查循环启动 | 间隔=8s | 下线阈值=2 | 上线阈值=1 | workers=1
|
|
||||||
2026-07-01 23:58:10 [INFO] gateway: 清理任务启动 | 间隔=60min | 保留=24h | 目录=/home/ubuntu/hair/static/annotations
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
|
|
||||||
2026-07-01 23:58:10 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 127.0.0.1:33732 - "GET /gateway-health HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:58:18 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:26 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:28 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/face/measure "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:28 [INFO] gateway.forward: base64→URL: annotated_image_base64 → http://117.50.213.111:8080/static/annotations/478efab4566a46c3af0065ad0ffafb67.png (21864 bytes)
|
|
||||||
INFO: 127.0.0.1:32982 - "POST /api/v1/face/measure HTTP/1.1" 200 OK
|
|
||||||
INFO: 117.50.213.111:57344 - "GET /static/annotations/478efab4566a46c3af0065ad0ffafb67.png HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:58:34 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:42 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:50 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:54 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/face/measure "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:58:54 [INFO] gateway.forward: base64→URL: annotated_image_base64 → http://117.50.213.111:8080/static/annotations/88d5eec74def44fdad5dd6f7b22e7be4.png (35965 bytes)
|
|
||||||
INFO: 111.192.98.24:7030 - "POST /api/v1/face/measure HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:7030 - "GET /static/annotations/88d5eec74def44fdad5dd6f7b22e7be4.png HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:58:58 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:7031 - "GET /static/test_interface2.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:59:06 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:14 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:22 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:27 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/hair/grow "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:27 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/8f770fef8bc14827b419698c90ebd6fa.jpg (105970 bytes)
|
|
||||||
2026-07-01 23:59:27 [INFO] gateway.forward: base64→URL: grown_image_base64 → http://117.50.213.111:8080/static/annotations/6f592882d9c84c1b916ac327f4e0b2af.jpg (113320 bytes)
|
|
||||||
INFO: 111.192.98.24:7062 - "POST /api/v1/hair/grow HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:7062 - "GET /static/annotations/8f770fef8bc14827b419698c90ebd6fa.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:7166 - "GET /static/annotations/6f592882d9c84c1b916ac327f4e0b2af.jpg HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:59:30 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:38 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:46 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:48 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/hair/grow "HTTP/1.1 200 OK"
|
|
||||||
2026-07-01 23:59:48 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/ed790d4888f742caa6625b3cf4704a77.jpg (105641 bytes)
|
|
||||||
2026-07-01 23:59:48 [INFO] gateway.forward: base64→URL: grown_image_base64 → http://117.50.213.111:8080/static/annotations/e4aed9d67e864d31909b30a58e08c16e.jpg (111495 bytes)
|
|
||||||
INFO: 111.192.98.24:5164 - "POST /api/v1/hair/grow HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5164 - "GET /static/annotations/ed790d4888f742caa6625b3cf4704a77.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5188 - "GET /static/annotations/e4aed9d67e864d31909b30a58e08c16e.jpg HTTP/1.1" 200 OK
|
|
||||||
2026-07-01 23:59:54 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:5213 - "GET /static/test_interface3.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:00:02 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:10 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:16 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/hair/grow-b "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:16 [INFO] gateway.forward: base64→URL: hair_growth_image_base64 → http://117.50.213.111:8080/static/annotations/0891a4b9375e4f58814502e893b12bf6.jpg (152092 bytes)
|
|
||||||
INFO: 111.192.98.24:5212 - "POST /api/v1/hair/grow-b HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5212 - "GET /static/annotations/0891a4b9375e4f58814502e893b12bf6.jpg HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:00:18 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:5332 - "GET /static/test_interface4.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:00:26 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:34 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:39 [ERROR] gateway: 接口4 豆包调用失败
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "/home/ubuntu/hair/gateway/app.py", line 298, in face_features
|
|
||||||
feats = await run_in_threadpool(analyze_features, img_bytes, image_url)
|
|
||||||
File "/home/ubuntu/hair/venv/lib/python3.10/site-packages/starlette/concurrency.py", line 37, in run_in_threadpool
|
|
||||||
return await anyio.to_thread.run_sync(func)
|
|
||||||
File "/home/ubuntu/hair/venv/lib/python3.10/site-packages/anyio/to_thread.py", line 63, in run_sync
|
|
||||||
return await get_async_backend().run_sync_in_worker_thread(
|
|
||||||
File "/home/ubuntu/hair/venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 2596, in run_sync_in_worker_thread
|
|
||||||
return await future
|
|
||||||
File "/home/ubuntu/hair/venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 1029, in run
|
|
||||||
result = context.run(func, *args)
|
|
||||||
File "/home/ubuntu/hair/face_features.py", line 98, in analyze_features
|
|
||||||
resp = get_client().chat.completions.create(
|
|
||||||
File "/home/ubuntu/hair/face_features.py", line 65, in get_client
|
|
||||||
from volcenginesdkarkruntime import Ark
|
|
||||||
ModuleNotFoundError: No module named 'volcenginesdkarkruntime'
|
|
||||||
INFO: 111.192.98.24:5331 - "POST /api/v1/face/features HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:00:42 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: 111.192.98.24:5436 - "GET /static/test_interface5.html HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:00:50 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:00:58 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:00 [INFO] httpx: HTTP Request: POST http://127.0.0.1:8187/api/v1/hairline/generate "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:00 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/4516a9491a1c41f4af048020f6709870.jpg (105674 bytes)
|
|
||||||
2026-07-02 00:01:00 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/ad51e082bb5540ea843259cdc1e76e6b.jpg (105970 bytes)
|
|
||||||
2026-07-02 00:01:00 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/12a6f7745c9c461ca963f4d49c5267ef.jpg (105735 bytes)
|
|
||||||
2026-07-02 00:01:00 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/316d8352062848e4b28337ef233d820e.jpg (105641 bytes)
|
|
||||||
2026-07-02 00:01:00 [INFO] gateway.forward: base64→URL: image_base64 → http://117.50.213.111:8080/static/annotations/2ae45e3579b0419ab8bff5e3129ab26e.jpg (105705 bytes)
|
|
||||||
INFO: 111.192.98.24:5435 - "POST /api/v1/hairline/generate HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5435 - "GET /static/annotations/4516a9491a1c41f4af048020f6709870.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5466 - "GET /static/annotations/ad51e082bb5540ea843259cdc1e76e6b.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5467 - "GET /static/annotations/12a6f7745c9c461ca963f4d49c5267ef.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5469 - "GET /static/annotations/316d8352062848e4b28337ef233d820e.jpg HTTP/1.1" 200 OK
|
|
||||||
INFO: 111.192.98.24:5471 - "GET /static/annotations/2ae45e3579b0419ab8bff5e3129ab26e.jpg HTTP/1.1" 200 OK
|
|
||||||
2026-07-02 00:01:06 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:14 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:22 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:30 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:38 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:46 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:01:54 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:02 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:10 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:18 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:26 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:34 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:42 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:50 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
2026-07-02 00:02:58 [INFO] httpx: HTTP Request: GET http://127.0.0.1:8187/health "HTTP/1.1 200 OK"
|
|
||||||
INFO: Shutting down
|
|
||||||
INFO: Waiting for application shutdown.
|
|
||||||
2026-07-02 00:02:59 [INFO] gateway: 网关关闭中...
|
|
||||||
2026-07-02 00:02:59 [INFO] gateway: 清理任务已停止
|
|
||||||
2026-07-02 00:02:59 [INFO] gateway.pool: 健康检查循环已停止
|
|
||||||
2026-07-02 00:02:59 [INFO] gateway.pool: Worker 池已关闭
|
|
||||||
2026-07-02 00:02:59 [INFO] gateway: 网关已关闭
|
|
||||||
INFO: Application shutdown complete.
|
|
||||||
INFO: Finished server process [31898]
|
|
||||||
@@ -170,10 +170,10 @@
|
|||||||
},
|
},
|
||||||
"16": {
|
"16": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
|
"unet_name": "flux-2-klein-9b-Q4_K_M.gguf",
|
||||||
"weight_dtype": "fp8_e4m3fn"
|
"weight_dtype": "fp8_e4m3fn"
|
||||||
},
|
},
|
||||||
"class_type": "UNETLoader",
|
"class_type": "UnetLoaderGGUF",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"title": "UNet加载器"
|
"title": "UNet加载器"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ _REPO = os.path.dirname(os.path.dirname(__file__))
|
|||||||
_INPUT_NODE = "26" # LoadImage:外部输入图(含 alpha 遮罩)
|
_INPUT_NODE = "26" # LoadImage:外部输入图(含 alpha 遮罩)
|
||||||
_SEED_NODE = "6" # RandomNoise
|
_SEED_NODE = "6" # RandomNoise
|
||||||
_PROMPT_NODE = "60" # JjkText:提示词
|
_PROMPT_NODE = "60" # JjkText:提示词
|
||||||
|
_UNET_NODE = "16" # UNETLoader / UnetLoaderGGUF:Flux 模型加载
|
||||||
|
_CLIP_NODE = "61" # CLIPLoader:qwen 文本编码器
|
||||||
|
|
||||||
|
# Flux 模型 → 配套文本编码器映射。切换 unet 时自动同步编码器,避免维度不匹配。
|
||||||
|
# 规则:4b 系列配 qwen_3_4b,9b 系列(fp8/GGUF)配 qwen_3_8b_fp8mixed。
|
||||||
|
def _clip_for_unet(unet_name: str) -> str | None:
|
||||||
|
"""根据 unet 文件名推断配套的文本编码器文件名;无法推断返回 None。"""
|
||||||
|
low = unet_name.lower()
|
||||||
|
if "4b" in low and "9b" not in low:
|
||||||
|
return "qwen_3_4b.safetensors"
|
||||||
|
if "9b" in low:
|
||||||
|
return "qwen_3_8b_fp8mixed.safetensors"
|
||||||
|
return None
|
||||||
|
|
||||||
_wf_cache: dict[str, dict] = {} # path → workflow JSON
|
_wf_cache: dict[str, dict] = {} # path → workflow JSON
|
||||||
_wf_output_node: dict[str, str] = {} # path → SaveImage 节点 ID
|
_wf_output_node: dict[str, str] = {} # path → SaveImage 节点 ID
|
||||||
@@ -86,13 +99,18 @@ def _get_output_node(workflow_path: str | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = None,
|
def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = None,
|
||||||
workflow_path: str | None = None, front: bool = False) -> bytes:
|
workflow_path: str | None = None, front: bool = False,
|
||||||
|
unet_name: str | None = None) -> bytes:
|
||||||
"""提交一次生发任务,返回输出 PNG 字节。失败抛异常。
|
"""提交一次生发任务,返回输出 PNG 字节。失败抛异常。
|
||||||
|
|
||||||
prompt:非 None 时替换工作流节点60(JjkText)的文本;None 时用工作流内置默认提示词。
|
prompt:非 None 时替换工作流节点60(JjkText)的文本;None 时用工作流内置默认提示词。
|
||||||
workflow_path:工作流 JSON 路径,None 则用默认 add_hair.json。
|
workflow_path:工作流 JSON 路径,None 则用默认 add_hair.json。
|
||||||
front:True 时任务插到 ComfyUI 队列最前(server 端 "front" 字段,队列号取负)。
|
front:True 时任务插到 ComfyUI 队列最前(server 端 "front" 字段,队列号取负)。
|
||||||
接口2 对时延敏感用 True,避免排在接口3/5 的批量任务后面;其余接口保持 False。
|
接口2 对时延敏感用 True,避免排在接口3/5 的批量任务后面;其余接口保持 False。
|
||||||
|
unet_name:非 None 时改写工作流里的模型加载节点(节点16),动态切换 Flux 模型。
|
||||||
|
.safetensors → 保持 UNETLoader 节点类型不变,只替换 unet_name;
|
||||||
|
.gguf → 自动把节点类型改成 UnetLoaderGGUF(需装 ComfyUI-GGUF 插件)。
|
||||||
|
None 时用工作流内置默认模型。
|
||||||
"""
|
"""
|
||||||
path = workflow_path or _WORKFLOW_DEFAULT
|
path = workflow_path or _WORKFLOW_DEFAULT
|
||||||
output_node = _get_output_node(path)
|
output_node = _get_output_node(path)
|
||||||
@@ -120,6 +138,21 @@ def run(rgba_png_bytes: bytes, timeout: float = COMFY_TIMEOUT, prompt: str = Non
|
|||||||
wf[_SEED_NODE]["inputs"]["noise_seed"] = random.randint(0, 2**63 - 1)
|
wf[_SEED_NODE]["inputs"]["noise_seed"] = random.randint(0, 2**63 - 1)
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
wf[_PROMPT_NODE]["inputs"]["text"] = prompt
|
wf[_PROMPT_NODE]["inputs"]["text"] = prompt
|
||||||
|
if unet_name is not None:
|
||||||
|
node = wf.get(_UNET_NODE)
|
||||||
|
if node is not None:
|
||||||
|
# .gguf 需切换到 ComfyUI-GGUF 插件的 UnetLoaderGGUF 节点;
|
||||||
|
# .safetensors/.ckpt 保持原 UNETLoader 节点类型不变
|
||||||
|
if unet_name.lower().endswith(".gguf"):
|
||||||
|
node["class_type"] = "UnetLoaderGGUF"
|
||||||
|
else:
|
||||||
|
node["class_type"] = "UNETLoader"
|
||||||
|
node["inputs"]["unet_name"] = unet_name
|
||||||
|
# 同步切换配套文本编码器(4b→qwen_3_4b, 9b→qwen_3_8b),避免维度不匹配
|
||||||
|
clip_node = wf.get(_CLIP_NODE)
|
||||||
|
clip_name = _clip_for_unet(unet_name)
|
||||||
|
if clip_node is not None and clip_name is not None:
|
||||||
|
clip_node["inputs"]["clip_name"] = clip_name
|
||||||
|
|
||||||
# 诊断:落盘实际提交的工作流 + 输入图,便于和手动 ComfyUI 跑的对比
|
# 诊断:落盘实际提交的工作流 + 输入图,便于和手动 ComfyUI 跑的对比
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def _process_mask_to_rgba(image_bytes: bytes, mask_bytes: bytes) -> bytes:
|
|||||||
|
|
||||||
def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
||||||
prompt: str | None = None, timeout: float = 300.0,
|
prompt: str | None = None, timeout: float = 300.0,
|
||||||
front: bool = False) -> bytes:
|
front: bool = False, unet_name: str | None = None) -> bytes:
|
||||||
"""直接调 ComfyUI 重绘 — 替代 local_test /api/generate。
|
"""直接调 ComfyUI 重绘 — 替代 local_test /api/generate。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -65,6 +65,7 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
|||||||
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||||
timeout: ComfyUI 超时秒数
|
timeout: ComfyUI 超时秒数
|
||||||
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
||||||
|
unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
重绘后的 PNG 图片字节
|
重绘后的 PNG 图片字节
|
||||||
@@ -75,4 +76,5 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
|||||||
"""
|
"""
|
||||||
rgba_png = _process_mask_to_rgba(image_bytes, mask_bytes)
|
rgba_png = _process_mask_to_rgba(image_bytes, mask_bytes)
|
||||||
return comfyui.run(rgba_png, timeout=timeout, prompt=prompt,
|
return comfyui.run(rgba_png, timeout=timeout, prompt=prompt,
|
||||||
workflow_path=_REPAINT_WORKFLOW, front=front)
|
workflow_path=_REPAINT_WORKFLOW, front=front,
|
||||||
|
unet_name=unet_name)
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ import logging
|
|||||||
logger = logging.getLogger("hair.worker")
|
logger = logging.getLogger("hair.worker")
|
||||||
|
|
||||||
# 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。
|
# 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。
|
||||||
# 与接口12 final 的 5 型一一对应。female 6/7(bigflower/clasicalflower)无对应 LoRA,
|
# 与接口12 final 的 5 型一一对应。
|
||||||
# 走与男性一致的原生生发(ComfyUI add_hair)管线,故不在本表。
|
|
||||||
_FEMALE_KEY_TO_CHANG = {
|
_FEMALE_KEY_TO_CHANG = {
|
||||||
"ellipse": "chang_tuoyuan", # 椭圆
|
"ellipse": "chang_tuoyuan", # 椭圆
|
||||||
"flower": "chang_huaban", # 花瓣
|
"flower": "chang_huaban", # 花瓣
|
||||||
@@ -39,16 +38,6 @@ _FEMALE_KEY_TO_CHANG = {
|
|||||||
"wave": "chang_bolang", # 波浪
|
"wave": "chang_bolang", # 波浪
|
||||||
}
|
}
|
||||||
|
|
||||||
# 发际线贴图显式顺序表:决定 hair_style 序号(1-indexed)。
|
|
||||||
# 不再依赖文件名字母序——字母序会因新增/重命名文件而错位,破坏现有前端/客户端取值。
|
|
||||||
# key 须与 _gender_key 派生结果一致(已去空格):如 "inverse_arc"(源 man_ inverse_arc.png)、
|
|
||||||
# "Softpetal"(源 man_Soft petal.png,大写 S 保留)。表外未知 key 兜底排到末尾。
|
|
||||||
_HAIRSTYLE_ORDER = {
|
|
||||||
"female": ["ellipse", "flower", "heart", "straight", "wave",
|
|
||||||
"bigflower", "clasicalflower"], # 1..7
|
|
||||||
"male": ["ellipse", "inverse_arc", "m", "straight", "heart", "Softpetal"], # 1..6
|
|
||||||
}
|
|
||||||
|
|
||||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||||
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture")
|
||||||
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
_BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
||||||
@@ -65,21 +54,26 @@ _REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发,皮
|
|||||||
# 默认压到 896 兜底(ComfyUI ~4s,女性总耗时 9~11s);追画质可设 REDRAW_MAX_SIDE=1024。
|
# 默认压到 896 兜底(ComfyUI ~4s,女性总耗时 9~11s);追画质可设 REDRAW_MAX_SIDE=1024。
|
||||||
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "896"))
|
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "896"))
|
||||||
|
|
||||||
def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0):
|
def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0,
|
||||||
|
max_side=None, unet_name=None):
|
||||||
"""直接调 ComfyUI 重绘(替代原 local_test HTTP 服务)。
|
"""直接调 ComfyUI 重绘(替代原 local_test HTTP 服务)。
|
||||||
|
|
||||||
传 final 图 + 纯红遮罩 PNG,返回重绘后的 PNG bytes。
|
传 final 图 + 纯红遮罩 PNG,返回重绘后的 PNG bytes。
|
||||||
失败抛异常(调用方负责 try/except 跳过)。
|
失败抛异常(调用方负责 try/except 跳过)。
|
||||||
|
|
||||||
|
max_side:送 ComfyUI 前长边压到多少像素,None 用全局默认 _REDRAW_MAX_SIDE。
|
||||||
|
unet_name:非 None 时切换 Flux 模型,None 用工作流内置默认。
|
||||||
"""
|
"""
|
||||||
from .redraw import run_redraw
|
from .redraw import run_redraw
|
||||||
|
eff_side = _REDRAW_MAX_SIDE if max_side is None else max_side
|
||||||
img = cv2.imdecode(np.frombuffer(image_png_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
|
img = cv2.imdecode(np.frombuffer(image_png_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
orig_w = orig_h = 0
|
orig_w = orig_h = 0
|
||||||
if img is not None:
|
if img is not None:
|
||||||
orig_h, orig_w = img.shape[:2]
|
orig_h, orig_w = img.shape[:2]
|
||||||
m = max(orig_h, orig_w)
|
m = max(orig_h, orig_w)
|
||||||
if _REDRAW_MAX_SIDE > 0 and m > _REDRAW_MAX_SIDE:
|
if eff_side > 0 and m > eff_side:
|
||||||
scale = _REDRAW_MAX_SIDE / float(m)
|
scale = eff_side / float(m)
|
||||||
nw, nh = max(1, round(orig_w * scale)), max(1, round(orig_h * scale))
|
nw, nh = max(1, round(orig_w * scale)), max(1, round(orig_h * scale))
|
||||||
msk = cv2.imdecode(np.frombuffer(mask_png_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
|
msk = cv2.imdecode(np.frombuffer(mask_png_bytes, np.uint8), cv2.IMREAD_UNCHANGED)
|
||||||
img_s = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
|
img_s = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
|
||||||
@@ -87,10 +81,10 @@ def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0):
|
|||||||
image_png_bytes = cv2.imencode(".png", img_s)[1].tobytes()
|
image_png_bytes = cv2.imencode(".png", img_s)[1].tobytes()
|
||||||
mask_png_bytes = cv2.imencode(".png", msk_s)[1].tobytes()
|
mask_png_bytes = cv2.imencode(".png", msk_s)[1].tobytes()
|
||||||
logger.info("接口2女 缩图送 Comfy: %dx%d → %dx%d (max_side=%d)",
|
logger.info("接口2女 缩图送 Comfy: %dx%d → %dx%d (max_side=%d)",
|
||||||
orig_w, orig_h, nw, nh, _REDRAW_MAX_SIDE)
|
orig_w, orig_h, nw, nh, eff_side)
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前,避免排在接口3/5 的批量任务后面
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前,避免排在接口3/5 的批量任务后面
|
||||||
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
out = run_redraw(image_png_bytes, mask_png_bytes, timeout=timeout,
|
||||||
prompt=_REDRAW_PROMPT, front=True)
|
prompt=_REDRAW_PROMPT, front=True, unet_name=unet_name)
|
||||||
if scale < 1.0 and out:
|
if scale < 1.0 and out:
|
||||||
out = _upscale_png_to(out, orig_w, orig_h)
|
out = _upscale_png_to(out, orig_w, orig_h)
|
||||||
return out
|
return out
|
||||||
@@ -134,13 +128,10 @@ def _gender_key(stem: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_texture_map(level: str = "middle") -> dict:
|
def get_texture_map(level: str = "middle") -> dict:
|
||||||
"""扫描指定档位贴图目录建 {gender: [(key, path)]},按显式顺序表排序、按档位缓存。
|
"""扫描指定档位贴图目录建 {gender: [(key, path)]},按 key 排序、按档位缓存。
|
||||||
|
|
||||||
level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。
|
level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。
|
||||||
文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。
|
文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。
|
||||||
|
|
||||||
排序依据 _HAIRSTYLE_ORDER:表内 key 按表序、表外未知 key 兜底排到末尾(再按字母序),
|
|
||||||
保证新增/重命名文件不会打乱现有 hair_style 序号。
|
|
||||||
"""
|
"""
|
||||||
if level not in _TEXTURE_DIRS:
|
if level not in _TEXTURE_DIRS:
|
||||||
raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}")
|
raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}")
|
||||||
@@ -154,9 +145,7 @@ def get_texture_map(level: str = "middle") -> dict:
|
|||||||
if gender:
|
if gender:
|
||||||
mapping[gender].append((key, path))
|
mapping[gender].append((key, path))
|
||||||
for g in mapping:
|
for g in mapping:
|
||||||
order = _HAIRSTYLE_ORDER.get(g, [])
|
mapping[g].sort(key=lambda kp: kp[0])
|
||||||
idx = {k: i for i, k in enumerate(order)}
|
|
||||||
mapping[g].sort(key=lambda kp: (idx.get(kp[0], len(idx)), kp[0]))
|
|
||||||
_texture_maps[level] = mapping
|
_texture_maps[level] = mapping
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
@@ -216,10 +205,11 @@ def generate_previews(image_bgr: np.ndarray, gender: str):
|
|||||||
|
|
||||||
def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True,
|
def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True,
|
||||||
prompt: str = None, hair_styles: list[int] | None = None,
|
prompt: str = None, hair_styles: list[int] | None = None,
|
||||||
workflow_path: str | None = None):
|
workflow_path: str | None = None,
|
||||||
|
unet_name: str | None = None):
|
||||||
"""指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。
|
"""指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。
|
||||||
|
|
||||||
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..7,male: 1..6。
|
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..5,male: 1..4。
|
||||||
为 None 时生成全部(兼容旧调用)。
|
为 None 时生成全部(兼容旧调用)。
|
||||||
use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。
|
use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。
|
||||||
False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线)。
|
False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线)。
|
||||||
@@ -252,7 +242,8 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
compose_comfy_rgba(img_s, msk_s).save(buf, format="PNG", compress_level=1)
|
compose_comfy_rgba(img_s, msk_s).save(buf, format="PNG", compress_level=1)
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||||
shared_grown = comfyui.run(buf.getvalue(), prompt=prompt,
|
shared_grown = comfyui.run(buf.getvalue(), prompt=prompt,
|
||||||
workflow_path=workflow_path, front=True)
|
workflow_path=workflow_path, front=True,
|
||||||
|
unet_name=unet_name)
|
||||||
if gsc < 1.0 and shared_grown:
|
if gsc < 1.0 and shared_grown:
|
||||||
shared_grown = _upscale_png_to(shared_grown, w, h)
|
shared_grown = _upscale_png_to(shared_grown, w, h)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
@@ -277,7 +268,8 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||||
grown_png = comfyui.run(buf.getvalue(), prompt=prompt,
|
grown_png = comfyui.run(buf.getvalue(), prompt=prompt,
|
||||||
workflow_path=workflow_path, front=True)
|
workflow_path=workflow_path, front=True,
|
||||||
|
unet_name=unet_name)
|
||||||
if gsc < 1.0 and grown_png:
|
if gsc < 1.0 and grown_png:
|
||||||
grown_png = _upscale_png_to(grown_png, w, h)
|
grown_png = _upscale_png_to(grown_png, w, h)
|
||||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
||||||
@@ -291,20 +283,15 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T
|
|||||||
def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None,
|
def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None,
|
||||||
redraw_defaults: dict,
|
redraw_defaults: dict,
|
||||||
redraw_max_side: int | None = None,
|
redraw_max_side: int | None = None,
|
||||||
unet_name: str | None = None,
|
unet_name: str | None = None):
|
||||||
prompt: str | None = None):
|
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型重绘图。
|
||||||
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 生发图。
|
|
||||||
|
|
||||||
生发图来源按发型分两路:
|
grown 图来源(新流程):对每个选中发型把 female key 映射到 change_hair 的 chang_* hair_id,
|
||||||
- 换发型(chang_* 表内,1..5):female key → change_hair 的 chang_* hair_id,调
|
调 face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用
|
||||||
face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用
|
|
||||||
redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调
|
redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调
|
||||||
ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘。
|
ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘,重绘结果作为生发图。
|
||||||
- 原生生发(表外,6/7 bigflower/clasicalflower 等):无对应 change_hair LoRA,改走与男性
|
|
||||||
一致的原生生发(ComfyUI add_hair),由 _grow_native_one 完成(黑模板 + inpaint 遮罩)。
|
|
||||||
|
|
||||||
overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。
|
overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。
|
||||||
prompt 仅用于原生生发分支(换发型分支的提示词由 redraw 流程内部固定)。
|
|
||||||
Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)};
|
Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)};
|
||||||
无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。
|
无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。
|
||||||
"""
|
"""
|
||||||
@@ -330,16 +317,17 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
|||||||
|
|
||||||
# 重绘管线(swapHair + ComfyUI)统一降分辨率:真实照片 swap(SD WebUI)~5s、blend、ComfyUI
|
# 重绘管线(swapHair + ComfyUI)统一降分辨率:真实照片 swap(SD WebUI)~5s、blend、ComfyUI
|
||||||
# 均随分辨率线性下降。overlay 预览仍用全分辨率;grown_png 最后放大回原尺寸。
|
# 均随分辨率线性下降。overlay 预览仍用全分辨率;grown_png 最后放大回原尺寸。
|
||||||
|
eff_side = _REDRAW_MAX_SIDE if redraw_max_side is None else redraw_max_side
|
||||||
redraw_img = image_bgr
|
redraw_img = image_bgr
|
||||||
hair_mask_redraw = hair_mask_reuse
|
hair_mask_redraw = hair_mask_reuse
|
||||||
if _REDRAW_MAX_SIDE > 0 and max(h, w) > _REDRAW_MAX_SIDE:
|
if eff_side > 0 and max(h, w) > eff_side:
|
||||||
redraw_img, _rs = _downscale_max_side(image_bgr, _REDRAW_MAX_SIDE)
|
redraw_img, _rs = _downscale_max_side(image_bgr, eff_side)
|
||||||
_nh, _nw = redraw_img.shape[:2]
|
_nh, _nw = redraw_img.shape[:2]
|
||||||
if hair_mask_redraw is not None:
|
if hair_mask_redraw is not None:
|
||||||
hair_mask_redraw = cv2.resize(hair_mask_reuse.astype(np.uint8), (_nw, _nh),
|
hair_mask_redraw = cv2.resize(hair_mask_reuse.astype(np.uint8), (_nw, _nh),
|
||||||
interpolation=cv2.INTER_NEAREST).astype(bool)
|
interpolation=cv2.INTER_NEAREST).astype(bool)
|
||||||
logger.info("接口2女 管线降分辨率: %dx%d → %dx%d (max_side=%d)",
|
logger.info("接口2女 管线降分辨率: %dx%d → %dx%d (max_side=%d)",
|
||||||
w, h, _nw, _nh, _REDRAW_MAX_SIDE)
|
w, h, _nw, _nh, eff_side)
|
||||||
|
|
||||||
for order, (key, white_path) in items:
|
for order, (key, white_path) in items:
|
||||||
white = load_texture_rgba(white_path)
|
white = load_texture_rgba(white_path)
|
||||||
@@ -348,10 +336,7 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
|||||||
grown_png = None
|
grown_png = None
|
||||||
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
|
chang_id = _FEMALE_KEY_TO_CHANG.get(key)
|
||||||
if chang_id is None:
|
if chang_id is None:
|
||||||
# 无对应 change_hair LoRA(如 bigflower/clasicalflower)→ 走与男性一致的原生生发
|
logger.warning("接口2 换发型:female key=%s 无对应 chang_id,跳过生发图", key)
|
||||||
logger.info("接口2 女 key=%s 无 chang_id,走原生生发(ComfyUI add_hair)", key)
|
|
||||||
grown_png = _grow_native_one(image_bgr, ctx, white_path,
|
|
||||||
prompt=prompt, unet_name=unet_name)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
import time as _t
|
import time as _t
|
||||||
@@ -375,7 +360,9 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
|||||||
mask_bytes = base64.b64decode(mask_b64)
|
mask_bytes = base64.b64decode(mask_b64)
|
||||||
# 后端直接调 ComfyUI 重绘,返回重绘后的 PNG
|
# 后端直接调 ComfyUI 重绘,返回重绘后的 PNG
|
||||||
_tr0 = _t.perf_counter()
|
_tr0 = _t.perf_counter()
|
||||||
grown_png = _call_local_redraw(final_bytes, mask_bytes)
|
grown_png = _call_local_redraw(final_bytes, mask_bytes,
|
||||||
|
max_side=redraw_max_side,
|
||||||
|
unet_name=unet_name)
|
||||||
_tr1 = _t.perf_counter()
|
_tr1 = _t.perf_counter()
|
||||||
_tm = data.get("timings_ms") or {}
|
_tm = data.get("timings_ms") or {}
|
||||||
logger.info("接口2女 分段计时 type=%s: swapHair管线=%.2fs (mask=%dms swap=%dms blend=%dms), ComfyUI重绘=%.2fs",
|
logger.info("接口2女 分段计时 type=%s: swapHair管线=%.2fs (mask=%dms swap=%dms blend=%dms), ComfyUI重绘=%.2fs",
|
||||||
@@ -420,32 +407,6 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _grow_native_one(image_bgr: np.ndarray, ctx: dict, white_path: str,
|
|
||||||
prompt: str | None = None, unet_name: str | None = None):
|
|
||||||
"""对单个发际线做原生生发(ComfyUI add_hair),与男性 generate_grow_results 一致。
|
|
||||||
|
|
||||||
接口2 女性新发型(bigflower/clasicalflower 等)无对应 change_hair LoRA,改走此路径:
|
|
||||||
黑模板 → build_inpaint_mask → 限边(_prep_comfy_input)→ comfyui.run(front=True)。
|
|
||||||
失败返回 None,不抛异常。结果按限边前原图尺寸放大回原尺寸(仅展示对齐)。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
h, w = image_bgr.shape[:2]
|
|
||||||
black = load_texture_rgba(_black_texture_path(white_path))
|
|
||||||
marked, mask = build_inpaint_mask(
|
|
||||||
image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black)
|
|
||||||
m_s, msk_s, gsc = _prep_comfy_input(marked, mask)
|
|
||||||
buf = io.BytesIO()
|
|
||||||
compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1)
|
|
||||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
|
||||||
grown_png = comfyui.run(buf.getvalue(), prompt=prompt, front=True, unet_name=unet_name)
|
|
||||||
if gsc < 1.0 and grown_png:
|
|
||||||
grown_png = _upscale_png_to(grown_png, w, h)
|
|
||||||
return grown_png
|
|
||||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
|
||||||
logger.warning("接口2 女原生生发图失败:%s", e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||||
hair_styles: list[int], use_mask: bool = True,
|
hair_styles: list[int], use_mask: bool = True,
|
||||||
prompt: str | None = None,
|
prompt: str | None = None,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 286 KiB |