diff --git a/.gitignore b/.gitignore
index 4a32e7f..6acd943 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,13 @@ gateway.log
# 工作流备份文件(不入 git)
*.json.bak.*
+
+# 分辨率对比测试产物(out/ 下结果图+原图副本+日志,体积大,不入 git)
+# 仅忽略 out/ 与运行期文件;测试脚本与 HTML 报告仍入库
+image/compare_test/out/
+image/res_test/out/
+image/compare_test/progress.json
+image/res_test/progress.json
+image/compare_test/batch_test.log
+image/res_test/batch_test.log
+image/compare_test/http.log
diff --git a/image/compare_test/archive_20260725/batch_test.log b/image/compare_test/archive_20260725/batch_test.log
new file mode 100644
index 0000000..345aa46
--- /dev/null
+++ b/image/compare_test/archive_20260725/batch_test.log
@@ -0,0 +1,75 @@
+=== 接口2 female 批量对比测试 ===
+矩阵: 4图 × 5发型 × 2档 = 40 次
+已跳过 0 个已完成项
+
+[1/40] ▶ girl2_ellipse_default896 (side=None)... ✅ 10.8s 出图 1082x1081 (204589B)
+[2/40] ▶ girl2_ellipse_origin0 (side=0)... ✅ 13.3s 出图 1088x1088 (232252B)
+[3/40] ▶ girl2_flower_default896 (side=None)... ✅ 11.0s 出图 1082x1081 (210875B)
+[4/40] ▶ girl2_flower_origin0 (side=0)... ✅ 12.5s 出图 1088x1088 (236073B)
+[5/40] ▶ girl2_heart_default896 (side=None)... ✅ 11.9s 出图 1082x1081 (198911B)
+[6/40] ▶ girl2_heart_origin0 (side=0)... ✅ 12.2s 出图 1088x1088 (249893B)
+[7/40] ▶ girl2_straight_default896 (side=None)... ✅ 12.1s 出图 1082x1081 (211803B)
+[8/40] ▶ girl2_straight_origin0 (side=0)... ✅ 12.3s 出图 1088x1088 (235377B)
+[9/40] ▶ girl2_wave_default896 (side=None)... ✅ 12.7s 出图 1082x1081 (207736B)
+[10/40] ▶ girl2_wave_origin0 (side=0)... ✅ 11.9s 出图 1088x1088 (238081B)
+[11/40] ▶ girl5_ellipse_default896 (side=None)... ✅ 9.6s 出图 768x752 (105867B)
+[12/40] ▶ girl5_ellipse_origin0 (side=0)... ✅ 7.3s 出图 768x752 (112963B)
+[13/40] ▶ girl5_flower_default896 (side=None)... ✅ 8.0s 出图 768x752 (114378B)
+[14/40] ▶ girl5_flower_origin0 (side=0)... ✅ 7.2s 出图 768x752 (108495B)
+[15/40] ▶ girl5_heart_default896 (side=None)... ✅ 8.1s 出图 768x752 (113874B)
+[16/40] ▶ girl5_heart_origin0 (side=0)... ✅ 7.6s 出图 768x752 (103160B)
+[17/40] ▶ girl5_straight_default896 (side=None)... ✅ 8.4s 出图 768x752 (104492B)
+[18/40] ▶ girl5_straight_origin0 (side=0)... ✅ 7.2s 出图 768x752 (100941B)
+[19/40] ▶ girl5_wave_default896 (side=None)... ✅ 8.4s 出图 768x752 (120122B)
+[20/40] ▶ girl5_wave_origin0 (side=0)... ✅ 7.7s 出图 768x752 (113282B)
+[21/40] ▶ qwer_ellipse_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (261812B)
+[22/40] ▶ qwer_ellipse_origin0 (side=0)... ✅ 30.0s 出图 1280x1680 (331013B)
+[23/40] ▶ qwer_flower_default896 (side=None)... ✅ 10.2s 出图 1288x1678 (312834B)
+[24/40] ▶ qwer_flower_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (328605B)
+[25/40] ▶ qwer_heart_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (331337B)
+[26/40] ▶ qwer_heart_origin0 (side=0)... ✅ 25.0s 出图 1280x1680 (355982B)
+[27/40] ▶ qwer_straight_default896 (side=None)... ✅ 10.7s 出图 1288x1678 (281665B)
+[28/40] ▶ qwer_straight_origin0 (side=0)... ✅ 23.4s 出图 1280x1680 (322183B)
+[29/40] ▶ qwer_wave_default896 (side=None)... ✅ 11.1s 出图 1288x1678 (289322B)
+[30/40] ▶ qwer_wave_origin0 (side=0)... ✅ 25.8s 出图 1280x1680 (351719B)
+[31/40] ▶ asdf_ellipse_default896 (side=None)... ✅ 11.2s 出图 1254x1666 (285362B)
+[32/40] ▶ asdf_ellipse_origin0 (side=0)... ✅ 25.7s 出图 1248x1664 (286768B)
+[33/40] ▶ asdf_flower_default896 (side=None)... ✅ 10.7s 出图 1254x1666 (266946B)
+[34/40] ▶ asdf_flower_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (353668B)
+[35/40] ▶ asdf_heart_default896 (side=None)... ✅ 10.4s 出图 1254x1666 (271975B)
+[36/40] ▶ asdf_heart_origin0 (side=0)... ✅ 22.9s 出图 1248x1664 (291983B)
+[37/40] ▶ asdf_straight_default896 (side=None)... ✅ 9.8s 出图 1254x1666 (258665B)
+[38/40] ▶ asdf_straight_origin0 (side=0)... ✅ 24.0s 出图 1248x1664 (279883B)
+[39/40] ▶ asdf_wave_default896 (side=None)... ✅ 11.9s 出图 1254x1666 (286515B)
+[40/40] ▶ asdf_wave_origin0 (side=0)... ✅ 24.3s 出图 1248x1664 (295694B)
+
+======================================================================
+汇总报告
+======================================================================
+
+--- 速度对比(秒,✓=成功 ✗=失败)---
+图 发型 默认896 原图0 差值
+girl2 ellipse 10.8✓ 13.3✓ +2.5
+girl2 flower 11.0✓ 12.5✓ +1.5
+girl2 heart 11.9✓ 12.2✓ +0.3
+girl2 straight 12.1✓ 12.3✓ +0.2
+girl2 wave 12.7✓ 11.9✓ -0.8
+girl5 ellipse 9.6✓ 7.3✓ -2.3
+girl5 flower 8.0✓ 7.2✓ -0.8
+girl5 heart 8.1✓ 7.6✓ -0.5
+girl5 straight 8.4✓ 7.2✓ -1.2
+girl5 wave 8.4✓ 7.7✓ -0.7
+qwer ellipse 10.2✓ 30.0✓ +19.8
+qwer flower 10.2✓ 25.0✓ +14.8
+qwer heart 11.1✓ 25.0✓ +13.9
+qwer straight 10.7✓ 23.4✓ +12.7
+qwer wave 11.1✓ 25.8✓ +14.7
+asdf ellipse 11.2✓ 25.7✓ +14.5
+asdf flower 10.7✓ 22.9✓ +12.2
+asdf heart 10.4✓ 22.9✓ +12.5
+asdf straight 9.8✓ 24.0✓ +14.2
+asdf wave 11.9✓ 24.3✓ +12.4
+
+结果图: /home/ubuntu/hair/image/compare_test/out/
+CSV报告: /home/ubuntu/hair/image/compare_test/out/report.csv
+JSON明细: /home/ubuntu/hair/image/compare_test/out/report.json
diff --git a/image/compare_test/archive_20260725/report.csv b/image/compare_test/archive_20260725/report.csv
new file mode 100644
index 0000000..d1be2ac
--- /dev/null
+++ b/image/compare_test/archive_20260725/report.csv
@@ -0,0 +1,41 @@
+img,style,side,ok,elapsed_s,out_w,out_h,err
+girl2,ellipse,default896,True,10.8,1082,1081,
+girl2,ellipse,origin0,True,13.3,1088,1088,
+girl2,flower,default896,True,11.0,1082,1081,
+girl2,flower,origin0,True,12.5,1088,1088,
+girl2,heart,default896,True,11.9,1082,1081,
+girl2,heart,origin0,True,12.2,1088,1088,
+girl2,straight,default896,True,12.1,1082,1081,
+girl2,straight,origin0,True,12.3,1088,1088,
+girl2,wave,default896,True,12.7,1082,1081,
+girl2,wave,origin0,True,11.9,1088,1088,
+girl5,ellipse,default896,True,9.6,768,752,
+girl5,ellipse,origin0,True,7.3,768,752,
+girl5,flower,default896,True,8.0,768,752,
+girl5,flower,origin0,True,7.2,768,752,
+girl5,heart,default896,True,8.1,768,752,
+girl5,heart,origin0,True,7.6,768,752,
+girl5,straight,default896,True,8.4,768,752,
+girl5,straight,origin0,True,7.2,768,752,
+girl5,wave,default896,True,8.4,768,752,
+girl5,wave,origin0,True,7.7,768,752,
+qwer,ellipse,default896,True,10.2,1288,1678,
+qwer,ellipse,origin0,True,30.0,1280,1680,
+qwer,flower,default896,True,10.2,1288,1678,
+qwer,flower,origin0,True,25.0,1280,1680,
+qwer,heart,default896,True,11.1,1288,1678,
+qwer,heart,origin0,True,25.0,1280,1680,
+qwer,straight,default896,True,10.7,1288,1678,
+qwer,straight,origin0,True,23.4,1280,1680,
+qwer,wave,default896,True,11.1,1288,1678,
+qwer,wave,origin0,True,25.8,1280,1680,
+asdf,ellipse,default896,True,11.2,1254,1666,
+asdf,ellipse,origin0,True,25.7,1248,1664,
+asdf,flower,default896,True,10.7,1254,1666,
+asdf,flower,origin0,True,22.9,1248,1664,
+asdf,heart,default896,True,10.4,1254,1666,
+asdf,heart,origin0,True,22.9,1248,1664,
+asdf,straight,default896,True,9.8,1254,1666,
+asdf,straight,origin0,True,24.0,1248,1664,
+asdf,wave,default896,True,11.9,1254,1666,
+asdf,wave,origin0,True,24.3,1248,1664,
diff --git a/image/compare_test/archive_20260725/report.html b/image/compare_test/archive_20260725/report.html
new file mode 100644
index 0000000..002b1cd
--- /dev/null
+++ b/image/compare_test/archive_20260725/report.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+接口2 分辨率对比测试报告
+
+
+
+
+
💄 接口2 生发 · 分辨率对比测试报告
+
POST /api/v1/hair/grow · female · 4 张图 × 5 发型 × 2 档分辨率 = 40 次 · 串行 · 4090 (24G)
+
+
+
+
+ 📊 结论: 原图长边 ≤ 1082(girl2/girl5)时两档耗时接近;原图长边 > 1600(qwer/asdf)时,原图直送比默认896慢约 2.3~2.5 倍 。
+ 4090 24G 显存扛得住长边 1678 的原图直送(无 OOM)。画质对比 见下方左右并排图,点击可放大。
+
+
+
+
📈 平均耗时对比(按图分组,单位:秒)
+
+
默认 896 原图直送 0
+
11.7s 默认896 12.4s 原图0 girl2长边1082 8.5s 默认896 7.4s 原图0 girl5长边767 10.7s 默认896 25.8s ⚠ 原图0 qwer长边1678 10.8s 默认896 24.0s ⚠ 原图0 asdf长边1666
+
+
+
+
🔍 画质对比(左:默认896 · 右:原图直送)
+
📷 girl2 原图长边 1082px
发型 默认 896 原图直送 0 耗时差 ellipse椭圆 ✅ 10.8s · 1082×1081
✅ 13.3s · 1088×1088
+2.5s flower花瓣 ✅ 11.0s · 1082×1081
✅ 12.5s · 1088×1088
+1.5s heart心形 ✅ 11.9s · 1082×1081
✅ 12.2s · 1088×1088
+0.3s straight直线 ✅ 12.1s · 1082×1081
✅ 12.3s · 1088×1088
+0.2s wave波浪 ✅ 12.7s · 1082×1081
✅ 11.9s · 1088×1088
-0.8s
📷 girl5 原图长边 767px
发型 默认 896 原图直送 0 耗时差 ellipse椭圆 ✅ 9.6s · 768×752
✅ 7.3s · 768×752
-2.3s flower花瓣 ✅ 8.0s · 768×752
✅ 7.2s · 768×752
-0.8s heart心形 ✅ 8.1s · 768×752
✅ 7.6s · 768×752
-0.5s straight直线 ✅ 8.4s · 768×752
✅ 7.2s · 768×752
-1.2s wave波浪 ✅ 8.4s · 768×752
✅ 7.7s · 768×752
-0.7s
📷 qwer 原图长边 1678px
发型 默认 896 原图直送 0 耗时差 ellipse椭圆 ✅ 10.2s · 1288×1678
✅ 30.0s · 1280×1680
+19.8s flower花瓣 ✅ 10.2s · 1288×1678
✅ 25.0s · 1280×1680
+14.8s heart心形 ✅ 11.1s · 1288×1678
✅ 25.0s · 1280×1680
+13.9s straight直线 ✅ 10.7s · 1288×1678
✅ 23.4s · 1280×1680
+12.7s wave波浪 ✅ 11.1s · 1288×1678
✅ 25.8s · 1280×1680
+14.7s
📷 asdf 原图长边 1666px
发型 默认 896 原图直送 0 耗时差 ellipse椭圆 ✅ 11.2s · 1254×1666
✅ 25.7s · 1248×1664
+14.5s flower花瓣 ✅ 10.7s · 1254×1666
✅ 22.9s · 1248×1664
+12.2s heart心形 ✅ 10.4s · 1254×1666
✅ 22.9s · 1248×1664
+12.5s straight直线 ✅ 9.8s · 1254×1666
✅ 24.0s · 1248×1664
+14.2s wave波浪 ✅ 11.9s · 1254×1666
✅ 24.3s · 1248×1664
+12.4s
+
+
+
+
+
+
+
+
diff --git a/image/compare_test/archive_20260725/report.json b/image/compare_test/archive_20260725/report.json
new file mode 100644
index 0000000..ab48e76
--- /dev/null
+++ b/image/compare_test/archive_20260725/report.json
@@ -0,0 +1,562 @@
+[
+ {
+ "key": "girl2_ellipse_default896",
+ "img": "girl2",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.8,
+ "err": "",
+ "out_w": 1082,
+ "out_h": 1081,
+ "bytes": 204589
+ },
+ {
+ "key": "girl2_ellipse_origin0",
+ "img": "girl2",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 13.3,
+ "err": "",
+ "out_w": 1088,
+ "out_h": 1088,
+ "bytes": 232252
+ },
+ {
+ "key": "girl2_flower_default896",
+ "img": "girl2",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.0,
+ "err": "",
+ "out_w": 1082,
+ "out_h": 1081,
+ "bytes": 210875
+ },
+ {
+ "key": "girl2_flower_origin0",
+ "img": "girl2",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 12.5,
+ "err": "",
+ "out_w": 1088,
+ "out_h": 1088,
+ "bytes": 236073
+ },
+ {
+ "key": "girl2_heart_default896",
+ "img": "girl2",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.9,
+ "err": "",
+ "out_w": 1082,
+ "out_h": 1081,
+ "bytes": 198911
+ },
+ {
+ "key": "girl2_heart_origin0",
+ "img": "girl2",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 12.2,
+ "err": "",
+ "out_w": 1088,
+ "out_h": 1088,
+ "bytes": 249893
+ },
+ {
+ "key": "girl2_straight_default896",
+ "img": "girl2",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 12.1,
+ "err": "",
+ "out_w": 1082,
+ "out_h": 1081,
+ "bytes": 211803
+ },
+ {
+ "key": "girl2_straight_origin0",
+ "img": "girl2",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 12.3,
+ "err": "",
+ "out_w": 1088,
+ "out_h": 1088,
+ "bytes": 235377
+ },
+ {
+ "key": "girl2_wave_default896",
+ "img": "girl2",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 12.7,
+ "err": "",
+ "out_w": 1082,
+ "out_h": 1081,
+ "bytes": 207736
+ },
+ {
+ "key": "girl2_wave_origin0",
+ "img": "girl2",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 11.9,
+ "err": "",
+ "out_w": 1088,
+ "out_h": 1088,
+ "bytes": 238081
+ },
+ {
+ "key": "girl5_ellipse_default896",
+ "img": "girl5",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 9.6,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 105867
+ },
+ {
+ "key": "girl5_ellipse_origin0",
+ "img": "girl5",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 7.3,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 112963
+ },
+ {
+ "key": "girl5_flower_default896",
+ "img": "girl5",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 8.0,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 114378
+ },
+ {
+ "key": "girl5_flower_origin0",
+ "img": "girl5",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 7.2,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 108495
+ },
+ {
+ "key": "girl5_heart_default896",
+ "img": "girl5",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 8.1,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 113874
+ },
+ {
+ "key": "girl5_heart_origin0",
+ "img": "girl5",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 7.6,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 103160
+ },
+ {
+ "key": "girl5_straight_default896",
+ "img": "girl5",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 8.4,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 104492
+ },
+ {
+ "key": "girl5_straight_origin0",
+ "img": "girl5",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 7.2,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 100941
+ },
+ {
+ "key": "girl5_wave_default896",
+ "img": "girl5",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 8.4,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 120122
+ },
+ {
+ "key": "girl5_wave_origin0",
+ "img": "girl5",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 7.7,
+ "err": "",
+ "out_w": 768,
+ "out_h": 752,
+ "bytes": 113282
+ },
+ {
+ "key": "qwer_ellipse_default896",
+ "img": "qwer",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.2,
+ "err": "",
+ "out_w": 1288,
+ "out_h": 1678,
+ "bytes": 261812
+ },
+ {
+ "key": "qwer_ellipse_origin0",
+ "img": "qwer",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 30.0,
+ "err": "",
+ "out_w": 1280,
+ "out_h": 1680,
+ "bytes": 331013
+ },
+ {
+ "key": "qwer_flower_default896",
+ "img": "qwer",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.2,
+ "err": "",
+ "out_w": 1288,
+ "out_h": 1678,
+ "bytes": 312834
+ },
+ {
+ "key": "qwer_flower_origin0",
+ "img": "qwer",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 25.0,
+ "err": "",
+ "out_w": 1280,
+ "out_h": 1680,
+ "bytes": 328605
+ },
+ {
+ "key": "qwer_heart_default896",
+ "img": "qwer",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.1,
+ "err": "",
+ "out_w": 1288,
+ "out_h": 1678,
+ "bytes": 331337
+ },
+ {
+ "key": "qwer_heart_origin0",
+ "img": "qwer",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 25.0,
+ "err": "",
+ "out_w": 1280,
+ "out_h": 1680,
+ "bytes": 355982
+ },
+ {
+ "key": "qwer_straight_default896",
+ "img": "qwer",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.7,
+ "err": "",
+ "out_w": 1288,
+ "out_h": 1678,
+ "bytes": 281665
+ },
+ {
+ "key": "qwer_straight_origin0",
+ "img": "qwer",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 23.4,
+ "err": "",
+ "out_w": 1280,
+ "out_h": 1680,
+ "bytes": 322183
+ },
+ {
+ "key": "qwer_wave_default896",
+ "img": "qwer",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.1,
+ "err": "",
+ "out_w": 1288,
+ "out_h": 1678,
+ "bytes": 289322
+ },
+ {
+ "key": "qwer_wave_origin0",
+ "img": "qwer",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 25.8,
+ "err": "",
+ "out_w": 1280,
+ "out_h": 1680,
+ "bytes": 351719
+ },
+ {
+ "key": "asdf_ellipse_default896",
+ "img": "asdf",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.2,
+ "err": "",
+ "out_w": 1254,
+ "out_h": 1666,
+ "bytes": 285362
+ },
+ {
+ "key": "asdf_ellipse_origin0",
+ "img": "asdf",
+ "style": "ellipse",
+ "style_idx": 1,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 25.7,
+ "err": "",
+ "out_w": 1248,
+ "out_h": 1664,
+ "bytes": 286768
+ },
+ {
+ "key": "asdf_flower_default896",
+ "img": "asdf",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.7,
+ "err": "",
+ "out_w": 1254,
+ "out_h": 1666,
+ "bytes": 266946
+ },
+ {
+ "key": "asdf_flower_origin0",
+ "img": "asdf",
+ "style": "flower",
+ "style_idx": 2,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 22.9,
+ "err": "",
+ "out_w": 1248,
+ "out_h": 1664,
+ "bytes": 353668
+ },
+ {
+ "key": "asdf_heart_default896",
+ "img": "asdf",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 10.4,
+ "err": "",
+ "out_w": 1254,
+ "out_h": 1666,
+ "bytes": 271975
+ },
+ {
+ "key": "asdf_heart_origin0",
+ "img": "asdf",
+ "style": "heart",
+ "style_idx": 3,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 22.9,
+ "err": "",
+ "out_w": 1248,
+ "out_h": 1664,
+ "bytes": 291983
+ },
+ {
+ "key": "asdf_straight_default896",
+ "img": "asdf",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 9.8,
+ "err": "",
+ "out_w": 1254,
+ "out_h": 1666,
+ "bytes": 258665
+ },
+ {
+ "key": "asdf_straight_origin0",
+ "img": "asdf",
+ "style": "straight",
+ "style_idx": 4,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 24.0,
+ "err": "",
+ "out_w": 1248,
+ "out_h": 1664,
+ "bytes": 279883
+ },
+ {
+ "key": "asdf_wave_default896",
+ "img": "asdf",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "default896",
+ "side_val": null,
+ "ok": true,
+ "elapsed": 11.9,
+ "err": "",
+ "out_w": 1254,
+ "out_h": 1666,
+ "bytes": 286515
+ },
+ {
+ "key": "asdf_wave_origin0",
+ "img": "asdf",
+ "style": "wave",
+ "style_idx": 5,
+ "side": "origin0",
+ "side_val": 0,
+ "ok": true,
+ "elapsed": 24.3,
+ "err": "",
+ "out_w": 1248,
+ "out_h": 1664,
+ "bytes": 295694
+ }
+]
\ No newline at end of file
diff --git a/image/compare_test/batch_test.py b/image/compare_test/batch_test.py
new file mode 100644
index 0000000..848e95c
--- /dev/null
+++ b/image/compare_test/batch_test.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+"""接口2 female 批量对比测试:4图 × 5发型 × 3分辨率档位 = 60次请求。
+串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
+用法: python3 batch_test.py
+支持断点续跑(progress.json);新增档位时只会补跑未完成项。
+"""
+import base64, csv, json, os, sys, time, traceback
+import requests
+
+API = "http://127.0.0.1:8187/api/v1/hair/grow"
+TOKEN = "dev-shared-secret-2026"
+TIMEOUT = 600
+OUT = "/home/ubuntu/hair/image/compare_test/out"
+PROGRESS = "/home/ubuntu/hair/image/compare_test/progress.json"
+
+IMG_DIR = "/home/ubuntu/hair/image"
+IMAGES = [
+ ("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
+ ("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
+ ("qwer", f"{IMG_DIR}/qwer.jpg"),
+ ("asdf", f"{IMG_DIR}/asdf.jpg"),
+]
+# female: 1=ellipse 2=flower 3=heart 4=straight 5=wave
+STYLES = [
+ (1, "ellipse"), (2, "flower"), (3, "heart"), (4, "straight"), (5, "wave"),
+]
+# 三档: 默认896 / 显式1024 / 原图直送0
+SIDES = [
+ ("default896", None), # 不传 → 后端默认896
+ ("side1024", 1024), # 长边压到 1024
+ ("origin0", 0), # 原图直送
+]
+SIDE_LABEL = {
+ "default896": "默认896",
+ "side1024": "1024",
+ "origin0": "原图0",
+}
+
+def load_progress():
+ if os.path.exists(PROGRESS):
+ try:
+ return json.load(open(PROGRESS))
+ except Exception:
+ pass
+ return {"done": [], "results": []}
+
+def save_progress(prog):
+ json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
+
+def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
+ """跑单次请求,返回 dict 结果。"""
+ key = f"{img_name}_{style_key}_{side_name}"
+ with open(img_path, "rb") as f:
+ img_b64 = base64.b64encode(f.read()).decode()
+ data = {
+ "image_base64": "data:image/jpeg;base64," + img_b64,
+ "gender": "female",
+ "hair_style": str(style_idx),
+ }
+ if side_val is not None:
+ data["redraw_max_side"] = str(side_val)
+ t0 = time.time()
+ rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
+ "side": side_name, "side_val": side_val, "ok": False,
+ "elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0}
+ try:
+ r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
+ rec["elapsed"] = round(time.time() - t0, 1)
+ d = r.json()
+ if d.get("code") != 0:
+ rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
+ return rec
+ results = (d.get("data") or {}).get("results") or []
+ if not results:
+ rec["err"] = "空结果"
+ return rec
+ it = results[0]
+ grown = it.get("grown_image_base64")
+ if not grown:
+ rec["err"] = "无生发图(grown_png=None, 重绘失败)"
+ return rec
+ raw = base64.b64decode(grown)
+ from PIL import Image
+ import io as _io
+ im = Image.open(_io.BytesIO(raw))
+ rec["out_w"], rec["out_h"] = im.size
+ out_path = f"{OUT}/{key}.jpg"
+ with open(out_path, "wb") as fo:
+ fo.write(raw)
+ rec["ok"] = True
+ rec["bytes"] = len(raw)
+ except requests.exceptions.Timeout:
+ rec["elapsed"] = round(time.time() - t0, 1)
+ rec["err"] = f"超时(>{TIMEOUT}s)"
+ except Exception as e:
+ rec["elapsed"] = round(time.time() - t0, 1)
+ rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
+ return rec
+
+def main():
+ os.makedirs(OUT, exist_ok=True)
+ prog = load_progress()
+ done_keys = set(prog["done"])
+ total = len(IMAGES) * len(STYLES) * len(SIDES)
+ print(f"=== 接口2 female 批量对比测试 ===")
+ print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
+ print(f"已跳过 {len(done_keys)} 个已完成项\n")
+
+ idx = 0
+ for img_name, img_path in IMAGES:
+ for style_idx, style_key in STYLES:
+ for side_name, side_val in SIDES:
+ idx += 1
+ key = f"{img_name}_{style_key}_{side_name}"
+ if key in done_keys:
+ print(f"[{idx}/{total}] ⏭ 跳过已完成 {key}")
+ continue
+ print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
+ rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
+ prog["results"].append(rec)
+ prog["done"].append(key)
+ save_progress(prog)
+ if rec["ok"]:
+ print(f"✅ {rec['elapsed']}s 出图 {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
+ else:
+ print(f"❌ {rec['elapsed']}s {rec['err']}")
+ time.sleep(2) # 串行间隔,避免队列堆积
+
+ # 汇总
+ print("\n" + "="*70)
+ print("汇总报告")
+ print("="*70)
+ write_report(prog["results"])
+ print(f"\n结果图: {OUT}/")
+ print(f"CSV报告: {OUT}/report.csv")
+ print(f"JSON明细: {OUT}/report.json")
+
+def write_report(results):
+ # CSV
+ csv_path = f"{OUT}/report.csv"
+ with open(csv_path, "w", newline="") as f:
+ w = csv.writer(f)
+ w.writerow(["img","style","side","ok","elapsed_s","out_w","out_h","err"])
+ for r in results:
+ w.writerow([r["img"],r["style"],r["side"],r["ok"],r["elapsed"],
+ r["out_w"],r["out_h"],r["err"]])
+ # JSON
+ json.dump(results, open(f"{OUT}/report.json","w"), ensure_ascii=False, indent=1)
+ # 控制台速度对比表
+ print("\n--- 速度对比(秒,✓=成功 ✗=失败)---")
+ headers = ["图", "发型"] + [SIDE_LABEL[s] for s, _ in SIDES]
+ print(f"{headers[0]:<8}{headers[1]:<10}" + "".join(f"{h:>12}" for h in headers[2:]))
+ for img_name, _ in IMAGES:
+ for _, style_key in STYLES:
+ cells = []
+ for side_name, _ in SIDES:
+ r = next((x for x in results if x["img"]==img_name and x["style"]==style_key and x["side"]==side_name), None)
+ cells.append(f"{r['elapsed']}{'✓' if r['ok'] else '✗'}" if r else "-")
+ print(f"{img_name:<8}{style_key:<10}" + "".join(f"{c:>12}" for c in cells))
+
+if __name__ == "__main__":
+ main()
diff --git a/image/compare_test/gen_report.py b/image/compare_test/gen_report.py
new file mode 100644
index 0000000..9d39dba
--- /dev/null
+++ b/image/compare_test/gen_report.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""把 report.json + 结果图生成为自包含 HTML 报告(支持 896 / 1024 / 原图0 三档)。"""
+import json, os, html
+
+OUT = "/home/ubuntu/hair/image/compare_test/out"
+REPORT_HTML = os.path.join(OUT, "report.html")
+results = json.load(open(os.path.join(OUT, "report.json")))
+
+IMAGES = ["girl2", "girl5", "qwer", "asdf"]
+STYLES = ["ellipse", "flower", "heart", "straight", "wave"]
+SIDES = [
+ ("default896", "默认 896", "#2563eb", "d"),
+ ("side1024", "1024", "#10b981", "m"),
+ ("origin0", "原图直送 0", "#f59e0b", "o"),
+]
+IMG_LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666}
+STYLE_CN = {"ellipse": "椭圆", "flower": "花瓣", "heart": "心形", "straight": "直线", "wave": "波浪"}
+
+def get(img, style, side):
+ for r in results:
+ if r["img"] == img and r["style"] == style and r["side"] == side:
+ return r
+ return None
+
+# 总览统计(>60s 视为冷启动/异常,不进均值)
+total = len(results)
+ok = sum(1 for r in results if r["ok"])
+COLD_S = 60.0
+
+def avg(seq):
+ seq = [x for x in seq if x is not None]
+ return sum(seq) / len(seq) if seq else 0
+
+def times(side, img=None):
+ out = []
+ for r in results:
+ if not r["ok"] or r["side"] != side:
+ continue
+ if img is not None and r["img"] != img:
+ continue
+ if r["elapsed"] >= COLD_S:
+ continue
+ out.append(r["elapsed"])
+ return out
+
+cold_n = sum(1 for r in results if r["ok"] and r["elapsed"] >= COLD_S)
+side_avgs = {sid: avg(times(sid)) for sid, *_ in SIDES}
+all_times = [t for sid, *_ in SIDES for t in times(sid)]
+max_bar = max(all_times + [1])
+
+# 构造速度对比图数据:每图 × 三档
+chart_rows = []
+for img in IMAGES:
+ chart_rows.append((img, IMG_LONGSIDE[img], [avg(times(sid, img)) for sid, *_ in SIDES]))
+
+def bar_svg():
+ bar_h = 22
+ gap = 12
+ label_w = 78
+ chart_w = 760
+ n_bars = len(SIDES)
+ rows = len(chart_rows)
+ h = rows * (bar_h * n_bars + gap) + 40
+ parts = [f'']
+ y = 10
+ for img, longside, avgs in chart_rows:
+ base = avgs[0] if avgs and avgs[0] else 1
+ for i, ((sid, label, color, _), a) in enumerate(zip(SIDES, avgs)):
+ yi = y + i * bar_h
+ w = int(a / max_bar * chart_w) if a else 0
+ warn = "⚠" if (i > 0 and a > base * 1.8) else ""
+ parts.append(f' ')
+ parts.append(f'{a:.1f}s {warn} ')
+ parts.append(f'{html.escape(label)} ')
+ parts.append(f'{img}长边{longside} ')
+ y += bar_h * n_bars + gap
+ parts.append(" ")
+ return "".join(parts)
+
+def img_cell(r, cls):
+ if not r:
+ return f'缺失 '
+ fname = r["key"] + ".jpg"
+ status = "✅" if r["ok"] else "❌"
+ t = f'{r["elapsed"]}s'
+ dim = f'{r["out_w"]}×{r["out_h"]}'
+ err = f'{html.escape(r["err"])}
' if r["err"] else ""
+ img_tag = (f' '
+ if r["ok"] else '无图
')
+ return (f'{img_tag}
'
+ f'{status} {t} · {dim}
{err} ')
+
+def compare_cards():
+ out = []
+ for img in IMAGES:
+ out.append(f'📷 {html.escape(img)} 原图长边 {IMG_LONGSIDE[img]}px
')
+ heads = "".join(f'
{html.escape(label)} ' for _, label, _, cls in SIDES)
+ out.append(f'
发型 {heads}')
+ for style in STYLES:
+ sc = STYLE_CN[style]
+ cells = "".join(img_cell(get(img, style, sid), cls) for sid, _, _, cls in SIDES)
+ out.append(f'{html.escape(style)}{sc} {cells} ')
+ out.append("
")
+ return "".join(out)
+
+# 结论
+small = [img for img in IMAGES if IMG_LONGSIDE[img] <= 1100]
+large = [img for img in IMAGES if IMG_LONGSIDE[img] > 1100]
+def ratio_range(num_side, den_side="default896"):
+ ratios = []
+ for img in large:
+ den = avg(times(den_side, img))
+ num = avg(times(num_side, img))
+ if den:
+ ratios.append(num / den)
+ if not ratios:
+ return 0, 0
+ return min(ratios), max(ratios)
+
+r1024_lo, r1024_hi = ratio_range("side1024")
+r0_lo, r0_hi = ratio_range("origin0")
+cold_note = f"冷启动 {cold_n} 次(≥{COLD_S:.0f}s)已从均值剔除。" if cold_n else ""
+conclusion = (
+ f"原图长边 ≤ 1100({'/'.join(small)})时三档耗时接近;"
+ f"长边 > 1600({'/'.join(large)})时相对默认896:"
+ f"1024 约 {r1024_lo:.1f}~{r1024_hi:.1f}× ,"
+ f"原图直送约 {r0_lo:.1f}~{r0_hi:.1f}× 。"
+ f"{cold_note}画质对比见下方三列并排,点击可放大。"
+)
+
+avg896 = side_avgs.get("default896", 0)
+avg1024 = side_avgs.get("side1024", 0)
+avg0 = side_avgs.get("origin0", 0)
+ratio1024 = avg1024 / avg896 if avg896 else 0
+ratio0 = avg0 / avg896 if avg896 else 0
+
+legend = "".join(
+ f' {html.escape(label)} '
+ for _, label, color, _ in SIDES
+)
+
+html_doc = f"""
+
+
+
+
+接口2 分辨率对比测试报告
+
+
+
+
+
接口2 生发 · 分辨率对比测试报告
+
POST /api/v1/hair/grow · female · 4 张图 × 5 发型 × 3 档分辨率 = {total} 次 · 串行
+
+
+
+
+
{avg1024:.1f}s
1024 平均 · ×{ratio1024:.2f}
+
{avg0:.1f}s
原图直送 平均 · ×{ratio0:.2f}
+
+
+
+
+ 结论: {conclusion}
+
+
+
+
平均耗时对比(按图分组,单位:秒)
+
+
{legend}
+ {bar_svg()}
+
+
+
+
画质对比(左→右:默认896 · 1024 · 原图直送)
+ {compare_cards()}
+
+
+
+
+
+
+
+
+"""
+
+with open(REPORT_HTML, "w") as f:
+ f.write(html_doc)
+# 同步首页
+with open(os.path.join(OUT, "index.html"), "w") as f:
+ f.write(html_doc)
+print(f"已生成: {REPORT_HTML}")
+print(f"图片目录: {OUT}")
diff --git a/image/compare_test/report-server.service b/image/compare_test/report-server.service
new file mode 100644
index 0000000..a8e0006
--- /dev/null
+++ b/image/compare_test/report-server.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=接口2 分辨率对比测试报告 HTTP 服务 (8848)
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=ubuntu
+WorkingDirectory=/home/ubuntu/hair/image/compare_test/out
+ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8848 --bind 0.0.0.0
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
diff --git a/image/res_test/batch_test.py b/image/res_test/batch_test.py
new file mode 100644
index 0000000..46af7bf
--- /dev/null
+++ b/image/res_test/batch_test.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""接口2 female 分辨率对比测试 v2: 5图 × 3发型 × 5档 = 75次。
+串行执行,记录耗时与成败,结果图按 图_发型_档位 命名保存。
+支持断点续跑(progress.json)。
+"""
+import base64, csv, json, os, sys, time, io
+import requests
+from PIL import Image
+
+API = "http://127.0.0.1:8187/api/v1/hair/grow"
+TOKEN = "dev-shared-secret-2026"
+TIMEOUT = 600
+OUT = "/home/ubuntu/hair/image/res_test/out"
+PROGRESS = "/home/ubuntu/hair/image/res_test/progress.json"
+
+IMG_DIR = "/home/ubuntu/hair/image"
+IMAGES = [
+ ("girl2", f"{IMG_DIR}/girl_img/girl2.jpg"),
+ ("girl5", f"{IMG_DIR}/girl_img/girl5.jpg"),
+ ("qwer", f"{IMG_DIR}/qwer.jpg"),
+ ("asdf", f"{IMG_DIR}/asdf.jpg"),
+ ("girl7", f"{IMG_DIR}/girl_img/girl7.jpg"),
+]
+# female: 2=flower(花瓣) 3=heart(心形) 5=wave(波浪)
+STYLES = [(2, "flower"), (3, "heart"), (5, "wave")]
+# 5档: 原图(0) / 1024 / 896 / 768 / 640
+SIDES = [
+ ("origin", 0),
+ ("s1024", 1024),
+ ("s896", 896),
+ ("s768", 768),
+ ("s640", 640),
+]
+
+def load_progress():
+ if os.path.exists(PROGRESS):
+ try:
+ return json.load(open(PROGRESS))
+ except Exception:
+ pass
+ return {"done": [], "results": []}
+
+def save_progress(prog):
+ json.dump(prog, open(PROGRESS, "w"), ensure_ascii=False, indent=1)
+
+def run_one(img_name, img_path, style_idx, style_key, side_name, side_val):
+ key = f"{img_name}_{style_key}_{side_name}"
+ with open(img_path, "rb") as f:
+ img_b64 = base64.b64encode(f.read()).decode()
+ data = {
+ "image_base64": "data:image/jpeg;base64," + img_b64,
+ "gender": "female",
+ "hair_style": str(style_idx),
+ "redraw_max_side": str(side_val), # 含原图档=0
+ }
+ t0 = time.time()
+ rec = {"key": key, "img": img_name, "style": style_key, "style_idx": style_idx,
+ "side": side_name, "side_val": side_val, "ok": False,
+ "elapsed": 0.0, "err": "", "out_w": 0, "out_h": 0, "bytes": 0}
+ try:
+ r = requests.post(API, data=data, headers={"X-Internal-Token": TOKEN}, timeout=TIMEOUT)
+ rec["elapsed"] = round(time.time() - t0, 1)
+ d = r.json()
+ if d.get("code") != 0:
+ rec["err"] = f"code={d.get('code')} {d.get('message','')}"[:200]
+ return rec
+ results = (d.get("data") or {}).get("results") or []
+ if not results:
+ rec["err"] = "空结果"
+ return rec
+ it = results[0]
+ grown = it.get("grown_image_base64")
+ if not grown:
+ rec["err"] = "无生发图(重绘失败/OOM?)"
+ return rec
+ raw = base64.b64decode(grown)
+ im = Image.open(io.BytesIO(raw))
+ rec["out_w"], rec["out_h"] = im.size
+ out_path = f"{OUT}/{key}.jpg"
+ with open(out_path, "wb") as fo:
+ fo.write(raw)
+ rec["ok"] = True
+ rec["bytes"] = len(raw)
+ except requests.exceptions.Timeout:
+ rec["elapsed"] = round(time.time() - t0, 1)
+ rec["err"] = f"超时(>{TIMEOUT}s)"
+ except Exception as e:
+ rec["elapsed"] = round(time.time() - t0, 1)
+ rec["err"] = f"{type(e).__name__}: {str(e)[:180]}"
+ return rec
+
+def main():
+ prog = load_progress()
+ done_keys = set(prog["done"])
+ total = len(IMAGES) * len(STYLES) * len(SIDES)
+ print(f"=== 接口2 female 分辨率对比测试 v2 ===")
+ print(f"矩阵: {len(IMAGES)}图 × {len(STYLES)}发型 × {len(SIDES)}档 = {total} 次")
+ print(f"已跳过 {len(done_keys)} 个已完成项\n")
+
+ idx = 0
+ for img_name, img_path in IMAGES:
+ for style_idx, style_key in STYLES:
+ for side_name, side_val in SIDES:
+ idx += 1
+ key = f"{img_name}_{style_key}_{side_name}"
+ if key in done_keys:
+ print(f"[{idx}/{total}] ⏭ 跳过 {key}")
+ continue
+ print(f"[{idx}/{total}] ▶ {key} (side={side_val})...", end=" ", flush=True)
+ rec = run_one(img_name, img_path, style_idx, style_key, side_name, side_val)
+ prog["results"].append(rec)
+ prog["done"].append(key)
+ save_progress(prog)
+ if rec["ok"]:
+ print(f"✅ {rec['elapsed']}s {rec['out_w']}x{rec['out_h']} ({rec['bytes']}B)")
+ else:
+ print(f"❌ {rec['elapsed']}s {rec['err']}")
+ time.sleep(2)
+
+ print("\n" + "=" * 70)
+ print("汇总")
+ print("=" * 70)
+ write_report(prog["results"])
+ print(f"\n结果图: {OUT}/")
+ print(f"CSV: {OUT}/report.csv JSON: {OUT}/report.json")
+
+def write_report(results):
+ with open(f"{OUT}/report.csv", "w", newline="") as f:
+ w = csv.writer(f)
+ w.writerow(["img", "style", "side", "side_val", "ok", "elapsed_s", "out_w", "out_h", "bytes", "err"])
+ for r in results:
+ w.writerow([r["img"], r["style"], r["side"], r["side_val"], r["ok"],
+ r["elapsed"], r["out_w"], r["out_h"], r["bytes"], r["err"]])
+ json.dump(results, open(f"{OUT}/report.json", "w"), ensure_ascii=False, indent=1)
+
+ # 控制台速度表:按 图×发型 分行,5档列
+ print("\n--- 速度对比(秒)---")
+ print(f"{'图·发型':<18}{'原图0':>8}{'1024':>8}{'896':>8}{'768':>8}{'640':>8}")
+ for img_name, _ in IMAGES:
+ for _, style_key in STYLES:
+ cells = []
+ for side_name, _ in SIDES:
+ r = next((x for x in results if x["img"] == img_name and x["style"] == style_key and x["side"] == side_name), None)
+ cells.append(f"{r['elapsed']}✓" if r and r["ok"] else (f"{r['elapsed']}✗" if r else "-"))
+ print(f"{img_name+'·'+style_key:<18}{cells[0]:>8}{cells[1]:>8}{cells[2]:>8}{cells[3]:>8}{cells[4]:>8}")
+
+if __name__ == "__main__":
+ main()
diff --git a/image/res_test/gen_report.py b/image/res_test/gen_report.py
new file mode 100644
index 0000000..f4d18fb
--- /dev/null
+++ b/image/res_test/gen_report.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+"""生成 v2 分辨率对比报告:每行=原图+5档,按图×发型组织15行。"""
+import json, os, html
+
+OUT = "/home/ubuntu/hair/image/res_test/out"
+REPORT = os.path.join(OUT, "report.html")
+results = json.load(open(os.path.join(OUT, "report.json")))
+
+IMAGES = ["girl2", "girl5", "qwer", "asdf", "girl7"]
+STYLES = [("flower", "花瓣"), ("heart", "心形"), ("wave", "波浪")]
+SIDES = [("origin", "原图直送", 0), ("s1024", "1024", 1024),
+ ("s896", "896", 896), ("s768", "768", 768), ("s640", "640", 640)]
+LONGSIDE = {"girl2": 1082, "girl5": 767, "qwer": 1678, "asdf": 1666, "girl7": 925}
+
+def get(img, style, side):
+ for r in results:
+ if r["img"] == img and r["style"] == style and r["side"] == side:
+ return r
+ return None
+
+# 统计
+total = len(results)
+ok = sum(1 for r in results if r["ok"])
+
+# 每档平均耗时(排除冷启动异常值:girl2_flower_origin=135s 明显是冷启动)
+def avg(side, exclude_first_cold=False):
+ ts = []
+ for r in results:
+ if r["side"] == side and r["ok"]:
+ if exclude_first_cold and r["key"] == "girl2_flower_origin":
+ continue # 跳过冷启动
+ ts.append(r["elapsed"])
+ return sum(ts) / len(ts) if ts else 0
+
+avg_origin = avg("origin", exclude_first_cold=True)
+avg_1024 = avg("s1024")
+avg_896 = avg("s896")
+avg_768 = avg("s768")
+avg_640 = avg("s640")
+avgs = [("origin", "原图直送", avg_origin, 0),
+ ("s1024", "1024", avg_1024, 1024),
+ ("s896", "896", avg_896, 896),
+ ("s768", "768", avg_768, 768),
+ ("s640", "640", avg_640, 640)]
+
+# 速度色阶:以全部耗时的 min-max 映射颜色(绿→黄→红)
+all_t = sorted([r["elapsed"] for r in results if r["ok"] and r["key"] != "girl2_flower_origin"])
+tmin, tmax = all_t[0], all_t[-1]
+def speed_color(t):
+ if tmax == tmin:
+ return "#16a34a"
+ ratio = (t - tmin) / (tmax - tmin) # 0=最快(绿) 1=最慢(红)
+ if ratio < 0.33:
+ return "#16a34a" # 绿
+ elif ratio < 0.66:
+ return "#f59e0b" # 橙
+ else:
+ return "#dc2626" # 红
+
+# 表格行
+rows_html = []
+for img in IMAGES:
+ for style_key, style_cn in STYLES:
+ # 原图格
+ orig_cell = (f''
+ f''
+ f'📷 原图
')
+ # 5档格
+ side_cells = []
+ for side_name, side_lbl, side_val in SIDES:
+ r = get(img, style_key, side_name)
+ if r and r["ok"]:
+ col = speed_color(r["elapsed"])
+ cell = (f''
+ f''
+ f'{r["elapsed"]}s · '
+ f'{r["out_w"]}×{r["out_h"]}
')
+ elif r:
+ cell = (f'❌
'
+ f'{html.escape(r["err"][:30])}
')
+ else:
+ cell = '—
'
+ side_cells.append(cell)
+ row_label = (f''
+ f'{html.escape(img)}
'
+ f'{html.escape(style_cn)}
'
+ f'长边 {LONGSIDE[img]}
')
+ rows_html.append("" + row_label + orig_cell + "".join(side_cells) + " ")
+
+# 顶部平均耗时卡
+def stat_card(lbl, val, col, sub=""):
+ return (f''
+ f'
{val:.1f}s
'
+ f'
{lbl}{sub}
')
+
+stat_cards = "".join([
+ stat_card("原图直送", avg_origin, "#dc2626", "排除冷启动 "),
+ stat_card("1024", avg_1024, "#f59e0b"),
+ stat_card("896 (默认)", avg_896, "#2563eb"),
+ stat_card("768", avg_768, "#16a34a"),
+ stat_card("640", avg_640, "#0d9488"),
+])
+
+html_doc = f"""
+
+
+
+
+接口2 分辨率对比测试 v2
+
+
+
+
+
💄 接口2 生发 · 5档分辨率对比报告
+
POST /api/v1/hair/grow · female · 5 图 × 3 发型(花瓣/心形/波浪) × 5 档分辨率 = 75 次 · 串行 · 4090 (24G)
+
+
+
+
+ 📊 结论: 耗时随送图分辨率单调下降。大图(qwer/asdf 长边~1670) 原图直送需 ~27-30s,是 896 档(10s) 的近 3 倍 ;
+ 中小图(girl2/girl5/girl7 长边 767-1082)各档差异较小(6-15s)。
+ 4090 24G 全程无 OOM ,75/75 成功。画质对比 见下表(横向滑动),点击任意图可放大。
+
+ 快(<{tmin+ (tmax-tmin)*0.33:.0f}s)
+ 中等
+ 慢(>{tmin+ (tmax-tmin)*0.66:.0f}s)
+
+
+
+
+
+
+
+ 图 · 发型
+ 📷 原图
+ 原图直送 (0)不缩放
+ 1024
+ 896默认
+ 768
+ 640
+
+
+
+ {"".join(rows_html)}
+
+
+
+
+
+
+
+
+"""
+with open(REPORT, "w") as f:
+ f.write(html_doc)
+print(f"已生成: {REPORT}")
diff --git a/image/res_test/report-server-v2.service b/image/res_test/report-server-v2.service
new file mode 100644
index 0000000..74b69f2
--- /dev/null
+++ b/image/res_test/report-server-v2.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=接口2 5档分辨率对比报告 v2 (8849)
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=ubuntu
+WorkingDirectory=/home/ubuntu/hair/image/res_test/out
+ExecStart=/home/ubuntu/miniconda3/envs/my_hair/bin/python -m http.server 8849 --bind 0.0.0.0
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
diff --git a/static/test_interface2.html b/static/test_interface2.html
index ed23551..31ff52f 100644
--- a/static/test_interface2.html
+++ b/static/test_interface2.html
@@ -101,6 +101,16 @@
⬜ 关闭 — 空遮罩,不烧模板线
+
+ 重绘分辨率
+
+ 默认 896
+ 1024(高清,慢)
+ 768(快)
+ 640(最快,糊)
+ 0 — 不缩图(原图直送)
+
+
🚀 提交
清除
@@ -155,6 +165,11 @@ async function submitTest() {
fd.append('hair_style', checked.join(','));
fd.append('use_mask', $('useMask').value);
fd.append('prompt', $('promptInput').value);
+ // 重绘分辨率:仅 female 传给后端(male 路径不接受此参数)
+ if ($('gender').value === 'female') {
+ const side = $('redrawMaxSide').value;
+ if (side !== '') fd.append('redraw_max_side', side); // 空串=不传→后端默认896
+ }
const _reqStart = performance.now();
try {
const r = await fetch(API_BASE + '/api/v1/hair/grow', { method:'POST', headers:{ 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
@@ -258,6 +273,8 @@ function onGenderChange() {
lb.appendChild(document.createTextNode(' ' + s.label));
div.appendChild(lb);
});
+ // 重绘分辨率选项仅 female 生效:male 时隐藏
+ $('redrawSideGroup').style.display = (g === 'female') ? '' : 'none';
}
function selectAllHair(select) {