DawnC commited on
Commit
6be27f7
1 Parent(s): cc27278

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1140 -0
app.py ADDED
@@ -0,0 +1,1140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import torch
4
+ import torch.nn as nn
5
+ import gradio as gr
6
+ from torchvision.models import efficientnet_v2_m, EfficientNet_V2_M_Weights
7
+ from torchvision.ops import nms, box_iou
8
+ import torch.nn.functional as F
9
+ from torchvision import transforms
10
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
11
+ from data_manager import get_dog_description
12
+ from urllib.parse import quote
13
+ from ultralytics import YOLO
14
+ import asyncio
15
+ import traceback
16
+
17
+
18
+ model_yolo = YOLO('yolov8l.pt')
19
+
20
+
21
+ dog_breeds = ["Afghan_Hound", "African_Hunting_Dog", "Airedale", "American_Staffordshire_Terrier",
22
+ "Appenzeller", "Australian_Terrier", "Bedlington_Terrier", "Bernese_Mountain_Dog",
23
+ "Blenheim_Spaniel", "Border_Collie", "Border_Terrier", "Boston_Bull", "Bouvier_Des_Flandres",
24
+ "Brabancon_Griffon", "Brittany_Spaniel", "Cardigan", "Chesapeake_Bay_Retriever",
25
+ "Chihuahua", "Dandie_Dinmont", "Doberman", "English_Foxhound", "English_Setter",
26
+ "English_Springer", "EntleBucher", "Eskimo_Dog", "French_Bulldog", "German_Shepherd",
27
+ "German_Short-Haired_Pointer", "Gordon_Setter", "Great_Dane", "Great_Pyrenees",
28
+ "Greater_Swiss_Mountain_Dog", "Ibizan_Hound", "Irish_Setter", "Irish_Terrier",
29
+ "Irish_Water_Spaniel", "Irish_Wolfhound", "Italian_Greyhound", "Japanese_Spaniel",
30
+ "Kerry_Blue_Terrier", "Labrador_Retriever", "Lakeland_Terrier", "Leonberg", "Lhasa",
31
+ "Maltese_Dog", "Mexican_Hairless", "Newfoundland", "Norfolk_Terrier", "Norwegian_Elkhound",
32
+ "Norwich_Terrier", "Old_English_Sheepdog", "Pekinese", "Pembroke", "Pomeranian",
33
+ "Rhodesian_Ridgeback", "Rottweiler", "Saint_Bernard", "Saluki", "Samoyed",
34
+ "Scotch_Terrier", "Scottish_Deerhound", "Sealyham_Terrier", "Shetland_Sheepdog",
35
+ "Shih-Tzu", "Siberian_Husky", "Staffordshire_Bullterrier", "Sussex_Spaniel",
36
+ "Tibetan_Mastiff", "Tibetan_Terrier", "Walker_Hound", "Weimaraner",
37
+ "Welsh_Springer_Spaniel", "West_Highland_White_Terrier", "Yorkshire_Terrier",
38
+ "Affenpinscher", "Basenji", "Basset", "Beagle", "Black-and-Tan_Coonhound", "Bloodhound",
39
+ "Bluetick", "Borzoi", "Boxer", "Briard", "Bull_Mastiff", "Cairn", "Chow", "Clumber",
40
+ "Cocker_Spaniel", "Collie", "Curly-Coated_Retriever", "Dhole", "Dingo",
41
+ "Flat-Coated_Retriever", "Giant_Schnauzer", "Golden_Retriever", "Groenendael", "Keeshond",
42
+ "Kelpie", "Komondor", "Kuvasz", "Malamute", "Malinois", "Miniature_Pinscher",
43
+ "Miniature_Poodle", "Miniature_Schnauzer", "Otterhound", "Papillon", "Pug", "Redbone",
44
+ "Schipperke", "Silky_Terrier", "Soft-Coated_Wheaten_Terrier", "Standard_Poodle",
45
+ "Standard_Schnauzer", "Toy_Poodle", "Toy_Terrier", "Vizsla", "Whippet",
46
+ "Wire-Haired_Fox_Terrier"]
47
+
48
+ class MultiHeadAttention(nn.Module):
49
+
50
+ def __init__(self, in_dim, num_heads=8):
51
+ super().__init__()
52
+ self.num_heads = num_heads
53
+ self.head_dim = max(1, in_dim // num_heads)
54
+ self.scaled_dim = self.head_dim * num_heads
55
+ self.fc_in = nn.Linear(in_dim, self.scaled_dim)
56
+ self.query = nn.Linear(self.scaled_dim, self.scaled_dim)
57
+ self.key = nn.Linear(self.scaled_dim, self.scaled_dim)
58
+ self.value = nn.Linear(self.scaled_dim, self.scaled_dim)
59
+ self.fc_out = nn.Linear(self.scaled_dim, in_dim)
60
+
61
+ def forward(self, x):
62
+ N = x.shape[0]
63
+ x = self.fc_in(x)
64
+ q = self.query(x).view(N, self.num_heads, self.head_dim)
65
+ k = self.key(x).view(N, self.num_heads, self.head_dim)
66
+ v = self.value(x).view(N, self.num_heads, self.head_dim)
67
+
68
+ energy = torch.einsum("nqd,nkd->nqk", [q, k])
69
+ attention = F.softmax(energy / (self.head_dim ** 0.5), dim=2)
70
+
71
+ out = torch.einsum("nqk,nvd->nqd", [attention, v])
72
+ out = out.reshape(N, self.scaled_dim)
73
+ out = self.fc_out(out)
74
+ return out
75
+
76
+ class BaseModel(nn.Module):
77
+ def __init__(self, num_classes, device='cuda' if torch.cuda.is_available() else 'cpu'):
78
+ super().__init__()
79
+ self.device = device
80
+ self.backbone = efficientnet_v2_m(weights=EfficientNet_V2_M_Weights.IMAGENET1K_V1)
81
+ self.feature_dim = self.backbone.classifier[1].in_features
82
+ self.backbone.classifier = nn.Identity()
83
+
84
+ self.num_heads = max(1, min(8, self.feature_dim // 64))
85
+ self.attention = MultiHeadAttention(self.feature_dim, num_heads=self.num_heads)
86
+
87
+ self.classifier = nn.Sequential(
88
+ nn.LayerNorm(self.feature_dim),
89
+ nn.Dropout(0.3),
90
+ nn.Linear(self.feature_dim, num_classes)
91
+ )
92
+
93
+ self.to(device)
94
+
95
+ def forward(self, x):
96
+ x = x.to(self.device)
97
+ features = self.backbone(x)
98
+ attended_features = self.attention(features)
99
+ logits = self.classifier(attended_features)
100
+ return logits, attended_features
101
+
102
+
103
+ num_classes = 120
104
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
105
+ model = BaseModel(num_classes=num_classes, device=device)
106
+
107
+ checkpoint = torch.load('best_model_81_dog.pth', map_location=torch.device('cpu'))
108
+ model.load_state_dict(checkpoint['model_state_dict'])
109
+
110
+ # evaluation mode
111
+ model.eval()
112
+
113
+ # Image preprocessing function
114
+ def preprocess_image(image):
115
+ # If the image is numpy.ndarray turn into PIL.Image
116
+ if isinstance(image, np.ndarray):
117
+ image = Image.fromarray(image)
118
+
119
+ # Use torchvision.transforms to process images
120
+ transform = transforms.Compose([
121
+ transforms.Resize((224, 224)),
122
+ transforms.ToTensor(),
123
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
124
+ ])
125
+
126
+ return transform(image).unsqueeze(0)
127
+
128
+
129
+ def get_akc_breeds_link(breed):
130
+ base_url = "https://www.akc.org/dog-breeds/"
131
+ breed_url = breed.lower().replace('_', '-')
132
+ return f"{base_url}{breed_url}/"
133
+
134
+
135
+ async def predict_single_dog(image):
136
+ image_tensor = preprocess_image(image)
137
+ with torch.no_grad():
138
+ output = model(image_tensor)
139
+ logits = output[0] if isinstance(output, tuple) else output
140
+ probabilities = F.softmax(logits, dim=1)
141
+ topk_probs, topk_indices = torch.topk(probabilities, k=3)
142
+ top1_prob = topk_probs[0][0].item()
143
+ topk_breeds = [dog_breeds[idx.item()] for idx in topk_indices[0]]
144
+
145
+ # Calculate relative probabilities for display
146
+ raw_probs = [prob.item() for prob in topk_probs[0]]
147
+ sum_probs = sum(raw_probs)
148
+ relative_probs = [f"{(prob/sum_probs * 100):.2f}%" for prob in raw_probs]
149
+
150
+ return top1_prob, topk_breeds, relative_probs
151
+
152
+
153
+ async def detect_multiple_dogs(image, conf_threshold=0.3, iou_threshold=0.45):
154
+ results = model_yolo(image, conf=conf_threshold, iou=iou_threshold)[0]
155
+ dogs = []
156
+ boxes = []
157
+ for box in results.boxes:
158
+ if box.cls == 16: # COCO dataset class for dog is 16
159
+ xyxy = box.xyxy[0].tolist()
160
+ confidence = box.conf.item()
161
+ boxes.append((xyxy, confidence))
162
+
163
+ if not boxes:
164
+ dogs.append((image, 1.0, [0, 0, image.width, image.height]))
165
+ else:
166
+ nms_boxes = non_max_suppression(boxes, iou_threshold)
167
+
168
+ for box, confidence in nms_boxes:
169
+ x1, y1, x2, y2 = box
170
+ w, h = x2 - x1, y2 - y1
171
+ x1 = max(0, x1 - w * 0.05)
172
+ y1 = max(0, y1 - h * 0.05)
173
+ x2 = min(image.width, x2 + w * 0.05)
174
+ y2 = min(image.height, y2 + h * 0.05)
175
+ cropped_image = image.crop((x1, y1, x2, y2))
176
+ dogs.append((cropped_image, confidence, [x1, y1, x2, y2]))
177
+
178
+ return dogs
179
+
180
+
181
+ def non_max_suppression(boxes, iou_threshold):
182
+ keep = []
183
+ boxes = sorted(boxes, key=lambda x: x[1], reverse=True)
184
+ while boxes:
185
+ current = boxes.pop(0)
186
+ keep.append(current)
187
+ boxes = [box for box in boxes if calculate_iou(current[0], box[0]) < iou_threshold]
188
+ return keep
189
+
190
+
191
+ def calculate_iou(box1, box2):
192
+ x1 = max(box1[0], box2[0])
193
+ y1 = max(box1[1], box2[1])
194
+ x2 = min(box1[2], box2[2])
195
+ y2 = min(box1[3], box2[3])
196
+
197
+ intersection = max(0, x2 - x1) * max(0, y2 - y1)
198
+ area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
199
+ area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
200
+
201
+ iou = intersection / float(area1 + area2 - intersection)
202
+ return iou
203
+
204
+
205
+ async def process_single_dog(image):
206
+ top1_prob, topk_breeds, relative_probs = await predict_single_dog(image)
207
+
208
+ # Case 1: Low confidence - unclear image or breed not in dataset
209
+ if top1_prob < 0.15:
210
+ error_message = '''
211
+ <div class="dog-info-card">
212
+ <div class="breed-info">
213
+ <p class="warning-message">
214
+ <span class="icon">⚠️</span>
215
+ The image is unclear or the breed is not in the dataset. Please upload a clearer image of a dog.
216
+ </p>
217
+ </div>
218
+ </div>
219
+ '''
220
+ initial_state = {
221
+ "explanation": error_message,
222
+ "image": None,
223
+ "is_multi_dog": False
224
+ }
225
+ return error_message, None, initial_state
226
+
227
+ breed = topk_breeds[0]
228
+
229
+ # Case 2: High confidence - single breed result
230
+ if top1_prob >= 0.45:
231
+ description = get_dog_description(breed)
232
+ formatted_description = format_description_html(description, breed) # 使用 format_description_html
233
+ html_content = f'''
234
+ <div class="dog-info-card">
235
+ <div class="breed-info">
236
+ {formatted_description}
237
+ </div>
238
+ </div>
239
+ '''
240
+ initial_state = {
241
+ "explanation": html_content,
242
+ "image": image,
243
+ "is_multi_dog": False
244
+ }
245
+ return html_content, image, initial_state
246
+
247
+ # Case 3: Medium confidence - show top 3 breeds with relative probabilities
248
+ else:
249
+ breeds_html = ""
250
+ for i, (breed, prob) in enumerate(zip(topk_breeds, relative_probs)):
251
+ description = get_dog_description(breed)
252
+ formatted_description = format_description_html(description, breed) # 使用 format_description_html
253
+ breeds_html += f'''
254
+ <div class="dog-info-card">
255
+ <div class="breed-info">
256
+ <div class="breed-header">
257
+ <span class="breed-name">Breed {i+1}: {breed}</span>
258
+ <span class="confidence-badge">Confidence: {prob}</span>
259
+ </div>
260
+ {formatted_description}
261
+ </div>
262
+ </div>
263
+ '''
264
+
265
+ initial_state = {
266
+ "explanation": breeds_html,
267
+ "image": image,
268
+ "is_multi_dog": False
269
+ }
270
+ return breeds_html, image, initial_state
271
+
272
+
273
+ async def predict(image):
274
+ if image is None:
275
+ return "Please upload an image to start.", None, None
276
+
277
+ try:
278
+ if isinstance(image, np.ndarray):
279
+ image = Image.fromarray(image)
280
+
281
+ dogs = await detect_multiple_dogs(image)
282
+ # 更新顏色組合
283
+ single_dog_color = '#34C759' # 清爽的綠色作為單狗顏色
284
+ color_list = [
285
+ '#FF5733', # 珊瑚紅
286
+ '#28A745', # 深綠色
287
+ '#3357FF', # 寶藍色
288
+ '#FF33F5', # 粉紫色
289
+ '#FFB733', # 橙黃色
290
+ '#33FFF5', # 青藍色
291
+ '#A233FF', # 紫色
292
+ '#FF3333', # 紅色
293
+ '#33FFB7', # 青綠色
294
+ '#FFE033' # 金黃色
295
+ ]
296
+ annotated_image = image.copy()
297
+ draw = ImageDraw.Draw(annotated_image)
298
+
299
+ try:
300
+ font = ImageFont.truetype("arial.ttf", 24)
301
+ except:
302
+ font = ImageFont.load_default()
303
+
304
+ dogs_info = ""
305
+
306
+ for i, (cropped_image, detection_confidence, box) in enumerate(dogs):
307
+ color = single_dog_color if len(dogs) == 1 else color_list[i % len(color_list)]
308
+
309
+ # 優化圖片上的標記
310
+ draw.rectangle(box, outline=color, width=4)
311
+ label = f"Dog {i+1}"
312
+ label_bbox = draw.textbbox((0, 0), label, font=font)
313
+ label_width = label_bbox[2] - label_bbox[0]
314
+ label_height = label_bbox[3] - label_bbox[1]
315
+
316
+ label_x = box[0] + 5
317
+ label_y = box[1] + 5
318
+ draw.rectangle(
319
+ [label_x - 2, label_y - 2, label_x + label_width + 4, label_y + label_height + 4],
320
+ fill='white',
321
+ outline=color,
322
+ width=2
323
+ )
324
+ draw.text((label_x, label_y), label, fill=color, font=font)
325
+
326
+ top1_prob, topk_breeds, relative_probs = await predict_single_dog(cropped_image)
327
+ combined_confidence = detection_confidence * top1_prob
328
+
329
+ # 開始資訊卡片
330
+ dogs_info += f'<div class="dog-info-card" style="border-left: 6px solid {color};">'
331
+
332
+ if combined_confidence < 0.15:
333
+ dogs_info += f'''
334
+ <div class="dog-info-header" style="background-color: {color}10;">
335
+ <span class="dog-label" style="color: {color};">Dog {i+1}</span>
336
+ </div>
337
+ <div class="breed-info">
338
+ <p class="warning-message">
339
+ <span class="icon">⚠️</span>
340
+ The image is unclear or the breed is not in the dataset. Please upload a clearer image.
341
+ </p>
342
+ </div>
343
+ '''
344
+ elif top1_prob >= 0.45:
345
+ breed = topk_breeds[0]
346
+ description = get_dog_description(breed)
347
+ dogs_info += f'''
348
+ <div class="dog-info-header" style="background-color: {color}10;">
349
+ <span class="dog-label" style="color: {color};">
350
+ <span class="icon">🐾</span> {breed}
351
+ </span>
352
+ </div>
353
+ <div class="breed-info">
354
+ <h2 class="section-title">
355
+ <span class="icon">📋</span> BASIC INFORMATION
356
+ </h2>
357
+ <div class="info-section">
358
+ <div class="info-item">
359
+ <span class="tooltip tooltip-left">
360
+ <span class="icon">📏</span>
361
+ <span class="label">Size:</span>
362
+ <span class="tooltip-icon">ⓘ</span>
363
+ <span class="tooltip-text">
364
+ <strong>Size Categories:</strong><br>
365
+ • Small: Under 20 pounds<br>
366
+ • Medium: 20-60 pounds<br>
367
+ • Large: Over 60 pounds<br>
368
+ • Giant: Over 100 pounds<br>
369
+ • Varies: Depends on variety
370
+ </span>
371
+ </span>
372
+ <span class="value">{description['Size']}</span>
373
+ </div>
374
+ <div class="info-item">
375
+ <span class="tooltip">
376
+ <span class="icon">⏳</span>
377
+ <span class="label">Lifespan:</span>
378
+ <span class="tooltip-icon">ⓘ</span>
379
+ <span class="tooltip-text">
380
+ <strong>Average Lifespan:</strong><br>
381
+ • Short: 6-8 years<br>
382
+ • Average: 10-15 years<br>
383
+ • Long: 12-20 years<br>
384
+ • Varies by size: Larger breeds typically have shorter lifespans
385
+ </span>
386
+ </span>
387
+ <span class="value">{description['Lifespan']}</span>
388
+ </div>
389
+ </div>
390
+
391
+ <h2 class="section-title">
392
+ <span class="icon">🐕</span> TEMPERAMENT & PERSONALITY
393
+ </h2>
394
+ <div class="temperament-section">
395
+ <span class="tooltip">
396
+ <span class="value">{description['Temperament']}</span>
397
+ <span class="tooltip-icon">ⓘ</span>
398
+ <span class="tooltip-text">
399
+ <strong>Temperament Guide:</strong><br>
400
+ • Describes the dog's natural behavior and personality<br>
401
+ • Important for matching with owner's lifestyle<br>
402
+ • Can be influenced by training and socialization
403
+ </span>
404
+ </span>
405
+ </div>
406
+
407
+ <h2 class="section-title">
408
+ <span class="icon">💪</span> CARE REQUIREMENTS
409
+ </h2>
410
+ <div class="care-section">
411
+ <div class="info-item">
412
+ <span class="tooltip tooltip-left">
413
+ <span class="icon">🏃</span>
414
+ <span class="label">Exercise:</span>
415
+ <span class="tooltip-icon">ⓘ</span>
416
+ <span class="tooltip-text">
417
+ <strong>Exercise Needs:</strong><br>
418
+ • Low: Short walks and play sessions<br>
419
+ • Moderate: 1-2 hours of daily activity<br>
420
+ • High: Extensive exercise (2+ hours/day)<br>
421
+ • Very High: Constant activity and mental stimulation needed
422
+ </span>
423
+ </span>
424
+ <span class="value">{description['Exercise Needs']}</span>
425
+ </div>
426
+ <div class="info-item">
427
+ <span class="tooltip">
428
+ <span class="icon">✂️</span>
429
+ <span class="label">Grooming:</span>
430
+ <span class="tooltip-icon">ⓘ</span>
431
+ <span class="tooltip-text">
432
+ <strong>Grooming Requirements:</strong><br>
433
+ • Low: Basic brushing, occasional baths<br>
434
+ • Moderate: Weekly brushing, occasional grooming<br>
435
+ • High: Daily brushing, frequent professional grooming needed<br>
436
+ • Professional care recommended for all levels
437
+ </span>
438
+ </span>
439
+ <span class="value">{description['Grooming Needs']}</span>
440
+ </div>
441
+ <div class="info-item">
442
+ <span class="tooltip">
443
+ <span class="icon">⭐</span>
444
+ <span class="label">Care Level:</span>
445
+ <span class="tooltip-icon">ⓘ</span>
446
+ <span class="tooltip-text">
447
+ <strong>Care Level Explained:</strong><br>
448
+ • Low: Basic care and attention needed<br>
449
+ • Moderate: Regular care and routine needed<br>
450
+ • High: Significant time and attention needed<br>
451
+ • Very High: Extensive care, training and attention required
452
+ </span>
453
+ </span>
454
+ <span class="value">{description['Care Level']}</span>
455
+ </div>
456
+ </div>
457
+
458
+ <h2 class="section-title">
459
+ <span class="icon">👨‍👩‍👧‍👦</span> FAMILY COMPATIBILITY
460
+ </h2>
461
+ <div class="family-section">
462
+ <div class="info-item">
463
+ <span class="tooltip">
464
+ <span class="icon"></span>
465
+ <span class="label">Good with Children:</span>
466
+ <span class="tooltip-icon">ⓘ</span>
467
+ <span class="tooltip-text">
468
+ <strong>Child Compatibility:</strong><br>
469
+ • Yes: Excellent with kids, patient and gentle<br>
470
+ • Moderate: Good with older children<br>
471
+ • No: Better suited for adult households
472
+ </span>
473
+ </span>
474
+ <span class="value">{description['Good with Children']}</span>
475
+ </div>
476
+ </div>
477
+
478
+ <h2 class="section-title">
479
+ <span class="icon">📝</span> DESCRIPTION
480
+ </h2>
481
+ <div class="description-section">
482
+ <p>{description.get('Description', '')}</p>
483
+ </div>
484
+
485
+ <div class="action-section">
486
+ <a href="{get_akc_breeds_link(breed)}" target="_blank" class="akc-button">
487
+ <span class="icon">🌐</span>
488
+ Learn more about {breed} on AKC website
489
+ </a>
490
+ </div>
491
+ </div>
492
+ '''
493
+ else:
494
+ dogs_info += f'''
495
+ <div class="dog-info-header" style="background-color: {color}10;">
496
+ <span class="dog-label" style="color: {color};">Dog {i+1}</span>
497
+ </div>
498
+ <div class="breed-info">
499
+ <div class="model-uncertainty-note">
500
+ <span class="icon">ℹ️</span>
501
+ Note: The model is showing some uncertainty in its predictions.
502
+ Here are the most likely breeds based on the available visual features.
503
+ </div>
504
+ <div class="breeds-list">
505
+ '''
506
+
507
+ for j, (breed, prob) in enumerate(zip(topk_breeds, relative_probs)):
508
+ description = get_dog_description(breed)
509
+ dogs_info += f'''
510
+ <div class="breed-option uncertainty-mode">
511
+ <div class="breed-header">
512
+ <span class="option-number">Option {j+1}</span>
513
+ <span class="breed-name">{breed}</span>
514
+ <span class="confidence-badge" style="background-color: {color}20; color: {color};">
515
+ Confidence: {prob}
516
+ </span>
517
+ </div>
518
+ <div class="breed-content">
519
+ {format_description_html(description, breed)}
520
+ </div>
521
+ </div>
522
+ '''
523
+ dogs_info += '</div></div>'
524
+
525
+ dogs_info += '</div>'
526
+
527
+
528
+ html_output = f"""
529
+ <style>
530
+ .dog-info-card {{
531
+ border: 1px solid #e1e4e8;
532
+ margin: 40px 0; /* 增加卡片間距 */
533
+ padding: 0;
534
+ border-radius: 12px;
535
+ box-shadow: 0 2px 12px rgba(0,0,0,0.08);
536
+ overflow: hidden;
537
+ transition: all 0.3s ease;
538
+ background: white;
539
+ }}
540
+
541
+ .dog-info-card:hover {{
542
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
543
+ }}
544
+
545
+ .dog-info-header {{
546
+ padding: 24px 28px; /* 增加內距 */
547
+ margin: 0;
548
+ font-size: 22px;
549
+ font-weight: bold;
550
+ border-bottom: 1px solid #e1e4e8;
551
+ }}
552
+
553
+ .breed-info {{
554
+ padding: 28px; /* 增加整體內距 */
555
+ line-height: 1.6;
556
+ }}
557
+
558
+ .section-title {{
559
+ font-size: 1.3em;
560
+ font-weight: 700;
561
+ color: #2c3e50;
562
+ margin: 32px 0 20px 0;
563
+ padding: 12px 0;
564
+ border-bottom: 2px solid #e1e4e8;
565
+ text-transform: uppercase;
566
+ letter-spacing: 0.5px;
567
+ display: flex;
568
+ align-items: center;
569
+ gap: 8px;
570
+ position: relative;
571
+ }}
572
+
573
+ .icon {{
574
+ font-size: 1.2em;
575
+ display: inline-flex;
576
+ align-items: center;
577
+ justify-content: center;
578
+ }}
579
+
580
+ .info-section, .care-section, .family-section {{
581
+ display: flex;
582
+ flex-wrap: wrap;
583
+ gap: 16px;
584
+ margin-bottom: 28px; /* 增加底部間距 */
585
+ padding: 20px; /* 增加內距 */
586
+ background: #f8f9fa;
587
+ border-radius: 12px;
588
+ border: 1px solid #e1e4e8; /* 添加邊框 */
589
+ }}
590
+
591
+ .info-item {{
592
+ background: white; /* 改為白色背景 */
593
+ padding: 14px 18px; /* 增加內距 */
594
+ border-radius: 8px;
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 10px;
598
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
599
+ border: 1px solid #e1e4e8;
600
+ flex: 1 1 auto;
601
+ min-width: 200px;
602
+ }}
603
+
604
+ .label {{
605
+ color: #666;
606
+ font-weight: 600;
607
+ font-size: 1.1rem;
608
+ }}
609
+
610
+ .value {{
611
+ color: #2c3e50;
612
+ font-weight: 500;
613
+ font-size: 1.1rem;
614
+ }}
615
+
616
+ .temperament-section {{
617
+ background: #f8f9fa;
618
+ padding: 20px; /* 增加內距 */
619
+ border-radius: 12px;
620
+ margin-bottom: 28px; /* 增加間距 */
621
+ color: #444;
622
+ border: 1px solid #e1e4e8; /* 添加邊框 */
623
+ }}
624
+
625
+ .description-section {{
626
+ background: #f8f9fa;
627
+ padding: 24px; /* 增加內距 */
628
+ border-radius: 12px;
629
+ margin: 28px 0; /* 增加上下間距 */
630
+ line-height: 1.8;
631
+ color: #444;
632
+ border: 1px solid #e1e4e8; /* 添加邊框 */
633
+ fontsize: 1.1rem;
634
+ }}
635
+
636
+ .description-section p {{
637
+ margin: 0;
638
+ padding: 0;
639
+ text-align: justify; /* 文字兩端對齊 */
640
+ word-wrap: break-word; /* 確保長單字會換行 */
641
+ white-space: pre-line; /* 保留換行但合併空白 */
642
+ max-width: 100%; /* 確保不會超出容器 */
643
+ }}
644
+
645
+ .action-section {{
646
+ margin-top: 24px;
647
+ text-align: center;
648
+ }}
649
+
650
+ .akc-button,
651
+ .breed-section .akc-link,
652
+ .breed-option .akc-link {{
653
+ display: inline-flex;
654
+ align-items: center;
655
+ padding: 14px 28px;
656
+ background: linear-gradient(145deg, #00509E, #003F7F);
657
+ color: white;
658
+ border-radius: 12px; /* 增加圓角 */
659
+ text-decoration: none;
660
+ gap: 12px; /* 增加圖標和文字間距 */
661
+ transition: all 0.3s ease;
662
+ font-weight: 600;
663
+ font-size: 1.1em;
664
+ box-shadow:
665
+ 0 2px 4px rgba(0,0,0,0.1),
666
+ inset 0 1px 1px rgba(255,255,255,0.1);
667
+ border: 1px solid rgba(255,255,255,0.1);
668
+ }}
669
+
670
+ .akc-button:hover,
671
+ .breed-section .akc-link:hover,
672
+ .breed-option .akc-link:hover {{
673
+ background: linear-gradient(145deg, #003F7F, #00509E);
674
+ transform: translateY(-2px);
675
+ color: white;
676
+ box-shadow:
677
+ 0 6px 12px rgba(0,0,0,0.2),
678
+ inset 0 1px 1px rgba(255,255,255,0.2);
679
+ border: 1px solid rgba(255,255,255,0.2);
680
+ }}
681
+
682
+ .icon {{
683
+ font-size: 1.3em;
684
+ filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
685
+ }}
686
+
687
+ .warning-message {{
688
+ display: flex;
689
+ align-items: center;
690
+ gap: 8px;
691
+ color: #ff3b30;
692
+ font-weight: 500;
693
+ margin: 0;
694
+ padding: 16px;
695
+ background: #fff5f5;
696
+ border-radius: 8px;
697
+ }}
698
+
699
+ .model-uncertainty-note {{
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 12px;
703
+ padding: 16px;
704
+ background-color: #f8f9fa;
705
+ border-left: 4px solid #6c757d;
706
+ margin-bottom: 20px;
707
+ color: #495057;
708
+ border-radius: 4px;
709
+ }}
710
+
711
+ .breeds-list {{
712
+ display: flex;
713
+ flex-direction: column;
714
+ gap: 20px;
715
+ }}
716
+
717
+ .breed-option {{
718
+ background: white;
719
+ border: 1px solid #e1e4e8;
720
+ border-radius: 8px;
721
+ overflow: hidden;
722
+ }}
723
+
724
+ .breed-header {{
725
+ display: flex;
726
+ align-items: center;
727
+ padding: 16px;
728
+ background: #f8f9fa;
729
+ gap: 12px;
730
+ border-bottom: 1px solid #e1e4e8;
731
+ }}
732
+
733
+ .option-number {{
734
+ font-weight: 600;
735
+ color: #666;
736
+ padding: 4px 8px;
737
+ background: #e1e4e8;
738
+ border-radius: 4px;
739
+ }}
740
+
741
+ .breed-name {{
742
+ font-size: 1.5em;
743
+ font-weight: bold;
744
+ color: #2c3e50;
745
+ flex-grow: 1;
746
+ }}
747
+
748
+ .confidence-badge {{
749
+ padding: 4px 12px;
750
+ border-radius: 20px;
751
+ font-size: 0.9em;
752
+ font-weight: 500;
753
+ }}
754
+
755
+ .breed-content {{
756
+ padding: 20px;
757
+ }}
758
+
759
+ .breed-content li {{
760
+ margin-bottom: 8px;
761
+ display: flex;
762
+ align-items: flex-start; /* 改為頂部對齊 */
763
+ gap: 8px;
764
+ flex-wrap: wrap; /* 允許內容換行 */
765
+ }}
766
+
767
+ .breed-content li strong {{
768
+ flex: 0 0 auto; /* 不讓標題縮放 */
769
+ min-width: 100px; /* 給標題一個固定最小寬度 */
770
+ }}
771
+
772
+ ul {{
773
+ padding-left: 0;
774
+ margin: 0;
775
+ list-style-type: none;
776
+ }}
777
+
778
+ li {{
779
+ margin-bottom: 8px;
780
+ display: flex;
781
+ align-items: center;
782
+ gap: 8px;
783
+ }}
784
+
785
+ .akc-link {{
786
+ color: white;
787
+ text-decoration: none;
788
+ font-weight: 600;
789
+ font-size: 1.1em;
790
+ transition: all 0.3s ease;
791
+ }}
792
+
793
+ .akc-link:hover {{
794
+ text-decoration: underline;
795
+ color: #D3E3F0;
796
+ }}
797
+ .tooltip {{
798
+ position: relative;
799
+ display: inline-flex;
800
+ align-items: center;
801
+ gap: 4px;
802
+ cursor: help;
803
+ }}
804
+
805
+ .tooltip .tooltip-icon {{
806
+ font-size: 14px;
807
+ color: #666;
808
+ }}
809
+
810
+ .tooltip .tooltip-text {{
811
+ visibility: hidden;
812
+ width: 250px;
813
+ background-color: rgba(44, 62, 80, 0.95);
814
+ color: white;
815
+ text-align: left;
816
+ border-radius: 8px;
817
+ padding: 8px 10px;
818
+ position: absolute;
819
+ z-index: 100;
820
+ bottom: 150%;
821
+ left: 50%;
822
+ transform: translateX(-50%);
823
+ opacity: 0;
824
+ transition: all 0.3s ease;
825
+ font-size: 14px;
826
+ line-height: 1.3;
827
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
828
+ border: 1px solid rgba(255, 255, 255, 0.1)
829
+ margin-bottom: 10px;
830
+ }}
831
+
832
+ .tooltip.tooltip-left .tooltip-text {{
833
+ left: 0;
834
+ transform: translateX(0);
835
+ }}
836
+
837
+ .tooltip.tooltip-right .tooltip-text {{
838
+ left: auto;
839
+ right: 0;
840
+ transform: translateX(0);
841
+ }}
842
+
843
+ .tooltip-text strong {{
844
+ color: white !important;
845
+ background-color: transparent !important;
846
+ display: block; /* 讓標題獨立一行 */
847
+ margin-bottom: 2px; /* 增加標題下方間距 */
848
+ padding-bottom: 2px; /* 加入小間距 */
849
+ border-bottom: 1px solid rgba(255,255,255,0.2);
850
+ }}
851
+
852
+ .tooltip-text {{
853
+ font-size: 13px; /* 稍微縮小字體 */
854
+ }}
855
+
856
+ /* 調整列表符號和文字的間距 */
857
+ .tooltip-text ul {{
858
+ margin: 0;
859
+ padding-left: 15px; /* 減少列表符號的縮進 */
860
+ }}
861
+
862
+ .tooltip-text li {{
863
+ margin-bottom: 1px; /* 減少列表項目間的間距 */
864
+ }}
865
+ .tooltip-text br {{
866
+ line-height: 1.2; /* 減少行距 */
867
+ }}
868
+
869
+ .tooltip .tooltip-text::after {{
870
+ content: "";
871
+ position: absolute;
872
+ top: 100%;
873
+ left: 20%; /* 調整箭頭位置 */
874
+ margin-left: -5px;
875
+ border-width: 5px;
876
+ border-style: solid;
877
+ border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
878
+ }}
879
+
880
+ .tooltip-left .tooltip-text::after {{
881
+ left: 20%;
882
+ }}
883
+
884
+ /* 右側箭頭 */
885
+ .tooltip-right .tooltip-text::after {{
886
+ left: 80%;
887
+ }}
888
+
889
+ .tooltip:hover .tooltip-text {{
890
+ visibility: visible;
891
+ opacity: 1;
892
+ }}
893
+
894
+ .tooltip .tooltip-text::after {{
895
+ content: "";
896
+ position: absolute;
897
+ top: 100%;
898
+ left: 50%;
899
+ transform: translateX(-50%);
900
+ border-width: 8px;
901
+ border-style: solid;
902
+ border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
903
+ }}
904
+
905
+ .uncertainty-mode .tooltip .tooltip-text {{
906
+ position: absolute;
907
+ left: 100%;
908
+ bottom: auto;
909
+ top: 50%;
910
+ transform: translateY(-50%);
911
+ margin-left: 10px;
912
+ z-index: 1000; /* 確保提示框在最上層 */
913
+ }}
914
+
915
+ .uncertainty-mode .tooltip .tooltip-text::after {{
916
+ content: "";
917
+ position: absolute;
918
+ top: 50%;
919
+ right: 100%;
920
+ transform: translateY(-50%);
921
+ border-width: 5px;
922
+ border-style: solid;
923
+ border-color: transparent rgba(44, 62, 80, 0.95) transparent transparent;
924
+ }}
925
+
926
+ .uncertainty-mode .breed-content {{
927
+ font-size: 1.1rem; /* 增加字體大小 */
928
+ }}
929
+
930
+ .description-section,
931
+ .description-section p,
932
+ .temperament-section,
933
+ .temperament-section .value,
934
+ .info-item,
935
+ .info-item .value,
936
+ .breed-content {{
937
+ font-size: 1.1rem !important; /* 使用 !important 確保覆蓋其他樣式 */
938
+ }}
939
+ </style>
940
+ {dogs_info}
941
+ """
942
+
943
+ initial_state = {
944
+ "dogs_info": dogs_info,
945
+ "image": annotated_image,
946
+ "is_multi_dog": len(dogs) > 1,
947
+ "html_output": html_output
948
+ }
949
+
950
+ return html_output, annotated_image, initial_state
951
+
952
+ except Exception as e:
953
+ error_msg = f"An error occurred: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
954
+ print(error_msg)
955
+ return error_msg, None, None
956
+
957
+
958
+ def show_details_html(choice, previous_output, initial_state):
959
+ if not choice:
960
+ return previous_output, gr.update(visible=True), initial_state
961
+
962
+ try:
963
+ breed = choice.split("More about ")[-1]
964
+ description = get_dog_description(breed)
965
+ formatted_description = format_description_html(description, breed)
966
+
967
+ html_output = f"""
968
+ <div class="dog-info">
969
+ <h2>{breed}</h2>
970
+ {formatted_description}
971
+ </div>
972
+ """
973
+
974
+ initial_state["current_description"] = html_output
975
+ initial_state["original_buttons"] = initial_state.get("buttons", [])
976
+
977
+ return html_output, gr.update(visible=True), initial_state
978
+ except Exception as e:
979
+ error_msg = f"An error occurred while showing details: {e}"
980
+ print(error_msg)
981
+ return f"<p style='color: red;'>{error_msg}</p>", gr.update(visible=True), initial_state
982
+
983
+
984
+ def format_description_html(description, breed):
985
+ html = "<ul style='list-style-type: none; padding-left: 0;'>"
986
+ if isinstance(description, dict):
987
+ for key, value in description.items():
988
+ if key != "Breed": # 跳過重複的品種顯示
989
+ if key == "Size":
990
+ html += f'''
991
+ <li style='margin-bottom: 10px;'>
992
+ <span class="tooltip">
993
+ <strong>{key}:</strong>
994
+ <span class="tooltip-icon">ⓘ</span>
995
+ <span class="tooltip-text">
996
+ <strong>Size Categories:</strong><br>
997
+ • Small: Under 20 pounds<br>
998
+ • Medium: 20-60 pounds<br>
999
+ • Large: Over 60 pounds
1000
+ </span>
1001
+ </span> {value}
1002
+ </li>
1003
+ '''
1004
+ elif key == "Exercise Needs":
1005
+ html += f'''
1006
+ <li style='margin-bottom: 10px;'>
1007
+ <span class="tooltip">
1008
+ <strong>{key}:</strong>
1009
+ <span class="tooltip-icon">ⓘ</span>
1010
+ <span class="tooltip-text">
1011
+ <strong>Exercise Needs:</strong><br>
1012
+ • High: 2+ hours of daily exercise<br>
1013
+ • Moderate: 1-2 hours of daily activity<br>
1014
+ • Low: Short walks and play sessions
1015
+ </span>
1016
+ </span> {value}
1017
+ </li>
1018
+ '''
1019
+ elif key == "Grooming Needs":
1020
+ html += f'''
1021
+ <li style='margin-bottom: 10px;'>
1022
+ <span class="tooltip">
1023
+ <strong>{key}:</strong>
1024
+ <span class="tooltip-icon">ⓘ</span>
1025
+ <span class="tooltip-text">
1026
+ <strong>Grooming Requirements:</strong><br>
1027
+ • High: Daily brushing, regular professional care<br>
1028
+ • Moderate: Weekly brushing, occasional grooming<br>
1029
+ • Low: Minimal brushing, basic maintenance
1030
+ </span>
1031
+ </span> {value}
1032
+ </li>
1033
+ '''
1034
+ elif key == "Care Level":
1035
+ html += f'''
1036
+ <li style='margin-bottom: 10px;'>
1037
+ <span class="tooltip">
1038
+ <strong>{key}:</strong>
1039
+ <span class="tooltip-icon">ⓘ</span>
1040
+ <span class="tooltip-text">
1041
+ <strong>Care Level Explained:</strong><br>
1042
+ • High: Needs significant training and attention<br>
1043
+ • Moderate: Regular care and routine needed<br>
1044
+ • Low: More independent, basic care sufficient
1045
+ </span>
1046
+ </span> {value}
1047
+ </li>
1048
+ '''
1049
+ elif key == "Good with Children":
1050
+ html += f'''
1051
+ <li style='margin-bottom: 10px;'>
1052
+ <span class="tooltip">
1053
+ <strong>{key}:</strong>
1054
+ <span class="tooltip-icon">ⓘ</span>
1055
+ <span class="tooltip-text">
1056
+ <strong>Child Compatibility:</strong><br>
1057
+ • Yes: Excellent with kids, patient and gentle<br>
1058
+ • Moderate: Good with older children<br>
1059
+ • No: Better suited for adult households
1060
+ </span>
1061
+ </span> {value}
1062
+ </li>
1063
+ '''
1064
+ elif key == "Lifespan":
1065
+ html += f'''
1066
+ <li style='margin-bottom: 10px;'>
1067
+ <span class="tooltip">
1068
+ <strong>{key}:</strong>
1069
+ <span class="tooltip-icon">ⓘ</span>
1070
+ <span class="tooltip-text">
1071
+ <strong>Average Lifespan:</strong><br>
1072
+ • Short: 6-8 years<br>
1073
+ • Average: 10-15 years<br>
1074
+ • Long: 12-20 years
1075
+ </span>
1076
+ </span> {value}
1077
+ </li>
1078
+ '''
1079
+ elif key == "Temperament":
1080
+ html += f'''
1081
+ <li style='margin-bottom: 10px;'>
1082
+ <span class="tooltip">
1083
+ <strong>{key}:</strong>
1084
+ <span class="tooltip-icon">ⓘ</span>
1085
+ <span class="tooltip-text">
1086
+ <strong>Temperament Guide:</strong><br>
1087
+ • Describes the dog's natural behavior<br>
1088
+ • Important for matching with owner
1089
+ </span>
1090
+ </span> {value}
1091
+ </li>
1092
+ '''
1093
+ else:
1094
+ # 其他欄位保持原樣顯示
1095
+ html += f"<li style='margin-bottom: 10px;'><strong>{key}:</strong> {value}</li>"
1096
+ else:
1097
+ html += f"<li>{description}</li>"
1098
+ html += "</ul>"
1099
+
1100
+ # 添加AKC連結
1101
+ html += f'''
1102
+ <div class="action-section">
1103
+ <a href="{get_akc_breeds_link(breed)}" target="_blank" class="akc-button">
1104
+ <span class="icon">🌐</span>
1105
+ Learn more about {breed} on AKC website
1106
+ </a>
1107
+ </div>
1108
+ '''
1109
+ return html
1110
+
1111
+
1112
+ with gr.Blocks() as iface:
1113
+ gr.HTML("<h1 style='text-align: center;'>🐶 Dog Breed Classifier 🔍</h1>")
1114
+ gr.HTML("<p style='text-align: center;'>Upload a picture of a dog, and the model will predict its breed and provide detailed information!</p>")
1115
+ gr.HTML("<p style='text-align: center; color: #666; font-size: 0.9em;'>Note: The model's predictions may not always be 100% accurate, and it is recommended to use the results as a reference.</p>")
1116
+
1117
+
1118
+ with gr.Row():
1119
+ input_image = gr.Image(label="Upload a dog image", type="pil")
1120
+ output_image = gr.Image(label="Annotated Image")
1121
+
1122
+ output = gr.HTML(label="Prediction Results")
1123
+ initial_state = gr.State()
1124
+
1125
+ input_image.change(
1126
+ predict,
1127
+ inputs=input_image,
1128
+ outputs=[output, output_image, initial_state]
1129
+ )
1130
+
1131
+ gr.Examples(
1132
+ examples=['Border_Collie.jpg', 'Golden_Retriever.jpeg', 'Saint_Bernard.jpeg', 'French_Bulldog.jpeg', 'Samoyed.jpg'],
1133
+ inputs=input_image
1134
+ )
1135
+
1136
+ gr.HTML('For more details on this project and other work, feel free to visit my GitHub <a href="https://github.com/Eric-Chung-0511/Learning-Record/tree/main/Data%20Science%20Projects/Dog_Breed_Classifier">Dog Breed Classifier</a>')
1137
+
1138
+
1139
+ if __name__ == "__main__":
1140
+ iface.launch()