ElMETRICO commited on
Commit
220f24a
·
verified ·
1 Parent(s): 88f4b71

Deploy final RCM-Pet multimodal app

Browse files
README.md CHANGED
@@ -1,13 +1,32 @@
1
  ---
2
- title: RCM PET Version Two
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: RCM-Pet
3
+ emoji: 🐾
4
+ colorFrom: blue
5
+ colorTo: blue
6
  sdk: gradio
 
 
7
  app_file: app.py
8
  pinned: false
9
  ---
10
 
11
+ # RCM-Pet: Risk-Calibrated Multimodal Pet Triage System
12
+
13
+ RCM-Pet supports image, text, and audio-based pet triage.
14
+
15
+ ## Input Modes
16
+ - Image only
17
+ - Text only
18
+ - Audio only
19
+ - Image + text
20
+ - Image + audio
21
+ - Image + text + audio
22
+
23
+ ## Models
24
+ - Species classifier: MobileNetV2 with unknown class
25
+ - Dog disease image ML: ResNet18 + EfficientNet-B0
26
+ - Cat disease image ML: EfficientNet-B0
27
+ - Fish disease image ML: EfficientNet-B0
28
+ - Symptom text ML: TF-IDF + Logistic Regression
29
+ - Audio-to-text: Whisper Tiny
30
+
31
+ ## Safety
32
+ This is a triage assistant, not a final veterinary diagnosis.
app.py ADDED
@@ -0,0 +1,1119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import json
4
+ import warnings
5
+ warnings.filterwarnings("ignore")
6
+
7
+ import numpy as np
8
+ import joblib
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+ from PIL import Image
14
+ from torchvision import models, transforms
15
+ from transformers import pipeline
16
+ import gradio as gr
17
+
18
+
19
+ # ==========================================================
20
+ # GLOBAL SETTINGS
21
+ # ==========================================================
22
+
23
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
24
+ MODEL_DIR = os.path.join(BASE_DIR, "models")
25
+
26
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
27
+
28
+ IMAGE_MEAN = [0.485, 0.456, 0.406]
29
+ IMAGE_STD = [0.229, 0.224, 0.225]
30
+
31
+ print("Using device:", DEVICE)
32
+ print("Model directory:", MODEL_DIR)
33
+
34
+
35
+ # ==========================================================
36
+ # UTILITY FUNCTIONS
37
+ # ==========================================================
38
+
39
+ def safe_torch_load(path):
40
+ return torch.load(path, map_location=DEVICE)
41
+
42
+
43
+ def clean_label(label):
44
+ if label is None:
45
+ return "Unknown"
46
+ return str(label).replace("_", " ").title()
47
+
48
+
49
+ def softmax_dict(probs, idx_to_class):
50
+ result = {}
51
+ for idx, prob in enumerate(probs):
52
+ label = idx_to_class.get(idx, str(idx))
53
+ result[label] = float(prob)
54
+ return result
55
+
56
+
57
+ def format_prob(x):
58
+ if x is None:
59
+ return None
60
+ try:
61
+ return round(float(x), 4)
62
+ except Exception:
63
+ return x
64
+
65
+
66
+ # ==========================================================
67
+ # SPECIES CLASSIFIER: MobileNetV2
68
+ # ==========================================================
69
+
70
+ SPECIES_MODEL_PATH = os.path.join(MODEL_DIR, "best_species_mobilenetv2_7class_unknown.pth")
71
+ SPECIES_LABEL_MAP_PATH = os.path.join(MODEL_DIR, "species_label_map_7class_unknown.json")
72
+
73
+ species_checkpoint = safe_torch_load(SPECIES_MODEL_PATH)
74
+
75
+ if isinstance(species_checkpoint, dict) and "class_names" in species_checkpoint:
76
+ species_class_names = species_checkpoint["class_names"]
77
+ elif os.path.exists(SPECIES_LABEL_MAP_PATH):
78
+ with open(SPECIES_LABEL_MAP_PATH, "r") as f:
79
+ label_map = json.load(f)
80
+
81
+ if isinstance(label_map, dict):
82
+ if all(str(k).isdigit() for k in label_map.keys()):
83
+ species_class_names = [label_map[str(i)] for i in range(len(label_map))]
84
+ else:
85
+ species_class_names = sorted(label_map, key=label_map.get)
86
+ else:
87
+ species_class_names = label_map
88
+ else:
89
+ species_class_names = ["bird", "cat", "dog", "goldfish", "parrot", "rabbit", "unknown"]
90
+
91
+ species_class_to_idx = {c: i for i, c in enumerate(species_class_names)}
92
+ species_idx_to_class = {i: c for c, i in species_class_to_idx.items()}
93
+
94
+ SPECIES_IMG_SIZE = species_checkpoint.get("img_size", 224) if isinstance(species_checkpoint, dict) else 224
95
+
96
+ species_model = models.mobilenet_v2(weights=None)
97
+ in_features = species_model.classifier[1].in_features
98
+ species_model.classifier = nn.Sequential(
99
+ nn.Dropout(p=0.3),
100
+ nn.Linear(in_features, len(species_class_names))
101
+ )
102
+
103
+ species_state = species_checkpoint["model_state_dict"] if isinstance(species_checkpoint, dict) and "model_state_dict" in species_checkpoint else species_checkpoint
104
+ species_model.load_state_dict(species_state, strict=False)
105
+ species_model = species_model.to(DEVICE)
106
+ species_model.eval()
107
+
108
+ species_transform = transforms.Compose([
109
+ transforms.Resize((SPECIES_IMG_SIZE, SPECIES_IMG_SIZE)),
110
+ transforms.ToTensor(),
111
+ transforms.Normalize(mean=IMAGE_MEAN, std=IMAGE_STD)
112
+ ])
113
+
114
+ print("Species model loaded:", species_class_names)
115
+
116
+
117
+ def predict_species_open_set_strict(image_path):
118
+ img = Image.open(image_path).convert("RGB")
119
+ x = species_transform(img).unsqueeze(0).to(DEVICE)
120
+
121
+ with torch.no_grad():
122
+ outputs = species_model(x)
123
+ probs = F.softmax(outputs, dim=1)[0].cpu().numpy()
124
+
125
+ top_indices = np.argsort(probs)[::-1]
126
+ top1_idx = int(top_indices[0])
127
+ top2_idx = int(top_indices[1])
128
+ top3_idx = int(top_indices[2])
129
+
130
+ top1_class = species_idx_to_class[top1_idx]
131
+ top2_class = species_idx_to_class[top2_idx]
132
+ top3_class = species_idx_to_class[top3_idx]
133
+
134
+ top1_prob = float(probs[top1_idx])
135
+ top2_prob = float(probs[top2_idx])
136
+ top3_prob = float(probs[top3_idx])
137
+ margin = top1_prob - top2_prob
138
+
139
+ top3 = [
140
+ {"class": top1_class, "confidence": top1_prob},
141
+ {"class": top2_class, "confidence": top2_prob},
142
+ {"class": top3_class, "confidence": top3_prob},
143
+ ]
144
+
145
+ # Open-set / safety logic
146
+ if top1_class == "unknown":
147
+ final_output = "unsupported_animal"
148
+ elif ("unknown" in [top2_class, top3_class]) and max(
149
+ top2_prob if top2_class == "unknown" else 0,
150
+ top3_prob if top3_class == "unknown" else 0
151
+ ) >= 0.10:
152
+ final_output = "uncertain_possible_unknown"
153
+ elif top1_prob < 0.90:
154
+ final_output = "unknown_or_unclear_image"
155
+ elif margin < 0.30:
156
+ final_output = "uncertain_species"
157
+ else:
158
+ final_output = top1_class
159
+
160
+ return {
161
+ "top1_class": top1_class,
162
+ "top1_confidence": top1_prob,
163
+ "top2_class": top2_class,
164
+ "top2_confidence": top2_prob,
165
+ "top3_class": top3_class,
166
+ "top3_confidence": top3_prob,
167
+ "margin": margin,
168
+ "top3": top3,
169
+ "final_output": final_output
170
+ }
171
+
172
+
173
+ # ==========================================================
174
+ # DOG IMAGE MODELS
175
+ # ==========================================================
176
+
177
+ DOG_BINARY_PATH = os.path.join(MODEL_DIR, "best_dog_disease_resnet18.pth")
178
+ DOG_TYPE_PATH = os.path.join(MODEL_DIR, "best_dog_disease_type_efficientnet_b0.pth")
179
+
180
+ DOG_BINARY_THRESHOLD = 0.35
181
+
182
+ dog_binary_checkpoint = safe_torch_load(DOG_BINARY_PATH)
183
+
184
+ dog_binary_model = models.resnet18(weights=None)
185
+
186
+ # Try 2-output first, then 1-output if checkpoint needs sigmoid binary
187
+ dog_binary_is_sigmoid = False
188
+
189
+ try:
190
+ dog_binary_model.fc = nn.Linear(dog_binary_model.fc.in_features, 2)
191
+ state = dog_binary_checkpoint["model_state_dict"] if isinstance(dog_binary_checkpoint, dict) and "model_state_dict" in dog_binary_checkpoint else dog_binary_checkpoint
192
+ dog_binary_model.load_state_dict(state, strict=True)
193
+ except Exception:
194
+ dog_binary_model = models.resnet18(weights=None)
195
+ dog_binary_model.fc = nn.Linear(dog_binary_model.fc.in_features, 1)
196
+ state = dog_binary_checkpoint["model_state_dict"] if isinstance(dog_binary_checkpoint, dict) and "model_state_dict" in dog_binary_checkpoint else dog_binary_checkpoint
197
+ dog_binary_model.load_state_dict(state, strict=False)
198
+ dog_binary_is_sigmoid = True
199
+
200
+ dog_binary_model = dog_binary_model.to(DEVICE)
201
+ dog_binary_model.eval()
202
+
203
+ DOG_BINARY_IMG_SIZE = dog_binary_checkpoint.get("img_size", 224) if isinstance(dog_binary_checkpoint, dict) else 224
204
+
205
+ dog_binary_classes = dog_binary_checkpoint.get("class_names", ["diseased", "healthy"]) if isinstance(dog_binary_checkpoint, dict) else ["diseased", "healthy"]
206
+ dog_binary_idx_to_class = {i: c for i, c in enumerate(dog_binary_classes)}
207
+
208
+ dog_type_checkpoint = safe_torch_load(DOG_TYPE_PATH)
209
+
210
+ dog_type_classes = dog_type_checkpoint.get(
211
+ "class_names",
212
+ [
213
+ "bacterial_dermatosis",
214
+ "fungal_infections",
215
+ "hypersensitivity_allergic_dermatosis"
216
+ ]
217
+ ) if isinstance(dog_type_checkpoint, dict) else [
218
+ "bacterial_dermatosis",
219
+ "fungal_infections",
220
+ "hypersensitivity_allergic_dermatosis"
221
+ ]
222
+
223
+ dog_type_idx_to_class = {i: c for i, c in enumerate(dog_type_classes)}
224
+
225
+ DOG_TYPE_IMG_SIZE = dog_type_checkpoint.get("img_size", 224) if isinstance(dog_type_checkpoint, dict) else 224
226
+
227
+ dog_type_model = models.efficientnet_b0(weights=None)
228
+ in_features = dog_type_model.classifier[1].in_features
229
+ dog_type_model.classifier = nn.Sequential(
230
+ nn.Dropout(p=0.4),
231
+ nn.Linear(in_features, len(dog_type_classes))
232
+ )
233
+
234
+ dog_type_state = dog_type_checkpoint["model_state_dict"] if isinstance(dog_type_checkpoint, dict) and "model_state_dict" in dog_type_checkpoint else dog_type_checkpoint
235
+ dog_type_model.load_state_dict(dog_type_state, strict=False)
236
+ dog_type_model = dog_type_model.to(DEVICE)
237
+ dog_type_model.eval()
238
+
239
+ dog_transform = transforms.Compose([
240
+ transforms.Resize((224, 224)),
241
+ transforms.ToTensor(),
242
+ transforms.Normalize(mean=IMAGE_MEAN, std=IMAGE_STD)
243
+ ])
244
+
245
+ DOG_TYPE_DISPLAY = {
246
+ "bacterial_dermatosis": "Bacterial dermatosis",
247
+ "fungal_infections": "Fungal infection",
248
+ "fungal_infection": "Fungal infection",
249
+ "hypersensitivity_allergic_dermatosis": "Hypersensitivity / allergic dermatosis",
250
+ "allergic_dermatosis": "Hypersensitivity / allergic dermatosis"
251
+ }
252
+
253
+
254
+ def predict_dog_image(image_path):
255
+ img = Image.open(image_path).convert("RGB")
256
+ x = dog_transform(img).unsqueeze(0).to(DEVICE)
257
+
258
+ with torch.no_grad():
259
+ binary_out = dog_binary_model(x)
260
+
261
+ if dog_binary_is_sigmoid or binary_out.shape[1] == 1:
262
+ disease_prob = float(torch.sigmoid(binary_out)[0][0].cpu().item())
263
+ is_diseased = disease_prob >= DOG_BINARY_THRESHOLD
264
+ binary_conf = disease_prob if is_diseased else 1.0 - disease_prob
265
+ else:
266
+ probs = F.softmax(binary_out, dim=1)[0].cpu().numpy()
267
+ label_probs = {dog_binary_idx_to_class[i].lower(): float(p) for i, p in enumerate(probs)}
268
+
269
+ disease_prob = 0.0
270
+ for k, v in label_probs.items():
271
+ if "disease" in k or "diseased" in k or "infect" in k:
272
+ disease_prob = max(disease_prob, v)
273
+
274
+ if disease_prob == 0.0 and len(probs) == 2:
275
+ disease_prob = float(probs[0])
276
+
277
+ is_diseased = disease_prob >= DOG_BINARY_THRESHOLD
278
+ binary_conf = disease_prob if is_diseased else 1.0 - disease_prob
279
+
280
+ if not is_diseased:
281
+ return {
282
+ "species": "dog",
283
+ "health_status": "healthy",
284
+ "image_prediction": "Healthy dog",
285
+ "image_disease_type": "Not detected",
286
+ "image_confidence": binary_conf,
287
+ "risk_level": "Normal",
288
+ "mode": "dog_image_ml"
289
+ }
290
+
291
+ with torch.no_grad():
292
+ type_out = dog_type_model(x)
293
+ type_probs = F.softmax(type_out, dim=1)[0].cpu().numpy()
294
+
295
+ top_idx = int(np.argmax(type_probs))
296
+ raw_type = dog_type_idx_to_class[top_idx]
297
+ type_conf = float(type_probs[top_idx])
298
+
299
+ return {
300
+ "species": "dog",
301
+ "health_status": "diseased",
302
+ "image_prediction": DOG_TYPE_DISPLAY.get(raw_type, clean_label(raw_type)),
303
+ "image_disease_type": DOG_TYPE_DISPLAY.get(raw_type, clean_label(raw_type)),
304
+ "image_confidence": type_conf,
305
+ "risk_level": "Monitor / See Vet",
306
+ "mode": "dog_image_ml"
307
+ }
308
+
309
+
310
+ # ==========================================================
311
+ # CAT IMAGE MODEL
312
+ # ==========================================================
313
+
314
+ CAT_IMAGE_PATH = os.path.join(MODEL_DIR, "best_cat_disease_efficientnet_b0.pth")
315
+ cat_checkpoint = safe_torch_load(CAT_IMAGE_PATH)
316
+
317
+ cat_class_names = cat_checkpoint.get("class_names", ["flea_allergy", "healthy", "ringworm", "scabies"]) if isinstance(cat_checkpoint, dict) else ["flea_allergy", "healthy", "ringworm", "scabies"]
318
+ cat_idx_to_class = {i: c for i, c in enumerate(cat_class_names)}
319
+ CAT_IMG_SIZE = cat_checkpoint.get("img_size", 224) if isinstance(cat_checkpoint, dict) else 224
320
+
321
+ cat_model = models.efficientnet_b0(weights=None)
322
+ in_features = cat_model.classifier[1].in_features
323
+ cat_model.classifier = nn.Sequential(
324
+ nn.Dropout(p=0.4),
325
+ nn.Linear(in_features, len(cat_class_names))
326
+ )
327
+
328
+ cat_state = cat_checkpoint["model_state_dict"] if isinstance(cat_checkpoint, dict) and "model_state_dict" in cat_checkpoint else cat_checkpoint
329
+ cat_model.load_state_dict(cat_state, strict=False)
330
+ cat_model = cat_model.to(DEVICE)
331
+ cat_model.eval()
332
+
333
+ cat_transform = transforms.Compose([
334
+ transforms.Resize((CAT_IMG_SIZE, CAT_IMG_SIZE)),
335
+ transforms.ToTensor(),
336
+ transforms.Normalize(mean=IMAGE_MEAN, std=IMAGE_STD)
337
+ ])
338
+
339
+ CAT_DISPLAY = {
340
+ "healthy": "Healthy cat",
341
+ "ringworm": "Ringworm / fungal infection",
342
+ "scabies": "Scabies",
343
+ "flea_allergy": "Flea allergy"
344
+ }
345
+
346
+ CAT_RISK = {
347
+ "healthy": "Normal",
348
+ "ringworm": "Monitor / See Vet",
349
+ "scabies": "See Vet",
350
+ "flea_allergy": "Monitor / See Vet"
351
+ }
352
+
353
+
354
+ def predict_cat_disease_image(image_path):
355
+ img = Image.open(image_path).convert("RGB")
356
+ x = cat_transform(img).unsqueeze(0).to(DEVICE)
357
+
358
+ with torch.no_grad():
359
+ outputs = cat_model(x)
360
+ probs = F.softmax(outputs, dim=1)[0].cpu().numpy()
361
+
362
+ top_idx = int(np.argmax(probs))
363
+ raw_label = cat_idx_to_class[top_idx]
364
+ conf = float(probs[top_idx])
365
+
366
+ health = "healthy" if raw_label == "healthy" else "diseased"
367
+
368
+ return {
369
+ "species": "cat",
370
+ "health_status": health,
371
+ "raw_label": raw_label,
372
+ "image_prediction": CAT_DISPLAY.get(raw_label, clean_label(raw_label)),
373
+ "image_confidence": conf,
374
+ "risk_level": CAT_RISK.get(raw_label, "Monitor / See Vet"),
375
+ "mode": "cat_image_ml"
376
+ }
377
+
378
+
379
+ # ==========================================================
380
+ # FISH IMAGE MODEL
381
+ # ==========================================================
382
+
383
+ FISH_IMAGE_PATH = os.path.join(MODEL_DIR, "best_fish_disease_efficientnet_b0.pth")
384
+ fish_checkpoint = safe_torch_load(FISH_IMAGE_PATH)
385
+
386
+ fish_class_names = fish_checkpoint.get("class_names", ["healthy", "infected"]) if isinstance(fish_checkpoint, dict) else ["healthy", "infected"]
387
+ fish_idx_to_class = {i: c for i, c in enumerate(fish_class_names)}
388
+ FISH_IMG_SIZE = fish_checkpoint.get("img_size", 224) if isinstance(fish_checkpoint, dict) else 224
389
+
390
+ fish_model = models.efficientnet_b0(weights=None)
391
+ in_features = fish_model.classifier[1].in_features
392
+ fish_model.classifier = nn.Sequential(
393
+ nn.Dropout(p=0.4),
394
+ nn.Linear(in_features, len(fish_class_names))
395
+ )
396
+
397
+ fish_state = fish_checkpoint["model_state_dict"] if isinstance(fish_checkpoint, dict) and "model_state_dict" in fish_checkpoint else fish_checkpoint
398
+ fish_model.load_state_dict(fish_state, strict=False)
399
+ fish_model = fish_model.to(DEVICE)
400
+ fish_model.eval()
401
+
402
+ fish_transform = transforms.Compose([
403
+ transforms.Resize((FISH_IMG_SIZE, FISH_IMG_SIZE)),
404
+ transforms.ToTensor(),
405
+ transforms.Normalize(mean=IMAGE_MEAN, std=IMAGE_STD)
406
+ ])
407
+
408
+ FISH_DISPLAY = {
409
+ "healthy": "Healthy fish",
410
+ "infected": "Infected fish"
411
+ }
412
+
413
+
414
+ def predict_fish_disease_image(image_path):
415
+ img = Image.open(image_path).convert("RGB")
416
+ x = fish_transform(img).unsqueeze(0).to(DEVICE)
417
+
418
+ with torch.no_grad():
419
+ outputs = fish_model(x)
420
+ probs = F.softmax(outputs, dim=1)[0].cpu().numpy()
421
+
422
+ top_idx = int(np.argmax(probs))
423
+ raw_label = fish_idx_to_class[top_idx]
424
+ conf = float(probs[top_idx])
425
+
426
+ health = "healthy" if raw_label == "healthy" else "diseased"
427
+ risk = "Normal" if raw_label == "healthy" else "Check water / Treat quickly"
428
+
429
+ return {
430
+ "species": "goldfish",
431
+ "health_status": health,
432
+ "raw_label": raw_label,
433
+ "image_prediction": FISH_DISPLAY.get(raw_label, clean_label(raw_label)),
434
+ "image_confidence": conf,
435
+ "risk_level": risk,
436
+ "mode": "fish_image_ml"
437
+ }
438
+
439
+
440
+ # ==========================================================
441
+ # TEXT MODELS
442
+ # ==========================================================
443
+
444
+ DOG_TEXT_PATH = os.path.join(MODEL_DIR, "dog_symptom_tfidf_logreg.pkl")
445
+ CAT_TEXT_PATH = os.path.join(MODEL_DIR, "cat_symptom_tfidf_logreg.pkl")
446
+ MULTI_TEXT_PATH = os.path.join(MODEL_DIR, "multispecies_symptom_tfidf_logreg.pkl")
447
+
448
+ dog_text_model = joblib.load(DOG_TEXT_PATH)
449
+ cat_text_model = joblib.load(CAT_TEXT_PATH)
450
+ multi_text_model = joblib.load(MULTI_TEXT_PATH)
451
+
452
+ DOG_TEXT_DISPLAY = {
453
+ "bacterial_dermatosis": "Bacterial dermatosis",
454
+ "fungal_infections": "Fungal infection",
455
+ "fungal_infection": "Fungal infection",
456
+ "hypersensitivity_allergic_dermatosis": "Hypersensitivity / allergic dermatosis",
457
+ "allergic_dermatosis": "Hypersensitivity / allergic dermatosis"
458
+ }
459
+
460
+ CAT_TEXT_DISPLAY = {
461
+ "cat_upper_respiratory": "Upper respiratory infection symptoms possible",
462
+ "cat_ringworm_skin": "Ringworm / fungal skin problem possible",
463
+ "cat_flea_allergy": "Flea allergy or itchy skin problem possible",
464
+ "cat_scabies_mites": "Scabies / mite problem possible",
465
+ "cat_worm_digestive": "Worm or digestive parasite problem possible",
466
+ "cat_diabetes_possible": "Diabetes symptoms possible",
467
+ "cat_emergency": "Emergency condition possible",
468
+ "cat_general_issue": "General health issue possible"
469
+ }
470
+
471
+ CAT_TEXT_RISK = {
472
+ "cat_upper_respiratory": "See Vet",
473
+ "cat_ringworm_skin": "Monitor / See Vet",
474
+ "cat_flea_allergy": "Monitor / See Vet",
475
+ "cat_scabies_mites": "See Vet",
476
+ "cat_worm_digestive": "See Vet",
477
+ "cat_diabetes_possible": "See Vet",
478
+ "cat_emergency": "Emergency Vet",
479
+ "cat_general_issue": "Monitor / See Vet"
480
+ }
481
+
482
+ MULTI_TEXT_DISPLAY = {
483
+ "rabbit_eye_problem": "Eye infection or eye irritation possible",
484
+ "rabbit_digestive_problem": "Digestive problem possible",
485
+ "rabbit_respiratory_problem": "Respiratory problem possible",
486
+ "rabbit_skin_problem": "Skin problem possible",
487
+ "bird_feather_skin_problem": "Feather or skin problem possible",
488
+ "bird_respiratory_problem": "Respiratory problem possible",
489
+ "bird_digestive_problem": "Digestive problem possible",
490
+ "bird_injury_possible": "Injury possible",
491
+ "fish_white_spot": "White spot disease possible",
492
+ "fish_fin_rot": "Fin rot possible",
493
+ "fish_swim_bladder": "Swim bladder problem possible",
494
+ "fish_fungal_problem": "Fungal problem possible",
495
+ "general_issue": "General health issue possible"
496
+ }
497
+
498
+ MULTI_TEXT_RISK = {
499
+ "rabbit_eye_problem": "See Vet",
500
+ "rabbit_digestive_problem": "See Vet",
501
+ "rabbit_respiratory_problem": "See Vet",
502
+ "rabbit_skin_problem": "Monitor / See Vet",
503
+ "bird_feather_skin_problem": "Monitor / See Vet",
504
+ "bird_respiratory_problem": "See Vet",
505
+ "bird_digestive_problem": "See Vet",
506
+ "bird_injury_possible": "See Vet",
507
+ "fish_white_spot": "Check water / Treat quickly",
508
+ "fish_fin_rot": "Check water / Treat quickly",
509
+ "fish_swim_bladder": "Monitor water and feeding",
510
+ "fish_fungal_problem": "Check water / Treat quickly",
511
+ "general_issue": "Monitor / See Vet"
512
+ }
513
+
514
+
515
+ def predict_text_model(model, text, display_map=None, risk_map=None):
516
+ label = model.predict([text])[0]
517
+ confidence = None
518
+
519
+ if hasattr(model, "predict_proba"):
520
+ probs = model.predict_proba([text])[0]
521
+ confidence = float(np.max(probs))
522
+
523
+ display = display_map.get(label, clean_label(label)) if display_map else clean_label(label)
524
+ risk = risk_map.get(label, "Monitor / See Vet") if risk_map else "Monitor / See Vet"
525
+
526
+ return {
527
+ "text_model_label": label,
528
+ "text_prediction": display,
529
+ "text_model_confidence": confidence,
530
+ "risk_level": risk
531
+ }
532
+
533
+
534
+ def predict_dog_symptom_text(text):
535
+ return predict_text_model(dog_text_model, text, DOG_TEXT_DISPLAY, None)
536
+
537
+
538
+ def predict_cat_symptom_text(text):
539
+ return predict_text_model(cat_text_model, text, CAT_TEXT_DISPLAY, CAT_TEXT_RISK)
540
+
541
+
542
+ def fallback_symptom_triage(species, symptom_text):
543
+ result = predict_text_model(multi_text_model, symptom_text, MULTI_TEXT_DISPLAY, MULTI_TEXT_RISK)
544
+ return {
545
+ "species": species,
546
+ "health_status": "possible_issue",
547
+ "final_prediction": result["text_prediction"],
548
+ "text_model_label": result["text_model_label"],
549
+ "text_model_confidence": result["text_model_confidence"],
550
+ "risk_level": result["risk_level"],
551
+ "mode": "multispecies_text_ml"
552
+ }
553
+
554
+
555
+ # ==========================================================
556
+ # AUDIO MODEL: Whisper ASR
557
+ # ==========================================================
558
+
559
+ try:
560
+ asr_pipe = pipeline(
561
+ task="automatic-speech-recognition",
562
+ model="openai/whisper-tiny",
563
+ device=0 if torch.cuda.is_available() else -1
564
+ )
565
+ ASR_READY = True
566
+ print("Whisper loaded")
567
+ except Exception as e:
568
+ print("Whisper failed to load:", e)
569
+ asr_pipe = None
570
+ ASR_READY = False
571
+
572
+
573
+ def transcribe_audio(audio_path):
574
+ if audio_path is None:
575
+ return ""
576
+
577
+ if not ASR_READY:
578
+ return ""
579
+
580
+ try:
581
+ result = asr_pipe(audio_path)
582
+ text = result.get("text", "")
583
+ return text.strip()
584
+ except Exception:
585
+ return ""
586
+
587
+
588
+ # ==========================================================
589
+ # SPECIES FROM TEXT + MISMATCH CHECK
590
+ # ==========================================================
591
+
592
+ def detect_species_from_text(text):
593
+ if text is None:
594
+ return None
595
+
596
+ t = text.lower()
597
+
598
+ if any(w in t for w in ["dog", "puppy"]):
599
+ return "dog"
600
+ if any(w in t for w in ["cat", "kitten"]):
601
+ return "cat"
602
+ if any(w in t for w in ["rabbit", "bunny"]):
603
+ return "rabbit"
604
+ if "parrot" in t:
605
+ return "parrot"
606
+ if "bird" in t:
607
+ return "bird"
608
+ if any(w in t for w in ["goldfish", "fish"]):
609
+ return "goldfish"
610
+
611
+ return None
612
+
613
+
614
+ def species_compatible(image_species, text_species):
615
+ if text_species is None:
616
+ return True
617
+
618
+ if image_species == text_species:
619
+ return True
620
+
621
+ if image_species in ["bird", "parrot"] and text_species in ["bird", "parrot"]:
622
+ return True
623
+
624
+ if image_species == "goldfish" and text_species == "goldfish":
625
+ return True
626
+
627
+ return False
628
+
629
+
630
+ def check_species_text_mismatch(image_species, symptom_text):
631
+ text_species = detect_species_from_text(symptom_text)
632
+
633
+ if text_species is None:
634
+ return {
635
+ "mismatch": False,
636
+ "text_species": None,
637
+ "message": "No species mentioned in text."
638
+ }
639
+
640
+ compatible = species_compatible(image_species, text_species)
641
+
642
+ return {
643
+ "mismatch": not compatible,
644
+ "text_species": text_species,
645
+ "message": f"Image suggests {image_species}, but text suggests {text_species}."
646
+ }
647
+
648
+
649
+ # ==========================================================
650
+ # ROUTING SAFETY FUNCTIONS
651
+ # ==========================================================
652
+
653
+ def is_safe_dog_for_disease(species_result):
654
+ return (
655
+ species_result["top1_class"] == "dog"
656
+ and (
657
+ species_result["final_output"] == "dog"
658
+ or species_result["top1_confidence"] >= 0.75
659
+ )
660
+ )
661
+
662
+
663
+ def is_safe_cat_for_disease(species_result):
664
+ top1 = species_result["top1_class"]
665
+ top1_prob = species_result["top1_confidence"]
666
+ margin = species_result["margin"]
667
+
668
+ if top1 == "cat" and species_result["final_output"] == "cat":
669
+ return True
670
+
671
+ if top1 == "cat" and top1_prob >= 0.45 and margin >= 0.10:
672
+ return True
673
+
674
+ # Rescue diseased cat close-ups where unknown is top1 but cat is in top predictions
675
+ for item in species_result.get("top3", []):
676
+ if item["class"] == "cat" and item["confidence"] >= 0.20 and top1_prob <= 0.70:
677
+ return True
678
+
679
+ return False
680
+
681
+
682
+ def is_safe_fish_for_disease(species_result):
683
+ return (
684
+ species_result["top1_class"] == "goldfish"
685
+ and (
686
+ species_result["final_output"] == "goldfish"
687
+ or species_result["top1_confidence"] >= 0.80
688
+ )
689
+ )
690
+
691
+
692
+ def is_safe_fallback_species(species_result, symptom_text):
693
+ top1 = species_result["top1_class"]
694
+ final_output = species_result["final_output"]
695
+
696
+ fallback_species = ["rabbit", "bird", "parrot"]
697
+
698
+ if top1 not in fallback_species:
699
+ return False
700
+
701
+ if final_output != top1 and species_result["top1_confidence"] < 0.85:
702
+ return False
703
+
704
+ if symptom_text is None or symptom_text.strip() == "":
705
+ return False
706
+
707
+ text_species = detect_species_from_text(symptom_text)
708
+
709
+ if text_species is None:
710
+ return False
711
+
712
+ return species_compatible(top1, text_species)
713
+
714
+
715
+ # ==========================================================
716
+ # FINAL PIPELINES
717
+ # ==========================================================
718
+
719
+ def final_text_only_triage(symptom_text):
720
+ if symptom_text is None or symptom_text.strip() == "":
721
+ return {
722
+ "input_mode": "text_only",
723
+ "status": "no_input",
724
+ "species": "unknown",
725
+ "health_status": "unknown",
726
+ "final_prediction": "Please provide symptoms or upload an image.",
727
+ "risk_level": "Unknown"
728
+ }
729
+
730
+ text_species = detect_species_from_text(symptom_text)
731
+
732
+ if text_species is None:
733
+ return {
734
+ "input_mode": "text_only",
735
+ "status": "species_not_found_in_text",
736
+ "species": "unknown",
737
+ "health_status": "unknown",
738
+ "final_prediction": "Species could not be identified from text. Please mention dog, cat, rabbit, parrot, bird, or fish.",
739
+ "risk_level": "Unknown"
740
+ }
741
+
742
+ if text_species == "dog":
743
+ r = predict_dog_symptom_text(symptom_text)
744
+ return {
745
+ "input_mode": "text_only",
746
+ "status": "dog_text_ml_used",
747
+ "species": "dog",
748
+ "health_status": "possible_issue",
749
+ "final_prediction": r["text_prediction"],
750
+ "text_prediction": r["text_prediction"],
751
+ "text_model_label": r["text_model_label"],
752
+ "text_model_confidence": r["text_model_confidence"],
753
+ "risk_level": "Monitor / See Vet"
754
+ }
755
+
756
+ if text_species == "cat":
757
+ r = predict_cat_symptom_text(symptom_text)
758
+ return {
759
+ "input_mode": "text_only",
760
+ "status": "cat_text_ml_used",
761
+ "species": "cat",
762
+ "health_status": "possible_issue",
763
+ "final_prediction": r["text_prediction"],
764
+ "text_prediction": r["text_prediction"],
765
+ "text_model_label": r["text_model_label"],
766
+ "text_model_confidence": r["text_model_confidence"],
767
+ "risk_level": r["risk_level"]
768
+ }
769
+
770
+ r = fallback_symptom_triage(text_species, symptom_text)
771
+ r["input_mode"] = "text_only"
772
+ r["status"] = "multispecies_text_ml_used"
773
+ return r
774
+
775
+
776
+ def final_cat_disease_pipeline(image_path, symptom_text=None):
777
+ image_result = predict_cat_disease_image(image_path)
778
+
779
+ if symptom_text is None or symptom_text.strip() == "":
780
+ return {
781
+ **image_result,
782
+ "text_prediction": "Not provided",
783
+ "text_model_label": None,
784
+ "text_model_confidence": None,
785
+ "final_prediction": image_result["image_prediction"],
786
+ "mode": "cat_image_ml_only"
787
+ }
788
+
789
+ text_result = predict_cat_symptom_text(symptom_text)
790
+
791
+ text_conf = text_result["text_model_confidence"]
792
+ text_pred = text_result["text_prediction"]
793
+ text_risk = text_result["risk_level"]
794
+
795
+ if text_conf is not None and text_conf < 0.40:
796
+ mode = "cat_image_ml_plus_low_confidence_text_ml"
797
+ final_risk = image_result["risk_level"]
798
+ text_phrase = f"{text_pred} from symptoms, but text confidence is low"
799
+ else:
800
+ mode = "cat_image_ml_plus_text_ml"
801
+ text_phrase = f"{text_pred} from symptoms"
802
+
803
+ if text_risk == "Emergency Vet":
804
+ final_risk = "Emergency Vet"
805
+ elif image_result["health_status"] == "diseased":
806
+ final_risk = image_result["risk_level"]
807
+ else:
808
+ final_risk = text_risk
809
+
810
+ return {
811
+ **image_result,
812
+ "text_prediction": text_pred,
813
+ "text_model_label": text_result["text_model_label"],
814
+ "text_model_confidence": text_conf,
815
+ "final_prediction": f"{image_result['image_prediction']} from image; {text_phrase}",
816
+ "risk_level": final_risk,
817
+ "mode": mode
818
+ }
819
+
820
+
821
+ def final_fish_disease_pipeline(image_path, symptom_text=None):
822
+ image_result = predict_fish_disease_image(image_path)
823
+
824
+ if symptom_text is None or symptom_text.strip() == "":
825
+ return {
826
+ **image_result,
827
+ "text_prediction": "Not provided",
828
+ "text_model_label": None,
829
+ "text_model_confidence": None,
830
+ "final_prediction": image_result["image_prediction"],
831
+ "mode": "fish_image_ml_only"
832
+ }
833
+
834
+ text_result = fallback_symptom_triage("goldfish", symptom_text)
835
+
836
+ return {
837
+ **image_result,
838
+ "text_prediction": text_result["final_prediction"],
839
+ "text_model_label": text_result.get("text_model_label"),
840
+ "text_model_confidence": text_result.get("text_model_confidence"),
841
+ "final_prediction": f"{image_result['image_prediction']} from image; {text_result['final_prediction']} from symptoms",
842
+ "risk_level": image_result["risk_level"] if image_result["health_status"] == "diseased" else text_result["risk_level"],
843
+ "mode": "fish_image_ml_plus_text_ml"
844
+ }
845
+
846
+
847
+ def final_dog_disease_pipeline(image_path, symptom_text=None):
848
+ image_result = predict_dog_image(image_path)
849
+
850
+ if symptom_text is None or symptom_text.strip() == "":
851
+ return {
852
+ **image_result,
853
+ "text_prediction": "Not provided",
854
+ "text_model_label": None,
855
+ "text_model_confidence": None,
856
+ "final_prediction": image_result["image_prediction"],
857
+ "mode": "dog_image_ml_only"
858
+ }
859
+
860
+ text_result = predict_dog_symptom_text(symptom_text)
861
+
862
+ return {
863
+ **image_result,
864
+ "text_prediction": text_result["text_prediction"],
865
+ "text_model_label": text_result["text_model_label"],
866
+ "text_model_confidence": text_result["text_model_confidence"],
867
+ "final_prediction": f"{image_result['image_prediction']} from image; {text_result['text_prediction']} from symptoms",
868
+ "risk_level": image_result["risk_level"] if image_result["health_status"] == "diseased" else "Monitor / See Vet",
869
+ "mode": "dog_image_ml_plus_text_ml"
870
+ }
871
+
872
+
873
+ def final_unified_pet_triage_pipeline(image_path, symptom_text=None):
874
+ species_result = predict_species_open_set_strict(image_path)
875
+ top_species = species_result["top1_class"]
876
+
877
+ if is_safe_dog_for_disease(species_result):
878
+ mismatch = check_species_text_mismatch("dog", symptom_text)
879
+ if mismatch["mismatch"]:
880
+ return {
881
+ "species_result": species_result,
882
+ "final_species": "unsupported_or_mismatch",
883
+ "triage_result": None,
884
+ "status": "species_text_mismatch",
885
+ "message": mismatch["message"]
886
+ }
887
+
888
+ return {
889
+ "species_result": species_result,
890
+ "final_species": "dog",
891
+ "triage_result": final_dog_disease_pipeline(image_path, symptom_text),
892
+ "status": "dog_image_disease_pipeline_used"
893
+ }
894
+
895
+ if is_safe_cat_for_disease(species_result):
896
+ mismatch = check_species_text_mismatch("cat", symptom_text)
897
+ if mismatch["mismatch"]:
898
+ return {
899
+ "species_result": species_result,
900
+ "final_species": "unsupported_or_mismatch",
901
+ "triage_result": None,
902
+ "status": "species_text_mismatch",
903
+ "message": mismatch["message"]
904
+ }
905
+
906
+ cat_result = final_cat_disease_pipeline(image_path, symptom_text)
907
+ if species_result["top1_class"] == "unknown":
908
+ cat_result["species_note"] = "Species gate was uncertain, but cat was rescued from top predictions."
909
+
910
+ return {
911
+ "species_result": species_result,
912
+ "final_species": "cat",
913
+ "triage_result": cat_result,
914
+ "status": "cat_smart_pipeline_used"
915
+ }
916
+
917
+ if is_safe_fish_for_disease(species_result):
918
+ mismatch = check_species_text_mismatch("goldfish", symptom_text)
919
+ if mismatch["mismatch"]:
920
+ return {
921
+ "species_result": species_result,
922
+ "final_species": "unsupported_or_mismatch",
923
+ "triage_result": None,
924
+ "status": "species_text_mismatch",
925
+ "message": mismatch["message"]
926
+ }
927
+
928
+ return {
929
+ "species_result": species_result,
930
+ "final_species": "goldfish",
931
+ "triage_result": final_fish_disease_pipeline(image_path, symptom_text),
932
+ "status": "fish_image_disease_pipeline_used"
933
+ }
934
+
935
+ if is_safe_fallback_species(species_result, symptom_text):
936
+ mismatch = check_species_text_mismatch(top_species, symptom_text)
937
+ if mismatch["mismatch"]:
938
+ return {
939
+ "species_result": species_result,
940
+ "final_species": "unsupported_or_mismatch",
941
+ "triage_result": None,
942
+ "status": "species_text_mismatch",
943
+ "message": mismatch["message"]
944
+ }
945
+
946
+ return {
947
+ "species_result": species_result,
948
+ "final_species": top_species,
949
+ "triage_result": fallback_symptom_triage(top_species, symptom_text),
950
+ "status": "ml_symptom_triage_used"
951
+ }
952
+
953
+ return {
954
+ "species_result": species_result,
955
+ "final_species": "unsupported_or_unclear_image",
956
+ "triage_result": None,
957
+ "status": "disease_prediction_stopped",
958
+ "message": "Disease prediction stopped because the image is unsupported, unclear, or not a trained pet species."
959
+ }
960
+
961
+
962
+ # ==========================================================
963
+ # GRADIO APP FUNCTION
964
+ # ==========================================================
965
+
966
+ def build_markdown_output(result, transcript=""):
967
+ lines = []
968
+
969
+ lines.append("## 🐾 RCM-Pet Triage Result")
970
+
971
+ if transcript:
972
+ lines.append(f"**Audio transcript:** {transcript}")
973
+
974
+ if "species_result" in result:
975
+ sr = result["species_result"]
976
+ tr = result.get("triage_result")
977
+
978
+ lines.append(f"**Top species guess:** {sr.get('top1_class')} ({format_prob(sr.get('top1_confidence'))})")
979
+ lines.append(f"**Final species / decision:** {result.get('final_species')}")
980
+ lines.append(f"**Status:** {result.get('status')}")
981
+
982
+ if tr is None:
983
+ lines.append(f"**Message:** {result.get('message')}")
984
+ lines.append("")
985
+ lines.append("⚠️ Disease prediction was stopped for safety.")
986
+ else:
987
+ lines.append(f"**Health status:** {clean_label(tr.get('health_status'))}")
988
+ lines.append(f"**Final prediction:** {tr.get('final_prediction')}")
989
+ lines.append(f"**Risk level:** {tr.get('risk_level')}")
990
+ lines.append(f"**Mode:** {clean_label(tr.get('mode'))}")
991
+
992
+ if tr.get("image_prediction") is not None:
993
+ lines.append(f"**Image finding:** {tr.get('image_prediction')}")
994
+
995
+ if tr.get("text_prediction") is not None:
996
+ lines.append(f"**Text finding:** {tr.get('text_prediction')}")
997
+
998
+ if tr.get("text_model_confidence") is not None:
999
+ lines.append(f"**Text ML confidence:** {format_prob(tr.get('text_model_confidence'))}")
1000
+
1001
+ if tr.get("image_confidence") is not None:
1002
+ lines.append(f"**Image ML confidence:** {format_prob(tr.get('image_confidence'))}")
1003
+
1004
+ else:
1005
+ lines.append(f"**Input mode:** {result.get('input_mode')}")
1006
+ lines.append(f"**Species:** {result.get('species')}")
1007
+ lines.append(f"**Health status:** {clean_label(result.get('health_status'))}")
1008
+ lines.append(f"**Final prediction:** {result.get('final_prediction')}")
1009
+ lines.append(f"**Risk level:** {result.get('risk_level')}")
1010
+ lines.append(f"**Status:** {result.get('status')}")
1011
+
1012
+ if result.get("text_model_confidence") is not None:
1013
+ lines.append(f"**Text ML confidence:** {format_prob(result.get('text_model_confidence'))}")
1014
+
1015
+ lines.append("")
1016
+ lines.append("---")
1017
+ lines.append("⚠️ **Safety note:** RCM-Pet is a triage assistant, not a final veterinary diagnosis. Please consult a veterinarian for serious symptoms.")
1018
+
1019
+ return "\n\n".join(lines)
1020
+
1021
+
1022
+ def run_rcm_pet(image_path, symptom_text, audio_path):
1023
+ symptom_text = symptom_text or ""
1024
+ audio_transcript = ""
1025
+
1026
+ if audio_path is not None:
1027
+ audio_transcript = transcribe_audio(audio_path)
1028
+
1029
+ combined_text = " ".join([symptom_text.strip(), audio_transcript.strip()]).strip()
1030
+
1031
+ if image_path is None and combined_text == "":
1032
+ result = {
1033
+ "input_mode": "none",
1034
+ "status": "no_input",
1035
+ "species": "unknown",
1036
+ "health_status": "unknown",
1037
+ "final_prediction": "Please provide an image, text symptoms, or voice symptoms.",
1038
+ "risk_level": "Unknown"
1039
+ }
1040
+ return build_markdown_output(result), result
1041
+
1042
+ if image_path is None:
1043
+ result = final_text_only_triage(combined_text)
1044
+ if audio_transcript:
1045
+ result["input_mode"] = "audio_to_text" if symptom_text.strip() == "" else "text_plus_audio"
1046
+ return build_markdown_output(result, audio_transcript), result
1047
+
1048
+ result = final_unified_pet_triage_pipeline(image_path, combined_text)
1049
+
1050
+ if audio_transcript:
1051
+ result["audio_transcript"] = audio_transcript
1052
+
1053
+ return build_markdown_output(result, audio_transcript), result
1054
+
1055
+
1056
+ # ==========================================================
1057
+ # GRADIO UI
1058
+ # ==========================================================
1059
+
1060
+ description = """
1061
+ RCM-Pet is a risk-calibrated multimodal pet triage assistant.
1062
+
1063
+ You can use:
1064
+ - Image only
1065
+ - Text only
1066
+ - Audio only
1067
+ - Image + text
1068
+ - Image + audio
1069
+ - Image + text + audio
1070
+
1071
+ Supported species: dog, cat, fish/goldfish, rabbit, bird/parrot, and unknown safety stop.
1072
+ """
1073
+
1074
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
1075
+ gr.Markdown("# 🐾 RCM-Pet: Multimodal Pet Disease Triage System")
1076
+ gr.Markdown(description)
1077
+
1078
+ with gr.Row():
1079
+ with gr.Column():
1080
+ image_input = gr.Image(
1081
+ type="filepath",
1082
+ label="Upload pet image",
1083
+ sources=["upload", "webcam"]
1084
+ )
1085
+
1086
+ text_input = gr.Textbox(
1087
+ label="Write symptoms",
1088
+ placeholder="Example: my cat is sneezing and has runny nose",
1089
+ lines=4
1090
+ )
1091
+
1092
+ audio_input = gr.Audio(
1093
+ sources=["microphone", "upload"],
1094
+ type="filepath",
1095
+ label="Speak or upload symptoms"
1096
+ )
1097
+
1098
+ submit_btn = gr.Button("Run RCM-Pet Triage", variant="primary")
1099
+
1100
+ with gr.Column():
1101
+ markdown_output = gr.Markdown(label="Triage Summary")
1102
+ json_output = gr.JSON(label="Raw Model Output")
1103
+
1104
+ submit_btn.click(
1105
+ fn=run_rcm_pet,
1106
+ inputs=[image_input, text_input, audio_input],
1107
+ outputs=[markdown_output, json_output]
1108
+ )
1109
+
1110
+ gr.Markdown(
1111
+ """
1112
+ ### Important
1113
+ This system provides preliminary triage support only. It does not replace professional veterinary diagnosis.
1114
+ """
1115
+ )
1116
+
1117
+
1118
+ if __name__ == "__main__":
1119
+ demo.launch()
dataset_evidence/cat_symptom_dataset.csv ADDED
The diff for this file is too large to render. See raw diff
 
dataset_evidence/multispecies_symptom_dataset.csv ADDED
The diff for this file is too large to render. See raw diff
 
model_coverage_summary.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project_name": "RCM-Pet",
3
+ "deployment": "Hugging Face Gradio Space",
4
+ "input_modes": [
5
+ "image_only",
6
+ "text_only",
7
+ "audio_only",
8
+ "image_plus_text",
9
+ "image_plus_audio",
10
+ "image_plus_text_plus_audio"
11
+ ],
12
+ "species_model": "MobileNetV2 7-class with unknown",
13
+ "image_disease_models": {
14
+ "dog": "ResNet18 binary + EfficientNet-B0 disease type",
15
+ "cat": "EfficientNet-B0 disease classifier",
16
+ "fish": "EfficientNet-B0 healthy/infected classifier"
17
+ },
18
+ "text_models": {
19
+ "dog": "TF-IDF + Logistic Regression",
20
+ "cat": "TF-IDF + Logistic Regression",
21
+ "rabbit_bird_parrot_fish": "TF-IDF + Logistic Regression"
22
+ },
23
+ "audio": "Whisper Tiny converts speech to text",
24
+ "safety": [
25
+ "unknown class",
26
+ "confidence gate",
27
+ "species-text mismatch check",
28
+ "low-confidence text guard"
29
+ ]
30
+ }
models/best_cat_disease_efficientnet_b0.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59ce33edf5e5075d42b993664ae5b6a4b3c1ce5c5c179d2bd6d738633a4cbf94
3
+ size 16354029
models/best_dog_disease_resnet18.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c68bda7594ed4a9d27b7eabaa5d4a936435b2eede98247ff55b63199caffe4de
3
+ size 44790347
models/best_dog_disease_type_efficientnet_b0.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db684667c0fca3fc6831295098ac5107aa7f8279ea6b5b04995497921d050bfd
3
+ size 16350611
models/best_fish_disease_efficientnet_b0.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d190b04a54113c2effab41c03578cf0bdc5b300576ddafdc8946142181100d4
3
+ size 16344091
models/best_species_mobilenetv2_7class_unknown.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d1f0dd5fd5364b7a2fbf6a9c9bcfd926111dd8856b47d127c6eb8009161ef88
3
+ size 9187147
models/cat_disease_config.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "efficientnet_b0",
3
+ "task": "cat_skin_disease_4class",
4
+ "class_names": [
5
+ "flea_allergy",
6
+ "healthy",
7
+ "ringworm",
8
+ "scabies"
9
+ ],
10
+ "class_to_idx": {
11
+ "flea_allergy": 0,
12
+ "healthy": 1,
13
+ "ringworm": 2,
14
+ "scabies": 3
15
+ },
16
+ "img_size": 224,
17
+ "best_model_path": "/content/drive/MyDrive/DIP_DATASET/Models/cat_disease_classifier/best_cat_disease_efficientnet_b0.pth",
18
+ "best_val_acc": 0.9583333333333334,
19
+ "test_acc": 0.9530201342281879
20
+ }
models/cat_symptom_tfidf_logreg.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2bc8c93459b9ba3453a6bef9b48bdbfe3a2086e71430f2f463145df176bb3e1
3
+ size 78940
models/dog_symptom_tfidf_logreg.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf94cabd45ab380975162556e226b8fdd67c1b9cd15f52c956ed99bceda6861f
3
+ size 27220
models/fish_disease_config.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "efficientnet_b0",
3
+ "task": "fish_disease_binary_healthy_infected",
4
+ "class_names": [
5
+ "healthy",
6
+ "infected"
7
+ ],
8
+ "class_to_idx": {
9
+ "healthy": 0,
10
+ "infected": 1
11
+ },
12
+ "img_size": 224,
13
+ "best_model_path": "/content/drive/MyDrive/DIP_DATASET/Models/fish_disease_classifier/best_fish_disease_efficientnet_b0.pth",
14
+ "best_val_acc": 0.9777777777777777,
15
+ "test_acc": 0.9787234042553191
16
+ }
models/multispecies_symptom_tfidf_logreg.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ff7b90c7d656cf2ed14fab3a232a34ecd67b386f594715b2c7ed1278ba58fab
3
+ size 45236
models/species_label_map_7class_unknown.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "class_to_idx": {
3
+ "bird": 0,
4
+ "cat": 1,
5
+ "dog": 2,
6
+ "goldfish": 3,
7
+ "parrot": 4,
8
+ "rabbit": 5,
9
+ "unknown": 6
10
+ },
11
+ "idx_to_class": {
12
+ "0": "bird",
13
+ "1": "cat",
14
+ "2": "dog",
15
+ "3": "goldfish",
16
+ "4": "parrot",
17
+ "5": "rabbit",
18
+ "6": "unknown"
19
+ }
20
+ }
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ torchvision
4
+ pillow
5
+ numpy
6
+ scikit-learn
7
+ joblib
8
+ transformers
9
+ accelerate
10
+ soundfile
11
+ librosa