DawnC commited on
Commit
d26f860
1 Parent(s): 348967e

Upload 3 files

Browse files
breed_recommendation.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sqlite3
3
+ import gradio as gr
4
+ from dog_database import get_dog_description, dog_data
5
+ from breed_health_info import breed_health_info
6
+ from breed_noise_info import breed_noise_info
7
+ from scoring_calculation_system import UserPreferences, calculate_compatibility_score
8
+ from recommendation_html_format import format_recommendation_html, get_breed_recommendations
9
+ from smart_breed_matcher import SmartBreedMatcher
10
+ from description_search_ui import create_description_search_tab
11
+
12
+ def create_recommendation_tab(UserPreferences, get_breed_recommendations, format_recommendation_html, history_component):
13
+
14
+ with gr.TabItem("Breed Recommendation"):
15
+ with gr.Tabs():
16
+ with gr.Tab("Find by Criteria"):
17
+ gr.HTML("""
18
+ <div style='
19
+ text-align: center;
20
+ padding: 20px 0;
21
+ margin: 15px 0;
22
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
23
+ border-radius: 10px;
24
+ '>
25
+ <p style='
26
+ font-size: 1.2em;
27
+ margin: 0;
28
+ padding: 0 20px;
29
+ line-height: 1.5;
30
+ background: linear-gradient(90deg, #4299e1, #48bb78);
31
+ -webkit-background-clip: text;
32
+ -webkit-text-fill-color: transparent;
33
+ font-weight: 600;
34
+ '>
35
+ Tell us about your lifestyle, and we'll recommend the perfect dog breeds for you!
36
+ </p>
37
+ </div>
38
+ """)
39
+
40
+ with gr.Row():
41
+ with gr.Column():
42
+ living_space = gr.Radio(
43
+ choices=["apartment", "house_small", "house_large"],
44
+ label="What type of living space do you have?",
45
+ info="Choose your current living situation",
46
+ value="apartment"
47
+ )
48
+
49
+ exercise_time = gr.Slider(
50
+ minimum=0,
51
+ maximum=180,
52
+ value=60,
53
+ label="Daily exercise time (minutes)",
54
+ info="Consider walks, play time, and training"
55
+ )
56
+
57
+ grooming_commitment = gr.Radio(
58
+ choices=["low", "medium", "high"],
59
+ label="Grooming commitment level",
60
+ info="Low: monthly, Medium: weekly, High: daily",
61
+ value="medium"
62
+ )
63
+
64
+ with gr.Column():
65
+ experience_level = gr.Radio(
66
+ choices=["beginner", "intermediate", "advanced"],
67
+ label="Dog ownership experience",
68
+ info="Be honest - this helps find the right match",
69
+ value="beginner"
70
+ )
71
+
72
+ has_children = gr.Checkbox(
73
+ label="Have children at home",
74
+ info="Helps recommend child-friendly breeds"
75
+ )
76
+
77
+ noise_tolerance = gr.Radio(
78
+ choices=["low", "medium", "high"],
79
+ label="Noise tolerance level",
80
+ info="Some breeds are more vocal than others",
81
+ value="medium"
82
+ )
83
+
84
+ get_recommendations_btn = gr.Button("Find My Perfect Match! 🔍", variant="primary")
85
+ recommendation_output = gr.HTML(label="Breed Recommendations")
86
+
87
+ with gr.Tab("Find by Description"):
88
+ description_input, description_search_btn, description_output, loading_msg = create_description_search_tab()
89
+
90
+
91
+ def on_find_match_click(*args):
92
+ try:
93
+ user_prefs = UserPreferences(
94
+ living_space=args[0],
95
+ exercise_time=args[1],
96
+ grooming_commitment=args[2],
97
+ experience_level=args[3],
98
+ has_children=args[4],
99
+ noise_tolerance=args[5],
100
+ space_for_play=True if args[0] != "apartment" else False,
101
+ other_pets=False,
102
+ climate="moderate",
103
+ health_sensitivity="medium", # 新增: 默認中等敏感度
104
+ barking_acceptance=args[5] # 使用 noise_tolerance 作為 barking_acceptance
105
+ )
106
+
107
+ recommendations = get_breed_recommendations(user_prefs, top_n=10)
108
+
109
+ history_results = [{
110
+ 'breed': rec['breed'],
111
+ 'rank': rec['rank'],
112
+ 'overall_score': rec['final_score'],
113
+ 'base_score': rec['base_score'],
114
+ 'bonus_score': rec['bonus_score'],
115
+ 'scores': rec['scores']
116
+ } for rec in recommendations]
117
+
118
+ # 保存到歷史記錄,也需要更新保存的偏好設定
119
+ history_component.save_search(
120
+ user_preferences={
121
+ 'living_space': args[0],
122
+ 'exercise_time': args[1],
123
+ 'grooming_commitment': args[2],
124
+ 'experience_level': args[3],
125
+ 'has_children': args[4],
126
+ 'noise_tolerance': args[5],
127
+ 'health_sensitivity': "medium",
128
+ 'barking_acceptance': args[5]
129
+ },
130
+ results=history_results
131
+ )
132
+
133
+ return format_recommendation_html(recommendations, is_description_search=False)
134
+
135
+ except Exception as e:
136
+ print(f"Error in find match: {str(e)}")
137
+ import traceback
138
+ print(traceback.format_exc())
139
+ return "Error getting recommendations"
140
+
141
+ def on_description_search(description: str):
142
+ try:
143
+ # 初始化匹配器
144
+ matcher = SmartBreedMatcher(dog_data)
145
+ breed_recommendations = matcher.match_user_preference(description, top_n=10)
146
+
147
+ # 從描述中提取用戶偏好
148
+ user_prefs = UserPreferences(
149
+ living_space="apartment" if any(word in description.lower()
150
+ for word in ["apartment", "flat", "condo"]) else "house_small",
151
+ exercise_time=120 if any(word in description.lower()
152
+ for word in ["active", "exercise", "running", "athletic", "high energy"]) else 60,
153
+ grooming_commitment="high" if any(word in description.lower()
154
+ for word in ["grooming", "brush", "maintain"]) else "medium",
155
+ experience_level="experienced" if any(word in description.lower()
156
+ for word in ["experienced", "trained", "professional"]) else "intermediate",
157
+ has_children=any(word in description.lower()
158
+ for word in ["children", "kids", "family", "child"]),
159
+ noise_tolerance="low" if any(word in description.lower()
160
+ for word in ["quiet", "peaceful", "silent"]) else "medium",
161
+ space_for_play=any(word in description.lower()
162
+ for word in ["yard", "garden", "outdoor", "space"]),
163
+ other_pets=any(word in description.lower()
164
+ for word in ["other pets", "cats", "dogs"]),
165
+ climate="moderate",
166
+ health_sensitivity="high" if any(word in description.lower()
167
+ for word in ["health", "medical", "sensitive"]) else "medium",
168
+ barking_acceptance="low" if any(word in description.lower()
169
+ for word in ["quiet", "no barking"]) else None
170
+ )
171
+
172
+ final_recommendations = []
173
+
174
+ for smart_rec in breed_recommendations:
175
+ breed_name = smart_rec['breed']
176
+ breed_info = get_dog_description(breed_name)
177
+ if not isinstance(breed_info, dict):
178
+ continue
179
+
180
+ # 獲取基礎分數
181
+ base_score = smart_rec.get('base_score', 0.7)
182
+ similarity = smart_rec.get('similarity', 0)
183
+ is_preferred = smart_rec.get('is_preferred', False)
184
+
185
+ bonus_reasons = []
186
+ bonus_score = 0
187
+
188
+ # 1. 尺寸評估
189
+ size = breed_info.get('Size', '')
190
+ if size in ['Small', 'Tiny']:
191
+ if "apartment" in description.lower():
192
+ bonus_score += 0.05
193
+ bonus_reasons.append("Suitable size for apartment (+5%)")
194
+ else:
195
+ bonus_score -= 0.25
196
+ bonus_reasons.append("Size too small (-25%)")
197
+ elif size == 'Medium':
198
+ bonus_score += 0.15
199
+ bonus_reasons.append("Ideal size (+15%)")
200
+ elif size == 'Large':
201
+ if "apartment" in description.lower():
202
+ bonus_score -= 0.05
203
+ bonus_reasons.append("May be too large for apartment (-5%)")
204
+ elif size == 'Giant':
205
+ bonus_score -= 0.20
206
+ bonus_reasons.append("Size too large (-20%)")
207
+
208
+ # 2. 運��需求評估
209
+ exercise_needs = breed_info.get('Exercise_Needs', '')
210
+ if any(word in description.lower() for word in ['active', 'energetic', 'running']):
211
+ if exercise_needs in ['High', 'Very High']:
212
+ bonus_score += 0.20
213
+ bonus_reasons.append("Exercise needs match (+20%)")
214
+ elif exercise_needs == 'Low':
215
+ bonus_score -= 0.15
216
+ bonus_reasons.append("Insufficient exercise level (-15%)")
217
+ else:
218
+ if exercise_needs == 'Moderate':
219
+ bonus_score += 0.10
220
+ bonus_reasons.append("Moderate exercise needs (+10%)")
221
+
222
+ # 3. 美容需求評估
223
+ grooming = breed_info.get('Grooming_Needs', '')
224
+ if user_prefs.grooming_commitment == "high":
225
+ if grooming == 'High':
226
+ bonus_score += 0.10
227
+ bonus_reasons.append("High grooming match (+10%)")
228
+ else:
229
+ if grooming == 'High':
230
+ bonus_score -= 0.15
231
+ bonus_reasons.append("High grooming needs (-15%)")
232
+ elif grooming == 'Low':
233
+ bonus_score += 0.10
234
+ bonus_reasons.append("Low grooming needs (+10%)")
235
+
236
+ # 4. 家庭適應性評估
237
+ if user_prefs.has_children:
238
+ if breed_info.get('Good_With_Children'):
239
+ bonus_score += 0.15
240
+ bonus_reasons.append("Excellent with children (+15%)")
241
+ temperament = breed_info.get('Temperament', '').lower()
242
+ if any(trait in temperament for trait in ['gentle', 'patient', 'friendly']):
243
+ bonus_score += 0.05
244
+ bonus_reasons.append("Family-friendly temperament (+5%)")
245
+
246
+ # 5. 噪音評估
247
+ if user_prefs.noise_tolerance == "low":
248
+ noise_level = breed_noise_info.get(breed_name, {}).get('noise_level', 'Unknown')
249
+ if noise_level == 'High':
250
+ bonus_score -= 0.10
251
+ bonus_reasons.append("High noise level (-10%)")
252
+ elif noise_level == 'Low':
253
+ bonus_score += 0.10
254
+ bonus_reasons.append("Low noise level (+10%)")
255
+
256
+ # 6. 健康考慮
257
+ if user_prefs.health_sensitivity == "high":
258
+ health_score = smart_rec.get('health_score', 0.5)
259
+ if health_score > 0.8:
260
+ bonus_score += 0.10
261
+ bonus_reasons.append("Excellent health score (+10%)")
262
+ elif health_score < 0.5:
263
+ bonus_score -= 0.10
264
+ bonus_reasons.append("Health concerns (-10%)")
265
+
266
+ # 7. 品種偏好獎勵
267
+ if is_preferred:
268
+ bonus_score += 0.15
269
+ bonus_reasons.append("Directly mentioned breed (+15%)")
270
+ elif similarity > 0.8:
271
+ bonus_score += 0.10
272
+ bonus_reasons.append("Very similar to preferred breed (+10%)")
273
+
274
+ # 計算最終分數
275
+ final_score = min(0.95, base_score + bonus_score)
276
+
277
+ space_score = _calculate_space_compatibility(
278
+ breed_info.get('Size', 'Medium'),
279
+ user_prefs.living_space
280
+ )
281
+
282
+ exercise_score = _calculate_exercise_compatibility(
283
+ breed_info.get('Exercise_Needs', 'Moderate'),
284
+ user_prefs.exercise_time
285
+ )
286
+
287
+ grooming_score = _calculate_grooming_compatibility(
288
+ breed_info.get('Grooming_Needs', 'Moderate'),
289
+ user_prefs.grooming_commitment
290
+ )
291
+
292
+ experience_score = _calculate_experience_compatibility(
293
+ breed_info.get('Care_Level', 'Moderate'),
294
+ user_prefs.experience_level
295
+ )
296
+
297
+ scores = {
298
+ 'overall': final_score,
299
+ 'space': space_score,
300
+ 'exercise': exercise_score,
301
+ 'grooming': grooming_score,
302
+ 'experience': experience_score,
303
+ 'noise': smart_rec.get('scores', {}).get('noise', 0.0),
304
+ 'health': smart_rec.get('health_score', 0.5),
305
+ 'temperament': smart_rec.get('scores', {}).get('temperament', 0.0)
306
+ }
307
+
308
+
309
+ final_recommendations.append({
310
+ 'rank': 0,
311
+ 'breed': breed_name,
312
+ 'scores': scores,
313
+ 'base_score': round(base_score, 4),
314
+ 'bonus_score': round(bonus_score, 4),
315
+ 'final_score': round(final_score, 4),
316
+ 'match_reason': ' • '.join(bonus_reasons) if bonus_reasons else "Standard match",
317
+ 'info': breed_info,
318
+ 'noise_info': breed_noise_info.get(breed_name, {}),
319
+ 'health_info': breed_health_info.get(breed_name, {})
320
+ })
321
+
322
+ # 根據最終分數排序
323
+ final_recommendations.sort(key=lambda x: (-x['final_score'], x['breed']))
324
+
325
+ # 更新排名
326
+ for i, rec in enumerate(final_recommendations, 1):
327
+ rec['rank'] = i
328
+
329
+ # 保存到歷史記錄
330
+ history_results = [{
331
+ 'breed': rec['breed'],
332
+ 'rank': rec['rank'],
333
+ 'final_score': rec['final_score']
334
+ } for rec in final_recommendations[:10]]
335
+
336
+ history_component.save_search(
337
+ user_preferences=None,
338
+ results=history_results,
339
+ search_type="description",
340
+ description=description
341
+ )
342
+
343
+ result = format_recommendation_html(final_recommendations, is_description_search=True)
344
+ return [gr.update(value=result), gr.update(visible=False)]
345
+
346
+ except Exception as e:
347
+ error_msg = f"Error processing your description. Details: {str(e)}"
348
+ return [gr.update(value=error_msg), gr.update(visible=False)]
349
+
350
+
351
+ def _calculate_space_compatibility(size: str, living_space: str) -> float:
352
+ """住宿空間適應性評分"""
353
+ if living_space == "apartment":
354
+ scores = {
355
+ 'Tiny': 0.6, # 公寓可以,但不是最佳
356
+ 'Small': 0.8, # 公寓較好
357
+ 'Medium': 1.0, # 最佳選擇
358
+ 'Medium-Large': 0.6, # 可能有點大
359
+ 'Large': 0.4, # 太大了
360
+ 'Giant': 0.2 # 不建議
361
+ }
362
+ else: # house
363
+ scores = {
364
+ 'Tiny': 0.4, # 房子太大了
365
+ 'Small': 0.6, # 可以但不是最佳
366
+ 'Medium': 0.8, # 不錯的選擇
367
+ 'Medium-Large': 1.0, # 最佳選擇
368
+ 'Large': 0.9, # 也很好
369
+ 'Giant': 0.7 # 可以考慮
370
+ }
371
+ return scores.get(size, 0.5)
372
+
373
+ def _calculate_exercise_compatibility(exercise_needs: str, exercise_time: int) -> float:
374
+ """運動需求相容性評分"""
375
+ # 轉換運動時間到評分標準
376
+ if exercise_time >= 120: # 高運動量
377
+ scores = {
378
+ 'Very High': 1.0,
379
+ 'High': 0.8,
380
+ 'Moderate': 0.5,
381
+ 'Low': 0.2
382
+ }
383
+ elif exercise_time >= 60: # 中等運動量
384
+ scores = {
385
+ 'Very High': 0.5,
386
+ 'High': 0.7,
387
+ 'Moderate': 1.0,
388
+ 'Low': 0.8
389
+ }
390
+ else: # 低運動量
391
+ scores = {
392
+ 'Very High': 0.2,
393
+ 'High': 0.4,
394
+ 'Moderate': 0.7,
395
+ 'Low': 1.0
396
+ }
397
+ return scores.get(exercise_needs, 0.5)
398
+
399
+ def _calculate_grooming_compatibility(grooming_needs: str, grooming_commitment: str) -> float:
400
+ """美容需求相容性評分"""
401
+ if grooming_commitment == "high":
402
+ scores = {
403
+ 'High': 1.0,
404
+ 'Moderate': 0.8,
405
+ 'Low': 0.5
406
+ }
407
+ elif grooming_commitment == "medium":
408
+ scores = {
409
+ 'High': 0.6,
410
+ 'Moderate': 1.0,
411
+ 'Low': 0.8
412
+ }
413
+ else: # low
414
+ scores = {
415
+ 'High': 0.3,
416
+ 'Moderate': 0.6,
417
+ 'Low': 1.0
418
+ }
419
+ return scores.get(grooming_needs, 0.5)
420
+
421
+ def _calculate_experience_compatibility(care_level: str, experience_level: str) -> float:
422
+ if experience_level == "experienced":
423
+ care_scores = {
424
+ 'High': 1.0,
425
+ 'Moderate': 0.8,
426
+ 'Low': 0.6
427
+ }
428
+ elif experience_level == "intermediate":
429
+ care_scores = {
430
+ 'High': 0.6,
431
+ 'Moderate': 1.0,
432
+ 'Low': 0.8
433
+ }
434
+ else: # beginner
435
+ care_scores = {
436
+ 'High': 0.3,
437
+ 'Moderate': 0.7,
438
+ 'Low': 1.0
439
+ }
440
+ return care_scores.get(care_level, 0.7)
441
+
442
+ def show_loading():
443
+ return [gr.update(value=""), gr.update(visible=True)]
444
+
445
+
446
+ get_recommendations_btn.click(
447
+ fn=on_find_match_click,
448
+ inputs=[
449
+ living_space,
450
+ exercise_time,
451
+ grooming_commitment,
452
+ experience_level,
453
+ has_children,
454
+ noise_tolerance
455
+ ],
456
+ outputs=recommendation_output
457
+ )
458
+
459
+ description_search_btn.click(
460
+ fn=show_loading, # 先顯示加載消息
461
+ outputs=[description_output, loading_msg]
462
+ ).then( # 然後執行搜索
463
+ fn=on_description_search,
464
+ inputs=[description_input],
465
+ outputs=[description_output, loading_msg]
466
+ )
467
+
468
+ return {
469
+ 'living_space': living_space,
470
+ 'exercise_time': exercise_time,
471
+ 'grooming_commitment': grooming_commitment,
472
+ 'experience_level': experience_level,
473
+ 'has_children': has_children,
474
+ 'noise_tolerance': noise_tolerance,
475
+ 'get_recommendations_btn': get_recommendations_btn,
476
+ 'recommendation_output': recommendation_output,
477
+ 'description_input': description_input,
478
+ 'description_search_btn': description_search_btn,
479
+ 'description_output': description_output
480
+ }
recommendation_html_format.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from breed_health_info import breed_health_info, default_health_note
3
+ from breed_noise_info import breed_noise_info
4
+ from dog_database import get_dog_description
5
+ from scoring_calculation_system import calculate_compatibility_score
6
+
7
+ def format_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str:
8
+ """將推薦結果格式化為HTML"""
9
+ def _convert_to_display_score(score: float, score_type: str = None) -> int:
10
+ """
11
+ 更改為生成更明顯差異的顯示分數
12
+ """
13
+ try:
14
+ # 基礎分數轉換(保持相對關係但擴大差異)
15
+ if score_type == 'bonus': # Breed Bonus 使用不同的轉換邏輯
16
+ base_score = 35 + (score * 60) # 35-95 範圍,差異更大
17
+ else:
18
+ # 其他類型的分數轉換
19
+ if score <= 0.3:
20
+ base_score = 40 + (score * 45) # 40-53.5 範圍
21
+ elif score <= 0.6:
22
+ base_score = 55 + ((score - 0.3) * 55) # 55-71.5 範圍
23
+ elif score <= 0.8:
24
+ base_score = 72 + ((score - 0.6) * 60) # 72-84 範圍
25
+ else:
26
+ base_score = 85 + ((score - 0.8) * 50) # 85-95 範圍
27
+
28
+ # 添加不規則的微調,但保持相對關係
29
+ import random
30
+ if score_type == 'bonus':
31
+ adjustment = random.uniform(-2, 2)
32
+ else:
33
+ # 根據分數範圍決定調整幅度
34
+ if score > 0.8:
35
+ adjustment = random.uniform(-3, 3)
36
+ elif score > 0.6:
37
+ adjustment = random.uniform(-4, 4)
38
+ else:
39
+ adjustment = random.uniform(-2, 2)
40
+
41
+ final_score = base_score + adjustment
42
+
43
+ # 確保最終分數在合理範圍內並避免5的倍數
44
+ final_score = min(95, max(40, final_score))
45
+ rounded_score = round(final_score)
46
+ if rounded_score % 5 == 0:
47
+ rounded_score += random.choice([-1, 1])
48
+
49
+ return rounded_score
50
+
51
+ except Exception as e:
52
+ print(f"Error in convert_to_display_score: {str(e)}")
53
+ return 70
54
+
55
+ def _generate_progress_bar(score: float) -> float:
56
+ """生成非線性的進度條寬度"""
57
+ if score <= 0.3:
58
+ width = 30 + (score / 0.3) * 20
59
+ elif score <= 0.6:
60
+ width = 50 + ((score - 0.3) / 0.3) * 20
61
+ elif score <= 0.8:
62
+ width = 70 + ((score - 0.6) / 0.2) * 15
63
+ else:
64
+ width = 85 + ((score - 0.8) / 0.2) * 15
65
+
66
+ import random
67
+ width += random.uniform(-2, 2)
68
+ return min(100, max(20, width))
69
+
70
+ html_content = "<div class='recommendations-container'>"
71
+
72
+ for rec in recommendations:
73
+ breed = rec['breed']
74
+ scores = rec['scores']
75
+ info = rec['info']
76
+ rank = rec.get('rank', 0)
77
+ final_score = rec.get('final_score', scores['overall'])
78
+ bonus_score = rec.get('bonus_score', 0)
79
+
80
+ if is_description_search:
81
+ display_scores = {
82
+ 'space': _convert_to_display_score(scores['space'], 'space'),
83
+ 'exercise': _convert_to_display_score(scores['exercise'], 'exercise'),
84
+ 'grooming': _convert_to_display_score(scores['grooming'], 'grooming'),
85
+ 'experience': _convert_to_display_score(scores['experience'], 'experience'),
86
+ 'noise': _convert_to_display_score(scores['noise'], 'noise')
87
+ }
88
+ else:
89
+ display_scores = scores # 圖片識別使用原始分數
90
+
91
+ progress_bars = {
92
+ 'space': _generate_progress_bar(scores['space']),
93
+ 'exercise': _generate_progress_bar(scores['exercise']),
94
+ 'grooming': _generate_progress_bar(scores['grooming']),
95
+ 'experience': _generate_progress_bar(scores['experience']),
96
+ 'noise': _generate_progress_bar(scores['noise'])
97
+ }
98
+
99
+ health_info = breed_health_info.get(breed, {"health_notes": default_health_note})
100
+ noise_info = breed_noise_info.get(breed, {
101
+ "noise_notes": "Noise information not available",
102
+ "noise_level": "Unknown",
103
+ "source": "N/A"
104
+ })
105
+
106
+ # 解析噪音資訊
107
+ noise_notes = noise_info.get('noise_notes', '').split('\n')
108
+ noise_characteristics = []
109
+ barking_triggers = []
110
+ noise_level = ''
111
+
112
+ current_section = None
113
+ for line in noise_notes:
114
+ line = line.strip()
115
+ if 'Typical noise characteristics:' in line:
116
+ current_section = 'characteristics'
117
+ elif 'Noise level:' in line:
118
+ noise_level = line.replace('Noise level:', '').strip()
119
+ elif 'Barking triggers:' in line:
120
+ current_section = 'triggers'
121
+ elif line.startswith('•'):
122
+ if current_section == 'characteristics':
123
+ noise_characteristics.append(line[1:].strip())
124
+ elif current_section == 'triggers':
125
+ barking_triggers.append(line[1:].strip())
126
+
127
+ # 生成特徵和觸發因素的HTML
128
+ noise_characteristics_html = '\n'.join([f'<li>{item}</li>' for item in noise_characteristics])
129
+ barking_triggers_html = '\n'.join([f'<li>{item}</li>' for item in barking_triggers])
130
+
131
+ # 處理健康資訊
132
+ health_notes = health_info.get('health_notes', '').split('\n')
133
+ health_considerations = []
134
+ health_screenings = []
135
+
136
+ current_section = None
137
+ for line in health_notes:
138
+ line = line.strip()
139
+ if 'Common breed-specific health considerations' in line:
140
+ current_section = 'considerations'
141
+ elif 'Recommended health screenings:' in line:
142
+ current_section = 'screenings'
143
+ elif line.startswith('•'):
144
+ if current_section == 'considerations':
145
+ health_considerations.append(line[1:].strip())
146
+ elif current_section == 'screenings':
147
+ health_screenings.append(line[1:].strip())
148
+
149
+ health_considerations_html = '\n'.join([f'<li>{item}</li>' for item in health_considerations])
150
+ health_screenings_html = '\n'.join([f'<li>{item}</li>' for item in health_screenings])
151
+
152
+ # 獎勵原因計算
153
+ bonus_reasons = []
154
+ temperament = info.get('Temperament', '').lower()
155
+ if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
156
+ bonus_reasons.append("Positive temperament traits")
157
+ if info.get('Good with Children') == 'Yes':
158
+ bonus_reasons.append("Excellent with children")
159
+ try:
160
+ lifespan = info.get('Lifespan', '10-12 years')
161
+ years = int(lifespan.split('-')[0])
162
+ if years > 12:
163
+ bonus_reasons.append("Above-average lifespan")
164
+ except:
165
+ pass
166
+
167
+ html_content += f"""
168
+ <div class="dog-info-card recommendation-card">
169
+ <div class="breed-info">
170
+ <h2 class="section-title">
171
+ <span class="icon">🏆</span> #{rank} {breed.replace('_', ' ')}
172
+ <span class="score-badge">
173
+ Overall Match: {final_score*100:.1f}%
174
+ </span>
175
+ </h2>
176
+ <div class="compatibility-scores">
177
+ <div class="score-item">
178
+ <span class="label">Space Compatibility:</span>
179
+ <div class="progress-bar">
180
+ <div class="progress" style="width: {progress_bars['space']}%"></div>
181
+ </div>
182
+ <span class="percentage">{display_scores['space'] if is_description_search else scores['space']*100:.1f}%</span>
183
+ </div>
184
+ <div class="score-item">
185
+ <span class="label">Exercise Match:</span>
186
+ <div class="progress-bar">
187
+ <div class="progress" style="width: {progress_bars['exercise']}%"></div>
188
+ </div>
189
+ <span class="percentage">{display_scores['exercise'] if is_description_search else scores['exercise']*100:.1f}%</span>
190
+ </div>
191
+ <div class="score-item">
192
+ <span class="label">Grooming Match:</span>
193
+ <div class="progress-bar">
194
+ <div class="progress" style="width: {progress_bars['grooming']}%"></div>
195
+ </div>
196
+ <span class="percentage">{display_scores['grooming'] if is_description_search else scores['grooming']*100:.1f}%</span>
197
+ </div>
198
+ <div class="score-item">
199
+ <span class="label">Experience Match:</span>
200
+ <div class="progress-bar">
201
+ <div class="progress" style="width: {progress_bars['experience']}%"></div>
202
+ </div>
203
+ <span class="percentage">{display_scores['experience'] if is_description_search else scores['experience']*100:.1f}%</span>
204
+ </div>
205
+ <div class="score-item">
206
+ <span class="label">
207
+ Noise Compatibility:
208
+ <span class="tooltip">
209
+ <span class="tooltip-icon">ⓘ</span>
210
+ <span class="tooltip-text">
211
+ <strong>Noise Compatibility Score:</strong><br>
212
+ • Based on your noise tolerance preference<br>
213
+ • Considers breed's typical noise level<br>
214
+ • Accounts for living environment
215
+ </span>
216
+ </span>
217
+ </span>
218
+ <div class="progress-bar">
219
+ <div class="progress" style="width: {progress_bars['noise']}%"></div>
220
+ </div>
221
+ <span class="percentage">{display_scores['noise'] if is_description_search else scores['noise']*100:.1f}%</span>
222
+ </div>
223
+ {f'''
224
+ <div class="score-item bonus-score">
225
+ <span class="label">
226
+ Breed Bonus:
227
+ <span class="tooltip">
228
+ <span class="tooltip-icon">ⓘ</span>
229
+ <span class="tooltip-text">
230
+ <strong>Breed Bonus Points:</strong><br>
231
+ • {('<br>• '.join(bonus_reasons)) if bonus_reasons else 'No additional bonus points'}<br>
232
+ <br>
233
+ <strong>Bonus Factors Include:</strong><br>
234
+ • Friendly temperament<br>
235
+ • Child compatibility<br>
236
+ • Longer lifespan<br>
237
+ • Living space adaptability
238
+ </span>
239
+ </span>
240
+ </span>
241
+ <div class="progress-bar">
242
+ <div class="progress" style="width: {progress_bars.get('bonus', bonus_score*100)}%"></div>
243
+ </div>
244
+ <span class="percentage">{bonus_score*100:.1f}%</span>
245
+ </div>
246
+ ''' if bonus_score > 0 else ''}
247
+ </div>
248
+ <div class="breed-details-section">
249
+ <h3 class="subsection-title">
250
+ <span class="icon">📋</span> Breed Details
251
+ </h3>
252
+ <div class="details-grid">
253
+ <div class="detail-item">
254
+ <span class="tooltip">
255
+ <span class="icon">📏</span>
256
+ <span class="label">Size:</span>
257
+ <span class="tooltip-icon">ⓘ</span>
258
+ <span class="tooltip-text">
259
+ <strong>Size Categories:</strong><br>
260
+ • Small: Under 20 pounds<br>
261
+ • Medium: 20-60 pounds<br>
262
+ • Large: Over 60 pounds
263
+ </span>
264
+ <span class="value">{info['Size']}</span>
265
+ </span>
266
+ </div>
267
+ <div class="detail-item">
268
+ <span class="tooltip">
269
+ <span class="icon">🏃</span>
270
+ <span class="label">Exercise Needs:</span>
271
+ <span class="tooltip-icon">ⓘ</span>
272
+ <span class="tooltip-text">
273
+ <strong>Exercise Needs:</strong><br>
274
+ • Low: Short walks<br>
275
+ • Moderate: 1-2 hours daily<br>
276
+ • High: 2+ hours daily<br>
277
+ • Very High: Constant activity
278
+ </span>
279
+ <span class="value">{info['Exercise Needs']}</span>
280
+ </span>
281
+ </div>
282
+ <div class="detail-item">
283
+ <span class="tooltip">
284
+ <span class="icon">👨‍👩‍👧‍👦</span>
285
+ <span class="label">Good with Children:</span>
286
+ <span class="tooltip-icon">ⓘ</span>
287
+ <span class="tooltip-text">
288
+ <strong>Child Compatibility:</strong><br>
289
+ • Yes: Excellent with kids<br>
290
+ • Moderate: Good with older children<br>
291
+ • No: Better for adult households
292
+ </span>
293
+ <span class="value">{info['Good with Children']}</span>
294
+ </span>
295
+ </div>
296
+ <div class="detail-item">
297
+ <span class="tooltip">
298
+ <span class="icon">⏳</span>
299
+ <span class="label">Lifespan:</span>
300
+ <span class="tooltip-icon">ⓘ</span>
301
+ <span class="tooltip-text">
302
+ <strong>Average Lifespan:</strong><br>
303
+ • Short: 6-8 years<br>
304
+ • Average: 10-15 years<br>
305
+ • Long: 12-20 years<br>
306
+ • Varies by size: Larger breeds typically have shorter lifespans
307
+ </span>
308
+ </span>
309
+ <span class="value">{info['Lifespan']}</span>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ <div class="description-section">
314
+ <h3 class="subsection-title">
315
+ <span class="icon">📝</span> Description
316
+ </h3>
317
+ <p class="description-text">{info.get('Description', '')}</p>
318
+ </div>
319
+ <div class="noise-section">
320
+ <h3 class="section-header">
321
+ <span class="icon">🔊</span> Noise Behavior
322
+ <span class="tooltip">
323
+ <span class="tooltip-icon">ⓘ</span>
324
+ <span class="tooltip-text">
325
+ <strong>Noise Behavior:</strong><br>
326
+ • Typical vocalization patterns<br>
327
+ • Common triggers and frequency<br>
328
+ • Based on breed characteristics
329
+ </span>
330
+ </span>
331
+ </h3>
332
+ <div class="noise-info">
333
+ <div class="noise-details">
334
+ <h4 class="section-header">Typical noise characteristics:</h4>
335
+ <div class="characteristics-list">
336
+ <div class="list-item">Moderate to high barker</div>
337
+ <div class="list-item">Alert watch dog</div>
338
+ <div class="list-item">Attention-seeking barks</div>
339
+ <div class="list-item">Social vocalizations</div>
340
+ </div>
341
+
342
+ <div class="noise-level-display">
343
+ <h4 class="section-header">Noise level:</h4>
344
+ <div class="level-indicator">
345
+ <span class="level-text">Moderate-High</span>
346
+ <div class="level-bars">
347
+ <span class="bar"></span>
348
+ <span class="bar"></span>
349
+ <span class="bar"></span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <h4 class="section-header">Barking triggers:</h4>
355
+ <div class="triggers-list">
356
+ <div class="list-item">Separation anxiety</div>
357
+ <div class="list-item">Attention needs</div>
358
+ <div class="list-item">Strange noises</div>
359
+ <div class="list-item">Excitement</div>
360
+ </div>
361
+ </div>
362
+ <div class="noise-disclaimer">
363
+ <p class="disclaimer-text source-text">Source: Compiled from various breed behavior resources, 2024</p>
364
+ <p class="disclaimer-text">Individual dogs may vary in their vocalization patterns.</p>
365
+ <p class="disclaimer-text">Training can significantly influence barking behavior.</p>
366
+ <p class="disclaimer-text">Environmental factors may affect noise levels.</p>
367
+ </div>
368
+ </div>
369
+ </div>
370
+
371
+ <div class="health-section">
372
+ <h3 class="section-header">
373
+ <span class="icon">🏥</span> Health Insights
374
+ <span class="tooltip">
375
+ <span class="tooltip-icon">ⓘ</span>
376
+ <span class="tooltip-text">
377
+ Health information is compiled from multiple sources including veterinary resources, breed guides, and international canine health databases.
378
+ Each dog is unique and may vary from these general guidelines.
379
+ </span>
380
+ </span>
381
+ </h3>
382
+ <div class="health-info">
383
+ <div class="health-details">
384
+ <div class="health-block">
385
+ <h4 class="section-header">Common breed-specific health considerations:</h4>
386
+ <div class="health-grid">
387
+ <div class="health-item">Patellar luxation</div>
388
+ <div class="health-item">Progressive retinal atrophy</div>
389
+ <div class="health-item">Von Willebrand's disease</div>
390
+ <div class="health-item">Open fontanel</div>
391
+ </div>
392
+ </div>
393
+
394
+ <div class="health-block">
395
+ <h4 class="section-header">Recommended health screenings:</h4>
396
+ <div class="health-grid">
397
+ <div class="health-item screening">Patella evaluation</div>
398
+ <div class="health-item screening">Eye examination</div>
399
+ <div class="health-item screening">Blood clotting tests</div>
400
+ <div class="health-item screening">Skull development monitoring</div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+ <div class="health-disclaimer">
405
+ <p class="disclaimer-text source-text">Source: Compiled from various veterinary and breed information resources, 2024</p>
406
+ <p class="disclaimer-text">This information is for reference only and based on breed tendencies.</p>
407
+ <p class="disclaimer-text">Each dog is unique and may not develop any or all of these conditions.</p>
408
+ <p class="disclaimer-text">Always consult with qualified veterinarians for professional advice.</p>
409
+ </div>
410
+ </div>
411
+ </div>
412
+
413
+ <div class="action-section">
414
+ <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-')}/"
415
+ target="_blank"
416
+ class="akc-button">
417
+ <span class="icon">🌐</span>
418
+ Learn more about {breed.replace('_', ' ')} on AKC website
419
+ </a>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ """
424
+
425
+ html_content += "</div>"
426
+ return html_content
427
+
428
+ def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 10) -> List[Dict]:
429
+ """基於使用者偏好推薦狗品種,確保正確的分數排序"""
430
+ print("Starting get_breed_recommendations")
431
+ recommendations = []
432
+ seen_breeds = set()
433
+
434
+ try:
435
+ # 獲取所有品種
436
+ conn = sqlite3.connect('animal_detector.db')
437
+ cursor = conn.cursor()
438
+ cursor.execute("SELECT Breed FROM AnimalCatalog")
439
+ all_breeds = cursor.fetchall()
440
+ conn.close()
441
+
442
+ # 收集所有品種的分數
443
+ for breed_tuple in all_breeds:
444
+ breed = breed_tuple[0]
445
+ base_breed = breed.split('(')[0].strip()
446
+
447
+ if base_breed in seen_breeds:
448
+ continue
449
+ seen_breeds.add(base_breed)
450
+
451
+ # 獲取品種資訊
452
+ breed_info = get_dog_description(breed)
453
+ if not isinstance(breed_info, dict):
454
+ continue
455
+
456
+ # 獲取噪音資訊
457
+ noise_info = breed_noise_info.get(breed, {
458
+ "noise_notes": "Noise information not available",
459
+ "noise_level": "Unknown",
460
+ "source": "N/A"
461
+ })
462
+
463
+ # 將噪音資訊整合到品種資訊中
464
+ breed_info['noise_info'] = noise_info
465
+
466
+ # 計算基礎相容性分數
467
+ compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)
468
+
469
+ # 計算品種特定加分
470
+ breed_bonus = 0.0
471
+
472
+ # 壽命加分
473
+ try:
474
+ lifespan = breed_info.get('Lifespan', '10-12 years')
475
+ years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
476
+ longevity_bonus = min(0.02, (max(years) - 10) * 0.005)
477
+ breed_bonus += longevity_bonus
478
+ except:
479
+ pass
480
+
481
+ # 性格特徵加分
482
+ temperament = breed_info.get('Temperament', '').lower()
483
+ positive_traits = ['friendly', 'gentle', 'affectionate', 'intelligent']
484
+ negative_traits = ['aggressive', 'stubborn', 'dominant']
485
+
486
+ breed_bonus += sum(0.01 for trait in positive_traits if trait in temperament)
487
+ breed_bonus -= sum(0.01 for trait in negative_traits if trait in temperament)
488
+
489
+ # 與孩童相容性加分
490
+ if user_prefs.has_children:
491
+ if breed_info.get('Good with Children') == 'Yes':
492
+ breed_bonus += 0.02
493
+ elif breed_info.get('Good with Children') == 'No':
494
+ breed_bonus -= 0.03
495
+
496
+ # 噪音相關加分
497
+ if user_prefs.noise_tolerance == 'low':
498
+ if noise_info['noise_level'].lower() == 'high':
499
+ breed_bonus -= 0.03
500
+ elif noise_info['noise_level'].lower() == 'low':
501
+ breed_bonus += 0.02
502
+ elif user_prefs.noise_tolerance == 'high':
503
+ if noise_info['noise_level'].lower() == 'high':
504
+ breed_bonus += 0.01
505
+
506
+ # 計算最終分數
507
+ breed_bonus = round(breed_bonus, 4)
508
+ final_score = round(compatibility_scores['overall'] + breed_bonus, 4)
509
+
510
+ recommendations.append({
511
+ 'breed': breed,
512
+ 'base_score': round(compatibility_scores['overall'], 4),
513
+ 'bonus_score': round(breed_bonus, 4),
514
+ 'final_score': final_score,
515
+ 'scores': compatibility_scores,
516
+ 'info': breed_info,
517
+ 'noise_info': noise_info # 添加噪音資訊到推薦結果
518
+ })
519
+ # 嚴格按照 final_score 排序
520
+ recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed'] )) # 負號使其降序排列,並確保4位小數
521
+
522
+ # 選擇前N名並確保正確排序
523
+ final_recommendations = []
524
+ last_score = None
525
+ rank = 1
526
+
527
+ for rec in recommendations:
528
+ if len(final_recommendations) >= top_n:
529
+ break
530
+
531
+ current_score = rec['final_score']
532
+
533
+ # 確保分數遞減
534
+ if last_score is not None and current_score > last_score:
535
+ continue
536
+
537
+ # 添加排名資訊
538
+ rec['rank'] = rank
539
+ final_recommendations.append(rec)
540
+
541
+ last_score = current_score
542
+ rank += 1
543
+
544
+ # 驗證最終排序
545
+ for i in range(len(final_recommendations)-1):
546
+ current = final_recommendations[i]
547
+ next_rec = final_recommendations[i+1]
548
+
549
+ if current['final_score'] < next_rec['final_score']:
550
+ print(f"Warning: Sorting error detected!")
551
+ print(f"#{i+1} {current['breed']}: {current['final_score']}")
552
+ print(f"#{i+2} {next_rec['breed']}: {next_rec['final_score']}")
553
+
554
+ # 交換位置
555
+ final_recommendations[i], final_recommendations[i+1] = \
556
+ final_recommendations[i+1], final_recommendations[i]
557
+
558
+ # 打印最終結果以供驗證
559
+ print("\nFinal Rankings:")
560
+ for rec in final_recommendations:
561
+ print(f"#{rec['rank']} {rec['breed']}")
562
+ print(f"Base Score: {rec['base_score']:.4f}")
563
+ print(f"Bonus: {rec['bonus_score']:.4f}")
564
+ print(f"Final Score: {rec['final_score']:.4f}\n")
565
+
566
+ return final_recommendations
567
+
568
+ except Exception as e:
569
+ print(f"Error in get_breed_recommendations: {str(e)}")
570
+ print(f"Traceback: {traceback.format_exc()}")
571
+ return []
smart_breed_matcher.py ADDED
@@ -0,0 +1,962 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import re
3
+ import numpy as np
4
+ from typing import List, Dict, Tuple, Optional
5
+ from dataclasses import dataclass
6
+ from breed_health_info import breed_health_info
7
+ from breed_noise_info import breed_noise_info
8
+ from dog_database import dog_data
9
+ from scoring_calculation_system import UserPreferences
10
+ from sentence_transformers import SentenceTransformer, util
11
+
12
+ class SmartBreedMatcher:
13
+ def __init__(self, dog_data: List[Tuple]):
14
+ self.dog_data = dog_data
15
+ self.model = SentenceTransformer('all-mpnet-base-v2')
16
+ self._embedding_cache = {}
17
+ self._clear_cache()
18
+
19
+ def _clear_cache(self):
20
+ self._embedding_cache = {}
21
+
22
+
23
+ def _get_cached_embedding(self, text: str) -> torch.Tensor:
24
+ if text not in self._embedding_cache:
25
+ self._embedding_cache[text] = self.model.encode(text)
26
+ return self._embedding_cache[text]
27
+
28
+ def _categorize_breeds(self) -> Dict:
29
+ """自動將狗品種分類"""
30
+ categories = {
31
+ 'working_dogs': [],
32
+ 'herding_dogs': [],
33
+ 'hunting_dogs': [],
34
+ 'companion_dogs': [],
35
+ 'guard_dogs': []
36
+ }
37
+
38
+ for breed_info in self.dog_data:
39
+ description = breed_info[9].lower()
40
+ temperament = breed_info[4].lower()
41
+
42
+ # 根據描述和性格特徵自動分類
43
+ if any(word in description for word in ['herding', 'shepherd', 'cattle', 'flock']):
44
+ categories['herding_dogs'].append(breed_info[1])
45
+ elif any(word in description for word in ['hunting', 'hunt', 'retriever', 'pointer']):
46
+ categories['hunting_dogs'].append(breed_info[1])
47
+ elif any(word in description for word in ['companion', 'toy', 'family', 'lap']):
48
+ categories['companion_dogs'].append(breed_info[1])
49
+ elif any(word in description for word in ['guard', 'protection', 'watchdog']):
50
+ categories['guard_dogs'].append(breed_info[1])
51
+ elif any(word in description for word in ['working', 'draft', 'cart']):
52
+ categories['working_dogs'].append(breed_info[1])
53
+
54
+ return categories
55
+
56
+ def find_similar_breeds(self, breed_name: str, top_n: int = 5) -> List[Tuple[str, float]]:
57
+ """
58
+ 找出與指定品種最相似的其他品種
59
+
60
+ Args:
61
+ breed_name: 目標品種名稱
62
+ top_n: 返回的相似品種數量
63
+
64
+ Returns:
65
+ List[Tuple[str, float]]: 相似品種列表,包含品種名稱和相似度分數
66
+ """
67
+ try:
68
+ target_breed = next((breed for breed in self.dog_data if breed[1] == breed_name), None)
69
+ if not target_breed:
70
+ return []
71
+
72
+ # 獲取完整的目標品種特徵
73
+ target_features = {
74
+ 'breed_name': target_breed[1],
75
+ 'size': target_breed[2],
76
+ 'temperament': target_breed[4],
77
+ 'exercise': target_breed[7],
78
+ 'grooming': target_breed[8],
79
+ 'description': target_breed[9],
80
+ 'good_with_children': target_breed[6] # 添加這個特徵
81
+ }
82
+
83
+ similarities = []
84
+ for breed in self.dog_data:
85
+ if breed[1] != breed_name:
86
+ breed_features = {
87
+ 'breed_name': breed[1],
88
+ 'size': breed[2],
89
+ 'temperament': breed[4],
90
+ 'exercise': breed[7],
91
+ 'grooming': breed[8],
92
+ 'description': breed[9],
93
+ 'good_with_children': breed[6] # 添加這個特徵
94
+ }
95
+
96
+ try:
97
+ similarity_score = self._calculate_breed_similarity(target_features, breed_features)
98
+ # 確保分數在有效範圍內
99
+ similarity_score = min(1.0, max(0.0, similarity_score))
100
+ similarities.append((breed[1], similarity_score))
101
+ except Exception as e:
102
+ print(f"Error calculating similarity for {breed[1]}: {str(e)}")
103
+ continue
104
+
105
+ # 根據相似度排序並返回前N個
106
+ return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_n]
107
+
108
+ except Exception as e:
109
+ print(f"Error in find_similar_breeds: {str(e)}")
110
+ return []
111
+
112
+
113
+ def _calculate_breed_similarity(self, breed1_features: Dict, breed2_features: Dict, weights: Dict[str, float]) -> float:
114
+ try:
115
+ # 1. 基礎相似度計算
116
+ size_similarity = self._calculate_size_similarity_enhanced(
117
+ breed1_features.get('size', 'Medium'),
118
+ breed2_features.get('size', 'Medium'),
119
+ breed2_features.get('description', '')
120
+ )
121
+
122
+ exercise_similarity = self._calculate_exercise_similarity_enhanced(
123
+ breed1_features.get('exercise', 'Moderate'),
124
+ breed2_features.get('exercise', 'Moderate')
125
+ )
126
+
127
+ # 性格相似度
128
+ temp1_embedding = self._get_cached_embedding(breed1_features.get('temperament', ''))
129
+ temp2_embedding = self._get_cached_embedding(breed2_features.get('temperament', ''))
130
+ temperament_similarity = float(util.pytorch_cos_sim(temp1_embedding, temp2_embedding))
131
+
132
+ # 其他相似度
133
+ grooming_similarity = self._calculate_grooming_similarity(
134
+ breed1_features.get('breed_name', ''),
135
+ breed2_features.get('breed_name', '')
136
+ )
137
+
138
+ health_similarity = self._calculate_health_score_similarity(
139
+ breed1_features.get('breed_name', ''),
140
+ breed2_features.get('breed_name', '')
141
+ )
142
+
143
+ noise_similarity = self._calculate_noise_similarity(
144
+ breed1_features.get('breed_name', ''),
145
+ breed2_features.get('breed_name', '')
146
+ )
147
+
148
+ # 2. 關鍵特徵評分
149
+ feature_scores = {}
150
+ for feature, similarity in {
151
+ 'size': size_similarity,
152
+ 'exercise': exercise_similarity,
153
+ 'temperament': temperament_similarity,
154
+ 'grooming': grooming_similarity,
155
+ 'health': health_similarity,
156
+ 'noise': noise_similarity
157
+ }.items():
158
+ # 根據權重調整每個特徵分數
159
+ importance = weights.get(feature, 0.1)
160
+ if importance > 0.3: # 高權重特徵
161
+ if similarity < 0.5: # 若關鍵特徵匹配度低
162
+ feature_scores[feature] = similarity * 0.5 # 大幅降低分數
163
+ else:
164
+ feature_scores[feature] = similarity * 1.2 # 提高匹配度好的分數
165
+ else: # 一般特徵
166
+ feature_scores[feature] = similarity
167
+
168
+ # 3. 計算最終相似度
169
+ weighted_sum = 0
170
+ weight_sum = 0
171
+ for feature, score in feature_scores.items():
172
+ feature_weight = weights.get(feature, 0.1)
173
+ weighted_sum += score * feature_weight
174
+ weight_sum += feature_weight
175
+
176
+ final_similarity = weighted_sum / weight_sum if weight_sum > 0 else 0.5
177
+
178
+ return min(1.0, max(0.2, final_similarity)) # 設定最低分數為0.2
179
+
180
+ except Exception as e:
181
+ print(f"Error in calculate_breed_similarity: {str(e)}")
182
+ return 0.5
183
+
184
+ def get_breed_characteristics_score(self, breed_features: Dict, description: str) -> float:
185
+ score = 1.0
186
+ description_lower = description.lower()
187
+ breed_score_multipliers = []
188
+
189
+ # 運動需求評估
190
+ exercise_needs = breed_features.get('exercise', 'Moderate')
191
+ exercise_keywords = ['active', 'running', 'energetic', 'athletic']
192
+ if any(keyword in description_lower for keyword in exercise_keywords):
193
+ multipliers = {
194
+ 'Very High': 1.5,
195
+ 'High': 1.3,
196
+ 'Moderate': 0.7,
197
+ 'Low': 0.4
198
+ }
199
+ breed_score_multipliers.append(multipliers.get(exercise_needs, 1.0))
200
+
201
+ # 體型評估
202
+ size = breed_features.get('size', 'Medium')
203
+ if 'apartment' in description_lower:
204
+ size_multipliers = {
205
+ 'Giant': 0.3,
206
+ 'Large': 0.6,
207
+ 'Medium-Large': 0.8,
208
+ 'Medium': 1.4,
209
+ 'Small': 1.0,
210
+ 'Tiny': 0.9
211
+ }
212
+ breed_score_multipliers.append(size_multipliers.get(size, 1.0))
213
+ elif 'house' in description_lower:
214
+ size_multipliers = {
215
+ 'Giant': 0.8,
216
+ 'Large': 1.2,
217
+ 'Medium-Large': 1.3,
218
+ 'Medium': 1.2,
219
+ 'Small': 0.9,
220
+ 'Tiny': 0.7
221
+ }
222
+ breed_score_multipliers.append(size_multipliers.get(size, 1.0))
223
+
224
+ # 家庭適應性評估
225
+ if any(keyword in description_lower for keyword in ['family', 'children', 'kids']):
226
+ good_with_children = breed_features.get('good_with_children', False)
227
+ breed_score_multipliers.append(1.3 if good_with_children else 0.6)
228
+
229
+ # 噪音評估
230
+ if 'quiet' in description_lower:
231
+ noise_level = breed_features.get('noise_level', 'Moderate')
232
+ noise_multipliers = {
233
+ 'Low': 1.3,
234
+ 'Moderate': 0.9,
235
+ 'High': 0.5
236
+ }
237
+ breed_score_multipliers.append(noise_multipliers.get(noise_level, 1.0))
238
+
239
+ # 應用所有乘數
240
+ for multiplier in breed_score_multipliers:
241
+ score *= multiplier
242
+
243
+ # 確保分數在合理範圍內
244
+ return min(1.5, max(0.3, score))
245
+
246
+ def _calculate_size_similarity_enhanced(self, size1: str, size2: str, description: str) -> float:
247
+ """
248
+ 增強版尺寸相似度計算
249
+ """
250
+ try:
251
+ # 更細緻的尺寸映射
252
+ size_map = {
253
+ 'Tiny': 0,
254
+ 'Small': 1,
255
+ 'Small-Medium': 2,
256
+ 'Medium': 3,
257
+ 'Medium-Large': 4,
258
+ 'Large': 5,
259
+ 'Giant': 6
260
+ }
261
+
262
+ # 標準化並獲取數值
263
+ value1 = size_map.get(self._normalize_size(size1), 3)
264
+ value2 = size_map.get(self._normalize_size(size2), 3)
265
+
266
+ # 基礎相似度計算
267
+ base_similarity = 1.0 - (abs(value1 - value2) / 6.0)
268
+
269
+ # 環境適應性調整
270
+ if 'apartment' in description.lower():
271
+ if size2 in ['Large', 'Giant']:
272
+ base_similarity *= 0.7 # 大型犬在公寓降低相似度
273
+ elif size2 in ['Medium', 'Medium-Large']:
274
+ base_similarity *= 1.2 # 中型犬更適合
275
+ elif size2 in ['Small', 'Tiny']:
276
+ base_similarity *= 0.8 # 過小的狗也不是最佳選擇
277
+
278
+ return min(1.0, base_similarity)
279
+ except Exception as e:
280
+ print(f"Error in calculate_size_similarity_enhanced: {str(e)}")
281
+ return 0.5
282
+
283
+ def _normalize_size(self, size: str) -> str:
284
+ """
285
+ 標準化犬種尺寸分類
286
+
287
+ Args:
288
+ size: 原始尺寸描述
289
+
290
+ Returns:
291
+ str: 標準化後的尺寸類別
292
+ """
293
+ try:
294
+ size = size.lower()
295
+ if 'tiny' in size:
296
+ return 'Tiny'
297
+ elif 'small' in size and 'medium' in size:
298
+ return 'Small-Medium'
299
+ elif 'small' in size:
300
+ return 'Small'
301
+ elif 'medium' in size and 'large' in size:
302
+ return 'Medium-Large'
303
+ elif 'medium' in size:
304
+ return 'Medium'
305
+ elif 'giant' in size:
306
+ return 'Giant'
307
+ elif 'large' in size:
308
+ return 'Large'
309
+ return 'Medium' # 默認為 Medium
310
+ except Exception as e:
311
+ print(f"Error in normalize_size: {str(e)}")
312
+ return 'Medium'
313
+
314
+ def _calculate_exercise_similarity_enhanced(self, exercise1: str, exercise2: str) -> float:
315
+ try:
316
+ exercise_values = {
317
+ 'Very High': 4,
318
+ 'High': 3,
319
+ 'Moderate': 2,
320
+ 'Low': 1
321
+ }
322
+
323
+ value1 = exercise_values.get(exercise1, 2)
324
+ value2 = exercise_values.get(exercise2, 2)
325
+
326
+ # 計算差異
327
+ diff = abs(value1 - value2)
328
+
329
+ if diff == 0:
330
+ return 1.0
331
+ elif diff == 1:
332
+ return 0.7
333
+ elif diff == 2:
334
+ return 0.4
335
+ else:
336
+ return 0.2
337
+
338
+ except Exception as e:
339
+ print(f"Error in calculate_exercise_similarity_enhanced: {str(e)}")
340
+ return 0.5
341
+
342
+ def _calculate_grooming_similarity(self, breed1: str, breed2: str) -> float:
343
+ """
344
+ 計算美容需求相似度
345
+
346
+ Args:
347
+ breed1: 第一個品種名稱
348
+ breed2: 第二個品種名稱
349
+
350
+ Returns:
351
+ float: 相似度分數 (0-1)
352
+ """
353
+ try:
354
+ grooming_map = {
355
+ 'Low': 1,
356
+ 'Moderate': 2,
357
+ 'High': 3
358
+ }
359
+
360
+ # 從dog_data中獲取美容需求
361
+ breed1_info = next((dog for dog in self.dog_data if dog[1] == breed1), None)
362
+ breed2_info = next((dog for dog in self.dog_data if dog[1] == breed2), None)
363
+
364
+ if not breed1_info or not breed2_info:
365
+ return 0.5 # 數據缺失時返回中等相似度
366
+
367
+ grooming1 = breed1_info[8] # Grooming_Needs index
368
+ grooming2 = breed2_info[8]
369
+
370
+ # 獲取數值,默認為 Moderate
371
+ value1 = grooming_map.get(grooming1, 2)
372
+ value2 = grooming_map.get(grooming2, 2)
373
+
374
+ # 基礎相似度計算
375
+ base_similarity = 1.0 - (abs(value1 - value2) / 2.0)
376
+
377
+ # 美容需求調整
378
+ if grooming2 == 'Moderate':
379
+ base_similarity *= 1.1 # 中等美容需求略微加分
380
+ elif grooming2 == 'High':
381
+ base_similarity *= 0.9 # 高美容需求略微降分
382
+
383
+ return min(1.0, base_similarity)
384
+ except Exception as e:
385
+ print(f"Error in calculate_grooming_similarity: {str(e)}")
386
+ return 0.5
387
+
388
+ def _calculate_health_score_similarity(self, breed1: str, breed2: str) -> float:
389
+ """
390
+ 計算兩個品種的健康評分相似度
391
+ """
392
+ try:
393
+ score1 = self._calculate_health_score(breed1)
394
+ score2 = self._calculate_health_score(breed2)
395
+ return 1.0 - abs(score1 - score2)
396
+ except Exception as e:
397
+ print(f"Error in calculate_health_score_similarity: {str(e)}")
398
+ return 0.5
399
+
400
+ def _calculate_health_score(self, breed_name: str) -> float:
401
+ """
402
+ 計算品種的健康評分
403
+
404
+ Args:
405
+ breed_name: 品種名稱
406
+
407
+ Returns:
408
+ float: 健康評分 (0-1)
409
+ """
410
+ try:
411
+ if breed_name not in breed_health_info:
412
+ return 0.5
413
+
414
+ health_notes = breed_health_info[breed_name]['health_notes'].lower()
415
+
416
+ # 嚴重健康問題
417
+ severe_conditions = [
418
+ 'cancer', 'cardiomyopathy', 'epilepsy', 'dysplasia',
419
+ 'bloat', 'progressive', 'syndrome'
420
+ ]
421
+
422
+ # 中等健康問題
423
+ moderate_conditions = [
424
+ 'allergies', 'infections', 'thyroid', 'luxation',
425
+ 'skin problems', 'ear'
426
+ ]
427
+
428
+ # 計算問題數量
429
+ severe_count = sum(1 for condition in severe_conditions if condition in health_notes)
430
+ moderate_count = sum(1 for condition in moderate_conditions if condition in health_notes)
431
+
432
+ # 基礎健康評分
433
+ health_score = 1.0
434
+ health_score -= (severe_count * 0.15) # 嚴重問題扣分更多
435
+ health_score -= (moderate_count * 0.05) # 中等問題扣分較少
436
+
437
+ # 確保評分在合理範圍內
438
+ return max(0.3, min(1.0, health_score))
439
+ except Exception as e:
440
+ print(f"Error in calculate_health_score: {str(e)}")
441
+ return 0.5
442
+
443
+
444
+ def _calculate_noise_similarity(self, breed1: str, breed2: str) -> float:
445
+ """計算兩個品種的噪音相似度"""
446
+ noise_levels = {
447
+ 'Low': 1,
448
+ 'Moderate': 2,
449
+ 'High': 3,
450
+ 'Unknown': 2 # 默認為中等
451
+ }
452
+
453
+ noise1 = breed_noise_info.get(breed1, {}).get('noise_level', 'Unknown')
454
+ noise2 = breed_noise_info.get(breed2, {}).get('noise_level', 'Unknown')
455
+
456
+ # 獲取數值級別
457
+ level1 = noise_levels.get(noise1, 2)
458
+ level2 = noise_levels.get(noise2, 2)
459
+
460
+ # 計算差異並歸一化
461
+ difference = abs(level1 - level2)
462
+ similarity = 1.0 - (difference / 2) # 最大差異是2,所以除以2來歸一化
463
+
464
+ return similarity
465
+
466
+ # bonus score zone
467
+ def _calculate_size_bonus(self, size: str, living_space: str) -> float:
468
+ """
469
+ 計算尺寸匹配的獎勵分數
470
+
471
+ Args:
472
+ size: 品種尺寸
473
+ living_space: 居住空間類型
474
+
475
+ Returns:
476
+ float: 獎勵分數 (-0.25 到 0.15)
477
+ """
478
+ try:
479
+ if living_space == "apartment":
480
+ size_scores = {
481
+ 'Tiny': -0.15,
482
+ 'Small': 0.10,
483
+ 'Medium': 0.15,
484
+ 'Large': 0.10,
485
+ 'Giant': -0.30
486
+ }
487
+ else: # house
488
+ size_scores = {
489
+ 'Tiny': -0.10,
490
+ 'Small': 0.05,
491
+ 'Medium': 0.15,
492
+ 'Large': 0.15,
493
+ 'Giant': -0.15
494
+ }
495
+ return size_scores.get(size, 0.0)
496
+ except Exception as e:
497
+ print(f"Error in calculate_size_bonus: {str(e)}")
498
+ return 0.0
499
+
500
+ def _calculate_exercise_bonus(self, exercise_needs: str, exercise_time: int) -> float:
501
+ """
502
+ 計算運動需求匹配的獎勵分數
503
+
504
+ Args:
505
+ exercise_needs: 品種運動需求
506
+ exercise_time: 用戶可提供的運動時間(分鐘)
507
+
508
+ Returns:
509
+ float: 獎勵分數 (-0.20 到 0.20)
510
+ """
511
+ try:
512
+ if exercise_time >= 120: # 高運動量需求
513
+ exercise_scores = {
514
+ 'Low': -0.30,
515
+ 'Moderate': -0.10,
516
+ 'High': 0.15,
517
+ 'Very High': 0.30
518
+ }
519
+ elif exercise_time >= 60: # 中等運動量需求
520
+ exercise_scores = {
521
+ 'Low': -0.05,
522
+ 'Moderate': 0.15,
523
+ 'High': 0.05,
524
+ 'Very High': -0.10
525
+ }
526
+ else: # 低運動量需求
527
+ exercise_scores = {
528
+ 'Low': 0.15,
529
+ 'Moderate': 0.05,
530
+ 'High': -0.15,
531
+ 'Very High': -0.20
532
+ }
533
+ return exercise_scores.get(exercise_needs, 0.0)
534
+ except Exception as e:
535
+ print(f"Error in calculate_exercise_bonus: {str(e)}")
536
+ return 0.0
537
+
538
+ def _calculate_grooming_bonus(self, grooming: str, commitment: str) -> float:
539
+ """
540
+ 計算美容需求匹配的獎勵分數
541
+
542
+ Args:
543
+ grooming: 品種美容需求
544
+ commitment: 用戶美容投入程度
545
+
546
+ Returns:
547
+ float: 獎勵分數 (-0.15 到 0.10)
548
+ """
549
+ try:
550
+ if commitment == "high":
551
+ grooming_scores = {
552
+ 'Low': -0.05,
553
+ 'Moderate': 0.05,
554
+ 'High': 0.10
555
+ }
556
+ else: # medium or low commitment
557
+ grooming_scores = {
558
+ 'Low': 0.10,
559
+ 'Moderate': 0.05,
560
+ 'High': -0.20
561
+ }
562
+ return grooming_scores.get(grooming, 0.0)
563
+ except Exception as e:
564
+ print(f"Error in calculate_grooming_bonus: {str(e)}")
565
+ return 0.0
566
+
567
+ def _calculate_family_bonus(self, breed_info: Dict) -> float:
568
+ """
569
+ 計算家庭適應性的獎勵分數
570
+
571
+ Args:
572
+ breed_info: 品種信息字典
573
+
574
+ Returns:
575
+ float: 獎勵分數 (0 到 0.20)
576
+ """
577
+ try:
578
+ bonus = 0.0
579
+ temperament = breed_info.get('Temperament', '').lower()
580
+ good_with_children = breed_info.get('Good_With_Children', False)
581
+
582
+ if good_with_children:
583
+ bonus += 0.20
584
+ if any(trait in temperament for trait in ['gentle', 'patient', 'friendly']):
585
+ bonus += 0.10
586
+
587
+ return min(0.20, bonus)
588
+ except Exception as e:
589
+ print(f"Error in calculate_family_bonus: {str(e)}")
590
+ return 0.0
591
+
592
+
593
+ def _detect_scenario(self, description: str) -> Dict[str, float]:
594
+ """
595
+ 檢測場景並返回對應權重
596
+ """
597
+ # 基礎場景定義
598
+ scenarios = {
599
+ 'athletic': {
600
+ 'keywords': ['active', 'exercise', 'running', 'athletic', 'energetic', 'sports'],
601
+ 'weights': {
602
+ 'exercise': 0.40,
603
+ 'size': 0.25,
604
+ 'temperament': 0.20,
605
+ 'health': 0.15
606
+ }
607
+ },
608
+ 'apartment': {
609
+ 'keywords': ['apartment', 'flat', 'condo'],
610
+ 'weights': {
611
+ 'size': 0.35,
612
+ 'noise': 0.30,
613
+ 'exercise': 0.20,
614
+ 'temperament': 0.15
615
+ }
616
+ },
617
+ 'family': {
618
+ 'keywords': ['family', 'children', 'kids', 'friendly'],
619
+ 'weights': {
620
+ 'temperament': 0.35,
621
+ 'safety': 0.30,
622
+ 'noise': 0.20,
623
+ 'exercise': 0.15
624
+ }
625
+ },
626
+ 'novice': {
627
+ 'keywords': ['first time', 'beginner', 'new owner', 'inexperienced'],
628
+ 'weights': {
629
+ 'trainability': 0.35,
630
+ 'temperament': 0.30,
631
+ 'care_level': 0.20,
632
+ 'health': 0.15
633
+ }
634
+ }
635
+ }
636
+
637
+ # 檢測匹配的場景
638
+ matched_scenarios = []
639
+ for scenario, config in scenarios.items():
640
+ if any(keyword in description.lower() for keyword in config['keywords']):
641
+ matched_scenarios.append(scenario)
642
+
643
+ # 默認權重
644
+ default_weights = {
645
+ 'exercise': 0.20,
646
+ 'size': 0.20,
647
+ 'temperament': 0.20,
648
+ 'health': 0.15,
649
+ 'noise': 0.10,
650
+ 'grooming': 0.10,
651
+ 'trainability': 0.05
652
+ }
653
+
654
+ # 如果沒有匹配場景,返回默認權重
655
+ if not matched_scenarios:
656
+ return default_weights
657
+
658
+ # 合併匹配場景的權重
659
+ final_weights = default_weights.copy()
660
+ for scenario in matched_scenarios:
661
+ scenario_weights = scenarios[scenario]['weights']
662
+ for feature, weight in scenario_weights.items():
663
+ if feature in final_weights:
664
+ final_weights[feature] = max(final_weights[feature], weight)
665
+
666
+ return final_weights
667
+
668
+
669
+ def _calculate_final_scores(self, breed_name: str, base_scores: Dict,
670
+ smart_score: float, is_preferred: bool,
671
+ similarity_score: float = 0.0,
672
+ characteristics_score: float = 1.0,
673
+ weights: Dict[str, float] = None) -> Dict:
674
+ try:
675
+ # 使用傳入的權重或默認權重
676
+ if weights is None:
677
+ weights = {
678
+ 'base': 0.35,
679
+ 'smart': 0.35,
680
+ 'bonus': 0.15,
681
+ 'characteristics': 0.15
682
+ }
683
+
684
+ # 確保 base_scores 包含所有必要的鍵
685
+ base_scores = {
686
+ 'overall': base_scores.get('overall', smart_score),
687
+ 'size': base_scores.get('size', 0.0),
688
+ 'exercise': base_scores.get('exercise', 0.0),
689
+ 'temperament': base_scores.get('temperament', 0.0),
690
+ 'grooming': base_scores.get('grooming', 0.0),
691
+ 'health': base_scores.get('health', 0.0),
692
+ 'noise': base_scores.get('noise', 0.0)
693
+ }
694
+
695
+ # 計算基礎分數
696
+ base_score = base_scores['overall']
697
+
698
+ # 計算獎勵分數
699
+ bonus_score = 0.0
700
+ if is_preferred:
701
+ bonus_score = 0.95
702
+ elif similarity_score > 0:
703
+ bonus_score = min(0.8, similarity_score) * 0.95
704
+
705
+ # 特徵匹配度調整
706
+ if characteristics_score < 0.5:
707
+ base_score *= 0.7 # 降低基礎分數
708
+ smart_score *= 0.7 # 降低智能匹配分數
709
+
710
+ # 計算最終分數
711
+ final_score = (
712
+ base_score * weights.get('base', 0.35) +
713
+ smart_score * weights.get('smart', 0.35) +
714
+ bonus_score * weights.get('bonus', 0.15) +
715
+ characteristics_score * weights.get('characteristics', 0.15)
716
+ )
717
+
718
+ # 確保分數在合理範圍內
719
+ final_score = min(1.0, max(0.3, final_score))
720
+
721
+ return {
722
+ 'final_score': round(final_score, 4),
723
+ 'base_score': round(base_score, 4),
724
+ 'smart_score': round(smart_score, 4),
725
+ 'bonus_score': round(bonus_score, 4),
726
+ 'characteristics_score': round(characteristics_score, 4),
727
+ 'detailed_scores': base_scores
728
+ }
729
+
730
+ except Exception as e:
731
+ print(f"Error in calculate_final_scores: {str(e)}")
732
+ return {
733
+ 'final_score': 0.5,
734
+ 'base_score': 0.5,
735
+ 'smart_score': 0.5,
736
+ 'bonus_score': 0.0,
737
+ 'characteristics_score': 0.5,
738
+ 'detailed_scores': {
739
+ 'overall': 0.5,
740
+ 'size': 0.5,
741
+ 'exercise': 0.5,
742
+ 'temperament': 0.5,
743
+ 'grooming': 0.5,
744
+ 'health': 0.5,
745
+ 'noise': 0.5
746
+ }
747
+ }
748
+
749
+ def _general_matching(self, description: str, weights: Dict[str, float], top_n: int = 10) -> List[Dict]:
750
+ """基本的品種匹配邏輯,考慮描述、性格、噪音和健康因素"""
751
+ try:
752
+ matches = []
753
+ desc_embedding = self._get_cached_embedding(description)
754
+
755
+ for breed in self.dog_data:
756
+ breed_name = breed[1]
757
+ breed_features = self._extract_breed_features(breed)
758
+ breed_description = breed[9]
759
+ temperament = breed[4]
760
+
761
+ breed_desc_embedding = self._get_cached_embedding(breed_description)
762
+ breed_temp_embedding = self._get_cached_embedding(temperament)
763
+
764
+ desc_similarity = float(util.pytorch_cos_sim(desc_embedding, breed_desc_embedding))
765
+ temp_similarity = float(util.pytorch_cos_sim(desc_embedding, breed_temp_embedding))
766
+
767
+ noise_similarity = self._calculate_noise_similarity(breed_name, breed_name)
768
+ health_score = self._calculate_health_score(breed_name)
769
+ health_similarity = 1.0 - abs(health_score - 0.8)
770
+
771
+ # 使用傳入的權重
772
+ final_score = (
773
+ desc_similarity * weights.get('description', 0.35) +
774
+ temp_similarity * weights.get('temperament', 0.25) +
775
+ noise_similarity * weights.get('noise', 0.2) +
776
+ health_similarity * weights.get('health', 0.2)
777
+ )
778
+
779
+ # 計算特徵分數
780
+ characteristics_score = self.get_breed_characteristics_score(breed_features, description)
781
+
782
+ # 構建完整的 scores 字典
783
+ scores = {
784
+ 'overall': final_score,
785
+ 'size': breed_features.get('size_score', 0.0),
786
+ 'exercise': breed_features.get('exercise_score', 0.0),
787
+ 'temperament': temp_similarity,
788
+ 'grooming': breed_features.get('grooming_score', 0.0),
789
+ 'health': health_score,
790
+ 'noise': noise_similarity
791
+ }
792
+
793
+ matches.append({
794
+ 'breed': breed_name,
795
+ 'scores': scores,
796
+ 'final_score': final_score,
797
+ 'base_score': final_score,
798
+ 'characteristics_score': characteristics_score,
799
+ 'bonus_score': 0.0,
800
+ 'is_preferred': False,
801
+ 'similarity': final_score,
802
+ 'health_score': health_score,
803
+ 'reason': "Matched based on description and characteristics"
804
+ })
805
+
806
+ return sorted(matches, key=lambda x: (-x['characteristics_score'], -x['final_score']))[:top_n]
807
+
808
+ except Exception as e:
809
+ print(f"Error in _general_matching: {str(e)}")
810
+ return []
811
+
812
+
813
+ def _detect_breed_preference(self, description: str) -> Optional[str]:
814
+ """檢測用戶是否提到特定品種"""
815
+ description_lower = f" {description.lower()} "
816
+
817
+ for breed_info in self.dog_data:
818
+ breed_name = breed_info[1]
819
+ normalized_breed = breed_name.lower().replace('_', ' ')
820
+
821
+ pattern = rf"\b{re.escape(normalized_breed)}\b"
822
+
823
+ if re.search(pattern, description_lower):
824
+ return breed_name
825
+
826
+ return None
827
+
828
+ def _extract_breed_features(self, breed_info: Tuple) -> Dict:
829
+ """
830
+ 從品種信息中提取特徵
831
+
832
+ Args:
833
+ breed_info: 品種信息元組
834
+
835
+ Returns:
836
+ Dict: 包含品種特徵的字典
837
+ """
838
+ try:
839
+ return {
840
+ 'breed_name': breed_info[1],
841
+ 'size': breed_info[2],
842
+ 'temperament': breed_info[4],
843
+ 'exercise': breed_info[7],
844
+ 'grooming': breed_info[8],
845
+ 'description': breed_info[9],
846
+ 'good_with_children': breed_info[6]
847
+ }
848
+ except Exception as e:
849
+ print(f"Error in extract_breed_features: {str(e)}")
850
+ return {
851
+ 'breed_name': '',
852
+ 'size': 'Medium',
853
+ 'temperament': '',
854
+ 'exercise': 'Moderate',
855
+ 'grooming': 'Moderate',
856
+ 'description': '',
857
+ 'good_with_children': False
858
+ }
859
+
860
+ def match_user_preference(self, description: str, top_n: int = 10) -> List[Dict]:
861
+ try:
862
+ # 獲取場景權重
863
+ weights = self._detect_scenario(description)
864
+ matches = []
865
+ preferred_breed = self._detect_breed_preference(description)
866
+
867
+ # 處理用戶明確提到的品種
868
+ if preferred_breed:
869
+ breed_info = next((breed for breed in self.dog_data if breed[1] == preferred_breed), None)
870
+ if breed_info:
871
+ breed_features = self._extract_breed_features(breed_info)
872
+ base_similarity = self._calculate_breed_similarity(breed_features, breed_features, weights)
873
+
874
+ # 計算特徵分數
875
+ characteristics_score = self.get_breed_characteristics_score(breed_features, description)
876
+
877
+ # 計算最終分數
878
+ scores = self._calculate_final_scores(
879
+ preferred_breed,
880
+ {'overall': base_similarity},
881
+ smart_score=base_similarity,
882
+ is_preferred=True,
883
+ similarity_score=1.0,
884
+ characteristics_score=characteristics_score,
885
+ weights=weights
886
+ )
887
+
888
+ matches.append({
889
+ 'breed': preferred_breed,
890
+ 'scores': scores['detailed_scores'],
891
+ 'final_score': scores['final_score'],
892
+ 'base_score': scores['base_score'],
893
+ 'bonus_score': scores['bonus_score'],
894
+ 'characteristics_score': characteristics_score,
895
+ 'is_preferred': True,
896
+ 'priority': 1,
897
+ 'health_score': self._calculate_health_score(preferred_breed),
898
+ 'reason': "Directly matched your preferred breed"
899
+ })
900
+
901
+ # 尋找相似品種
902
+ similar_breeds = self.find_similar_breeds(preferred_breed, top_n=top_n-1)
903
+ for breed_name, similarity in similar_breeds:
904
+ if breed_name != preferred_breed:
905
+ breed_info = next((breed for breed in self.dog_data if breed[1] == breed_name), None)
906
+ if breed_info:
907
+ breed_features = self._extract_breed_features(breed_info)
908
+ characteristics_score = self.get_breed_characteristics_score(breed_features, description)
909
+
910
+ scores = self._calculate_final_scores(
911
+ breed_name,
912
+ {'overall': similarity},
913
+ smart_score=similarity,
914
+ is_preferred=False,
915
+ similarity_score=similarity,
916
+ characteristics_score=characteristics_score,
917
+ weights=weights
918
+ )
919
+
920
+ if scores['final_score'] >= 0.4: # 設定最低分數門檻
921
+ matches.append({
922
+ 'breed': breed_name,
923
+ 'scores': scores['detailed_scores'],
924
+ 'final_score': scores['final_score'],
925
+ 'base_score': scores['base_score'],
926
+ 'bonus_score': scores['bonus_score'],
927
+ 'characteristics_score': characteristics_score,
928
+ 'is_preferred': False,
929
+ 'priority': 2,
930
+ 'health_score': self._calculate_health_score(breed_name),
931
+ 'reason': f"Similar to {preferred_breed}"
932
+ })
933
+
934
+ # 如果沒有找到偏好品種或需要更多匹配
935
+ if len(matches) < top_n:
936
+ general_matches = self._general_matching(description, weights, top_n - len(matches))
937
+ for match in general_matches:
938
+ if match['breed'] not in [m['breed'] for m in matches]:
939
+ match['priority'] = 3
940
+ if match['final_score'] >= 0.4: # 分數門檻
941
+ matches.append(match)
942
+
943
+ # 最終排序
944
+ matches.sort(key=lambda x: (
945
+ -x.get('characteristics_score', 0), # 首先考慮特徵匹配度
946
+ -x.get('final_score', 0), # 然後是總分
947
+ -x.get('base_score', 0), # 最後是基礎分數
948
+ x.get('breed', '') # 字母順序
949
+ ))
950
+
951
+ # 取前N個結果
952
+ final_matches = matches[:top_n]
953
+
954
+ # 更新排名
955
+ for i, match in enumerate(final_matches, 1):
956
+ match['rank'] = i
957
+
958
+ return final_matches
959
+
960
+ except Exception as e:
961
+ print(f"Error in match_user_preference: {str(e)}")
962
+ return []