接口5改造(生发机制与接口2一致,按性别分流): - generate_hairline_pngs: 生发图来源从 _grow_from_texture(局部inpaint) 改为按性别分流——female走 generate_grow_results_swap(swapHair+Flux整帧重绘), male走 generate_grow_results(ComfyUI add_hair) - 新增参数 redraw_max_side/unet_name/v2_defaults(female路径透传) - 接口5 handler 加 flux_model/redraw_max_side Form参数 - 保留接口5独有输出: 3档叠图(middle/high/low)/中心点/face_measure - 已验证: female日志出现"接口2女 管线降分辨率max_side=640"+swap+Flux; male走add_hair.json; generate_grow_image=false正确跳过生发 统一ComfyUI重绘提示词: - "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜" → "填充遮罩区域的头发" - 覆盖: _REDRAW_PROMPT/_DEFAULT_PROMPT常量 + app.py各接口Form默认 + 工作流JSON节点60(add_hair/0716add-hair-api/hair_repaint) + 全部测试页输入框
135 lines
5.2 KiB
Python
135 lines
5.2 KiB
Python
#!/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()
|