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