File size: 8,274 Bytes
54b590a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | # test.py
# 放在 RECOMMEND_SERVICE/ 同一層執行:
# python test.py saved_requests/request_xxx.json
# 或不帶參數(自動抓 saved_requests/ 最新的 .json):
# python test.py
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Dict, Optional
# 你的專案模組(同層)
from recommender import (
ALPHA_SKIN,
COLOR_DB,
DOUBLE_NEUTRAL_EXTRA_BONUS,
NEUTRAL_COLOR_BONUS,
_is_neutral_color,
color_harmony_scores_for_combo,
extract_skin_rgb,
get_color_combos_for_user,
rgb_to_hsl,
run_recommend_model,
skin_compatibility_for_outfit,
)
from body_type_classifier import classify_male_body_type, classify_female_body_type
DEFAULT_GENDER = "male" # 跟你 API 裡的行為一致:沒給 gender 就先用 male
def extract_gender(report: Dict[str, Any]) -> Optional[str]:
"""
從 report 抓性別。找不到就回 DEFAULT_GENDER。
"""
gender = (
report.get("gender")
or report.get("sex")
or report.get("Gender")
or report.get("user_gender")
or report.get("body_gender") # 若前面已經有,就沿用
)
if isinstance(gender, str):
g = gender.strip().lower()
if g in ("male", "m", "boy", "man", "男", "男性"):
return "male"
if g in ("female", "f", "girl", "woman", "女", "女性"):
return "female"
# 找不到就用預設
if DEFAULT_GENDER is not None:
print(f"[BodyType] report 中沒有可用的 gender 欄位,暫時使用 DEFAULT_GENDER={DEFAULT_GENDER}")
return DEFAULT_GENDER
return None
def extract_body_measurements(report: Dict[str, Any]) -> Optional[Dict[str, float]]:
"""
從 report 取得 body_measurements dict。
你提供的 request JSON 結構是 report["body_measurements"] = {...}
"""
bm = report.get("body_measurements")
if isinstance(bm, dict) and bm:
return bm # type: ignore[return-value]
return None
def attach_body_type(report: Dict[str, Any]) -> None:
"""
先依 body_measurements + gender 判斷身形,寫回 report:
report["body_type"]
report["body_gender"]
"""
body_measurements = extract_body_measurements(report)
gender = extract_gender(report)
if not body_measurements or not gender:
print("[BodyType] 無法判斷:缺少 body_measurements 或 gender(且 DEFAULT_GENDER=None)")
return
try:
if gender == "male":
body_type = classify_male_body_type(body_measurements)
else:
body_type = classify_female_body_type(body_measurements)
print(f"[BodyType] gender={gender} → body_type={body_type}")
report["body_type"] = body_type
report["body_gender"] = gender
except Exception as e:
print(f"[BodyType] 判斷身形時發生錯誤: {e}")
def pick_default_request_file(base_dir: Path) -> Path:
"""
不帶參數時:自動挑 saved_requests/ 裡最新修改的 .json
"""
req_dir = base_dir / "saved_requests"
if not req_dir.exists():
raise FileNotFoundError(f"找不到資料夾:{req_dir}")
candidates = sorted(req_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
if not candidates:
raise FileNotFoundError(f"{req_dir} 底下沒有任何 .json 檔可以測試")
return candidates[0]
def load_request_json(path: Path) -> Dict[str, Any]:
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ValueError("request JSON 的最外層必須是 dict")
return data
def print_top10_color_combos(report: Dict[str, Any]) -> None:
"""
印出該使用者顏色組合前 10 名(含分數)。
分數公式與 recommender 內一致:
S_color = max(S_sim, S_comp, S_cont) + ALPHA_SKIN * S_skin + neutral_bonus
"""
gender = (
report.get("body_gender")
or report.get("gender")
or report.get("sex")
)
skin_info = report.get("skin_analysis", {}) or {}
if not isinstance(skin_info, dict):
skin_info = {}
r, g, b = extract_skin_rgb(skin_info)
skin_hsl = rgb_to_hsl(r, g, b)
color_combos = get_color_combos_for_user(report, gender)
if not color_combos:
print("[ColorDebug] 沒有可用的顏色組合。")
return
scored = []
for top_zh, bottom_zh in color_combos:
c1 = COLOR_DB.get(top_zh)
c2 = COLOR_DB.get(bottom_zh)
if not c1 or not c2:
continue
hsl1 = c1["hsl"]
hsl2 = c2["hsl"]
s_sim, s_comp, s_cont = color_harmony_scores_for_combo([hsl1, hsl2])
s_skin = skin_compatibility_for_outfit(skin_hsl, [hsl1, hsl2])
neutral_bonus = 0.0
if _is_neutral_color(top_zh) or _is_neutral_color(bottom_zh):
neutral_bonus += NEUTRAL_COLOR_BONUS
if _is_neutral_color(top_zh) and _is_neutral_color(bottom_zh):
neutral_bonus += DOUBLE_NEUTRAL_EXTRA_BONUS
s_color = max(s_sim, s_comp, s_cont) + ALPHA_SKIN * s_skin + neutral_bonus
scored.append((s_color, top_zh, bottom_zh, c1["en"], c2["en"]))
if not scored:
print("[ColorDebug] 顏色組合分數計算失敗。")
return
scored.sort(key=lambda x: x[0], reverse=True)
print("[ColorDebug] Top 10 顏色組合(含分數):")
for rank, (score, top_zh, bottom_zh, top_en, bottom_en) in enumerate(scored[:20], start=1):
print(
f" {rank:02d}. ({top_zh}/{top_en}) + ({bottom_zh}/{bottom_en}) "
f"=> score={score:.2f}"
)
def main() -> int:
base_dir = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="純 Python 測試:先判斷身形,再跑推薦,輸出到 output/")
parser.add_argument(
"input",
nargs="?",
help="request JSON 路徑(例如 saved_requests/request_xxx.json)。不填則自動抓 saved_requests/ 最新檔。",
)
args = parser.parse_args()
# 決定輸入檔
if args.input:
input_path = (base_dir / args.input).resolve() if not Path(args.input).is_absolute() else Path(args.input)
else:
input_path = pick_default_request_file(base_dir)
if not input_path.exists():
print(f"[Error] 找不到輸入檔:{input_path}")
return 2
print(f"[Test] 使用輸入檔:{input_path}")
# 讀 JSON
data = load_request_json(input_path)
# 解析 report / weather
if "report" in data and "weather" in data:
report = data["report"]
weather = data["weather"]
else:
raise ValueError("request JSON 必須包含 'report' 與 'weather' 兩個 key(最外層)")
if not isinstance(report, dict):
raise ValueError("'report' 必須是 dict")
if not isinstance(weather, dict):
raise ValueError("'weather' 必須是 dict")
# 方式 1:先做身形判斷(寫回 report)
attach_body_type(report)
# 額外印出顏色組合分數前 10 名
print_top10_color_combos(report)
# 跑推薦
result = run_recommend_model(report, weather)
# 輸出資料夾 output/
out_dir = base_dir / "output"
out_dir.mkdir(exist_ok=True)
# 輸出檔名:跟輸入檔對應
stem = input_path.stem # e.g. request_anonymous_1763...
out_path = out_dir / f"{stem}_output.json"
with out_path.open("w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"[Test] 推薦結果已輸出:{out_path}")
print("[Test] 預覽(前幾項):")
# 簡單預覽
preview_items = list(result.items())[:3]
print(json.dumps(dict(preview_items), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as e:
print(f"[Fatal] 測試失敗:{e}")
raise
|