# 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