Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a7c3fa07b | ||
|
|
b61ea6f33b | ||
|
|
b354c8c627 | ||
|
|
fd9c41430c | ||
|
|
5101bb5f6b | ||
|
|
ff4019c570 | ||
|
|
5226e23989 | ||
|
|
c462fd3634 | ||
|
|
ab6b0cb0bb | ||
|
|
94cdfd6de5 |
@@ -53,3 +53,28 @@ static/report_hairline_v2.zip
|
||||
# local_test 运行期日志 / pid(不入 git)
|
||||
local_test/hair_service.log
|
||||
local_test/hair_service.pid
|
||||
|
||||
# benchmark 原始输出(含结果图+原图,体积大,不入 git)
|
||||
benchmark_out/
|
||||
|
||||
# benchmark 部署的 HTML 报告(图片 base64 内嵌,体积大,不入 git)
|
||||
static/hairstyle_thumbs/
|
||||
|
||||
# 网关运行期日志(不入 git)
|
||||
gateway.log
|
||||
|
||||
# 工作流备份文件(不入 git)
|
||||
*.json.bak.*
|
||||
|
||||
# 分辨率对比测试产物(out/ 下结果图+原图副本+日志,体积大,不入 git)
|
||||
# 仅忽略 out/ 与运行期文件;测试脚本与 HTML 报告仍入库
|
||||
image/compare_test/out/
|
||||
image/res_test/out/
|
||||
image/wave_test/out/
|
||||
image/compare_test/progress.json
|
||||
image/res_test/progress.json
|
||||
image/wave_test/progress.json
|
||||
image/compare_test/batch_test.log
|
||||
image/res_test/batch_test.log
|
||||
image/wave_test/batch_test.log
|
||||
image/compare_test/http.log
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"16": {
|
||||
"class_type": "UNETLoader",
|
||||
"class_type": "UnetLoaderGGUF",
|
||||
"inputs": {
|
||||
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
||||
"unet_name": "flux-2-klein-9b-Q4_K_M.gguf",
|
||||
"weight_dtype": "fp8_e4m3fn_fast"
|
||||
}
|
||||
},
|
||||
@@ -15,7 +15,7 @@
|
||||
"61": {
|
||||
"class_type": "CLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b.safetensors",
|
||||
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
||||
"type": "flux2",
|
||||
"device": "cpu"
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"60": {
|
||||
"class_type": "JjkText",
|
||||
"inputs": {
|
||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||
"text": "填充遮罩区域的头发"
|
||||
}
|
||||
},
|
||||
"22": {
|
||||
|
||||
@@ -170,10 +170,10 @@
|
||||
},
|
||||
"16": {
|
||||
"inputs": {
|
||||
"unet_name": "flux-2-klein-4b-fp8.safetensors",
|
||||
"unet_name": "flux-2-klein-9b-Q4_K_M.gguf",
|
||||
"weight_dtype": "fp8_e4m3fn_fast"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"class_type": "UnetLoaderGGUF",
|
||||
"_meta": {
|
||||
"title": "UNet加载器"
|
||||
}
|
||||
@@ -410,7 +410,7 @@
|
||||
},
|
||||
"60": {
|
||||
"inputs": {
|
||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||
"text": "填充遮罩区域的头发"
|
||||
},
|
||||
"class_type": "JjkText",
|
||||
"_meta": {
|
||||
@@ -419,7 +419,7 @@
|
||||
},
|
||||
"61": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b.safetensors",
|
||||
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
|
||||
"type": "flux2",
|
||||
"device": "cpu"
|
||||
},
|
||||
|
||||
@@ -146,7 +146,7 @@ _AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static",
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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"])
|
||||
@@ -775,7 +775,9 @@ async def hair_grow(
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
|
||||
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
|
||||
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)
|
||||
if gender not in ("male", "female"):
|
||||
@@ -804,11 +806,13 @@ async def hair_grow(
|
||||
if gender == "female":
|
||||
from hairline.service import generate_grow_results_swap
|
||||
items = await run_in_threadpool(
|
||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS)
|
||||
generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS,
|
||||
redraw_max_side=redraw_max_side, unet_name=flux_model)
|
||||
else:
|
||||
from hairline.service import generate_grow_results
|
||||
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:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
@@ -886,7 +890,7 @@ async def hair_grow_b(
|
||||
marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"),
|
||||
marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"),
|
||||
use_mask: bool = Form(default=True, description="是否画发际线(测试对比用)。false 时跳过划线检测、直接送划线图"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"),
|
||||
):
|
||||
# 划线图三选一取图(只需这一张)
|
||||
marked_raw, e = await resolve_image_bytes(marked_image_file, marked_image_url, marked_image_base64)
|
||||
@@ -1015,10 +1019,13 @@ async def face_features(
|
||||
- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。
|
||||
- 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。
|
||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数(仅 male 路径生效)。
|
||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(最耗时)。
|
||||
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
|
||||
- **生发机制(同接口2,按性别分流)**:
|
||||
`female` 走「换发型 + Flux-2 整帧重绘」(依赖 change_hair:8801 与 ComfyUI:8188);
|
||||
`male` 走 ComfyUI `add_hair` 原生 inpaint。
|
||||
- 可选 `flux_model` / `redraw_max_side`:同接口2(仅 female 路径生效)。
|
||||
|
||||
**返回说明**:
|
||||
|
||||
@@ -1095,9 +1102,11 @@ async def hairline_generate(
|
||||
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
|
||||
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,仅 male 路径生效)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2,仅 male 路径生效),会替换工作流节点60的文本"),
|
||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||
flux_model: Optional[str] = Form(default=None, description="Flux 模型文件名(同接口2,切换模型用)。None=工作流默认"),
|
||||
redraw_max_side: Optional[int] = Form(default=None, description="重绘压图长边像素(同接口2,仅 female 路径生效)。None=默认896;0=不缩图(原图直送);其他如 768/640/1024"),
|
||||
):
|
||||
if gender not in ("male", "female"):
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
@@ -1122,7 +1131,9 @@ async def hairline_generate(
|
||||
|
||||
res = await run_in_threadpool(
|
||||
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
|
||||
generate_grow_image=generate_grow_image)
|
||||
generate_grow_image=generate_grow_image,
|
||||
redraw_max_side=redraw_max_side, unet_name=flux_model,
|
||||
v2_defaults=_V2_FINAL_DEFAULTS)
|
||||
if res is None:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
@@ -1490,7 +1501,7 @@ async def hairline_grow_v2(
|
||||
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图 | 1=噪声 | 2=纯色 | 3=潜变量。默认 1"),
|
||||
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素,默认 11"),
|
||||
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放,默认 1.0"),
|
||||
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜」"),
|
||||
comfyui_prompt: Optional[str] = Form(default=None, description="Flux-2 重绘提示词,None 用默认「填充遮罩区域的头发」"),
|
||||
beauty_alpha: float = Form(default=0.6, description="redraw_band 版 band 外的全脸美颜融入强度(0=band外无美颜纯用final,1≈整帧版),默认 0.6"),
|
||||
band_lo_mult: float = Form(default=0.5, description="重绘带外推倍率下限(相对 hairline_push_cm,内轮廓=0×、原外推线=1.0×),默认 0.5"),
|
||||
band_hi_mult: float = Form(default=1.5, description="重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5"),
|
||||
@@ -1656,7 +1667,7 @@ async def hairline_grow_v2_final_v2(
|
||||
async def api_redraw(
|
||||
image_file: UploadFile = File(..., description="人物图片(JPG/PNG)"),
|
||||
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
||||
prompt: str = Form(default="填充遮罩区域的头发",
|
||||
description="ComfyUI 提示词"),
|
||||
):
|
||||
image_bytes = await image_file.read()
|
||||
|
||||
@@ -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()
|
||||
@@ -202,7 +202,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
||||
|
||||
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
||||
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))
|
||||
dash_len = max(4, round(s * 0.008))
|
||||
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
||||
|
||||
@@ -1107,7 +1107,7 @@ def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="seg
|
||||
"hairline_id": hairline_id,
|
||||
"blend_method": blend_method,
|
||||
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
||||
"comfyui_prompt": comfyui_prompt or "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜",
|
||||
"comfyui_prompt": comfyui_prompt or "填充遮罩区域的头发",
|
||||
"beauty_alpha": beauty_alpha,
|
||||
"px_per_cm": round(float(px_per_cm), 4),
|
||||
"mask_pixels": mask_viz["mask_pixels"],
|
||||
|
||||
@@ -12,7 +12,7 @@ from face_analysis.calibration import (
|
||||
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
||||
)
|
||||
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_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):
|
||||
"""眉心 = 索引 9(眉间上点)。"""
|
||||
return normalized_to_pixel(lm[GLABELLA_9], w, h)
|
||||
"""眉心 = 索引 9 / 151 中点。"""
|
||||
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):
|
||||
|
||||
@@ -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": {
|
||||
"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"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"class_type": "UnetLoaderGGUF",
|
||||
"_meta": {
|
||||
"title": "UNet加载器"
|
||||
}
|
||||
@@ -410,7 +410,7 @@
|
||||
},
|
||||
"60": {
|
||||
"inputs": {
|
||||
"text": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||
"text": "填充遮罩区域的头发"
|
||||
},
|
||||
"class_type": "JjkText",
|
||||
"_meta": {
|
||||
|
||||
@@ -30,6 +30,19 @@ _REPO = os.path.dirname(os.path.dirname(__file__))
|
||||
_INPUT_NODE = "26" # LoadImage:外部输入图(含 alpha 遮罩)
|
||||
_SEED_NODE = "6" # RandomNoise
|
||||
_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_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,
|
||||
workflow_path: str | None = None, front: bool = False) -> bytes:
|
||||
workflow_path: str | None = None, front: bool = False,
|
||||
unet_name: str | None = None) -> bytes:
|
||||
"""提交一次生发任务,返回输出 PNG 字节。失败抛异常。
|
||||
|
||||
prompt:非 None 时替换工作流节点60(JjkText)的文本;None 时用工作流内置默认提示词。
|
||||
workflow_path:工作流 JSON 路径,None 则用默认 add_hair.json。
|
||||
front:True 时任务插到 ComfyUI 队列最前(server 端 "front" 字段,队列号取负)。
|
||||
接口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
|
||||
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)
|
||||
if prompt is not None:
|
||||
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 跑的对比
|
||||
try:
|
||||
|
||||
@@ -16,7 +16,7 @@ from . import comfyui
|
||||
|
||||
logger = logging.getLogger("hair.worker")
|
||||
|
||||
_DEFAULT_PROMPT = "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||
_DEFAULT_PROMPT = "填充遮罩区域的头发"
|
||||
_REPO = os.path.dirname(os.path.dirname(__file__))
|
||||
_REPAINT_WORKFLOW = os.path.join(_REPO, "0716add-hair-api.json")
|
||||
|
||||
@@ -56,15 +56,16 @@ def _process_mask_to_rgba(image_bytes: bytes, mask_bytes: bytes) -> bytes:
|
||||
|
||||
def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
||||
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。
|
||||
|
||||
Args:
|
||||
image_bytes: 人物图片字节(JPG/PNG)
|
||||
mask_bytes: 遮罩图片字节(支持红/白/alpha 遮罩格式)
|
||||
prompt: 提示词,None 用默认 "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"
|
||||
prompt: 提示词,None 用默认 "填充遮罩区域的头发"
|
||||
timeout: ComfyUI 超时秒数
|
||||
front: True 时任务插到 ComfyUI 队列最前(接口2 时延敏感路径用)
|
||||
unet_name: 非 None 时切换 Flux 模型(如 flux-2-klein-9b-Q5_K_M.gguf),None 用工作流默认
|
||||
|
||||
Returns:
|
||||
重绘后的 PNG 图片字节
|
||||
@@ -75,4 +76,5 @@ def run_redraw(image_bytes: bytes, mask_bytes: bytes,
|
||||
"""
|
||||
rgba_png = _process_mask_to_rgba(image_bytes, mask_bytes)
|
||||
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)
|
||||
|
||||
@@ -46,29 +46,35 @@ _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black")
|
||||
# 关键:ComfyUI 单卡显存装不下 Flux(7.7G)+qwen CLIP(3.9G) 同驻,靠缓存 CLIP 文本条件避免重载。
|
||||
# prompt 不同会使缓存失效 → 重载 CLIP 并挤出 Flux(每次 +4s)。三接口用同一字符串即可全程命中。
|
||||
# 与 app.py 接口2/接口3 的默认 prompt 保持一致;可用 REDRAW_PROMPT 覆盖。
|
||||
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||
_REDRAW_PROMPT = os.getenv("REDRAW_PROMPT", "填充遮罩区域的头发")
|
||||
|
||||
# 接口2 女重绘整条管线(swapHair + ComfyUI)送模型前限边。真实照片常达 1257x1495:
|
||||
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。女性路径含 swapHair(SD WebUI ~5.3s
|
||||
# 固定地板) + ComfyUI 两段串行。1024 档画质更好但部分大图会踩 12s 线,
|
||||
# 默认压到 896 兜底(ComfyUI ~4s,女性总耗时 9~11s);追画质可设 REDRAW_MAX_SIDE=1024。
|
||||
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "896"))
|
||||
# 全分辨率 ComfyUI 重绘要 13~21s 且激活显存把模型挤出。
|
||||
# 策略:输入图长边 > REDRAW_MAX_SIDE 才等比缩到该长边;≤ 时原图分辨率直送(不放大)。
|
||||
# 默认 1024:大于 1024 的图压到 1024(画质/速度均衡),≤1024 的小图保持原分辨率重绘。
|
||||
# 可用 REDRAW_MAX_SIDE 覆盖;0=永不缩图(原图直送)。
|
||||
_REDRAW_MAX_SIDE = int(os.getenv("REDRAW_MAX_SIDE", "1024"))
|
||||
|
||||
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 服务)。
|
||||
|
||||
传 final 图 + 纯红遮罩 PNG,返回重绘后的 PNG bytes。
|
||||
失败抛异常(调用方负责 try/except 跳过)。
|
||||
|
||||
max_side:送 ComfyUI 前长边压到多少像素,None 用全局默认 _REDRAW_MAX_SIDE。
|
||||
unet_name:非 None 时切换 Flux 模型,None 用工作流内置默认。
|
||||
"""
|
||||
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)
|
||||
scale = 1.0
|
||||
orig_w = orig_h = 0
|
||||
if img is not None:
|
||||
orig_h, orig_w = img.shape[:2]
|
||||
m = max(orig_h, orig_w)
|
||||
if _REDRAW_MAX_SIDE > 0 and m > _REDRAW_MAX_SIDE:
|
||||
scale = _REDRAW_MAX_SIDE / float(m)
|
||||
if eff_side > 0 and m > eff_side:
|
||||
scale = eff_side / float(m)
|
||||
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)
|
||||
img_s = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
|
||||
@@ -76,10 +82,10 @@ def _call_local_redraw(image_png_bytes, mask_png_bytes, timeout=300.0):
|
||||
image_png_bytes = cv2.imencode(".png", img_s)[1].tobytes()
|
||||
mask_png_bytes = cv2.imencode(".png", msk_s)[1].tobytes()
|
||||
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 的批量任务后面
|
||||
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:
|
||||
out = _upscale_png_to(out, orig_w, orig_h)
|
||||
return out
|
||||
@@ -200,7 +206,8 @@ def generate_previews(image_bgr: np.ndarray, gender: str):
|
||||
|
||||
def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True,
|
||||
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)。
|
||||
|
||||
hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..5,male: 1..4。
|
||||
@@ -236,7 +243,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)
|
||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||
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:
|
||||
shared_grown = _upscale_png_to(shared_grown, w, h)
|
||||
except Exception as e: # noqa: BLE001
|
||||
@@ -261,7 +269,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)
|
||||
# front=True:接口2 时延敏感,插到 ComfyUI 队列最前
|
||||
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:
|
||||
grown_png = _upscale_png_to(grown_png, w, h)
|
||||
except Exception as e: # noqa: BLE001 单张失败不拖垮整请求
|
||||
@@ -273,7 +282,9 @@ 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,
|
||||
redraw_defaults: dict):
|
||||
redraw_defaults: dict,
|
||||
redraw_max_side: int | None = None,
|
||||
unet_name: str | None = None):
|
||||
"""接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型重绘图。
|
||||
|
||||
grown 图来源(新流程):对每个选中发型把 female key 映射到 change_hair 的 chang_* hair_id,
|
||||
@@ -307,16 +318,17 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
||||
|
||||
# 重绘管线(swapHair + ComfyUI)统一降分辨率:真实照片 swap(SD WebUI)~5s、blend、ComfyUI
|
||||
# 均随分辨率线性下降。overlay 预览仍用全分辨率;grown_png 最后放大回原尺寸。
|
||||
eff_side = _REDRAW_MAX_SIDE if redraw_max_side is None else redraw_max_side
|
||||
redraw_img = image_bgr
|
||||
hair_mask_redraw = hair_mask_reuse
|
||||
if _REDRAW_MAX_SIDE > 0 and max(h, w) > _REDRAW_MAX_SIDE:
|
||||
redraw_img, _rs = _downscale_max_side(image_bgr, _REDRAW_MAX_SIDE)
|
||||
if eff_side > 0 and max(h, w) > eff_side:
|
||||
redraw_img, _rs = _downscale_max_side(image_bgr, 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)
|
||||
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:
|
||||
white = load_texture_rgba(white_path)
|
||||
@@ -349,7 +361,9 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N
|
||||
mask_bytes = base64.b64decode(mask_b64)
|
||||
# 后端直接调 ComfyUI 重绘,返回重绘后的 PNG
|
||||
_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()
|
||||
_tm = data.get("timings_ms") or {}
|
||||
logger.info("接口2女 分段计时 type=%s: swapHair管线=%.2fs (mask=%dms swap=%dms blend=%dms), ComfyUI重绘=%.2fs",
|
||||
@@ -397,14 +411,22 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
hair_styles: list[int], use_mask: bool = True,
|
||||
prompt: str | None = None,
|
||||
generate_grow_image: bool = True):
|
||||
generate_grow_image: bool = True,
|
||||
redraw_max_side: int | None = None,
|
||||
unet_name: str | None = None,
|
||||
v2_defaults: dict | None = None):
|
||||
"""接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。
|
||||
|
||||
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
|
||||
每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图;
|
||||
三档贴图同名,生发黑模板固定取自 hairline_texture_black/(middle),故生发目标固定 middle 档。
|
||||
use_mask/prompt:同接口2 的生发参数。
|
||||
generate_grow_image(默认 True):是否生成生发图(ComfyUI,最耗时)。False 时跳过生发,
|
||||
|
||||
生发机制(同接口2,按性别分流):
|
||||
- female:generate_grow_results_swap(swapHair + Flux-2 整帧重绘)
|
||||
- male:generate_grow_results(ComfyUI add_hair inpaint)
|
||||
redraw_max_side / unet_name / v2_defaults:female 路径参数,同接口2。
|
||||
male 路径仅用 unet_name;redraw_max_side/v2_defaults 对 male 无意义(忽略)。
|
||||
use_mask/prompt:仅 male 路径生效(同接口2 male)。
|
||||
generate_grow_image(默认 True):是否生成生发图(最耗时)。False 时跳过生发,
|
||||
各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。
|
||||
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}],
|
||||
"best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。
|
||||
@@ -426,11 +448,27 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
# 三档贴图表(同性别、同 key 顺序,因三个文件夹同名)
|
||||
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
|
||||
|
||||
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
||||
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None
|
||||
shared_grown = None
|
||||
if generate_grow_image and not use_mask:
|
||||
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
||||
# 生发图(同接口2,按性别分流):一次性算出所有选中发型的生发图,按 order 对应回叠图。
|
||||
# female→generate_grow_results_swap(swapHair+Flux-2 整帧重绘);
|
||||
# male→generate_grow_results(ComfyUI add_hair inpaint)。
|
||||
# generate_grow_image=False 时跳过,grown_by_order 为空 dict(各发型 grown_png 恒 None)。
|
||||
grown_by_order: dict[int, bytes | None] = {}
|
||||
if generate_grow_image:
|
||||
try:
|
||||
if gender == "female":
|
||||
items = generate_grow_results_swap(
|
||||
image_bgr, hair_styles, v2_defaults or {},
|
||||
redraw_max_side=redraw_max_side, unet_name=unet_name)
|
||||
else:
|
||||
items = generate_grow_results(
|
||||
image_bgr, gender, use_mask, prompt, hair_styles,
|
||||
unet_name=unet_name)
|
||||
if items is None:
|
||||
return None # 无人脸(同接口2 的 None 语义)
|
||||
for it in items:
|
||||
grown_by_order[it["order"]] = it.get("grown_png")
|
||||
except Exception as e: # noqa: BLE001 整批生发失败不拖垮叠图主结果
|
||||
logger.warning("接口5 生发批量失败(gender=%s):%s", gender, e)
|
||||
|
||||
def _center_of(overlay):
|
||||
"""从某档发际线透明叠图取面部中轴处的发际线中点 (x,y),无像素返回 None。"""
|
||||
@@ -443,18 +481,13 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
|
||||
images, best_centers = [], None
|
||||
for s in hair_styles: # s = 1-indexed 发型序号
|
||||
key, mid_path = tex_by_level["middle"][s - 1]
|
||||
key, _mid_path = tex_by_level["middle"][s - 1]
|
||||
overlays = {}
|
||||
for lv in _TEXTURE_DIRS:
|
||||
white = load_texture_rgba(tex_by_level[lv][s - 1][1])
|
||||
overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
|
||||
# 生发:固定 middle 黑模板(generate_grow_image=False 时跳过,恒 None)
|
||||
if not generate_grow_image:
|
||||
grown_png = None
|
||||
elif not use_mask:
|
||||
grown_png = shared_grown
|
||||
else:
|
||||
grown_png = _grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
|
||||
# 生发图:从按性别算好的结果里按 order 取(generate_grow_image=False 时缺省 None)
|
||||
grown_png = grown_by_order.get(s)
|
||||
images.append({"hairline_type": key, "order": s,
|
||||
"overlays": overlays, "grown_png": grown_png})
|
||||
# best_centers:首个选中发型三档(middle/high/low)发际线中点
|
||||
|
||||
|
After Width: | Height: | Size: 440 KiB |
@@ -0,0 +1,75 @@
|
||||
=== 接口2 female 批量对比测试 ===
|
||||
矩阵: 4图 × 5发型 × 2档 = 40 次
|
||||
已跳过 0 个已完成项
|
||||
|
||||
[1/40] ▶ girl2_ellipse_default896 (side=None)... ✅ 10.8s 出图 1082x1081 (204589B)
|
||||
[2/40] ▶ girl2_ellipse_origin0 (side=0)... ✅ 13.3s 出图 1088x1088 (232252B)
|
||||
[3/40] ▶ girl2_flower_default896 (side=None)... ✅ 11.0s 出图 1082x1081 (210875B)
|
||||
[4/40] ▶ girl2_flower_origin0 (side=0)... ✅ 12.5s 出图 1088x1088 (236073B)
|
||||
[5/40] ▶ girl2_heart_default896 (side=None)... ✅ 11.9s 出图 1082x1081 (198911B)
|
||||
[6/40] ▶ girl2_heart_origin0 (side=0)... ✅ 12.2s 出图 1088x1088 (249893B)
|
||||
[7/40] ▶ girl2_straight_default896 (side=None)... ✅ 12.1s 出图 1082x1081 (211803B)
|
||||
[8/40] ▶ girl2_straight_origin0 (side=0)... ✅ 12.3s 出图 1088x1088 (235377B)
|
||||
[9/40] ▶ girl2_wave_default896 (side=None)... ✅ 12.7s 出图 1082x1081 (207736B)
|
||||
[10/40] ▶ girl2_wave_origin0 (side=0)... ✅ 11.9s 出图 1088x1088 (238081B)
|
||||
[11/40] ▶ girl5_ellipse_default896 (side=None)... ✅ 9.6s 出图 768x752 (105867B)
|
||||
[12/40] ▶ girl5_ellipse_origin0 (side=0)... ✅ 7.3s 出图 768x752 (112963B)
|
||||
[13/40] ▶ girl5_flower_default896 (side=None)... ✅ 8.0s 出图 768x752 (114378B)
|
||||
[14/40] ▶ girl5_flower_origin0 (side=0)... ✅ 7.2s 出图 768x752 (108495B)
|
||||
[15/40] ▶ girl5_heart_default896 (side=None)... ✅ 8.1s 出图 768x752 (113874B)
|
||||
[16/40] ▶ girl5_heart_origin0 (side=0)... ✅ 7.6s 出图 768x752 (103160B)
|
||||
[17/40] ▶ girl5_straight_default896 (side=None)... ✅ 8.4s 出图 768x752 (104492B)
|
||||
[18/40] ▶ girl5_straight_origin0 (side=0)... ✅ 7.2s 出图 768x752 (100941B)
|
||||
[19/40] ▶ girl5_wave_default896 (side=None)... ✅ 8.4s 出图 768x752 (120122B)
|
||||
[20/40] ▶ girl5_wave_origin0 (side=0)... ✅ 7.7s 出图 768x752 (113282B)
|
||||
[21/40] ▶ qwer_ellipse_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (261812B)
|
||||
[22/40] ▶ qwer_ellipse_origin0 (side=0)... ✅ 30.0s 出图 1280x1680 (331013B)
|
||||
[23/40] ▶ qwer_flower_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (312834B)
|
||||
[24/40] ▶ qwer_flower_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (328605B)
|
||||
[25/40] ▶ qwer_heart_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (331337B)
|
||||
[26/40] ▶ qwer_heart_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (355982B)
|
||||
[27/40] ▶ qwer_straight_default896 (side=None)... ✅ 10.7s 出图 1288x1678 (281665B)
|
||||
[28/40] ▶ qwer_straight_origin0 (side=0)... ✅ 23.4s 出图 1280x1680 (322183B)
|
||||
[29/40] ▶ qwer_wave_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (289322B)
|
||||
[30/40] ▶ qwer_wave_origin0 (side=0)... ✅ 25.8s 出图 1280x1680 (351719B)
|
||||
[31/40] ▶ asdf_ellipse_default896 (side=None)... ✅ 11.2s 出图 1254x1666 (285362B)
|
||||
[32/40] ▶ asdf_ellipse_origin0 (side=0)... ✅ 25.7s 出图 1248x1664 (286768B)
|
||||
[33/40] ▶ asdf_flower_default896 (side=None)... ✅ 10.7s 出图 1254x1666 (266946B)
|
||||
[34/40] ▶ asdf_flower_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (353668B)
|
||||
[35/40] ▶ asdf_heart_default896 (side=None)... ✅ 10.4s 出图 1254x1666 (271975B)
|
||||
[36/40] ▶ asdf_heart_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (291983B)
|
||||
[37/40] ▶ asdf_straight_default896 (side=None)... ✅ 9.8s 出图 1254x1666 (258665B)
|
||||
[38/40] ▶ asdf_straight_origin0 (side=0)... ✅ 24.0s 出图 1248x1664 (279883B)
|
||||
[39/40] ▶ asdf_wave_default896 (side=None)... ✅ 11.9s 出图 1254x1666 (286515B)
|
||||
[40/40] ▶ asdf_wave_origin0 (side=0)... ✅ 24.3s 出图 1248x1664 (295694B)
|
||||
|
||||
======================================================================
|
||||
汇总报告
|
||||
======================================================================
|
||||
|
||||
--- 速度对比(秒,✓=成功 ✗=失败)---
|
||||
图 发型 默认896 原图0 差值
|
||||
girl2 ellipse 10.8✓ 13.3✓ +2.5
|
||||
girl2 flower 11.0✓ 12.5✓ +1.5
|
||||
girl2 heart 11.9✓ 12.2✓ +0.3
|
||||
girl2 straight 12.1✓ 12.3✓ +0.2
|
||||
girl2 wave 12.7✓ 11.9✓ -0.8
|
||||
girl5 ellipse 9.6✓ 7.3✓ -2.3
|
||||
girl5 flower 8.0✓ 7.2✓ -0.8
|
||||
girl5 heart 8.1✓ 7.6✓ -0.5
|
||||
girl5 straight 8.4✓ 7.2✓ -1.2
|
||||
girl5 wave 8.4✓ 7.7✓ -0.7
|
||||
qwer ellipse 10.2✓ 30.0✓ +19.8
|
||||
qwer flower 10.2✓ 25.0✓ +14.8
|
||||
qwer heart 11.1✓ 25.0✓ +13.9
|
||||
qwer straight 10.7✓ 23.4✓ +12.7
|
||||
qwer wave 11.1✓ 25.8✓ +14.7
|
||||
asdf ellipse 11.2✓ 25.7✓ +14.5
|
||||
asdf flower 10.7✓ 22.9✓ +12.2
|
||||
asdf heart 10.4✓ 22.9✓ +12.5
|
||||
asdf straight 9.8✓ 24.0✓ +14.2
|
||||
asdf wave 11.9✓ 24.3✓ +12.4
|
||||
|
||||
结果图: /home/ubuntu/hair/image/compare_test/out/
|
||||
CSV报告: /home/ubuntu/hair/image/compare_test/out/report.csv
|
||||
JSON明细: /home/ubuntu/hair/image/compare_test/out/report.json
|
||||
@@ -0,0 +1,41 @@
|
||||
img,style,side,ok,elapsed_s,out_w,out_h,err
|
||||
girl2,ellipse,default896,True,10.8,1082,1081,
|
||||
girl2,ellipse,origin0,True,13.3,1088,1088,
|
||||
girl2,flower,default896,True,11.0,1082,1081,
|
||||
girl2,flower,origin0,True,12.5,1088,1088,
|
||||
girl2,heart,default896,True,11.9,1082,1081,
|
||||
girl2,heart,origin0,True,12.2,1088,1088,
|
||||
girl2,straight,default896,True,12.1,1082,1081,
|
||||
girl2,straight,origin0,True,12.3,1088,1088,
|
||||
girl2,wave,default896,True,12.7,1082,1081,
|
||||
girl2,wave,origin0,True,11.9,1088,1088,
|
||||
girl5,ellipse,default896,True,9.6,768,752,
|
||||
girl5,ellipse,origin0,True,7.3,768,752,
|
||||
girl5,flower,default896,True,8.0,768,752,
|
||||
girl5,flower,origin0,True,7.2,768,752,
|
||||
girl5,heart,default896,True,8.1,768,752,
|
||||
girl5,heart,origin0,True,7.6,768,752,
|
||||
girl5,straight,default896,True,8.4,768,752,
|
||||
girl5,straight,origin0,True,7.2,768,752,
|
||||
girl5,wave,default896,True,8.4,768,752,
|
||||
girl5,wave,origin0,True,7.7,768,752,
|
||||
qwer,ellipse,default896,True,10.2,1288,1678,
|
||||
qwer,ellipse,origin0,True,30.0,1280,1680,
|
||||
qwer,flower,default896,True,10.2,1288,1678,
|
||||
qwer,flower,origin0,True,25.0,1280,1680,
|
||||
qwer,heart,default896,True,11.1,1288,1678,
|
||||
qwer,heart,origin0,True,25.0,1280,1680,
|
||||
qwer,straight,default896,True,10.7,1288,1678,
|
||||
qwer,straight,origin0,True,23.4,1280,1680,
|
||||
qwer,wave,default896,True,11.1,1288,1678,
|
||||
qwer,wave,origin0,True,25.8,1280,1680,
|
||||
asdf,ellipse,default896,True,11.2,1254,1666,
|
||||
asdf,ellipse,origin0,True,25.7,1248,1664,
|
||||
asdf,flower,default896,True,10.7,1254,1666,
|
||||
asdf,flower,origin0,True,22.9,1248,1664,
|
||||
asdf,heart,default896,True,10.4,1254,1666,
|
||||
asdf,heart,origin0,True,22.9,1248,1664,
|
||||
asdf,straight,default896,True,9.8,1254,1666,
|
||||
asdf,straight,origin0,True,24.0,1248,1664,
|
||||
asdf,wave,default896,True,11.9,1254,1666,
|
||||
asdf,wave,origin0,True,24.3,1248,1664,
|
||||
|
@@ -0,0 +1,562 @@
|
||||
[
|
||||
{
|
||||
"key": "girl2_ellipse_default896",
|
||||
"img": "girl2",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.8,
|
||||
"err": "",
|
||||
"out_w": 1082,
|
||||
"out_h": 1081,
|
||||
"bytes": 204589
|
||||
},
|
||||
{
|
||||
"key": "girl2_ellipse_origin0",
|
||||
"img": "girl2",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 13.3,
|
||||
"err": "",
|
||||
"out_w": 1088,
|
||||
"out_h": 1088,
|
||||
"bytes": 232252
|
||||
},
|
||||
{
|
||||
"key": "girl2_flower_default896",
|
||||
"img": "girl2",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.0,
|
||||
"err": "",
|
||||
"out_w": 1082,
|
||||
"out_h": 1081,
|
||||
"bytes": 210875
|
||||
},
|
||||
{
|
||||
"key": "girl2_flower_origin0",
|
||||
"img": "girl2",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 12.5,
|
||||
"err": "",
|
||||
"out_w": 1088,
|
||||
"out_h": 1088,
|
||||
"bytes": 236073
|
||||
},
|
||||
{
|
||||
"key": "girl2_heart_default896",
|
||||
"img": "girl2",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.9,
|
||||
"err": "",
|
||||
"out_w": 1082,
|
||||
"out_h": 1081,
|
||||
"bytes": 198911
|
||||
},
|
||||
{
|
||||
"key": "girl2_heart_origin0",
|
||||
"img": "girl2",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 12.2,
|
||||
"err": "",
|
||||
"out_w": 1088,
|
||||
"out_h": 1088,
|
||||
"bytes": 249893
|
||||
},
|
||||
{
|
||||
"key": "girl2_straight_default896",
|
||||
"img": "girl2",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 12.1,
|
||||
"err": "",
|
||||
"out_w": 1082,
|
||||
"out_h": 1081,
|
||||
"bytes": 211803
|
||||
},
|
||||
{
|
||||
"key": "girl2_straight_origin0",
|
||||
"img": "girl2",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 12.3,
|
||||
"err": "",
|
||||
"out_w": 1088,
|
||||
"out_h": 1088,
|
||||
"bytes": 235377
|
||||
},
|
||||
{
|
||||
"key": "girl2_wave_default896",
|
||||
"img": "girl2",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 12.7,
|
||||
"err": "",
|
||||
"out_w": 1082,
|
||||
"out_h": 1081,
|
||||
"bytes": 207736
|
||||
},
|
||||
{
|
||||
"key": "girl2_wave_origin0",
|
||||
"img": "girl2",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 11.9,
|
||||
"err": "",
|
||||
"out_w": 1088,
|
||||
"out_h": 1088,
|
||||
"bytes": 238081
|
||||
},
|
||||
{
|
||||
"key": "girl5_ellipse_default896",
|
||||
"img": "girl5",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 9.6,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 105867
|
||||
},
|
||||
{
|
||||
"key": "girl5_ellipse_origin0",
|
||||
"img": "girl5",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 7.3,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 112963
|
||||
},
|
||||
{
|
||||
"key": "girl5_flower_default896",
|
||||
"img": "girl5",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 8.0,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 114378
|
||||
},
|
||||
{
|
||||
"key": "girl5_flower_origin0",
|
||||
"img": "girl5",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 7.2,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 108495
|
||||
},
|
||||
{
|
||||
"key": "girl5_heart_default896",
|
||||
"img": "girl5",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 8.1,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 113874
|
||||
},
|
||||
{
|
||||
"key": "girl5_heart_origin0",
|
||||
"img": "girl5",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 7.6,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 103160
|
||||
},
|
||||
{
|
||||
"key": "girl5_straight_default896",
|
||||
"img": "girl5",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 8.4,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 104492
|
||||
},
|
||||
{
|
||||
"key": "girl5_straight_origin0",
|
||||
"img": "girl5",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 7.2,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 100941
|
||||
},
|
||||
{
|
||||
"key": "girl5_wave_default896",
|
||||
"img": "girl5",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 8.4,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 120122
|
||||
},
|
||||
{
|
||||
"key": "girl5_wave_origin0",
|
||||
"img": "girl5",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 7.7,
|
||||
"err": "",
|
||||
"out_w": 768,
|
||||
"out_h": 752,
|
||||
"bytes": 113282
|
||||
},
|
||||
{
|
||||
"key": "qwer_ellipse_default896",
|
||||
"img": "qwer",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.2,
|
||||
"err": "",
|
||||
"out_w": 1288,
|
||||
"out_h": 1678,
|
||||
"bytes": 261812
|
||||
},
|
||||
{
|
||||
"key": "qwer_ellipse_origin0",
|
||||
"img": "qwer",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 30.0,
|
||||
"err": "",
|
||||
"out_w": 1280,
|
||||
"out_h": 1680,
|
||||
"bytes": 331013
|
||||
},
|
||||
{
|
||||
"key": "qwer_flower_default896",
|
||||
"img": "qwer",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.2,
|
||||
"err": "",
|
||||
"out_w": 1288,
|
||||
"out_h": 1678,
|
||||
"bytes": 312834
|
||||
},
|
||||
{
|
||||
"key": "qwer_flower_origin0",
|
||||
"img": "qwer",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 25.0,
|
||||
"err": "",
|
||||
"out_w": 1280,
|
||||
"out_h": 1680,
|
||||
"bytes": 328605
|
||||
},
|
||||
{
|
||||
"key": "qwer_heart_default896",
|
||||
"img": "qwer",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.1,
|
||||
"err": "",
|
||||
"out_w": 1288,
|
||||
"out_h": 1678,
|
||||
"bytes": 331337
|
||||
},
|
||||
{
|
||||
"key": "qwer_heart_origin0",
|
||||
"img": "qwer",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 25.0,
|
||||
"err": "",
|
||||
"out_w": 1280,
|
||||
"out_h": 1680,
|
||||
"bytes": 355982
|
||||
},
|
||||
{
|
||||
"key": "qwer_straight_default896",
|
||||
"img": "qwer",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.7,
|
||||
"err": "",
|
||||
"out_w": 1288,
|
||||
"out_h": 1678,
|
||||
"bytes": 281665
|
||||
},
|
||||
{
|
||||
"key": "qwer_straight_origin0",
|
||||
"img": "qwer",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 23.4,
|
||||
"err": "",
|
||||
"out_w": 1280,
|
||||
"out_h": 1680,
|
||||
"bytes": 322183
|
||||
},
|
||||
{
|
||||
"key": "qwer_wave_default896",
|
||||
"img": "qwer",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.1,
|
||||
"err": "",
|
||||
"out_w": 1288,
|
||||
"out_h": 1678,
|
||||
"bytes": 289322
|
||||
},
|
||||
{
|
||||
"key": "qwer_wave_origin0",
|
||||
"img": "qwer",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 25.8,
|
||||
"err": "",
|
||||
"out_w": 1280,
|
||||
"out_h": 1680,
|
||||
"bytes": 351719
|
||||
},
|
||||
{
|
||||
"key": "asdf_ellipse_default896",
|
||||
"img": "asdf",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.2,
|
||||
"err": "",
|
||||
"out_w": 1254,
|
||||
"out_h": 1666,
|
||||
"bytes": 285362
|
||||
},
|
||||
{
|
||||
"key": "asdf_ellipse_origin0",
|
||||
"img": "asdf",
|
||||
"style": "ellipse",
|
||||
"style_idx": 1,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 25.7,
|
||||
"err": "",
|
||||
"out_w": 1248,
|
||||
"out_h": 1664,
|
||||
"bytes": 286768
|
||||
},
|
||||
{
|
||||
"key": "asdf_flower_default896",
|
||||
"img": "asdf",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.7,
|
||||
"err": "",
|
||||
"out_w": 1254,
|
||||
"out_h": 1666,
|
||||
"bytes": 266946
|
||||
},
|
||||
{
|
||||
"key": "asdf_flower_origin0",
|
||||
"img": "asdf",
|
||||
"style": "flower",
|
||||
"style_idx": 2,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 22.9,
|
||||
"err": "",
|
||||
"out_w": 1248,
|
||||
"out_h": 1664,
|
||||
"bytes": 353668
|
||||
},
|
||||
{
|
||||
"key": "asdf_heart_default896",
|
||||
"img": "asdf",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 10.4,
|
||||
"err": "",
|
||||
"out_w": 1254,
|
||||
"out_h": 1666,
|
||||
"bytes": 271975
|
||||
},
|
||||
{
|
||||
"key": "asdf_heart_origin0",
|
||||
"img": "asdf",
|
||||
"style": "heart",
|
||||
"style_idx": 3,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 22.9,
|
||||
"err": "",
|
||||
"out_w": 1248,
|
||||
"out_h": 1664,
|
||||
"bytes": 291983
|
||||
},
|
||||
{
|
||||
"key": "asdf_straight_default896",
|
||||
"img": "asdf",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 9.8,
|
||||
"err": "",
|
||||
"out_w": 1254,
|
||||
"out_h": 1666,
|
||||
"bytes": 258665
|
||||
},
|
||||
{
|
||||
"key": "asdf_straight_origin0",
|
||||
"img": "asdf",
|
||||
"style": "straight",
|
||||
"style_idx": 4,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 24.0,
|
||||
"err": "",
|
||||
"out_w": 1248,
|
||||
"out_h": 1664,
|
||||
"bytes": 279883
|
||||
},
|
||||
{
|
||||
"key": "asdf_wave_default896",
|
||||
"img": "asdf",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "default896",
|
||||
"side_val": null,
|
||||
"ok": true,
|
||||
"elapsed": 11.9,
|
||||
"err": "",
|
||||
"out_w": 1254,
|
||||
"out_h": 1666,
|
||||
"bytes": 286515
|
||||
},
|
||||
{
|
||||
"key": "asdf_wave_origin0",
|
||||
"img": "asdf",
|
||||
"style": "wave",
|
||||
"style_idx": 5,
|
||||
"side": "origin0",
|
||||
"side_val": 0,
|
||||
"ok": true,
|
||||
"elapsed": 24.3,
|
||||
"err": "",
|
||||
"out_w": 1248,
|
||||
"out_h": 1664,
|
||||
"bytes": 295694
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""接口2 female 批量对比测试:4图 × 5发型 × 3分辨率档位 = 60次请求。
|
||||
串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
|
||||
用法: python3 batch_test.py
|
||||
支持断点续跑(progress.json);新增档位时只会补跑未完成项。
|
||||
"""
|
||||
import base64, csv, json, os, sys, time, traceback
|
||||
import requests
|
||||
|
||||
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||
TOKEN = "dev-shared-secret-2026"
|
||||
TIMEOUT = 600
|
||||
OUT = "/home/ubuntu/hair/image/compare_test/out"
|
||||
PROGRESS = "/home/ubuntu/hair/image/compare_test/progress.json"
|
||||
|
||||
IMG_DIR = "/home/ubuntu/hair/image"
|
||||
IMAGES = [
|
||||
("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
|
||||
("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
|
||||
("qwer", f"{IMG_DIR}/qwer.jpg"),
|
||||
("asdf", f"{IMG_DIR}/asdf.jpg"),
|
||||
]
|
||||
# female: 1=ellipse 2=flower 3=heart 4=straight 5=wave
|
||||
STYLES = [
|
||||
(1, "ellipse"), (2, "flower"), (3, "heart"), (4, "straight"), (5, "wave"),
|
||||
]
|
||||
# 三档: 默认896 / 显式1024 / 原图直送0
|
||||
SIDES = [
|
||||
("default896", None), # 不传 → 后端默认896
|
||||
("side1024", 1024), # 长边压到 1024
|
||||
("origin0", 0), # 原图直送
|
||||
]
|
||||
SIDE_LABEL = {
|
||||
"default896": "默认896",
|
||||
"side1024": "1024",
|
||||
"origin0": "原图0",
|
||||
}
|
||||
|
||||
def load_progress():
|
||||
if os.path.exists(PROGRESS):
|
||||
try:
|
||||
return json.load(open(PROGRESS))
|
||||
except Exception:
|
||||
pass
|
||||
return {"done": [], "results": []}
|
||||
|
||||
def save_progress(prog):
|
||||
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||
|
||||
def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
|
||||
"""跑单次请求,返回 dict 结果。"""
|
||||
key = f"{img_name}_{style_key}_{side_name}"
|
||||
with open(img_path, "rb") as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode()
|
||||
data = {
|
||||
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||
"gender": "female",
|
||||
"hair_style": str(style_idx),
|
||||
}
|
||||
if side_val is not None:
|
||||
data["redraw_max_side"] = str(side_val)
|
||||
t0 = time.time()
|
||||
rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
|
||||
"side": side_name, "side_val": side_val, "ok": False,
|
||||
"elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0}
|
||||
try:
|
||||
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
d = r.json()
|
||||
if d.get("code") != 0:
|
||||
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||
return rec
|
||||
results = (d.get("data") or {}).get("results") or []
|
||||
if not results:
|
||||
rec["err"] = "空结果"
|
||||
return rec
|
||||
it = results[0]
|
||||
grown = it.get("grown_image_base64")
|
||||
if not grown:
|
||||
rec["err"] = "无生发图(grown_png=None, 重绘失败)"
|
||||
return rec
|
||||
raw = base64.b64decode(grown)
|
||||
from PIL import Image
|
||||
import io as _io
|
||||
im = Image.open(_io.BytesIO(raw))
|
||||
rec["out_w"], rec["out_h"] = im.size
|
||||
out_path = f"{OUT}/{key}.jpg"
|
||||
with open(out_path, "wb") as fo:
|
||||
fo.write(raw)
|
||||
rec["ok"] = True
|
||||
rec["bytes"] = len(raw)
|
||||
except requests.exceptions.Timeout:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||
except Exception as e:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||
return rec
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
prog = load_progress()
|
||||
done_keys = set(prog["done"])
|
||||
total = len(IMAGES) * len(STYLES) * len(SIDES)
|
||||
print(f"=== 接口2 female 批量对比测试 ===")
|
||||
print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
|
||||
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||
|
||||
idx = 0
|
||||
for img_name, img_path in IMAGES:
|
||||
for style_idx, style_key in STYLES:
|
||||
for side_name, side_val in SIDES:
|
||||
idx += 1
|
||||
key = f"{img_name}_{style_key}_{side_name}"
|
||||
if key in done_keys:
|
||||
print(f"[{idx}/{total}] ⏭ 跳过已完成 {key}")
|
||||
continue
|
||||
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||
rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
|
||||
prog["results"].append(rec)
|
||||
prog["done"].append(key)
|
||||
save_progress(prog)
|
||||
if rec["ok"]:
|
||||
print(f"✅ {rec['elapsed']}s 出图 {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||
else:
|
||||
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||
time.sleep(2) # 串行间隔,避免队列堆积
|
||||
|
||||
# 汇总
|
||||
print("\n" + "="*70)
|
||||
print("汇总报告")
|
||||
print("="*70)
|
||||
write_report(prog["results"])
|
||||
print(f"\n结果图: {OUT}/")
|
||||
print(f"CSV报告: {OUT}/report.csv")
|
||||
print(f"JSON明细: {OUT}/report.json")
|
||||
|
||||
def write_report(results):
|
||||
# CSV
|
||||
csv_path = f"{OUT}/report.csv"
|
||||
with open(csv_path, "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["img","style","side","ok","elapsed_s","out_w","out_h","err"])
|
||||
for r in results:
|
||||
w.writerow([r["img"],r["style"],r["side"],r["ok"],r["elapsed"],
|
||||
r["out_w"],r["out_h"],r["err"]])
|
||||
# JSON
|
||||
json.dump(results, open(f"{OUT}/report.json","w"), ensure_ascii=False, indent=1)
|
||||
# 控制台速度对比表
|
||||
print("\n--- 速度对比(秒,✓=成功 ✗=失败)---")
|
||||
headers = ["图", "发型"] + [SIDE_LABEL[s] for s, _ in SIDES]
|
||||
print(f"{headers[0]:<8}{headers[1]:<10}" + "".join(f"{h:>12}" for h in headers[2:]))
|
||||
for img_name, _ in IMAGES:
|
||||
for _, style_key in STYLES:
|
||||
cells = []
|
||||
for side_name, _ in SIDES:
|
||||
r = next((x for x in results if x["img"]==img_name and x["style"]==style_key and x["side"]==side_name), None)
|
||||
cells.append(f"{r['elapsed']}{'✓' if r['ok'] else '✗'}" if r else "-")
|
||||
print(f"{img_name:<8}{style_key:<10}" + "".join(f"{c:>12}" for c in cells))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把 report.json + 结果图生成为自包含 HTML 报告(支持 896 / 1024 / 原图0 三档)。"""
|
||||
import json, os, html
|
||||
|
||||
OUT = "/home/ubuntu/hair/image/compare_test/out"
|
||||
REPORT_HTML = os.path.join(OUT, "report.html")
|
||||
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||
|
||||
IMAGES = ["girl2", "girl5", "qwer", "asdf"]
|
||||
STYLES = ["ellipse", "flower", "heart", "straight", "wave"]
|
||||
SIDES = [
|
||||
("default896", "默认 896", "#2563eb", "d"),
|
||||
("side1024", "1024", "#10b981", "m"),
|
||||
("origin0", "原图直送 0", "#f59e0b", "o"),
|
||||
]
|
||||
IMG_LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666}
|
||||
STYLE_CN = {"ellipse": "椭圆", "flower": "花瓣", "heart": "心形", "straight": "直线", "wave": "波浪"}
|
||||
|
||||
def get(img, style, side):
|
||||
for r in results:
|
||||
if r["img"] == img and r["style"] == style and r["side"] == side:
|
||||
return r
|
||||
return None
|
||||
|
||||
# 总览统计(>60s 视为冷启动/异常,不进均值)
|
||||
total = len(results)
|
||||
ok = sum(1 for r in results if r["ok"])
|
||||
COLD_S = 60.0
|
||||
|
||||
def avg(seq):
|
||||
seq = [x for x in seq if x is not None]
|
||||
return sum(seq) / len(seq) if seq else 0
|
||||
|
||||
def times(side, img=None):
|
||||
out = []
|
||||
for r in results:
|
||||
if not r["ok"] or r["side"] != side:
|
||||
continue
|
||||
if img is not None and r["img"] != img:
|
||||
continue
|
||||
if r["elapsed"] >= COLD_S:
|
||||
continue
|
||||
out.append(r["elapsed"])
|
||||
return out
|
||||
|
||||
cold_n = sum(1 for r in results if r["ok"] and r["elapsed"] >= COLD_S)
|
||||
side_avgs = {sid: avg(times(sid)) for sid, *_ in SIDES}
|
||||
all_times = [t for sid, *_ in SIDES for t in times(sid)]
|
||||
max_bar = max(all_times + [1])
|
||||
|
||||
# 构造速度对比图数据:每图 × 三档
|
||||
chart_rows = []
|
||||
for img in IMAGES:
|
||||
chart_rows.append((img, IMG_LONGSIDE[img], [avg(times(sid, img)) for sid, *_ in SIDES]))
|
||||
|
||||
def bar_svg():
|
||||
bar_h = 22
|
||||
gap = 12
|
||||
label_w = 78
|
||||
chart_w = 760
|
||||
n_bars = len(SIDES)
|
||||
rows = len(chart_rows)
|
||||
h = rows * (bar_h * n_bars + gap) + 40
|
||||
parts = [f'<svg viewBox="0 0 {label_w + chart_w + 80} {h}" class="chart">']
|
||||
y = 10
|
||||
for img, longside, avgs in chart_rows:
|
||||
base = avgs[0] if avgs and avgs[0] else 1
|
||||
for i, ((sid, label, color, _), a) in enumerate(zip(SIDES, avgs)):
|
||||
yi = y + i * bar_h
|
||||
w = int(a / max_bar * chart_w) if a else 0
|
||||
warn = "⚠" if (i > 0 and a > base * 1.8) else ""
|
||||
parts.append(f'<rect x="{label_w}" y="{yi}" width="{w}" height="{bar_h-4}" rx="3" fill="{color}"/>')
|
||||
parts.append(f'<text x="{label_w + w + 6}" y="{yi + bar_h - 10}" class="barlabel">{a:.1f}s {warn}</text>')
|
||||
parts.append(f'<text x="{label_w-8}" y="{yi + bar_h - 10}" class="rowlabel" text-anchor="end">{html.escape(label)}</text>')
|
||||
parts.append(f'<text x="0" y="{y + bar_h - 2}" class="imglabel">{img}<tspan class="imgside">长边{longside}</tspan></text>')
|
||||
y += bar_h * n_bars + gap
|
||||
parts.append("</svg>")
|
||||
return "".join(parts)
|
||||
|
||||
def img_cell(r, cls):
|
||||
if not r:
|
||||
return f'<td class="{cls} fail">缺失</td>'
|
||||
fname = r["key"] + ".jpg"
|
||||
status = "✅" if r["ok"] else "❌"
|
||||
t = f'{r["elapsed"]}s'
|
||||
dim = f'{r["out_w"]}×{r["out_h"]}'
|
||||
err = f'<div class="err">{html.escape(r["err"])}</div>' if r["err"] else ""
|
||||
img_tag = (f'<img loading="lazy" src="{fname}" onclick="openImg(this.src)" alt="{html.escape(r["key"])}">'
|
||||
if r["ok"] else '<div class="noimg">无图</div>')
|
||||
return (f'<td class="{cls}"><div class="thumb">{img_tag}</div>'
|
||||
f'<div class="meta">{status} {t} · {dim}</div>{err}</td>')
|
||||
|
||||
def compare_cards():
|
||||
out = []
|
||||
for img in IMAGES:
|
||||
out.append(f'<div class="card"><div class="card-h">📷 {html.escape(img)} <span class="tag">原图长边 {IMG_LONGSIDE[img]}px</span></div><div class="card-b">')
|
||||
heads = "".join(f'<th class="{cls}">{html.escape(label)}</th>' for _, label, _, cls in SIDES)
|
||||
out.append(f'<table class="cmp"><thead><tr><th>发型</th>{heads}</tr></thead><tbody>')
|
||||
for style in STYLES:
|
||||
sc = STYLE_CN[style]
|
||||
cells = "".join(img_cell(get(img, style, sid), cls) for sid, _, _, cls in SIDES)
|
||||
out.append(f'<tr><td class="sname">{html.escape(style)}<span>{sc}</span></td>{cells}</tr>')
|
||||
out.append("</tbody></table></div></div>")
|
||||
return "".join(out)
|
||||
|
||||
# 结论
|
||||
small = [img for img in IMAGES if IMG_LONGSIDE[img] <= 1100]
|
||||
large = [img for img in IMAGES if IMG_LONGSIDE[img] > 1100]
|
||||
def ratio_range(num_side, den_side="default896"):
|
||||
ratios = []
|
||||
for img in large:
|
||||
den = avg(times(den_side, img))
|
||||
num = avg(times(num_side, img))
|
||||
if den:
|
||||
ratios.append(num / den)
|
||||
if not ratios:
|
||||
return 0, 0
|
||||
return min(ratios), max(ratios)
|
||||
|
||||
r1024_lo, r1024_hi = ratio_range("side1024")
|
||||
r0_lo, r0_hi = ratio_range("origin0")
|
||||
cold_note = f"冷启动 {cold_n} 次(≥{COLD_S:.0f}s)已从均值剔除。" if cold_n else ""
|
||||
conclusion = (
|
||||
f"原图长边 ≤ 1100({'/'.join(small)})时三档耗时接近;"
|
||||
f"长边 > 1600({'/'.join(large)})时相对默认896:"
|
||||
f"<b>1024 约 {r1024_lo:.1f}~{r1024_hi:.1f}×</b>,"
|
||||
f"<b>原图直送约 {r0_lo:.1f}~{r0_hi:.1f}×</b>。"
|
||||
f"{cold_note}画质对比见下方三列并排,点击可放大。"
|
||||
)
|
||||
|
||||
avg896 = side_avgs.get("default896", 0)
|
||||
avg1024 = side_avgs.get("side1024", 0)
|
||||
avg0 = side_avgs.get("origin0", 0)
|
||||
ratio1024 = avg1024 / avg896 if avg896 else 0
|
||||
ratio0 = avg0 / avg896 if avg896 else 0
|
||||
|
||||
legend = "".join(
|
||||
f'<span><i style="background:{color}"></i>{html.escape(label)}</span>'
|
||||
for _, label, color, _ in SIDES
|
||||
)
|
||||
|
||||
html_doc = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>接口2 分辨率对比测试报告</title>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 20px; }}
|
||||
.wrap {{ max-width: 1400px; margin: 0 auto; }}
|
||||
h1 {{ font-size: 24px; margin-bottom: 4px; }}
|
||||
.sub {{ color: #6b7280; font-size: 13px; margin-bottom: 20px; }}
|
||||
.summary {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 24px; }}
|
||||
.stat {{ background: #fff; border-radius: 10px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }}
|
||||
.stat .num {{ font-size: 26px; font-weight: 700; }}
|
||||
.stat .lbl {{ font-size: 12px; color: #6b7280; margin-top: 2px; }}
|
||||
.stat.d .num {{ color: #2563eb; }}
|
||||
.stat.m .num {{ color: #10b981; }}
|
||||
.stat.o .num {{ color: #f59e0b; }}
|
||||
.card {{ background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.07); margin-bottom: 20px; overflow: hidden; }}
|
||||
.card-h {{ padding: 12px 18px; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; }}
|
||||
.card-h .tag {{ font-size: 11px; font-weight: 500; color: #6b7280; background: #f3f4f6; padding: 2px 8px; border-radius: 10px; }}
|
||||
.card-b {{ padding: 16px; }}
|
||||
.chart {{ width: 100%; height: auto; max-width: 920px; }}
|
||||
.barlabel {{ font-size: 12px; fill: #374151; font-weight: 600; }}
|
||||
.rowlabel {{ font-size: 11px; fill: #6b7280; }}
|
||||
.imglabel {{ font-size: 14px; fill: #111; font-weight: 700; }}
|
||||
.imgside {{ font-size: 10px; fill: #9ca3af; font-weight: 400; }}
|
||||
table.cmp {{ width: 100%; border-collapse: collapse; table-layout: fixed; }}
|
||||
table.cmp th {{ font-size: 12px; color: #6b7280; font-weight: 600; padding: 8px; text-align: center; border-bottom: 2px solid #f0f0f0; }}
|
||||
table.cmp td {{ padding: 8px; border-bottom: 1px solid #f6f6f6; vertical-align: top; text-align: center; }}
|
||||
table.cmp th.d, table.cmp td.d {{ background: #eff6ff; }}
|
||||
table.cmp th.m, table.cmp td.m {{ background: #ecfdf5; }}
|
||||
table.cmp th.o, table.cmp td.o {{ background: #fffbeb; }}
|
||||
td.sname {{ font-weight: 600; text-align: left; width: 90px; }}
|
||||
td.sname span {{ display: block; font-size: 11px; color: #9ca3af; font-weight: 400; }}
|
||||
.thumb {{ background: #222; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||
.thumb img {{ width: 100%; height: 200px; object-fit: contain; display: block; }}
|
||||
.noimg {{ color: #d1d5db; font-size: 12px; padding: 40px 0; }}
|
||||
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||
.err {{ font-size: 10px; color: #dc2626; margin-top: 2px; }}
|
||||
.note {{ background: #fef3c7; border-left: 3px solid #f59e0b; padding: 12px 16px; border-radius: 6px; font-size: 13px; margin-bottom: 20px; }}
|
||||
.legend {{ display: flex; gap: 20px; font-size: 12px; color: #6b7280; margin-bottom: 12px; flex-wrap: wrap; }}
|
||||
.legend span {{ display: inline-flex; align-items: center; gap: 5px; }}
|
||||
.legend i {{ width: 12px; height: 12px; border-radius: 2px; display: inline-block; }}
|
||||
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.9); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 30px; }}
|
||||
.overlay.active {{ display: flex; }}
|
||||
.overlay img {{ max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 4px; }}
|
||||
@media (max-width: 900px) {{
|
||||
.summary {{ grid-template-columns: repeat(2, 1fr); }}
|
||||
.thumb img {{ height: 160px; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>接口2 生发 · 分辨率对比测试报告</h1>
|
||||
<p class="sub">POST /api/v1/hair/grow · female · 4 张图 × 5 发型 × 3 档分辨率 = {total} 次 · 串行</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="stat"><div class="num">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||
<div class="stat d"><div class="num">{avg896:.1f}s</div><div class="lbl">默认896 平均</div></div>
|
||||
<div class="stat m"><div class="num">{avg1024:.1f}s</div><div class="lbl">1024 平均 · ×{ratio1024:.2f}</div></div>
|
||||
<div class="stat o"><div class="num">{avg0:.1f}s</div><div class="lbl">原图直送 平均 · ×{ratio0:.2f}</div></div>
|
||||
<div class="stat"><div class="num" style="color:{'#dc2626' if ratio0>1.3 else '#16a34a'}">×{ratio0:.2f}</div><div class="lbl">原图/默认 倍率</div></div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>结论:</b>{conclusion}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h">平均耗时对比(按图分组,单位:秒)</div>
|
||||
<div class="card-b">
|
||||
<div class="legend">{legend}</div>
|
||||
{bar_svg()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="font-size:18px;margin:8px 0 14px">画质对比(左→右:默认896 · 1024 · 原图直送)</h2>
|
||||
{compare_cards()}
|
||||
|
||||
</div>
|
||||
<div class="overlay" id="overlay" onclick="this.classList.remove('active')">
|
||||
<img id="overlayImg" src="">
|
||||
</div>
|
||||
<script>
|
||||
function openImg(src) {{
|
||||
document.getElementById('overlayImg').src = src;
|
||||
document.getElementById('overlay').classList.add('active');
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(REPORT_HTML, "w") as f:
|
||||
f.write(html_doc)
|
||||
# 同步首页
|
||||
with open(os.path.join(OUT, "index.html"), "w") as f:
|
||||
f.write(html_doc)
|
||||
print(f"已生成: {REPORT_HTML}")
|
||||
print(f"图片目录: {OUT}")
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=接口2 分辨率对比测试报告 HTTP 服务 (8848)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/home/ubuntu/hair/image/compare_test/out
|
||||
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8848 --bind 0.0.0.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
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 |
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>测试报告索引</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; padding: 40px 20px; }
|
||||
.wrap { max-width: 720px; margin: 0 auto; }
|
||||
h1 { font-size: 24px; margin-bottom: 4px; }
|
||||
.sub { color: #6b7280; font-size: 13px; margin-bottom: 28px; }
|
||||
.card { display: block; background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.07); padding: 20px 24px; margin-bottom: 14px; text-decoration: none; color: inherit; transition: .15s; border-left: 4px solid #2563eb; }
|
||||
.card:hover { transform: translateX(4px); box-shadow: 0 4px 12px rgba(0,0,0,.1); }
|
||||
.card .title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
||||
.card .desc { font-size: 13px; color: #6b7280; }
|
||||
.card .url { font-size: 11px; color: #9ca3af; margin-top: 6px; font-family: monospace; }
|
||||
.card.wave { border-left-color: #16a34a; }
|
||||
.card.v2 { border-left-color: #f59e0b; }
|
||||
.card.v1 { border-left-color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>📊 测试报告索引</h1>
|
||||
<p class="sub">接口2 / 接口5 分辨率对比测试报告合集</p>
|
||||
|
||||
<a class="card wave" href="wave/">
|
||||
<div class="title">💇 wave发型 · 5档分辨率对比(最新)</div>
|
||||
<div class="desc">21图 × 5档(原图/1024/896/768/640) = 105次 · wave发型 · female</div>
|
||||
<div class="url">/wave/</div>
|
||||
</a>
|
||||
|
||||
<a class="card v2" href="v2/">
|
||||
<div class="title">💄 5图 · 5档分辨率对比 v2</div>
|
||||
<div class="desc">5图 × 3发型(花瓣/心形/波浪) × 5档 = 75次 · female</div>
|
||||
<div class="url">/v2/</div>
|
||||
</a>
|
||||
|
||||
<a class="card v1" href="v1/">
|
||||
<div class="title">💄 4图 · 2档分辨率对比 v1</div>
|
||||
<div class="desc">4图 × 5发型 × 2档(默认896/原图) = 40次 · female</div>
|
||||
<div class="url">/v1/</div>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=测试报告统一HTTP服务 (8850, 路径区分: /wave /v2 /v1)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/home/ubuntu/hair/image/reports
|
||||
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8850 --bind 0.0.0.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1 @@
|
||||
/home/ubuntu/hair/image/compare_test/out
|
||||
@@ -0,0 +1 @@
|
||||
/home/ubuntu/hair/image/res_test/out
|
||||
@@ -0,0 +1 @@
|
||||
/home/ubuntu/hair/image/wave_test/out
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""接口2 female 分辨率对比测试 v2: 5图 × 3发型 × 5档 = 75次。
|
||||
串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
|
||||
支持断点续跑(progress.json)。
|
||||
"""
|
||||
import base64, csv, json, os, sys, time, io
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||
TOKEN = "dev-shared-secret-2026"
|
||||
TIMEOUT = 600
|
||||
OUT = "/home/ubuntu/hair/image/res_test/out"
|
||||
PROGRESS = "/home/ubuntu/hair/image/res_test/progress.json"
|
||||
|
||||
IMG_DIR = "/home/ubuntu/hair/image"
|
||||
IMAGES = [
|
||||
("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
|
||||
("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
|
||||
("qwer", f"{IMG_DIR}/qwer.jpg"),
|
||||
("asdf", f"{IMG_DIR}/asdf.jpg"),
|
||||
("girl7", f"{IMG_DIR}/girl_img/girl7.jpg"),
|
||||
]
|
||||
# female: 2=flower(花瓣) 3=heart(心形) 5=wave(波浪)
|
||||
STYLES = [(2, "flower"), (3, "heart"), (5, "wave")]
|
||||
# 5档: 原图(0) / 1024 / 896 / 768 / 640
|
||||
SIDES = [
|
||||
("origin", 0),
|
||||
("s1024", 1024),
|
||||
("s896", 896),
|
||||
("s768", 768),
|
||||
("s640", 640),
|
||||
]
|
||||
|
||||
def load_progress():
|
||||
if os.path.exists(PROGRESS):
|
||||
try:
|
||||
return json.load(open(PROGRESS))
|
||||
except Exception:
|
||||
pass
|
||||
return {"done": [], "results": []}
|
||||
|
||||
def save_progress(prog):
|
||||
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||
|
||||
def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
|
||||
key = f"{img_name}_{style_key}_{side_name}"
|
||||
with open(img_path, "rb") as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode()
|
||||
data = {
|
||||
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||
"gender": "female",
|
||||
"hair_style": str(style_idx),
|
||||
"redraw_max_side": str(side_val), # 含原图档=0
|
||||
}
|
||||
t0 = time.time()
|
||||
rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
|
||||
"side": side_name, "side_val": side_val, "ok": False,
|
||||
"elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0, "bytes": 0}
|
||||
try:
|
||||
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
d = r.json()
|
||||
if d.get("code") != 0:
|
||||
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||
return rec
|
||||
results = (d.get("data") or {}).get("results") or []
|
||||
if not results:
|
||||
rec["err"] = "空结果"
|
||||
return rec
|
||||
it = results[0]
|
||||
grown = it.get("grown_image_base64")
|
||||
if not grown:
|
||||
rec["err"] = "无生发图(重绘失败/OOM?)"
|
||||
return rec
|
||||
raw = base64.b64decode(grown)
|
||||
im = Image.open(io.BytesIO(raw))
|
||||
rec["out_w"], rec["out_h"] = im.size
|
||||
out_path = f"{OUT}/{key}.jpg"
|
||||
with open(out_path, "wb") as fo:
|
||||
fo.write(raw)
|
||||
rec["ok"] = True
|
||||
rec["bytes"] = len(raw)
|
||||
except requests.exceptions.Timeout:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||
except Exception as e:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||
return rec
|
||||
|
||||
def main():
|
||||
prog = load_progress()
|
||||
done_keys = set(prog["done"])
|
||||
total = len(IMAGES) * len(STYLES) * len(SIDES)
|
||||
print(f"=== 接口2 female 分辨率对比测试 v2 ===")
|
||||
print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
|
||||
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||
|
||||
idx = 0
|
||||
for img_name, img_path in IMAGES:
|
||||
for style_idx, style_key in STYLES:
|
||||
for side_name, side_val in SIDES:
|
||||
idx += 1
|
||||
key = f"{img_name}_{style_key}_{side_name}"
|
||||
if key in done_keys:
|
||||
print(f"[{idx}/{total}] ⏭ 跳过 {key}")
|
||||
continue
|
||||
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||
rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
|
||||
prog["results"].append(rec)
|
||||
prog["done"].append(key)
|
||||
save_progress(prog)
|
||||
if rec["ok"]:
|
||||
print(f"✅ {rec['elapsed']}s {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||
else:
|
||||
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("汇总")
|
||||
print("=" * 70)
|
||||
write_report(prog["results"])
|
||||
print(f"\n结果图: {OUT}/")
|
||||
print(f"CSV: {OUT}/report.csv JSON: {OUT}/report.json")
|
||||
|
||||
def write_report(results):
|
||||
with open(f"{OUT}/report.csv", "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["img", "style", "side", "side_val", "ok", "elapsed_s", "out_w", "out_h", "bytes", "err"])
|
||||
for r in results:
|
||||
w.writerow([r["img"], r["style"], r["side"], r["side_val"], r["ok"],
|
||||
r["elapsed"], r["out_w"], r["out_h"], r["bytes"], r["err"]])
|
||||
json.dump(results, open(f"{OUT}/report.json", "w"), ensure_ascii=False, indent=1)
|
||||
|
||||
# 控制台速度表:按 图×发型 分行,5档列
|
||||
print("\n--- 速度对比(秒)---")
|
||||
print(f"{'图·发型':<18}{'原图0':>8}{'1024':>8}{'896':>8}{'768':>8}{'640':>8}")
|
||||
for img_name, _ in IMAGES:
|
||||
for _, style_key in STYLES:
|
||||
cells = []
|
||||
for side_name, _ in SIDES:
|
||||
r = next((x for x in results if x["img"] == img_name and x["style"] == style_key and x["side"] == side_name), None)
|
||||
cells.append(f"{r['elapsed']}✓" if r and r["ok"] else (f"{r['elapsed']}✗" if r else "-"))
|
||||
print(f"{img_name+'·'+style_key:<18}{cells[0]:>8}{cells[1]:>8}{cells[2]:>8}{cells[3]:>8}{cells[4]:>8}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 v2 分辨率对比报告:每行=原图+5档,按图×发型组织15行。"""
|
||||
import json, os, html
|
||||
|
||||
OUT = "/home/ubuntu/hair/image/res_test/out"
|
||||
REPORT = os.path.join(OUT, "report.html")
|
||||
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||
|
||||
IMAGES = ["girl2", "girl5", "qwer", "asdf", "girl7"]
|
||||
STYLES = [("flower", "花瓣"), ("heart", "心形"), ("wave", "波浪")]
|
||||
SIDES = [("origin", "原图直送", 0), ("s1024", "1024", 1024),
|
||||
("s896", "896", 896), ("s768", "768", 768), ("s640", "640", 640)]
|
||||
LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666, "girl7": 925}
|
||||
|
||||
def get(img, style, side):
|
||||
for r in results:
|
||||
if r["img"] == img and r["style"] == style and r["side"] == side:
|
||||
return r
|
||||
return None
|
||||
|
||||
# 统计
|
||||
total = len(results)
|
||||
ok = sum(1 for r in results if r["ok"])
|
||||
|
||||
# 每档平均耗时(排除冷启动异常值:girl2_flower_origin=135s 明显是冷启动)
|
||||
def avg(side, exclude_first_cold=False):
|
||||
ts = []
|
||||
for r in results:
|
||||
if r["side"] == side and r["ok"]:
|
||||
if exclude_first_cold and r["key"] == "girl2_flower_origin":
|
||||
continue # 跳过冷启动
|
||||
ts.append(r["elapsed"])
|
||||
return sum(ts) / len(ts) if ts else 0
|
||||
|
||||
avg_origin = avg("origin", exclude_first_cold=True)
|
||||
avg_1024 = avg("s1024")
|
||||
avg_896 = avg("s896")
|
||||
avg_768 = avg("s768")
|
||||
avg_640 = avg("s640")
|
||||
avgs = [("origin", "原图直送", avg_origin, 0),
|
||||
("s1024", "1024", avg_1024, 1024),
|
||||
("s896", "896", avg_896, 896),
|
||||
("s768", "768", avg_768, 768),
|
||||
("s640", "640", avg_640, 640)]
|
||||
|
||||
# 速度色阶:以全部耗时的 min-max 映射颜色(绿→黄→红)
|
||||
all_t = sorted([r["elapsed"] for r in results if r["ok"] and r["key"] != "girl2_flower_origin"])
|
||||
tmin, tmax = all_t[0], all_t[-1]
|
||||
def speed_color(t):
|
||||
if tmax == tmin:
|
||||
return "#16a34a"
|
||||
ratio = (t - tmin) / (tmax - tmin) # 0=最快(绿) 1=最慢(红)
|
||||
if ratio < 0.33:
|
||||
return "#16a34a" # 绿
|
||||
elif ratio < 0.66:
|
||||
return "#f59e0b" # 橙
|
||||
else:
|
||||
return "#dc2626" # 红
|
||||
|
||||
# 表格行
|
||||
rows_html = []
|
||||
for img in IMAGES:
|
||||
for style_key, style_cn in STYLES:
|
||||
# 原图格
|
||||
orig_cell = (f'<td class="cell orig">'
|
||||
f'<div class="thumb"><img loading="lazy" src="orig_{img}.jpg" '
|
||||
f'onclick="openImg(this.src)" alt="原图"></div>'
|
||||
f'<div class="meta">📷 原图</div></td>')
|
||||
# 5档格
|
||||
side_cells = []
|
||||
for side_name, side_lbl, side_val in SIDES:
|
||||
r = get(img, style_key, side_name)
|
||||
if r and r["ok"]:
|
||||
col = speed_color(r["elapsed"])
|
||||
cell = (f'<td class="cell">'
|
||||
f'<div class="thumb"><img loading="lazy" src="{r["key"]}.jpg" '
|
||||
f'onclick="openImg(this.src)" alt="{html.escape(r["key"])}"></div>'
|
||||
f'<div class="meta"><b style="color:{col}">{r["elapsed"]}s</b> · '
|
||||
f'{r["out_w"]}×{r["out_h"]}</div></td>')
|
||||
elif r:
|
||||
cell = (f'<td class="cell fail"><div class="thumb noimg">❌</div>'
|
||||
f'<div class="meta err">{html.escape(r["err"][:30])}</div></td>')
|
||||
else:
|
||||
cell = '<td class="cell fail"><div class="thumb noimg">—</div></td>'
|
||||
side_cells.append(cell)
|
||||
row_label = (f'<td class="rowlabel">'
|
||||
f'<div class="rimg">{html.escape(img)}</div>'
|
||||
f'<div class="rs">{html.escape(style_cn)}</div>'
|
||||
f'<div class="rl">长边 {LONGSIDE[img]}</div></td>')
|
||||
rows_html.append("<tr>" + row_label + orig_cell + "".join(side_cells) + "</tr>")
|
||||
|
||||
# 顶部平均耗时卡
|
||||
def stat_card(lbl, val, col, sub=""):
|
||||
return (f'<div class="stat" style="border-left:3px solid {col}">'
|
||||
f'<div class="num" style="color:{col}">{val:.1f}s</div>'
|
||||
f'<div class="lbl">{lbl}{sub}</div></div>')
|
||||
|
||||
stat_cards = "".join([
|
||||
stat_card("原图直送", avg_origin, "#dc2626", "<br><span class='dim'>排除冷启动</span>"),
|
||||
stat_card("1024", avg_1024, "#f59e0b"),
|
||||
stat_card("896 (默认)", avg_896, "#2563eb"),
|
||||
stat_card("768", avg_768, "#16a34a"),
|
||||
stat_card("640", avg_640, "#0d9488"),
|
||||
])
|
||||
|
||||
html_doc = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>接口2 分辨率对比测试 v2</title>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 16px; }}
|
||||
.wrap {{ max-width: 100%; margin: 0 auto; }}
|
||||
h1 {{ font-size: 22px; margin-bottom: 4px; }}
|
||||
.sub {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; }}
|
||||
.stats {{ display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }}
|
||||
.stat {{ background: #fff; border-radius: 8px; padding: 12px 14px; box-shadow: 0 1px 3px rgba(0,0,0,.06); flex: 1; min-width: 110px; }}
|
||||
.stat .num {{ font-size: 22px; font-weight: 700; }}
|
||||
.stat .lbl {{ font-size: 11px; color: #6b7280; margin-top: 2px; }}
|
||||
.stat .dim {{ color: #9ca3af; font-size: 10px; }}
|
||||
.summary-bar {{ background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); font-size: 13px; }}
|
||||
.summary-bar b {{ color: #dc2626; }}
|
||||
.table-wrap {{ overflow-x: auto; background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.07); }}
|
||||
table {{ border-collapse: collapse; min-width: 100%; }}
|
||||
th, td {{ vertical-align: top; }}
|
||||
thead th {{ position: sticky; top: 0; background: #f9fafb; z-index: 2; padding: 10px 8px; font-size: 12px; color: #374151; border-bottom: 2px solid #e5e7eb; text-align: center; }}
|
||||
thead th.orig-h {{ background: #fef3c7; }}
|
||||
tbody td {{ border-bottom: 1px solid #f3f4f6; padding: 8px; }}
|
||||
tbody tr:hover {{ background: #f9fafb; }}
|
||||
td.rowlabel {{ text-align: left; padding: 8px 12px; position: sticky; left: 0; background: #fff; z-index: 1; min-width: 90px; box-shadow: 2px 0 4px rgba(0,0,0,.04); }}
|
||||
tbody tr:hover td.rowlabel {{ background: #f9fafb; }}
|
||||
.rimg {{ font-weight: 700; font-size: 14px; }}
|
||||
.rs {{ font-size: 12px; color: #6b7280; }}
|
||||
.rl {{ font-size: 10px; color: #9ca3af; margin-top: 2px; }}
|
||||
.cell {{ width: 180px; min-width: 180px; text-align: center; }}
|
||||
.cell.orig {{ width: 180px; background: #fffbeb; }}
|
||||
.thumb {{ background: #1f2937; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||
.thumb img {{ width: 100%; height: 240px; object-fit: contain; display: block; }}
|
||||
.thumb.noimg {{ color: #d1d5db; font-size: 16px; padding: 100px 0; text-align: center; }}
|
||||
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||
.meta.err {{ color: #dc2626; }}
|
||||
.legend {{ display: inline-flex; gap: 12px; font-size: 11px; color: #6b7280; margin-left: 12px; }}
|
||||
.legend span {{ display: inline-flex; align-items: center; gap: 4px; }}
|
||||
.legend i {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
|
||||
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 24px; }}
|
||||
.overlay.active {{ display: flex; }}
|
||||
.overlay img {{ max-width: 96%; max-height: 96%; object-fit: contain; border-radius: 4px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>💄 接口2 生发 · 5档分辨率对比报告</h1>
|
||||
<p class="sub">POST /api/v1/hair/grow · female · 5 图 × 3 发型(花瓣/心形/波浪) × 5 档分辨率 = 75 次 · 串行 · 4090 (24G)</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat" style="border-left:3px solid #16a34a"><div class="num" style="color:#16a34a">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||
{stat_cards}
|
||||
</div>
|
||||
|
||||
<div class="summary-bar">
|
||||
📊 <b>结论:</b>耗时随送图分辨率单调下降。<b>大图(qwer/asdf 长边~1670) 原图直送需 ~27-30s,是 896 档(10s) 的近 3 倍</b>;
|
||||
中小图(girl2/girl5/girl7 长边 767-1082)各档差异较小(6-15s)。
|
||||
<b>4090 24G 全程无 OOM</b>,75/75 成功。<b>画质对比</b>见下表(横向滑动),点击任意图可放大。
|
||||
<span class="legend">
|
||||
<span><i style="background:#16a34a"></i>快(<{tmin+ (tmax-tmin)*0.33:.0f}s)</span>
|
||||
<span><i style="background:#f59e0b"></i>中等</span>
|
||||
<span><i style="background:#dc2626"></i>慢(>{tmin+ (tmax-tmin)*0.66:.0f}s)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>图 · 发型</th>
|
||||
<th class="orig-h">📷 原图</th>
|
||||
<th>原图直送 (0)<br><span class="dim">不缩放</span></th>
|
||||
<th>1024</th>
|
||||
<th>896<br><span class="dim">默认</span></th>
|
||||
<th>768</th>
|
||||
<th>640</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{"".join(rows_html)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" id="overlay" onclick="this.classList.remove('active')"><img id="overlayImg" src=""></div>
|
||||
<script>
|
||||
function openImg(src) {{
|
||||
document.getElementById('overlayImg').src = src;
|
||||
document.getElementById('overlay').classList.add('active');
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
with open(REPORT, "w") as f:
|
||||
f.write(html_doc)
|
||||
print(f"已生成: {REPORT}")
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=接口2 5档分辨率对比报告 v2 (8849)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/home/ubuntu/hair/image/res_test/out
|
||||
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8849 --bind 0.0.0.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""接口2 female wave发型 分辨率对比测试 v3: 21图 × 5档 = 105次。
|
||||
串行执行,记录耗时与成败,结果图按 图_档位 命名保存。支持断点续跑。
|
||||
"""
|
||||
import base64, csv, json, os, time, io
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
API = "http://127.0.0.1:8187/api/v1/hair/grow"
|
||||
TOKEN = "dev-shared-secret-2026"
|
||||
TIMEOUT = 600
|
||||
OUT = "/home/ubuntu/hair/image/wave_test/out"
|
||||
PROGRESS = "/home/ubuntu/hair/image/wave_test/progress.json"
|
||||
|
||||
IMG_DIR = "/home/ubuntu/hair/image"
|
||||
# 21张图:19张girl_img + asdf + qwer
|
||||
IMAGES = []
|
||||
for f in sorted(os.listdir(os.path.join(IMG_DIR, "girl_img"))):
|
||||
if f.lower().endswith((".jpg", ".jpeg", ".png")):
|
||||
IMAGES.append((os.path.splitext(f)[0], os.path.join(IMG_DIR, "girl_img", f)))
|
||||
IMAGES.append(("asdf", os.path.join(IMG_DIR, "asdf.jpg")))
|
||||
IMAGES.append(("qwer", os.path.join(IMG_DIR, "qwer.jpg")))
|
||||
|
||||
# wave = female hair_style 5
|
||||
STYLE_IDX = 5
|
||||
# 5档: 原图(0) / 1024 / 896 / 768 / 640
|
||||
SIDES = [
|
||||
("origin", 0),
|
||||
("s1024", 1024),
|
||||
("s896", 896),
|
||||
("s768", 768),
|
||||
("s640", 640),
|
||||
]
|
||||
|
||||
def load_progress():
|
||||
if os.path.exists(PROGRESS):
|
||||
try:
|
||||
return json.load(open(PROGRESS))
|
||||
except Exception:
|
||||
pass
|
||||
return {"done": [], "results": []}
|
||||
|
||||
def save_progress(prog):
|
||||
json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
|
||||
|
||||
def run_one(img_name, img_path, side_name, side_val):
|
||||
key = f"{img_name}_{side_name}"
|
||||
with open(img_path, "rb") as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode()
|
||||
data = {
|
||||
"image_base64": "data:image/jpeg;base64," + img_b64,
|
||||
"gender": "female",
|
||||
"hair_style": str(STYLE_IDX),
|
||||
"redraw_max_side": str(side_val),
|
||||
}
|
||||
t0 = time.time()
|
||||
rec = {"key": key, "img": img_name, "side": side_name, "side_val": side_val,
|
||||
"ok": False, "elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0, "bytes": 0}
|
||||
try:
|
||||
r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
d = r.json()
|
||||
if d.get("code") != 0:
|
||||
rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
|
||||
return rec
|
||||
results = (d.get("data") or {}).get("results") or []
|
||||
if not results:
|
||||
rec["err"] = "空结果"
|
||||
return rec
|
||||
it = results[0]
|
||||
grown = it.get("grown_image_base64")
|
||||
if not grown:
|
||||
rec["err"] = "无生发图(重绘失败/OOM?)"
|
||||
return rec
|
||||
raw = base64.b64decode(grown)
|
||||
im = Image.open(io.BytesIO(raw))
|
||||
rec["out_w"], rec["out_h"] = im.size
|
||||
out_path = f"{OUT}/{key}.jpg"
|
||||
with open(out_path, "wb") as fo:
|
||||
fo.write(raw)
|
||||
rec["ok"] = True
|
||||
rec["bytes"] = len(raw)
|
||||
except requests.exceptions.Timeout:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"超时(>{TIMEOUT}s)"
|
||||
except Exception as e:
|
||||
rec["elapsed"] = round(time.time() - t0, 1)
|
||||
rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
|
||||
return rec
|
||||
|
||||
def main():
|
||||
prog = load_progress()
|
||||
done_keys = set(prog["done"])
|
||||
total = len(IMAGES) * len(SIDES)
|
||||
print(f"=== 接口2 female wave 分辨率对比测试 v3 ===")
|
||||
print(f"矩阵: {len(IMAGES)}图 × {len(SIDES)}档 = {total} 次 (发型固定 wave)")
|
||||
print(f"已跳过 {len(done_keys)} 个已完成项\n")
|
||||
|
||||
idx = 0
|
||||
for img_name, img_path in IMAGES:
|
||||
for side_name, side_val in SIDES:
|
||||
idx += 1
|
||||
key = f"{img_name}_{side_name}"
|
||||
if key in done_keys:
|
||||
print(f"[{idx}/{total}] ⏭ 跳过 {key}")
|
||||
continue
|
||||
print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
|
||||
rec = run_one(img_name, img_path, side_name, side_val)
|
||||
prog["results"].append(rec)
|
||||
prog["done"].append(key)
|
||||
save_progress(prog)
|
||||
if rec["ok"]:
|
||||
print(f"✅ {rec['elapsed']}s {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
|
||||
else:
|
||||
print(f"❌ {rec['elapsed']}s {rec['err']}")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("汇总")
|
||||
print("=" * 70)
|
||||
write_report(prog["results"])
|
||||
print(f"\n结果图: {OUT}/")
|
||||
print(f"CSV: {OUT}/report.csv JSON: {OUT}/report.json")
|
||||
|
||||
def write_report(results):
|
||||
with open(f"{OUT}/report.csv", "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["img", "side", "side_val", "ok", "elapsed_s", "out_w", "out_h", "bytes", "err"])
|
||||
for r in results:
|
||||
w.writerow([r["img"], r["side"], r["side_val"], r["ok"],
|
||||
r["elapsed"], r["out_w"], r["out_h"], r["bytes"], r["err"]])
|
||||
json.dump(results, open(f"{OUT}/report.json", "w"), ensure_ascii=False, indent=1)
|
||||
|
||||
# 各档平均耗时
|
||||
print("\n--- 各档平均耗时(秒)---")
|
||||
for side_name, side_val in SIDES:
|
||||
ts = [r["elapsed"] for r in results if r["side"] == side_name and r["ok"]]
|
||||
if ts:
|
||||
avg = sum(ts) / len(ts)
|
||||
print(f" {side_name:8s} (={side_val:>4}): 平均 {avg:5.1f}s [{min(ts):.1f}~{max(ts):.1f}] 成功 {len(ts)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 v3 wave发型 分辨率对比报告:每行=原图+5档,21行;含各档耗时统计。"""
|
||||
import json, os, html
|
||||
|
||||
OUT = "/home/ubuntu/hair/image/wave_test/out"
|
||||
REPORT = os.path.join(OUT, "report.html")
|
||||
results = json.load(open(os.path.join(OUT, "report.json")))
|
||||
|
||||
SIDES = [("origin", "原图直送", 0), ("s1024", "1024", 1024),
|
||||
("s896", "896", 896), ("s768", "768", 768), ("s640", "640", 640)]
|
||||
|
||||
# 图片顺序(与测试脚本一致):girl_img 排序 + asdf + qwer
|
||||
IMG_DIR = "/home/ubuntu/hair/image"
|
||||
IMAGES = []
|
||||
for f in sorted(os.listdir(os.path.join(IMG_DIR, "girl_img"))):
|
||||
if f.lower().endswith((".jpg", ".jpeg", ".png")):
|
||||
IMAGES.append(os.path.splitext(f)[0])
|
||||
IMAGES.append("asdf")
|
||||
IMAGES.append("qwer")
|
||||
|
||||
def get(img, side):
|
||||
for r in results:
|
||||
if r["img"] == img and r["side"] == side:
|
||||
return r
|
||||
return None
|
||||
|
||||
# 统计
|
||||
total = len(results)
|
||||
ok = sum(1 for r in results if r["ok"])
|
||||
|
||||
# 各档统计
|
||||
def avg(side):
|
||||
ts = [r["elapsed"] for r in results if r["side"] == side and r["ok"]]
|
||||
return sum(ts)/len(ts) if ts else 0
|
||||
avgs = {s[0]: avg(s[0]) for s in SIDES}
|
||||
|
||||
# 速度色阶
|
||||
all_t = sorted(r["elapsed"] for r in results if r["ok"])
|
||||
tmin, tmax = all_t[0], all_t[-1]
|
||||
def speed_color(t):
|
||||
if tmax == tmin: return "#16a34a"
|
||||
ratio = (t - tmin) / (tmax - tmin)
|
||||
if ratio < 0.33: return "#16a34a"
|
||||
elif ratio < 0.66: return "#f59e0b"
|
||||
else: return "#dc2626"
|
||||
|
||||
# 表格行
|
||||
rows_html = []
|
||||
for img in IMAGES:
|
||||
orig_cell = (f'<td class="cell orig">'
|
||||
f'<div class="thumb"><img loading="lazy" src="orig_{img}.jpg" '
|
||||
f'onclick="openImg(this.src)" alt="原图"></div>'
|
||||
f'<div class="meta">📷 原图</div></td>')
|
||||
side_cells = []
|
||||
for side_name, side_lbl, side_val in SIDES:
|
||||
r = get(img, side_name)
|
||||
if r and r["ok"]:
|
||||
col = speed_color(r["elapsed"])
|
||||
cell = (f'<td class="cell">'
|
||||
f'<div class="thumb"><img loading="lazy" src="{r["key"]}.jpg" '
|
||||
f'onclick="openImg(this.src)" alt="{html.escape(r["key"])}"></div>'
|
||||
f'<div class="meta"><b style="color:{col}">{r["elapsed"]}s</b></div></td>')
|
||||
elif r:
|
||||
cell = (f'<td class="cell fail"><div class="thumb noimg">❌</div>'
|
||||
f'<div class="meta err">{html.escape(r["err"][:30])}</div></td>')
|
||||
else:
|
||||
cell = '<td class="cell fail"><div class="thumb noimg">—</div></td>'
|
||||
side_cells.append(cell)
|
||||
rows_html.append("<tr>" + orig_cell + "".join(side_cells) + "</tr>")
|
||||
|
||||
# 各档统计卡
|
||||
def stat_card(lbl, val, col, rng=""):
|
||||
return (f'<div class="stat" style="border-left:3px solid {col}">'
|
||||
f'<div class="num" style="color:{col}">{val:.1f}s</div>'
|
||||
f'<div class="lbl">{lbl}{rng}</div></div>')
|
||||
|
||||
stat_cards = "".join([
|
||||
stat_card("原图直送(0)", avgs["origin"], "#dc2626"),
|
||||
stat_card("1024", avgs["s1024"], "#f59e0b"),
|
||||
stat_card("896", avgs["s896"], "#2563eb"),
|
||||
stat_card("768", avgs["s768"], "#16a34a"),
|
||||
stat_card("640", avgs["s640"], "#0d9488"),
|
||||
])
|
||||
|
||||
html_doc = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>wave发型 5档分辨率对比</title>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.5; padding: 16px; }}
|
||||
.wrap {{ max-width: 100%; margin: 0 auto; }}
|
||||
h1 {{ font-size: 22px; margin-bottom: 4px; }}
|
||||
.sub {{ color: #6b7280; font-size: 12px; margin-bottom: 16px; }}
|
||||
.stats {{ display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }}
|
||||
.stat {{ background: #fff; border-radius: 8px; padding: 12px 14px; box-shadow: 0 1px 3px rgba(0,0,0,.06); flex: 1; min-width: 110px; }}
|
||||
.stat .num {{ font-size: 22px; font-weight: 700; }}
|
||||
.stat .lbl {{ font-size: 11px; color: #6b7280; margin-top: 2px; }}
|
||||
.summary-bar {{ background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); font-size: 13px; }}
|
||||
.summary-bar b {{ color: #dc2626; }}
|
||||
.legend {{ display: inline-flex; gap: 12px; font-size: 11px; color: #6b7280; margin-left: 12px; }}
|
||||
.legend span {{ display: inline-flex; align-items: center; gap: 4px; }}
|
||||
.legend i {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
|
||||
.table-wrap {{ overflow-x: auto; background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.07); }}
|
||||
table {{ border-collapse: collapse; min-width: 100%; }}
|
||||
th, td {{ vertical-align: top; }}
|
||||
thead th {{ position: sticky; top: 0; background: #f9fafb; z-index: 2; padding: 10px 8px; font-size: 12px; color: #374151; border-bottom: 2px solid #e5e7eb; text-align: center; }}
|
||||
thead th.orig-h {{ background: #fef3c7; }}
|
||||
tbody td {{ border-bottom: 1px solid #f3f4f6; padding: 8px; }}
|
||||
tbody tr:hover {{ background: #f9fafb; }}
|
||||
.cell {{ width: 200px; min-width: 200px; text-align: center; }}
|
||||
.cell.orig {{ background: #fffbeb; }}
|
||||
.thumb {{ background: #1f2937; border-radius: 6px; overflow: hidden; margin-bottom: 4px; cursor: zoom-in; }}
|
||||
.thumb img {{ width: 100%; height: 260px; object-fit: contain; display: block; }}
|
||||
.thumb.noimg {{ color: #d1d5db; font-size: 16px; padding: 110px 0; text-align: center; }}
|
||||
.meta {{ font-size: 11px; color: #6b7280; }}
|
||||
.meta.err {{ color: #dc2626; }}
|
||||
.overlay {{ display: none; position: fixed; inset: 0; background: rgba(0,0,0,.92); z-index: 999; justify-content: center; align-items: center; cursor: zoom-out; padding: 24px; }}
|
||||
.overlay.active {{ display: flex; }}
|
||||
.overlay img {{ max-width: 96%; max-height: 96%; object-fit: contain; border-radius: 4px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>💇 wave发型 · 5档分辨率对比报告</h1>
|
||||
<p class="sub">POST /api/v1/hair/grow · female · wave(波浪) · 21 图 × 5 档分辨率 = 105 次 · 串行 · 4090 (24G)</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat" style="border-left:3px solid #16a34a"><div class="num" style="color:#16a34a">{ok}/{total}</div><div class="lbl">成功 / 总数</div></div>
|
||||
{stat_cards}
|
||||
</div>
|
||||
|
||||
<div class="summary-bar">
|
||||
📊 <b>结论:</b>耗时随分辨率单调下降。<b>大图(asdf 1666/qwer 1678) 原图直送需 24~26s,是 1024 档(11s) 的 2.3 倍</b>;
|
||||
中小图(≤1024) 各档差异较小(6~13s),因小图本身不触发缩图。
|
||||
<b>4090 24G 全程无 OOM</b>,105/105 成功。<b>画质对比</b>见下表(横向滑动),点击任意图可放大。
|
||||
<span class="legend">
|
||||
<span><i style="background:#16a34a"></i>快(<{(tmin+(tmax-tmin)*0.33):.0f}s)</span>
|
||||
<span><i style="background:#f59e0b"></i>中等</span>
|
||||
<span><i style="background:#dc2626"></i>慢(>{(tmin+(tmax-tmin)*0.66):.0f}s)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="orig-h">📷 原图</th>
|
||||
<th>原图直送 (0)<br><span class="dim">不缩放</span></th>
|
||||
<th>1024</th>
|
||||
<th>896<br><span class="dim">默认</span></th>
|
||||
<th>768</th>
|
||||
<th>640</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{"".join(rows_html)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" id="overlay" onclick="this.classList.remove('active')"><img id="overlayImg" src=""></div>
|
||||
<script>
|
||||
function openImg(src) {{
|
||||
document.getElementById('overlayImg').src = src;
|
||||
document.getElementById('overlay').classList.add('active');
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
with open(REPORT, "w") as f:
|
||||
f.write(html_doc)
|
||||
print(f"已生成: {REPORT}")
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=wave发型5档分辨率对比报告 (8850)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
WorkingDirectory=/home/ubuntu/hair/image/wave_test/out
|
||||
ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8850 --bind 0.0.0.0
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -206,7 +206,7 @@ def generate():
|
||||
return jsonify({"error": msg}), 400
|
||||
image_file = request.files["image"]
|
||||
mask_file = request.files["mask"]
|
||||
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||
prompt_text = request.form.get("prompt", "填充遮罩区域的头发")
|
||||
log.info(
|
||||
"收到请求: image=%s mask=%s prompt=%r",
|
||||
image_file.filename, mask_file.filename, prompt_text,
|
||||
|
||||
@@ -27,7 +27,7 @@ def prep_and_upload():
|
||||
|
||||
|
||||
def run_once(fname, model, dtype, steps):
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发")
|
||||
wf["16"]["inputs"]["unet_name"] = model
|
||||
wf["16"]["inputs"]["weight_dtype"] = dtype
|
||||
wf["1"]["inputs"]["steps"] = steps
|
||||
|
||||
@@ -33,7 +33,7 @@ def upload(scale=1.0):
|
||||
|
||||
|
||||
def run(fname, steps):
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜")
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发")
|
||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||
wf["1"]["inputs"]["steps"] = steps
|
||||
|
||||
@@ -30,7 +30,7 @@ SEED = 123456789
|
||||
imgs = []
|
||||
labels = []
|
||||
for steps in [2, 3, 4, 6]:
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", seed=SEED)
|
||||
wf = A.build_workflow(fname, "填充遮罩区域的头发", seed=SEED)
|
||||
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||
wf["1"]["inputs"]["steps"] = steps
|
||||
|
||||
@@ -55,7 +55,7 @@ button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer;
|
||||
</div>
|
||||
<div class="controls" style="margin-top:16px">
|
||||
<label>提示词:</label>
|
||||
<input type="text" id="promptInput" value="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜">
|
||||
<input type="text" id="promptInput" value="填充遮罩区域的头发">
|
||||
</div>
|
||||
<div style="text-align:center; margin-top:16px">
|
||||
<button class="btn-generate" id="generateBtn" disabled>🚀 生成</button>
|
||||
|
||||
@@ -25,7 +25,7 @@ resp = requests.post(
|
||||
"image": ("original.jpg", img_data, "image/jpeg"),
|
||||
"mask": ("mask.png", mask_data, "image/png"),
|
||||
},
|
||||
data={"prompt": "填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜"},
|
||||
data={"prompt": "填充遮罩区域的头发"},
|
||||
timeout=600,
|
||||
)
|
||||
|
||||
|
||||
|
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 |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 222 KiB |