zhenyuxyz commited on
Commit
54b590a
·
verified ·
1 Parent(s): bd5dd2b

Upload 7 files

Browse files
Files changed (7) hide show
  1. body_type_classifier.py +262 -0
  2. colors.json +138 -0
  3. outfits.json +1990 -0
  4. outfits.txt +16 -0
  5. recommend_api.py +137 -0
  6. recommender.py +900 -0
  7. test.py +252 -0
body_type_classifier.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ # ===========================
4
+ # 男性體型分類
5
+ # ===========================
6
+ def classify_male_body_type(body_measurements):
7
+ """
8
+ 根據胸圍、腰圍、臀圍判斷男生體型:
9
+ 回傳其中一個:
10
+ "矩形", "三角形", "倒三角", "梯形", "橢圓形"
11
+ """
12
+
13
+ C = body_measurements["chest circumference"] # 胸圍
14
+ W = body_measurements["waist circumference"] # 腰圍
15
+ H = body_measurements["hip circumference"] # 臀圍
16
+
17
+ if C <= 0 or W <= 0 or H <= 0:
18
+ raise ValueError("胸圍、腰圍、臀圍必須為正數")
19
+
20
+ # 1) 轉成比例(避免整體放大時結果改變)
21
+ total = C + W + H
22
+ pC, pW, pH = C / total, W / total, H / total
23
+ v = (pC, pW, pH)
24
+
25
+ # 2) 各體型的「理想比例」(pC, pW, pH)
26
+ ideal_patterns = {
27
+ "矩形": (0.34, 0.32, 0.34),
28
+ "三角形": (0.32, 0.31, 0.37),
29
+ "倒三角": (0.37, 0.31, 0.32),
30
+ "梯形": (0.36, 0.31, 0.33),
31
+ "橢圓形": (0.32, 0.36, 0.32),
32
+ }
33
+
34
+ # 3) 表格規則的門檻
35
+ def gate_rect(C, W, H):
36
+ # 胸臀差 ≤ 5%,腰比胸臀小
37
+ diff_CH = abs(C - H) / max(C, H)
38
+ return diff_CH <= 0.05 and W <= min(C, H)
39
+
40
+ def gate_tri(C, W, H):
41
+ # 三角形(下寬上窄):臀比胸大 > 10%,且腰比胸臀都小
42
+ return (H >= C * 1.10) and (W <= min(C, H))
43
+
44
+ def gate_inv(C, W, H):
45
+ # 倒三角(上寬下窄):胸比臀大 > 10%,且腰比胸臀都小
46
+ return (C >= H * 1.10) and (W <= min(C, H))
47
+
48
+ def gate_trap(C, W, H):
49
+ # 梯形:胸比臀大 5%~10%,腰比胸臀小
50
+ return (C > H * 1.05) and (C <= H * 1.10) and (W <= min(C, H))
51
+
52
+ def gate_oval(C, W, H):
53
+ # 橢圓形:腰比胸臀都大 5% 以上
54
+ return (W >= C * 1.05) and (W >= H * 1.05)
55
+
56
+ gates = {
57
+ "矩形": gate_rect,
58
+ "三角形": gate_tri,
59
+ "倒三角": gate_inv,
60
+ "梯形": gate_trap,
61
+ "橢圓形": gate_oval,
62
+ }
63
+
64
+ def dist(a, b):
65
+ return math.sqrt(
66
+ (a[0] - b[0]) ** 2 +
67
+ (a[1] - b[1]) ** 2 +
68
+ (a[2] - b[2]) ** 2
69
+ )
70
+
71
+ # 4) 先找有通過門檻的
72
+ candidates = []
73
+ for name in ideal_patterns.keys():
74
+ if gates[name](C, W, H):
75
+ d = dist(v, ideal_patterns[name])
76
+ candidates.append((d, name))
77
+
78
+ if candidates:
79
+ candidates.sort()
80
+ return candidates[0][1]
81
+
82
+ # 5) 若完全沒有門檻通過,就純看誰最接近 ideal pattern
83
+ best_name = None
84
+ best_d = float("inf")
85
+ for name, ideal_v in ideal_patterns.items():
86
+ d = dist(v, ideal_v)
87
+ if d < best_d:
88
+ best_d = d
89
+ best_name = name
90
+
91
+ return best_name
92
+
93
+
94
+ # ===========================
95
+ # 女性體型分類
96
+ # ===========================
97
+ def classify_female_body_type(body_measurements):
98
+ """
99
+ 使用【比例】而不是【公分】來判斷女性體型,並引入 ideal_patterns + 距離判斷:
100
+ - 沙漏型:胸 & 臀 都明顯 > 腰,且胸臀相近
101
+ - 蘋果型:腰明顯 > 臀(且多半 ≥ 胸)
102
+ - 梨型:臀明顯 > 肩寬x2
103
+ - 倒三角型:肩寬x2 明顯 > 臀
104
+ - H 型:胸/腰/臀差距都不大、肩寬x2 與 臀也接近
105
+
106
+ 回傳:
107
+ "蘋果型", "梨型", "沙漏型", "倒三角型", "H 型"
108
+ """
109
+
110
+ C = body_measurements["chest circumference"] # 胸圍(任意長度單位)
111
+ W = body_measurements["waist circumference"] # 腰圍
112
+ H = body_measurements["hip circumference"] # 臀圍
113
+ S = body_measurements["shoulder breadth"] # 肩寬(寬度,不是圍度)
114
+ shoulders2 = S * 2
115
+
116
+ if min(C, W, H, shoulders2) <= 0:
117
+ raise ValueError("胸/腰/臀/肩寬 必須為正數")
118
+
119
+ # ========= 1) 轉成比例(不吃單位) =========
120
+ total = C + W + H
121
+ pC, pW, pH = C / total, W / total, H / total
122
+ v = (pC, pW, pH)
123
+
124
+ # ========= 2) 各體型的 ideal pattern(比例) =========
125
+ ideal_patterns = {
126
+ "沙漏型": (0.35, 0.30, 0.35), # 胸 & 臀大、腰細
127
+ "蘋果型": (0.32, 0.36, 0.32), # 腰較大
128
+ "梨型": (0.32, 0.31, 0.37), # 臀較大
129
+ "倒三角型": (0.37, 0.31, 0.32), # 胸較大
130
+ "H 型": (0.34, 0.32, 0.34), # 三者接近
131
+ }
132
+
133
+ def dist(a, b):
134
+ return math.sqrt(
135
+ (a[0] - b[0]) ** 2 +
136
+ (a[1] - b[1]) ** 2 +
137
+ (a[2] - b[2]) ** 2
138
+ )
139
+
140
+ # ========= 3) 定義比例型 gate(門檻) =========
141
+ cw_ratio = (C - W) / W # 胸比腰大幾%
142
+ hw_ratio = (H - W) / W # 臀比腰大幾%
143
+ hip_shoulder_diff = (H - shoulders2) / H # 臀比肩寬x2 大幾%(<0 表肩更寬)
144
+
145
+ ch_diff_ratio = abs(C - H) / max(C, H) # 胸���接近程度
146
+ w_vs_ch_hip = (W - min(C, H)) / min(C, H) # 腰比胸/臀大多少(判斷蘋果用)
147
+
148
+ # 沙漏型 gate:胸 & 臀 都比腰大,且胸臀接近
149
+ def gate_hourglass(C, W, H, shoulders2):
150
+ return (cw_ratio >= 0.25 and hw_ratio >= 0.30 and ch_diff_ratio <= 0.07)
151
+
152
+ # 蘋果型 gate:腰明顯 > 臀(通常也 ≥ 胸)
153
+ def gate_apple(C, W, H, shoulders2):
154
+ return (W >= max(C, H)) and ((W - H) / H > 0.03)
155
+
156
+ # 梨型 gate:臀明顯 > 肩寬x2(原本「> 3cm」,改成約 3%)
157
+ def gate_pear(C, W, H, shoulders2):
158
+ return hip_shoulder_diff > 0.03
159
+
160
+ # 倒三角 gate:肩寬x2 明顯 > 臀
161
+ def gate_inv_tri(C, W, H, shoulders2):
162
+ return hip_shoulder_diff < -0.03
163
+
164
+ # H 型 gate:肩寬x2 與 臀差距小,胸臀也跟腰差距不算大
165
+ def gate_h(C, W, H, shoulders2):
166
+ return (
167
+ abs(hip_shoulder_diff) <= 0.03 and
168
+ abs(cw_ratio) <= 0.25 and
169
+ abs(hw_ratio) <= 0.25 and
170
+ w_vs_ch_hip < 0.10 # 腰不會比胸/臀大太多(避免蘋果)
171
+ )
172
+
173
+ gates = {
174
+ "沙漏型": gate_hourglass,
175
+ "蘋果型": gate_apple,
176
+ "梨型": gate_pear,
177
+ "倒三角型": gate_inv_tri,
178
+ "H 型": gate_h,
179
+ }
180
+
181
+ # ========= 4) 先找「有通過門檻」的體型,裡面選距離最近 =========
182
+ candidates = []
183
+ for name in ideal_patterns.keys():
184
+ if gates[name](C, W, H, shoulders2):
185
+ d = dist(v, ideal_patterns[name])
186
+ candidates.append((d, name))
187
+
188
+ if candidates:
189
+ candidates.sort()
190
+ return candidates[0][1]
191
+
192
+ # ========= 5) 若完全沒有門檻通過,就純看 ideal pattern 距離 =========
193
+ best_name = None
194
+ best_d = float("inf")
195
+ for name, ideal_v in ideal_patterns.items():
196
+ d = dist(v, ideal_v)
197
+ if d < best_d:
198
+ best_d = d
199
+ best_name = name
200
+
201
+ return best_name
202
+
203
+
204
+ # ===========================
205
+ # 測試範例
206
+ # ===========================
207
+ if __name__ == "__main__":
208
+ # === 男性測試資料(用你之前那組) ===
209
+ male_body_measurements = {
210
+ "height": 196.27,
211
+ "shoulder to crotch height": 77.3,
212
+ "arm left length": 55.35,
213
+ "arm right length": 58.13,
214
+ "inside leg height": 81.09,
215
+ "shoulder breadth": 62.82,
216
+ "arm length (shoulder to elbow)": 36.15,
217
+ "crotch height": 83.7,
218
+ "Hip circumference max height": 94.81,
219
+ "arm length (spine to wrist)": 32.54,
220
+ "head circumference": 60.98,
221
+ "neck circumference": 33.72,
222
+ "chest circumference": 150.78,
223
+ "waist circumference": 154.85,
224
+ "hip circumference": 158.92,
225
+ "wrist right circumference": 25.12,
226
+ "bicep right circumference": 39.0,
227
+ "forearm right circumference": 35.64,
228
+ "thigh left circumference": 72.86,
229
+ "calf left circumference": 49.86,
230
+ "ankle left circumference": 29.34
231
+ }
232
+
233
+ male_type = classify_male_body_type(male_body_measurements)
234
+ print("男性體型判斷結果:", male_type)
235
+
236
+ # === 女性測試資料(先給一組假資料,你之後可以換成實際量測) ===
237
+ female_body_measurements = {
238
+ "height": 165.0,
239
+ "shoulder to crotch height": 60.0,
240
+ "arm left length": 50.0,
241
+ "arm right length": 50.0,
242
+ "inside leg height": 75.0,
243
+ "shoulder breadth": 40.0, # 肩寬 40 → 肩寬x2 = 80
244
+ "arm length (shoulder to elbow)": 30.0,
245
+ "crotch height": 78.0,
246
+ "Hip circumference max height": 95.0,
247
+ "arm length (spine to wrist)": 30.0,
248
+ "head circumference": 55.0,
249
+ "neck circumference": 33.0,
250
+ "chest circumference": 90.0, # 胸 90
251
+ "waist circumference": 70.0, # 腰 70
252
+ "hip circumference": 95.0, # 臀 95
253
+ "wrist right circumference": 16.0,
254
+ "bicep right circumference": 28.0,
255
+ "forearm right circumference": 24.0,
256
+ "thigh left circumference": 55.0,
257
+ "calf left circumference": 36.0,
258
+ "ankle left circumference": 22.0
259
+ }
260
+
261
+ female_type = classify_female_body_type(female_body_measurements)
262
+ print("女性體型判斷結果:", female_type)
colors.json ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "colors": {
3
+ "中藍": { "en": "medium blue", "hex": "#3366AA", "tags": ["cool", "medium", "casual", "denim-friendly"] },
4
+ "咖啡色": { "en": "coffee brown", "hex": "#5A3E2B", "tags": ["warm", "dark", "earth", "casual"] },
5
+ "深藍": { "en": "navy blue", "hex": "#1E2A78", "tags": ["cool", "dark", "formal"] },
6
+ "深紫": { "en": "deep purple", "hex": "#4B0082", "tags": ["cool", "dark", "formal", "accent"] },
7
+ "黑": { "en": "black", "hex": "#000000", "tags": ["neutral", "dark", "formal", "basic"] },
8
+ "白": { "en": "white", "hex": "#FFFFFF", "tags": ["neutral", "light", "formal", "basic"] },
9
+ "米白": { "en": "off white", "hex": "#F5F1E3", "tags": ["neutral", "light", "soft", "basic"] },
10
+ "灰": { "en": "gray", "hex": "#808080", "tags": ["neutral", "medium", "formal", "basic"] },
11
+ "灰色": { "en": "gray", "hex": "#808080", "tags": ["neutral", "medium", "formal", "basic"] },
12
+
13
+ "淺藍": { "en": "light blue", "hex": "#9EC9FF", "tags": ["cool", "light", "soft", "casual"] },
14
+ "淺紫": { "en": "light purple", "hex": "#C3A6FF", "tags": ["cool", "light", "soft", "cute"] },
15
+ "深棕色": { "en": "dark brown", "hex": "#4B2E19", "tags": ["warm", "dark", "earth", "formal"] },
16
+ "藍色": { "en": "blue", "hex": "#1F75FE", "tags": ["cool", "medium", "casual", "denim-friendly"] },
17
+ "粉": { "en": "pink", "hex": "#FFC0CB", "tags": ["warm", "light", "cute", "accent"] },
18
+ "桃紅": { "en": "peach pink", "hex": "#FF728B", "tags": ["warm", "medium", "bright", "accent"] },
19
+ "藍牛仔": { "en": "denim blue", "hex": "#3C5A92", "tags": ["cool", "medium", "denim", "casual"] },
20
+ "深藍牛仔": { "en": "dark denim", "hex": "#2C3E50", "tags": ["cool", "dark", "denim", "casual"] },
21
+
22
+ "卡其": { "en": "khaki", "hex": "#C3B091", "tags": ["neutral", "light", "earth", "casual"] },
23
+ "橄欖綠": { "en": "olive green", "hex": "#708238", "tags": ["warm", "medium", "earth", "casual"] },
24
+ "酒紅": { "en": "burgundy", "hex": "#800020", "tags": ["warm", "dark", "formal", "accent"] },
25
+ "駝色": { "en": "camel", "hex": "#C19A6B", "tags": ["warm", "medium", "earth", "casual"] },
26
+ "深綠": { "en": "dark green", "hex": "#00563F", "tags": ["cool", "dark", "formal", "earth"] },
27
+ "綠": { "en": "green", "hex": "#008000", "tags": ["cool", "medium", "casual"] },
28
+
29
+ "深灰": { "en": "dark gray", "hex": "#555555", "tags": ["neutral", "dark", "formal", "basic"] },
30
+ "淺灰": { "en": "light gray", "hex": "#D3D3D3", "tags": ["neutral", "light", "basic"] },
31
+ "銀": { "en": "silver", "hex": "#C0C0C0", "tags": ["neutral", "light", "metallic", "accent"] },
32
+ "金": { "en": "gold", "hex": "#FFD700", "tags": ["warm", "light", "metallic", "accent"] },
33
+ "橘": { "en": "orange", "hex": "#FFA500", "tags": ["warm", "bright", "accent"] },
34
+ "紅": { "en": "red", "hex": "#FF0000", "tags": ["warm", "bright", "accent"] },
35
+ "黃": { "en": "yellow", "hex": "#FFFF00", "tags": ["warm", "bright", "accent"] },
36
+ "米色": { "en": "beige", "hex": "#F5F5DC", "tags": ["warm", "light", "earth", "basic"] }
37
+ },
38
+
39
+ "skin_tone_to_palette": {
40
+ "male_Type I": "male_cool_fair",
41
+ "male_Type II": "male_cool_fair",
42
+ "male_Type III": "male_cool_fair",
43
+ "male_Type IV": "male_neutral_yellow",
44
+ "male_Type V": "male_neutral_yellow",
45
+ "male_Type VI": "male_neutral_yellow",
46
+ "male_Type VII": "male_warm_wheat",
47
+ "male_Type VIII": "male_warm_wheat",
48
+ "male_Type IX": "male_warm_wheat",
49
+ "male_Type X": "male_warm_wheat",
50
+ "male_Type XI": "male_warm_wheat",
51
+
52
+ "female_Type I": "female_cool_fair",
53
+ "female_Type II": "female_cool_fair",
54
+ "female_Type III": "female_cool_fair",
55
+ "female_Type IV": "female_warm",
56
+ "female_Type V": "female_warm",
57
+ "female_Type VI": "female_warm",
58
+ "female_Type VII": "female_healthy_wheat",
59
+ "female_Type VIII": "female_healthy_wheat",
60
+ "female_Type IX": "female_healthy_wheat",
61
+ "female_Type X": "female_healthy_wheat",
62
+ "female_Type XI": "female_healthy_wheat"
63
+ },
64
+
65
+
66
+ "palettes": {
67
+ "male_cool_fair": {
68
+ "note": "男-冷色調白皙膚色 (禁忌白色調)。大地色系避免作為主色。",
69
+ "combos": [
70
+ { "top": "粉", "bottoms": ["藍牛仔", "卡其", "駝色", "咖啡色", "深棕色", "灰", "黑"] },
71
+ { "top": "淺藍", "bottoms": ["深藍", "咖啡色", "粉", "黑", "灰"] },
72
+ { "top": "深藍", "bottoms": ["淺藍", "咖啡色", "黑", "灰"] },
73
+ { "top": "深灰", "bottoms": ["黑", "白", "灰"] }
74
+ ]
75
+ },
76
+
77
+ "male_neutral_yellow": {
78
+ "note": "男-中性調偏黃膚色 (禁忌大地色系)。",
79
+ "combos": [
80
+ { "top": "黑", "bottoms": ["白"] },
81
+ { "top": "白", "bottoms": ["黑"] },
82
+ { "top": "深綠", "bottoms": ["咖啡色", "黑", "灰", "米白", "白"] },
83
+ { "top": "深藍", "bottoms": ["淺藍", "咖啡色", "黑", "灰", "米白", "白"] },
84
+ { "top": "酒紅", "bottoms": ["粉", "黑", "灰", "米白", "白"] }
85
+ ]
86
+ },
87
+
88
+ "male_warm_wheat": {
89
+ "note": "男-暖色調小麥膚色 (禁忌裸色、大地色、螢光色)。",
90
+ "combos": [
91
+ { "top": "黑", "bottoms": ["白"] },
92
+ { "top": "白", "bottoms": ["黑"] },
93
+ { "top": "淺藍", "bottoms": ["深藍", "咖啡色", "粉", "黑", "灰", "米白", "白"] },
94
+ { "top": "淺灰", "bottoms": ["藍牛仔", "深藍牛仔", "黑", "白", "米白", "灰"] }
95
+ ]
96
+ },
97
+
98
+ "female_cool_fair": {
99
+ "note": "女-白皙冷調膚色 (避免大地色系、黃橘色系)。",
100
+ "combos": [
101
+ { "top": "中藍", "bottoms": ["咖啡色", "深藍", "黑", "白", "米白", "灰"] },
102
+ { "top": "深藍", "bottoms": ["淺藍", "咖啡色", "黑", "白", "米白", "灰"] },
103
+ { "top": "淺紫", "bottoms": ["深棕色", "藍色", "黑", "白", "米白", "灰"] },
104
+ { "top": "深紫", "bottoms": ["黑"] },
105
+ { "top": "粉", "bottoms": ["藍牛仔", "灰", "黑", "白", "米白"] },
106
+ { "top": "桃紅", "bottoms": ["淺藍", "黑", "白", "米白", "灰"] },
107
+ { "top": "灰色", "bottoms": ["黑", "白", "灰"] },
108
+ { "top": "銀", "bottoms": ["黑", "白"] }
109
+ ]
110
+ },
111
+
112
+ "female_warm": {
113
+ "note": "女-暖調膚色 (避免全黑/全白,建議搭配)。",
114
+ "combos": [
115
+ { "top": "橘", "bottoms": ["藍色", "綠", "米白", "灰"] },
116
+ { "top": "紅", "bottoms": ["咖啡色", "藍色", "黃", "米白", "灰"] },
117
+ { "top": "酒紅", "bottoms": ["粉", "米白", "灰"] },
118
+ { "top": "金", "bottoms": ["深藍", "灰"] },
119
+ { "top": "深藍", "bottoms": ["淺藍", "咖啡色", "米白", "灰"] },
120
+ { "top": "綠", "bottoms": ["粉", "藍色", "卡其", "駝色", "咖啡色", "深棕色", "米白", "白"] },
121
+ { "top": "深綠", "bottoms": ["咖啡色", "米白", "白"] }
122
+ ]
123
+ },
124
+
125
+ "female_healthy_wheat": {
126
+ "note": "女-健康小麥肌膚顏色 (盡量選具反光效果的顏色)。",
127
+ "combos": [
128
+ { "top": "紅", "bottoms": ["咖啡色", "藍色", "黃", "黑", "灰", "米白", "白"] },
129
+ { "top": "酒紅", "bottoms": ["粉", "黑", "灰", "米白", "白"] },
130
+ { "top": "綠", "bottoms": ["粉", "藍色", "卡其", "駝色", "咖啡色", "深棕色", "黑", "灰", "米白", "白"] },
131
+ { "top": "深綠", "bottoms": ["咖啡色", "黑", "灰", "米白", "白"] },
132
+ { "top": "駝色", "bottoms": ["咖啡色", "米色"] },
133
+ { "top": "白", "bottoms": ["黑", "深藍牛仔"] },
134
+ { "top": "淺灰", "bottoms": ["黑", "白"] }
135
+ ]
136
+ }
137
+ }
138
+ }
outfits.json ADDED
@@ -0,0 +1,1990 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "outfits": [
3
+ {
4
+ "comment": "=== 男:矩形 (Rectangle/H) ===",
5
+ "id": "M_RECT_01_SUITPANTS",
6
+ "gender": "male",
7
+ "body_types": ["矩形"],
8
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
9
+ "min_temp": 15,
10
+ "max_temp": 30,
11
+ "style_tags": ["casual", "smart-casual"],
12
+ "top_color_tags": [],
13
+ "bottom_color_tags": [],
14
+ "allowed_top_colors": ["白", "灰", "深灰", "淺藍", "中藍", "深藍", "粉", "米色", "卡其", "駝色"],
15
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色", "卡其"],
16
+ "desc_zh": "T恤 + 開襟衫 + 西裝褲 (避免低腰)",
17
+ "prompt_items_en": {
18
+ "top": [
19
+ "{top_color_en} plain tshirt",
20
+ "{top_color_en} crewneck tshirt",
21
+ "{top_color_en} cotton tshirt"
22
+ ],
23
+ "layer": [
24
+ "{top_color_en} knit cardigan",
25
+ "{top_color_en} open cardigan",
26
+ "{top_color_en} relaxed cardigan"
27
+ ],
28
+ "bottom": [
29
+ "{bottom_color_en} highrise suitpants",
30
+ "{bottom_color_en} straight suitpants",
31
+ "{bottom_color_en} pleated suitpants"
32
+ ]
33
+ }
34
+ },
35
+ {
36
+ "id": "M_RECT_01_JEANS",
37
+ "gender": "male",
38
+ "body_types": ["矩形"],
39
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
40
+ "min_temp": 15,
41
+ "max_temp": 30,
42
+ "style_tags": ["casual", "smart-casual"],
43
+ "top_color_tags": [],
44
+ "bottom_color_tags": [],
45
+ "allowed_top_colors": ["白", "灰", "深灰", "淺藍", "中藍", "深藍", "粉", "米色", "卡其", "駝色"],
46
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
47
+ "desc_zh": "T恤 + 開襟衫 + 牛仔褲 (避免低腰)",
48
+ "prompt_items_en": {
49
+ "top": [
50
+ "{top_color_en} plain tshirt",
51
+ "{top_color_en} crewneck tshirt",
52
+ "{top_color_en} cotton tshirt"
53
+ ],
54
+ "layer": [
55
+ "{top_color_en} knit cardigan",
56
+ "{top_color_en} open cardigan",
57
+ "{top_color_en} relaxed cardigan"
58
+ ],
59
+ "bottom": [
60
+ "{bottom_color_en} highrise jeans",
61
+ "{bottom_color_en} straight jeans",
62
+ "{bottom_color_en} tapered jeans"
63
+ ]
64
+ }
65
+ },
66
+
67
+ {
68
+ "id": "M_RECT_02_SUITPANTS",
69
+ "gender": "male",
70
+ "body_types": ["矩形"],
71
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
72
+ "min_temp": 15,
73
+ "max_temp": 30,
74
+ "style_tags": ["casual", "smart-casual"],
75
+ "top_color_tags": [],
76
+ "bottom_color_tags": [],
77
+ "allowed_top_colors": ["白", "灰", "深灰", "淺藍", "中藍", "深藍", "粉", "米色", "卡其", "駝色"],
78
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色", "卡其"],
79
+ "desc_zh": "襯衫 + 西裝褲",
80
+ "prompt_items_en": {
81
+ "top": [
82
+ "{top_color_en} buttondown shirt",
83
+ "{top_color_en} oxford shirt",
84
+ "{top_color_en} collared shirt"
85
+ ],
86
+ "bottom": [
87
+ "{bottom_color_en} highrise suitpants",
88
+ "{bottom_color_en} straight suitpants",
89
+ "{bottom_color_en} pleated suitpants"
90
+ ]
91
+ }
92
+ },
93
+ {
94
+ "id": "M_RECT_02_JEANS",
95
+ "gender": "male",
96
+ "body_types": ["矩形"],
97
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
98
+ "min_temp": 15,
99
+ "max_temp": 30,
100
+ "style_tags": ["casual", "smart-casual"],
101
+ "top_color_tags": [],
102
+ "bottom_color_tags": [],
103
+ "allowed_top_colors": ["白", "灰", "深灰", "淺藍", "中藍", "深藍", "粉", "米色", "卡其", "駝色"],
104
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
105
+ "desc_zh": "襯衫 + 牛仔褲",
106
+ "prompt_items_en": {
107
+ "top": [
108
+ "{top_color_en} buttondown shirt",
109
+ "{top_color_en} oxford shirt",
110
+ "{top_color_en} collared shirt"
111
+ ],
112
+ "bottom": [
113
+ "{bottom_color_en} highrise jeans",
114
+ "{bottom_color_en} straight jeans",
115
+ "{bottom_color_en} tapered jeans"
116
+ ]
117
+ }
118
+ },
119
+
120
+ {
121
+ "id": "M_RECT_03_SUITPANTS",
122
+ "gender": "male",
123
+ "body_types": ["矩形"],
124
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
125
+ "min_temp": 10,
126
+ "max_temp": 25,
127
+ "style_tags": ["formal", "smart-casual"],
128
+ "top_color_tags": [],
129
+ "bottom_color_tags": [],
130
+ "allowed_top_colors": ["黑", "白", "灰", "深灰", "深藍"],
131
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍"],
132
+ "desc_zh": "單排扣西裝 + 西裝褲",
133
+ "prompt_items_en": {
134
+ "top": [
135
+ "{top_color_en} singlebreasted suitjacket",
136
+ "{top_color_en} tailored blazer",
137
+ "{top_color_en} structured blazer"
138
+ ],
139
+ "bottom": [
140
+ "{bottom_color_en} highrise suitpants",
141
+ "{bottom_color_en} straight suitpants",
142
+ "{bottom_color_en} pleated suitpants"
143
+ ]
144
+ }
145
+ },
146
+ {
147
+ "id": "M_RECT_03_JEANS",
148
+ "gender": "male",
149
+ "body_types": ["矩形"],
150
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
151
+ "min_temp": 10,
152
+ "max_temp": 25,
153
+ "style_tags": ["formal", "smart-casual"],
154
+ "top_color_tags": [],
155
+ "bottom_color_tags": [],
156
+ "allowed_top_colors": ["黑", "白", "灰", "深灰", "深藍"],
157
+ "allowed_bottom_colors": ["深藍", "灰", "黑"],
158
+ "desc_zh": "單排扣西裝 + 牛仔褲",
159
+ "prompt_items_en": {
160
+ "top": [
161
+ "{top_color_en} singlebreasted suitjacket",
162
+ "{top_color_en} tailored blazer",
163
+ "{top_color_en} structured blazer"
164
+ ],
165
+ "bottom": [
166
+ "{bottom_color_en} highrise jeans",
167
+ "{bottom_color_en} straight jeans",
168
+ "{bottom_color_en} tapered jeans"
169
+ ]
170
+ }
171
+ },
172
+
173
+ {
174
+ "id": "M_RECT_04_SUITPANTS",
175
+ "gender": "male",
176
+ "body_types": ["矩形"],
177
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
178
+ "min_temp": 10,
179
+ "max_temp": 20,
180
+ "style_tags": ["casual", "winter"],
181
+ "top_color_tags": [],
182
+ "bottom_color_tags": [],
183
+ "allowed_top_colors": ["黑", "灰", "深灰", "深藍", "中藍", "米色", "咖啡色", "駝色"],
184
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
185
+ "desc_zh": "橫條紋毛衣 + 圍巾 + 西裝褲",
186
+ "prompt_items_en": {
187
+ "top": [
188
+ "{top_color_en} striped sweater",
189
+ "{top_color_en} knit sweater",
190
+ "{top_color_en} crewneck sweater"
191
+ ],
192
+ "accessory": [
193
+ "{top_color_en} wool scarf",
194
+ "{top_color_en} warm scarf",
195
+ "{top_color_en} knit scarf"
196
+ ],
197
+ "bottom": [
198
+ "{bottom_color_en} highrise suitpants",
199
+ "{bottom_color_en} straight suitpants",
200
+ "{bottom_color_en} pleated suitpants"
201
+ ]
202
+ }
203
+ },
204
+ {
205
+ "id": "M_RECT_04_JEANS",
206
+ "gender": "male",
207
+ "body_types": ["矩形"],
208
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
209
+ "min_temp": 10,
210
+ "max_temp": 20,
211
+ "style_tags": ["casual", "winter"],
212
+ "top_color_tags": [],
213
+ "bottom_color_tags": [],
214
+ "allowed_top_colors": ["黑", "灰", "深灰", "深藍", "中藍", "米色", "咖啡色", "駝色"],
215
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
216
+ "desc_zh": "橫條紋毛衣 + 圍巾 + 牛仔褲",
217
+ "prompt_items_en": {
218
+ "top": [
219
+ "{top_color_en} striped sweater",
220
+ "{top_color_en} knit sweater",
221
+ "{top_color_en} crewneck sweater"
222
+ ],
223
+ "accessory": [
224
+ "{top_color_en} wool scarf",
225
+ "{top_color_en} warm scarf",
226
+ "{top_color_en} knit scarf"
227
+ ],
228
+ "bottom": [
229
+ "{bottom_color_en} highrise jeans",
230
+ "{bottom_color_en} straight jeans",
231
+ "{bottom_color_en} tapered jeans"
232
+ ]
233
+ }
234
+ },
235
+
236
+ {
237
+ "comment": "=== 男:正三角 (Triangle/梨形) ===",
238
+ "id": "M_TRI_01_SWEATER_STRAIGHT",
239
+ "gender": "male",
240
+ "body_types": ["三角形"],
241
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
242
+ "min_temp": 15,
243
+ "max_temp": 28,
244
+ "style_tags": ["casual"],
245
+ "top_color_tags": ["bright", "warm", "pattern"],
246
+ "bottom_color_tags": ["dark", "neutral"],
247
+ "allowed_top_colors": ["粉", "淺藍", "中藍", "米色", "駝色", "卡其"],
248
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
249
+ "desc_zh": "圖案鮮豔毛衣 + 直筒褲 (避免窄褲)",
250
+ "prompt_items_en": {
251
+ "top": [
252
+ "{top_color_en} patterned sweater",
253
+ "{top_color_en} colorful sweater",
254
+ "{top_color_en} bold sweater"
255
+ ],
256
+ "bottom": [
257
+ "{bottom_color_en} straightleg pants",
258
+ "{bottom_color_en} relaxed trousers",
259
+ "{bottom_color_en} straight trousers"
260
+ ]
261
+ }
262
+ },
263
+ {
264
+ "id": "M_TRI_01_SWEATER_WIDE",
265
+ "gender": "male",
266
+ "body_types": ["三角形"],
267
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
268
+ "min_temp": 15,
269
+ "max_temp": 28,
270
+ "style_tags": ["casual"],
271
+ "top_color_tags": ["bright", "warm", "pattern"],
272
+ "bottom_color_tags": ["dark", "neutral"],
273
+ "allowed_top_colors": ["粉", "淺藍", "中藍", "米色", "駝色", "卡其"],
274
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
275
+ "desc_zh": "圖案鮮豔毛衣 + 闊腿褲 (避免窄褲)",
276
+ "prompt_items_en": {
277
+ "top": [
278
+ "{top_color_en} patterned sweater",
279
+ "{top_color_en} colorful sweater",
280
+ "{top_color_en} bold sweater"
281
+ ],
282
+ "bottom": [
283
+ "{bottom_color_en} wideleg trousers",
284
+ "{bottom_color_en} loose trousers",
285
+ "{bottom_color_en} wideleg pants"
286
+ ]
287
+ }
288
+ },
289
+ {
290
+ "id": "M_TRI_01_TSHIRT_STRAIGHT",
291
+ "gender": "male",
292
+ "body_types": ["三角形"],
293
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
294
+ "min_temp": 15,
295
+ "max_temp": 28,
296
+ "style_tags": ["casual"],
297
+ "top_color_tags": ["bright", "warm", "pattern"],
298
+ "bottom_color_tags": ["dark", "neutral"],
299
+ "allowed_top_colors": ["粉", "淺藍", "中藍", "米色", "駝色", "卡其"],
300
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
301
+ "desc_zh": "圖案鮮豔T恤 + 直筒褲 (避免窄褲)",
302
+ "prompt_items_en": {
303
+ "top": [
304
+ "{top_color_en} graphic tshirt",
305
+ "{top_color_en} printed tshirt",
306
+ "{top_color_en} vibrant tshirt"
307
+ ],
308
+ "bottom": [
309
+ "{bottom_color_en} straightleg pants",
310
+ "{bottom_color_en} relaxed trousers",
311
+ "{bottom_color_en} straight trousers"
312
+ ]
313
+ }
314
+ },
315
+ {
316
+ "id": "M_TRI_01_TSHIRT_WIDE",
317
+ "gender": "male",
318
+ "body_types": ["三角形"],
319
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
320
+ "min_temp": 15,
321
+ "max_temp": 28,
322
+ "style_tags": ["casual"],
323
+ "top_color_tags": ["bright", "warm", "pattern"],
324
+ "bottom_color_tags": ["dark", "neutral"],
325
+ "allowed_top_colors": ["粉", "淺藍", "中藍", "米色", "駝色", "卡其"],
326
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
327
+ "desc_zh": "圖案鮮豔T恤 + 闊腿褲 (避免窄褲)",
328
+ "prompt_items_en": {
329
+ "top": [
330
+ "{top_color_en} graphic tshirt",
331
+ "{top_color_en} printed tshirt",
332
+ "{top_color_en} vibrant tshirt"
333
+ ],
334
+ "bottom": [
335
+ "{bottom_color_en} wideleg trousers",
336
+ "{bottom_color_en} loose trousers",
337
+ "{bottom_color_en} wideleg pants"
338
+ ]
339
+ }
340
+ },
341
+
342
+ {
343
+ "id": "M_TRI_02",
344
+ "gender": "male",
345
+ "body_types": ["三角形"],
346
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
347
+ "min_temp": 10,
348
+ "max_temp": 20,
349
+ "style_tags": ["smart-casual", "winter"],
350
+ "top_color_tags": [],
351
+ "bottom_color_tags": ["dark", "neutral"],
352
+ "allowed_top_colors": ["黑", "灰", "深灰", "深藍", "咖啡色", "米色"],
353
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
354
+ "desc_zh": "長版大衣 + 一般T恤 + 修身直筒褲",
355
+ "prompt_items_en": {
356
+ "outer": [
357
+ "{top_color_en} long overcoat",
358
+ "{top_color_en} long trenchcoat",
359
+ "{top_color_en} long coat"
360
+ ],
361
+ "top": [
362
+ "{top_color_en} plain tshirt",
363
+ "{top_color_en} crewneck tshirt",
364
+ "{top_color_en} basic tshirt"
365
+ ],
366
+ "bottom": [
367
+ "{bottom_color_en} slimstraight trousers",
368
+ "{bottom_color_en} straightleg pants",
369
+ "{bottom_color_en} tailored trousers"
370
+ ]
371
+ }
372
+ },
373
+
374
+ {
375
+ "id": "M_TRI_03_TSHIRT",
376
+ "gender": "male",
377
+ "body_types": ["三角形"],
378
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
379
+ "min_temp": 15,
380
+ "max_temp": 25,
381
+ "style_tags": ["smart-casual"],
382
+ "top_color_tags": [],
383
+ "bottom_color_tags": [],
384
+ "allowed_top_colors": ["白", "灰", "深藍", "淺藍", "粉", "黑"],
385
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍"],
386
+ "desc_zh": "西裝外套 + T恤 + 西裝褲",
387
+ "prompt_items_en": {
388
+ "outer": [
389
+ "{top_color_en} tailored blazer",
390
+ "{top_color_en} structured blazer",
391
+ "{top_color_en} classic blazer"
392
+ ],
393
+ "top": [
394
+ "{top_color_en} plain tshirt",
395
+ "{top_color_en} crewneck tshirt",
396
+ "{top_color_en} cotton tshirt"
397
+ ],
398
+ "bottom": [
399
+ "{bottom_color_en} straight suitpants",
400
+ "{bottom_color_en} tailored suitpants",
401
+ "{bottom_color_en} pleated suitpants"
402
+ ]
403
+ }
404
+ },
405
+ {
406
+ "id": "M_TRI_03_SHIRT",
407
+ "gender": "male",
408
+ "body_types": ["三角形"],
409
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
410
+ "min_temp": 15,
411
+ "max_temp": 25,
412
+ "style_tags": ["smart-casual"],
413
+ "top_color_tags": [],
414
+ "bottom_color_tags": [],
415
+ "allowed_top_colors": ["白", "灰", "深藍", "淺藍", "粉", "黑"],
416
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍"],
417
+ "desc_zh": "西裝外套 + 襯衫 + 西裝褲",
418
+ "prompt_items_en": {
419
+ "outer": [
420
+ "{top_color_en} tailored blazer",
421
+ "{top_color_en} structured blazer",
422
+ "{top_color_en} classic blazer"
423
+ ],
424
+ "top": [
425
+ "{top_color_en} buttondown shirt",
426
+ "{top_color_en} oxford shirt",
427
+ "{top_color_en} collared shirt"
428
+ ],
429
+ "bottom": [
430
+ "{bottom_color_en} straight suitpants",
431
+ "{bottom_color_en} tailored suitpants",
432
+ "{bottom_color_en} pleated suitpants"
433
+ ]
434
+ }
435
+ },
436
+
437
+ {
438
+ "id": "M_TRI_04_SUITPANTS",
439
+ "gender": "male",
440
+ "body_types": ["三角形"],
441
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
442
+ "min_temp": 15,
443
+ "max_temp": 25,
444
+ "style_tags": ["casual"],
445
+ "top_color_tags": [],
446
+ "bottom_color_tags": [],
447
+ "allowed_top_colors": ["黑", "白", "灰", "深藍", "中藍", "卡其", "咖啡色"],
448
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍"],
449
+ "desc_zh": "夾克 + 一般T恤 + 西裝褲",
450
+ "prompt_items_en": {
451
+ "outer": [
452
+ "{top_color_en} casual jacket",
453
+ "{top_color_en} bomber jacket",
454
+ "{top_color_en} zipup jacket"
455
+ ],
456
+ "top": [
457
+ "{top_color_en} plain tshirt",
458
+ "{top_color_en} crewneck tshirt",
459
+ "{top_color_en} cotton tshirt"
460
+ ],
461
+ "bottom": [
462
+ "{bottom_color_en} straight suitpants",
463
+ "{bottom_color_en} tailored suitpants",
464
+ "{bottom_color_en} pleated suitpants"
465
+ ]
466
+ }
467
+ },
468
+ {
469
+ "id": "M_TRI_04_JEANS",
470
+ "gender": "male",
471
+ "body_types": ["三角形"],
472
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
473
+ "min_temp": 15,
474
+ "max_temp": 25,
475
+ "style_tags": ["casual"],
476
+ "top_color_tags": [],
477
+ "bottom_color_tags": [],
478
+ "allowed_top_colors": ["黑", "白", "灰", "深藍", "中藍", "卡其", "咖啡色"],
479
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
480
+ "desc_zh": "夾克 + 一般T恤 + 牛仔褲",
481
+ "prompt_items_en": {
482
+ "outer": [
483
+ "{top_color_en} casual jacket",
484
+ "{top_color_en} bomber jacket",
485
+ "{top_color_en} zipup jacket"
486
+ ],
487
+ "top": [
488
+ "{top_color_en} plain tshirt",
489
+ "{top_color_en} crewneck tshirt",
490
+ "{top_color_en} cotton tshirt"
491
+ ],
492
+ "bottom": [
493
+ "{bottom_color_en} straight jeans",
494
+ "{bottom_color_en} regular jeans",
495
+ "{bottom_color_en} tapered jeans"
496
+ ]
497
+ }
498
+ },
499
+
500
+ {
501
+ "comment": "=== 男:梯形 (Trapezoid/運動均衡) ===",
502
+ "id": "M_TRAP_01",
503
+ "gender": "male",
504
+ "body_types": ["梯形"],
505
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
506
+ "min_temp": 15,
507
+ "max_temp": 25,
508
+ "style_tags": ["casual", "street"],
509
+ "top_color_tags": [],
510
+ "bottom_color_tags": [],
511
+ "allowed_top_colors": ["黑", "深藍", "灰", "咖啡色", "卡其"],
512
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
513
+ "desc_zh": "白色T恤 + 飛行外套 + 合身牛仔褲",
514
+ "prompt_items_en": {
515
+ "top": [
516
+ "white plain tshirt",
517
+ "white crewneck tshirt",
518
+ "white cotton tshirt"
519
+ ],
520
+ "outer": [
521
+ "{top_color_en} bomber jacket",
522
+ "{top_color_en} nylon bomber",
523
+ "{top_color_en} padded bomber"
524
+ ],
525
+ "bottom": [
526
+ "{bottom_color_en} slim jeans",
527
+ "{bottom_color_en} fitted jeans",
528
+ "{bottom_color_en} tapered jeans"
529
+ ]
530
+ }
531
+ },
532
+
533
+ {
534
+ "id": "M_TRAP_02_JEANS",
535
+ "gender": "male",
536
+ "body_types": ["梯形"],
537
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
538
+ "min_temp": 18,
539
+ "max_temp": 30,
540
+ "style_tags": ["smart-casual"],
541
+ "top_color_tags": [],
542
+ "bottom_color_tags": [],
543
+ "allowed_top_colors": ["白", "淺藍", "深藍", "灰", "粉"],
544
+ "allowed_bottom_colors": ["深藍", "中��", "灰", "黑"],
545
+ "desc_zh": "修身襯衫 + 合身牛仔褲",
546
+ "prompt_items_en": {
547
+ "top": [
548
+ "{top_color_en} slimfit shirt",
549
+ "{top_color_en} fitted shirt",
550
+ "{top_color_en} buttondown shirt"
551
+ ],
552
+ "bottom": [
553
+ "{bottom_color_en} slim jeans",
554
+ "{bottom_color_en} fitted jeans",
555
+ "{bottom_color_en} tapered jeans"
556
+ ]
557
+ }
558
+ },
559
+ {
560
+ "id": "M_TRAP_02_SUITPANTS",
561
+ "gender": "male",
562
+ "body_types": ["梯形"],
563
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
564
+ "min_temp": 18,
565
+ "max_temp": 30,
566
+ "style_tags": ["smart-casual"],
567
+ "top_color_tags": [],
568
+ "bottom_color_tags": [],
569
+ "allowed_top_colors": ["白", "淺藍", "深藍", "灰", "粉"],
570
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其"],
571
+ "desc_zh": "修身襯衫 + 合身西裝褲",
572
+ "prompt_items_en": {
573
+ "top": [
574
+ "{top_color_en} slimfit shirt",
575
+ "{top_color_en} fitted shirt",
576
+ "{top_color_en} buttondown shirt"
577
+ ],
578
+ "bottom": [
579
+ "{bottom_color_en} fitted suitpants",
580
+ "{bottom_color_en} slim suitpants",
581
+ "{bottom_color_en} straight suitpants"
582
+ ]
583
+ }
584
+ },
585
+
586
+ {
587
+ "id": "M_TRAP_03_TSHIRT_CASUALPANTS",
588
+ "gender": "male",
589
+ "body_types": ["梯形"],
590
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
591
+ "min_temp": 15,
592
+ "max_temp": 30,
593
+ "style_tags": ["casual", "sporty"],
594
+ "top_color_tags": [],
595
+ "bottom_color_tags": [],
596
+ "allowed_top_colors": ["白", "灰", "黑", "中藍", "深藍", "米色", "粉"],
597
+ "allowed_bottom_colors": ["灰", "黑", "卡其", "米色", "深藍"],
598
+ "desc_zh": "棉T + 休閒褲",
599
+ "prompt_items_en": {
600
+ "top": [
601
+ "{top_color_en} cotton tshirt",
602
+ "{top_color_en} plain tshirt",
603
+ "{top_color_en} jersey tshirt"
604
+ ],
605
+ "bottom": [
606
+ "{bottom_color_en} casual pants",
607
+ "{bottom_color_en} chino pants",
608
+ "{bottom_color_en} relaxed trousers"
609
+ ]
610
+ }
611
+ },
612
+ {
613
+ "id": "M_TRAP_03_TSHIRT_SWEATPANTS",
614
+ "gender": "male",
615
+ "body_types": ["梯形"],
616
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
617
+ "min_temp": 15,
618
+ "max_temp": 30,
619
+ "style_tags": ["casual", "sporty"],
620
+ "top_color_tags": [],
621
+ "bottom_color_tags": [],
622
+ "allowed_top_colors": ["白", "灰", "黑", "中藍", "深藍", "米色", "粉"],
623
+ "allowed_bottom_colors": ["灰", "深灰", "黑", "深藍"],
624
+ "desc_zh": "棉T + 棉褲",
625
+ "prompt_items_en": {
626
+ "top": [
627
+ "{top_color_en} cotton tshirt",
628
+ "{top_color_en} plain tshirt",
629
+ "{top_color_en} jersey tshirt"
630
+ ],
631
+ "bottom": [
632
+ "{bottom_color_en} fleece sweatpants",
633
+ "{bottom_color_en} relaxed sweatpants",
634
+ "{bottom_color_en} tapered sweatpants"
635
+ ]
636
+ }
637
+ },
638
+ {
639
+ "id": "M_TRAP_03_SWEATER_CASUALPANTS",
640
+ "gender": "male",
641
+ "body_types": ["梯形"],
642
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
643
+ "min_temp": 15,
644
+ "max_temp": 30,
645
+ "style_tags": ["casual", "sporty"],
646
+ "top_color_tags": [],
647
+ "bottom_color_tags": [],
648
+ "allowed_top_colors": ["白", "灰", "黑", "中藍", "深藍", "米色", "粉"],
649
+ "allowed_bottom_colors": ["灰", "黑", "卡其", "米色", "深藍"],
650
+ "desc_zh": "針織 + 休閒褲",
651
+ "prompt_items_en": {
652
+ "top": [
653
+ "{top_color_en} knit sweater",
654
+ "{top_color_en} casual sweater",
655
+ "{top_color_en} crewneck sweater"
656
+ ],
657
+ "bottom": [
658
+ "{bottom_color_en} casual pants",
659
+ "{bottom_color_en} chino pants",
660
+ "{bottom_color_en} relaxed trousers"
661
+ ]
662
+ }
663
+ },
664
+ {
665
+ "id": "M_TRAP_03_SWEATER_SWEATPANTS",
666
+ "gender": "male",
667
+ "body_types": ["梯形"],
668
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
669
+ "min_temp": 15,
670
+ "max_temp": 30,
671
+ "style_tags": ["casual", "sporty"],
672
+ "top_color_tags": [],
673
+ "bottom_color_tags": [],
674
+ "allowed_top_colors": ["白", "灰", "黑", "中藍", "深藍", "米色", "粉"],
675
+ "allowed_bottom_colors": ["灰", "深灰", "黑", "深藍"],
676
+ "desc_zh": "針織 + 棉褲",
677
+ "prompt_items_en": {
678
+ "top": [
679
+ "{top_color_en} knit sweater",
680
+ "{top_color_en} casual sweater",
681
+ "{top_color_en} crewneck sweater"
682
+ ],
683
+ "bottom": [
684
+ "{bottom_color_en} fleece sweatpants",
685
+ "{bottom_color_en} relaxed sweatpants",
686
+ "{bottom_color_en} tapered sweatpants"
687
+ ]
688
+ }
689
+ },
690
+
691
+ {
692
+ "comment": "=== 男:倒三角 (Inverted Triangle/V) ===",
693
+ "id": "M_INV_01_VNECK_TROUSERS",
694
+ "gender": "male",
695
+ "body_types": ["倒三角"],
696
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
697
+ "min_temp": 18,
698
+ "max_temp": 35,
699
+ "style_tags": ["casual", "smart"],
700
+ "top_color_tags": [],
701
+ "bottom_color_tags": [],
702
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "深藍", "粉"],
703
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "咖啡色"],
704
+ "desc_zh": "V領T恤 + 直筒褲 (避免太緊身)",
705
+ "prompt_items_en": {
706
+ "top": [
707
+ "{top_color_en} vneck tshirt",
708
+ "{top_color_en} vneck tee",
709
+ "{top_color_en} vneck top"
710
+ ],
711
+ "bottom": [
712
+ "{bottom_color_en} straight trousers",
713
+ "{bottom_color_en} straight pants",
714
+ "{bottom_color_en} relaxed trousers"
715
+ ]
716
+ }
717
+ },
718
+ {
719
+ "id": "M_INV_01_VNECK_JEANS",
720
+ "gender": "male",
721
+ "body_types": ["倒三角"],
722
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
723
+ "min_temp": 18,
724
+ "max_temp": 35,
725
+ "style_tags": ["casual", "smart"],
726
+ "top_color_tags": [],
727
+ "bottom_color_tags": [],
728
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "深藍", "粉"],
729
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
730
+ "desc_zh": "V領T恤 + 牛仔褲 (避免太緊身)",
731
+ "prompt_items_en": {
732
+ "top": [
733
+ "{top_color_en} vneck tshirt",
734
+ "{top_color_en} vneck tee",
735
+ "{top_color_en} vneck top"
736
+ ],
737
+ "bottom": [
738
+ "{bottom_color_en} straight jeans",
739
+ "{bottom_color_en} regular jeans",
740
+ "{bottom_color_en} straightleg jeans"
741
+ ]
742
+ }
743
+ },
744
+ {
745
+ "id": "M_INV_01_SHIRT_TROUSERS",
746
+ "gender": "male",
747
+ "body_types": ["倒三角"],
748
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
749
+ "min_temp": 18,
750
+ "max_temp": 35,
751
+ "style_tags": ["casual", "smart"],
752
+ "top_color_tags": [],
753
+ "bottom_color_tags": [],
754
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "深藍", "粉"],
755
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "咖啡色"],
756
+ "desc_zh": "修身襯衫 + 直筒褲 (避免太緊身)",
757
+ "prompt_items_en": {
758
+ "top": [
759
+ "{top_color_en} slimfit shirt",
760
+ "{top_color_en} fitted shirt",
761
+ "{top_color_en} dress shirt"
762
+ ],
763
+ "bottom": [
764
+ "{bottom_color_en} straight trousers",
765
+ "{bottom_color_en} straight pants",
766
+ "{bottom_color_en} relaxed trousers"
767
+ ]
768
+ }
769
+ },
770
+ {
771
+ "id": "M_INV_01_SHIRT_JEANS",
772
+ "gender": "male",
773
+ "body_types": ["倒三角"],
774
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
775
+ "min_temp": 18,
776
+ "max_temp": 35,
777
+ "style_tags": ["casual", "smart"],
778
+ "top_color_tags": [],
779
+ "bottom_color_tags": [],
780
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "深藍", "粉"],
781
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
782
+ "desc_zh": "修身襯衫 + 牛仔褲 (避免太緊身)",
783
+ "prompt_items_en": {
784
+ "top": [
785
+ "{top_color_en} slimfit shirt",
786
+ "{top_color_en} fitted shirt",
787
+ "{top_color_en} dress shirt"
788
+ ],
789
+ "bottom": [
790
+ "{bottom_color_en} straight jeans",
791
+ "{bottom_color_en} regular jeans",
792
+ "{bottom_color_en} straightleg jeans"
793
+ ]
794
+ }
795
+ },
796
+
797
+ {
798
+ "comment": "=== 男:橢圓 (Oval/O/蘋果) ===",
799
+ "id": "M_OVAL_01_OVERSHIRT_PANTS",
800
+ "gender": "male",
801
+ "body_types": ["橢圓形"],
802
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
803
+ "min_temp": 15,
804
+ "max_temp": 30,
805
+ "style_tags": ["casual"],
806
+ "top_color_tags": [],
807
+ "bottom_color_tags": [],
808
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
809
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "咖啡色"],
810
+ "desc_zh": "外套襯衫 + T恤 + 直筒褲",
811
+ "prompt_items_en": {
812
+ "outer": [
813
+ "{top_color_en} open overshirt",
814
+ "{top_color_en} casual overshirt",
815
+ "{top_color_en} utility overshirt"
816
+ ],
817
+ "top": [
818
+ "{top_color_en} plain tshirt",
819
+ "{top_color_en} crewneck tshirt",
820
+ "{top_color_en} cotton tshirt"
821
+ ],
822
+ "bottom": [
823
+ "{bottom_color_en} straight pants",
824
+ "{bottom_color_en} straight trousers",
825
+ "{bottom_color_en} straightleg pants"
826
+ ]
827
+ }
828
+ },
829
+ {
830
+ "id": "M_OVAL_01_OVERSHIRT_JEANS",
831
+ "gender": "male",
832
+ "body_types": ["橢圓形"],
833
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
834
+ "min_temp": 15,
835
+ "max_temp": 30,
836
+ "style_tags": ["casual"],
837
+ "top_color_tags": [],
838
+ "bottom_color_tags": [],
839
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
840
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
841
+ "desc_zh": "外套襯衫 + T恤 + 直筒牛仔",
842
+ "prompt_items_en": {
843
+ "outer": [
844
+ "{top_color_en} open overshirt",
845
+ "{top_color_en} casual overshirt",
846
+ "{top_color_en} utility overshirt"
847
+ ],
848
+ "top": [
849
+ "{top_color_en} plain tshirt",
850
+ "{top_color_en} crewneck tshirt",
851
+ "{top_color_en} cotton tshirt"
852
+ ],
853
+ "bottom": [
854
+ "{bottom_color_en} straight jeans",
855
+ "{bottom_color_en} straightleg jeans",
856
+ "{bottom_color_en} regular jeans"
857
+ ]
858
+ }
859
+ },
860
+ {
861
+ "id": "M_OVAL_01_JACKET_PANTS",
862
+ "gender": "male",
863
+ "body_types": ["橢圓形"],
864
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
865
+ "min_temp": 15,
866
+ "max_temp": 30,
867
+ "style_tags": ["casual"],
868
+ "top_color_tags": [],
869
+ "bottom_color_tags": [],
870
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
871
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "咖啡色"],
872
+ "desc_zh": "夾克 + T恤 + 直筒褲",
873
+ "prompt_items_en": {
874
+ "outer": [
875
+ "{top_color_en} casual jacket",
876
+ "{top_color_en} zipup jacket",
877
+ "{top_color_en} light jacket"
878
+ ],
879
+ "top": [
880
+ "{top_color_en} plain tshirt",
881
+ "{top_color_en} crewneck tshirt",
882
+ "{top_color_en} cotton tshirt"
883
+ ],
884
+ "bottom": [
885
+ "{bottom_color_en} straight pants",
886
+ "{bottom_color_en} straight trousers",
887
+ "{bottom_color_en} straightleg pants"
888
+ ]
889
+ }
890
+ },
891
+ {
892
+ "id": "M_OVAL_01_JACKET_JEANS",
893
+ "gender": "male",
894
+ "body_types": ["橢圓形"],
895
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
896
+ "min_temp": 15,
897
+ "max_temp": 30,
898
+ "style_tags": ["casual"],
899
+ "top_color_tags": [],
900
+ "bottom_color_tags": [],
901
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
902
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
903
+ "desc_zh": "夾克 + T恤 + 直筒牛仔",
904
+ "prompt_items_en": {
905
+ "outer": [
906
+ "{top_color_en} casual jacket",
907
+ "{top_color_en} zipup jacket",
908
+ "{top_color_en} light jacket"
909
+ ],
910
+ "top": [
911
+ "{top_color_en} plain tshirt",
912
+ "{top_color_en} crewneck tshirt",
913
+ "{top_color_en} cotton tshirt"
914
+ ],
915
+ "bottom": [
916
+ "{bottom_color_en} straight jeans",
917
+ "{bottom_color_en} straightleg jeans",
918
+ "{bottom_color_en} regular jeans"
919
+ ]
920
+ }
921
+ },
922
+ {
923
+ "id": "M_OVAL_01_SHIRT_PANTS",
924
+ "gender": "male",
925
+ "body_types": ["橢圓形"],
926
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
927
+ "min_temp": 15,
928
+ "max_temp": 30,
929
+ "style_tags": ["casual"],
930
+ "top_color_tags": [],
931
+ "bottom_color_tags": [],
932
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
933
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "咖啡色"],
934
+ "desc_zh": "襯衫外搭 + T恤 + 直筒褲",
935
+ "prompt_items_en": {
936
+ "outer": [
937
+ "{top_color_en} open shirt",
938
+ "{top_color_en} loose shirt",
939
+ "{top_color_en} long shirt"
940
+ ],
941
+ "top": [
942
+ "{top_color_en} plain tshirt",
943
+ "{top_color_en} crewneck tshirt",
944
+ "{top_color_en} cotton tshirt"
945
+ ],
946
+ "bottom": [
947
+ "{bottom_color_en} straight pants",
948
+ "{bottom_color_en} straight trousers",
949
+ "{bottom_color_en} straightleg pants"
950
+ ]
951
+ }
952
+ },
953
+ {
954
+ "id": "M_OVAL_01_SHIRT_JEANS",
955
+ "gender": "male",
956
+ "body_types": ["橢圓形"],
957
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
958
+ "min_temp": 15,
959
+ "max_temp": 30,
960
+ "style_tags": ["casual"],
961
+ "top_color_tags": [],
962
+ "bottom_color_tags": [],
963
+ "allowed_top_colors": ["白", "灰", "深灰", "深藍", "中藍", "黑", "咖啡色", "卡其"],
964
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
965
+ "desc_zh": "襯衫外搭 + T恤 + 直筒牛仔",
966
+ "prompt_items_en": {
967
+ "outer": [
968
+ "{top_color_en} open shirt",
969
+ "{top_color_en} loose shirt",
970
+ "{top_color_en} long shirt"
971
+ ],
972
+ "top": [
973
+ "{top_color_en} plain tshirt",
974
+ "{top_color_en} crewneck tshirt",
975
+ "{top_color_en} cotton tshirt"
976
+ ],
977
+ "bottom": [
978
+ "{bottom_color_en} straight jeans",
979
+ "{bottom_color_en} straightleg jeans",
980
+ "{bottom_color_en} regular jeans"
981
+ ]
982
+ }
983
+ },
984
+
985
+ {
986
+ "comment": "=== 女:沙漏 (Hourglass/X/S) ===",
987
+ "id": "F_HOUR_01",
988
+ "gender": "female",
989
+ "body_types": ["沙漏型"],
990
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
991
+ "min_temp": 20,
992
+ "max_temp": 35,
993
+ "style_tags": ["elegant", "feminine"],
994
+ "top_color_tags": [],
995
+ "bottom_color_tags": [],
996
+ "allowed_dress_colors": ["黑", "白", "粉", "深紫", "深藍", "米色", "駝色"],
997
+ "desc_zh": "收腰洋裝",
998
+ "prompt_items_en": {
999
+ "dress": [
1000
+ "{top_color_en} belted dress",
1001
+ "{top_color_en} fitted dress",
1002
+ "{top_color_en} waist dress"
1003
+ ]
1004
+ }
1005
+ },
1006
+ {
1007
+ "id": "F_HOUR_02",
1008
+ "gender": "female",
1009
+ "body_types": ["沙漏型"],
1010
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1011
+ "min_temp": 18,
1012
+ "max_temp": 30,
1013
+ "style_tags": ["elegant", "formal"],
1014
+ "top_color_tags": [],
1015
+ "bottom_color_tags": [],
1016
+ "allowed_top_colors": ["黑", "白", "粉", "深藍", "深紫", "米色"],
1017
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
1018
+ "desc_zh": "整套包臀裙 (套裝)",
1019
+ "prompt_items_en": {
1020
+ "top": [
1021
+ "{top_color_en} fitted top",
1022
+ "{top_color_en} matching top",
1023
+ "{top_color_en} formal top"
1024
+ ],
1025
+ "bottom": [
1026
+ "{bottom_color_en} pencil skirt",
1027
+ "{bottom_color_en} bodycon skirt",
1028
+ "{bottom_color_en} fitted skirt"
1029
+ ]
1030
+ }
1031
+ },
1032
+
1033
+ {
1034
+ "id": "F_HOUR_03_JEANS",
1035
+ "gender": "female",
1036
+ "body_types": ["沙漏型"],
1037
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1038
+ "min_temp": 18,
1039
+ "max_temp": 30,
1040
+ "style_tags": ["smart-casual", "office"],
1041
+ "top_color_tags": [],
1042
+ "bottom_color_tags": [],
1043
+ "allowed_top_colors": ["白", "粉", "淺藍", "灰", "深藍", "米色"],
1044
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1045
+ "desc_zh": "合身襯衫 + 牛仔褲/鉛筆裙/包臀裙",
1046
+ "prompt_items_en": {
1047
+ "top": [
1048
+ "{top_color_en} fitted shirt",
1049
+ "{top_color_en} slimfit blouse",
1050
+ "{top_color_en} tailored shirt"
1051
+ ],
1052
+ "bottom": [
1053
+ "{bottom_color_en} slim jeans",
1054
+ "{bottom_color_en} straight jeans",
1055
+ "{bottom_color_en} fitted jeans"
1056
+ ]
1057
+ }
1058
+ },
1059
+ {
1060
+ "id": "F_HOUR_03_PENCILSKIRT",
1061
+ "gender": "female",
1062
+ "body_types": ["沙漏型"],
1063
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1064
+ "min_temp": 18,
1065
+ "max_temp": 30,
1066
+ "style_tags": ["smart-casual", "office"],
1067
+ "top_color_tags": [],
1068
+ "bottom_color_tags": [],
1069
+ "allowed_top_colors": ["白", "粉", "淺藍", "灰", "深藍", "米色"],
1070
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
1071
+ "desc_zh": "合身襯衫 + 牛仔褲/鉛筆裙/包臀裙",
1072
+ "prompt_items_en": {
1073
+ "top": [
1074
+ "{top_color_en} fitted shirt",
1075
+ "{top_color_en} slimfit blouse",
1076
+ "{top_color_en} tailored shirt"
1077
+ ],
1078
+ "bottom": [
1079
+ "{bottom_color_en} pencil skirt",
1080
+ "{bottom_color_en} fitted skirt",
1081
+ "{bottom_color_en} office skirt"
1082
+ ]
1083
+ }
1084
+ },
1085
+ {
1086
+ "id": "F_HOUR_03_BODYCONSKIRT",
1087
+ "gender": "female",
1088
+ "body_types": ["沙漏型"],
1089
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1090
+ "min_temp": 18,
1091
+ "max_temp": 30,
1092
+ "style_tags": ["smart-casual", "office"],
1093
+ "top_color_tags": [],
1094
+ "bottom_color_tags": [],
1095
+ "allowed_top_colors": ["白", "粉", "淺藍", "灰", "深藍", "米色"],
1096
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
1097
+ "desc_zh": "合身襯衫 + 牛仔褲/鉛筆裙/包臀裙",
1098
+ "prompt_items_en": {
1099
+ "top": [
1100
+ "{top_color_en} fitted shirt",
1101
+ "{top_color_en} slimfit blouse",
1102
+ "{top_color_en} tailored shirt"
1103
+ ],
1104
+ "bottom": [
1105
+ "{bottom_color_en} bodycon skirt",
1106
+ "{bottom_color_en} fitted skirt",
1107
+ "{bottom_color_en} tight skirt"
1108
+ ]
1109
+ }
1110
+ },
1111
+
1112
+ {
1113
+ "id": "F_HOUR_04",
1114
+ "gender": "female",
1115
+ "body_types": ["沙漏型"],
1116
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1117
+ "min_temp": 20,
1118
+ "max_temp": 32,
1119
+ "style_tags": ["elegant", "feminine"],
1120
+ "top_color_tags": [],
1121
+ "bottom_color_tags": [],
1122
+ "allowed_top_colors": ["白"],
1123
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "粉", "深紫", "米色"],
1124
+ "desc_zh": "白色簡單上衣 + 魚尾裙",
1125
+ "prompt_items_en": {
1126
+ "top": [
1127
+ "white simple top",
1128
+ "white fitted blouse",
1129
+ "white plain blouse"
1130
+ ],
1131
+ "bottom": [
1132
+ "{bottom_color_en} mermaid skirt",
1133
+ "{bottom_color_en} fishtail skirt",
1134
+ "{bottom_color_en} fitted skirt"
1135
+ ]
1136
+ }
1137
+ },
1138
+
1139
+ {
1140
+ "id": "F_HOUR_05_TSHIRT_JEANS",
1141
+ "gender": "female",
1142
+ "body_types": ["沙漏型"],
1143
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1144
+ "min_temp": 15,
1145
+ "max_temp": 25,
1146
+ "style_tags": ["smart-casual"],
1147
+ "top_color_tags": [],
1148
+ "bottom_color_tags": [],
1149
+ "allowed_top_colors": ["白", "灰", "米色", "卡其", "駝色", "深藍", "粉"],
1150
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1151
+ "desc_zh": "收腰風衣 + T恤 + 牛仔褲",
1152
+ "prompt_items_en": {
1153
+ "outer": [
1154
+ "{top_color_en} belted trenchcoat",
1155
+ "{top_color_en} waist trenchcoat",
1156
+ "{top_color_en} classic trenchcoat"
1157
+ ],
1158
+ "top": [
1159
+ "{top_color_en} plain tshirt",
1160
+ "{top_color_en} crewneck tshirt",
1161
+ "{top_color_en} cotton tshirt"
1162
+ ],
1163
+ "bottom": [
1164
+ "{bottom_color_en} straight jeans",
1165
+ "{bottom_color_en} slim jeans",
1166
+ "{bottom_color_en} highrise jeans"
1167
+ ]
1168
+ }
1169
+ },
1170
+ {
1171
+ "id": "F_HOUR_05_TSHIRT_PANTS",
1172
+ "gender": "female",
1173
+ "body_types": ["沙漏型"],
1174
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1175
+ "min_temp": 15,
1176
+ "max_temp": 25,
1177
+ "style_tags": ["smart-casual"],
1178
+ "top_color_tags": [],
1179
+ "bottom_color_tags": [],
1180
+ "allowed_top_colors": ["白", "灰", "米色", "卡其", "駝色", "深藍", "粉"],
1181
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "米色"],
1182
+ "desc_zh": "收腰風衣 + T恤 + 直筒褲",
1183
+ "prompt_items_en": {
1184
+ "outer": [
1185
+ "{top_color_en} belted trenchcoat",
1186
+ "{top_color_en} waist trenchcoat",
1187
+ "{top_color_en} classic trenchcoat"
1188
+ ],
1189
+ "top": [
1190
+ "{top_color_en} plain tshirt",
1191
+ "{top_color_en} crewneck tshirt",
1192
+ "{top_color_en} cotton tshirt"
1193
+ ],
1194
+ "bottom": [
1195
+ "{bottom_color_en} straight pants",
1196
+ "{bottom_color_en} straight trousers",
1197
+ "{bottom_color_en} tailored pants"
1198
+ ]
1199
+ }
1200
+ },
1201
+ {
1202
+ "id": "F_HOUR_05_SWEATER_JEANS",
1203
+ "gender": "female",
1204
+ "body_types": ["沙漏型"],
1205
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1206
+ "min_temp": 15,
1207
+ "max_temp": 25,
1208
+ "style_tags": ["smart-casual"],
1209
+ "top_color_tags": [],
1210
+ "bottom_color_tags": [],
1211
+ "allowed_top_colors": ["白", "灰", "米色", "卡其", "駝色", "深藍", "粉"],
1212
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1213
+ "desc_zh": "收腰風衣 + 毛衣 + 牛仔褲",
1214
+ "prompt_items_en": {
1215
+ "outer": [
1216
+ "{top_color_en} belted trenchcoat",
1217
+ "{top_color_en} waist trenchcoat",
1218
+ "{top_color_en} classic trenchcoat"
1219
+ ],
1220
+ "top": [
1221
+ "{top_color_en} knit sweater",
1222
+ "{top_color_en} soft sweater",
1223
+ "{top_color_en} crewneck sweater"
1224
+ ],
1225
+ "bottom": [
1226
+ "{bottom_color_en} straight jeans",
1227
+ "{bottom_color_en} slim jeans",
1228
+ "{bottom_color_en} highrise jeans"
1229
+ ]
1230
+ }
1231
+ },
1232
+ {
1233
+ "id": "F_HOUR_05_SWEATER_PANTS",
1234
+ "gender": "female",
1235
+ "body_types": ["沙漏型"],
1236
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1237
+ "min_temp": 15,
1238
+ "max_temp": 25,
1239
+ "style_tags": ["smart-casual"],
1240
+ "top_color_tags": [],
1241
+ "bottom_color_tags": [],
1242
+ "allowed_top_colors": ["白", "灰", "米色", "卡其", "駝色", "深藍", "粉"],
1243
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其", "米色"],
1244
+ "desc_zh": "收腰風衣 + 毛衣 + 直筒褲",
1245
+ "prompt_items_en": {
1246
+ "outer": [
1247
+ "{top_color_en} belted trenchcoat",
1248
+ "{top_color_en} waist trenchcoat",
1249
+ "{top_color_en} classic trenchcoat"
1250
+ ],
1251
+ "top": [
1252
+ "{top_color_en} knit sweater",
1253
+ "{top_color_en} soft sweater",
1254
+ "{top_color_en} crewneck sweater"
1255
+ ],
1256
+ "bottom": [
1257
+ "{bottom_color_en} straight pants",
1258
+ "{bottom_color_en} straight trousers",
1259
+ "{bottom_color_en} tailored pants"
1260
+ ]
1261
+ }
1262
+ },
1263
+
1264
+ {
1265
+ "comment": "=== 女:梨型 (Pear/A) ===",
1266
+ "id": "F_PEAR_01_ALINESKIRT_OFFSHOULDER",
1267
+ "gender": "female",
1268
+ "body_types": ["梨型"],
1269
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1270
+ "min_temp": 20,
1271
+ "max_temp": 35,
1272
+ "style_tags": ["feminine", "cute"],
1273
+ "top_color_tags": ["light", "bright", "warm"],
1274
+ "bottom_color_tags": ["dark", "neutral"],
1275
+ "allowed_top_colors": ["白", "粉", "米色", "駝色", "淺藍"],
1276
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "咖啡色"],
1277
+ "desc_zh": "一字上衣 + A字裙",
1278
+ "prompt_items_en": {
1279
+ "top": [
1280
+ "{top_color_en} off-shoulder top",
1281
+ "{top_color_en} ruched top",
1282
+ "{top_color_en} ruffle top"
1283
+ ],
1284
+ "bottom": [
1285
+ "{bottom_color_en} aline skirt",
1286
+ "{bottom_color_en} flared skirt",
1287
+ "{bottom_color_en} skater skirt"
1288
+ ]
1289
+ }
1290
+ },
1291
+ {
1292
+ "id": "F_PEAR_01_JEANS_SQUARENECK",
1293
+ "gender": "female",
1294
+ "body_types": ["梨型"],
1295
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1296
+ "min_temp": 20,
1297
+ "max_temp": 35,
1298
+ "style_tags": ["feminine", "cute"],
1299
+ "top_color_tags": ["light", "bright", "warm"],
1300
+ "bottom_color_tags": ["dark", "neutral"],
1301
+ "allowed_top_colors": ["白", "粉", "米色", "駝色", "淺藍"],
1302
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1303
+ "desc_zh": "方領上衣 + 牛仔褲",
1304
+ "prompt_items_en": {
1305
+ "top": [
1306
+ "{top_color_en} square-neck blouse",
1307
+ "{top_color_en} square-neck top",
1308
+ "{top_color_en} sweet blouse"
1309
+ ],
1310
+ "bottom": [
1311
+ "{bottom_color_en} straight jeans",
1312
+ "{bottom_color_en} slim jeans",
1313
+ "{bottom_color_en} highrise jeans"
1314
+ ]
1315
+ }
1316
+ },
1317
+
1318
+ {
1319
+ "id": "F_PEAR_02_MAXIDRESS_SQUARENECK",
1320
+ "gender": "female",
1321
+ "body_types": ["梨型"],
1322
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1323
+ "min_temp": 18,
1324
+ "max_temp": 30,
1325
+ "style_tags": ["elegant"],
1326
+ "top_color_tags": [],
1327
+ "bottom_color_tags": [],
1328
+ "allowed_dress_colors": ["白", "粉", "淺藍", "米色", "深藍", "深紫"],
1329
+ "desc_zh": "方領長裙",
1330
+ "prompt_items_en": {
1331
+ "dress": [
1332
+ "{top_color_en} square-neck maxidress",
1333
+ "{top_color_en} fitted maxidress",
1334
+ "{top_color_en} elegant maxidress"
1335
+ ]
1336
+ }
1337
+ },
1338
+ {
1339
+ "id": "F_PEAR_02_LONGDRESS_OFFSHOULDER",
1340
+ "gender": "female",
1341
+ "body_types": ["梨型"],
1342
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1343
+ "min_temp": 18,
1344
+ "max_temp": 30,
1345
+ "style_tags": ["elegant"],
1346
+ "top_color_tags": [],
1347
+ "bottom_color_tags": [],
1348
+ "allowed_dress_colors": ["白", "粉", "淺藍", "米色", "深藍", "深紫"],
1349
+ "desc_zh": "一字長裙",
1350
+ "prompt_items_en": {
1351
+ "dress": [
1352
+ "{top_color_en} off-shoulder dress",
1353
+ "{top_color_en} long dress",
1354
+ "{top_color_en} elegant dress"
1355
+ ]
1356
+ }
1357
+ },
1358
+ {
1359
+ "id": "F_PEAR_02_ALINESKIRT_VNECK_JACKET",
1360
+ "gender": "female",
1361
+ "body_types": ["梨型"],
1362
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1363
+ "min_temp": 18,
1364
+ "max_temp": 30,
1365
+ "style_tags": ["elegant"],
1366
+ "top_color_tags": [],
1367
+ "bottom_color_tags": [],
1368
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "深藍"],
1369
+ "allowed_bottom_colors": ["黑", "深藍", "灰", "咖啡色", "米色"],
1370
+ "desc_zh": "短款A字��� + V領上衣 + 外套",
1371
+ "prompt_items_en": {
1372
+ "top": [
1373
+ "{top_color_en} vneck top",
1374
+ "{top_color_en} fitted top",
1375
+ "{top_color_en} dressy top"
1376
+ ],
1377
+ "outer": [
1378
+ "{top_color_en} short jacket",
1379
+ "{top_color_en} light jacket",
1380
+ "{top_color_en} cropped jacket"
1381
+ ],
1382
+ "bottom": [
1383
+ "{bottom_color_en} aline skirt",
1384
+ "{bottom_color_en} short skirt",
1385
+ "{bottom_color_en} flared skirt"
1386
+ ]
1387
+ }
1388
+ },
1389
+
1390
+ {
1391
+ "id": "F_PEAR_03",
1392
+ "gender": "female",
1393
+ "body_types": ["梨型"],
1394
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1395
+ "min_temp": 18,
1396
+ "max_temp": 28,
1397
+ "style_tags": ["casual", "soft"],
1398
+ "top_color_tags": [],
1399
+ "bottom_color_tags": [],
1400
+ "allowed_top_colors": ["白", "粉", "米色", "駝色", "淺藍", "深紫"],
1401
+ "allowed_bottom_colors": ["米色", "駝色", "灰", "深藍", "咖啡色", "粉"],
1402
+ "desc_zh": "長款A字裙 + 針織上衣",
1403
+ "prompt_items_en": {
1404
+ "top": [
1405
+ "{top_color_en} knit top",
1406
+ "{top_color_en} soft sweater",
1407
+ "{top_color_en} cozy knitwear"
1408
+ ],
1409
+ "bottom": [
1410
+ "{bottom_color_en} aline skirt",
1411
+ "{bottom_color_en} midi skirt",
1412
+ "{bottom_color_en} long skirt"
1413
+ ]
1414
+ }
1415
+ },
1416
+
1417
+ {
1418
+ "id": "F_PEAR_04_SKIRT",
1419
+ "gender": "female",
1420
+ "body_types": ["梨型"],
1421
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1422
+ "min_temp": 18,
1423
+ "max_temp": 30,
1424
+ "style_tags": ["feminine", "trendy"],
1425
+ "top_color_tags": [],
1426
+ "bottom_color_tags": [],
1427
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "深紫"],
1428
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色", "米色"],
1429
+ "desc_zh": "燈籠袖上衣 + 裙子",
1430
+ "prompt_items_en": {
1431
+ "top": [
1432
+ "{top_color_en} puff-sleeve blouse",
1433
+ "{top_color_en} lantern-sleeve top",
1434
+ "{top_color_en} statement blouse"
1435
+ ],
1436
+ "bottom": [
1437
+ "{bottom_color_en} midi skirt",
1438
+ "{bottom_color_en} fitted skirt",
1439
+ "{bottom_color_en} aline skirt"
1440
+ ]
1441
+ }
1442
+ },
1443
+ {
1444
+ "id": "F_PEAR_04_JEANS",
1445
+ "gender": "female",
1446
+ "body_types": ["梨型"],
1447
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1448
+ "min_temp": 18,
1449
+ "max_temp": 30,
1450
+ "style_tags": ["feminine", "trendy"],
1451
+ "top_color_tags": [],
1452
+ "bottom_color_tags": [],
1453
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "深紫"],
1454
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1455
+ "desc_zh": "燈籠袖上衣 + 牛仔褲",
1456
+ "prompt_items_en": {
1457
+ "top": [
1458
+ "{top_color_en} puff-sleeve blouse",
1459
+ "{top_color_en} lantern-sleeve top",
1460
+ "{top_color_en} statement blouse"
1461
+ ],
1462
+ "bottom": [
1463
+ "{bottom_color_en} straight jeans",
1464
+ "{bottom_color_en} slim jeans",
1465
+ "{bottom_color_en} highrise jeans"
1466
+ ]
1467
+ }
1468
+ },
1469
+ {
1470
+ "id": "F_PEAR_04_SLIMPANTS",
1471
+ "gender": "female",
1472
+ "body_types": ["梨型"],
1473
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1474
+ "min_temp": 18,
1475
+ "max_temp": 30,
1476
+ "style_tags": ["feminine", "trendy"],
1477
+ "top_color_tags": [],
1478
+ "bottom_color_tags": [],
1479
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "深紫"],
1480
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色", "米色"],
1481
+ "desc_zh": "燈籠袖上衣 + 修身直筒褲",
1482
+ "prompt_items_en": {
1483
+ "top": [
1484
+ "{top_color_en} puff-sleeve blouse",
1485
+ "{top_color_en} lantern-sleeve top",
1486
+ "{top_color_en} statement blouse"
1487
+ ],
1488
+ "bottom": [
1489
+ "{bottom_color_en} slimstraight pants",
1490
+ "{bottom_color_en} straight pants",
1491
+ "{bottom_color_en} tailored pants"
1492
+ ]
1493
+ }
1494
+ },
1495
+
1496
+ {
1497
+ "id": "F_PEAR_05",
1498
+ "gender": "female",
1499
+ "body_types": ["梨型"],
1500
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1501
+ "min_temp": 22,
1502
+ "max_temp": 35,
1503
+ "style_tags": ["casual", "summer"],
1504
+ "top_color_tags": [],
1505
+ "bottom_color_tags": [],
1506
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "卡其"],
1507
+ "allowed_bottom_colors": ["白", "米色", "卡其", "駝色", "灰", "深藍"],
1508
+ "desc_zh": "短版上衣/T恤/法式襯衫 + 寬褲",
1509
+ "prompt_items_en": {
1510
+ "top": [
1511
+ "{top_color_en} crop top",
1512
+ "{top_color_en} plain tshirt",
1513
+ "{top_color_en} french blouse"
1514
+ ],
1515
+ "bottom": [
1516
+ "{bottom_color_en} wideleg pants",
1517
+ "{bottom_color_en} wide trousers",
1518
+ "{bottom_color_en} loose pants"
1519
+ ]
1520
+ }
1521
+ },
1522
+
1523
+ {
1524
+ "comment": "=== 女:蘋果 (Apple/O) ===",
1525
+ "id": "F_APPLE_01_VNECK_ALINESKIRT",
1526
+ "gender": "female",
1527
+ "body_types": ["蘋果型"],
1528
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1529
+ "min_temp": 18,
1530
+ "max_temp": 30,
1531
+ "style_tags": ["casual", "soft"],
1532
+ "top_color_tags": [],
1533
+ "bottom_color_tags": [],
1534
+ "allowed_top_colors": ["白", "粉", "米色", "灰", "淺藍", "深藍"],
1535
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色", "米色"],
1536
+ "desc_zh": "V領上衣 + A字裙",
1537
+ "prompt_items_en": {
1538
+ "top": [
1539
+ "{top_color_en} vneck top",
1540
+ "{top_color_en} vneck knit",
1541
+ "{top_color_en} relaxed blouse"
1542
+ ],
1543
+ "bottom": [
1544
+ "{bottom_color_en} aline skirt",
1545
+ "{bottom_color_en} flared skirt",
1546
+ "{bottom_color_en} swing skirt"
1547
+ ]
1548
+ }
1549
+ },
1550
+ {
1551
+ "id": "F_APPLE_01_COWLNECK_FLAREDSKIRT",
1552
+ "gender": "female",
1553
+ "body_types": ["蘋果型"],
1554
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1555
+ "min_temp": 18,
1556
+ "max_temp": 30,
1557
+ "style_tags": ["casual", "soft"],
1558
+ "top_color_tags": [],
1559
+ "bottom_color_tags": [],
1560
+ "allowed_top_colors": ["白", "粉", "米色", "灰", "淺藍", "深藍"],
1561
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色", "米色"],
1562
+ "desc_zh": "垂領上衣 + 腰下開展裙",
1563
+ "prompt_items_en": {
1564
+ "top": [
1565
+ "{top_color_en} cowlneck top",
1566
+ "{top_color_en} draped blouse",
1567
+ "{top_color_en} relaxed top"
1568
+ ],
1569
+ "bottom": [
1570
+ "{bottom_color_en} flared skirt",
1571
+ "{bottom_color_en} swing skirt",
1572
+ "{bottom_color_en} skater skirt"
1573
+ ]
1574
+ }
1575
+ },
1576
+ {
1577
+ "id": "F_APPLE_01_KNIT_ALINESKIRT",
1578
+ "gender": "female",
1579
+ "body_types": ["蘋果型"],
1580
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1581
+ "min_temp": 18,
1582
+ "max_temp": 30,
1583
+ "style_tags": ["casual", "soft"],
1584
+ "top_color_tags": [],
1585
+ "bottom_color_tags": [],
1586
+ "allowed_top_colors": ["白", "粉", "米色", "灰", "淺藍", "深藍"],
1587
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色", "米色"],
1588
+ "desc_zh": "針織上衣 + A字裙",
1589
+ "prompt_items_en": {
1590
+ "top": [
1591
+ "{top_color_en} knit top",
1592
+ "{top_color_en} soft sweater",
1593
+ "{top_color_en} cozy knitwear"
1594
+ ],
1595
+ "bottom": [
1596
+ "{bottom_color_en} aline skirt",
1597
+ "{bottom_color_en} flared skirt",
1598
+ "{bottom_color_en} swing skirt"
1599
+ ]
1600
+ }
1601
+ },
1602
+
1603
+ {
1604
+ "id": "F_APPLE_02_VNECK_DRESS",
1605
+ "gender": "female",
1606
+ "body_types": ["蘋果型"],
1607
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1608
+ "min_temp": 20,
1609
+ "max_temp": 35,
1610
+ "style_tags": ["feminine"],
1611
+ "top_color_tags": [],
1612
+ "bottom_color_tags": [],
1613
+ "allowed_dress_colors": ["黑", "深藍", "深紫", "粉", "米色", "白"],
1614
+ "desc_zh": "V領洋裝",
1615
+ "prompt_items_en": {
1616
+ "dress": [
1617
+ "{top_color_en} vneck dress",
1618
+ "{top_color_en} flowy dress",
1619
+ "{top_color_en} relaxed dress"
1620
+ ]
1621
+ }
1622
+ },
1623
+ {
1624
+ "id": "F_APPLE_02_COWLNECK_DRESS",
1625
+ "gender": "female",
1626
+ "body_types": ["蘋果型"],
1627
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1628
+ "min_temp": 20,
1629
+ "max_temp": 35,
1630
+ "style_tags": ["feminine"],
1631
+ "top_color_tags": [],
1632
+ "bottom_color_tags": [],
1633
+ "allowed_dress_colors": ["黑", "深藍", "深紫", "粉", "米色", "白"],
1634
+ "desc_zh": "垂領洋裝",
1635
+ "prompt_items_en": {
1636
+ "dress": [
1637
+ "{top_color_en} cowlneck dress",
1638
+ "{top_color_en} draped dress",
1639
+ "{top_color_en} flowy dress"
1640
+ ]
1641
+ }
1642
+ },
1643
+
1644
+ {
1645
+ "id": "F_APPLE_03",
1646
+ "gender": "female",
1647
+ "body_types": ["蘋果型"],
1648
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1649
+ "min_temp": 22,
1650
+ "max_temp": 35,
1651
+ "style_tags": ["casual", "chic"],
1652
+ "top_color_tags": [],
1653
+ "bottom_color_tags": [],
1654
+ "allowed_top_colors": ["白", "粉", "米色", "淺藍", "灰"],
1655
+ "allowed_bottom_colors": ["白", "米色", "卡其", "駝色", "深藍", "灰"],
1656
+ "desc_zh": "短版上衣/T恤/法式襯衫 + 寬褲",
1657
+ "prompt_items_en": {
1658
+ "top": [
1659
+ "{top_color_en} crop top",
1660
+ "{top_color_en} plain tshirt",
1661
+ "{top_color_en} french blouse"
1662
+ ],
1663
+ "bottom": [
1664
+ "{bottom_color_en} wideleg pants",
1665
+ "{bottom_color_en} wide trousers",
1666
+ "{bottom_color_en} loose pants"
1667
+ ]
1668
+ }
1669
+ },
1670
+
1671
+ {
1672
+ "comment": "=== 女:直筒/H型 (Rectangle/H) ===",
1673
+ "id": "F_RECT_01_JEANS",
1674
+ "gender": "female",
1675
+ "body_types": ["H 型"],
1676
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1677
+ "min_temp": 18,
1678
+ "max_temp": 30,
1679
+ "style_tags": ["casual"],
1680
+ "top_color_tags": [],
1681
+ "bottom_color_tags": [],
1682
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "粉", "米色"],
1683
+ "allowed_bottom_colors": ["深藍", "中藍", "灰", "黑"],
1684
+ "desc_zh": "直筒牛仔褲 + T恤",
1685
+ "prompt_items_en": {
1686
+ "top": [
1687
+ "{top_color_en} plain tshirt",
1688
+ "{top_color_en} crewneck tee",
1689
+ "{top_color_en} basic tee"
1690
+ ],
1691
+ "bottom": [
1692
+ "{bottom_color_en} straight jeans",
1693
+ "{bottom_color_en} straightleg jeans",
1694
+ "{bottom_color_en} regular jeans"
1695
+ ]
1696
+ }
1697
+ },
1698
+ {
1699
+ "id": "F_RECT_01_PANTS",
1700
+ "gender": "female",
1701
+ "body_types": ["H 型"],
1702
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1703
+ "min_temp": 18,
1704
+ "max_temp": 30,
1705
+ "style_tags": ["casual"],
1706
+ "top_color_tags": [],
1707
+ "bottom_color_tags": [],
1708
+ "allowed_top_colors": ["白", "灰", "黑", "淺藍", "粉", "米色"],
1709
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "卡其", "米色"],
1710
+ "desc_zh": "直筒褲 + T恤",
1711
+ "prompt_items_en": {
1712
+ "top": [
1713
+ "{top_color_en} plain tshirt",
1714
+ "{top_color_en} crewneck tee",
1715
+ "{top_color_en} basic tee"
1716
+ ],
1717
+ "bottom": [
1718
+ "{bottom_color_en} straight pants",
1719
+ "{bottom_color_en} straight trousers",
1720
+ "{bottom_color_en} tailored pants"
1721
+ ]
1722
+ }
1723
+ },
1724
+
1725
+ {
1726
+ "id": "F_RECT_02",
1727
+ "gender": "female",
1728
+ "body_types": ["H 型"],
1729
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1730
+ "min_temp": 18,
1731
+ "max_temp": 28,
1732
+ "style_tags": ["smart-casual", "office"],
1733
+ "top_color_tags": [],
1734
+ "bottom_color_tags": [],
1735
+ "allowed_top_colors": ["白", "淺藍", "灰", "粉", "深藍"],
1736
+ "allowed_bottom_colors": ["黑", "灰", "深灰", "深藍", "卡其"],
1737
+ "desc_zh": "西裝褲 + 襯衫",
1738
+ "prompt_items_en": {
1739
+ "top": [
1740
+ "{top_color_en} buttondown shirt",
1741
+ "{top_color_en} office blouse",
1742
+ "{top_color_en} collared shirt"
1743
+ ],
1744
+ "bottom": [
1745
+ "{bottom_color_en} suitpants",
1746
+ "{bottom_color_en} tailored pants",
1747
+ "{bottom_color_en} straight trousers"
1748
+ ]
1749
+ }
1750
+ },
1751
+
1752
+ {
1753
+ "id": "F_RECT_03_PENCILSKIRT_SHIRT",
1754
+ "gender": "female",
1755
+ "body_types": ["H 型"],
1756
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1757
+ "min_temp": 20,
1758
+ "max_temp": 30,
1759
+ "style_tags": ["feminine", "chic"],
1760
+ "top_color_tags": [],
1761
+ "bottom_color_tags": [],
1762
+ "allowed_top_colors": ["白", "粉", "淺藍", "灰", "米色"],
1763
+ "allowed_bottom_colors": ["黑", "灰", "深藍", "咖啡色"],
1764
+ "desc_zh": "窄裙 + 襯衫",
1765
+ "prompt_items_en": {
1766
+ "top": [
1767
+ "{top_color_en} fitted shirt",
1768
+ "{top_color_en} blouse shirt",
1769
+ "{top_color_en} collared blouse"
1770
+ ],
1771
+ "bottom": [
1772
+ "{bottom_color_en} pencil skirt",
1773
+ "{bottom_color_en} narrow skirt",
1774
+ "{bottom_color_en} fitted skirt"
1775
+ ]
1776
+ }
1777
+ },
1778
+ {
1779
+ "id": "F_RECT_03_ALINESKIRT_TSHIRT",
1780
+ "gender": "female",
1781
+ "body_types": ["H 型"],
1782
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1783
+ "min_temp": 20,
1784
+ "max_temp": 30,
1785
+ "style_tags": ["feminine", "chic"],
1786
+ "top_color_tags": [],
1787
+ "bottom_color_tags": [],
1788
+ "allowed_top_colors": ["白", "灰", "黑", "粉", "淺藍", "米色", "深藍"],
1789
+ "allowed_bottom_colors": ["白", "灰", "黑", "粉", "淺藍", "米色", "深藍"],
1790
+ "desc_zh": "A字裙 + 同色系T恤",
1791
+ "prompt_items_en": {
1792
+ "top": [
1793
+ "{top_color_en} monochrome tshirt",
1794
+ "{top_color_en} plain tshirt",
1795
+ "{top_color_en} fitted tee"
1796
+ ],
1797
+ "bottom": [
1798
+ "{bottom_color_en} aline skirt",
1799
+ "{bottom_color_en} flared skirt",
1800
+ "{bottom_color_en} chic skirt"
1801
+ ]
1802
+ }
1803
+ },
1804
+
1805
+ {
1806
+ "comment": "=== 女:倒三角 (Inverted Triangle/V) ===",
1807
+ "id": "F_INV_01",
1808
+ "gender": "female",
1809
+ "body_types": ["倒三角型"],
1810
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1811
+ "min_temp": 20,
1812
+ "max_temp": 35,
1813
+ "style_tags": ["casual", "relaxed"],
1814
+ "top_color_tags": ["dark", "cool"],
1815
+ "bottom_color_tags": ["light", "bright", "warm"],
1816
+ "allowed_top_colors": ["黑", "深藍", "深灰", "灰", "深紫"],
1817
+ "allowed_bottom_colors": ["白", "米色", "駝色", "卡其", "粉", "淺藍"],
1818
+ "desc_zh": "寬鬆上衣 + 寬褲",
1819
+ "prompt_items_en": {
1820
+ "top": [
1821
+ "{top_color_en} loose top",
1822
+ "{top_color_en} oversized blouse",
1823
+ "{top_color_en} relaxed shirt"
1824
+ ],
1825
+ "bottom": [
1826
+ "{bottom_color_en} wideleg pants",
1827
+ "{bottom_color_en} wide trousers",
1828
+ "{bottom_color_en} flowy pants"
1829
+ ]
1830
+ }
1831
+ },
1832
+
1833
+ {
1834
+ "id": "F_INV_02_ALINESKIRT_SHIRT",
1835
+ "gender": "female",
1836
+ "body_types": ["倒三角型"],
1837
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1838
+ "min_temp": 18,
1839
+ "max_temp": 28,
1840
+ "style_tags": ["smart-casual"],
1841
+ "top_color_tags": [],
1842
+ "bottom_color_tags": [],
1843
+ "allowed_top_colors": ["白", "淺藍", "灰", "粉", "深藍"],
1844
+ "allowed_bottom_colors": ["白", "米色", "卡其", "淺藍", "灰", "粉"],
1845
+ "desc_zh": "襯衫 + A字裙 + 無領外套/夾克",
1846
+ "prompt_items_en": {
1847
+ "outer": [
1848
+ "{top_color_en} collarless jacket",
1849
+ "{top_color_en} cropped jacket",
1850
+ "{top_color_en} light jacket"
1851
+ ],
1852
+ "top": [
1853
+ "{top_color_en} buttondown shirt",
1854
+ "{top_color_en} collared shirt",
1855
+ "{top_color_en} office blouse"
1856
+ ],
1857
+ "bottom": [
1858
+ "{bottom_color_en} aline skirt",
1859
+ "{bottom_color_en} flared skirt",
1860
+ "{bottom_color_en} midi skirt"
1861
+ ]
1862
+ }
1863
+ },
1864
+ {
1865
+ "id": "F_INV_02_SUITPANTS_TSHIRT",
1866
+ "gender": "female",
1867
+ "body_types": ["倒三角型"],
1868
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1869
+ "min_temp": 18,
1870
+ "max_temp": 28,
1871
+ "style_tags": ["smart-casual"],
1872
+ "top_color_tags": [],
1873
+ "bottom_color_tags": [],
1874
+ "allowed_top_colors": ["白", "淺藍", "灰", "粉", "深藍"],
1875
+ "allowed_bottom_colors": ["白", "米色", "卡其", "淺藍", "灰", "粉"],
1876
+ "desc_zh": "T恤 + 西裝褲 + 無領外套/夾克",
1877
+ "prompt_items_en": {
1878
+ "outer": [
1879
+ "{top_color_en} collarless jacket",
1880
+ "{top_color_en} cropped jacket",
1881
+ "{top_color_en} light jacket"
1882
+ ],
1883
+ "top": [
1884
+ "{top_color_en} plain tshirt",
1885
+ "{top_color_en} crewneck tee",
1886
+ "{top_color_en} basic tee"
1887
+ ],
1888
+ "bottom": [
1889
+ "{bottom_color_en} suitpants",
1890
+ "{bottom_color_en} tailored pants",
1891
+ "{bottom_color_en} straight trousers"
1892
+ ]
1893
+ }
1894
+ },
1895
+
1896
+ {
1897
+ "id": "F_INV_03_MINI_FLUFFY",
1898
+ "gender": "female",
1899
+ "body_types": ["倒三角型"],
1900
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1901
+ "min_temp": 22,
1902
+ "max_temp": 35,
1903
+ "style_tags": ["feminine", "cute"],
1904
+ "top_color_tags": [],
1905
+ "bottom_color_tags": [],
1906
+ "allowed_dress_colors": ["粉", "白", "淺藍", "米色", "深紫"],
1907
+ "desc_zh": "蓬鬆下擺迷你洋裝",
1908
+ "prompt_items_en": {
1909
+ "dress": [
1910
+ "{top_color_en} mini dress",
1911
+ "{top_color_en} fluffy hem",
1912
+ "{top_color_en} aline mini"
1913
+ ]
1914
+ }
1915
+ },
1916
+ {
1917
+ "id": "F_INV_03_VNECK_DRESS",
1918
+ "gender": "female",
1919
+ "body_types": ["倒三角型"],
1920
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1921
+ "min_temp": 22,
1922
+ "max_temp": 35,
1923
+ "style_tags": ["feminine", "cute"],
1924
+ "top_color_tags": [],
1925
+ "bottom_color_tags": [],
1926
+ "allowed_dress_colors": ["粉", "白", "淺藍", "米色", "深紫"],
1927
+ "desc_zh": "V領洋裝",
1928
+ "prompt_items_en": {
1929
+ "dress": [
1930
+ "{top_color_en} vneck dress",
1931
+ "{top_color_en} cute dress",
1932
+ "{top_color_en} aline dress"
1933
+ ]
1934
+ }
1935
+ },
1936
+
1937
+ {
1938
+ "id": "F_INV_04_SUITPANTS",
1939
+ "gender": "female",
1940
+ "body_types": ["倒三角型"],
1941
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1942
+ "min_temp": 18,
1943
+ "max_temp": 30,
1944
+ "style_tags": ["smart-casual", "office"],
1945
+ "top_color_tags": ["dark", "cool"],
1946
+ "bottom_color_tags": ["light", "bright"],
1947
+ "allowed_top_colors": ["黑", "深藍", "深灰", "灰", "深紫"],
1948
+ "allowed_bottom_colors": ["白", "米色", "卡其", "粉", "淺藍", "灰"],
1949
+ "desc_zh": "V領襯衫 + 西裝褲",
1950
+ "prompt_items_en": {
1951
+ "top": [
1952
+ "{top_color_en} vneck shirt",
1953
+ "{top_color_en} vneck blouse",
1954
+ "{top_color_en} office blouse"
1955
+ ],
1956
+ "bottom": [
1957
+ "{bottom_color_en} suitpants",
1958
+ "{bottom_color_en} tailored pants",
1959
+ "{bottom_color_en} straight trousers"
1960
+ ]
1961
+ }
1962
+ },
1963
+ {
1964
+ "id": "F_INV_04_ALINESKIRT",
1965
+ "gender": "female",
1966
+ "body_types": ["倒三角型"],
1967
+ "face_shapes": ["圓臉", "方臉", "長形臉", "鵝蛋臉", "心型臉", "鑽石臉", "圓下巴"],
1968
+ "min_temp": 18,
1969
+ "max_temp": 30,
1970
+ "style_tags": ["smart-casual", "office"],
1971
+ "top_color_tags": ["dark", "cool"],
1972
+ "bottom_color_tags": ["light", "bright"],
1973
+ "allowed_top_colors": ["黑", "深藍", "深灰", "灰", "深紫"],
1974
+ "allowed_bottom_colors": ["白", "米色", "卡其", "粉", "淺藍", "灰"],
1975
+ "desc_zh": "V領襯衫 + A字裙",
1976
+ "prompt_items_en": {
1977
+ "top": [
1978
+ "{top_color_en} vneck shirt",
1979
+ "{top_color_en} vneck blouse",
1980
+ "{top_color_en} office blouse"
1981
+ ],
1982
+ "bottom": [
1983
+ "{bottom_color_en} aline skirt",
1984
+ "{bottom_color_en} flared skirt",
1985
+ "{bottom_color_en} midi skirt"
1986
+ ]
1987
+ }
1988
+ }
1989
+ ]
1990
+ }
outfits.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 男性服裝款式套數(不含顏色組合):
2
+ 矩形:8套
3
+ 正三角:9套
4
+ 梯形:7套
5
+ 倒三角:4套
6
+ 橢圓:6套
7
+ total:34套
8
+
9
+
10
+ 女性服裝款式套數(不含顏色組合):
11
+ 沙漏:10套
12
+ 梨形:10套
13
+ 蘋果:6套
14
+ 直筒:5套
15
+ 倒三角:7套
16
+ total:38套
recommend_api.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 檔名:recommend_api.py
2
+ # 功能:
3
+ # - 提供 /recommend API
4
+ # - 內部呼叫 recommender.run_recommend_model() 來產生 clothe.json
5
+ # - 自動把每次收到的 request 存成 json 檔,放在 saved_requests/
6
+ # - 自動把每次回傳的 clothe_json 存成 json 檔,放在 saved_clothes/
7
+ # - ✅ 自動判斷身形,印出身形,並把結果塞進 report 給推薦系統用
8
+
9
+ from fastapi import FastAPI
10
+ from pydantic import BaseModel
11
+ from typing import Optional, Dict, Any
12
+
13
+ from pathlib import Path
14
+ import json
15
+ import time
16
+
17
+ from recommender import run_recommend_model, ClotheJSON
18
+ from body_type_classifier import classify_male_body_type, classify_female_body_type
19
+
20
+ app = FastAPI(title="Recommendation Service")
21
+
22
+ BASE_DIR = Path(__file__).resolve().parent
23
+
24
+ LOG_REQ_DIR = BASE_DIR / "saved_requests"
25
+ LOG_REQ_DIR.mkdir(exist_ok=True)
26
+
27
+ LOG_RES_DIR = BASE_DIR / "saved_clothes"
28
+ LOG_RES_DIR.mkdir(exist_ok=True)
29
+
30
+
31
+ class WeatherInfo(BaseModel):
32
+ condition: str
33
+ temperature: float
34
+ humidity: Optional[float] = None
35
+
36
+
37
+ class RecommendRequest(BaseModel):
38
+ user_id: Optional[str] = None
39
+ report: dict
40
+ weather: WeatherInfo
41
+
42
+
43
+ DEFAULT_GENDER = "male"
44
+
45
+
46
+ def extract_gender(report: Dict[str, Any]) -> Optional[str]:
47
+ gender = (
48
+ report.get("gender") or
49
+ report.get("sex") or
50
+ report.get("Gender") or
51
+ report.get("user_gender")
52
+ )
53
+
54
+ if isinstance(gender, str):
55
+ g = gender.lower()
56
+ if g in ("male", "m", "boy", "man", "男", "男性"):
57
+ return "male"
58
+ if g in ("female", "f", "girl", "woman", "女", "女性"):
59
+ return "female"
60
+
61
+ if DEFAULT_GENDER is not None:
62
+ print(f"[BodyType] report 中沒有 gender 欄位,暫時使用 DEFAULT_GENDER={DEFAULT_GENDER}")
63
+ return DEFAULT_GENDER
64
+
65
+ return None
66
+
67
+
68
+ def extract_body_measurements(report: Dict[str, Any]) -> Optional[Dict[str, float]]:
69
+ bm = report.get("body_measurements")
70
+ if isinstance(bm, dict):
71
+ return bm
72
+ return None
73
+
74
+
75
+ def attach_body_type(report: Dict[str, Any]) -> None:
76
+ body_measurements = extract_body_measurements(report)
77
+ gender = extract_gender(report)
78
+
79
+ if not body_measurements or not gender:
80
+ print(
81
+ f"[BodyType] 無法判斷:缺少 body_measurements 或 gender,"
82
+ f"gender={gender}, has_body={bool(body_measurements)}"
83
+ )
84
+ return
85
+
86
+ try:
87
+ if gender == "male":
88
+ body_type = classify_male_body_type(body_measurements)
89
+ else:
90
+ body_type = classify_female_body_type(body_measurements)
91
+
92
+ print(f"[BodyType] gender={gender} → body_type={body_type}")
93
+
94
+ report["body_type"] = body_type
95
+ report["body_gender"] = gender
96
+
97
+ except Exception as e:
98
+ print(f"[BodyType] 判斷身形時發生錯誤: {e}")
99
+
100
+
101
+ @app.post("/recommend", response_model=ClotheJSON)
102
+ def recommend(req: RecommendRequest) -> ClotheJSON:
103
+ weather_dict = req.weather.dict()
104
+
105
+ timestamp = int(time.time())
106
+ user_part = req.user_id if req.user_id else "anonymous"
107
+ base_name = f"{user_part}_{timestamp}"
108
+
109
+ report_dict: Dict[str, Any] = dict(req.report)
110
+
111
+ attach_body_type(report_dict)
112
+
113
+ req_path = LOG_REQ_DIR / f"request_{base_name}.json"
114
+ with req_path.open("w", encoding="utf-8") as f:
115
+ json.dump(
116
+ {
117
+ "user_id": req.user_id,
118
+ "report": report_dict,
119
+ "weather": weather_dict,
120
+ },
121
+ f,
122
+ ensure_ascii=False,
123
+ indent=2,
124
+ )
125
+
126
+ clothe_json = run_recommend_model(report_dict, weather_dict)
127
+
128
+ res_path = LOG_RES_DIR / f"clothe_{base_name}.json"
129
+ with res_path.open("w", encoding="utf-8") as f:
130
+ json.dump(
131
+ clothe_json,
132
+ f,
133
+ ensure_ascii=False,
134
+ indent=2,
135
+ )
136
+
137
+ return clothe_json
recommender.py ADDED
@@ -0,0 +1,900 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 檔名:recommender.py
2
+ # 功能:
3
+ # - 負責「推薦模型 / 規則」
4
+ # - 對外只提供一個函式:run_recommend_model(report, weather_dict)
5
+ #
6
+ # 目前流程:
7
+ # 1. 從 report 讀取:身形、臉型、性別、膚色資訊。
8
+ # 2. 依身形 / 臉型 / 性別 / 氣溫,從 outfits.json 篩出候選款式。
9
+ # 3. 依 skin_analysis.skin_tone_type / skin_tone_name 找到色盤(colors.json -> palettes),
10
+ # 展開出顏色搭配 (top_color_name, bottom_color_name)。
11
+ # 4. 對每組顏色搭配計算:
12
+ # - 相似色分數 S_sim
13
+ # - 互補色分數 S_comp
14
+ # - 對比色分數 S_cont
15
+ # - 與膚色的相容度 S_skin
16
+ # 再用:S_color = max(S_sim, S_comp, S_cont) + α · S_skin。
17
+ # 5. 對每個 outfit,從高分顏色搭配中,挑選一組「顏色 tags 符合
18
+ # top_color_tags / bottom_color_tags」的組合,生成英文 prompt。
19
+ #
20
+ # ✅ 新版 prompt 結構(給 MGD / VTON):
21
+ # outfits.json 每個 outfit 用 prompt_items_en,像:
22
+ # "prompt_items_en": {
23
+ # "top": ["{top_color_en} plain tshirt", ...],
24
+ # "bottom": ["{bottom_color_en} straight jeans", ...],
25
+ # "outer": ["{top_color_en} bomber jacket", ...]
26
+ # }
27
+ #
28
+ # 回傳 clothe_json(ClotheJSON):
29
+ # {
30
+ # "M_RECT_01_SUITPANTS_01": {
31
+ # "top": [... 3 prompts ...],
32
+ # "bottom": [... 3 prompts ...],
33
+ # "outer": [... 3 prompts ...]
34
+ # },
35
+ # ...
36
+ # }
37
+
38
+ from __future__ import annotations
39
+
40
+ from typing import Dict, List, Any, Tuple, Optional
41
+ from pathlib import Path
42
+ import json
43
+ import math
44
+ import colorsys
45
+ import random
46
+
47
+
48
+ # ============================================================
49
+ # 型別:回傳給 API 的 clothe_json
50
+ # ============================================================
51
+
52
+ # 每套 outfit 會回傳多個「部位」的 prompt(每部位通常 3 句)
53
+ # 例如:{"top":[...], "bottom":[...], "outer":[...]}
54
+ PromptItemsEN = Dict[str, List[str]]
55
+
56
+ # clothe_id -> PromptItemsEN
57
+ ClotheJSON = Dict[str, PromptItemsEN]
58
+
59
+
60
+ # ============================================================
61
+ # 讀取 outfits.json
62
+ # ============================================================
63
+
64
+ BASE_DIR = Path(__file__).resolve().parent
65
+ OUTFITS_PATH = BASE_DIR / "outfits.json"
66
+
67
+ try:
68
+ with OUTFITS_PATH.open("r", encoding="utf-8") as f:
69
+ _outfits_raw = json.load(f)
70
+ OUTFIT_LIBRARY: List[dict] = _outfits_raw.get("outfits", [])
71
+ print(f"[Recommender] 載入 outfits.json,共 {len(OUTFIT_LIBRARY)} 套穿搭。")
72
+ except FileNotFoundError:
73
+ print("[Recommender] 找不到 outfits.json,OUTFIT_LIBRARY 為空,請確認檔案放在與 recommender.py 同一層。")
74
+ OUTFIT_LIBRARY = []
75
+ except Exception as e:
76
+ print(f"[Recommender] 讀取 outfits.json 發生錯誤:{e}")
77
+ OUTFIT_LIBRARY = []
78
+
79
+
80
+ # ============================================================
81
+ # 讀取 colors.json
82
+ # ============================================================
83
+
84
+ COLORS_PATH = BASE_DIR / "colors.json"
85
+
86
+ try:
87
+ with COLORS_PATH.open("r", encoding="utf-8") as f:
88
+ _colors_raw = json.load(f)
89
+ _RAW_COLORS: Dict[str, Dict[str, Any]] = _colors_raw.get("colors", {})
90
+ SKIN_TONE_TO_PALETTE: Dict[str, str] = _colors_raw.get("skin_tone_to_palette", {})
91
+ PALETTES: Dict[str, Any] = _colors_raw.get("palettes", {})
92
+ print(f"[Recommender] 載入 colors.json,顏色數量={len(_RAW_COLORS)},色盤數量={len(PALETTES)}。")
93
+ except FileNotFoundError:
94
+ print("[Recommender] 找不到 colors.json,請確認檔案放在與 recommender.py 同一層。")
95
+ _RAW_COLORS = {}
96
+ SKIN_TONE_TO_PALETTE = {}
97
+ PALETTES = {}
98
+ except Exception as e:
99
+ print(f"[Recommender] 讀取 colors.json 發生錯誤:{e}")
100
+ _RAW_COLORS = {}
101
+ SKIN_TONE_TO_PALETTE = {}
102
+ PALETTES = {}
103
+
104
+
105
+ # ============================================================
106
+ # 色碼轉換:RGB / HEX -> HSL
107
+ # ============================================================
108
+
109
+ def rgb_to_hsl(r: float, g: float, b: float) -> Tuple[float, float, float]:
110
+ """RGB(0–255) -> HSL (H:0–360, S/L:0–1)"""
111
+ r_n, g_n, b_n = r / 255.0, g / 255.0, b / 255.0
112
+ # colorsys 回傳的是 HLS(注意順序)
113
+ h, l, s = colorsys.rgb_to_hls(r_n, g_n, b_n)
114
+ return h * 360.0, s, l
115
+
116
+
117
+ def hex_to_hsl(hex_str: str) -> Tuple[float, float, float]:
118
+ """將十六進位色碼 (#RRGGBB) 轉成 HSL。"""
119
+ hex_str = hex_str.strip().lstrip("#")
120
+ if len(hex_str) == 3:
121
+ # 例如 #abc -> #aabbcc
122
+ hex_str = "".join([c * 2 for c in hex_str])
123
+ if len(hex_str) != 6:
124
+ # 給一個中性灰的預設
125
+ return rgb_to_hsl(128, 128, 128)
126
+ try:
127
+ r = int(hex_str[0:2], 16)
128
+ g = int(hex_str[2:4], 16)
129
+ b = int(hex_str[4:6], 16)
130
+ except ValueError:
131
+ # hex 解析失敗也用預設
132
+ return rgb_to_hsl(128, 128, 128)
133
+ return rgb_to_hsl(r, g, b)
134
+
135
+
136
+ # 建立顏色資料庫:中文名稱 -> {en, hsl, tags}
137
+ COLOR_DB: Dict[str, Dict[str, Any]] = {}
138
+ for name, info in _RAW_COLORS.items():
139
+ hex_code = info.get("hex", "#888888")
140
+ hsl = hex_to_hsl(hex_code)
141
+ COLOR_DB[name] = {
142
+ "en": info.get("en", name),
143
+ "hsl": hsl,
144
+ "tags": info.get("tags", []),
145
+ }
146
+
147
+
148
+ # ============================================================
149
+ # 超參數(之後都可以自己調)
150
+ # ============================================================
151
+
152
+ ALPHA_SKIN = 0.55 # 原 0.8:降低膚色權重,避免壓過色彩和諧分
153
+ TOP_K_COLOR_COMBOS = 60 # 原 20:增加候選顏色組合,降低配不到的機率
154
+ MAX_OUTFITS = 3 # 維持輸出 3 套
155
+ PER_OUTFIT_COLOR_OPTIONS = 20 # 每個款式從前幾個可用顏色中抽一個,避免結果過度固定
156
+ FINAL_OUTFIT_SELECTION_POOL = 10 # 最終挑套裝時,不只硬取前 3 套,改從前幾名中抽樣
157
+
158
+ # skin 相容度相關參數:控制「色盤寬度」與「理想亮度/飽和度差」
159
+ SIGMA_H_SKIN = 55.0 # 原 40:放寬色相差容許
160
+ SIGMA_L_SKIN = 0.28 # 原 0.20:放寬亮度差容許
161
+ SIGMA_S_SKIN = 0.38 # 原 0.30:放寬飽和度差容許
162
+ MU_L_SKIN = 0.12 # 原 0.15:偏向較小亮度差,較自然
163
+ MU_S_SKIN = 0.08 # 原 0.10:稍降飽和度偏好,減少過度鮮豔
164
+
165
+
166
+ # ============================================================
167
+ # 色碼常數
168
+ # ============================================================
169
+
170
+ # 中性色設定(黑/白/灰)
171
+ NEUTRAL_COLOR_NAMES = {
172
+ "黑", "黑色", "白", "白色", "灰", "灰色",
173
+ "black", "white", "gray", "grey",
174
+ }
175
+ NEUTRAL_COLOR_BONUS = 6.0
176
+ DOUBLE_NEUTRAL_EXTRA_BONUS = 2.0
177
+
178
+ def _is_neutral_color(color_name: str) -> bool:
179
+ """判斷是否為中性色(黑/白/灰)。"""
180
+ if not isinstance(color_name, str):
181
+ return False
182
+
183
+ key = color_name.strip().lower()
184
+ if key in NEUTRAL_COLOR_NAMES:
185
+ return True
186
+
187
+ info = COLOR_DB.get(color_name) or {}
188
+ en_name = str(info.get("en", "")).strip().lower()
189
+ return en_name in NEUTRAL_COLOR_NAMES
190
+
191
+
192
+ # ============================================================
193
+ # 色彩評分相關工具
194
+ # ============================================================
195
+
196
+ def hue_distance(h1: float, h2: float) -> float:
197
+ """色相環上的距離 (0–180)。"""
198
+ dh = abs(h1 - h2) % 360.0
199
+ if dh > 180.0:
200
+ dh = 360.0 - dh
201
+ return dh
202
+
203
+
204
+ def gaussian(x: float, mu: float, sigma: float) -> float:
205
+ """一維高斯函數,輸出約 0–1。"""
206
+ if sigma <= 0:
207
+ return 0.0
208
+ return math.exp(-((x - mu) / sigma) ** 2)
209
+
210
+
211
+ def skin_compatibility_for_color(
212
+ skin_hsl: Tuple[float, float, float],
213
+ cloth_hsl: Tuple[float, float, float],
214
+ ) -> float:
215
+ """
216
+ 計算單一衣服顏色與膚色的相容度 C_skin(c_k),輸出 0–100。
217
+ - 色相差越小越好;
218
+ - 與膚色亮度差約 MU_L_SKIN 最佳;
219
+ - 飽和度比膚色稍高(約 MU_S_SKIN)最佳。
220
+ """
221
+ Hs, Ss, Ls = skin_hsl
222
+ Hc, Sc, Lc = cloth_hsl
223
+
224
+ delta_H = hue_distance(Hc, Hs) # 色相差 (degree)
225
+ delta_L = abs(Lc - Ls) # 亮度差
226
+ delta_S = Sc - Ss # 飽和度差(衣服 - 膚色)
227
+
228
+ term_h = gaussian(delta_H, 0.0, SIGMA_H_SKIN)
229
+ term_l = gaussian(delta_L, MU_L_SKIN, SIGMA_L_SKIN)
230
+ term_s = gaussian(delta_S, MU_S_SKIN, SIGMA_S_SKIN)
231
+
232
+ return 100.0 * term_h * term_l * term_s
233
+
234
+
235
+ def skin_compatibility_for_outfit(
236
+ skin_hsl: Tuple[float, float, float],
237
+ outfit_colors_hsl: List[Tuple[float, float, float]],
238
+ weights: Optional[List[float]] = None,
239
+ ) -> float:
240
+ """
241
+ S_skin(o) = sum_k w_k · C_skin(c_k) / sum_k w_k
242
+ 如果沒有給 weights,預設每個部位權重都一樣。
243
+ """
244
+ n = len(outfit_colors_hsl)
245
+ if n == 0:
246
+ return 0.0
247
+ if weights is None:
248
+ weights = [1.0] * n
249
+
250
+ total_w = sum(weights)
251
+ if total_w <= 0:
252
+ return 0.0
253
+
254
+ acc = 0.0
255
+ for w, color_hsl in zip(weights, outfit_colors_hsl):
256
+ acc += w * skin_compatibility_for_color(skin_hsl, color_hsl)
257
+
258
+ return acc / total_w
259
+
260
+
261
+ def sim_score(hsl1: Tuple[float, float, float],
262
+ hsl2: Tuple[float, float, float]) -> float:
263
+ """
264
+ 相似色分數:
265
+ - 色相差角度小;
266
+ - 明度 / 飽和度差距小(但不要完全 0)。
267
+ """
268
+ H1, S1, L1 = hsl1
269
+ H2, S2, L2 = hsl2
270
+ dh = hue_distance(H1, H2)
271
+ ds = abs(S1 - S2)
272
+ dl = abs(L1 - L2)
273
+
274
+ score_h = gaussian(dh, 0.0, 25.0)
275
+ score_s = gaussian(ds, 0.15, 0.20)
276
+ score_l = gaussian(dl, 0.10, 0.20)
277
+
278
+ return 100.0 * score_h * score_s * score_l
279
+
280
+
281
+ def comp_score(hsl1: Tuple[float, float, float],
282
+ hsl2: Tuple[float, float, float]) -> float:
283
+ """
284
+ 互補色分數:
285
+ - 色相差約 180 度;
286
+ - 飽和度差中等;
287
+ - 明度差不大。
288
+ """
289
+ H1, S1, L1 = hsl1
290
+ H2, S2, L2 = hsl2
291
+ dh = hue_distance(H1, H2)
292
+ ds = abs(S1 - S2)
293
+ dl = abs(L1 - L2)
294
+
295
+ score_h = gaussian(dh, 180.0, 25.0)
296
+ score_s = gaussian(ds, 0.25, 0.20)
297
+ score_l = gaussian(dl, 0.10, 0.20)
298
+
299
+ return 100.0 * score_h * score_s * score_l
300
+
301
+
302
+ def cont_score(hsl1: Tuple[float, float, float],
303
+ hsl2: Tuple[float, float, float]) -> float:
304
+ """
305
+ 對比色分數:
306
+ - 色相差約 90 度;
307
+ - 明度 / 飽和度差都「不小也不大」。
308
+ """
309
+ H1, S1, L1 = hsl1
310
+ H2, S2, L2 = hsl2
311
+ dh = hue_distance(H1, H2)
312
+ ds = abs(S1 - S2)
313
+ dl = abs(L1 - L2)
314
+
315
+ score_h = gaussian(dh, 90.0, 25.0)
316
+ score_s = gaussian(ds, 0.30, 0.20)
317
+ score_l = gaussian(dl, 0.25, 0.20)
318
+
319
+ return 100.0 * score_h * score_s * score_l
320
+
321
+
322
+ def color_harmony_scores_for_combo(
323
+ colors_hsl: List[Tuple[float, float, float]]
324
+ ) -> Tuple[float, float, float]:
325
+ """
326
+ 給一組顏色(目前假設 2 個顏色),計算:
327
+ S_sim(o), S_comp(o), S_cont(o)
328
+ """
329
+ if len(colors_hsl) < 2:
330
+ return 0.0, 0.0, 0.0
331
+
332
+ hsl1, hsl2 = colors_hsl[0], colors_hsl[1]
333
+ s_sim = sim_score(hsl1, hsl2)
334
+ s_comp = comp_score(hsl1, hsl2)
335
+ s_cont = cont_score(hsl1, hsl2)
336
+ return s_sim, s_comp, s_cont
337
+
338
+
339
+ # ============================================================
340
+ # 從 report 抽資訊、篩選款式、決定顏色候選
341
+ # ============================================================
342
+
343
+ def _normalize_gender(g: Optional[str]) -> Optional[str]:
344
+ """把各種寫法的性別字串統一成 'male' / 'female'。"""
345
+ if g is None:
346
+ return None
347
+ if not isinstance(g, str):
348
+ return None
349
+ g = g.strip().lower()
350
+ if g in ("male", "m", "boy", "man", "男", "男性"):
351
+ return "male"
352
+ if g in ("female", "f", "girl", "woman", "女", "女性"):
353
+ return "female"
354
+ return None
355
+
356
+
357
+ def extract_skin_rgb(skin_info: Dict[str, Any]) -> Tuple[float, float, float]:
358
+ """
359
+ 盡量從 skin_analysis 裡萃取一組 RGB:
360
+ - {"color_rgb": [r,g,b]} ← 你現在 JSON 的主要來源
361
+ - {"skin_rgb": {"r":..., "g":..., "b":...}}
362
+ - 其它 fallback。
363
+ 找不到時回傳一個中性的膚色。
364
+ """
365
+ v = skin_info.get("color_rgb")
366
+ if isinstance(v, (list, tuple)) and len(v) >= 3:
367
+ try:
368
+ r, g, b = v[:3]
369
+ return float(r), float(g), float(b)
370
+ except Exception:
371
+ pass
372
+
373
+ for key in ("skin_rgb", "skin_color_rgb", "avg_rgb", "rgb"):
374
+ v = skin_info.get(key)
375
+ if isinstance(v, dict) and all(ch in v for ch in ("r", "g", "b")):
376
+ try:
377
+ return float(v["r"]), float(v["g"]), float(v["b"])
378
+ except Exception:
379
+ pass
380
+ if isinstance(v, (list, tuple)) and len(v) >= 3:
381
+ try:
382
+ r, g, b = v[:3]
383
+ return float(r), float(g), float(b)
384
+ except Exception:
385
+ pass
386
+
387
+ for v in skin_info.values():
388
+ if isinstance(v, dict) and all(ch in v for ch in ("r", "g", "b")):
389
+ try:
390
+ return float(v["r"]), float(v["g"]), float(v["b"])
391
+ except Exception:
392
+ continue
393
+ if isinstance(v, (list, tuple)) and len(v) >= 3:
394
+ try:
395
+ r, g, b = v[:3]
396
+ return float(r), float(g), float(b)
397
+ except Exception:
398
+ continue
399
+
400
+ return 190.0, 164.0, 133.0
401
+
402
+
403
+ def filter_outfits(body_type: Optional[str],
404
+ face_shape: Optional[str],
405
+ gender: Optional[str],
406
+ weather: Dict[str, Any]) -> List[dict]:
407
+ """
408
+ 依體型 / 臉型 / 性別 / 氣溫,從 OUTFIT_LIBRARY 裡挑出候選。
409
+ """
410
+ temperature = weather.get("temperature")
411
+ norm_gender = _normalize_gender(gender)
412
+
413
+ candidates: List[dict] = []
414
+ for outfit in OUTFIT_LIBRARY:
415
+ outfit_gender = _normalize_gender(outfit.get("gender"))
416
+ if norm_gender and outfit_gender and outfit_gender != norm_gender:
417
+ continue
418
+
419
+ outfit_body_types = outfit.get("body_types") or []
420
+ if body_type and outfit_body_types and body_type not in outfit_body_types:
421
+ continue
422
+
423
+ outfit_face_shapes = outfit.get("face_shapes") or []
424
+ if face_shape and outfit_face_shapes and face_shape not in outfit_face_shapes:
425
+ continue
426
+
427
+ if isinstance(temperature, (int, float)):
428
+ min_temp = outfit.get("min_temp")
429
+ max_temp = outfit.get("max_temp")
430
+ if isinstance(min_temp, (int, float)) and temperature < float(min_temp):
431
+ continue
432
+ if isinstance(max_temp, (int, float)) and temperature > float(max_temp):
433
+ continue
434
+
435
+ candidates.append(outfit)
436
+
437
+ if not candidates:
438
+ candidates = list(OUTFIT_LIBRARY)
439
+
440
+ return candidates
441
+
442
+
443
+ def get_color_combos_for_user(report: dict, gender: Optional[str] = None) -> List[Tuple[str, str]]:
444
+ """
445
+ 根據 skin_analysis 中的 skin_tone_type / skin_tone_name,
446
+ 從 colors.json 的 palettes 裡挑出這個人的顏色搭配候選。
447
+ 支援性別區分:會優先查找 `{gender}_{type}` 的 key。
448
+ """
449
+ skin = report.get("skin_analysis", {}) or {}
450
+ tone_type = skin.get("skin_tone_type")
451
+ tone_name = skin.get("skin_tone_name")
452
+
453
+ norm_gender = _normalize_gender(gender)
454
+ palette_key: Optional[str] = None
455
+
456
+ if norm_gender and isinstance(tone_type, str):
457
+ key_with_gender = f"{norm_gender}_{tone_type}"
458
+ if key_with_gender in SKIN_TONE_TO_PALETTE:
459
+ palette_key = SKIN_TONE_TO_PALETTE[key_with_gender]
460
+
461
+ if not palette_key and isinstance(tone_type, str) and tone_type in SKIN_TONE_TO_PALETTE:
462
+ palette_key = SKIN_TONE_TO_PALETTE[tone_type]
463
+
464
+ if not palette_key and norm_gender and isinstance(tone_name, str):
465
+ key_with_gender = f"{norm_gender}_{tone_name}"
466
+ if key_with_gender in SKIN_TONE_TO_PALETTE:
467
+ palette_key = SKIN_TONE_TO_PALETTE[key_with_gender]
468
+
469
+ if not palette_key and isinstance(tone_name, str) and tone_name in SKIN_TONE_TO_PALETTE:
470
+ palette_key = SKIN_TONE_TO_PALETTE[tone_name]
471
+
472
+ print(f"[Recommender] Skin Type: {tone_type}, Gender: {norm_gender} -> Palette Key: {palette_key}")
473
+
474
+ combos: List[Tuple[str, str]] = []
475
+
476
+ if palette_key and palette_key in PALETTES:
477
+ palette = PALETTES[palette_key]
478
+ for entry in palette.get("combos", []):
479
+ top = entry.get("top")
480
+ bottoms = entry.get("bottoms", [])
481
+ if not top or not bottoms:
482
+ continue
483
+ for b in bottoms:
484
+ if top in COLOR_DB and b in COLOR_DB:
485
+ combos.append((top, b))
486
+
487
+ if not combos and COLOR_DB:
488
+ print("[Recommender] 找不到對應色盤或 combos 為空,使用全顏色 fallback。")
489
+ names = list(COLOR_DB.keys())
490
+ for i, c1 in enumerate(names):
491
+ for c2 in names[i + 1:]:
492
+ combos.append((c1, c2))
493
+
494
+ return combos[:TOP_K_COLOR_COMBOS]
495
+
496
+
497
+ # ============================================================
498
+ # prompt helper(新版:prompt_items_en)
499
+ # ============================================================
500
+
501
+ def _safe_format_prompt(s: str, top_color_en: str, bottom_color_en: str) -> str:
502
+ """
503
+ 安全 format:允許字串沒有 placeholder,也允許只用其中一種。
504
+ """
505
+ top_color_en = top_color_en.replace(" ", "-")
506
+ bottom_color_en = bottom_color_en.replace(" ", "-")
507
+ try:
508
+ return s.format(top_color_en=top_color_en, bottom_color_en=bottom_color_en)
509
+ except KeyError as e:
510
+ # 如果 outfits.json 不小心寫了別的 placeholder 名稱,避免整套爆掉
511
+ print(f"[Recommender] prompt format 缺少 placeholder:{e},原字串:{s}")
512
+ return s
513
+ except Exception as e:
514
+ print(f"[Recommender] prompt format 失敗:{e},原字串:{s}")
515
+ return s
516
+
517
+
518
+ def _build_prompt_items_en(outfit: dict, top_color_en: str, bottom_color_en: str) -> PromptItemsEN:
519
+ """
520
+ 優先使用 outfits.json 的 prompt_items_en(你新定義的結構)。
521
+ 若沒有,才回退到 prompt_template_en,包成 {"full": [...]}。
522
+ """
523
+ prompt_items = outfit.get("prompt_items_en")
524
+
525
+ # ✅ 新格式:dict[str, list[str]]
526
+ if isinstance(prompt_items, dict):
527
+ out: PromptItemsEN = {}
528
+ for part, arr in prompt_items.items():
529
+ if not isinstance(part, str):
530
+ continue
531
+ if isinstance(arr, list):
532
+ formatted = []
533
+ for x in arr:
534
+ if not isinstance(x, str):
535
+ continue
536
+ formatted.append(_safe_format_prompt(x, top_color_en, bottom_color_en).strip())
537
+ if formatted:
538
+ out[part] = formatted
539
+ if out:
540
+ return out
541
+
542
+ # 🔁 舊格式 fallback:prompt_template_en(整句)
543
+ prompts_en: List[str] = []
544
+ for tmpl in outfit.get("prompt_template_en", []):
545
+ if not isinstance(tmpl, str):
546
+ continue
547
+ prompts_en.append(_safe_format_prompt(tmpl, top_color_en, bottom_color_en).strip())
548
+
549
+ if not prompts_en:
550
+ prompts_en = [
551
+ f"{top_color_en} top with {bottom_color_en} bottom",
552
+ f"outfit with {top_color_en} upper garment and {bottom_color_en} lower garment",
553
+ f"{top_color_en} and {bottom_color_en} color-coordinated outfit",
554
+ ]
555
+
556
+ seen = set()
557
+ prompts_en = [x for x in prompts_en if not (x in seen or seen.add(x))]
558
+ return {"full": prompts_en[:3]}
559
+
560
+
561
+ def _outfit_uses_dress_color(outfit: dict) -> bool:
562
+ """判斷此 outfit 是否為 dress 單件式輸出。"""
563
+ allowed_dress_colors = outfit.get("allowed_dress_colors") or []
564
+ if allowed_dress_colors:
565
+ return True
566
+
567
+ prompt_items = outfit.get("prompt_items_en")
568
+ if isinstance(prompt_items, dict):
569
+ dress_items = prompt_items.get("dress")
570
+ if isinstance(dress_items, list) and dress_items:
571
+ return True
572
+
573
+ return False
574
+
575
+
576
+ def _color_allowed_by_rules(
577
+ color_name: str,
578
+ allowed_colors: List[str],
579
+ allowed_tags: List[str],
580
+ ) -> bool:
581
+ """
582
+ 顏色檢查規則:
583
+ 1. 若 outfit 有明確 allowed_*_colors,優先用顯式顏色名單。
584
+ 2. 否則回退到 *_color_tags。
585
+ 3. 若兩者都沒填,視為可用。
586
+ """
587
+ if allowed_colors:
588
+ return color_name in allowed_colors
589
+
590
+ if not allowed_tags:
591
+ return True
592
+
593
+ if _is_neutral_color(color_name):
594
+ return True
595
+
596
+ color_info = COLOR_DB.get(color_name)
597
+ if not color_info:
598
+ return False
599
+
600
+ color_tags = color_info.get("tags") or []
601
+ return any(tag in allowed_tags for tag in color_tags)
602
+
603
+
604
+ def _weighted_pick_by_score(candidates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
605
+ """
606
+ 從少量高分候選中做加權隨機:
607
+ - 分數越高越容易被選到
608
+ - 但不會永遠只選第一名
609
+ 使用 sqrt 壓縮分數差,避免第一名權重過大。
610
+ """
611
+ if not candidates:
612
+ return None
613
+ if len(candidates) == 1:
614
+ return candidates[0]
615
+
616
+ weights: List[float] = []
617
+ for item in candidates:
618
+ score = max(float(item.get("score", 0.0)), 0.0)
619
+ weights.append(math.sqrt(score) + 1e-6)
620
+
621
+ return random.choices(candidates, weights=weights, k=1)[0]
622
+
623
+
624
+ # ============================================================
625
+ # 主函式:run_recommend_model
626
+ # ============================================================
627
+
628
+ def run_recommend_model(report: dict, weather: Dict[str, Any]) -> ClotheJSON:
629
+ """
630
+ 推薦邏輯(回傳新版 ClotheJSON):
631
+ - 每個 clothe_id 對應一套 outfit
632
+ - 每套 outfit 內是「部位 -> prompts(list[str])」
633
+ """
634
+
635
+ # -------- 1) 從 report 抽資訊 --------
636
+ body_type = (
637
+ report.get("body_type") or
638
+ report.get("body_shape_analysis", {}).get("body_shape_type")
639
+ )
640
+ face_shape = report.get("face_analysis", {}).get("face_shape")
641
+
642
+ gender = (
643
+ report.get("body_gender") or
644
+ report.get("gender") or
645
+ report.get("sex")
646
+ )
647
+
648
+ skin_info = report.get("skin_analysis", {}) or {}
649
+ if not isinstance(skin_info, dict):
650
+ skin_info = {}
651
+ r, g, b = extract_skin_rgb(skin_info)
652
+ skin_hsl = rgb_to_hsl(r, g, b)
653
+
654
+ # -------- 2) 篩選候選款式 --------
655
+ outfit_candidates = filter_outfits(body_type, face_shape, gender, weather)
656
+ if not outfit_candidates:
657
+ print("[Recommender] OUTFIT_LIBRARY 為空或篩選後沒有任何款式。")
658
+ return {}
659
+
660
+ # -------- 3) 取得這個人的顏色搭配候選 --------
661
+ color_combos = get_color_combos_for_user(report, gender)
662
+ if not color_combos:
663
+ print("[Recommender] 沒有任何顏色搭配候選,請檢查 colors.json。")
664
+ return {}
665
+
666
+ # -------- 4) 對每一組顏色搭配計算分數 --------
667
+ combo_scores: List[Tuple[float, Tuple[str, str]]] = []
668
+
669
+ for name1, name2 in color_combos:
670
+ color1 = COLOR_DB.get(name1)
671
+ color2 = COLOR_DB.get(name2)
672
+ if not color1 or not color2:
673
+ continue
674
+
675
+ hsl1 = color1["hsl"]
676
+ hsl2 = color2["hsl"]
677
+
678
+ s_sim, s_comp, s_cont = color_harmony_scores_for_combo([hsl1, hsl2])
679
+ s_skin = skin_compatibility_for_outfit(skin_hsl, [hsl1, hsl2])
680
+
681
+ neutral_bonus = 0.0
682
+ n1 = _is_neutral_color(name1)
683
+ n2 = _is_neutral_color(name2)
684
+ if n1 or n2:
685
+ neutral_bonus += NEUTRAL_COLOR_BONUS
686
+ if n1 and n2:
687
+ neutral_bonus += DOUBLE_NEUTRAL_EXTRA_BONUS
688
+
689
+ s_color = max(s_sim, s_comp, s_cont) + ALPHA_SKIN * s_skin + neutral_bonus
690
+ combo_scores.append((s_color, (name1, name2)))
691
+
692
+ if not combo_scores:
693
+ print("[Recommender] 所有顏色搭配都無法計算分數,可能是 COLOR_DB 空的。")
694
+ return {}
695
+
696
+ combo_scores.sort(key=lambda x: x[0], reverse=True)
697
+
698
+ dress_color_score_map: Dict[str, float] = {}
699
+ for score, (name1, name2) in combo_scores:
700
+ dress_color_score_map[name1] = max(dress_color_score_map.get(name1, float("-inf")), score)
701
+ dress_color_score_map[name2] = max(dress_color_score_map.get(name2, float("-inf")), score)
702
+
703
+ dress_color_scores: List[Tuple[float, str]] = sorted(
704
+ ((score, color_name) for color_name, score in dress_color_score_map.items()),
705
+ key=lambda x: x[0],
706
+ reverse=True,
707
+ )
708
+
709
+ # -------- 5) 先找出每個 outfit 可用的最佳顏色,再挑整體最高分 --------
710
+ results: ClotheJSON = {}
711
+
712
+ outfit_matches: List[Dict[str, Any]] = []
713
+
714
+ for outfit in outfit_candidates:
715
+ allowed_top_tags = outfit.get("top_color_tags") or []
716
+ allowed_bottom_tags = outfit.get("bottom_color_tags") or []
717
+ allowed_top_colors = outfit.get("allowed_top_colors") or []
718
+ allowed_bottom_colors = outfit.get("allowed_bottom_colors") or []
719
+ allowed_dress_colors = outfit.get("allowed_dress_colors") or []
720
+
721
+ if _outfit_uses_dress_color(outfit):
722
+ dress_candidates: List[Dict[str, Any]] = []
723
+
724
+ for score, color_name in dress_color_scores:
725
+ if not _color_allowed_by_rules(color_name, allowed_dress_colors, allowed_top_tags):
726
+ continue
727
+ dress_candidates.append({
728
+ "color_name": color_name,
729
+ "score": score,
730
+ })
731
+ if len(dress_candidates) >= PER_OUTFIT_COLOR_OPTIONS:
732
+ break
733
+
734
+ chosen_dress = _weighted_pick_by_score(dress_candidates)
735
+ if not chosen_dress:
736
+ continue
737
+
738
+ chosen_color = str(chosen_dress["color_name"])
739
+ chosen_score = float(chosen_dress["score"])
740
+
741
+ color_info = COLOR_DB.get(chosen_color)
742
+ if not color_info:
743
+ continue
744
+
745
+ outfit_matches.append({
746
+ "outfit": outfit,
747
+ "score": chosen_score,
748
+ "mode": "dress",
749
+ "top_color_zh": chosen_color,
750
+ "bottom_color_zh": chosen_color,
751
+ "top_color_en": color_info["en"],
752
+ "bottom_color_en": color_info["en"],
753
+ "signature": ("dress", chosen_color),
754
+ })
755
+ continue
756
+
757
+ combo_candidates_for_outfit: List[Dict[str, Any]] = []
758
+
759
+ for score, (name1, name2) in combo_scores:
760
+ if not _color_allowed_by_rules(name1, allowed_top_colors, allowed_top_tags):
761
+ continue
762
+ if not _color_allowed_by_rules(name2, allowed_bottom_colors, allowed_bottom_tags):
763
+ continue
764
+ combo_candidates_for_outfit.append({
765
+ "combo": (name1, name2),
766
+ "score": score,
767
+ })
768
+ if len(combo_candidates_for_outfit) >= PER_OUTFIT_COLOR_OPTIONS:
769
+ break
770
+
771
+ chosen_combo_entry = _weighted_pick_by_score(combo_candidates_for_outfit)
772
+ if not chosen_combo_entry:
773
+ continue
774
+
775
+ chosen_combo = tuple(chosen_combo_entry["combo"])
776
+ chosen_score = float(chosen_combo_entry["score"])
777
+
778
+ color1 = COLOR_DB.get(chosen_combo[0])
779
+ color2 = COLOR_DB.get(chosen_combo[1])
780
+ if not color1 or not color2:
781
+ continue
782
+
783
+ outfit_matches.append({
784
+ "outfit": outfit,
785
+ "score": chosen_score,
786
+ "mode": "combo",
787
+ "top_color_zh": chosen_combo[0],
788
+ "bottom_color_zh": chosen_combo[1],
789
+ "top_color_en": color1["en"],
790
+ "bottom_color_en": color2["en"],
791
+ "signature": ("combo", chosen_combo[0], chosen_combo[1]),
792
+ })
793
+
794
+ if not outfit_matches:
795
+ print("[Recommender] 沒有任何 outfit 成功配到顏色組合(可能是 allowed colors / color_tags 規則太嚴)。")
796
+ return {}
797
+
798
+ outfit_matches.sort(key=lambda item: item["score"], reverse=True)
799
+
800
+ print(f"[Recommender] 已為 {len(outfit_matches)} 個候選款式找到可用顏色,開始挑選前 {MAX_OUTFITS} 套。")
801
+
802
+ selected_matches: List[Dict[str, Any]] = []
803
+ used_signatures = set()
804
+ used_primary_colors = set()
805
+ remaining_matches = list(outfit_matches)
806
+
807
+ while remaining_matches and len(selected_matches) < MAX_OUTFITS:
808
+ eligible_matches = [
809
+ match for match in remaining_matches
810
+ if match["signature"] not in used_signatures
811
+ and match["top_color_zh"] not in used_primary_colors
812
+ ]
813
+
814
+ if not eligible_matches:
815
+ eligible_matches = [
816
+ match for match in remaining_matches
817
+ if match["signature"] not in used_signatures
818
+ ]
819
+
820
+ if not eligible_matches:
821
+ break
822
+
823
+ selection_pool = eligible_matches[:min(FINAL_OUTFIT_SELECTION_POOL, len(eligible_matches))]
824
+ chosen_match = _weighted_pick_by_score(selection_pool)
825
+ if not chosen_match:
826
+ break
827
+
828
+ selected_matches.append(chosen_match)
829
+ used_signatures.add(chosen_match["signature"])
830
+ used_primary_colors.add(chosen_match["top_color_zh"])
831
+ remaining_matches = [match for match in remaining_matches if match is not chosen_match]
832
+
833
+ if len(selected_matches) < MAX_OUTFITS:
834
+ print(
835
+ f"[Recommender] 放寬主色不可重複後,仍只選到 {len(selected_matches)} 套;"
836
+ "若要更多變化,可再放寬 allowed colors。"
837
+ )
838
+
839
+ for outfit_assigned, match in enumerate(selected_matches, start=1):
840
+ outfit = match["outfit"]
841
+ top_color_zh = match["top_color_zh"]
842
+ bottom_color_zh = match["bottom_color_zh"]
843
+ top_color_en = match["top_color_en"]
844
+ bottom_color_en = match["bottom_color_en"]
845
+ chosen_score = float(match["score"])
846
+
847
+ desc_zh = outfit.get("desc_zh", "")
848
+ if match["mode"] == "dress":
849
+ zh_desc = f"{top_color_zh}色{desc_zh}".strip()
850
+ elif "+" in desc_zh:
851
+ left, right = [part.strip() for part in desc_zh.split("+", 1)]
852
+ zh_desc = f"{top_color_zh}色{left} + {bottom_color_zh}色{right}"
853
+ else:
854
+ zh_desc = f"{top_color_zh}色 / {bottom_color_zh}色 {desc_zh}".strip()
855
+
856
+ prompt_items_en = _build_prompt_items_en(outfit, top_color_en, bottom_color_en)
857
+
858
+ outfit_id = str(outfit.get("id", "O"))
859
+ clothe_id = f"{outfit_id}_{outfit_assigned:02d}"
860
+ results[clothe_id] = prompt_items_en
861
+
862
+ if match["mode"] == "dress":
863
+ print(
864
+ f"[Recommender] 推薦 {clothe_id}: {zh_desc} | "
865
+ f"dress_color={top_color_zh}, score={chosen_score:.2f}"
866
+ )
867
+ else:
868
+ print(
869
+ f"[Recommender] 推薦 {clothe_id}: {zh_desc} | "
870
+ f"colors=({top_color_zh}, {bottom_color_zh}), score={chosen_score:.2f}"
871
+ )
872
+
873
+ if not results:
874
+ print("[Recommender] 沒有任何 outfit 成功產生 prompt。")
875
+
876
+ return results
877
+
878
+
879
+
880
+ """
881
+ # === 目前先回傳你提供的 6 句描述(兩件洋裝) ===
882
+ dress_1_prompts = [
883
+ "long red sleeveless dress",
884
+ "red floor-length dress",
885
+ "solid red long dress",
886
+ ]
887
+
888
+ dress_2_prompts = [
889
+ "cream dress",
890
+ "natural sleeveless v-neck dress",
891
+ "sleevless beige dress", # 保留你原本的拼字
892
+ ]
893
+
894
+ clothe_json: ClotheJSON = {
895
+ "000001": dress_1_prompts,
896
+ "000002": dress_2_prompts,
897
+ }
898
+
899
+ return clothe_json
900
+ """
test.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # test.py
2
+ # 放在 RECOMMEND_SERVICE/ 同一層執行:
3
+ # python test.py saved_requests/request_xxx.json
4
+ # 或不帶參數(自動抓 saved_requests/ 最新的 .json):
5
+ # python test.py
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ # 你的專案模組(同層)
16
+ from recommender import (
17
+ ALPHA_SKIN,
18
+ COLOR_DB,
19
+ DOUBLE_NEUTRAL_EXTRA_BONUS,
20
+ NEUTRAL_COLOR_BONUS,
21
+ _is_neutral_color,
22
+ color_harmony_scores_for_combo,
23
+ extract_skin_rgb,
24
+ get_color_combos_for_user,
25
+ rgb_to_hsl,
26
+ run_recommend_model,
27
+ skin_compatibility_for_outfit,
28
+ )
29
+ from body_type_classifier import classify_male_body_type, classify_female_body_type
30
+
31
+
32
+ DEFAULT_GENDER = "male" # 跟你 API 裡的行為一致:沒給 gender 就先用 male
33
+
34
+
35
+ def extract_gender(report: Dict[str, Any]) -> Optional[str]:
36
+ """
37
+ 從 report 抓性別。找不到就回 DEFAULT_GENDER。
38
+ """
39
+ gender = (
40
+ report.get("gender")
41
+ or report.get("sex")
42
+ or report.get("Gender")
43
+ or report.get("user_gender")
44
+ or report.get("body_gender") # 若前面已經有,就沿用
45
+ )
46
+
47
+ if isinstance(gender, str):
48
+ g = gender.strip().lower()
49
+ if g in ("male", "m", "boy", "man", "男", "男性"):
50
+ return "male"
51
+ if g in ("female", "f", "girl", "woman", "女", "女性"):
52
+ return "female"
53
+
54
+ # 找不到就用預設
55
+ if DEFAULT_GENDER is not None:
56
+ print(f"[BodyType] report 中沒有可用的 gender 欄位,暫時使用 DEFAULT_GENDER={DEFAULT_GENDER}")
57
+ return DEFAULT_GENDER
58
+
59
+ return None
60
+
61
+
62
+ def extract_body_measurements(report: Dict[str, Any]) -> Optional[Dict[str, float]]:
63
+ """
64
+ 從 report 取得 body_measurements dict。
65
+ 你提供的 request JSON 結構是 report["body_measurements"] = {...}
66
+ """
67
+ bm = report.get("body_measurements")
68
+ if isinstance(bm, dict) and bm:
69
+ return bm # type: ignore[return-value]
70
+ return None
71
+
72
+
73
+ def attach_body_type(report: Dict[str, Any]) -> None:
74
+ """
75
+ 先依 body_measurements + gender 判斷身形,寫回 report:
76
+ report["body_type"]
77
+ report["body_gender"]
78
+ """
79
+ body_measurements = extract_body_measurements(report)
80
+ gender = extract_gender(report)
81
+
82
+ if not body_measurements or not gender:
83
+ print("[BodyType] 無法判斷:缺少 body_measurements 或 gender(且 DEFAULT_GENDER=None)")
84
+ return
85
+
86
+ try:
87
+ if gender == "male":
88
+ body_type = classify_male_body_type(body_measurements)
89
+ else:
90
+ body_type = classify_female_body_type(body_measurements)
91
+
92
+ print(f"[BodyType] gender={gender} → body_type={body_type}")
93
+
94
+ report["body_type"] = body_type
95
+ report["body_gender"] = gender
96
+
97
+ except Exception as e:
98
+ print(f"[BodyType] 判斷身形時發生錯誤: {e}")
99
+
100
+
101
+ def pick_default_request_file(base_dir: Path) -> Path:
102
+ """
103
+ 不帶參數時:自動挑 saved_requests/ 裡最新修改的 .json
104
+ """
105
+ req_dir = base_dir / "saved_requests"
106
+ if not req_dir.exists():
107
+ raise FileNotFoundError(f"找不到資料夾:{req_dir}")
108
+
109
+ candidates = sorted(req_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
110
+ if not candidates:
111
+ raise FileNotFoundError(f"{req_dir} 底下沒有任何 .json 檔可以測試")
112
+ return candidates[0]
113
+
114
+
115
+ def load_request_json(path: Path) -> Dict[str, Any]:
116
+ with path.open("r", encoding="utf-8") as f:
117
+ data = json.load(f)
118
+ if not isinstance(data, dict):
119
+ raise ValueError("request JSON 的最外層必須是 dict")
120
+ return data
121
+
122
+
123
+ def print_top10_color_combos(report: Dict[str, Any]) -> None:
124
+ """
125
+ 印出該使用者顏色組合前 10 名(含分數)。
126
+ 分數公式與 recommender 內一致:
127
+ S_color = max(S_sim, S_comp, S_cont) + ALPHA_SKIN * S_skin + neutral_bonus
128
+ """
129
+ gender = (
130
+ report.get("body_gender")
131
+ or report.get("gender")
132
+ or report.get("sex")
133
+ )
134
+
135
+ skin_info = report.get("skin_analysis", {}) or {}
136
+ if not isinstance(skin_info, dict):
137
+ skin_info = {}
138
+
139
+ r, g, b = extract_skin_rgb(skin_info)
140
+ skin_hsl = rgb_to_hsl(r, g, b)
141
+
142
+ color_combos = get_color_combos_for_user(report, gender)
143
+ if not color_combos:
144
+ print("[ColorDebug] 沒有可用的顏色組合。")
145
+ return
146
+
147
+ scored = []
148
+ for top_zh, bottom_zh in color_combos:
149
+ c1 = COLOR_DB.get(top_zh)
150
+ c2 = COLOR_DB.get(bottom_zh)
151
+ if not c1 or not c2:
152
+ continue
153
+
154
+ hsl1 = c1["hsl"]
155
+ hsl2 = c2["hsl"]
156
+ s_sim, s_comp, s_cont = color_harmony_scores_for_combo([hsl1, hsl2])
157
+ s_skin = skin_compatibility_for_outfit(skin_hsl, [hsl1, hsl2])
158
+ neutral_bonus = 0.0
159
+ if _is_neutral_color(top_zh) or _is_neutral_color(bottom_zh):
160
+ neutral_bonus += NEUTRAL_COLOR_BONUS
161
+ if _is_neutral_color(top_zh) and _is_neutral_color(bottom_zh):
162
+ neutral_bonus += DOUBLE_NEUTRAL_EXTRA_BONUS
163
+
164
+ s_color = max(s_sim, s_comp, s_cont) + ALPHA_SKIN * s_skin + neutral_bonus
165
+ scored.append((s_color, top_zh, bottom_zh, c1["en"], c2["en"]))
166
+
167
+ if not scored:
168
+ print("[ColorDebug] 顏色組合分數計算失敗。")
169
+ return
170
+
171
+ scored.sort(key=lambda x: x[0], reverse=True)
172
+ print("[ColorDebug] Top 10 顏色組合(含分數):")
173
+ for rank, (score, top_zh, bottom_zh, top_en, bottom_en) in enumerate(scored[:20], start=1):
174
+ print(
175
+ f" {rank:02d}. ({top_zh}/{top_en}) + ({bottom_zh}/{bottom_en}) "
176
+ f"=> score={score:.2f}"
177
+ )
178
+
179
+
180
+ def main() -> int:
181
+ base_dir = Path(__file__).resolve().parent
182
+
183
+ parser = argparse.ArgumentParser(description="純 Python 測試:先判斷身形,再跑推薦,輸出到 output/")
184
+ parser.add_argument(
185
+ "input",
186
+ nargs="?",
187
+ help="request JSON 路徑(例如 saved_requests/request_xxx.json)。不填則自動抓 saved_requests/ 最新檔。",
188
+ )
189
+ args = parser.parse_args()
190
+
191
+ # 決定輸入檔
192
+ if args.input:
193
+ input_path = (base_dir / args.input).resolve() if not Path(args.input).is_absolute() else Path(args.input)
194
+ else:
195
+ input_path = pick_default_request_file(base_dir)
196
+
197
+ if not input_path.exists():
198
+ print(f"[Error] 找不到輸入檔:{input_path}")
199
+ return 2
200
+
201
+ print(f"[Test] 使用輸入檔:{input_path}")
202
+
203
+ # 讀 JSON
204
+ data = load_request_json(input_path)
205
+
206
+ # 解析 report / weather
207
+ if "report" in data and "weather" in data:
208
+ report = data["report"]
209
+ weather = data["weather"]
210
+ else:
211
+ raise ValueError("request JSON 必須包含 'report' 與 'weather' 兩個 key(最外層)")
212
+
213
+ if not isinstance(report, dict):
214
+ raise ValueError("'report' 必須是 dict")
215
+ if not isinstance(weather, dict):
216
+ raise ValueError("'weather' 必須是 dict")
217
+
218
+ # 方式 1:先做身形判斷(寫回 report)
219
+ attach_body_type(report)
220
+
221
+ # 額外印出顏色組合分數前 10 名
222
+ print_top10_color_combos(report)
223
+
224
+ # 跑推薦
225
+ result = run_recommend_model(report, weather)
226
+
227
+ # 輸出資料夾 output/
228
+ out_dir = base_dir / "output"
229
+ out_dir.mkdir(exist_ok=True)
230
+
231
+ # 輸出檔名:跟輸入檔對應
232
+ stem = input_path.stem # e.g. request_anonymous_1763...
233
+ out_path = out_dir / f"{stem}_output.json"
234
+
235
+ with out_path.open("w", encoding="utf-8") as f:
236
+ json.dump(result, f, ensure_ascii=False, indent=2)
237
+
238
+ print(f"[Test] 推薦結果已輸出:{out_path}")
239
+ print("[Test] 預覽(前幾項):")
240
+ # 簡單預覽
241
+ preview_items = list(result.items())[:3]
242
+ print(json.dumps(dict(preview_items), ensure_ascii=False, indent=2))
243
+
244
+ return 0
245
+
246
+
247
+ if __name__ == "__main__":
248
+ try:
249
+ raise SystemExit(main())
250
+ except Exception as e:
251
+ print(f"[Fatal] 測試失敗:{e}")
252
+ raise