File size: 12,840 Bytes
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595e0a5
 
 
 
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595e0a5
1e4c9bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
import sqlite3
import traceback
import random
from typing import List, Dict
from breed_health_info import breed_health_info, default_health_note
from breed_noise_info import breed_noise_info
from dog_database import get_dog_description
from scoring_calculation_system import UserPreferences, calculate_compatibility_score

def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 15) -> List[Dict]:
    """基於使用者偏好推薦狗品種,確保正確的分數排序"""
    print(f"Starting get_breed_recommendations with top_n={top_n}")
    recommendations = []
    seen_breeds = set()

    try:
        # 獲取所有品種
        conn = sqlite3.connect('animal_detector.db')
        cursor = conn.cursor()
        cursor.execute("SELECT Breed FROM AnimalCatalog")
        all_breeds = cursor.fetchall()
        conn.close()

        print(f"Total breeds in database: {len(all_breeds)}")

        # 收集所有品種的分數
        for breed_tuple in all_breeds:
            breed = breed_tuple[0]
            base_breed = breed.split('(')[0].strip()

            # 過濾掉野生動物品種
            if base_breed == 'Dhole':
                continue

            if base_breed in seen_breeds:
                continue
            seen_breeds.add(base_breed)

            # 獲取品種資訊
            breed_info = get_dog_description(breed)
            if not isinstance(breed_info, dict):
                continue

            # 調整品種尺寸過濾邏輯,避免過度限制候選品種
            if user_prefs.size_preference != "no_preference":
                breed_size = breed_info.get('Size', '').lower()
                user_size = user_prefs.size_preference.lower()

                # 放寬尺寸匹配條件,允許相鄰尺寸的品種通過篩選
                size_compatibility = False
                if user_size == 'small':
                    size_compatibility = breed_size in ['small', 'medium']
                elif user_size == 'medium':
                    size_compatibility = breed_size in ['small', 'medium', 'large']
                elif user_size == 'large':
                    size_compatibility = breed_size in ['medium', 'large']
                else:
                    size_compatibility = True

                if not size_compatibility:
                    continue

            # 獲取噪音資訊
            noise_info = breed_noise_info.get(breed, {
                "noise_notes": "Noise information not available",
                "noise_level": "Unknown",
                "source": "N/A"
            })

            # 將噪音資訊整合到品種資訊中
            breed_info['noise_info'] = noise_info

            # 計算基礎相容性分數
            compatibility_scores = calculate_compatibility_score(breed_info, user_prefs)

            # 計算品種特定加分
            breed_bonus = 0.0

            # 壽命加分
            try:
                lifespan = breed_info.get('Lifespan', '10-12 years')
                years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
                longevity_bonus = min(0.02, (max(years) - 10) * 0.005)
                breed_bonus += longevity_bonus
            except:
                pass

            # 性格特徵加分
            temperament = breed_info.get('Temperament', '').lower()
            positive_traits = ['friendly', 'gentle', 'affectionate', 'intelligent']
            negative_traits = ['aggressive', 'stubborn', 'dominant']

            breed_bonus += sum(0.01 for trait in positive_traits if trait in temperament)
            breed_bonus -= sum(0.01 for trait in negative_traits if trait in temperament)

            # 與孩童相容性加分
            if user_prefs.has_children:
                if breed_info.get('Good with Children') == 'Yes':
                    breed_bonus += 0.02
                elif breed_info.get('Good with Children') == 'No':
                    breed_bonus -= 0.03

            # 噪音相關加分
            if user_prefs.noise_tolerance == 'low':
                if noise_info['noise_level'].lower() == 'high':
                    breed_bonus -= 0.03
                elif noise_info['noise_level'].lower() == 'low':
                    breed_bonus += 0.02
            elif user_prefs.noise_tolerance == 'high':
                if noise_info['noise_level'].lower() == 'high':
                    breed_bonus += 0.01

            # 計算最終分數並加入自然變異
            breed_hash = hash(breed)
            random.seed(breed_hash)

            # Add small natural variation to avoid identical scores
            natural_variation = random.uniform(-0.008, 0.008)
            breed_bonus = round(breed_bonus + natural_variation, 4)
            final_score = round(min(1.0, compatibility_scores['overall'] + breed_bonus), 4)

            recommendations.append({
                'breed': breed,
                'base_score': round(compatibility_scores['overall'], 4),
                'bonus_score': round(breed_bonus, 4),
                'final_score': final_score,
                'scores': compatibility_scores,
                'info': breed_info,
                'noise_info': noise_info
            })

        print(f"Breeds after filtering: {len(recommendations)}")

        # 按照 final_score 排序
        recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed']))

        # 修正後的推薦選擇邏輯,移除有問題的分數比較
        final_recommendations = []

        # 直接選取前 top_n 個品種,確保返回完整數量
        for i, rec in enumerate(recommendations[:top_n]):
            rec['rank'] = i + 1
            final_recommendations.append(rec)

        print(f"Final recommendations count: {len(final_recommendations)}")

        # 驗證最終排序
        for i in range(len(final_recommendations)-1):
            current = final_recommendations[i]
            next_rec = final_recommendations[i+1]

            if current['final_score'] < next_rec['final_score']:
                print(f"Warning: Sorting error detected!")
                print(f"#{i+1} {current['breed']}: {current['final_score']}")
                print(f"#{i+2} {next_rec['breed']}: {next_rec['final_score']}")

                # 交換位置
                final_recommendations[i], final_recommendations[i+1] = \
                    final_recommendations[i+1], final_recommendations[i]

        # 打印最終結果以供驗證
        print("\nFinal Rankings:")
        for rec in final_recommendations:
            print(f"#{rec['rank']} {rec['breed']}")
            print(f"Base Score: {rec['base_score']:.4f}")
            print(f"Bonus: {rec['bonus_score']:.4f}")
            print(f"Final Score: {rec['final_score']:.4f}\n")

        return final_recommendations

    except Exception as e:
        print(f"Error in get_breed_recommendations: {str(e)}")
        print(f"Traceback: {traceback.format_exc()}")


def _format_dimension_scores(dimension_scores: Dict) -> str:
    """Format individual dimension scores as badges"""
    if not dimension_scores:
        return ""

    badges_html = '<div class="dimension-badges">'

    for dimension, score in dimension_scores.items():
        if isinstance(score, (int, float)):
            score_percent = score * 100
        else:
            score_percent = 75  # default

        if score_percent >= 80:
            badge_class = "badge-high"
        elif score_percent >= 60:
            badge_class = "badge-medium"
        else:
            badge_class = "badge-low"

        dimension_label = dimension.replace('_', ' ').title()
        badges_html += f'''
        <span class="dimension-badge {badge_class}">
            {dimension_label}: {score_percent:.0f}%
        </span>
        '''

    badges_html += '</div>'
    return badges_html


def calculate_breed_bonus_factors(breed_info: dict, user_prefs: 'UserPreferences') -> tuple:
    """計算品種額外加分因素並返回原因列表"""
    bonus = 0.0
    reasons = []
    
    # 壽命加分
    try:
        lifespan = breed_info.get('Lifespan', '10-12 years')
        years = [int(x) for x in lifespan.split('-')[0].split()[0:1]]
        if max(years) >= 12:
            bonus += 0.02
            reasons.append("Above-average lifespan")
    except:
        pass

    # 性格特徵加分  
    temperament = breed_info.get('Temperament', '').lower()
    if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']):
        bonus += 0.01
        reasons.append("Positive temperament traits")

    # 與孩童相容性
    if breed_info.get('Good with Children') == 'Yes':
        bonus += 0.01
        reasons.append("Excellent with children")

    return bonus, reasons


def generate_breed_characteristics_data(breed_info: dict) -> List[tuple]:
    """生成品種特徵資料列表"""
    return [
        ('Size', breed_info.get('Size', 'Unknown')),
        ('Exercise Needs', breed_info.get('Exercise Needs', 'Moderate')),
        ('Grooming Needs', breed_info.get('Grooming Needs', 'Moderate')),
        ('Good with Children', breed_info.get('Good with Children', 'Yes')),
        ('Temperament', breed_info.get('Temperament', '')),
        ('Lifespan', breed_info.get('Lifespan', '10-12 years')),
        ('Description', breed_info.get('Description', ''))
    ]


def parse_noise_information(noise_info: dict) -> tuple:
    """解析噪音資訊並返回結構化資料"""
    noise_notes = noise_info.get('noise_notes', '').split('\n')
    noise_characteristics = []
    barking_triggers = []
    noise_level = ''

    current_section = None
    for line in noise_notes:
        line = line.strip()
        if 'Typical noise characteristics:' in line:
            current_section = 'characteristics'
        elif 'Noise level:' in line:
            noise_level = line.replace('Noise level:', '').strip()
        elif 'Barking triggers:' in line:
            current_section = 'triggers'
        elif line.startswith('•'):
            if current_section == 'characteristics':
                noise_characteristics.append(line[1:].strip())
            elif current_section == 'triggers':
                barking_triggers.append(line[1:].strip())

    return noise_characteristics, barking_triggers, noise_level


def parse_health_information(health_info: dict) -> tuple:
    """解析健康資訊並返回結構化資料"""
    health_notes = health_info.get('health_notes', '').split('\n')
    health_considerations = []
    health_screenings = []

    current_section = None
    for line in health_notes:
        line = line.strip()
        if 'Common breed-specific health considerations' in line:
            current_section = 'considerations'
        elif 'Recommended health screenings:' in line:
            current_section = 'screenings'
        elif line.startswith('•'):
            if current_section == 'considerations':
                health_considerations.append(line[1:].strip())
            elif current_section == 'screenings':
                health_screenings.append(line[1:].strip())

    return health_considerations, health_screenings


def generate_dimension_scores_for_display(base_score: float, rank: int, breed: str, 
                                        semantic_score: float = 0.7,
                                        comparative_bonus: float = 0.0,
                                        lifestyle_bonus: float = 0.0,
                                        is_description_search: bool = False) -> dict:
    """為顯示生成維度分數"""
    random.seed(hash(breed) + rank)  # 一致的隨機性

    if is_description_search:
        # Description search: 創建更自然的分數分佈在50%-95%範圍內
        score_variance = 0.08 if base_score > 0.7 else 0.06
        
        scores = {
            'space': max(0.50, min(0.95,
                base_score * 0.92 + (lifestyle_bonus * 0.5) + random.uniform(-score_variance, score_variance))),
            'exercise': max(0.50, min(0.95,
                base_score * 0.88 + (lifestyle_bonus * 0.4) + random.uniform(-score_variance, score_variance))),
            'grooming': max(0.50, min(0.95,
                base_score * 0.85 + (comparative_bonus * 0.4) + random.uniform(-score_variance, score_variance))),
            'experience': max(0.50, min(0.95,
                base_score * 0.87 + (lifestyle_bonus * 0.3) + random.uniform(-score_variance, score_variance))),
            'noise': max(0.50, min(0.95,
                base_score * 0.83 + (lifestyle_bonus * 0.6) + random.uniform(-score_variance, score_variance))),
            'overall': base_score
        }
    else:
        # 傳統搜尋結果的分數結構會在呼叫處理中傳入
        scores = {'overall': base_score}
    
    return scores