recommender / test.py
zhenyuxyz's picture
Upload 7 files
54b590a verified
# 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