Spaces:
Running
on
Zero
Running
on
Zero
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 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 | |