DawnC commited on
Commit
6166858
1 Parent(s): cb898de

Upload 3 files

Browse files
breed_recommendation.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
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
+ matcher = SmartBreedMatcher(dog_data)
144
+ breed_recommendations = matcher.match_user_preference(description, top_n=10)
145
+
146
+ print("Creating user preferences...")
147
+ user_prefs = UserPreferences(
148
+ living_space="apartment" if "apartment" in description.lower() else "house_small",
149
+ exercise_time=60,
150
+ grooming_commitment="medium",
151
+ experience_level="intermediate",
152
+ has_children="children" in description.lower() or "kids" in description.lower(),
153
+ noise_tolerance="medium",
154
+ space_for_play=True if "yard" in description.lower() or "garden" in description.lower() else False,
155
+ other_pets=False,
156
+ climate="moderate",
157
+ health_sensitivity="medium",
158
+ barking_acceptance=None
159
+ )
160
+
161
+ final_recommendations = []
162
+
163
+ for smart_rec in breed_recommendations:
164
+ breed_name = smart_rec['breed']
165
+ breed_info = get_dog_description(breed_name)
166
+ if not isinstance(breed_info, dict):
167
+ continue
168
+
169
+ # 計算基礎相容性分數
170
+ compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)
171
+
172
+ bonus_reasons = []
173
+ bonus_score = 0
174
+ is_preferred = smart_rec.get('is_preferred', False)
175
+ similarity = smart_rec.get('similarity', 0)
176
+
177
+ # 用戶直接提到的品種
178
+ if is_preferred:
179
+ bonus_score = 0.15 # 15% bonus
180
+ bonus_reasons.append("Directly mentioned breed (+15%)")
181
+ # 高相似度品種
182
+ elif similarity > 0.8:
183
+ bonus_score = 0.10 # 10% bonus
184
+ bonus_reasons.append("Very similar to preferred breed (+10%)")
185
+ # 中等相似度品種
186
+ elif similarity > 0.6:
187
+ bonus_score = 0.05 # 5% bonus
188
+ bonus_reasons.append("Similar to preferred breed (+5%)")
189
+
190
+ # 基於品種特性的額外加分
191
+ temperament = breed_info.get('Temperament', '').lower()
192
+ if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
193
+ bonus_score += 0.02 # 2% bonus
194
+ bonus_reasons.append("Positive temperament traits (+2%)")
195
+
196
+ if breed_info.get('Good with Children') == 'Yes' and user_prefs.has_children:
197
+ bonus_score += 0.03 # 3% bonus
198
+ bonus_reasons.append("Excellent with children (+3%)")
199
+
200
+ # 基礎分數和最終分數計算
201
+ base_score = compatibility_scores.get('overall', 0.7)
202
+ final_score = min(0.95, base_score + bonus_score) # 確保不超過95%
203
+
204
+ final_recommendations.append({
205
+ 'rank': 0,
206
+ 'breed': breed_name,
207
+ 'base_score': round(base_score, 4),
208
+ 'bonus_score': round(bonus_score, 4),
209
+ 'final_score': round(final_score, 4),
210
+ 'scores': compatibility_scores,
211
+ 'match_reason': ' • '.join(bonus_reasons) if bonus_reasons else "Standard match",
212
+ 'info': breed_info,
213
+ 'noise_info': breed_noise_info.get(breed_name, {}),
214
+ 'health_info': breed_health_info.get(breed_name, {})
215
+ })
216
+
217
+ # 根據最終分數排序
218
+ final_recommendations.sort(key=lambda x: (-x['final_score'], x['breed']))
219
+
220
+ # 更新排名
221
+ for i, rec in enumerate(final_recommendations, 1):
222
+ rec['rank'] = i
223
+
224
+ # 新增:保存到歷史記錄
225
+ history_results = [{
226
+ 'breed': rec['breed'],
227
+ 'rank': rec['rank'],
228
+ 'final_score': rec['final_score']
229
+ } for rec in final_recommendations[:10]] # 只保存前10名
230
+
231
+ history_component.save_search(
232
+ user_preferences=None, # description搜尋不需要preferences
233
+ results=history_results,
234
+ search_type="description",
235
+ description=description # 用戶輸入的描述文字
236
+ )
237
+
238
+ # 驗證排序
239
+ print("\nFinal Rankings:")
240
+ for rec in final_recommendations:
241
+ print(f"#{rec['rank']} {rec['breed']}")
242
+ print(f"Base Score: {rec['base_score']:.4f}")
243
+ print(f"Bonus Score: {rec['bonus_score']:.4f}")
244
+ print(f"Final Score: {rec['final_score']:.4f}")
245
+ print(f"Reason: {rec['match_reason']}\n")
246
+
247
+ result = format_recommendation_html(final_recommendations)
248
+ return [gr.update(value=result), gr.update(visible=False)]
249
+
250
+ except Exception as e:
251
+ error_msg = f"Error processing your description. Details: {str(e)}"
252
+ return [gr.update(value=error_msg), gr.update(visible=False)]
253
+
254
+ def show_loading():
255
+ return [gr.update(value=""), gr.update(visible=True)]
256
+
257
+
258
+ get_recommendations_btn.click(
259
+ fn=on_find_match_click,
260
+ inputs=[
261
+ living_space,
262
+ exercise_time,
263
+ grooming_commitment,
264
+ experience_level,
265
+ has_children,
266
+ noise_tolerance
267
+ ],
268
+ outputs=recommendation_output
269
+ )
270
+
271
+ description_search_btn.click(
272
+ fn=show_loading, # 先顯示加載消息
273
+ outputs=[description_output, loading_msg]
274
+ ).then( # 然後執行搜索
275
+ fn=on_description_search,
276
+ inputs=[description_input],
277
+ outputs=[description_output, loading_msg]
278
+ )
279
+
280
+ return {
281
+ 'living_space': living_space,
282
+ 'exercise_time': exercise_time,
283
+ 'grooming_commitment': grooming_commitment,
284
+ 'experience_level': experience_level,
285
+ 'has_children': has_children,
286
+ 'noise_tolerance': noise_tolerance,
287
+ 'get_recommendations_btn': get_recommendations_btn,
288
+ 'recommendation_output': recommendation_output,
289
+ 'description_input': description_input,
290
+ 'description_search_btn': description_search_btn,
291
+ 'description_output': description_output
292
+ }
description_search_ui.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+
4
+ def create_description_search_tab():
5
+ """創建描述搜尋頁面的UI程式碼"""
6
+ guide_html = """
7
+ <div class="breed-search-container">
8
+ <div class="description-guide">
9
+ <h2 class="guide-title" style="
10
+ background: linear-gradient(90deg, #4299e1, #48bb78);
11
+ -webkit-background-clip: text;
12
+ -webkit-text-fill-color: transparent;
13
+ font-weight: 600;
14
+ font-size: 1.8em;
15
+ ">🐾 Describe Your Ideal Dog</h2>
16
+
17
+ <div class="guide-content">
18
+ <p class="intro-text" style="
19
+ background: linear-gradient(90deg, #4299e1, #48bb78);
20
+ -webkit-background-clip: text;
21
+ -webkit-text-fill-color: transparent;
22
+ font-weight: 600;
23
+ font-size: 1.2em;
24
+ margin-bottom: 20px;
25
+ ">Help us find your perfect companion! Please consider including the following details:</p>
26
+
27
+ <div class="criteria-grid" style="
28
+ background: linear-gradient(to right, rgba(66, 153, 225, 0.1), rgba(72, 187, 120, 0.1));
29
+ border-radius: 10px;
30
+ padding: 20px;
31
+ ">
32
+ <div class="criteria-item">
33
+ <span class="icon">🏃</span>
34
+ <div class="criteria-content">
35
+ <h3>Activity Level</h3>
36
+ <p>Low • Moderate • High • Very Active</p>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="criteria-item">
41
+ <span class="icon">🏠</span>
42
+ <div class="criteria-content">
43
+ <h3>Living Environment</h3>
44
+ <p>Apartment • House • Yard Space</p>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="criteria-item">
49
+ <span class="icon">👨‍👩‍👧‍👦</span>
50
+ <div class="criteria-content">
51
+ <h3>Family Situation</h3>
52
+ <p>Children • Other Pets • Single Adult</p>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="criteria-item">
57
+ <span class="icon">✂️</span>
58
+ <div class="criteria-content">
59
+ <h3>Grooming Commitment</h3>
60
+ <p>Low • Medium • High Maintenance</p>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="criteria-item">
65
+ <span class="icon">🎭</span>
66
+ <div class="criteria-content">
67
+ <h3>Desired Personality</h3>
68
+ <p>Friendly • Independent • Intelligent • Calm</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ """
76
+
77
+ # 增加 CSS 的樣式
78
+ css = """
79
+ <style>
80
+ .breed-search-container {
81
+ background: white;
82
+ border-radius: 12px;
83
+ padding: 24px;
84
+ margin-bottom: 20px;
85
+ }
86
+ .guide-title {
87
+ font-size: 1.8rem;
88
+ color: #2c3e50;
89
+ margin-bottom: 20px;
90
+ text-align: center;
91
+ }
92
+ .intro-text {
93
+ color: #666;
94
+ text-align: center;
95
+ margin-bottom: 24px;
96
+ font-size: 1.1rem;
97
+ }
98
+ .criteria-grid {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
101
+ gap: 20px;
102
+ margin-bottom: 24px;
103
+ }
104
+ .criteria-item {
105
+ display: flex;
106
+ align-items: flex-start;
107
+ padding: 16px;
108
+ background: #f8fafc;
109
+ border-radius: 8px;
110
+ transition: all 0.3s ease;
111
+ }
112
+ .criteria-item:hover {
113
+ transform: translateY(-2px);
114
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
115
+ }
116
+ .criteria-item .icon {
117
+ font-size: 24px;
118
+ margin-right: 12px;
119
+ margin-top: 3px;
120
+ }
121
+ .criteria-content h3 {
122
+ font-size: 1.1rem;
123
+ color: #2c3e50;
124
+ margin: 0 0 4px 0;
125
+ }
126
+ .criteria-content p {
127
+ color: #666;
128
+ margin: 0;
129
+ font-size: 0.95rem;
130
+ }
131
+ </style>
132
+ """
133
+
134
+ with gr.Column():
135
+ # 顯示指南和樣式
136
+ gr.HTML(css + guide_html)
137
+
138
+ # 描述輸入區
139
+ description_input = gr.Textbox(
140
+ label="",
141
+ placeholder="Example: I'm looking for a medium-sized, friendly dog that's good with kids...",
142
+ lines=5
143
+ )
144
+
145
+ # 搜索按鈕
146
+ search_button = gr.Button(
147
+ "Find My Perfect Match! 🔍",
148
+ variant="primary",
149
+ size="lg"
150
+ )
151
+
152
+ # 加載消息
153
+ loading_msg = gr.HTML("""
154
+ <div style='text-align: center; color: #666;'>
155
+ <p><b>Finding your perfect match...</b></p>
156
+ <p>Please wait 25-30 seconds while we analyze your preferences.</p>
157
+ </div>
158
+ """, visible=False)
159
+
160
+ # 結果顯示區域
161
+ result_output = gr.HTML(label="Breed Recommendations")
162
+
163
+ return description_input, search_button, result_output, loading_msg
smart_breed_matcher.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import torch
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
+
18
+ def _get_cached_embedding(self, text: str) -> torch.Tensor:
19
+ if text not in self._embedding_cache:
20
+ self._embedding_cache[text] = self.model.encode(text)
21
+ return self._embedding_cache[text]
22
+
23
+ def _categorize_breeds(self) -> Dict:
24
+ """自動將狗品種分類"""
25
+ categories = {
26
+ 'working_dogs': [],
27
+ 'herding_dogs': [],
28
+ 'hunting_dogs': [],
29
+ 'companion_dogs': [],
30
+ 'guard_dogs': []
31
+ }
32
+
33
+ for breed_info in self.dog_data:
34
+ description = breed_info[9].lower()
35
+ temperament = breed_info[4].lower()
36
+
37
+ # 根據描述和性格特徵自動分類
38
+ if any(word in description for word in ['herding', 'shepherd', 'cattle', 'flock']):
39
+ categories['herding_dogs'].append(breed_info[1])
40
+ elif any(word in description for word in ['hunting', 'hunt', 'retriever', 'pointer']):
41
+ categories['hunting_dogs'].append(breed_info[1])
42
+ elif any(word in description for word in ['companion', 'toy', 'family', 'lap']):
43
+ categories['companion_dogs'].append(breed_info[1])
44
+ elif any(word in description for word in ['guard', 'protection', 'watchdog']):
45
+ categories['guard_dogs'].append(breed_info[1])
46
+ elif any(word in description for word in ['working', 'draft', 'cart']):
47
+ categories['working_dogs'].append(breed_info[1])
48
+
49
+ return categories
50
+
51
+ def find_similar_breeds(self, breed_name: str, top_n: int = 5) -> List[Tuple[str, float]]:
52
+ """找出與指定品種最相似的其他品種"""
53
+ target_breed = next((breed for breed in self.dog_data if breed[1] == breed_name), None)
54
+ if not target_breed:
55
+ return []
56
+
57
+ # 獲取目標品種的特徵
58
+ target_features = {
59
+ 'breed_name': target_breed[1], # 添加品種名稱
60
+ 'size': target_breed[2],
61
+ 'temperament': target_breed[4],
62
+ 'exercise': target_breed[7],
63
+ 'description': target_breed[9]
64
+ }
65
+
66
+ similarities = []
67
+ for breed in self.dog_data:
68
+ if breed[1] != breed_name:
69
+ breed_features = {
70
+ 'breed_name': breed[1], # 添加品種名稱
71
+ 'size': breed[2],
72
+ 'temperament': breed[4],
73
+ 'exercise': breed[7],
74
+ 'description': breed[9]
75
+ }
76
+
77
+ similarity_score = self._calculate_breed_similarity(target_features, breed_features)
78
+ similarities.append((breed[1], similarity_score))
79
+
80
+ return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_n]
81
+
82
+
83
+ def _calculate_breed_similarity(self, breed1_features: Dict, breed2_features: Dict) -> float:
84
+ """計算兩個品種之間的相似度,包含健康和噪音因素"""
85
+ # 計算描述文本的相似度
86
+ desc1_embedding = self._get_cached_embedding(breed1_features['description'])
87
+ desc2_embedding = self._get_cached_embedding(breed2_features['description'])
88
+ description_similarity = float(util.pytorch_cos_sim(desc1_embedding, desc2_embedding))
89
+
90
+ # 基本特徵相似度
91
+ size_similarity = 1.0 if breed1_features['size'] == breed2_features['size'] else 0.5
92
+ exercise_similarity = 1.0 if breed1_features['exercise'] == breed2_features['exercise'] else 0.5
93
+
94
+ # 性格相似度
95
+ temp1_embedding = self._get_cached_embedding(breed1_features['temperament'])
96
+ temp2_embedding = self._get_cached_embedding(breed2_features['temperament'])
97
+ temperament_similarity = float(util.pytorch_cos_sim(temp1_embedding, temp2_embedding))
98
+
99
+ # 健康分數相似度
100
+ health_score1 = self._calculate_health_score(breed1_features['breed_name'])
101
+ health_score2 = self._calculate_health_score(breed2_features['breed_name'])
102
+ health_similarity = 1.0 - abs(health_score1 - health_score2)
103
+
104
+ # 噪音水平相似度
105
+ noise_similarity = self._calculate_noise_similarity(
106
+ breed1_features['breed_name'],
107
+ breed2_features['breed_name']
108
+ )
109
+
110
+ # 加權計算
111
+ weights = {
112
+ 'description': 0.25,
113
+ 'temperament': 0.20,
114
+ 'exercise': 0.2,
115
+ 'size': 0.05,
116
+ 'health': 0.15,
117
+ 'noise': 0.15
118
+ }
119
+
120
+ final_similarity = (
121
+ description_similarity * weights['description'] +
122
+ temperament_similarity * weights['temperament'] +
123
+ exercise_similarity * weights['exercise'] +
124
+ size_similarity * weights['size'] +
125
+ health_similarity * weights['health'] +
126
+ noise_similarity * weights['noise']
127
+ )
128
+
129
+ return final_similarity
130
+
131
+
132
+ def _calculate_final_scores(self, breed_name: str, base_scores: Dict,
133
+ smart_score: float, is_preferred: bool,
134
+ similarity_score: float = 0.0) -> Dict:
135
+ """
136
+ 計算最終分數,包含基礎分數和獎勵分數
137
+
138
+ Args:
139
+ breed_name: 品種名稱
140
+ base_scores: 基礎評分 (空間、運動等)
141
+ smart_score: 智能匹配分數
142
+ is_preferred: 是否為用戶指定品種
143
+ similarity_score: 與指定品種的相似度 (0-1)
144
+ """
145
+ # 基礎權重
146
+ weights = {
147
+ 'base': 0.6, # 基礎分數權重
148
+ 'smart': 0.25, # 智能匹配權重
149
+ 'bonus': 0.15 # 獎勵分數權重
150
+ }
151
+
152
+ # 計算基礎分數
153
+ base_score = base_scores.get('overall', 0.7)
154
+
155
+ # 計算獎勵分數
156
+ bonus_score = 0.0
157
+ if is_preferred:
158
+ # 用戶指定品種獲得最高獎勵
159
+ bonus_score = 0.95
160
+ elif similarity_score > 0:
161
+ # 相似品種獲得部分獎勵,但不超過80%的最高獎勵
162
+ bonus_score = min(0.8, similarity_score) * 0.95
163
+
164
+ # 計算最終分數
165
+ final_score = (
166
+ base_score * weights['base'] +
167
+ smart_score * weights['smart'] +
168
+ bonus_score * weights['bonus']
169
+ )
170
+
171
+ # 更新各項分數
172
+ scores = base_scores.copy()
173
+
174
+ # 如果是用戶指定品種,稍微提升各項基礎分數,但保持合理範圍
175
+ if is_preferred:
176
+ for key in scores:
177
+ if key != 'overall':
178
+ scores[key] = min(1.0, scores[key] * 1.1) # 最多提升10%
179
+
180
+ # 為相似品種調整分數
181
+ elif similarity_score > 0:
182
+ boost_factor = 1.0 + (similarity_score * 0.05) # 最多提升5%
183
+ for key in scores:
184
+ if key != 'overall':
185
+ scores[key] = min(0.95, scores[key] * boost_factor) # 確保不超過95%
186
+
187
+ return {
188
+ 'final_score': round(final_score, 4),
189
+ 'base_score': round(base_score, 4),
190
+ 'bonus_score': round(bonus_score, 4),
191
+ 'scores': {k: round(v, 4) for k, v in scores.items()}
192
+ }
193
+
194
+ def _calculate_health_score(self, breed_name: str) -> float:
195
+ """計算品種的健康分數"""
196
+ if breed_name not in breed_health_info:
197
+ return 0.5
198
+
199
+ health_notes = breed_health_info[breed_name]['health_notes'].lower()
200
+
201
+ # 嚴重健康問題
202
+ severe_conditions = [
203
+ 'cancer', 'cardiomyopathy', 'epilepsy', 'dysplasia',
204
+ 'bloat', 'progressive', 'syndrome'
205
+ ]
206
+
207
+ # 中等健康問題
208
+ moderate_conditions = [
209
+ 'allergies', 'infections', 'thyroid', 'luxation',
210
+ 'skin problems', 'ear'
211
+ ]
212
+
213
+ severe_count = sum(1 for condition in severe_conditions if condition in health_notes)
214
+ moderate_count = sum(1 for condition in moderate_conditions if condition in health_notes)
215
+
216
+ health_score = 1.0
217
+ health_score -= (severe_count * 0.1)
218
+ health_score -= (moderate_count * 0.05)
219
+
220
+ # 特殊條件調整(根據用戶偏好)
221
+ if hasattr(self, 'user_preferences'):
222
+ if self.user_preferences.has_children:
223
+ if 'requires frequent' in health_notes or 'regular monitoring' in health_notes:
224
+ health_score *= 0.9
225
+
226
+ if self.user_preferences.health_sensitivity == 'high':
227
+ health_score *= 0.9
228
+
229
+ return max(0.3, min(1.0, health_score))
230
+
231
+
232
+
233
+ def _calculate_noise_similarity(self, breed1: str, breed2: str) -> float:
234
+ """計算兩個品種的噪音相似度"""
235
+ noise_levels = {
236
+ 'Low': 1,
237
+ 'Moderate': 2,
238
+ 'High': 3,
239
+ 'Unknown': 2 # 默認為中等
240
+ }
241
+
242
+ noise1 = breed_noise_info.get(breed1, {}).get('noise_level', 'Unknown')
243
+ noise2 = breed_noise_info.get(breed2, {}).get('noise_level', 'Unknown')
244
+
245
+ # 獲取數值級別
246
+ level1 = noise_levels.get(noise1, 2)
247
+ level2 = noise_levels.get(noise2, 2)
248
+
249
+ # 計算差異並歸一化
250
+ difference = abs(level1 - level2)
251
+ similarity = 1.0 - (difference / 2) # 最大差異是2,所以除以2來歸一化
252
+
253
+ return similarity
254
+
255
+ def _general_matching(self, description: str, top_n: int = 10) -> List[Dict]:
256
+ """基本的品種匹配邏輯,考慮描述、性格、噪音和健康因素"""
257
+ matches = []
258
+ # 預先計算描述的 embedding 並快取
259
+ desc_embedding = self._get_cached_embedding(description)
260
+
261
+ for breed in self.dog_data:
262
+ breed_name = breed[1]
263
+ breed_description = breed[9]
264
+ temperament = breed[4]
265
+
266
+ # 使用快取計算相似度
267
+ breed_desc_embedding = self._get_cached_embedding(breed_description)
268
+ breed_temp_embedding = self._get_cached_embedding(temperament)
269
+
270
+ desc_similarity = float(util.pytorch_cos_sim(desc_embedding, breed_desc_embedding))
271
+ temp_similarity = float(util.pytorch_cos_sim(desc_embedding, breed_temp_embedding))
272
+
273
+ # 其餘計算保持不變
274
+ noise_similarity = self._calculate_noise_similarity(breed_name, breed_name)
275
+ health_score = self._calculate_health_score(breed_name)
276
+ health_similarity = 1.0 - abs(health_score - 0.8)
277
+
278
+ weights = {
279
+ 'description': 0.35,
280
+ 'temperament': 0.25,
281
+ 'noise': 0.2,
282
+ 'health': 0.2
283
+ }
284
+
285
+ final_score = (
286
+ desc_similarity * weights['description'] +
287
+ temp_similarity * weights['temperament'] +
288
+ noise_similarity * weights['noise'] +
289
+ health_similarity * weights['health']
290
+ )
291
+
292
+ matches.append({
293
+ 'breed': breed_name,
294
+ 'score': final_score,
295
+ 'is_preferred': False,
296
+ 'similarity': final_score,
297
+ 'reason': "Matched based on description, temperament, noise level, and health score"
298
+ })
299
+
300
+ return sorted(matches, key=lambda x: -x['score'])[:top_n]
301
+
302
+
303
+ def match_user_preference(self, description: str, top_n: int = 10) -> List[Dict]:
304
+ """根據用戶描述匹配最適合的品種"""
305
+ preferred_breed = self._detect_breed_preference(description)
306
+
307
+ matches = []
308
+ if preferred_breed:
309
+ similar_breeds = self.find_similar_breeds(preferred_breed, top_n=top_n)
310
+
311
+ # 首先添加偏好品種
312
+ breed_info = next((breed for breed in self.dog_data if breed[1] == preferred_breed), None)
313
+ if breed_info:
314
+ health_score = self._calculate_health_score(preferred_breed)
315
+ noise_info = breed_noise_info.get(preferred_breed, {
316
+ "noise_level": "Unknown",
317
+ "noise_notes": "No noise information available"
318
+ })
319
+
320
+ # 偏好品種必定是最高分
321
+ matches.append({
322
+ 'breed': preferred_breed,
323
+ 'score': 1.0,
324
+ 'is_preferred': True,
325
+ 'similarity': 1.0,
326
+ 'health_score': health_score,
327
+ 'noise_level': noise_info['noise_level'],
328
+ 'reason': "Directly matched your preferred breed"
329
+ })
330
+
331
+ # 添加相似品種
332
+ for breed_name, similarity in similar_breeds:
333
+ if breed_name != preferred_breed:
334
+ health_score = self._calculate_health_score(breed_name)
335
+ noise_info = breed_noise_info.get(breed_name, {
336
+ "noise_level": "Unknown",
337
+ "noise_notes": "No noise information available"
338
+ })
339
+
340
+ # 調整相似品種分數計算
341
+ base_similarity = similarity * 0.6
342
+ health_factor = health_score * 0.2
343
+ noise_factor = self._calculate_noise_similarity(preferred_breed, breed_name) * 0.2
344
+
345
+ # 確保相似品種分數不會超過偏好品種
346
+ final_score = min(0.95, base_similarity + health_factor + noise_factor)
347
+
348
+ matches.append({
349
+ 'breed': breed_name,
350
+ 'score': final_score,
351
+ 'is_preferred': False,
352
+ 'similarity': similarity,
353
+ 'health_score': health_score,
354
+ 'noise_level': noise_info['noise_level'],
355
+ 'reason': f"Similar to {preferred_breed} in characteristics, health profile, and noise level"
356
+ })
357
+ else:
358
+ matches = self._general_matching(description, top_n)
359
+
360
+ return sorted(matches,
361
+ key=lambda x: (-int(x.get('is_preferred', False)),
362
+ -x['score'], # 降序排列
363
+ x['breed']))[:top_n]
364
+
365
+ def _detect_breed_preference(self, description: str) -> Optional[str]:
366
+ """檢測用戶是否提到特定品種"""
367
+ description_lower = description.lower()
368
+
369
+ for breed_info in self.dog_data:
370
+ breed_name = breed_info[1]
371
+ normalized_breed = breed_name.lower().replace('_', ' ')
372
+
373
+ if any(phrase in description_lower for phrase in [
374
+ f"love {normalized_breed}",
375
+ f"like {normalized_breed}",
376
+ f"prefer {normalized_breed}",
377
+ f"want {normalized_breed}",
378
+ normalized_breed
379
+ ]):
380
+ return breed_name
381
+
382
+ return None