ElMETRICO commited on
Commit
5004307
·
verified ·
1 Parent(s): c90ccdd

Deploy CrossTalk AI full app code

Browse files
README.md CHANGED
@@ -1,32 +1,27 @@
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.
 
 
 
 
 
 
1
  ---
2
+ title: CrossTalk AI Full
3
+ emoji: 🌐
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
  app_file: app.py
8
  pinned: false
9
+ models:
10
+ - ElMETRICO/crosstalk-ai-full-artifacts
11
  ---
12
 
13
+ # CrossTalk AI Full
14
 
15
+ This Space runs the full CrossTalk AI trained hybrid retrieval system.
16
 
17
+ It downloads the full trained artifacts from:
 
 
 
 
 
 
18
 
19
+ `ElMETRICO/crosstalk-ai-full-artifacts`
 
 
 
 
 
 
20
 
21
+ ## Components
22
+
23
+ - exact lexical dictionary matching
24
+ - base-form matching
25
+ - fine-tuned multilingual E5 semantic fallback
26
+ - FAISS vector search
27
+ - confidence-aware safe output handling
app.py CHANGED
@@ -1,1119 +1,294 @@
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.05:
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()
 
1
 
2
+ import re
 
 
 
 
3
  import numpy as np
4
+ import pandas as pd
5
+ import faiss
 
 
 
 
 
 
6
  import gradio as gr
7
+ from pathlib import Path
8
+ from huggingface_hub import snapshot_download
9
+ from sentence_transformers import SentenceTransformer
10
 
11
+ MODEL_REPO_ID = "ElMETRICO/crosstalk-ai-full-artifacts"
12
 
13
+ print("Downloading CrossTalk AI full artifacts from:", MODEL_REPO_ID)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ artifact_dir = Path(snapshot_download(
16
+ repo_id=MODEL_REPO_ID,
17
+ repo_type="model"
18
+ ))
19
 
20
+ MODEL_DIR = artifact_dir / "model" / "e5_lexical_contrastive_finetuned"
21
+ INDEX_PATH = artifact_dir / "artifacts" / "trained_e5_source_to_meaning.index"
22
+ DF_PATH = artifact_dir / "artifacts" / "trained_e5_source_to_meaning_df.csv"
23
 
24
+ print("Model path:", MODEL_DIR)
25
+ print("Index path:", INDEX_PATH)
26
+ print("Data path:", DF_PATH)
 
 
 
27
 
28
+ print("Loading fine-tuned E5 model...")
29
+ model = SentenceTransformer(str(MODEL_DIR))
30
+ model.max_seq_length = 128
31
 
32
+ print("Loading FAISS index...")
33
+ source_index = faiss.read_index(str(INDEX_PATH))
 
 
 
 
 
34
 
35
+ print("Loading source dataframe...")
36
+ source_df = pd.read_csv(DF_PATH, encoding="utf-8-sig")
37
+ print("Rows loaded:", len(source_df))
38
 
 
 
 
39
 
40
+ def clean_text(x):
41
+ x = "" if pd.isna(x) else str(x)
42
+ x = re.sub(r"[\u200b-\u200d\ufeff]", "", x)
43
+ x = re.sub(r"\s+", " ", x).strip()
44
+ return x
45
 
 
46
 
47
+ def normalize_lookup_text(x):
48
+ x = clean_text(x).lower()
49
+ x = re.sub(r"\s+", " ", x).strip()
50
+ return x
 
51
 
 
 
 
 
 
 
 
 
 
52
 
53
+ def remove_parentheses_text(x):
54
+ x = clean_text(x)
55
+ x = re.sub(r"\(.*?\)", "", x)
56
+ x = re.sub(r"\s+", " ", x).strip().lower()
57
+ return x
58
 
 
 
 
 
 
 
 
 
59
 
60
+ def count_source_files(x):
61
+ x = "" if pd.isna(x) else str(x)
62
+ if not x.strip():
63
+ return 0
64
+ return len([p for p in x.split("||") if p.strip()])
65
 
 
 
 
 
 
66
 
67
+ def add_quality_score(df):
68
+ df = df.copy()
69
 
70
+ for col in ["english_meaning", "bangla_meaning", "source_file"]:
71
+ if col not in df.columns:
72
+ df[col] = ""
73
+ df[col] = df[col].fillna("").astype(str)
74
 
75
+ if "duplicate_count" not in df.columns:
76
+ df["duplicate_count"] = 1
 
77
 
78
+ df["duplicate_count"] = pd.to_numeric(df["duplicate_count"], errors="coerce").fillna(1)
79
+ df["has_english"] = df["english_meaning"].str.strip().ne("").astype(int)
80
+ df["has_bangla"] = df["bangla_meaning"].str.strip().ne("").astype(int)
81
+ df["source_file_count"] = df["source_file"].apply(count_source_files)
82
 
83
+ df["quality_score"] = (
84
+ df["has_english"] * 3.0 +
85
+ df["has_bangla"] * 2.0 +
86
+ np.log1p(df["duplicate_count"]) * 0.5 +
87
+ np.log1p(df["source_file_count"]) * 0.5
88
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ return df
91
 
 
92
 
93
+ source_df = add_quality_score(source_df)
 
94
 
95
+ for col in ["language", "source_text", "english_meaning", "bangla_meaning"]:
96
+ if col not in source_df.columns:
97
+ source_df[col] = ""
98
+ source_df[col] = source_df[col].apply(clean_text)
 
 
 
 
 
 
99
 
100
+ source_df["norm_source"] = source_df["source_text"].apply(normalize_lookup_text)
101
+ source_df["base_source"] = source_df["source_text"].apply(remove_parentheses_text)
102
 
 
103
 
104
+ def format_verified_result(df, query, method, top_k=10):
105
+ result = df.copy()
106
 
107
+ result = result.sort_values(
108
+ by=["quality_score", "duplicate_count"],
109
+ ascending=[False, False]
110
+ )
111
 
112
+ keep_cols = [
113
+ "language",
114
+ "source_text",
115
+ "english_meaning",
116
+ "bangla_meaning",
117
+ "part_of_speech",
118
+ "duplicate_count",
119
+ "quality_score"
120
  ]
 
 
 
 
 
121
 
122
+ for col in keep_cols:
123
+ if col not in result.columns:
124
+ result[col] = ""
125
 
126
+ result = result[keep_cols].head(top_k).copy()
127
+ result.insert(0, "query", query)
128
+ result.insert(1, "score", 1.0)
129
+ result.insert(2, "method", method)
130
 
131
+ if result["language"].nunique() > 1 or result["source_text"].nunique() > 1:
132
+ result["confidence"] = "high_but_ambiguous"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  else:
134
+ result["confidence"] = "high"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ result["note"] = "Verified dictionary match."
 
 
137
 
138
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
 
 
 
 
 
140
 
141
+ def trained_semantic_fallback(query, top_k=5, search_k_per_language=20):
142
+ languages = sorted(source_df["language"].dropna().unique().tolist())
143
 
144
+ query_texts = [
145
+ f"query: {lang} word: {query}"
146
+ for lang in languages
147
+ ]
148
 
149
+ query_emb = model.encode(
150
+ query_texts,
151
+ batch_size=16,
152
+ convert_to_numpy=True,
153
+ normalize_embeddings=True,
154
+ show_progress_bar=False
155
+ ).astype("float32")
 
156
 
157
+ scores, indices = source_index.search(query_emb, search_k_per_language)
158
 
159
+ best_by_index = {}
 
 
 
160
 
161
+ for lang_i, lang in enumerate(languages):
162
+ for score, idx in zip(scores[lang_i], indices[lang_i]):
163
+ idx = int(idx)
164
+ score = float(score)
165
 
166
+ if idx < 0:
167
+ continue
168
 
169
+ if idx not in best_by_index or score > best_by_index[idx]["score"]:
170
+ best_by_index[idx] = {
171
+ "score": score
172
+ }
173
 
174
+ ranked = sorted(
175
+ best_by_index.items(),
176
+ key=lambda x: x[1]["score"],
177
+ reverse=True
178
+ )[:top_k]
179
 
180
+ if len(ranked) == 0:
181
+ return pd.DataFrame()
182
 
183
+ selected_indices = [idx for idx, _ in ranked]
184
+ result = source_df.iloc[selected_indices].copy().reset_index(drop=True)
 
 
 
 
 
 
185
 
186
+ result.insert(0, "query", query)
187
+ result.insert(1, "score", [x["score"] for _, x in ranked])
188
+ result.insert(2, "method", "fine_tuned_e5_semantic_fallback")
189
 
190
+ def label_score(score):
191
+ if score >= 0.88:
192
+ return "medium_high_trained_semantic_candidate"
193
+ elif score >= 0.80:
194
+ return "medium_trained_semantic_candidate"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  else:
196
+ return "low_trained_semantic_candidate"
197
+
198
+ result["confidence"] = result["score"].apply(label_score)
199
+ result["note"] = "No verified dictionary match. Semantic candidate only; not a confirmed translation."
200
+
201
+ keep_cols = [
202
+ "query",
203
+ "score",
204
+ "method",
205
+ "confidence",
206
+ "language",
207
+ "source_text",
208
+ "english_meaning",
209
+ "bangla_meaning",
210
+ "part_of_speech",
211
+ "duplicate_count",
212
+ "quality_score",
213
+ "note"
214
+ ]
215
 
216
+ for col in keep_cols:
217
+ if col not in result.columns:
218
+ result[col] = ""
219
 
220
+ return result[keep_cols]
 
 
 
 
 
 
221
 
 
 
222
 
223
+ def safe_search(query):
224
+ query = clean_text(query)
 
225
 
226
+ if not query:
227
+ return "Please enter a word.", pd.DataFrame()
228
 
229
+ q_norm = normalize_lookup_text(query)
230
+ q_base = remove_parentheses_text(query)
231
 
232
+ exact = source_df[source_df["norm_source"] == q_norm].copy()
 
 
233
 
234
+ if len(exact) > 0:
235
+ result = format_verified_result(exact, query, "hybrid_exact_source_match", top_k=10)
236
+ return "Verified dictionary match found.", result
237
 
238
+ base = source_df[source_df["base_source"] == q_base].copy()
239
 
240
+ if len(base) > 0:
241
+ result = format_verified_result(base, query, "hybrid_base_form_match", top_k=10)
242
+ return "Verified base-form dictionary match found.", result
 
 
 
 
 
 
 
243
 
244
+ semantic = trained_semantic_fallback(query, top_k=5, search_k_per_language=20)
 
 
 
 
245
 
246
+ if len(semantic) == 0:
247
+ return "No verified dictionary match or semantic candidate found.", pd.DataFrame()
248
 
249
+ strong = semantic[semantic["score"] >= 0.88].copy()
 
250
 
251
+ if len(strong) > 0:
252
+ return "No verified dictionary match found. Showing trained semantic candidates only.", strong
253
 
254
+ return "No verified dictionary match found. Semantic scores are below safe threshold; no translation is claimed.", semantic
255
 
 
 
 
256
 
257
  description = """
258
+ CrossTalk AI Full Deployment
259
 
260
+ This is the full trained hybrid lexical retrieval system:
261
+ 1. exact dictionary matching
262
+ 2. base-form matching
263
+ 3. fine-tuned multilingual E5 semantic fallback
264
+ 4. FAISS vector search
265
+ 5. confidence-aware safe output handling
 
266
 
267
+ Semantic fallback results are candidate suggestions only, not verified translations.
268
  """
269
 
270
+ demo = gr.Interface(
271
+ fn=safe_search,
272
+ inputs=gr.Textbox(
273
+ label="Enter source / ethnic word",
274
+ placeholder="Example: kəkhyáŋ, Hula, Aina"
275
+ ),
276
+ outputs=[
277
+ gr.Textbox(label="System Message"),
278
+ gr.Dataframe(label="Results")
279
+ ],
280
+ title="CrossTalk AI Full",
281
+ description=description,
282
+ examples=[
283
+ ["kəkhyáŋ"],
284
+ ["Hula"],
285
+ ["Aina"],
286
+ ["aam"],
287
+ ["bajaoo"],
288
+ ["unknown tribal word"]
289
+ ],
290
+ flagging_mode="never"
291
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  if __name__ == "__main__":
294
  demo.launch()
dataset_evidence/cat_symptom_dataset.csv DELETED
The diff for this file is too large to render. See raw diff
 
dataset_evidence/multispecies_symptom_dataset.csv DELETED
The diff for this file is too large to render. See raw diff
 
model_coverage_summary.json DELETED
@@ -1,30 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:59ce33edf5e5075d42b993664ae5b6a4b3c1ce5c5c179d2bd6d738633a4cbf94
3
- size 16354029
 
 
 
 
models/best_dog_disease_resnet18.pth DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c68bda7594ed4a9d27b7eabaa5d4a936435b2eede98247ff55b63199caffe4de
3
- size 44790347
 
 
 
 
models/best_dog_disease_type_efficientnet_b0.pth DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:db684667c0fca3fc6831295098ac5107aa7f8279ea6b5b04995497921d050bfd
3
- size 16350611
 
 
 
 
models/best_fish_disease_efficientnet_b0.pth DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7d190b04a54113c2effab41c03578cf0bdc5b300576ddafdc8946142181100d4
3
- size 16344091
 
 
 
 
models/best_species_mobilenetv2_7class_unknown.pth DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7d1f0dd5fd5364b7a2fbf6a9c9bcfd926111dd8856b47d127c6eb8009161ef88
3
- size 9187147
 
 
 
 
models/cat_disease_config.json DELETED
@@ -1,20 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c2bc8c93459b9ba3453a6bef9b48bdbfe3a2086e71430f2f463145df176bb3e1
3
- size 78940
 
 
 
 
models/dog_symptom_tfidf_logreg.pkl DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:bf94cabd45ab380975162556e226b8fdd67c1b9cd15f52c956ed99bceda6861f
3
- size 27220
 
 
 
 
models/fish_disease_config.json DELETED
@@ -1,16 +0,0 @@
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 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:1ff7b90c7d656cf2ed14fab3a232a34ecd67b386f594715b2c7ed1278ba58fab
3
- size 45236
 
 
 
 
models/species_label_map_7class_unknown.json DELETED
@@ -1,20 +0,0 @@
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 CHANGED
@@ -1,11 +1,7 @@
1
  gradio
2
- torch
3
- torchvision
4
- pillow
 
5
  numpy
6
- scikit-learn
7
- joblib
8
- transformers
9
- accelerate
10
- soundfile
11
- librosa
 
1
  gradio
2
+ sentence-transformers
3
+ faiss-cpu
4
+ huggingface_hub
5
+ pandas
6
  numpy
7
+ torch