35 Commits
Author SHA1 Message Date
Ubuntu 9379fbb8f8 save 2026-08-02 01:27:04 +08:00
xsl c20606c003 save 2026-07-31 23:29:07 +08:00
xslandCursor 1ca033f25a feat: 接口1/6 标注层字号上调一档 + 眉心改用 9 号点定位
- annotation: 自适应字号系数 0.017→0.020(下限 8→9),标注文字更大更清晰
- measure: _brow_center 只取 FaceMesh 9 号点(眉间上点),不再与 151 取中点

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 23:02:19 +08:00
UbuntuandCursor 87ff2c15d0 chore: 精简生发提示词,去掉磨皮/美颜要求
统一改为「填充遮罩区域的头发」,涉及后端默认值、ComfyUI 工作流 JSON、
测试页、benchmark 脚本、local_test。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 15:57:57 +08:00
UbuntuandCursor c536e4ccb1 feat: 屏蔽接口3(B端生发)
网关层直接拦截返回 1007,不再转发到 worker 池;worker 侧路由同步标记
deprecated 并短路返回,保留原参数签名避免老客户端裸 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 15:57:46 +08:00
xslandCursor 9fb5b486c0 fix(接口4): worker 移除脸型 Mock + face_shape 改本机 MediaPipe 计算
- worker /api/v1/face/features 不再返回假成功数据,直接告知仅网关实现,
  避免本机误打 :8187 被 Mock 结果误导。
- 网关 ark_api_key 加载优先级改为 gateway/config.json 优先(原先误读
  worker_config.json 里的失效 key)。
- 接口4 face_shape 不再采信豆包结果,改用本机 face/face_shape_classifier.py
  (MediaPipe 7 类)计算覆盖;其余 5 项特征仍走豆包。
- 修复 face_shape_classifier 共享 FaceMesh 实例的线程安全问题(加锁),
  避免网关侧接口4 并发请求时崩溃/结果错乱。
- 新增 /api/v1/debug/face-shape 调试接口 + static/test_face_shape.html
  单图调试页(worker 侧)。
- 更新文档:网关机现在也需要 mediapipe/opencv-python/numpy<2。

⚠️ 部署前提醒:网关机需先安装 mediapipe==0.10.14 / opencv-python==4.10.0.84 /
numpy==1.26.4,否则接口4 会返回 1007「分析服务异常」。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:41:49 +08:00
xsl a748dfd1e5 Merge branch 'main' of http://git.xiangsilian.com:3000/xsl/hair 2026-07-29 13:26:44 +08:00
xslandCursor 587876f8a0 feat: 脸型分类器(7分类)+批量报告生成
基于 MediaPipe 468 关键点提取几何特征,转 z 分数后与各脸型原型加权匹配。
参考分布与原型靶心取自 1093 张测试集的实测画像,不再靠人工设定绝对阈值。

方形脸占比从 29.6% 降到 13.8%,两处原因:一是参考统计量原先只由 50 张样本
估得,相对全量人群有系统性偏移,且三项偏移都在给方形脸加分;二是原型把
aspect_ratio 当作方形脸的主特征,但实测方脸组该值中位仅 +0.16,真正"宽"的
是圆脸(+1.01),等于在拿脸宽找方脸。

原型参数在「6 张基准标注图判定不变、且领先第二名 >=3 分」的约束下搜索得到。
余量约束是必要的:早前一版余量仅 0.008 分,权重写码时四舍五入就会翻转结论。

测试素材(人像照片)与报告输出体积大,一并加入 .gitignore。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 13:16:43 +08:00
xsl 81c7a0b55c feat: 接口1/5/6 发际线弃用逻辑(顶庭<0.7cm)
发际线离头顶<0.7cm时判定分割不可靠,弃用发际线:
顶/上庭字段置null、face_total只算中下庭、标注图保留头顶线去掉发际线、
只标中/下庭。eye1/7竖向范围改用眉心。
2026-07-27 23:21:01 +08:00
xsl 218818bdd0 docs: 同步 integration.html / 接口文档.md / test_interface5.html
- 接口5: 补 generate_grow_image 参数说明(接口文档/integration/test_interface5 加控件)
- 接口1/5/6: 补 left_position/right_position 字段(MediaPipe 21/251号点)
- 接口4: features 字段纠正为固定6个英文字段(原误写~42项含中文, 与代码不符)
- 接口7: 完全移除(代码已 deprecated=True 固定返回错误, 文档却当正常接口详述)
- 错误码: 删错误的'1004已废弃'(1004仍用于接口2/5 gender校验), 补 1004 正确描述 + 1009(X-Internal-Token鉴权)
- test_interface5.html: 加 generate_grow_image 复选框
2026-07-24 00:52:14 +08:00
xsl ae47472a63 feat(接口1/5/6): 返回数据新增 left_position/right_position(MediaPipe 21/251号点)
- face_mesh_landmarks.py: 加常量 LEFT_POSITION=21 / RIGHT_POSITION=251
- measure.py: MeasureResult 收 landmarks/宽高, to_response 顶层输出两点(原图像素 {x,y}, 与 landmarks 同格式)
- measure_face 透传 landmarks(签名不变, 6处调用零改动); __init__ 用 None 默认值守卫向后兼容
- 三接口自动生效: 接口1/6 在 data 顶层, 接口5 在 face_measure 对象里(复用同一 to_response)
- 实测坐标左右镜像合理, 44 个现有测试全过无回归
2026-07-24 00:52:14 +08:00
xsl cd8985e93d feat(接口5): 新增 generate_grow_image 参数控制是否生成生发效果图
- app.py: 接口5 路由加表单参数 generate_grow_image(bool, 默认 True)并透传
- hairline/service.py: generate_hairline_pngs 加同名参数, False 时跳过 ComfyUI 生发、grown_png 恒 None
- 默认行为不变(向后兼容); false 时仅返回三档发际线叠图与中心点, 大幅降低耗时
- 网关字节级透传 multipart, 新参数自动到达 worker, 无需改网关
2026-07-24 00:52:04 +08:00
UbuntuandClaude bb9f55e93c feat(gateway): 全局串行化(并发=1) + 记录入参/出参/耗时日志
并发模型从「每worker并发1 + 多worker并行」改为全局串行:同一时间
只处理1个请求,其余排队;多 worker 仅作热备(主 worker 坏了才用备机)。
接口4(face/features) 走豆包、不占 GPU,不纳入串行。

- pool: asyncio.Semaphore(max_global_concurrency=1) + acquire/release_global_slot
  (依赖单进程 uvicorn 部署,已在注释中标注)
- forward: proxy_request 最外层 acquire 全局槽、try/finally 全路径释放;
  入参 multipart 解析挂 request.state;原重试/故障转移逻辑抽到 _dispatch_with_retries
- reqlog(新): 标量入参保留;图片(file/base64)存盘转URL,绝不内嵌base64;
  出参递归摘要截断(landmarks/长串/大数组)
- logging_middleware: RequestLogEntry 加 request_params/response_data,
  jsonl 全量记录;_load_from_logfile 同步映射防重启丢字段;get_stats recent 暴露
- /gateway-health 暴露 global_max/global_busy/global_waiting
- config: dispatch 新增 max_global_concurrency / max_queue_wait_seconds
- tests: test_reqlog + test_gateway_serialization(9 用例)

顺带提交此前未提交的网关统计(daily stats 接口)与耗时看板(api_timing_dashboard.html)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 22:56:48 +08:00
xslandCursor 98b9108837 perf(接口2): 稳定混跑耗时至12s内 —— ComfyUI插队 + CLIP挪CPU + 提示词全局统一
问题:接口2 与接口3/5 乱序调用时耗时抖动(最差 15~22s)。两个根因:
1. GPU 24G 常驻 21.4G,Flux-2(3.9G) 无法完全驻留显存,每次采样动态换页,
   速度随空闲显存波动(2s~8s);
2. ComfyUI 单队列 FIFO,接口2 排在接口3/5 批量任务后面。

改动:
- hairline/comfyui.py: run() 新增 front 参数,/prompt 带 "front": true 插队到队列最前;
  redraw.py 透传;service.py 接口2 三处调用(女重绘 + 男有/无遮罩)传 front=True,
  接口3/5 仍走普通队列。
- add_hair.json / 0716add-hair-api.json: 节点61 CLIPLoader device default→cpu。
  qwen CLIP(4G) 不再占显存(文本条件缓存常年命中),ComfyUI 显存 8.8G→4.5G,
  Flux-2 完全驻留,采样稳定 ~3-5s。代价:换 prompt 后首次请求 CPU 编码 ~11s(一次性)。
- 提示词全局统一为「填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜」:
  app.py 4处默认值、service.py _REDRAW_PROMPT、redraw.py _DEFAULT_PROMPT、
  4个工作流节点60内置文案、测试页(test_interface2/3/7/12/12_final)、local_test。
  任何两个不同 prompt 交替提交都会打爆 CLIP 编码缓存(--cache-classic 只存最近一次),
  之前测试页旧文案与服务端不一致导致交替测试每次 +11s。
- app.py: 接口7 /api/v1/hair/grow-v2 下线(业务弃用;add_hair2.json 的 Klein-9b
  会把常驻 Klein-4b 挤出显存)。保留 stub 返回 1007 明确报错,避免裸 404。

实测(1024 档):接口2女 8.5~10s、接口2男 ~5s、接口3 ~7-10s,交替混跑无尖刺。

Co-authored-by: Cursor <cursoragent@cursor.com>

(cherry-picked from ubuntu3090 e7b62f2;已适配 main 分支代码结构:main 无 _REDRAW_PROMPT/_REDRAW_MAX_SIDE 缩图逻辑,front=True 直接加在 _call_local_redraw / generate_grow_results 的调用点;另把 main 独有的 benchmark_*.py 里的 prompt 一并统一)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:35:42 +08:00
xslandCursor d4aae9722c fix(pose): 修正正面照被误判为1003(solvePnP翻转解)
ITERATIVE 偶发收敛到相机后方(tz<0),roll≈±180° 超阈值,
把正面照误判为非正面。检测到负深度时回退 SQPNP 重解正深度解。
补充回归测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 16:46:05 +08:00
xsl f870c20f7f fix(接口3测试页): base64 图片补 data:image/jpeg;base64, 前缀
后端返回的 hair_growth_image_base64 是裸 base64(无 data: 前缀),
原代码直接赋给 <img>.src,浏览器把 base64 当相对路径请求,
拼成 http://host:8187/9j/4AAQ... 发 GET 导致 400 Bad Request。

参照接口2 测试页的写法,手动拼接 data:image/jpeg;base64, 前缀。
后端代码无需改动(接口2 一直正常)。
2026-07-19 15:14:41 +08:00
xsl 6d9e92f710 chore: torch 升级为 2.11.0+cu128 支持 RTX 5090 (sm_120)
原 torch 2.2.2+cu121 只编到 sm_90,在 RTX 5090 上 GPU 算子报
'no kernel image',接口2 因 SegFormer/BiSeNet 跑不动 GPU 而失败。
venv 实际已升级到 torch 2.11.0+cu128 / torchvision 0.26.0+cu128,
此处同步 requirements.txt 让文档与实际环境一致。

numpy 保持 1.26.4(<2,mediapipe/scikit-image 依赖),cu128 兼容。
2026-07-18 16:30:18 +08:00
xsl b58cd4c441 优化为4b 模型 2026-07-18 15:30:31 +08:00
xslandCursor 659c037270 feat: ComfyUI 改走 10.60.74.221,测试页上传图超阈值自动降采样
将 worker 默认 ComfyUI 地址改为远端 10.60.74.221:8188;前端测试页在像素超过 1536000 时等比缩小到 786432 以内。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 02:23:35 +08:00
xsl c1bb9614c7 save code 2026-07-17 01:54:04 +08:00
xsl 2b1f528ddd perf: 接口2/12 跳过 viz 叠图生成,省 ~80ms
compute_mask 新增 render_viz 开关(默认 True 保持接口11 行为不变)。
接口2/12 路径(generate_hairline_redraw)传 render_viz=False,跳过 6+ 张
overlay JPG 的 _jpg_b64 编码(baseline/upper/hair_seg/mask/hairline/pushed),
只保留必需的数据字段(_inner_pts/_outer_pts/_upper_mask/mask_pixels)。
2026-07-17 01:37:53 +08:00
xsl d9e96aca87 chore: 重绘服务改走内网 10.60.74.221:8899
gpu_worker 与重绘服务在同一内网,走内网地址(3ms 延迟,比公网更稳定)。
三处调用地址(service.py / test_interface12 / test_interface12_final)
由公网 117.50.183.232 改为内网 10.60.74.221。仍可用 HAIR_LOCAL_REDRAW_URL 覆盖。
2026-07-17 00:57:58 +08:00
xsl 8aed389d79 chore: 重绘服务拆到远程机器 117.50.183.232:8899
接口2 female 后端(generate_grow_results_swap)、test_interface12.html、
test_interface12_final.html 三处重绘调用地址由本机 127.0.0.1:8899 改为远程
117.50.183.232:8899,本机不再跑重绘服务(释放本机显存)。

地址仍可用 HAIR_LOCAL_REDRAW_URL 环境变量覆盖。
2026-07-17 00:44:20 +08:00
xsl c72e3ceda9 asdf 2026-07-17 00:32:13 +08:00
xsl 99ce21334a asdf 2026-07-17 00:26:47 +08:00
xsl 5fbc03a6df feat: 接口12/接口2 发际线重绘改走 local_test 外部 ComfyUI 服务
后端 generate_hairline_redraw 跳过内置 Flux-2 重绘,改为产出 final(接缝融合基底)
+ 纯红遮罩 PNG(redraw_band_mask_base64,遮罩区=(255,0,0,255)、其余全透明)。

接口2 female(generate_grow_results_swap) 取 final+遮罩后在后端调 local_test
(0716add-hair.json 工作流) 完成重绘,结果作为生发图返回;male 分支不变。

测试页 test_interface12.html / test_interface12_final.html 改为两阶段:
先生成 final+纯红遮罩,再前端调 local_test 重绘并展示;color_match 默认不勾选。

local_test/app.py 加 CORS 头(OPTIONS 预检),支持浏览器跨域直连。
2026-07-17 00:00:15 +08:00
xsl 0bbb15d668 添加服务 2026-07-16 22:57:12 +08:00
xsl 632e75317b 接口6 增加字段 2026-07-16 12:38:10 +08:00
xsl 1cd4115b26 Merge branch 'main' of http://git.xiangsilian.com:3000/xsl/hair 2026-07-16 09:38:53 +08:00
xslandCursor e8a2c5a8a1 fix: 日志目录去掉硬编码 /home/xsl,改为基于仓库根解析(支持 HAIR_LOG_DIR 覆盖)
worker 部署到 /home/ubuntu 等其他路径时,原硬编码 /home/xsl/hair/log
会导致 Permission denied。改为相对仓库根,兼容多机部署。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 08:07:03 +08:00
xsl 0f8a7e27c0 部署线上5090时候的改动git commit -m 部署线上5090时候的改动。 2026-07-16 07:54:07 +08:00
xsl 12f34c44f2 修改发型为最终发型 2026-07-16 00:22:54 +08:00
xslandCursor a208fe88ec feat: 拆分接口11/12,新增接口12 final / final v2 精简重绘端点
- 接口11 移除重绘,仅生成 final;接口12 (grow_v2) 负责发际线带 Flux-2 重绘
- 接口12 重绘带改为发际线外推 band_lo_mult~band_hi_mult 倍 push(默认 0.5~1.5),页面可调
- 接口12 同时产出 A 整帧重绘 与 B 局部加发+全脸美颜(beauty_alpha 可调)
- 新增 grow_v2_final(整帧重绘)/ grow_v2_final_v2(B 局部+美颜)端点:仅需图片+发型 ID,其余用固化默认值(color_match 关闭)
- 配套精简测试页 test_interface12_final.html / test_interface12_final_v2.html / test_interface12.html
- 恢复接口11 调试页多频段与换发型可调参数、color_match 默认不勾选
- 删除旧脚本 batch_grow_v2.py / gen_report_hairline_v2.py / test_simple.html

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 00:04:36 +08:00
xsl 4001df2c34 save code 2026-07-15 01:16:32 +08:00
xsl 9386f84c88 docs: 同步默认提示词「加一点美颜」到文档/接入页/测试页
- docs/接口文档.md:接口2/3/5/7 参数表统一更新默认提示词,接口2/3/7
  补全此前缺失的 prompt 参数行;顺带修正接口7 输出表 image_url 描述
  为透明 PNG(上一轮透明 PNG 改动的遗漏)
- static/integration.html:接口2 描述句 + 接口7 字段表同步透明 PNG 描述
- static/test_interface2/3/7.html:prompt 输入框默认值同步更新
2026-07-13 23:46:40 +08:00
376 changed files with 18414 additions and 1240 deletions
+33
View File
@@ -49,3 +49,36 @@ image/test/
# 批量报告输出(生成图+原图,体积大,不入 git)
static/report_hairline_v2/
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.*
# 脸型测试素材(人像照片,体积大,不入 git;仅保留 6 张基准标注图)
face/test_img/脸型测试集合/
face/test_img/girl/
face/test_img/man/
# 脸型特征缓存(由 face/dump_features.py 生成,可随时重跑)
face/cache/
# 脸型报告输出(标注图体积大,不入 git)
static/face_shape_report/
static/face_shape_report.html
static/facetest_report/
static/facetest_report.html
static/facetest_all_report/
static/facetest_all_report.html
+327
View File
@@ -0,0 +1,327 @@
{
"16": {
"class_type": "UNETLoader",
"inputs": {
"unet_name": "flux-2-klein-4b-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn_fast"
}
},
"3": {
"class_type": "VAELoader",
"inputs": {
"vae_name": "flux2-vae.safetensors"
}
},
"61": {
"class_type": "CLIPLoader",
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "flux2",
"device": "cpu"
}
},
"26": {
"class_type": "LoadImage",
"inputs": {
"image": "placeholder.png"
}
},
"60": {
"class_type": "JjkText",
"inputs": {
"text": "填充遮罩区域的头发"
}
},
"22": {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [
"61",
0
],
"text": [
"60",
0
]
}
},
"31": {
"class_type": "easy imageSize",
"inputs": {
"image": [
"26",
0
]
}
},
"33": {
"class_type": "Mask Fill Holes",
"inputs": {
"masks": [
"26",
1
]
}
},
"36": {
"class_type": "Convert Masks to Images",
"inputs": {
"masks": [
"33",
0
]
}
},
"39": {
"class_type": "ImageScale",
"inputs": {
"image": [
"36",
0
],
"upscale_method": "nearest-exact",
"width": [
"31",
0
],
"height": [
"31",
1
],
"crop": "disabled"
}
},
"37": {
"class_type": "Image To Mask",
"inputs": {
"image": [
"39",
0
],
"method": "intensity"
}
},
"32": {
"class_type": "LayerUtility: ImageScaleByAspectRatio V2",
"inputs": {
"image": [
"26",
0
],
"mask": [
"37",
0
],
"aspect_ratio": "custom",
"proportional_width": [
"31",
0
],
"proportional_height": [
"31",
1
],
"fit": "letterbox",
"method": "lanczos",
"round_to_multiple": "8",
"scale_to_side": "None",
"scale_to_length": 1024,
"background_color": "#000000"
}
},
"44": {
"class_type": "ImageAndMaskPreview",
"inputs": {
"image": [
"32",
0
],
"mask": [
"32",
1
],
"mask_opacity": 1,
"mask_color": "FFFF00",
"pass_through": true
}
},
"14": {
"class_type": "GetImageSize+",
"inputs": {
"image": [
"44",
0
]
}
},
"13": {
"class_type": "VAEEncode",
"inputs": {
"pixels": [
"44",
0
],
"vae": [
"3",
0
]
}
},
"2": {
"class_type": "ModelSamplingFlux",
"inputs": {
"model": [
"16",
0
],
"max_shift": 1.15,
"base_shift": 0.5,
"width": [
"14",
0
],
"height": [
"14",
1
]
}
},
"19": {
"class_type": "FluxGuidance",
"inputs": {
"conditioning": [
"22",
0
],
"guidance": 1
}
},
"5": {
"class_type": "ReferenceLatent",
"inputs": {
"conditioning": [
"19",
0
],
"latent": [
"13",
0
]
}
},
"7": {
"class_type": "EmptySD3LatentImage",
"inputs": {
"width": [
"14",
0
],
"height": [
"14",
1
],
"batch_size": 1
}
},
"1": {
"class_type": "BasicScheduler",
"inputs": {
"model": [
"2",
0
],
"scheduler": "simple",
"steps": 4,
"denoise": 1
}
},
"20": {
"class_type": "BasicGuider",
"inputs": {
"model": [
"2",
0
],
"conditioning": [
"5",
0
]
}
},
"6": {
"class_type": "RandomNoise",
"inputs": {
"noise_seed": 0
}
},
"8": {
"class_type": "KSamplerSelect",
"inputs": {
"sampler_name": "euler"
}
},
"9": {
"class_type": "SamplerCustomAdvanced",
"inputs": {
"noise": [
"6",
0
],
"guider": [
"20",
0
],
"sampler": [
"8",
0
],
"sigmas": [
"1",
0
],
"latent_image": [
"7",
0
]
}
},
"10": {
"class_type": "VAEDecode",
"inputs": {
"samples": [
"9",
0
],
"vae": [
"3",
0
]
}
},
"62": {
"class_type": "ColorMatch",
"inputs": {
"image_ref": [
"26",
0
],
"image_target": [
"10",
0
],
"method": "mkl",
"strength": 1,
"multithread": true
}
},
"17": {
"class_type": "SaveImage",
"inputs": {
"images": [
"62",
0
],
"filename_prefix": "hair_inpaint"
}
}
}
+117
View File
@@ -0,0 +1,117 @@
{
"16": {"class_type": "UNETLoader", "inputs": {
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn_fast"}},
"3": {"class_type": "VAELoader", "inputs": {
"vae_name": "flux2-vae.safetensors"}},
"61": {"class_type": "CLIPLoader", "inputs": {
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
"type": "flux2",
"device": "default"}},
"26": {"class_type": "LoadImage", "inputs": {
"image": "placeholder.png"}},
"60": {"class_type": "JjkText", "inputs": {
"text": "填充遮罩区域的头发,皮肤加一点磨皮"}},
"22": {"class_type": "CLIPTextEncode", "inputs": {
"clip": ["61", 0],
"text": ["60", 0]}},
"31": {"class_type": "easy imageSize", "inputs": {
"image": ["26", 0]}},
"33": {"class_type": "Mask Fill Holes", "inputs": {
"masks": ["26", 1]}},
"36": {"class_type": "Convert Masks to Images", "inputs": {
"masks": ["33", 0]}},
"39": {"class_type": "ImageScale", "inputs": {
"image": ["36", 0],
"upscale_method": "nearest-exact",
"width": ["31", 0],
"height": ["31", 1],
"crop": "disabled"}},
"37": {"class_type": "Image To Mask", "inputs": {
"image": ["39", 0],
"method": "intensity"}},
"32": {"class_type": "LayerUtility: ImageScaleByAspectRatio V2", "inputs": {
"image": ["26", 0],
"mask": ["37", 0],
"aspect_ratio": "custom",
"proportional_width": ["31", 0],
"proportional_height": ["31", 1],
"fit": "letterbox",
"method": "lanczos",
"round_to_multiple": "8",
"scale_to_side": "None",
"scale_to_length": 1024,
"background_color": "#000000"}},
"44": {"class_type": "ImageAndMaskPreview", "inputs": {
"image": ["32", 0],
"mask": ["32", 1],
"mask_opacity": 1,
"mask_color": "FFFF00",
"pass_through": true}},
"14": {"class_type": "GetImageSize+", "inputs": {
"image": ["44", 0]}},
"13": {"class_type": "VAEEncode", "inputs": {
"pixels": ["44", 0],
"vae": ["3", 0]}},
"2": {"class_type": "ModelSamplingFlux", "inputs": {
"model": ["16", 0],
"max_shift": 1.15,
"base_shift": 0.5,
"width": ["14", 0],
"height": ["14", 1]}},
"19": {"class_type": "FluxGuidance", "inputs": {
"conditioning": ["22", 0],
"guidance": 1}},
"5": {"class_type": "ReferenceLatent", "inputs": {
"conditioning": ["19", 0],
"latent": ["13", 0]}},
"7": {"class_type": "EmptySD3LatentImage", "inputs": {
"width": ["14", 0],
"height": ["14", 1],
"batch_size": 1}},
"1": {"class_type": "BasicScheduler", "inputs": {
"model": ["2", 0],
"scheduler": "simple",
"steps": 4,
"denoise": 1}},
"20": {"class_type": "BasicGuider", "inputs": {
"model": ["2", 0],
"conditioning": ["5", 0]}},
"6": {"class_type": "RandomNoise", "inputs": {
"noise_seed": 0}},
"8": {"class_type": "KSamplerSelect", "inputs": {
"sampler_name": "euler"}},
"9": {"class_type": "SamplerCustomAdvanced", "inputs": {
"noise": ["6", 0],
"guider": ["20", 0],
"sampler": ["8", 0],
"sigmas": ["1", 0],
"latent_image": ["7", 0]}},
"10": {"class_type": "VAEDecode", "inputs": {
"samples": ["9", 0],
"vae": ["3", 0]}},
"62": {"class_type": "ColorMatch", "inputs": {
"image_ref": ["26", 0],
"image_target": ["10", 0],
"method": "mkl",
"strength": 1,
"multithread": true}},
"17": {"class_type": "SaveImage", "inputs": {
"images": ["62", 0],
"filename_prefix": "hair_inpaint"}}
}
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -82,7 +82,8 @@ model.safetensors https://huggingface.co/jonathandinu/face-parsing/resol
模型已就位,但**内网机还需要 Python 依赖的离线 wheel 包**,否则 `pip install` 在内网无法联网安装。这部分**与目标机的操作系统、Python 版本、CUDA 版本强相关**,需确认后单独打包:
- **workerGPU 机)**`mediapipe` / `opencv-python` / `numpy<2` / `Pillow` / **`torch`+`torchvision` 的 CUDA 版**(按 GPU 的 CUDA 版本选 cu118/cu121 等)/ `transformers`(接口2 SegFormer+ FastAPI/uvicorn 全家桶。
- **网关机**很轻,只需 FastAPI/uvicorn/httpx 等代理依赖**不需要 torch/mediapipe**。
- **网关机**FastAPI/uvicorn/httpx 等代理依赖 + **接口4 现需 `mediapipe`/`opencv-python`/`numpy<2`**(脸型本机计算),
仍**不需要 torch**(无 GPU 推理需求)。
> 架构已拆分(见 `docs/实现说明.md`):算法依赖只装在 worker,网关保持轻量。
+6 -6
View File
@@ -2,7 +2,7 @@
"1": {
"inputs": {
"scheduler": "simple",
"steps": 6,
"steps": 4,
"denoise": 1,
"model": [
"2",
@@ -170,7 +170,7 @@
},
"16": {
"inputs": {
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
"unet_name": "flux-2-klein-4b-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn"
},
"class_type": "UNETLoader",
@@ -410,7 +410,7 @@
},
"60": {
"inputs": {
"text": "补充遮罩区补充遮罩区域的头发,头发填满遮罩区域。发际线往下挡住额头"
"text": "充遮罩区域的头发"
},
"class_type": "JjkText",
"_meta": {
@@ -419,9 +419,9 @@
},
"61": {
"inputs": {
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
"clip_name": "qwen_3_4b.safetensors",
"type": "flux2",
"device": "default"
"device": "cpu"
},
"class_type": "CLIPLoader",
"_meta": {
@@ -447,4 +447,4 @@
"title": "Color Match"
}
}
}
}
+450
View File
@@ -0,0 +1,450 @@
{
"1": {
"inputs": {
"scheduler": "simple",
"steps": 6,
"denoise": 1,
"model": [
"2",
0
]
},
"class_type": "BasicScheduler",
"_meta": {
"title": "基本调度器"
}
},
"2": {
"inputs": {
"max_shift": 1.15,
"base_shift": 0.5,
"width": [
"14",
0
],
"height": [
"14",
1
],
"model": [
"16",
0
]
},
"class_type": "ModelSamplingFlux",
"_meta": {
"title": "采样算法(Flux"
}
},
"3": {
"inputs": {
"vae_name": "flux2-vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"5": {
"inputs": {
"conditioning": [
"19",
0
],
"latent": [
"13",
0
]
},
"class_type": "ReferenceLatent",
"_meta": {
"title": "参考Latent"
}
},
"6": {
"inputs": {
"noise_seed": 217742615722421
},
"class_type": "RandomNoise",
"_meta": {
"title": "随机噪波"
}
},
"7": {
"inputs": {
"width": [
"14",
0
],
"height": [
"14",
1
],
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "空Latent图像(SD3"
}
},
"8": {
"inputs": {
"sampler_name": "euler"
},
"class_type": "KSamplerSelect",
"_meta": {
"title": "K采样器选择"
}
},
"9": {
"inputs": {
"noise": [
"6",
0
],
"guider": [
"20",
0
],
"sampler": [
"8",
0
],
"sigmas": [
"1",
0
],
"latent_image": [
"7",
0
]
},
"class_type": "SamplerCustomAdvanced",
"_meta": {
"title": "自定义采样器(高级)"
}
},
"10": {
"inputs": {
"samples": [
"9",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"13": {
"inputs": {
"pixels": [
"44",
0
],
"vae": [
"3",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE编码"
}
},
"14": {
"inputs": {
"image": [
"44",
0
]
},
"class_type": "GetImageSize+",
"_meta": {
"title": "🔧 Get Image Size"
}
},
"16": {
"inputs": {
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn"
},
"class_type": "UNETLoader",
"_meta": {
"title": "UNet加载器"
}
},
"17": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"62",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"19": {
"inputs": {
"guidance": 1,
"conditioning": [
"22",
0
]
},
"class_type": "FluxGuidance",
"_meta": {
"title": "Flux引导"
}
},
"20": {
"inputs": {
"model": [
"2",
0
],
"conditioning": [
"5",
0
]
},
"class_type": "BasicGuider",
"_meta": {
"title": "基本引导器"
}
},
"22": {
"inputs": {
"text": [
"60",
0
],
"clip": [
"61",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP文本编码"
}
},
"26": {
"inputs": {
"image": "clipspace/clipspace-painted-masked-1781655058499.png [input]"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"31": {
"inputs": {
"image": [
"26",
0
]
},
"class_type": "easy imageSize",
"_meta": {
"title": "图像尺寸"
}
},
"32": {
"inputs": {
"aspect_ratio": "custom",
"proportional_width": [
"31",
0
],
"proportional_height": [
"31",
1
],
"fit": "letterbox",
"method": "lanczos",
"round_to_multiple": "8",
"scale_to_side": "None",
"scale_to_length": 1024,
"background_color": "#000000",
"image": [
"26",
0
],
"mask": [
"37",
0
]
},
"class_type": "LayerUtility: ImageScaleByAspectRatio V2",
"_meta": {
"title": "LayerUtility: ImageScaleByAspectRatio V2"
}
},
"33": {
"inputs": {
"masks": [
"26",
1
]
},
"class_type": "Mask Fill Holes",
"_meta": {
"title": "Mask Fill Holes"
}
},
"36": {
"inputs": {
"masks": [
"33",
0
]
},
"class_type": "Convert Masks to Images",
"_meta": {
"title": "Convert Masks to Images"
}
},
"37": {
"inputs": {
"method": "intensity",
"image": [
"39",
0
]
},
"class_type": "Image To Mask",
"_meta": {
"title": "Image To Mask"
}
},
"39": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"31",
0
],
"height": [
"31",
1
],
"crop": "disabled",
"image": [
"36",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"44": {
"inputs": {
"mask_opacity": 1,
"mask_color": "FFFF00",
"pass_through": true,
"image": [
"32",
0
],
"mask": [
"32",
1
]
},
"class_type": "ImageAndMaskPreview",
"_meta": {
"title": "ImageAndMaskPreview"
}
},
"45": {
"inputs": {
"images": [
"44",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "预览图像"
}
},
"53": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00119_.png&type=temp&subfolder=&rand=0.7926013627811991"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_zuixa_00120_.png&type=temp&subfolder=&rand=0.43692946883795813"
}
]
},
"image_a": [
"62",
0
],
"image_b": [
"26",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"60": {
"inputs": {
"text": "补充遮罩区补充遮罩区域内的头发,头发填满遮罩区域。发际线往下挡住额头"
},
"class_type": "JjkText",
"_meta": {
"title": "Text"
}
},
"61": {
"inputs": {
"clip_name": "qwen_3_8b_fp8mixed.safetensors",
"type": "flux2",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "加载CLIP"
}
},
"62": {
"inputs": {
"method": "mkl",
"strength": 1,
"multithread": true,
"image_ref": [
"26",
0
],
"image_target": [
"10",
0
]
},
"class_type": "ColorMatch",
"_meta": {
"title": "Color Match"
}
}
}
+2 -2
View File
@@ -381,7 +381,7 @@
},
"60": {
"inputs": {
"text": "充遮罩区域的头发,区域内填充满头发,不要保留皮肤,发际线下移填充头发。自然的头发生长方向,逼真的头发质感,自然发质。"
"text": "充遮罩区域的头发"
},
"class_type": "JjkText",
"_meta": {
@@ -684,7 +684,7 @@
},
"87": {
"inputs": {
"text": "去掉头发接缝的黄色痕迹,头发完美融合,保持发型不变,发色不变。其他不变。",
"text": "填充遮罩区域的头发",
"clip": [
"78",
0
+410 -250
View File
@@ -1,6 +1,6 @@
"""旷视五接口 — worker 侧(高性能 GPU 后端)。
接口 1(四庭七眼测量)已为**真实算法实现**(见 face_analysis 包);接口 2~5 仍为 Mock。
接口 1 等算法在 worker;接口4(用户特征)已迁到网关本机(调豆包),worker 同路径只返回明确错误、无 Mock。
拆分架构:worker 跑算法、返回 `annotated_image_base64`(不落盘、不拼 URL,由网关完成)。
worker 对 `/api/*` 校验内网鉴权头 `X-Internal-Token``/health` 供网关探测不校验。
"""
@@ -138,7 +138,8 @@ app = FastAPI(
app.mount("/static", StaticFiles(directory="static"), name="static")
# 不校验鉴权的路径前缀(供网关探测 / 文档 / 静态)
_AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static", "/api/v1/debug")
_AUTH_EXEMPT = ("/health", "/docs", "/openapi.json", "/redoc", "/static",
"/api/v1/debug", "/api/v1/redraw")
@app.middleware("http")
@@ -361,41 +362,65 @@ def _run_face_measure_data(image, variant="v1"):
logger.warning("头发/耳朵分割失败,回退方案A%s", seg_e)
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
discarded = result.hairline_discarded
data = result.to_response()
vd = result.vertical
if variant == "v6":
vd = result.vertical
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
data["four_courts"]["ratios"] = {
"upper_court": round(vd["upper_court_px"] / base_px, 3),
"middle_court": round(vd["middle_court_px"] / base_px, 3),
"lower_court": round(vd["lower_court_px"] / base_px, 3),
}
data["four_courts"].pop("top_court_cm", None)
data["face_total_height_cm"] = round(
result.upper_cm + result.middle_cm + result.lower_cm, 2)
data["landmarks"].pop("hair_top", None)
if discarded:
# 发际线弃用:接口6 的上庭也依赖发际线,一并置 null;只保留中/下庭。
base_px = vd["middle_court_px"] + vd["lower_court_px"]
data["four_courts"]["upper_court_cm"] = None
data["four_courts"]["ratios"] = {
"upper_court": None,
"middle_court": round(vd["middle_court_px"] / base_px, 3),
"lower_court": round(vd["lower_court_px"] / base_px, 3),
}
data["four_courts"].pop("top_court_cm", None)
data["face_total_height_cm"] = round(
result.middle_cm + result.lower_cm, 2)
data["landmarks"]["hairline"] = None
else:
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top
data["four_courts"]["ratios"] = {
"upper_court": round(vd["upper_court_px"] / base_px, 3),
"middle_court": round(vd["middle_court_px"] / base_px, 3),
"lower_court": round(vd["lower_court_px"] / base_px, 3),
}
data["four_courts"].pop("top_court_cm", None)
data["face_total_height_cm"] = round(
result.upper_cm + result.middle_cm + result.lower_cm, 2)
# 注:landmarks.hair_top 保留返回(供前端/下游定位头顶),但顶庭数值、
# 占比、标注图仍按三庭处理,显示效果不变。
# 七眼:从左到右共 7 段宽度(cm),仅接口1(v1)含人头最左/最右端线段。
# eye1=左耳外段 eye2=左脸颊段 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊段 eye7=右耳外段
# 端线取自耳朵分割外缘(与标注图同源);某侧耳朵不可见 → 该侧端线缺失 → 对应 eye 置 null(保留键)。
if variant != "v6":
try:
# 七眼段宽度(cm)。eye1=左耳外段 eye2=左脸颊 eye3=左眼 eye4=两眼间距 eye5=右眼 eye6=右脸颊 eye7=右耳外段。
# eye2~eye6(5段)只用内部分点,接口1/6 共用;eye1/eye7 需耳朵分割端线,仅接口1 有。
try:
epts = result.eyes["points"]
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
pc = result.px_per_cm
inner_xs = [lcx, epts["left_outer"][0], epts["left_inner"][0],
epts["right_inner"][0], epts["right_outer"][0], rcx]
for i in range(5):
a, b = inner_xs[i], inner_xs[i + 1]
data["seven_eyes"][f"eye{i + 2}"] = (
None if (a is None or b is None) else round((b - a) / pc, 2))
if variant != "v6":
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
from face_analysis.annotation import _ear_edges_from_mask
epts = result.eyes["points"]
lcx, rcx = epts["left_cheek"][0], epts["right_cheek"][0]
top_y = (vd["brow_center"][1] if discarded
else vd["hair_top"][1])
head_l, head_r = _ear_edges_from_mask(
ear_mask, hair_mask,
result.vertical["hair_top"][1], result.vertical["chin_tip"][1],
top_y, vd["chin_tip"][1],
lcx, rcx, (lcx + rcx) / 2)
xs = [head_l, lcx, epts["left_outer"][0], epts["left_inner"][0],
epts["right_inner"][0], epts["right_outer"][0], rcx, head_r]
pc = result.px_per_cm
for i in range(7):
a, b = xs[i], xs[i + 1]
data["seven_eyes"][f"eye{i + 1}"] = (
None if (a is None or b is None) else round((b - a) / pc, 2))
except Exception as seg_e: # noqa: BLE001
logger.warning("七眼段宽度计算失败:%s", seg_e)
data["seven_eyes"]["eye1"] = (
None if (head_l is None) else round((lcx - head_l) / pc, 2))
data["seven_eyes"]["eye7"] = (
None if (head_r is None) else round((head_r - rcx) / pc, 2))
except Exception as seg_e: # noqa: BLE001
logger.warning("七眼段宽度计算失败:%s", seg_e)
return data, result, hair_mask, ear_mask
@@ -547,13 +572,13 @@ async def face_measure(
输入用户正面照,返回:
- 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物)
- 三庭(上庭/中庭/下庭)各段**厘米数值及占比**(**不含顶庭**)
- 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比**
- 五眼段宽(eye2~eye6:左脸颊/左眼/两眼间距/右眼/右脸颊)**厘米数值**,另含眼宽/脸宽/两眼间距
- 四个关键分界点的**原图像素坐标**(发际线/眉心/鼻翼下缘/下巴尖)
基于接口1 的变体,与接口1 的差异:
- **去顶庭**:不画头顶横线、不返回顶庭数据;`face_total_height_cm` 为三庭之和
- **竖线范围**:纵向竖线从发际线画到下巴尖(接口1 为头顶→下巴尖)
- **不画人头最左/最右端线**:仅七眼 6 点共 5 段标尺,不取头发轮廓端线(接口1 为 8 线 7 段)
- **不画人头最左/最右端线**:仅七眼 6 点共 5 段标尺eye2~eye6,不取头发轮廓端线(接口1 为 8 线 7 段 eye1~eye7
其余(箭头/虚线/字体/单位cm/七眼数据)与接口1 一致。
@@ -598,6 +623,7 @@ async def face_measure(
"face_width_cm": 24.08,
"inter_eye_distance_cm": 3.44,
"ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143},
"eye2": 3.44, "eye3": 3.44, "eye4": 3.44, "eye5": 3.44, "eye6": 3.44,
},
"landmarks": {
"hairline": {"x": 540, "y": 430},
@@ -649,6 +675,11 @@ async def face_measure_v2(
- 发际线类型 `hairline_type`(英文 key
- 顺序 `order`(本期固定 `1..N`,不排序)
> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair
> + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。
> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。
> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
@@ -705,7 +736,7 @@ 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的文本"),
):
# 1. gender 必填校验(非法/缺失 → 1004)
if gender not in ("male", "female"):
@@ -728,10 +759,17 @@ async def hair_grow(
try:
from fastapi.concurrency import run_in_threadpool
from hairline.service import generate_grow_results
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles)
# 预览 + 生发/换发型 都是阻塞且较慢,放线程池避免卡住事件循环
# female:换发型 + Flux-2 整帧重绘(= 接口12 final 管线);male:仍走原生发管线。
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)
else:
from hairline.service import generate_grow_results
items = await run_in_threadpool(
generate_grow_results, image, gender, use_mask, prompt, hair_styles)
if items is None:
return err(1001, "无法识别人像")
@@ -751,117 +789,15 @@ async def hair_grow(
# ---------------------------------------------------------------------------
# 接口 7C 端生发 v2add_hair2.json 工作流)
# 接口 7C 端生发 v2 —— 已弃用add_hair2.json 用 Klein-9b 大模型,会把常驻的
# Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。
# 保留路由返回明确错误,避免老客户端拿到裸 404。
# ---------------------------------------------------------------------------
_WORKFLOW2_PATH = os.path.join(os.path.dirname(__file__), "add_hair2.json")
@app.post(
"/api/v1/hair/grow-v2",
summary="接口7 C端生发 v2add_hair2 工作流)",
tags=["生发"],
description=f"""
输入用户正面照 + **性别** + **发型序号**,使用 add_hair2.json 工作流生成指定发际线类型的预览图与生发图。
功能与接口2 完全一致,仅 ComfyUI 工作流不同。
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
---
- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。
非法或缺失返回 `1004`。
- **hair_style**(必填):`int`,发型序号。`female`1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave
`male`1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界返回 `1007`。
- **beauty_enabled**:本期保留但不生效。
`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`female),
`ellipse` / `m` / `straight` / `inverse_arc`male)。
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"results": [
{"image_base64": "iVBORw0KGgo...", "hairline_type": "ellipse", "order": 1},
]
},
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"examples": {
"图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}},
"非正面照": {"value": {"code": 1003, "message": "角度问题,请上传正面照", "request_id": "x", "data": None}},
}
}
},
},
},
)
async def hair_grow_v2(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"),
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"),
beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"),
use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"),
prompt: str = Form(default="补充遮罩区域的头发,加一点美颜", description="ComfyUI 提示词,会替换工作流节点60的文本"),
):
# 1. gender 必填校验(非法/缺失 → 1004)
if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female")
# 2. hair_style 必填校验(解析逗号分隔,越界 → 1007)
max_styles = {"female": 5, "male": 4}[gender]
hair_styles = _parse_hair_styles(hair_style, max_styles)
if hair_styles is None:
return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}")
# 3. 三选一取图
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None:
return e
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
if image is None:
return err(1008, "图片格式不支持(仅 JPG / PNG)")
try:
from fastapi.concurrency import run_in_threadpool
from hairline.service import generate_grow_results
# 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环
items = await run_in_threadpool(generate_grow_results, image, gender, use_mask, prompt, hair_styles, _WORKFLOW2_PATH)
if items is None:
return err(1001, "无法识别人像")
results = []
for p in items:
results.append({
"image_base64": _jpg_b64(p["image_bgr"]), # 预览图 JPG
"grown_image_base64": (_png_to_jpg_b64(p["grown_png"]) # 生发图 JPG
if p["grown_png"] else None),
"hairline_type": p["hairline_type"],
"order": p["order"],
})
return ok({"results": results})
except Exception as ex: # noqa: BLE001
logger.exception("接口7 处理异常")
return err(1007, f"处理失败:{ex}")
@app.post("/api/v1/hair/grow-v2", include_in_schema=False, deprecated=True)
async def hair_grow_v2():
"""接口7 已弃用:请改用 /api/v1/hair/grow(接口2)。"""
return err(1007, "接口7/api/v1/hair/grow-v2)已弃用,请使用 /api/v1/hair/grow")
# ---------------------------------------------------------------------------
@@ -872,6 +808,7 @@ async def hair_grow_v2(
"/api/v1/hair/grow-b",
summary="接口3 B端生发(医生/操作端)",
tags=["生发"],
deprecated=True,
description="""
医生/操作端在用户照片上**手动用马克笔划线标注**目标发际线后,**只需上传这一张划线图**,返回:
- 生发后效果图(系统检测划线 → 据此生成「植发 3 个月」效果)
@@ -911,36 +848,11 @@ 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)
if e is not None:
return e
marked = cv2.imdecode(np.frombuffer(marked_raw, np.uint8), cv2.IMREAD_COLOR)
if marked is None:
return err(1008, "图片格式不支持(仅 JPG / PNG)")
try:
from fastapi.concurrency import run_in_threadpool
from hairline.service import generate_grow_b
res = await run_in_threadpool(generate_grow_b, marked, use_mask, prompt)
if res["status"] == "no_face":
return err(1001, "无法识别人像")
if res["status"] == "no_line":
return err(1001, "未检测到发际线划线,请确认划线图额头有清晰的手绘发际线")
grown_b64 = _png_to_jpg_b64(res["grown_png"]) if res["grown_png"] else None # 生发图 JPG
data = {
"hair_growth_image_base64": grown_b64,
"hairline_type": "custom",
}
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("接口3 处理异常")
return err(1007, f"处理失败:{ex}")
"""接口3 已屏蔽:调用直接返回错误,不再执行生发/ComfyUI 逻辑。
保留路由(含原参数签名)避免老客户端裸 404,multipart 入参仍被接受但不处理。"""
return err(1007, "接口3/api/v1/hair/grow-b)已屏蔽,暂不提供服务")
# ---------------------------------------------------------------------------
@@ -949,56 +861,28 @@ async def hair_grow_b(
@app.post(
"/api/v1/face/features",
summary="接口4 用户特征分析",
summary="接口4 用户特征分析(仅网关)",
tags=["人脸分析"],
description=f"""
输入用户照片,返回 N 个用户面部特征字段。
description="""
**本接口不在 worker 实现。** 请调用网关(本机默认 `http://127.0.0.1:8080`)的同路径;
网关本机调火山方舟豆包视觉模型,不转发到 worker。
{_image_fields_desc}
图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。
---
由**火山方舟 豆包视觉模型**分析,返回**固定 6 个英文字段**。
**返回格式**`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。
| 字段 | 说明 |
|------|------|
| face_shape | 脸形(如"鹅蛋脸" |
| eyebrow_shape | 眉形(如"平眉" |
| facial_age | 面部年龄(区间,如"18-25岁" |
| dynamic_static_type | 动静类型("静态型"/"动态型" |
| gender | 性别(""/"" |
| gene_style | 基因风格(如"自然型" |
> 无人脸返回 `1001`。
直接打 worker(如 `:8187`)会返回错误,避免误用假数据。
""",
responses={
200: {
"description": "成功",
"description": "worker 不提供本接口",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"code": 1007,
"message": "接口4 仅在网关实现,请访问网关(本机默认 :8080),worker 不提供本接口",
"request_id": "mock-request-id",
"data": {
"features": '{"face_shape":"鹅蛋脸","eyebrow_shape":"平眉","facial_age":"18-25岁","dynamic_static_type":"静态型","gender":"","gene_style":"少年型"}',
},
"data": None,
}
}
},
},
400: {
"description": "参数错误 / 图片识别失败",
"content": {
"application/json": {
"example": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None}
}
},
},
},
)
async def face_features(
@@ -1006,16 +890,12 @@ async def face_features(
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
):
# ⚠️ 接口4 已迁到**网关本机**实现(直接调豆包视觉模型,见 gateway/app.py)。
# 网关不会把本接口转发到 worker,故此处仅留 Mock 占位、保持 worker 无外网依赖
features = json.dumps(
{
"face_shape": "鹅蛋脸", "eyebrow_shape": "平眉", "facial_age": "18-25岁",
"dynamic_static_type": "静态型", "gender": "", "gene_style": "少年型",
},
ensure_ascii=False,
# 接口4 只在网关实现(gateway/app.py → face_features.analyze_features)。
# 不再返回 Mock 成功数据,避免本机打 :8187 时被假结果误导
return err(
1007,
"接口4 仅在网关实现,请访问网关(本机默认 :8080),worker 不提供本接口",
)
return ok({"features": features})
# ---------------------------------------------------------------------------
@@ -1042,6 +922,8 @@ async def face_features(
`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 生发,全流程最耗时)。
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
**返回说明**
@@ -1050,8 +932,10 @@ async def face_features(
- `grown_image_url`:该发型的**生发图**(生发失败时为 `null`)。
- `hairline_type`:发际线类型 key。
worker 返回 `*_base64`,网关落盘后改写为 `*_url`。
- `best_hairline_center_point`**首个选中发型**的 middle 档发际线曲线**面部中间点**坐标,
- `best_hairline_center_point`**首个选中发型**的 **middle 档**发际线曲线**面部中间点**坐标,
以**原图像素**为基准(左上角为原点,x 向右,y 向下)。
- `high_hairline_center_point`:同上,**high 档**发际线中点。
- `low_hairline_center_point`:同上,**low 档**发际线中点。
""",
responses={
200: {
@@ -1068,6 +952,8 @@ async def face_features(
{"hairline_type": "flower", "image_middle_base64": "iVBORw0KGgo...", "image_high_base64": "iVBORw0KGgo...", "image_low_base64": "iVBORw0KGgo...", "grown_image_base64": None, "order": 2},
],
"best_hairline_center_point": {"x": 540, "y": 430},
"high_hairline_center_point": {"x": 540, "y": 380},
"low_hairline_center_point": {"x": 540, "y": 480},
"face_measure": {
"face_total_height_cm": 13.76,
"four_courts": {
@@ -1115,7 +1001,8 @@ async def hairline_generate(
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的文本"),
prompt: str = Form(default="充遮罩区域的头发", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
):
if gender not in ("male", "female"):
return err(1004, "gender 必填且只能为 male / female")
@@ -1139,7 +1026,8 @@ async def hairline_generate(
from hairline.service import generate_hairline_pngs
res = await run_in_threadpool(
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt)
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
generate_grow_image=generate_grow_image)
if res is None:
return err(1001, "无法识别人像")
@@ -1155,10 +1043,14 @@ async def hairline_generate(
if it.get("grown_png") else None),
"order": it["order"],
})
c = res["best_center"]
c = res.get("best_centers") or {}
def _pt(p):
return ({"x": p[0], "y": p[1]} if p else None)
data = {
"hairline_images": hairline_images,
"best_hairline_center_point": ({"x": c[0], "y": c[1]} if c else None),
"best_hairline_center_point": _pt(c.get("middle")),
"high_hairline_center_point": _pt(c.get("high")),
"low_hairline_center_point": _pt(c.get("low")),
}
# face_measure:复用接口1测量数值(四庭/七眼 eye1~eye7/landmarks/姿态),不含标注图。
@@ -1395,8 +1287,16 @@ async def hairline_grow(
edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"),
denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"),
mb_levels: int = Form(default=5, description="多频段金字塔层数(2~6,越大低频色差抹得越宽),默认 5"),
blend_method: str = Form(default="multiband", description="接缝融合方法:multiband(多频段金字塔,默认) | seamless(泊松无缝克隆) | two_stage(泊松→多频段两段式,大色差场景) | feather(高斯羽化) | alpha_gradient(距离变换内渐变)"),
color_match: bool = Form(default=True, description="融合前 Reinhard 颜色迁移消除整体色差(对 multiband/feather/alpha_gradient 有效;seamless/two_stage 自带调色故跳过),默认 True"),
color_match_strength: float = Form(default=1.0, description="颜色迁移强度(0~1,1=全迁移,<1 只迁移部分防过度改色),默认 1.0"),
mb_feather_px: int = Form(default=1, description="多频段最细层掩码轻羽化像素(0=不羽化,消除发丝边缘锯齿),默认 1"),
transition_band_px: int = Form(default=-1, description="keep-region 过渡带边距(-1=自动按层数 2**n,>=0 用绝对像素与层数解耦),默认 -1"),
inpainting_fill: int = Form(default=1, description="change_hair服务端重绘填充:0=保留原图(治染绿) | 1=填充噪声(默认/原始) | 2=纯色 | 3=潜变量噪声。默认 1"),
mask_blur: int = Form(default=11, description="change_hair服务端遮罩边缘模糊像素(原始11,越大颜色越易从边缘渗透),默认 11"),
mask_dilate_scale: float = Form(default=1.0, description="change_hair服务端遮罩膨胀缩放(1.0=原始核尺寸,<1收缩防越界),默认 1.0"),
):
"""接口11:发际线生发 + 分步可视化。遮罩固定 pushed融合固定 multiband。"""
"""接口11:发际线生发 + 分步可视化**不含重绘**,重绘见接口12。遮罩固定 pushed融合默认 multiband(可选 seamless/two_stage/feather"""
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None:
return e
@@ -1410,8 +1310,12 @@ async def hairline_grow(
from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError
from uuid import uuid4 as _uuid4
rid = _uuid4().hex[:8]
logger.info("[%s] 接口11 收到请求: hairline_push_cm=%s hairline_edge=%s mb_levels=%s",
rid, hairline_push_cm, hairline_edge, mb_levels)
logger.info("[%s] 接口11 收到请求: hairline_push_cm=%s hairline_edge=%s mb_levels=%s "
"blend=%s color_match=%s cm_strength=%s mb_feather_px=%s transition_band_px=%s "
"inpainting_fill=%s mask_blur=%s mask_dilate_scale=%s",
rid, hairline_push_cm, hairline_edge, mb_levels, blend_method,
color_match, color_match_strength, mb_feather_px, transition_band_px,
inpainting_fill, mask_blur, mask_dilate_scale)
try:
data = await run_in_threadpool(
generate_hairline_grow, image, hairline_id,
@@ -1419,7 +1323,11 @@ async def hairline_grow(
edge_erode_px=edge_erode_px, denoising_strength=denoising_strength,
gen_backend=gen_backend, hairgrow_strength=hairgrow_strength,
mb_levels=mb_levels, hairline_push_cm=hairline_push_cm,
hairline_edge=hairline_edge, rid=rid)
hairline_edge=hairline_edge, blend_method=blend_method,
color_match=color_match, color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, rid=rid)
except NoFaceError:
return err(1001, "无法识别人像")
except SwapError as se:
@@ -1432,20 +1340,33 @@ async def hairline_grow(
# ---------------------------------------------------------------------------
# 接口 12:发际线生发(接口11 固定参数精简版
# 接口 12:发际线带重绘(调用接口11 的 ④final 作输入 + ⑤-①重绘带作遮罩,Flux-2 重绘
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/hairline/grow_v2",
summary="接口12 发际线生发(固定 pushed 遮罩 + 多频段融合,仅返回最终图",
summary="接口12 发际线带重绘(接口11 final + 发际线重绘带 → Flux-2 保色重绘",
tags=["生发"],
description=f"""
接口11 的固定参数精简版,适合生产直调。与接口11 共用同一管线,遮罩固定 pushed(发际线外推)
融合固定 multiband(多频段金字塔)。本接口固定 `erode_cm=0.6`、`mb_levels=5` 不暴露,
**只返回 `final_base64`**(最终合成图),不附带分步可视化。
接口12 是接口11 的**下游重绘阶段**。内部先跑接口11 核心管线拿到 **④ 接缝融合最终图(final)**
再取 **⑤-① 发际线重绘带**(发际线沿外推方向 `band_lo_mult×push` ~ `band_hi_mult×push`
之间、经 ①-a baseline 截断只留上部的带状区域,默认 0.5×~1.5×push
作为遮罩,用 **Flux-2ComfyUI** 做 reference-latent 保色重绘(不易染绿),重绘结果再与 final 融合。
其余参数(hairline_id、seg_model、gen_backend、is_hr、denoising_strength、edge_erode_px、
hairline_push_cm、hairline_edge 等)保留为可选 Form,调用方可按需覆盖。
与接口11 的关系:接口11 只负责生成 final(不再含重绘);接口12 负责在 final 上做发际线带重绘。
接口11 的可调参数(seg_model/gen_backend/hairline_push_cm/blend_method/color_match 等)
在本接口同样暴露,用于内部生成 final 与重绘带;另有 `comfyui_prompt` 控制 Flux-2 提示词。
⚠️ 依赖 ComfyUI(默认 :8188)在跑,否则重绘失败会在 `data.redraw.c_error` 报告。
**同时产出两版结果供对比**
- `redraw_full`ComfyUI 整帧输出(全脸美颜 + 全脸重绘),与手动跑 ComfyUI 一致。
- `redraw_band`:加发只在发际线带、美颜保留全脸(band 内用 ComfyUI 重绘,band 外 = final 结构
+ 按 `beauty_alpha` 融入全脸美颜)。
返回 `data.steps``input`(原图)/ `final`(接口11 的 ④,重绘输入基底)/
`redraw_band_overlay`(⑤-① 重绘带可视化)/ `redraw_full`A 整帧)/ `redraw_band`B 局部加发+全脸美颜)/
`redraw_c`(兼容旧字段,=redraw_full)。
{_image_fields_desc}
""",
@@ -1459,13 +1380,27 @@ async def hairline_grow_v2(
hairgrow_strength: float = Form(default=0.75, description="区域生发强度(仅 hairgrow 后端),默认 0.75"),
is_hr: bool = Form(default=False, description="高清模式(换发型输出 1152×1536,否则 576×768"),
seg_model: str = Form(default="segformer", description="头发分割模型:bisenet | segformer(默认 segformer"),
erode_cm: float = Form(default=0.6, description="baseline 参考内缩距离(厘米),默认 0.6"),
swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask | as_is(默认 ext_mask"),
edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"),
denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"),
mb_levels: int = Form(default=5, description="多频段金字塔层数(2~6),默认 5"),
hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米),默认 1.0"),
hairline_edge: str = Form(default="column", description="发际线提取方式:column | contour,默认 column"),
blend_method: str = Form(default="multiband", description="接缝融合方法:multiband | seamless | two_stage | feather | alpha_gradient"),
color_match: bool = Form(default=True, description="融合前 Reinhard 颜色迁移消除整体色差,默认 True"),
color_match_strength: float = Form(default=1.0, description="颜色迁移强度(0~1),默认 1.0"),
mb_feather_px: int = Form(default=1, description="多频段最细层掩码轻羽化像素,默认 1"),
transition_band_px: int = Form(default=-1, description="keep-region 过渡带边距(-1=自动),默认 -1"),
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 用默认「填充遮罩区域的头发」"),
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"),
):
"""接口12:发际线生发(固定 pushed 遮罩 + multiband 融合,仅返回最终图)"""
"""接口12:发际线带重绘。同时产出 redraw_full(整帧美颜) 与 redraw_band(局部加发+全脸美颜) 两版对比"""
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None:
return e
@@ -1476,43 +1411,190 @@ async def hairline_grow_v2(
try:
from fastapi.concurrency import run_in_threadpool
from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError
from face_analysis.hairline_grow import generate_hairline_redraw, NoFaceError, SwapError
from uuid import uuid4 as _uuid4
rid = _uuid4().hex[:8]
logger.info("[%s] 接口12 收到请求: hairline_id=%s hairline_push_cm=%s blend=%s comfyui_prompt=%r",
rid, hairline_id, hairline_push_cm, blend_method, comfyui_prompt)
try:
# 固定:mask_type=pushed、blend_method=multiband、erode_cm=0.6、mb_levels=5
data = await run_in_threadpool(
generate_hairline_grow, image, hairline_id,
is_hr=is_hr, seg_model=seg_model, erode_cm=0.6, swap_mode=swap_mode,
generate_hairline_redraw, image, hairline_id,
is_hr=is_hr, seg_model=seg_model, erode_cm=erode_cm, swap_mode=swap_mode,
edge_erode_px=edge_erode_px, denoising_strength=denoising_strength,
gen_backend=gen_backend, hairgrow_strength=hairgrow_strength,
mb_levels=5, hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge)
mb_levels=mb_levels, hairline_push_cm=hairline_push_cm,
hairline_edge=hairline_edge, blend_method=blend_method,
color_match=color_match, color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, comfyui_prompt=comfyui_prompt,
beauty_alpha=beauty_alpha, band_lo_mult=band_lo_mult,
band_hi_mult=band_hi_mult, rid=rid)
except NoFaceError:
return err(1001, "无法识别人像")
except SwapError as se:
return err(1007, f"换发型失败:{se}")
# 精简返回:只取最终合成图,丢掉接口11 的全部分步可视化
return ok({
"hairline_id": data["hairline_id"],
"image_size": data["image_size"],
"final_base64": data["steps"]["final_base64"],
})
logger.info("[%s] 接口12 成功返回", rid)
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("接口12 处理异常")
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------
# 接口 12 final:精简版发际线带重绘(仅需图片 + 发型 ID,其余参数全用默认值)
# ---------------------------------------------------------------------------
# 接口12 final 固化的默认参数(= test_interface12.html 当前默认值,color_match 关闭)
_V2_FINAL_DEFAULTS = dict(
gen_backend="swaphair", hairgrow_strength=0.75, is_hr=False, seg_model="segformer",
erode_cm=0.6, swap_mode="ext_mask", edge_erode_px=3, denoising_strength=0.6,
mb_levels=5, hairline_push_cm=0.8, hairline_edge="column", blend_method="two_stage",
color_match=False, color_match_strength=0.4, mb_feather_px=1, transition_band_px=-1,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0, comfyui_prompt=None,
beauty_alpha=0.6, band_lo_mult=0.5, band_hi_mult=1.5,
)
async def _run_v2_final(image_file, image_url, image_base64, hairline_id, tag):
"""接口12 final / final v2 共用:仅需图片 + hairline_id,其余用固化默认值。
两者后端计算完全一致(同一次 ComfyUI 输出同时含 A 整帧与 B 局部+美颜),
差异仅在配套测试页展示哪一版。"""
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None:
return e
image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
if image is None:
return err(1008, "图片格式不支持(仅 JPG / PNG)")
try:
from fastapi.concurrency import run_in_threadpool
from face_analysis.hairline_grow import generate_hairline_redraw, NoFaceError, SwapError
from uuid import uuid4 as _uuid4
rid = _uuid4().hex[:8]
logger.info("[%s] %s 收到请求: hairline_id=%s(其余用默认值)", rid, tag, hairline_id)
try:
data = await run_in_threadpool(
generate_hairline_redraw, image, hairline_id,
rid=rid, **_V2_FINAL_DEFAULTS)
except NoFaceError:
return err(1001, "无法识别人像")
except SwapError as se:
return err(1007, f"换发型失败:{se}")
logger.info("[%s] %s 成功返回", rid, tag)
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("%s 处理异常", tag)
return err(1007, f"处理失败:{ex}")
@app.post(
"/api/v1/hairline/grow_v2_final",
summary="接口12 final 精简重绘(整帧重绘;仅图片 + 发型 ID)",
tags=["生发"],
description=f"""
接口12 的**精简/生产版**:只需上传图片 + 选择 `hairline_id`,其余所有参数固化为当前调优默认值
hairline_push_cm=0.8 / blend_method=two_stage / **color_match=关闭** / color_match_strength=0.4 /
beauty_alpha=0.6 / band_lo_mult=0.5 / band_hi_mult=1.5 等)。
**最终重绘取整帧重绘**`data.steps.redraw_full`,全脸美颜 + 全脸重绘)。
返回结构与接口12 一致(`data.steps` 同时含 `redraw_full`A 整帧)/ `redraw_band`B 局部+美颜))。
⚠️ 依赖 ComfyUI(默认 :8188)在跑。
{_image_fields_desc}
""",
)
async def hairline_grow_v2_final(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
hairline_id: str = Form(..., description="发际线类型 ID= change_hair hair_id,如 chang_bolang"),
):
"""接口12 final:整帧重绘版,仅需图片 + hairline_id。"""
return await _run_v2_final(image_file, image_url, image_base64, hairline_id, "接口12final")
@app.post(
"/api/v1/hairline/grow_v2_final_v2",
summary="接口12 final v2 精简重绘(B 局部加发+全脸美颜;仅图片 + 发型 ID)",
tags=["生发"],
description=f"""
接口12 final 的**局部加发版**:参数与 `grow_v2_final` 完全相同(color_match 关闭等),
唯一区别是**最终重绘取 B 局部加发+全脸美颜**`data.steps.redraw_band`
加发只在发际线带内、band 外保留 final 结构并按 beauty_alpha 融入全脸美颜)。
返回结构与接口12 一致(`data.steps` 同时含 `redraw_full`A 整帧)/ `redraw_band`B 局部+美颜))。
⚠️ 依赖 ComfyUI(默认 :8188)在跑。
{_image_fields_desc}
""",
)
async def hairline_grow_v2_final_v2(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
hairline_id: str = Form(..., description="发际线类型 ID= change_hair hair_id,如 chang_bolang"),
):
"""接口12 final v2:B 局部加发+全脸美颜版,仅需图片 + hairline_id。"""
return await _run_v2_final(image_file, image_url, image_base64, hairline_id, "接口12finalv2")
# ---------------------------------------------------------------------------
# 重绘端点(替代 local_test /api/generate
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/redraw",
summary="ComfyUI 重绘",
tags=["重绘"],
description="""
传入人物图片 + 遮罩图片,直接调 ComfyUI0716add-hair 工作流)执行局部重绘。
替代原 local_test :8899 的 /api/generate 接口。
**遮罩图片格式**:支持红色遮罩(R=255)、白色遮罩(R=G=B=255)、Alpha遮罩(A=255),服务取所有通道最大值。
**遮罩区域**表示需要重绘的部分,非遮罩区域保持原图不变。
""",
)
async def api_redraw(
image_file: UploadFile = File(..., description="人物图片(JPG/PNG"),
mask_file: UploadFile = File(..., description="遮罩图片(PNG,支持红/白/alpha 格式)"),
prompt: str = Form(default="填充遮罩区域的头发",
description="ComfyUI 提示词"),
):
image_bytes = await image_file.read()
mask_bytes = await mask_file.read()
from fastapi.concurrency import run_in_threadpool
from hairline.redraw import run_redraw
try:
png_bytes = await run_in_threadpool(
run_redraw, image_bytes, mask_bytes, prompt)
except Exception as e: # noqa: BLE001
logger.warning("重绘失败: %s", e)
return err(500, f"重绘失败: {e}")
b64 = base64.b64encode(png_bytes).decode()
return ok({"image_base64": f"data:image/png;base64,{b64}"})
# ---------------------------------------------------------------------------
# 调试:下载后端日志(接口11 遮罩计算全过程)
# ---------------------------------------------------------------------------
@app.get("/api/v1/debug/hairline_log", include_in_schema=False)
async def download_hairline_log(rid: Optional[str] = None, tail: int = 500):
"""返回 /home/xsl/hair/log/hairline_grow.log 的内容。
"""返回 <仓库根>/log/hairline_grow.log 的内容。
rid 非空时只返回该 request id 相关的行;tail 限制返回最后 N 行(默认 500)。
供调试页"下载日志"按钮调用。
"""
from fastapi.responses import PlainTextResponse
log_path = "/home/xsl/hair/log/hairline_grow.log"
log_path = os.getenv(
"HAIR_LOG_DIR",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "log"),
)
log_path = os.path.join(log_path, "hairline_grow.log")
try:
with open(log_path, encoding="utf-8") as fh:
lines = fh.readlines()
@@ -1524,6 +1606,84 @@ async def download_hairline_log(rid: Optional[str] = None, tail: int = 500):
return PlainTextResponse("".join(lines), media_type="text/plain; charset=utf-8")
# ---------------------------------------------------------------------------
# 调试:MediaPipe 脸型分类(face/face_shape_classifier.py,非接口4 豆包)
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/debug/face-shape",
summary="调试 单张脸型分类(MediaPipe)",
tags=["调试"],
description="""
离线脸型分类调试接口(`face/face_shape_classifier.py`),**不是**接口4 的豆包视觉分析。
上传正面照 → MediaPipe 468 点 → 7 类脸型评分 + 特征标注图。
""",
include_in_schema=True,
)
async def debug_face_shape(
image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG"),
image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64"),
):
raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e:
return e
try:
nparr = np.frombuffer(raw, np.uint8)
bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if bgr is None:
return err(1008, "图片格式不支持(仅 JPG / PNG)")
except Exception: # noqa: BLE001
return err(1008, "图片格式不支持(仅 JPG / PNG)")
def _jsonable(obj):
if isinstance(obj, dict):
return {k: _jsonable(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_jsonable(v) for v in obj]
if hasattr(obj, "item"):
return obj.item()
if isinstance(obj, (float, int, str, bool)) or obj is None:
return obj
return obj
from fastapi.concurrency import run_in_threadpool
try:
from face.face_shape_classifier import classify_from_image
result = await run_in_threadpool(
classify_from_image, bgr, True, True,
)
except ValueError as ex:
return err(1001, str(ex) or "无法识别人像")
except Exception as ex: # noqa: BLE001
return err(1007, f"脸型分类失败:{ex}")
details = result.get("details") or {}
ranked = [
{"shape": name, "score": round(float(score), 2)}
for name, score in (details.get("ranked") or [])
]
annotated = result.pop("annotated", None)
h, w = bgr.shape[:2]
data = {
"face_shape": result["face_shape"],
"display": result["display"],
"confidence": round(float(result["confidence"]), 4),
"is_mixed": bool(details.get("is_mixed")),
"second_shape": details.get("second_shape"),
"score_gap": round(float(details["score_gap"]), 2) if details.get("score_gap") is not None else None,
"ranked": ranked,
"features": _jsonable(result.get("features") or {}),
"zscores": _jsonable(details.get("zscores") or {}),
"image_size": {"width": w, "height": h},
"annotated_image_base64": _jpg_b64(annotated) if annotated is not None else None,
}
return ok(data)
# ---------------------------------------------------------------------------
# 健康检查
# ---------------------------------------------------------------------------
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""9B vs 4B 模型对比:3 张图 × 5 种发型 = 15 张对比图。
第一次运行:PLAN_TAG=A (9B 模型,需先切工作流到 .bak)
第二次运行:PLAN_TAG=Bplus4B 模型,需切回 4B 工作流)
"""
import json, os, time, base64, subprocess, urllib.request
from datetime import datetime
API_BASE = "http://127.0.0.1:8187"
TOKEN = "dev-shared-secret-2026"
IMAGES = [
"/home/ubuntu/hair/image/girl_img/girl1.jpg",
"/home/ubuntu/hair/image/girl_img/girl7.jpg",
"/home/ubuntu/hair/image/girl_img/girl13.jpg",
]
PLAN_TAG = os.getenv("PLAN_TAG", "A")
OUT_DIR = f"/home/ubuntu/hair/benchmark_out/9b_vs_4b_{PLAN_TAG}"
RESULT_JSON = os.path.join(OUT_DIR, "results.json")
REPORT_HTML = os.path.join(OUT_DIR, "report.html")
HAIR_STYLES = [(1, "椭圆"), (2, "花瓣"), (3, "心形"), (4, "直线"), (5, "波浪")]
WARMUP = os.getenv("WARMUP", "1") == "1" # 第一次调用做预热
os.makedirs(OUT_DIR, exist_ok=True)
def get_vram():
try:
out = subprocess.check_output(
["nvidia-smi", "--query-gpu=memory.used,memory.free",
"--format=csv,noheader,nounits"], text=True, timeout=5).strip()
used, free = out.split(",")
return int(used.strip()), int(free.strip())
except Exception:
return -1, -1
def _multipart(fields, files=None):
boundary = "----CmpTest" + str(int(time.time() * 1000))
parts = []
for k, v in fields.items():
parts.append(f"--{boundary}\r\n".encode())
parts.append(f'Content-Disposition: form-data; name="{k}"\r\n\r\n'.encode())
parts.append(str(v).encode())
parts.append(b"\r\n")
if files:
for field_name, (filename, data, mime) in files.items():
parts.append(f"--{boundary}\r\n".encode())
parts.append(f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"\r\n'.encode())
parts.append(f"Content-Type: {mime}\r\n\r\n".encode())
parts.append(data)
parts.append(b"\r\n")
parts.append(f"--{boundary}--\r\n".encode())
return b"".join(parts), boundary
def call_iface2(image_path, hair_style):
with open(image_path, "rb") as f:
img_data = f.read()
fields = {"gender": "female", "hair_style": str(hair_style),
"prompt": "填充遮罩区域的头发"}
body, boundary = _multipart(
fields, {"image_file": (os.path.basename(image_path), img_data, "image/jpeg")})
req = urllib.request.Request(f"{API_BASE}/api/v1/hair/grow", data=body, method="POST")
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
req.add_header("X-Internal-Token", TOKEN)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=600) as resp:
data = json.loads(resp.read())
return data, time.time() - t0, None
except Exception as e:
return {}, time.time() - t0, str(e)
def extract_image(data, img_idx, hs):
"""从响应提取 grown_image_base64 并保存为 jpg。"""
if not data or not data.get("data"):
return None
items = data["data"].get("results") or []
if not items:
return None
b64 = items[0].get("grown_image_base64") or ""
if b64.startswith("data:"):
b64 = b64.split(",", 1)[1]
if not b64:
return None
fname = f"img{img_idx}_style{hs}.jpg"
path = os.path.join(OUT_DIR, fname)
with open(path, "wb") as f:
f.write(base64.b64decode(b64))
return path
def run_test():
results = []
print(f"9B vs 4B 对比测试 — Plan={PLAN_TAG}")
print(f"图片: {[os.path.basename(p) for p in IMAGES]}")
print(f"发型: {[(s,n) for s,n in HAIR_STYLES]}")
print(f"总调用: {len(IMAGES)*len(HAIR_STYLES)}\n")
# 预热请求(避免第一次冷启动计入统计)
if WARMUP:
print("[warmup] 预热请求 (girl13, style=1) ...")
t0 = time.time()
_, warmup_time, _ = call_iface2(IMAGES[2], 1)
print(f" warmup: {warmup_time:.1f}s\n")
for img_idx, img_path in enumerate(IMAGES, start=1):
img_name = os.path.basename(img_path)
for hs, hs_name in HAIR_STYLES:
print(f"[img{img_idx}/{len(IMAGES)}] {img_name} style={hs}({hs_name}) ...")
vram_before, _ = get_vram()
data, elapsed, error = call_iface2(img_path, hs)
vram_after, _ = get_vram()
code = data.get("code", -1) if data else -1
ok = (code == 0)
img_saved = extract_image(data, img_idx, hs) if ok else None
record = {
"plan": PLAN_TAG,
"image_idx": img_idx,
"image_name": img_name,
"image_path": img_path,
"hair_style": hs,
"hair_style_name": hs_name,
"timestamp": datetime.now().strftime("%H:%M:%S"),
"elapsed_s": round(elapsed, 2),
"success": ok,
"code": code,
"error": error,
"vram_before_mb": vram_before,
"vram_after_mb": vram_after,
"vram_delta_mb": vram_after - vram_before,
"saved_image_path": img_saved,
}
results.append(record)
status = "" if ok else ""
print(f"{status} code={code} time={elapsed:.1f}s "
f"vram={vram_before}{vram_after}MB (Δ{vram_after-vram_before:+d}) "
f"img={'saved' if img_saved else 'none'}")
with open(RESULT_JSON, "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n✅ 完成!{RESULT_JSON}")
print(f"📊 成功: {sum(1 for r in results if r['success'])}/{len(results)}")
if any(r['success'] for r in results):
avg = sum(r['elapsed_s'] for r in results if r['success']) / sum(1 for r in results if r['success'])
print(f"⏱ 平均: {avg:.2f}s")
if __name__ == "__main__":
run_test()
+450
View File
@@ -0,0 +1,450 @@
#!/usr/bin/env python3
"""基准测试:19张图片 × 5种发际线,记录每步耗时和显存变化。
用法: python3 benchmark_grow.py
输出: benchmark_results.json + benchmark_report.html
"""
import json, os, time, base64, subprocess, re, glob, shutil
from datetime import datetime
from pathlib import Path
import urllib.request, urllib.error
API_URL = "http://127.0.0.1:8187/api/v1/hair/grow"
TOKEN = "dev-shared-secret-2026"
IMG_DIR = "/home/ubuntu/hair/image/girl_img"
OUT_DIR = "/home/ubuntu/hair/benchmark_out"
RESULT_JSON = os.path.join(OUT_DIR, "benchmark_results.json")
REPORT_HTML = os.path.join(OUT_DIR, "benchmark_report.html")
HAIRLINES = [
("1", "ellipse", "椭圆形"),
("2", "flower", "花瓣形"),
("3", "heart", "心形"),
("4", "straight", "直线形"),
("5", "wave", "波浪形"),
]
os.makedirs(OUT_DIR, exist_ok=True)
# ── 工具函数 ──────────────────────────────────────────────
def get_vram():
"""返回 (used_MB, free_MB)"""
try:
out = subprocess.check_output(
["nvidia-smi", "--query-gpu=memory.used,memory.free",
"--format=csv,noheader,nounits"], text=True, timeout=5
).strip()
used, free = out.split(",")
return int(used.strip()), int(free.strip())
except Exception:
return -1, -1
def get_gpu_procs():
"""返回各进程显存占用 dict"""
try:
out = subprocess.check_output(
["nvidia-smi", "--query-compute-apps=pid,used_memory",
"--format=csv,noheader,nounits"], text=True, timeout=5
).strip()
procs = {}
for line in out.splitlines():
parts = line.split(",")
if len(parts) >= 2:
procs[parts[0].strip()] = int(parts[1].strip())
return procs
except Exception:
return {}
def read_worker_log_tail(n=50):
"""读取 worker.log 最后 n 行"""
log_path = "/home/ubuntu/hair/worker.log"
try:
result = subprocess.run(["tail", "-n", str(n), log_path],
capture_output=True, text=True, timeout=5)
return result.stdout
except Exception:
return ""
def call_api(image_path, hair_style_value):
"""调用接口2,返回 (json_dict, elapsed_sec, error_or_None)"""
import mimetypes
boundary = "----BenchmarkBoundary" + str(int(time.time()*1000))
filename = os.path.basename(image_path)
mime = mimetypes.guess_type(image_path)[0] or "image/jpeg"
with open(image_path, "rb") as f:
img_data = f.read()
body_parts = []
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(f'Content-Disposition: form-data; name="image_file"; filename="{filename}"\r\n'.encode())
body_parts.append(f"Content-Type: {mime}\r\n\r\n".encode())
body_parts.append(img_data)
body_parts.append(f"\r\n--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="gender"\r\n\r\n')
body_parts.append(b"female")
body_parts.append(f"\r\n--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="hair_style"\r\n\r\n')
body_parts.append(hair_style_value.encode())
body_parts.append(f"\r\n--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="use_mask"\r\n\r\n')
body_parts.append(b"0")
body_parts.append(f"\r\n--{boundary}\r\n".encode())
body_parts.append(b'Content-Disposition: form-data; name="prompt"\r\n\r\n')
body_parts.append(b"\xe5\xa1\xab\xe5\x85\x85\xe9\x81\xae\xe7\xbd\xa9\xe5\x8c\xba\xe5\x9f\x9f\xe7\x9a\x84\xe5\xa4\xb4\xe5\x8f\x91\xef\xbc\x8c\xe7\x9a\xae\xe8\x82\xa4\xe5\x8a\xa0\xe4\xb8\x80\xe7\x82\xb9\xe7\xa3\xa8\xe7\x9a\xae")
body_parts.append(f"\r\n--{boundary}--\r\n".encode())
body = b"".join(body_parts)
req = urllib.request.Request(API_URL, data=body, method="POST")
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
req.add_header("X-Internal-Token", TOKEN)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=600) as resp:
raw = resp.read()
elapsed = time.time() - t0
data = json.loads(raw)
return data, elapsed, None
except urllib.error.HTTPError as e:
elapsed = time.time() - t0
try:
data = json.loads(e.read())
except Exception:
data = {"error": str(e)}
return data, elapsed, f"HTTP {e.code}"
except Exception as e:
elapsed = time.time() - t0
return {}, elapsed, str(e)
def parse_step_timing(log_text):
"""从 worker.log 文本中解析步骤耗时"""
timing = {}
for line in log_text.splitlines():
# 匹配 "步骤1 遮罩完成 耗时=949ms"
m = re.search(r"步骤(\d+)\s+\S+\s+耗时=(\d+)ms", line)
if m:
timing[f"step{m.group(1)}_ms"] = int(m.group(2))
# 匹配 "人脸检出 px_per_cm=30.669 图尺寸=813x967"
if "人脸检出" in line:
m2 = re.search(r"px_per_cm=([\d.]+)", line)
if m2:
timing["px_per_cm"] = float(m2.group(1))
# 匹配 "头发分割完成"
if "头发分割完成" in line:
timing["hair_seg"] = True
# 匹配 "换发型图失败"
if "换发型图失败" in line or "换发型服务不可达" in line:
timing["swap_error"] = line.strip()[-100:]
# 匹配 "重绘" 相关
if "重绘" in line and ("完成" in line or "失败" in line):
timing["redraw_status"] = "完成" if "完成" in line else "失败"
return timing
# ── 主流程 ────────────────────────────────────────────────
def run_benchmark():
images = sorted(glob.glob(os.path.join(IMG_DIR, "girl*.jpg")))
print(f"找到 {len(images)} 张图片")
# 加载已有结果(支持断点续跑)
results = []
if os.path.exists(RESULT_JSON):
with open(RESULT_JSON) as f:
results = json.load(f)
print(f"已有 {len(results)} 条记录,继续未完成的测试")
total_calls = len(images) * len(HAIRLINES)
done = len(results)
print(f"总计 {total_calls} 次调用,已完成 {done},剩余 {total_calls - done}")
log_offset = 0
try:
log_offset = subprocess.check_output(["wc", "-l", "/home/ubuntu/hair/worker.log"],
text=True, timeout=5).split()[0]
log_offset = int(log_offset)
except Exception:
pass
for img_idx, img_path in enumerate(images):
img_name = os.path.basename(img_path)
# 跳过已完成的图片
img_results = [r for r in results if r["image"] == img_name]
if len(img_results) >= len(HAIRLINES):
print(f"[{img_idx+1}/{len(images)}] {img_name} 已完成,跳过")
continue
for hl_id, hl_key, hl_label in HAIRLINES:
# 跳过已完成的
existing = [r for r in results if r["image"] == img_name and r["hairline_id"] == hl_id]
if existing:
continue
print(f"\n[{img_idx+1}/{len(images)}] {img_name}{hl_label}({hl_id}) ...")
# 记录 worker.log 行数
try:
log_before = int(subprocess.check_output(
["wc", "-l", "/home/ubuntu/hair/worker.log"], text=True, timeout=5).split()[0])
except Exception:
log_before = 0
# VRAM before
vram_before, vram_free_before = get_vram()
procs_before = get_gpu_procs()
t_start = time.time()
# 调用API
api_result, elapsed, error = call_api(img_path, hl_id)
t_end = time.time()
# VRAM after
vram_after, vram_free_after = get_vram()
procs_after = get_gpu_procs()
# 读取新日志
try:
log_diff = subprocess.check_output(
["tail", "-n", "+{}".format(log_before + 1), "/home/ubuntu/hair/worker.log"],
text=True, timeout=5)
except Exception:
log_diff = ""
step_timing = parse_step_timing(log_diff)
# 提取结果图片
preview_b64 = ""
grown_b64 = ""
api_code = api_result.get("code", -1)
api_msg = api_result.get("message", "")
api_results = api_result.get("data", {}).get("results", [])
if api_results:
r0 = api_results[0]
preview_b64 = r0.get("image_base64", "")
grown_b64 = r0.get("grown_image_base64", "")
# 保存缩略图
thumb_dir = os.path.join(OUT_DIR, "thumbs")
os.makedirs(thumb_dir, exist_ok=True)
if grown_b64:
grown_bytes = base64.b64decode(grown_b64)
thumb_path = os.path.join(thumb_dir, f"{img_name}_hl{hl_id}_grown.jpg")
with open(thumb_path, "wb") as f:
f.write(grown_bytes)
if preview_b64:
preview_bytes = base64.b64decode(preview_b64)
thumb_path = os.path.join(thumb_dir, f"{img_name}_hl{hl_id}_preview.png")
with open(thumb_path, "wb") as f:
f.write(preview_bytes)
record = {
"image": img_name,
"image_idx": img_idx + 1,
"hairline_id": hl_id,
"hairline_key": hl_key,
"hairline_label": hl_label,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_time_s": round(elapsed, 2),
"api_code": api_code,
"api_msg": api_msg,
"error": error,
"has_preview": bool(preview_b64),
"has_grown": bool(grown_b64),
"preview_size": len(preview_b64),
"grown_size": len(grown_b64),
"vram_before_mb": vram_before,
"vram_after_mb": vram_after,
"vram_free_before_mb": vram_free_before,
"vram_free_after_mb": vram_free_after,
"vram_delta_mb": vram_after - vram_before,
"procs_before": procs_before,
"procs_after": procs_after,
"step_timing": step_timing,
}
results.append(record)
print(f" → HTTP code={api_code} time={elapsed:.1f}s vram={vram_before}{vram_after}MB "
f"preview={'' if preview_b64 else ''} grown={'' if grown_b64 else ''}")
# 保存中间结果
with open(RESULT_JSON, "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
# 生成中间报告
generate_html(results)
# 最终报告
generate_html(results)
print(f"\n✅ 完成!结果: {RESULT_JSON}")
print(f"📄 报告: {REPORT_HTML}")
print(f"📊 总调用: {len(results)}/{total_calls}")
def generate_html(results):
"""生成HTML报告"""
# 统计
total = len(results)
success = sum(1 for r in results if r["has_grown"])
failed = total - success
times = [r["total_time_s"] for r in results if r["has_grown"]]
avg_time = sum(times) / len(times) if times else 0
max_time = max(times) if times else 0
min_time = min(times) if times else 0
# 按发际线类型分组统计
hl_stats = {}
for r in results:
if r["has_grown"]:
hl = r["hairline_label"]
if hl not in hl_stats:
hl_stats[hl] = {"count": 0, "times": []}
hl_stats[hl]["count"] += 1
hl_stats[hl]["times"].append(r["total_time_s"])
# 按图片分组
img_groups = {}
for r in results:
img = r["image"]
if img not in img_groups:
img_groups[img] = []
img_groups[img].append(r)
# 生成VRAM变化数据
vram_data = [(i, r["vram_after_mb"]) for i, r in enumerate(results)]
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>接口2 基准测试报告</title>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:#1a1a2e; color:#e0e0e0; padding:20px; }}
h1 {{ text-align:center; margin-bottom:20px; color:#00d4ff; }}
.subtitle {{ text-align:center; color:#888; margin-bottom:30px; font-size:14px; }}
.summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:15px; margin-bottom:30px; }}
.card {{ background:#16213e; border-radius:12px; padding:20px; text-align:center; border:1px solid #333; }}
.card .num {{ font-size:32px; font-weight:700; }}
.card .label {{ font-size:12px; color:#888; margin-top:5px; }}
.card.ok .num {{ color:#0f0; }}
.card.err .num {{ color:#f44; }}
.card.time .num {{ color:#00d4ff; }}
table {{ width:100%; border-collapse:collapse; margin-bottom:30px; background:#16213e; border-radius:12px; overflow:hidden; }}
th {{ background:#0f3460; padding:12px 8px; text-align:center; font-size:13px; color:#fff; }}
td {{ padding:8px; text-align:center; border-bottom:1px solid #222; font-size:13px; }}
tr:hover {{ background:#1a1a3e; }}
.img-cell {{ text-align:left; }}
.time-bar {{ display:inline-block; height:20px; background:linear-gradient(90deg,#0f3460,#00d4ff); border-radius:4px; vertical-align:middle; min-width:2px; }}
.hl-badge {{ display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }}
.hl-ellipse {{ background:#1b4332; color:#52b788; }}
.hl-flower {{ background:#3a0ca3; color:#c77dff; }}
.hl-heart {{ background:#6a040f; color:#ff6b6b; }}
.hl-straight {{ background:#0077b6; color:#90e0ef; }}
.hl-wave {{ background:#9d4edd; color:#e0aaff; }}
.ok {{ color:#0f0; }} .fail {{ color:#f44; }}
.vram-chart {{ margin:20px 0; }}
.vram-bars {{ display:flex; align-items:flex-end; height:120px; gap:2px; padding:10px; background:#0d1117; border-radius:8px; }}
.vram-bar {{ flex:1; background:linear-gradient(180deg,#00d4ff,#0f3460); border-radius:2px 2px 0 0; min-height:2px; position:relative; }}
.vram-bar:hover::after {{ content:attr(data-val) 'MB'; position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background:#333; padding:2px 6px; border-radius:4px; font-size:10px; white-space:nowrap; }}
.section-title {{ font-size:18px; font-weight:600; margin:30px 0 15px; color:#00d4ff; border-bottom:1px solid #333; padding-bottom:10px; }}
.hl-stats {{ display:grid; grid-template-columns:repeat(5,1fr); gap:15px; margin-bottom:20px; }}
.hl-card {{ background:#16213e; border-radius:12px; padding:15px; text-align:center; }}
.hl-card .avg {{ font-size:24px; font-weight:700; color:#00d4ff; }}
.hl-card .minmax {{ font-size:11px; color:#888; margin-top:5px; }}
.thumb {{ max-width:100px; max-height:100px; border-radius:4px; cursor:pointer; }}
.thumb:hover {{ transform:scale(2); transition:transform 0.3s; }}
</style>
</head>
<body>
<h1>📊 接口2 基准测试报告</h1>
<div class="subtitle">生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} {total} 次调用</div>
<div class="summary">
<div class="card ok"><div class="num">{success}</div><div class="label">成功</div></div>
<div class="card err"><div class="num">{failed}</div><div class="label">失败</div></div>
<div class="card time"><div class="num">{avg_time:.1f}s</div><div class="label">平均耗时</div></div>
<div class="card time"><div class="num">{min_time:.1f}s</div><div class="label">最快</div></div>
<div class="card time"><div class="num">{max_time:.1f}s</div><div class="label">最慢</div></div>
</div>
"""
# 按发际线类型统计
if hl_stats:
html += '<div class="section-title">按发际线类型统计</div><div class="hl-stats">'
for hl_label in ["椭圆形","花瓣形","心形","直线形","波浪形"]:
if hl_label in hl_stats:
s = hl_stats[hl_label]
times = s["times"]
avg = sum(times) / len(times)
mn, mx = min(times), max(times)
cls = hl_label
html += f'<div class="hl-card"><div class="hl-badge hl-{{cls}}">{hl_label}</div><div class="avg">{avg:.1f}s</div><div class="minmax">{mn:.1f}~{mx:.1f}s ({s["count"]}次)</div></div>'
else:
html += f'<div class="hl-card"><div class="hl-badge">{hl_label}</div><div class="avg">-</div><div class="minmax">未完成</div></div>'
html += '</div>'
# VRAM 变化图
if vram_data:
max_vram = max(v for _, v in vram_data if v > 0) or 1
html += '<div class="section-title">显存变化</div><div class="vram-chart"><div class="vram-bars">'
for i, (_, vram) in enumerate(vram_data):
if vram > 0:
h = int(vram / max_vram * 100)
html += f'<div class="vram-bar" style="height:{h}%" data-val="{vram}" title="{i+1}"></div>'
else:
html += f'<div class="vram-bar" style="height:0%" data-val="0"></div>'
html += '</div></div>'
# 详细结果表格
html += '<div class="section-title">详细结果</div><table><thead><tr>'
html += '<th>#</th><th>图片</th><th>发际线</th><th>总耗时</th><th>遮罩步骤</th>'
html += '<th>显存前</th><th>显存后</th><th>显存变化</th>'
html += '<th>预览图</th><th>生发图</th><th>状态</th>'
html += '</tr></thead><tbody>'
for i, r in enumerate(results):
hl_cls = r["hairline_key"]
status = '<span class="ok">✓ 成功</span>' if r["has_grown"] else f'<span class="fail">✗ {r.get("error","")}</span>'
step_ms = r.get("step_timing", {}).get("step1_ms", "")
step_str = f"{step_ms}ms" if step_ms else "-"
vram_d = r["vram_delta_mb"]
vram_d_str = f'<span style="color:{"#f44" if vram_d>0 else "#0f0"}">{"+" if vram_d>=0 else ""}{vram_d}</span>'
# 缩略图
thumb_grown = ""
if r["has_grown"]:
thumb_path = f"thumbs/{r['image']}_hl{r['hairline_id']}_grown.jpg"
if os.path.exists(os.path.join(OUT_DIR, thumb_path)):
thumb_grown = f'<img class="thumb" src="{thumb_path}">'
html += f"""<tr>
<td>{i+1}</td>
<td class="img-cell">{r['image']}</td>
<td><span class="hl-badge hl-{hl_cls}">{r['hairline_label']}</span></td>
<td><div class="time-bar" style="width:{min(r['total_time_s'],300)}px"></div> {r['total_time_s']:.1f}s</td>
<td>{step_str}</td>
<td>{r['vram_before_mb']}MB</td>
<td>{r['vram_after_mb']}MB</td>
<td>{vram_d_str}</td>
<td>{'' if r['has_preview'] else ''}</td>
<td>{thumb_grown if thumb_grown else ('' if r['has_grown'] else '')}</td>
<td>{status}</td>
</tr>"""
html += '</tbody></table>'
html += '</body></html>'
with open(REPORT_HTML, "w", encoding="utf-8") as f:
f.write(html)
if __name__ == "__main__":
run_benchmark()
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""接口2女性5种发型对比测试:Plan B vs Plan B+。
测试 hair_style=1..5(椭圆/花瓣/心形/直线/波浪),每种发型一次,记录耗时+显存+生成图。
"""
import json, os, time, base64, subprocess, urllib.request, urllib.error
from datetime import datetime
API_BASE = "http://127.0.0.1:8187"
TOKEN = "dev-shared-secret-2026"
GIRL_IMG = "/home/ubuntu/hair/image/girl_img/girl13.jpg"
PLAN_TAG = os.getenv("PLAN_TAG", "Bplus") # Bplus / B
OUT_DIR = f"/home/ubuntu/hair/benchmark_out/iface2_female_{PLAN_TAG}"
RESULT_JSON = os.path.join(OUT_DIR, "results.json")
REPORT_HTML = os.path.join(OUT_DIR, "report.html")
HAIR_STYLE_NAMES = {
"1": "椭圆", "2": "花瓣", "3": "心形", "4": "直线", "5": "波浪",
}
os.makedirs(OUT_DIR, exist_ok=True)
def get_vram():
try:
out = subprocess.check_output(
["nvidia-smi", "--query-gpu=memory.used,memory.free",
"--format=csv,noheader,nounits"], text=True, timeout=5).strip()
used, free = out.split(",")
return int(used.strip()), int(free.strip())
except Exception:
return -1, -1
def _multipart(fields, files=None):
boundary = "----If2Test" + str(int(time.time() * 1000))
parts = []
for k, v in fields.items():
parts.append(f"--{boundary}\r\n".encode())
parts.append(f'Content-Disposition: form-data; name="{k}"\r\n\r\n'.encode())
parts.append(str(v).encode())
parts.append(b"\r\n")
if files:
for field_name, (filename, data, mime) in files.items():
parts.append(f"--{boundary}\r\n".encode())
parts.append(f'Content-Disposition: form-data; name="{field_name}"; filename="{filename}"\r\n'.encode())
parts.append(f"Content-Type: {mime}\r\n\r\n".encode())
parts.append(data)
parts.append(b"\r\n")
parts.append(f"--{boundary}--\r\n".encode())
return b"".join(parts), boundary
def call_iface2_female(image_path, hair_style):
"""接口2 女性 + 指定发型(走 swapHair 路径)"""
with open(image_path, "rb") as f:
img_data = f.read()
fields = {"gender": "female", "hair_style": str(hair_style),
"prompt": "填充遮罩区域的头发"}
body, boundary = _multipart(
fields, {"image_file": (os.path.basename(image_path), img_data, "image/jpeg")})
req = urllib.request.Request(f"{API_BASE}/api/v1/hair/grow", data=body, method="POST")
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
req.add_header("X-Internal-Token", TOKEN)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=600) as resp:
data = json.loads(resp.read())
return data, time.time() - t0, None
except Exception as e:
return {}, time.time() - t0, str(e)
def run_test():
results = []
print(f"接口2女性5种发型测试 — Plan={PLAN_TAG}")
print(f"图片:{GIRL_IMG}\n")
for hs in range(1, 6):
name = HAIR_STYLE_NAMES[str(hs)]
print(f"\n[{hs}/5] hair_style={hs} ({name}) ...")
vram_before, vram_free_before = get_vram()
data, elapsed, error = call_iface2_female(GIRL_IMG, hs)
vram_after, vram_free_after = get_vram()
code = data.get("code", -1) if data else -1
ok = (code == 0)
# 保存生成图(如果有)
img_path = None
if ok and data.get("data"):
try:
items = data["data"].get("results") or []
if items and isinstance(items, list):
first = items[0]
b64 = first.get("grown_image_base64") or ""
if b64.startswith("data:"):
b64 = b64.split(",", 1)[1]
if b64:
img_path = os.path.join(OUT_DIR, f"hair_style_{hs}_{name}.jpg")
with open(img_path, "wb") as f:
f.write(base64.b64decode(b64))
except Exception as e:
print(f" 保存图片失败: {e}")
record = {
"plan": PLAN_TAG,
"hair_style": hs,
"hair_style_name": name,
"timestamp": datetime.now().strftime("%H:%M:%S"),
"elapsed_s": round(elapsed, 2),
"success": ok,
"code": code,
"error": error,
"vram_before_mb": vram_before,
"vram_after_mb": vram_after,
"vram_delta_mb": vram_after - vram_before,
"image_path": img_path,
}
results.append(record)
status = "" if ok else ""
print(f"{status} code={code} time={elapsed:.1f}s "
f"vram={vram_before}{vram_after}MB (Δ{vram_after-vram_before:+d}) "
f"img={'saved' if img_path else 'none'}")
with open(RESULT_JSON, "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
generate_html(results)
generate_html(results)
print(f"\n✅ 完成!{RESULT_JSON}")
print(f"📄 报告:{REPORT_HTML}")
def generate_html(results):
total = len(results)
success = sum(1 for r in results if r["success"])
times = [r["elapsed_s"] for r in results if r["success"]]
avg_time = sum(times) / len(times) if times else 0
html = f"""<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>接口2女5种发型 - Plan {PLAN_TAG}</title>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:20px}}
h1{{text-align:center;color:#00d4ff;margin-bottom:10px}}
.subtitle{{text-align:center;color:#888;margin-bottom:30px;font-size:14px}}
.summary{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:15px;margin-bottom:30px}}
.card{{background:#16213e;border-radius:12px;padding:20px;text-align:center;border:1px solid #333}}
.card .num{{font-size:28px;font-weight:700}}
.card .label{{font-size:12px;color:#888;margin-top:5px}}
.card.ok .num{{color:#0f0}} .card.err .num{{color:#f44}} .card.time .num{{color:#00d4ff}}
table{{width:100%;border-collapse:collapse;margin-bottom:30px;background:#16213e;border-radius:12px;overflow:hidden}}
th{{background:#0f3460;padding:10px 8px;text-align:center;font-size:13px;color:#fff}}
td{{padding:8px;text-align:center;border-bottom:1px solid #222;font-size:13px}}
tr:hover{{background:#1a1a3e}}
.section-title{{font-size:18px;font-weight:600;margin:30px 0 15px;color:#00d4ff;border-bottom:1px solid #333;padding-bottom:10px}}
.ok{{color:#0f0}} .fail{{color:#f44}}
.time-bar{{display:inline-block;height:18px;background:linear-gradient(90deg,#0f3460,#00d4ff);border-radius:3px;vertical-align:middle;min-width:2px}}
.gallery{{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px}}
.gallery img{{width:100%;border-radius:8px;border:1px solid #333}}
.gallery .item{{text-align:center}}
.gallery .cap{{margin-top:5px;font-size:12px;color:#888}}
</style></head><body>
<h1>📊 接口2女性5种发型测试报告</h1>
<div class="subtitle">Plan {PLAN_TAG} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} {total} 次调用</div>
<div class="summary">
<div class="card ok"><div class="num">{success}</div><div class="label">成功</div></div>
<div class="card err"><div class="num">{total-success}</div><div class="label">失败</div></div>
<div class="card time"><div class="num">{avg_time:.1f}s</div><div class="label">平均耗时</div></div>
</div>
<div class="section-title">详细结果</div>
<table><thead><tr><th>发型</th><th>名称</th><th>耗时</th><th>显存前</th><th>显存后</th><th>Δ</th><th>状态</th></tr></thead><tbody>
"""
for r in results:
status = '<span class="ok">✓</span>' if r["success"] else f'<span class="fail">✗ {str(r.get("error",""))[:30]}</span>'
html += f'<tr><td>style {r["hair_style"]}</td><td>{r["hair_style_name"]}</td>' \
f'<td><div class="time-bar" style="width:{min(r["elapsed_s"]*15,200)}px"></div> {r["elapsed_s"]:.1f}s</td>' \
f'<td>{r["vram_before_mb"]}MB</td><td>{r["vram_after_mb"]}MB</td>' \
f'<td>{r["vram_delta_mb"]:+d}</td><td>{status}</td></tr>'
html += '</tbody></table>'
# 图片画廊
saved = [r for r in results if r.get("image_path") and os.path.isfile(r["image_path"])]
if saved:
html += '<div class="section-title">生成图片</div><div class="gallery">'
for r in saved:
rel = os.path.relpath(r["image_path"], OUT_DIR)
html += f'<div class="item"><img src="{rel}"><div class="cap">{r["hair_style_name"]} ({r["elapsed_s"]:.1f}s)</div></div>'
html += '</div>'
html += '</body></html>'
with open(REPORT_HTML, "w", encoding="utf-8") as f:
f.write(html)
if __name__ == "__main__":
run_test()
+43
View File
@@ -0,0 +1,43 @@
nohup: ignoring input
9B vs 4B 对比测试 — Plan=A
图片: ['girl1.jpg', 'girl7.jpg', 'girl13.jpg']
发型: [(1, '椭圆'), (2, '花瓣'), (3, '心形'), (4, '直线'), (5, '波浪')]
总调用: 15
[warmup] 预热请求 (girl13, style=1) ...
warmup: 25.7s
[img1/3] girl1.jpg style=1(椭圆) ...
→ ✓ code=0 time=9.4s vram=26207→29449MB (Δ+3242) img=saved
[img1/3] girl1.jpg style=2(花瓣) ...
→ ✓ code=0 time=10.5s vram=29449→26341MB (Δ-3108) img=saved
[img1/3] girl1.jpg style=3(心形) ...
→ ✓ code=0 time=10.5s vram=26341→26051MB (Δ-290) img=saved
[img1/3] girl1.jpg style=4(直线) ...
→ ✓ code=0 time=10.4s vram=26051→26257MB (Δ+206) img=saved
[img1/3] girl1.jpg style=5(波浪) ...
→ ✓ code=0 time=10.8s vram=26257→26319MB (Δ+62) img=saved
[img2/3] girl7.jpg style=1(椭圆) ...
→ ✓ code=0 time=7.9s vram=26319→28417MB (Δ+2098) img=saved
[img2/3] girl7.jpg style=2(花瓣) ...
→ ✓ code=0 time=7.8s vram=28417→28397MB (Δ-20) img=saved
[img2/3] girl7.jpg style=3(心形) ...
→ ✓ code=0 time=7.7s vram=28397→28409MB (Δ+12) img=saved
[img2/3] girl7.jpg style=4(直线) ...
→ ✓ code=0 time=8.2s vram=28409→26335MB (Δ-2074) img=saved
[img2/3] girl7.jpg style=5(波浪) ...
→ ✓ code=0 time=7.9s vram=26335→28419MB (Δ+2084) img=saved
[img3/3] girl13.jpg style=1(椭圆) ...
→ ✓ code=0 time=8.9s vram=28419→28835MB (Δ+416) img=saved
[img3/3] girl13.jpg style=2(花瓣) ...
→ ✓ code=0 time=9.1s vram=28835→28817MB (Δ-18) img=saved
[img3/3] girl13.jpg style=3(心形) ...
→ ✓ code=0 time=8.8s vram=28817→28795MB (Δ-22) img=saved
[img3/3] girl13.jpg style=4(直线) ...
→ ✓ code=0 time=8.8s vram=28795→28845MB (Δ+50) img=saved
[img3/3] girl13.jpg style=5(波浪) ...
→ ✓ code=0 time=9.0s vram=28845→26291MB (Δ-2554) img=saved
✅ 完成!/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/results.json
📊 成功: 15/15
⏱ 平均: 9.06s
Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

+257
View File
@@ -0,0 +1,257 @@
[
{
"plan": "A",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:04:32",
"elapsed_s": 9.41,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26207,
"vram_after_mb": 29449,
"vram_delta_mb": 3242,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img1_style1.jpg"
},
{
"plan": "A",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:04:42",
"elapsed_s": 10.51,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 29449,
"vram_after_mb": 26341,
"vram_delta_mb": -3108,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img1_style2.jpg"
},
{
"plan": "A",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:04:53",
"elapsed_s": 10.49,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26341,
"vram_after_mb": 26051,
"vram_delta_mb": -290,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img1_style3.jpg"
},
{
"plan": "A",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:05:03",
"elapsed_s": 10.45,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26051,
"vram_after_mb": 26257,
"vram_delta_mb": 206,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img1_style4.jpg"
},
{
"plan": "A",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:05:14",
"elapsed_s": 10.85,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26257,
"vram_after_mb": 26319,
"vram_delta_mb": 62,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img1_style5.jpg"
},
{
"plan": "A",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:05:22",
"elapsed_s": 7.91,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26319,
"vram_after_mb": 28417,
"vram_delta_mb": 2098,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img2_style1.jpg"
},
{
"plan": "A",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:05:30",
"elapsed_s": 7.81,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28417,
"vram_after_mb": 28397,
"vram_delta_mb": -20,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img2_style2.jpg"
},
{
"plan": "A",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:05:38",
"elapsed_s": 7.74,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28397,
"vram_after_mb": 28409,
"vram_delta_mb": 12,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img2_style3.jpg"
},
{
"plan": "A",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:05:46",
"elapsed_s": 8.17,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28409,
"vram_after_mb": 26335,
"vram_delta_mb": -2074,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img2_style4.jpg"
},
{
"plan": "A",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:05:54",
"elapsed_s": 7.92,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 26335,
"vram_after_mb": 28419,
"vram_delta_mb": 2084,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img2_style5.jpg"
},
{
"plan": "A",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:06:03",
"elapsed_s": 8.95,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28419,
"vram_after_mb": 28835,
"vram_delta_mb": 416,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img3_style1.jpg"
},
{
"plan": "A",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:06:12",
"elapsed_s": 9.07,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28835,
"vram_after_mb": 28817,
"vram_delta_mb": -18,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img3_style2.jpg"
},
{
"plan": "A",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:06:21",
"elapsed_s": 8.82,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28817,
"vram_after_mb": 28795,
"vram_delta_mb": -22,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img3_style3.jpg"
},
{
"plan": "A",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:06:30",
"elapsed_s": 8.84,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28795,
"vram_after_mb": 28845,
"vram_delta_mb": 50,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img3_style4.jpg"
},
{
"plan": "A",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:06:39",
"elapsed_s": 8.96,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28845,
"vram_after_mb": 26291,
"vram_delta_mb": -2554,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_A/img3_style5.jpg"
}
]
+43
View File
@@ -0,0 +1,43 @@
nohup: ignoring input
9B vs 4B 对比测试 — Plan=Bplus
图片: ['girl1.jpg', 'girl7.jpg', 'girl13.jpg']
发型: [(1, '椭圆'), (2, '花瓣'), (3, '心形'), (4, '直线'), (5, '波浪')]
总调用: 15
[warmup] 预热请求 (girl13, style=1) ...
warmup: 12.1s
[img1/3] girl1.jpg style=1(椭圆) ...
→ ✓ code=0 time=7.7s vram=25531→28691MB (Δ+3160) img=saved
[img1/3] girl1.jpg style=2(花瓣) ...
→ ✓ code=0 time=8.8s vram=28691→28655MB (Δ-36) img=saved
[img1/3] girl1.jpg style=3(心形) ...
→ ✓ code=0 time=9.1s vram=28655→28651MB (Δ-4) img=saved
[img1/3] girl1.jpg style=4(直线) ...
→ ✓ code=0 time=8.7s vram=28651→28641MB (Δ-10) img=saved
[img1/3] girl1.jpg style=5(波浪) ...
→ ✓ code=0 time=8.8s vram=28641→28637MB (Δ-4) img=saved
[img2/3] girl7.jpg style=1(椭圆) ...
→ ✓ code=0 time=7.0s vram=28637→25539MB (Δ-3098) img=saved
[img2/3] girl7.jpg style=2(花瓣) ...
→ ✓ code=0 time=6.7s vram=25539→27599MB (Δ+2060) img=saved
[img2/3] girl7.jpg style=3(心形) ...
→ ✓ code=0 time=6.7s vram=27599→27597MB (Δ-2) img=saved
[img2/3] girl7.jpg style=4(直线) ...
→ ✓ code=0 time=7.0s vram=27597→25533MB (Δ-2064) img=saved
[img2/3] girl7.jpg style=5(波浪) ...
→ ✓ code=0 time=6.7s vram=25533→27585MB (Δ+2052) img=saved
[img3/3] girl13.jpg style=1(椭圆) ...
→ ✓ code=0 time=7.6s vram=27585→28023MB (Δ+438) img=saved
[img3/3] girl13.jpg style=2(花瓣) ...
→ ✓ code=0 time=7.8s vram=28023→27997MB (Δ-26) img=saved
[img3/3] girl13.jpg style=3(心形) ...
→ ✓ code=0 time=7.8s vram=27997→25557MB (Δ-2440) img=saved
[img3/3] girl13.jpg style=4(直线) ...
→ ✓ code=0 time=7.6s vram=25557→28011MB (Δ+2454) img=saved
[img3/3] girl13.jpg style=5(波浪) ...
→ ✓ code=0 time=7.8s vram=28011→28017MB (Δ+6) img=saved
✅ 完成!/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/results.json
📊 成功: 15/15
⏱ 平均: 7.72s
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

+257
View File
@@ -0,0 +1,257 @@
[
{
"plan": "Bplus",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:08:20",
"elapsed_s": 7.69,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25531,
"vram_after_mb": 28691,
"vram_delta_mb": 3160,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img1_style1.jpg"
},
{
"plan": "Bplus",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:08:28",
"elapsed_s": 8.77,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28691,
"vram_after_mb": 28655,
"vram_delta_mb": -36,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img1_style2.jpg"
},
{
"plan": "Bplus",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:08:38",
"elapsed_s": 9.07,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28655,
"vram_after_mb": 28651,
"vram_delta_mb": -4,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img1_style3.jpg"
},
{
"plan": "Bplus",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:08:46",
"elapsed_s": 8.73,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28651,
"vram_after_mb": 28641,
"vram_delta_mb": -10,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img1_style4.jpg"
},
{
"plan": "Bplus",
"image_idx": 1,
"image_name": "girl1.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl1.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:08:55",
"elapsed_s": 8.81,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28641,
"vram_after_mb": 28637,
"vram_delta_mb": -4,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img1_style5.jpg"
},
{
"plan": "Bplus",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:09:02",
"elapsed_s": 7.04,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28637,
"vram_after_mb": 25539,
"vram_delta_mb": -3098,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img2_style1.jpg"
},
{
"plan": "Bplus",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:09:09",
"elapsed_s": 6.67,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25539,
"vram_after_mb": 27599,
"vram_delta_mb": 2060,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img2_style2.jpg"
},
{
"plan": "Bplus",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:09:16",
"elapsed_s": 6.73,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27599,
"vram_after_mb": 27597,
"vram_delta_mb": -2,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img2_style3.jpg"
},
{
"plan": "Bplus",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:09:23",
"elapsed_s": 6.96,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27597,
"vram_after_mb": 25533,
"vram_delta_mb": -2064,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img2_style4.jpg"
},
{
"plan": "Bplus",
"image_idx": 2,
"image_name": "girl7.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl7.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:09:29",
"elapsed_s": 6.74,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25533,
"vram_after_mb": 27585,
"vram_delta_mb": 2052,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img2_style5.jpg"
},
{
"plan": "Bplus",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "14:09:37",
"elapsed_s": 7.62,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27585,
"vram_after_mb": 28023,
"vram_delta_mb": 438,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img3_style1.jpg"
},
{
"plan": "Bplus",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "14:09:45",
"elapsed_s": 7.84,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28023,
"vram_after_mb": 27997,
"vram_delta_mb": -26,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img3_style2.jpg"
},
{
"plan": "Bplus",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "14:09:53",
"elapsed_s": 7.83,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27997,
"vram_after_mb": 25557,
"vram_delta_mb": -2440,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img3_style3.jpg"
},
{
"plan": "Bplus",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "14:10:00",
"elapsed_s": 7.59,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25557,
"vram_after_mb": 28011,
"vram_delta_mb": 2454,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img3_style4.jpg"
},
{
"plan": "Bplus",
"image_idx": 3,
"image_name": "girl13.jpg",
"image_path": "/home/ubuntu/hair/image/girl_img/girl13.jpg",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "14:10:08",
"elapsed_s": 7.77,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28011,
"vram_after_mb": 28017,
"vram_delta_mb": 6,
"saved_image_path": "/home/ubuntu/hair/benchmark_out/9b_vs_4b_Bplus/img3_style5.jpg"
}
]
File diff suppressed because one or more lines are too long
+292
View File
@@ -0,0 +1,292 @@
nohup: ignoring input
找到 19 张图片
总计 95 次调用,已完成 0,剩余 95
[1/19] girl1.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.8s vram=22353→22353MB preview=✓ grown=✓
[1/19] girl1.jpg → 花瓣形(2) ...
→ HTTP code=0 time=13.7s vram=22353→22389MB preview=✓ grown=✓
[1/19] girl1.jpg → 心形(3) ...
→ HTTP code=0 time=13.6s vram=22389→22375MB preview=✓ grown=✓
[1/19] girl1.jpg → 直线形(4) ...
→ HTTP code=0 time=14.0s vram=22375→22359MB preview=✓ grown=✓
[1/19] girl1.jpg → 波浪形(5) ...
→ HTTP code=0 time=13.6s vram=22359→22393MB preview=✓ grown=✓
[2/19] girl10.jpg → 椭圆形(1) ...
→ HTTP code=0 time=9.6s vram=22393→22413MB preview=✓ grown=✓
[2/19] girl10.jpg → 花瓣形(2) ...
→ HTTP code=0 time=9.8s vram=22413→22397MB preview=✓ grown=✓
[2/19] girl10.jpg → 心形(3) ...
→ HTTP code=0 time=9.8s vram=22397→22391MB preview=✓ grown=✓
[2/19] girl10.jpg → 直线形(4) ...
→ HTTP code=0 time=9.8s vram=22391→22393MB preview=✓ grown=✓
[2/19] girl10.jpg → 波浪形(5) ...
→ HTTP code=0 time=9.8s vram=22393→22395MB preview=✓ grown=✓
[3/19] girl11.jpg → 椭圆形(1) ...
→ HTTP code=0 time=10.0s vram=22395→22421MB preview=✓ grown=✓
[3/19] girl11.jpg → 花瓣形(2) ...
→ HTTP code=0 time=10.3s vram=22421→22393MB preview=✓ grown=✓
[3/19] girl11.jpg → 心形(3) ...
→ HTTP code=0 time=10.4s vram=22393→22375MB preview=✓ grown=✓
[3/19] girl11.jpg → 直线形(4) ...
→ HTTP code=0 time=10.4s vram=22375→22385MB preview=✓ grown=✓
[3/19] girl11.jpg → 波浪形(5) ...
→ HTTP code=0 time=9.9s vram=22385→22389MB preview=✓ grown=✓
[4/19] girl12.jpg → 椭圆形(1) ...
→ HTTP code=0 time=13.0s vram=22389→22389MB preview=✓ grown=✓
[4/19] girl12.jpg → 花瓣形(2) ...
→ HTTP code=0 time=12.9s vram=22389→22385MB preview=✓ grown=✓
[4/19] girl12.jpg → 心形(3) ...
→ HTTP code=0 time=12.9s vram=22385→22389MB preview=✓ grown=✓
[4/19] girl12.jpg → 直线形(4) ...
→ HTTP code=0 time=13.0s vram=22389→22389MB preview=✓ grown=✓
[4/19] girl12.jpg → 波浪形(5) ...
→ HTTP code=0 time=13.3s vram=22389→22409MB preview=✓ grown=✓
[5/19] girl13.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.5s vram=22409→22365MB preview=✓ grown=✓
[5/19] girl13.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.2s vram=22365→22395MB preview=✓ grown=✓
[5/19] girl13.jpg → 心形(3) ...
→ HTTP code=0 time=11.2s vram=22395→22421MB preview=✓ grown=✓
[5/19] girl13.jpg → 直线形(4) ...
→ HTTP code=0 time=11.4s vram=22421→22409MB preview=✓ grown=✓
[5/19] girl13.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.2s vram=22409→22431MB preview=✓ grown=✓
[6/19] girl14.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.5s vram=22431→22391MB preview=✓ grown=✓
[6/19] girl14.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.5s vram=22391→22375MB preview=✓ grown=✓
[6/19] girl14.jpg → 心形(3) ...
→ HTTP code=0 time=11.5s vram=22375→22409MB preview=✓ grown=✓
[6/19] girl14.jpg → 直线形(4) ...
→ HTTP code=0 time=11.5s vram=22409→22359MB preview=✓ grown=✓
[6/19] girl14.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.5s vram=22359→22393MB preview=✓ grown=✓
[7/19] girl15.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.2s vram=22393→22385MB preview=✓ grown=✓
[7/19] girl15.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.3s vram=22385→22387MB preview=✓ grown=✓
[7/19] girl15.jpg → 心形(3) ...
→ HTTP code=0 time=11.4s vram=22387→22357MB preview=✓ grown=✓
[7/19] girl15.jpg → 直线形(4) ...
→ HTTP code=0 time=11.3s vram=22357→22383MB preview=✓ grown=✓
[7/19] girl15.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.7s vram=22383→22387MB preview=✓ grown=✓
[8/19] girl16.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.2s vram=22387→22377MB preview=✓ grown=✓
[8/19] girl16.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.2s vram=22377→22359MB preview=✓ grown=✓
[8/19] girl16.jpg → 心形(3) ...
→ HTTP code=0 time=11.2s vram=22359→22347MB preview=✓ grown=✓
[8/19] girl16.jpg → 直线形(4) ...
→ HTTP code=0 time=11.3s vram=22347→22367MB preview=✓ grown=✓
[8/19] girl16.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.4s vram=22367→22399MB preview=✓ grown=✓
[9/19] girl17.jpg → 椭圆形(1) ...
→ HTTP code=0 time=13.0s vram=22399→22403MB preview=✓ grown=✓
[9/19] girl17.jpg → 花瓣形(2) ...
→ HTTP code=0 time=13.0s vram=22403→22421MB preview=✓ grown=✓
[9/19] girl17.jpg → 心形(3) ...
→ HTTP code=0 time=13.1s vram=22421→22381MB preview=✓ grown=✓
[9/19] girl17.jpg → 直线形(4) ...
→ HTTP code=0 time=13.1s vram=22381→22419MB preview=✓ grown=✓
[9/19] girl17.jpg → 波浪形(5) ...
→ HTTP code=0 time=13.0s vram=22419→22399MB preview=✓ grown=✓
[10/19] girl18.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.6s vram=22399→22419MB preview=✓ grown=✓
[10/19] girl18.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.2s vram=22419→22383MB preview=✓ grown=✓
[10/19] girl18.jpg → 心形(3) ...
→ HTTP code=0 time=11.4s vram=22383→22409MB preview=✓ grown=✓
[10/19] girl18.jpg → 直线形(4) ...
→ HTTP code=0 time=11.4s vram=22409→22375MB preview=✓ grown=✓
[10/19] girl18.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.3s vram=22375→22413MB preview=✓ grown=✓
[11/19] girl19.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.1s vram=22413→22385MB preview=✓ grown=✓
[11/19] girl19.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.1s vram=22385→22393MB preview=✓ grown=✓
[11/19] girl19.jpg → 心形(3) ...
→ HTTP code=0 time=11.3s vram=22393→22387MB preview=✓ grown=✓
[11/19] girl19.jpg → 直线形(4) ...
→ HTTP code=0 time=11.2s vram=22387→22415MB preview=✓ grown=✓
[11/19] girl19.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.2s vram=22415→22405MB preview=✓ grown=✓
[12/19] girl2.jpg → 椭圆形(1) ...
→ HTTP code=0 time=13.1s vram=22405→22395MB preview=✓ grown=✓
[12/19] girl2.jpg → 花瓣形(2) ...
→ HTTP code=0 time=12.9s vram=22395→22387MB preview=✓ grown=✓
[12/19] girl2.jpg → 心形(3) ...
→ HTTP code=0 time=12.9s vram=22387→22381MB preview=✓ grown=✓
[12/19] girl2.jpg → 直线形(4) ...
→ HTTP code=0 time=12.9s vram=22381→22387MB preview=✓ grown=✓
[12/19] girl2.jpg → 波浪形(5) ...
→ HTTP code=0 time=12.9s vram=22387→22309MB preview=✓ grown=✓
[13/19] girl3.jpg → 椭圆形(1) ...
→ HTTP code=0 time=9.8s vram=22309→22381MB preview=✓ grown=✓
[13/19] girl3.jpg → 花瓣形(2) ...
→ HTTP code=0 time=9.8s vram=22381→22403MB preview=✓ grown=✓
[13/19] girl3.jpg → 心形(3) ...
→ HTTP code=0 time=9.9s vram=22403→22419MB preview=✓ grown=✓
[13/19] girl3.jpg → 直线形(4) ...
→ HTTP code=0 time=9.9s vram=22419→22401MB preview=✓ grown=✓
[13/19] girl3.jpg → 波浪形(5) ...
→ HTTP code=0 time=9.9s vram=22401→22385MB preview=✓ grown=✓
[14/19] girl4.jpg → 椭圆形(1) ...
→ HTTP code=0 time=10.2s vram=22385→22377MB preview=✓ grown=✓
[14/19] girl4.jpg → 花瓣形(2) ...
→ HTTP code=0 time=10.5s vram=22377→22387MB preview=✓ grown=✓
[14/19] girl4.jpg → 心形(3) ...
→ HTTP code=0 time=10.0s vram=22387→22365MB preview=✓ grown=✓
[14/19] girl4.jpg → 直线形(4) ...
→ HTTP code=0 time=9.9s vram=22365→22371MB preview=✓ grown=✓
[14/19] girl4.jpg → 波浪形(5) ...
→ HTTP code=0 time=10.0s vram=22371→22389MB preview=✓ grown=✓
[15/19] girl5.jpg → 椭圆形(1) ...
→ HTTP code=0 time=8.5s vram=22389→23903MB preview=✓ grown=✓
[15/19] girl5.jpg → 花瓣形(2) ...
→ HTTP code=0 time=8.6s vram=23903→23929MB preview=✓ grown=✓
[15/19] girl5.jpg → 心形(3) ...
→ HTTP code=0 time=8.6s vram=23929→23925MB preview=✓ grown=✓
[15/19] girl5.jpg → 直线形(4) ...
→ HTTP code=0 time=8.7s vram=23925→23931MB preview=✓ grown=✓
[15/19] girl5.jpg → 波浪形(5) ...
→ HTTP code=0 time=8.7s vram=23931→23951MB preview=✓ grown=✓
[16/19] girl6.jpg → 椭圆形(1) ...
→ HTTP code=0 time=8.4s vram=23951→23729MB preview=✓ grown=✓
[16/19] girl6.jpg → 花瓣形(2) ...
→ HTTP code=0 time=8.7s vram=23729→22377MB preview=✓ grown=✓
[16/19] girl6.jpg → 心形(3) ...
→ HTTP code=0 time=8.4s vram=22377→23685MB preview=✓ grown=✓
[16/19] girl6.jpg → 直线形(4) ...
→ HTTP code=0 time=8.3s vram=23685→23731MB preview=✓ grown=✓
[16/19] girl6.jpg → 波浪形(5) ...
→ HTTP code=0 time=8.4s vram=23731→23713MB preview=✓ grown=✓
[17/19] girl7.jpg → 椭圆形(1) ...
→ HTTP code=0 time=9.9s vram=23713→24481MB preview=✓ grown=✓
[17/19] girl7.jpg → 花瓣形(2) ...
→ HTTP code=0 time=10.1s vram=24481→24447MB preview=✓ grown=✓
[17/19] girl7.jpg → 心形(3) ...
→ HTTP code=0 time=10.0s vram=24447→24481MB preview=✓ grown=✓
[17/19] girl7.jpg → 直线形(4) ...
→ HTTP code=0 time=10.0s vram=24481→24469MB preview=✓ grown=✓
[17/19] girl7.jpg → 波浪形(5) ...
→ HTTP code=0 time=10.0s vram=24469→24475MB preview=✓ grown=✓
[18/19] girl8.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.4s vram=24475→24861MB preview=✓ grown=✓
[18/19] girl8.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.5s vram=24861→24823MB preview=✓ grown=✓
[18/19] girl8.jpg → 心形(3) ...
→ HTTP code=0 time=11.3s vram=24823→22393MB preview=✓ grown=✓
[18/19] girl8.jpg → 直线形(4) ...
→ HTTP code=0 time=11.1s vram=22393→22389MB preview=✓ grown=✓
[18/19] girl8.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.1s vram=22389→22365MB preview=✓ grown=✓
[19/19] girl9.jpg → 椭圆形(1) ...
→ HTTP code=0 time=11.9s vram=22365→22391MB preview=✓ grown=✓
[19/19] girl9.jpg → 花瓣形(2) ...
→ HTTP code=0 time=11.9s vram=22391→22375MB preview=✓ grown=✓
[19/19] girl9.jpg → 心形(3) ...
→ HTTP code=0 time=11.9s vram=22375→22391MB preview=✓ grown=✓
[19/19] girl9.jpg → 直线形(4) ...
→ HTTP code=0 time=11.9s vram=22391→22371MB preview=✓ grown=✓
[19/19] girl9.jpg → 波浪形(5) ...
→ HTTP code=0 time=11.8s vram=22371→22373MB preview=✓ grown=✓
✅ 完成!结果: /home/ubuntu/hair/benchmark_out/benchmark_results.json
📄 报告: /home/ubuntu/hair/benchmark_out/benchmark_report.html
📊 总调用: 95/95
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
nohup: ignoring input
接口2女性5种发型测试 — Plan=B
图片:/home/ubuntu/hair/image/girl_img/girl13.jpg
[1/5] hair_style=1 (椭圆) ...
→ ✓ code=0 time=8.7s vram=24981→25025MB (Δ+44) img=saved
[2/5] hair_style=2 (花瓣) ...
→ ✓ code=0 time=8.4s vram=25025→27451MB (Δ+2426) img=saved
[3/5] hair_style=3 (心形) ...
→ ✓ code=0 time=8.4s vram=27451→27431MB (Δ-20) img=saved
[4/5] hair_style=4 (直线) ...
→ ✓ code=0 time=8.5s vram=27431→27433MB (Δ+2) img=saved
[5/5] hair_style=5 (波浪) ...
→ ✓ code=0 time=8.4s vram=27433→27407MB (Δ-26) img=saved
✅ 完成!/home/ubuntu/hair/benchmark_out/iface2_female_B/results.json
📄 报告:/home/ubuntu/hair/benchmark_out/iface2_female_B/report.html
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>接口2女5种发型 - Plan B</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:20px}
h1{text-align:center;color:#00d4ff;margin-bottom:10px}
.subtitle{text-align:center;color:#888;margin-bottom:30px;font-size:14px}
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:15px;margin-bottom:30px}
.card{background:#16213e;border-radius:12px;padding:20px;text-align:center;border:1px solid #333}
.card .num{font-size:28px;font-weight:700}
.card .label{font-size:12px;color:#888;margin-top:5px}
.card.ok .num{color:#0f0} .card.err .num{color:#f44} .card.time .num{color:#00d4ff}
table{width:100%;border-collapse:collapse;margin-bottom:30px;background:#16213e;border-radius:12px;overflow:hidden}
th{background:#0f3460;padding:10px 8px;text-align:center;font-size:13px;color:#fff}
td{padding:8px;text-align:center;border-bottom:1px solid #222;font-size:13px}
tr:hover{background:#1a1a3e}
.section-title{font-size:18px;font-weight:600;margin:30px 0 15px;color:#00d4ff;border-bottom:1px solid #333;padding-bottom:10px}
.ok{color:#0f0} .fail{color:#f44}
.time-bar{display:inline-block;height:18px;background:linear-gradient(90deg,#0f3460,#00d4ff);border-radius:3px;vertical-align:middle;min-width:2px}
.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px}
.gallery img{width:100%;border-radius:8px;border:1px solid #333}
.gallery .item{text-align:center}
.gallery .cap{margin-top:5px;font-size:12px;color:#888}
</style></head><body>
<h1>📊 接口2女性5种发型测试报告</h1>
<div class="subtitle">Plan B 2026-07-18 13:52:20 5 次调用</div>
<div class="summary">
<div class="card ok"><div class="num">5</div><div class="label">成功</div></div>
<div class="card err"><div class="num">0</div><div class="label">失败</div></div>
<div class="card time"><div class="num">8.5s</div><div class="label">平均耗时</div></div>
</div>
<div class="section-title">详细结果</div>
<table><thead><tr><th>发型</th><th>名称</th><th>耗时</th><th>显存前</th><th>显存后</th><th>Δ</th><th>状态</th></tr></thead><tbody>
<tr><td>style 1</td><td>椭圆</td><td><div class="time-bar" style="width:130.65px"></div> 8.7s</td><td>24981MB</td><td>25025MB</td><td>+44</td><td><span class="ok"></span></td></tr><tr><td>style 2</td><td>花瓣</td><td><div class="time-bar" style="width:125.54999999999998px"></div> 8.4s</td><td>25025MB</td><td>27451MB</td><td>+2426</td><td><span class="ok"></span></td></tr><tr><td>style 3</td><td>心形</td><td><div class="time-bar" style="width:125.54999999999998px"></div> 8.4s</td><td>27451MB</td><td>27431MB</td><td>-20</td><td><span class="ok"></span></td></tr><tr><td>style 4</td><td>直线</td><td><div class="time-bar" style="width:127.5px"></div> 8.5s</td><td>27431MB</td><td>27433MB</td><td>+2</td><td><span class="ok"></span></td></tr><tr><td>style 5</td><td>波浪</td><td><div class="time-bar" style="width:126.0px"></div> 8.4s</td><td>27433MB</td><td>27407MB</td><td>-26</td><td><span class="ok"></span></td></tr></tbody></table><div class="section-title">生成图片</div><div class="gallery"><div class="item"><img src="hair_style_1_椭圆.jpg"><div class="cap">椭圆 (8.7s)</div></div><div class="item"><img src="hair_style_2_花瓣.jpg"><div class="cap">花瓣 (8.4s)</div></div><div class="item"><img src="hair_style_3_心形.jpg"><div class="cap">心形 (8.4s)</div></div><div class="item"><img src="hair_style_4_直线.jpg"><div class="cap">直线 (8.5s)</div></div><div class="item"><img src="hair_style_5_波浪.jpg"><div class="cap">波浪 (8.4s)</div></div></div></body></html>
@@ -0,0 +1,72 @@
[
{
"plan": "B",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "13:51:46",
"elapsed_s": 8.71,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 24981,
"vram_after_mb": 25025,
"vram_delta_mb": 44,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_B/hair_style_1_椭圆.jpg"
},
{
"plan": "B",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "13:51:55",
"elapsed_s": 8.37,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25025,
"vram_after_mb": 27451,
"vram_delta_mb": 2426,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_B/hair_style_2_花瓣.jpg"
},
{
"plan": "B",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "13:52:03",
"elapsed_s": 8.37,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27451,
"vram_after_mb": 27431,
"vram_delta_mb": -20,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_B/hair_style_3_心形.jpg"
},
{
"plan": "B",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "13:52:11",
"elapsed_s": 8.5,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27431,
"vram_after_mb": 27433,
"vram_delta_mb": 2,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_B/hair_style_4_直线.jpg"
},
{
"plan": "B",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "13:52:20",
"elapsed_s": 8.4,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 27433,
"vram_after_mb": 27407,
"vram_delta_mb": -26,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_B/hair_style_5_波浪.jpg"
}
]
+22
View File
@@ -0,0 +1,22 @@
nohup: ignoring input
接口2女性5种发型测试 — Plan=Bplus
图片:/home/ubuntu/hair/image/girl_img/girl13.jpg
[1/5] hair_style=1 (椭圆) ...
→ ✓ code=0 time=7.9s vram=25875→25889MB (Δ+14) img=saved
[2/5] hair_style=2 (花瓣) ...
→ ✓ code=0 time=8.0s vram=25889→28337MB (Δ+2448) img=saved
[3/5] hair_style=3 (心形) ...
→ ✓ code=0 time=7.8s vram=28337→28355MB (Δ+18) img=saved
[4/5] hair_style=4 (直线) ...
→ ✓ code=0 time=7.6s vram=28355→28333MB (Δ-22) img=saved
[5/5] hair_style=5 (波浪) ...
→ ✓ code=0 time=8.3s vram=28333→25885MB (Δ-2448) img=saved
✅ 完成!/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/results.json
📄 报告:/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/report.html
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>接口2女5种发型 - Plan Bplus</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:20px}
h1{text-align:center;color:#00d4ff;margin-bottom:10px}
.subtitle{text-align:center;color:#888;margin-bottom:30px;font-size:14px}
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:15px;margin-bottom:30px}
.card{background:#16213e;border-radius:12px;padding:20px;text-align:center;border:1px solid #333}
.card .num{font-size:28px;font-weight:700}
.card .label{font-size:12px;color:#888;margin-top:5px}
.card.ok .num{color:#0f0} .card.err .num{color:#f44} .card.time .num{color:#00d4ff}
table{width:100%;border-collapse:collapse;margin-bottom:30px;background:#16213e;border-radius:12px;overflow:hidden}
th{background:#0f3460;padding:10px 8px;text-align:center;font-size:13px;color:#fff}
td{padding:8px;text-align:center;border-bottom:1px solid #222;font-size:13px}
tr:hover{background:#1a1a3e}
.section-title{font-size:18px;font-weight:600;margin:30px 0 15px;color:#00d4ff;border-bottom:1px solid #333;padding-bottom:10px}
.ok{color:#0f0} .fail{color:#f44}
.time-bar{display:inline-block;height:18px;background:linear-gradient(90deg,#0f3460,#00d4ff);border-radius:3px;vertical-align:middle;min-width:2px}
.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px}
.gallery img{width:100%;border-radius:8px;border:1px solid #333}
.gallery .item{text-align:center}
.gallery .cap{margin-top:5px;font-size:12px;color:#888}
</style></head><body>
<h1>📊 接口2女性5种发型测试报告</h1>
<div class="subtitle">Plan Bplus 2026-07-18 13:50:43 5 次调用</div>
<div class="summary">
<div class="card ok"><div class="num">5</div><div class="label">成功</div></div>
<div class="card err"><div class="num">0</div><div class="label">失败</div></div>
<div class="card time"><div class="num">7.9s</div><div class="label">平均耗时</div></div>
</div>
<div class="section-title">详细结果</div>
<table><thead><tr><th>发型</th><th>名称</th><th>耗时</th><th>显存前</th><th>显存后</th><th>Δ</th><th>状态</th></tr></thead><tbody>
<tr><td>style 1</td><td>椭圆</td><td><div class="time-bar" style="width:119.10000000000001px"></div> 7.9s</td><td>25875MB</td><td>25889MB</td><td>+14</td><td><span class="ok"></span></td></tr><tr><td>style 2</td><td>花瓣</td><td><div class="time-bar" style="width:120.6px"></div> 8.0s</td><td>25889MB</td><td>28337MB</td><td>+2448</td><td><span class="ok"></span></td></tr><tr><td>style 3</td><td>心形</td><td><div class="time-bar" style="width:116.55px"></div> 7.8s</td><td>28337MB</td><td>28355MB</td><td>+18</td><td><span class="ok"></span></td></tr><tr><td>style 4</td><td>直线</td><td><div class="time-bar" style="width:114.6px"></div> 7.6s</td><td>28355MB</td><td>28333MB</td><td>-22</td><td><span class="ok"></span></td></tr><tr><td>style 5</td><td>波浪</td><td><div class="time-bar" style="width:123.89999999999999px"></div> 8.3s</td><td>28333MB</td><td>25885MB</td><td>-2448</td><td><span class="ok"></span></td></tr></tbody></table><div class="section-title">生成图片</div><div class="gallery"><div class="item"><img src="hair_style_1_椭圆.jpg"><div class="cap">椭圆 (7.9s)</div></div><div class="item"><img src="hair_style_2_花瓣.jpg"><div class="cap">花瓣 (8.0s)</div></div><div class="item"><img src="hair_style_3_心形.jpg"><div class="cap">心形 (7.8s)</div></div><div class="item"><img src="hair_style_4_直线.jpg"><div class="cap">直线 (7.6s)</div></div><div class="item"><img src="hair_style_5_波浪.jpg"><div class="cap">波浪 (8.3s)</div></div></div></body></html>
@@ -0,0 +1,72 @@
[
{
"plan": "Bplus",
"hair_style": 1,
"hair_style_name": "椭圆",
"timestamp": "13:50:11",
"elapsed_s": 7.94,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25875,
"vram_after_mb": 25889,
"vram_delta_mb": 14,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/hair_style_1_椭圆.jpg"
},
{
"plan": "Bplus",
"hair_style": 2,
"hair_style_name": "花瓣",
"timestamp": "13:50:19",
"elapsed_s": 8.04,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25889,
"vram_after_mb": 28337,
"vram_delta_mb": 2448,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/hair_style_2_花瓣.jpg"
},
{
"plan": "Bplus",
"hair_style": 3,
"hair_style_name": "心形",
"timestamp": "13:50:27",
"elapsed_s": 7.77,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28337,
"vram_after_mb": 28355,
"vram_delta_mb": 18,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/hair_style_3_心形.jpg"
},
{
"plan": "Bplus",
"hair_style": 4,
"hair_style_name": "直线",
"timestamp": "13:50:35",
"elapsed_s": 7.64,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28355,
"vram_after_mb": 28333,
"vram_delta_mb": -22,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/hair_style_4_直线.jpg"
},
{
"plan": "Bplus",
"hair_style": 5,
"hair_style_name": "波浪",
"timestamp": "13:50:43",
"elapsed_s": 8.26,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28333,
"vram_after_mb": 25885,
"vram_delta_mb": -2448,
"image_path": "/home/ubuntu/hair/benchmark_out/iface2_female_Bplus/hair_style_5_波浪.jpg"
}
]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,562 @@
[
{
"round": 1,
"test_id": "iface1",
"test_name": "接口1-测量",
"timestamp": "13:36:27",
"elapsed_s": 0.37,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25303,
"vram_after_mb": 25425,
"vram_free_before_mb": 6818,
"vram_free_after_mb": 6696,
"vram_delta_mb": 122,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 12524,
"298802": 1015
},
"procs_after": {
"858": 3242,
"24879": 8476,
"271640": 12524,
"298802": 1137
},
"procs_changed": true
},
{
"round": 1,
"test_id": "iface2f",
"test_name": "接口2-女-椭圆",
"timestamp": "13:36:35",
"elapsed_s": 7.54,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25425,
"vram_after_mb": 25887,
"vram_free_before_mb": 6696,
"vram_free_after_mb": 6234,
"vram_delta_mb": 462,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 12524,
"298802": 1137
},
"procs_after": {
"858": 3242,
"24879": 8522,
"271640": 12492,
"298802": 1585
},
"procs_changed": true
},
{
"round": 1,
"test_id": "iface2m",
"test_name": "接口2-男-椭圆",
"timestamp": "13:36:39",
"elapsed_s": 4.11,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25887,
"vram_after_mb": 28383,
"vram_free_before_mb": 6234,
"vram_free_after_mb": 3738,
"vram_delta_mb": 2496,
"procs_before": {
"858": 3242,
"24879": 8522,
"271640": 12492,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8522,
"271640": 14988,
"298802": 1585
},
"procs_changed": true
},
{
"round": 1,
"test_id": "iface3",
"test_name": "接口3-B端生发",
"timestamp": "13:36:42",
"elapsed_s": 3.12,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28383,
"vram_after_mb": 28415,
"vram_free_before_mb": 3738,
"vram_free_after_mb": 3706,
"vram_delta_mb": 32,
"procs_before": {
"858": 3242,
"24879": 8522,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8522,
"271640": 15020,
"298802": 1585
},
"procs_changed": true
},
{
"round": 2,
"test_id": "iface1",
"test_name": "接口1-测量",
"timestamp": "13:36:42",
"elapsed_s": 0.08,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28415,
"vram_after_mb": 28415,
"vram_free_before_mb": 3706,
"vram_free_after_mb": 3706,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8522,
"271640": 15020,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8522,
"271640": 15020,
"298802": 1585
},
"procs_changed": false
},
{
"round": 2,
"test_id": "iface2f",
"test_name": "接口2-女-椭圆",
"timestamp": "13:36:50",
"elapsed_s": 7.51,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28415,
"vram_after_mb": 28353,
"vram_free_before_mb": 3706,
"vram_free_after_mb": 3768,
"vram_delta_mb": -62,
"procs_before": {
"858": 3242,
"24879": 8522,
"271640": 15020,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_changed": true
},
{
"round": 2,
"test_id": "iface2m",
"test_name": "接口2-男-椭圆",
"timestamp": "13:36:54",
"elapsed_s": 4.0,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28353,
"vram_after_mb": 28353,
"vram_free_before_mb": 3768,
"vram_free_after_mb": 3768,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 2,
"test_id": "iface3",
"test_name": "接口3-B端生发",
"timestamp": "13:36:57",
"elapsed_s": 3.27,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28353,
"vram_after_mb": 28353,
"vram_free_before_mb": 3768,
"vram_free_after_mb": 3768,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 3,
"test_id": "iface1",
"test_name": "接口1-测量",
"timestamp": "13:36:58",
"elapsed_s": 0.07,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28353,
"vram_after_mb": 28353,
"vram_free_before_mb": 3768,
"vram_free_after_mb": 3768,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 3,
"test_id": "iface2f",
"test_name": "接口2-女-椭圆",
"timestamp": "13:37:05",
"elapsed_s": 7.63,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28353,
"vram_after_mb": 25855,
"vram_free_before_mb": 3768,
"vram_free_after_mb": 6266,
"vram_delta_mb": -2498,
"procs_before": {
"858": 3242,
"24879": 8492,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 12492,
"298802": 1585
},
"procs_changed": true
},
{
"round": 3,
"test_id": "iface2m",
"test_name": "接口2-男-椭圆",
"timestamp": "13:37:09",
"elapsed_s": 4.1,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25855,
"vram_after_mb": 28351,
"vram_free_before_mb": 6266,
"vram_free_after_mb": 3770,
"vram_delta_mb": 2496,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 12492,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_changed": true
},
{
"round": 3,
"test_id": "iface3",
"test_name": "接口3-B端生发",
"timestamp": "13:37:13",
"elapsed_s": 3.13,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28351,
"vram_after_mb": 28351,
"vram_free_before_mb": 3770,
"vram_free_after_mb": 3770,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 4,
"test_id": "iface1",
"test_name": "接口1-测量",
"timestamp": "13:37:13",
"elapsed_s": 0.07,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28351,
"vram_after_mb": 28351,
"vram_free_before_mb": 3770,
"vram_free_after_mb": 3770,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 4,
"test_id": "iface2f",
"test_name": "接口2-女-椭圆",
"timestamp": "13:37:20",
"elapsed_s": 7.32,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28351,
"vram_after_mb": 28337,
"vram_free_before_mb": 3770,
"vram_free_after_mb": 3784,
"vram_delta_mb": -14,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_changed": true
},
{
"round": 4,
"test_id": "iface2m",
"test_name": "接口2-男-椭圆",
"timestamp": "13:37:24",
"elapsed_s": 4.21,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28337,
"vram_after_mb": 28337,
"vram_free_before_mb": 3784,
"vram_free_after_mb": 3784,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 4,
"test_id": "iface3",
"test_name": "接口3-B端生发",
"timestamp": "13:37:28",
"elapsed_s": 3.18,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28337,
"vram_after_mb": 28337,
"vram_free_before_mb": 3784,
"vram_free_after_mb": 3784,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 5,
"test_id": "iface1",
"test_name": "接口1-测量",
"timestamp": "13:37:28",
"elapsed_s": 0.08,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28337,
"vram_after_mb": 28337,
"vram_free_before_mb": 3784,
"vram_free_after_mb": 3784,
"vram_delta_mb": 0,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_changed": false
},
{
"round": 5,
"test_id": "iface2f",
"test_name": "接口2-女-椭圆",
"timestamp": "13:37:35",
"elapsed_s": 7.43,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28337,
"vram_after_mb": 25855,
"vram_free_before_mb": 3784,
"vram_free_after_mb": 6266,
"vram_delta_mb": -2482,
"procs_before": {
"858": 3242,
"24879": 8476,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 12492,
"298802": 1585
},
"procs_changed": true
},
{
"round": 5,
"test_id": "iface2m",
"test_name": "接口2-男-椭圆",
"timestamp": "13:37:39",
"elapsed_s": 4.07,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 25855,
"vram_after_mb": 28351,
"vram_free_before_mb": 6266,
"vram_free_after_mb": 3770,
"vram_delta_mb": 2496,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 12492,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_changed": true
},
{
"round": 5,
"test_id": "iface3",
"test_name": "接口3-B端生发",
"timestamp": "13:37:43",
"elapsed_s": 3.11,
"success": true,
"code": 0,
"error": null,
"vram_before_mb": 28351,
"vram_after_mb": 28383,
"vram_free_before_mb": 3770,
"vram_free_after_mb": 3738,
"vram_delta_mb": 32,
"procs_before": {
"858": 3242,
"24879": 8490,
"271640": 14988,
"298802": 1585
},
"procs_after": {
"858": 3242,
"24879": 8490,
"271640": 15020,
"298802": 1585
},
"procs_changed": true
}
]
+87
View File
@@ -0,0 +1,87 @@
nohup: ignoring input
轮换测试:4 个接口 × 5 轮 = 20 次调用
测试图片:/home/ubuntu/hair/image/girl_img/girl13.jpg
============================================================
第 1/5 轮
============================================================
[1/5] 接口1-测量 ...
→ ✓ code=0 time=0.4s vram=25303→25425MB (Δ+122) ⚠模型换出!
[1/5] 接口2-女-椭圆 ...
→ ✓ code=0 time=7.5s vram=25425→25887MB (Δ+462) ⚠模型换出!
[1/5] 接口2-男-椭圆 ...
→ ✓ code=0 time=4.1s vram=25887→28383MB (Δ+2496) ⚠模型换出!
[1/5] 接口3-B端生发 ...
→ ✓ code=0 time=3.1s vram=28383→28415MB (Δ+32) ⚠模型换出!
============================================================
第 2/5 轮
============================================================
[2/5] 接口1-测量 ...
→ ✓ code=0 time=0.1s vram=28415→28415MB (Δ+0)
[2/5] 接口2-女-椭圆 ...
→ ✓ code=0 time=7.5s vram=28415→28353MB (Δ-62) ⚠模型换出!
[2/5] 接口2-男-椭圆 ...
→ ✓ code=0 time=4.0s vram=28353→28353MB (Δ+0)
[2/5] 接口3-B端生发 ...
→ ✓ code=0 time=3.3s vram=28353→28353MB (Δ+0)
============================================================
第 3/5 轮
============================================================
[3/5] 接口1-测量 ...
→ ✓ code=0 time=0.1s vram=28353→28353MB (Δ+0)
[3/5] 接口2-女-椭圆 ...
→ ✓ code=0 time=7.6s vram=28353→25855MB (Δ-2498) ⚠模型换出!
[3/5] 接口2-男-椭圆 ...
→ ✓ code=0 time=4.1s vram=25855→28351MB (Δ+2496) ⚠模型换出!
[3/5] 接口3-B端生发 ...
→ ✓ code=0 time=3.1s vram=28351→28351MB (Δ+0)
============================================================
第 4/5 轮
============================================================
[4/5] 接口1-测量 ...
→ ✓ code=0 time=0.1s vram=28351→28351MB (Δ+0)
[4/5] 接口2-女-椭圆 ...
→ ✓ code=0 time=7.3s vram=28351→28337MB (Δ-14) ⚠模型换出!
[4/5] 接口2-男-椭圆 ...
→ ✓ code=0 time=4.2s vram=28337→28337MB (Δ+0)
[4/5] 接口3-B端生发 ...
→ ✓ code=0 time=3.2s vram=28337→28337MB (Δ+0)
============================================================
第 5/5 轮
============================================================
[5/5] 接口1-测量 ...
→ ✓ code=0 time=0.1s vram=28337→28337MB (Δ+0)
[5/5] 接口2-女-椭圆 ...
→ ✓ code=0 time=7.4s vram=28337→25855MB (Δ-2482) ⚠模型换出!
[5/5] 接口2-男-椭圆 ...
→ ✓ code=0 time=4.1s vram=25855→28351MB (Δ+2496) ⚠模型换出!
[5/5] 接口3-B端生发 ...
→ ✓ code=0 time=3.1s vram=28351→28383MB (Δ+32) ⚠模型换出!
✅ 完成!/home/ubuntu/hair/benchmark_out/rotation/rotation_results.json
📄 报告:/home/ubuntu/hair/benchmark_out/rotation/rotation_report.html
Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Some files were not shown because too many files have changed in this diff Show More