|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
|
| 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
|
| 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}")
|
|
|
|
|
| data = load_request_json(input_path)
|
|
|
|
|
| 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")
|
|
|
|
|
| attach_body_type(report)
|
|
|
|
|
| print_top10_color_combos(report)
|
|
|
|
|
| result = run_recommend_model(report, weather)
|
|
|
|
|
| out_dir = base_dir / "output"
|
| out_dir.mkdir(exist_ok=True)
|
|
|
|
|
| stem = input_path.stem
|
| 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
|
|
|