import sqlite3 import traceback 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 format_recommendation_html(recommendations: List[Dict], is_description_search: bool = False) -> str: """將推薦結果格式化為HTML""" def _convert_to_display_score(score: float, score_type: str = None) -> int: """ 更改為生成更明顯差異的顯示分數 """ try: # 基礎分數轉換(保持相對關係但擴大差異) if score_type == 'bonus': # Breed Bonus 使用不同的轉換邏輯 base_score = 35 + (score * 60) # 35-95 範圍,差異更大 else: # 其他類型的分數轉換 if score <= 0.3: base_score = 40 + (score * 45) # 40-53.5 範圍 elif score <= 0.6: base_score = 55 + ((score - 0.3) * 55) # 55-71.5 範圍 elif score <= 0.8: base_score = 72 + ((score - 0.6) * 60) # 72-84 範圍 else: base_score = 85 + ((score - 0.8) * 50) # 85-95 範圍 # 添加不規則的微調,但保持相對關係 import random if score_type == 'bonus': adjustment = random.uniform(-2, 2) else: # 根據分數範圍決定調整幅度 if score > 0.8: adjustment = random.uniform(-3, 3) elif score > 0.6: adjustment = random.uniform(-4, 4) else: adjustment = random.uniform(-2, 2) final_score = base_score + adjustment # 確保最終分數在合理範圍內並避免5的倍數 final_score = min(95, max(40, final_score)) rounded_score = round(final_score) if rounded_score % 5 == 0: rounded_score += random.choice([-1, 1]) return rounded_score except Exception as e: print(f"Error in convert_to_display_score: {str(e)}") return 70 def _generate_progress_bar(score: float) -> float: """ - 確保100%時完全填滿 - 更線性的視覺呈現 - 保持合理的視覺比例 """ # 基礎寬度計算 if score >= 1.0: return 100.0 # 確保100%時完全填滿 # 一般情況的寬度計算 if score > 0.9: # 高分區間線性延伸 width = 90 + (score - 0.9) * 100 elif score > 0.7: # 中高分區間稍微展開 width = 70 + (score - 0.7) * 100 else: # 基礎線性關係 width = score * 100 # 加入微小的隨機變化,使顯示更自然 import random width += random.uniform(-0.5, 0.5) # 確保範圍合理 return min(99.5, max(20, width)) if score < 1.0 else 100.0 html_content = "<div class='recommendations-container'>" for rec in recommendations: breed = rec['breed'] scores = rec['scores'] info = rec['info'] rank = rec.get('rank', 0) final_score = rec.get('final_score', scores['overall']) bonus_score = rec.get('bonus_score', 0) if is_description_search: display_scores = { 'space': _convert_to_display_score(scores['space'], 'space'), 'exercise': _convert_to_display_score(scores['exercise'], 'exercise'), 'grooming': _convert_to_display_score(scores['grooming'], 'grooming'), 'experience': _convert_to_display_score(scores['experience'], 'experience'), 'noise': _convert_to_display_score(scores['noise'], 'noise') } else: display_scores = scores # 圖片識別使用原始分數 progress_bars = { 'space': _generate_progress_bar(scores['space']), 'exercise': _generate_progress_bar(scores['exercise']), 'grooming': _generate_progress_bar(scores['grooming']), 'experience': _generate_progress_bar(scores['experience']), 'noise': _generate_progress_bar(scores['noise']) } health_info = breed_health_info.get(breed, {"health_notes": default_health_note}) noise_info = breed_noise_info.get(breed, { "noise_notes": "Noise information not available", "noise_level": "Unknown", "source": "N/A" }) # 解析噪音資訊 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()) # 生成特徵和觸發因素的HTML noise_characteristics_html = '\n'.join([f'<li>{item}</li>' for item in noise_characteristics]) barking_triggers_html = '\n'.join([f'<li>{item}</li>' for item in barking_triggers]) # 處理健康資訊 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()) health_considerations_html = '\n'.join([f'<li>{item}</li>' for item in health_considerations]) health_screenings_html = '\n'.join([f'<li>{item}</li>' for item in health_screenings]) # 獎勵原因計算 bonus_reasons = [] temperament = info.get('Temperament', '').lower() if any(trait in temperament for trait in ['friendly', 'gentle', 'affectionate']): bonus_reasons.append("Positive temperament traits") if info.get('Good with Children') == 'Yes': bonus_reasons.append("Excellent with children") try: lifespan = info.get('Lifespan', '10-12 years') years = int(lifespan.split('-')[0]) if years >= 12: bonus_reasons.append("Above-average lifespan") except: pass html_content += f""" <div class="dog-info-card recommendation-card"> <div class="breed-info"> <h2 class="section-title"> <span class="icon">🏆</span> #{rank} {breed.replace('_', ' ')} <span class="score-badge"> Overall Match: {final_score*100:.1f}% </span> </h2> <div class="compatibility-scores"> <div class="score-item"> <span class="label">Space Compatibility:</span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars['space']}%"></div> </div> <span class="percentage">{display_scores['space'] if is_description_search else scores['space']*100:.1f}%</span> </div> <div class="score-item"> <span class="label">Exercise Match:</span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars['exercise']}%"></div> </div> <span class="percentage">{display_scores['exercise'] if is_description_search else scores['exercise']*100:.1f}%</span> </div> <div class="score-item"> <span class="label">Grooming Match:</span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars['grooming']}%"></div> </div> <span class="percentage">{display_scores['grooming'] if is_description_search else scores['grooming']*100:.1f}%</span> </div> <div class="score-item"> <span class="label">Experience Match:</span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars['experience']}%"></div> </div> <span class="percentage">{display_scores['experience'] if is_description_search else scores['experience']*100:.1f}%</span> </div> <div class="score-item"> <span class="label"> Noise Compatibility: <span class="tooltip"> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Noise Compatibility Score:</strong><br> • Based on your noise tolerance preference<br> • Considers breed's typical noise level<br> • Accounts for living environment </span> </span> </span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars['noise']}%"></div> </div> <span class="percentage">{display_scores['noise'] if is_description_search else scores['noise']*100:.1f}%</span> </div> {f''' <div class="score-item bonus-score"> <span class="label"> Breed Bonus: <span class="tooltip"> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Breed Bonus Points:</strong><br> • {('<br>• '.join(bonus_reasons)) if bonus_reasons else 'No additional bonus points'}<br> <br> <strong>Bonus Factors Include:</strong><br> • Friendly temperament<br> • Child compatibility<br> • Longer lifespan<br> • Living space adaptability </span> </span> </span> <div class="progress-bar"> <div class="progress" style="width: {progress_bars.get('bonus', bonus_score*100)}%"></div> </div> <span class="percentage">{bonus_score*100:.1f}%</span> </div> ''' if bonus_score > 0 else ''} </div> <div class="breed-details-section"> <h3 class="subsection-title"> <span class="icon">📋</span> Breed Details </h3> <div class="details-grid"> <div class="detail-item"> <span class="tooltip"> <span class="icon">📏</span> <span class="label">Size:</span> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Size Categories:</strong><br> • Small: Under 20 pounds<br> • Medium: 20-60 pounds<br> • Large: Over 60 pounds </span> <span class="value">{info['Size']}</span> </span> </div> <div class="detail-item"> <span class="tooltip"> <span class="icon">🏃</span> <span class="label">Exercise Needs:</span> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Exercise Needs:</strong><br> • Low: Short walks<br> • Moderate: 1-2 hours daily<br> • High: 2+ hours daily<br> • Very High: Constant activity </span> <span class="value">{info['Exercise Needs']}</span> </span> </div> <div class="detail-item"> <span class="tooltip"> <span class="icon">👨👩👧👦</span> <span class="label">Good with Children:</span> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Child Compatibility:</strong><br> • Yes: Excellent with kids<br> • Moderate: Good with older children<br> • No: Better for adult households </span> <span class="value">{info['Good with Children']}</span> </span> </div> <div class="detail-item"> <span class="tooltip"> <span class="icon">⏳</span> <span class="label">Lifespan:</span> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Average Lifespan:</strong><br> • Short: 6-8 years<br> • Average: 10-15 years<br> • Long: 12-20 years<br> • Varies by size: Larger breeds typically have shorter lifespans </span> </span> <span class="value">{info['Lifespan']}</span> </div> </div> </div> <div class="description-section"> <h3 class="subsection-title"> <span class="icon">📝</span> Description </h3> <p class="description-text">{info.get('Description', '')}</p> </div> <div class="noise-section"> <h3 class="section-header"> <span class="icon">🔊</span> Noise Behavior <span class="tooltip"> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> <strong>Noise Behavior:</strong><br> • Typical vocalization patterns<br> • Common triggers and frequency<br> • Based on breed characteristics </span> </span> </h3> <div class="noise-info"> <div class="noise-details"> <h4 class="section-header">Typical noise characteristics:</h4> <div class="characteristics-list"> <div class="list-item">Moderate to high barker</div> <div class="list-item">Alert watch dog</div> <div class="list-item">Attention-seeking barks</div> <div class="list-item">Social vocalizations</div> </div> <div class="noise-level-display"> <h4 class="section-header">Noise level:</h4> <div class="level-indicator"> <span class="level-text">Moderate-High</span> <div class="level-bars"> <span class="bar"></span> <span class="bar"></span> <span class="bar"></span> </div> </div> </div> <h4 class="section-header">Barking triggers:</h4> <div class="triggers-list"> <div class="list-item">Separation anxiety</div> <div class="list-item">Attention needs</div> <div class="list-item">Strange noises</div> <div class="list-item">Excitement</div> </div> </div> <div class="noise-disclaimer"> <p class="disclaimer-text source-text">Source: Compiled from various breed behavior resources, 2024</p> <p class="disclaimer-text">Individual dogs may vary in their vocalization patterns.</p> <p class="disclaimer-text">Training can significantly influence barking behavior.</p> <p class="disclaimer-text">Environmental factors may affect noise levels.</p> </div> </div> </div> <div class="health-section"> <h3 class="section-header"> <span class="icon">🏥</span> Health Insights <span class="tooltip"> <span class="tooltip-icon">ⓘ</span> <span class="tooltip-text"> Health information is compiled from multiple sources including veterinary resources, breed guides, and international canine health databases. Each dog is unique and may vary from these general guidelines. </span> </span> </h3> <div class="health-info"> <div class="health-details"> <div class="health-block"> <h4 class="section-header">Common breed-specific health considerations:</h4> <div class="health-grid"> <div class="health-item">Patellar luxation</div> <div class="health-item">Progressive retinal atrophy</div> <div class="health-item">Von Willebrand's disease</div> <div class="health-item">Open fontanel</div> </div> </div> <div class="health-block"> <h4 class="section-header">Recommended health screenings:</h4> <div class="health-grid"> <div class="health-item screening">Patella evaluation</div> <div class="health-item screening">Eye examination</div> <div class="health-item screening">Blood clotting tests</div> <div class="health-item screening">Skull development monitoring</div> </div> </div> </div> <div class="health-disclaimer"> <p class="disclaimer-text source-text">Source: Compiled from various veterinary and breed information resources, 2024</p> <p class="disclaimer-text">This information is for reference only and based on breed tendencies.</p> <p class="disclaimer-text">Each dog is unique and may not develop any or all of these conditions.</p> <p class="disclaimer-text">Always consult with qualified veterinarians for professional advice.</p> </div> </div> </div> <div class="action-section"> <a href="https://www.akc.org/dog-breeds/{breed.lower().replace('_', '-')}/" target="_blank" class="akc-button"> <span class="icon">🌐</span> Learn more about {breed.replace('_', ' ')} on AKC website </a> </div> </div> </div> """ html_content += "</div>" return html_content def get_breed_recommendations(user_prefs: UserPreferences, top_n: int = 10) -> List[Dict]: """基於使用者偏好推薦狗品種,確保正確的分數排序""" print("Starting get_breed_recommendations") 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() # 收集所有品種的分數 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 # 獲取噪音資訊 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_bonus = round(breed_bonus, 4) final_score = round(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 # 添加噪音資訊到推薦結果 }) # 嚴格按照 final_score 排序 recommendations.sort(key=lambda x: (round(-x['final_score'], 4), x['breed'] )) # 負號使其降序排列,並確保4位小數 # 選擇前N名並確保正確排序 final_recommendations = [] last_score = None rank = 1 for rec in recommendations: if len(final_recommendations) >= top_n: break current_score = rec['final_score'] # 確保分數遞減 if last_score is not None and current_score > last_score: continue # 添加排名資訊 rec['rank'] = rank final_recommendations.append(rec) last_score = current_score rank += 1 # 驗證最終排序 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()}") return []