morefaat69 commited on
Commit
deb74b4
·
verified ·
1 Parent(s): bd23023

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +24 -0
  2. main.py +1337 -0
  3. model_lung.py +54 -0
  4. requirements.txt +15 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ libgl1 \
5
+ libglib2.0-0 \
6
+ libsm6 \
7
+ libxext6 \
8
+ libxrender-dev \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ WORKDIR /app
12
+
13
+ COPY requirements.txt .
14
+ COPY main.py .
15
+ COPY model_lung.py .
16
+
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ RUN mkdir -p weights
20
+
21
+ EXPOSE 7860
22
+
23
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
24
+
main.py ADDED
@@ -0,0 +1,1337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Medical AI FastAPI — 7 Models in One API
3
+ ==========================================
4
+ Models:
5
+ 1. Skin Cancer Classification → /predict/skin
6
+ 2. Breast Cancer Seg + Class → /predict/breast
7
+ 3. Eye Disease Classification → /predict/eye
8
+ 4. Brain Tumor Classification → /predict/brain
9
+ 5. Heart CT Segmentation → /predict/heart
10
+ 6. Lung/Chest X-Ray Classification → /predict/lung
11
+ 7. Kidney Disease Classification → /predict/kidney
12
+ """
13
+
14
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Form
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from contextlib import asynccontextmanager
17
+ import numpy as np
18
+ from PIL import Image
19
+ import io, os, logging
20
+ import base64
21
+ import matplotlib
22
+ matplotlib.use("Agg")
23
+ import matplotlib.pyplot as plt
24
+ import matplotlib.cm as cm
25
+ import google.generativeai as genai
26
+ from typing import Optional
27
+
28
+ # ── Gemini setup ──────────────────────────────────────────────────────────────
29
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
30
+ if GEMINI_API_KEY:
31
+ genai.configure(api_key=GEMINI_API_KEY)
32
+
33
+ GEMINI_MODEL = "gemini-1.5-flash"
34
+
35
+ # نوع الصورة لكل موديل — بيساعد Gemini يفهم السياق
36
+ MODEL_IMAGE_TYPE = {
37
+ "skin": "صورة جلدية (ديرماتوسكوبي)",
38
+ "breast": "أشعة موجات فوق صوتية للثدي",
39
+ "eye": "صورة قاع العين (فانداس)",
40
+ "brain": "صورة رنين مغناطيسي للمخ",
41
+ "heart": "أشعة مقطعية CT للصدر",
42
+ "lung": "أشعة X-Ray للصدر",
43
+ "kidney": "أشعة مقطعية CT للكلى",
44
+ }
45
+
46
+ def generate_medical_report(
47
+ model_type: str,
48
+ predicted_class: str,
49
+ disease_name: str,
50
+ confidence: float,
51
+ age: Optional[int],
52
+ gender: Optional[str],
53
+ symptoms: Optional[str],
54
+ chronic_diseases: Optional[str],
55
+ medications: Optional[str],
56
+ ) -> str:
57
+ """
58
+ يبعت بيانات المريض والتشخيص لـ Gemini ويرجع تقرير طبي مخصص بالعربي.
59
+ لو Gemini مش متاح يرجع None وهنستخدم الـ recommendations الثابتة.
60
+ """
61
+ if not GEMINI_API_KEY:
62
+ return None
63
+
64
+ # تحديد مستوى الـ confidence بلغة مفهومة
65
+ if confidence >= 85:
66
+ confidence_note = "نتيجة ذات ثقة عالية"
67
+ elif confidence >= 65:
68
+ confidence_note = "نتيجة تحتاج تأكيداً من الطبيب"
69
+ else:
70
+ confidence_note = "نتيجة غير حاسمة — الفحص الطبي ضروري للتأكيد"
71
+
72
+ gender_ar = "ذكر" if gender and gender.lower() in ("male", "m", "ذكر") else "أنثى" if gender else "غير محدد"
73
+
74
+ patient_info = f"العمر: {age} سنة" if age else "العمر: غير محدد"
75
+ patient_info += f" | الجنس: {gender_ar}"
76
+ if symptoms:
77
+ patient_info += f" | الأعراض: {symptoms}"
78
+ if chronic_diseases:
79
+ patient_info += f" | الأمراض المزمنة: {chronic_diseases}"
80
+ if medications:
81
+ patient_info += f" | الأدوية الحالية: {medications}"
82
+
83
+ prompt = f"""أنت مساعد طبي متعاطف ومتوازن. مهمتك كتابة رسالة طبية قصيرة ومطمئنة بالعربي للمريض.
84
+
85
+ [بيانات الحالة]
86
+ - نوع الفحص: {MODEL_IMAGE_TYPE.get(model_type, model_type)}
87
+ - نتيجة التحليل: {disease_name}
88
+ - مستوى الثقة في النتيجة: {confidence:.1f}% ({confidence_note})
89
+ - {patient_info}
90
+
91
+ [قواعد صارمة لا تخالفها أبداً]
92
+ - لا تذكر أرقام أو إحصائيات عن نسب الوفاة أو الانتشار
93
+ - لا تجزم بالتشخيص النهائي — هذا حق الطبيب المختص فقط
94
+ - لا تذكر أسماء أدوية أو جرعات أو بروتوكولات علاج بالتفصيل
95
+ - لا تستخدم عبارات تخويف مثل: "قد يتطور إلى"، "في الحالات الشديدة"، "خطر الانتشار"
96
+ - لا تكرر نسبة الـ confidence بالأرقام في التقرير
97
+ - إذا كانت الثقة أقل من 65% ابدأ بجملة تؤكد ضرورة الفحص الطبي للتأكد
98
+ - إذا كانت الحالة طبيعية (Normal / No Tumor) ركز على الطمأنينة والوقاية
99
+
100
+ [تعليمات الأسلوب]
101
+ - النبرة: دافئة، واضحة، غير مبالغة
102
+ - الطول: 4 إلى 6 جمل فقط لا أكثر
103
+ - اللغة: عربية فصحى مبسطة بدون مصطلحات طبية معقدة
104
+
105
+ [هيكل التقرير]
106
+ 1. جملة افتتاحية مطمئنة تذكر فيها نتيجة الفحص بشكل محايد
107
+ 2. ملاحظة واحدة مخصصة لبيانات المريض (العمر أو الجنس أو الأعراض إذا كانت متوفرة)
108
+ 3. الخطوة العملية الواحدة القادمة (مراجعة طبيب — أيّ نوع)
109
+ 4. جملة ختامية داعمة ومشجعة
110
+
111
+ اكتب التقرير مباشرة بدون عناوين أو نقاط أو مقدمات."""
112
+
113
+ try:
114
+ gemini = genai.GenerativeModel(GEMINI_MODEL)
115
+ response = gemini.generate_content(
116
+ prompt,
117
+ generation_config=genai.types.GenerationConfig(
118
+ temperature=0.4, # منخفض عشان يكون consistent ومضبوط
119
+ max_output_tokens=350,
120
+ ),
121
+ )
122
+ return response.text.strip()
123
+ except Exception as e:
124
+ logger.warning(f"⚠️ Gemini report generation failed: {e}")
125
+ return None
126
+
127
+
128
+ def _mask_to_base64(mask: np.ndarray, colormap: str = "plasma") -> str:
129
+ """Convert a 2-D float mask → PNG base64 string."""
130
+ import io
131
+ norm = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8)
132
+ cmap = cm.get_cmap(colormap)
133
+ rgba = (cmap(norm) * 255).astype(np.uint8)
134
+ img = Image.fromarray(rgba)
135
+ buf = io.BytesIO()
136
+ img.save(buf, format="PNG")
137
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
138
+
139
+
140
+ def _overlay_to_base64(orig: Image.Image, mask: np.ndarray,
141
+ size: tuple, alpha: float = 0.45) -> str:
142
+ """Blend original image with coloured mask → PNG base64 string."""
143
+ import io
144
+ img_rgb = np.array(orig.convert("RGB").resize(size))
145
+ norm = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8)
146
+ cmap = cm.get_cmap("plasma")
147
+ colored = (cmap(norm)[:, :, :3] * 255).astype(np.uint8)
148
+ overlay = (img_rgb * (1 - alpha) + colored * alpha).astype(np.uint8)
149
+ buf = io.BytesIO()
150
+ Image.fromarray(overlay).save(buf, format="PNG")
151
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
152
+
153
+
154
+
155
+ logging.basicConfig(level=logging.INFO)
156
+ logger = logging.getLogger(__name__)
157
+
158
+ # ──────────────────────────────────────────────────────────────────────────────
159
+ # Global model store
160
+ # ──────────────────────────────────────────────────────────────────────────────
161
+ MODELS: dict = {}
162
+
163
+
164
+ def _build_skin_cnn():
165
+ """Custom CNN — same architecture used during skin cancer training (28x28x3, 8 classes)."""
166
+ import tensorflow as tf
167
+ from tensorflow.keras.regularizers import l2
168
+ model = tf.keras.Sequential([
169
+ tf.keras.layers.Conv2D(32, (3,3), activation='relu', padding='same',
170
+ kernel_initializer='he_normal', kernel_regularizer=l2(0.01),
171
+ input_shape=(28, 28, 3)),
172
+ tf.keras.layers.MaxPooling2D(),
173
+ tf.keras.layers.BatchNormalization(),
174
+ tf.keras.layers.Conv2D(64, (3,3), activation='relu', padding='same',
175
+ kernel_initializer='he_normal', kernel_regularizer=l2(0.01)),
176
+ tf.keras.layers.MaxPooling2D(),
177
+ tf.keras.layers.BatchNormalization(),
178
+ tf.keras.layers.Conv2D(128, (3,3), activation='relu', padding='same',
179
+ kernel_initializer='he_normal', kernel_regularizer=l2(0.01)),
180
+ tf.keras.layers.MaxPooling2D(),
181
+ tf.keras.layers.BatchNormalization(),
182
+ tf.keras.layers.Flatten(),
183
+ tf.keras.layers.Dropout(0.5),
184
+ tf.keras.layers.Dense(256, activation='relu', kernel_initializer='he_normal',
185
+ kernel_regularizer=l2(0.01)),
186
+ tf.keras.layers.BatchNormalization(),
187
+ tf.keras.layers.Dense(128, activation='relu', kernel_initializer='he_normal',
188
+ kernel_regularizer=l2(0.01)),
189
+ tf.keras.layers.BatchNormalization(),
190
+ tf.keras.layers.Dense(64, activation='relu', kernel_initializer='he_normal',
191
+ kernel_regularizer=l2(0.01)),
192
+ tf.keras.layers.BatchNormalization(),
193
+ tf.keras.layers.Dense(8, activation='softmax'),
194
+ ])
195
+ return model
196
+
197
+
198
+ def _build_eye_model():
199
+ """EfficientNetB3 + Dense — same architecture used during eye disease training (224x224x3, 4 classes)."""
200
+ import tensorflow as tf
201
+ from tensorflow.keras import regularizers
202
+ base_model = tf.keras.applications.EfficientNetB3(
203
+ include_top=False, weights=None, input_shape=(224, 224, 3), pooling='max'
204
+ )
205
+ model = tf.keras.Sequential([
206
+ base_model,
207
+ tf.keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001),
208
+ tf.keras.layers.Dense(256,
209
+ kernel_regularizer=regularizers.l2(0.016),
210
+ activity_regularizer=regularizers.l1(0.006),
211
+ bias_regularizer=regularizers.l1(0.006),
212
+ activation='relu'),
213
+ tf.keras.layers.Dropout(rate=0.45, seed=123),
214
+ tf.keras.layers.Dense(4, activation='softmax'),
215
+ ])
216
+ return model
217
+
218
+
219
+ def _build_brain_cnn():
220
+ """VGG-like CNN — same architecture used during brain tumor training."""
221
+ import tensorflow as tf
222
+ model = tf.keras.Sequential([
223
+ tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu", input_shape=(224,224,3)),
224
+ tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu"),
225
+ tf.keras.layers.MaxPooling2D((2,2)),
226
+ tf.keras.layers.Conv2D(128, (3,3), padding="same", activation="relu"),
227
+ tf.keras.layers.Conv2D(128, (3,3), padding="same", activation="relu"),
228
+ tf.keras.layers.MaxPooling2D((2,2)),
229
+ tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"),
230
+ tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"),
231
+ tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"),
232
+ tf.keras.layers.MaxPooling2D((2,2)),
233
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
234
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
235
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
236
+ tf.keras.layers.MaxPooling2D((2,2)),
237
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
238
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
239
+ tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"),
240
+ tf.keras.layers.MaxPooling2D((2,2)),
241
+ tf.keras.layers.Flatten(),
242
+ tf.keras.layers.Dense(256, activation="relu"),
243
+ tf.keras.layers.Dense(64, activation="relu"),
244
+ tf.keras.layers.Dense(4, activation="softmax"),
245
+ ])
246
+ return model
247
+
248
+
249
+ def _build_kidney_cnn():
250
+ """CNN architecture used during kidney disease training."""
251
+ import tensorflow as tf
252
+ model = tf.keras.Sequential([
253
+ tf.keras.layers.Conv2D(32, (3,3), activation="relu", input_shape=(200,200,1)),
254
+ tf.keras.layers.MaxPooling2D(2),
255
+ tf.keras.layers.Conv2D(32, (3,3), activation="relu"),
256
+ tf.keras.layers.MaxPooling2D(2),
257
+ tf.keras.layers.Conv2D(64, (3,3), activation="relu"),
258
+ tf.keras.layers.MaxPooling2D(2),
259
+ tf.keras.layers.Conv2D(64, (3,3), activation="relu"),
260
+ tf.keras.layers.MaxPooling2D(2),
261
+ tf.keras.layers.Conv2D(128, (3,3), activation="relu"),
262
+ tf.keras.layers.MaxPooling2D(2),
263
+ tf.keras.layers.Conv2D(128, (3,3), activation="relu"),
264
+ tf.keras.layers.MaxPooling2D(2),
265
+ tf.keras.layers.Flatten(),
266
+ tf.keras.layers.Dense(512, activation="relu"),
267
+ tf.keras.layers.Dense(4, activation="softmax"),
268
+ ])
269
+ return model
270
+
271
+
272
+ HF_REPO = "morefaat69/medical-ai-models"
273
+ WEIGHTS_DIR = "weights"
274
+
275
+ def _download_weight(filename: str) -> str:
276
+ """Download a model file from Hugging Face if not already cached locally."""
277
+ from huggingface_hub import hf_hub_download
278
+ local_path = os.path.join(WEIGHTS_DIR, filename)
279
+ if not os.path.exists(local_path):
280
+ logger.info(f"⬇️ Downloading {filename} from Hugging Face...")
281
+ os.makedirs(WEIGHTS_DIR, exist_ok=True)
282
+ downloaded = hf_hub_download(
283
+ repo_id=HF_REPO,
284
+ filename=filename,
285
+ local_dir=WEIGHTS_DIR,
286
+ )
287
+ logger.info(f"✅ {filename} downloaded")
288
+ return downloaded
289
+ logger.info(f"📦 {filename} already cached locally")
290
+ return local_path
291
+
292
+
293
+ def load_all_models():
294
+ """Download weights from Hugging Face (if needed) then load all models."""
295
+ import tensorflow as tf
296
+
297
+ # 1 ─ Skin
298
+ try:
299
+ path = _download_weight("skin_model.h5")
300
+ MODELS["skin"] = tf.keras.models.load_model(path, compile=False)
301
+ logger.info("✅ Skin model loaded")
302
+ except Exception as e:
303
+ logger.warning(f"⚠️ Skin model failed: {e}")
304
+
305
+ # 2 ─ Breast (two sub-models)
306
+ try:
307
+ seg_path = _download_weight("Final Final Breast Cancer Segmentation.h5")
308
+ cls_path = _download_weight("bes__model.h5")
309
+ MODELS["breast_seg"] = tf.keras.models.load_model(seg_path, compile=False)
310
+ MODELS["breast_cls"] = tf.keras.models.load_model(cls_path, compile=False)
311
+ logger.info("✅ Breast models loaded")
312
+ except Exception as e:
313
+ logger.warning(f"⚠️ Breast models failed: {e}")
314
+
315
+ # 3 ─ Eye
316
+ try:
317
+ path = _download_weight("eye_model_fixed.h5")
318
+ MODELS["eye"] = tf.keras.models.load_model(path, compile=False)
319
+ logger.info("✅ Eye model loaded")
320
+ except Exception as e:
321
+ logger.warning(f"⚠️ Eye model failed: {e}")
322
+
323
+ # 4 ─ Brain
324
+ try:
325
+ path = _download_weight("brain_model.h5")
326
+ try:
327
+ MODELS["brain"] = tf.keras.models.load_model(path, compile=False)
328
+ except TypeError:
329
+ brain_model = _build_brain_cnn()
330
+ brain_model.load_weights(path)
331
+ MODELS["brain"] = brain_model
332
+ logger.info("✅ Brain model loaded")
333
+ except Exception as e:
334
+ logger.warning(f"⚠️ Brain model failed: {e}")
335
+
336
+ # 5 ─ Heart
337
+ try:
338
+ path = _download_weight("heart_segmentation_model.h5")
339
+ MODELS["heart"] = _build_unet()
340
+ MODELS["heart"].load_weights(path)
341
+ logger.info("✅ Heart model loaded")
342
+ except Exception as e:
343
+ logger.warning(f"⚠️ Heart model failed: {e}")
344
+
345
+ # 6 ─ Lung (PyTorch)
346
+ try:
347
+ import torch
348
+ from model_lung import HybridModel
349
+ path = _download_weight("final_ChestX6_hybrid_model.pth")
350
+ m = HybridModel(num_classes=6)
351
+ state = torch.load(path, map_location="cpu")
352
+ clean = {k.replace("_orig_mod.", ""): v for k, v in state.items()}
353
+ m.load_state_dict(clean)
354
+ m.eval()
355
+ MODELS["lung"] = m
356
+ logger.info("✅ Lung model loaded")
357
+ except Exception as e:
358
+ logger.warning(f"⚠️ Lung model failed: {e}")
359
+
360
+ # 7 ─ Kidney
361
+ try:
362
+ path = _download_weight("kidney_model1.h5")
363
+ try:
364
+ MODELS["kidney"] = tf.keras.models.load_model(path, compile=False)
365
+ except TypeError:
366
+ kidney_model = _build_kidney_cnn()
367
+ kidney_model.load_weights(path)
368
+ MODELS["kidney"] = kidney_model
369
+ logger.info("✅ Kidney model loaded")
370
+ except Exception as e:
371
+ logger.warning(f"⚠️ Kidney model failed: {e}")
372
+
373
+
374
+ # ──────────────────────────────────────────────────────────────────────────────
375
+ # U-Net builder (Heart)
376
+ # ──────────────────────────────────────────────────────────────────────────────
377
+ def _build_unet(input_shape=(128, 128, 1)):
378
+ import tensorflow as tf
379
+ L = tf.keras.layers
380
+ inputs = L.Input(shape=input_shape)
381
+ c1 = L.Conv2D(16, 3, activation="relu", padding="same")(inputs)
382
+ c1 = L.Conv2D(16, 3, activation="relu", padding="same")(c1)
383
+ p1 = L.MaxPooling2D()(c1)
384
+ c2 = L.Conv2D(32, 3, activation="relu", padding="same")(p1)
385
+ c2 = L.Conv2D(32, 3, activation="relu", padding="same")(c2)
386
+ p2 = L.MaxPooling2D()(c2)
387
+ c3 = L.Conv2D(64, 3, activation="relu", padding="same")(p2)
388
+ c3 = L.Conv2D(64, 3, activation="relu", padding="same")(c3)
389
+ p3 = L.MaxPooling2D()(c3)
390
+ c4 = L.Conv2D(128, 3, activation="relu", padding="same")(p3)
391
+ c4 = L.Conv2D(128, 3, activation="relu", padding="same")(c4)
392
+ p4 = L.MaxPooling2D()(c4)
393
+ c5 = L.Conv2D(256, 3, activation="relu", padding="same")(p4)
394
+ c5 = L.Conv2D(256, 3, activation="relu", padding="same")(c5)
395
+ u6 = L.Conv2DTranspose(128, 2, strides=2, padding="same")(c5)
396
+ u6 = L.concatenate([u6, c4])
397
+ c6 = L.Conv2D(128, 3, activation="relu", padding="same")(u6)
398
+ c6 = L.Conv2D(128, 3, activation="relu", padding="same")(c6)
399
+ u7 = L.Conv2DTranspose(64, 2, strides=2, padding="same")(c6)
400
+ u7 = L.concatenate([u7, c3])
401
+ c7 = L.Conv2D(64, 3, activation="relu", padding="same")(u7)
402
+ c7 = L.Conv2D(64, 3, activation="relu", padding="same")(c7)
403
+ u8 = L.Conv2DTranspose(32, 2, strides=2, padding="same")(c7)
404
+ u8 = L.concatenate([u8, c2])
405
+ c8 = L.Conv2D(32, 3, activation="relu", padding="same")(u8)
406
+ c8 = L.Conv2D(32, 3, activation="relu", padding="same")(c8)
407
+ u9 = L.Conv2DTranspose(16, 2, strides=2, padding="same")(c8)
408
+ u9 = L.concatenate([u9, c1])
409
+ c9 = L.Conv2D(16, 3, activation="relu", padding="same")(u9)
410
+ c9 = L.Conv2D(16, 3, activation="relu", padding="same")(c9)
411
+ outputs = L.Conv2D(1, 1, activation="sigmoid")(c9)
412
+ return tf.keras.models.Model(inputs, outputs)
413
+
414
+
415
+ # ──────────────────────────────────────────────────────────────────────────────
416
+ # App lifecycle
417
+ # ──────────────────────────────────────────────────────────────────────────────
418
+ @asynccontextmanager
419
+ async def lifespan(app: FastAPI):
420
+ load_all_models()
421
+ yield
422
+ MODELS.clear()
423
+
424
+
425
+ app = FastAPI(
426
+ title="Medical AI API",
427
+ description="7 medical AI models in one unified API",
428
+ version="1.0.0",
429
+ lifespan=lifespan,
430
+ )
431
+
432
+ app.add_middleware(
433
+ CORSMiddleware,
434
+ allow_origins=["*"],
435
+ allow_methods=["*"],
436
+ allow_headers=["*"],
437
+ )
438
+
439
+
440
+ # ──────────────────────────────────────────────────────────────────────────────
441
+ # Utility
442
+ # ──────────────────────────────────────────────────────────────────────────────
443
+ async def read_image(file: UploadFile) -> Image.Image:
444
+ data = await file.read()
445
+ return Image.open(io.BytesIO(data))
446
+
447
+
448
+ def _require(key: str):
449
+ if key not in MODELS:
450
+ raise HTTPException(503, f"Model '{key}' is not loaded. Check weight file.")
451
+
452
+
453
+ # ──────────────────────────────────────────────────────────────────────────────
454
+ # Root
455
+ # ──────────────────────────────────────────────────────────────────────────────
456
+ # ══════════════════════════════════════════════════════════════════════════════
457
+ # RECOMMENDATIONS — Arabic medical info for every disease
458
+ # ══════════════════════════════════════════════════════════════════════════════
459
+ RECOMMENDATIONS = {
460
+
461
+ # ── Skin ──────────────────────────────────────────────────────────────────
462
+ "AK": {
463
+ "disease_name": "AK — Actinic Keratosis",
464
+ "description": "آفة جلدية تظهر نتيجة التعرض المفرط للشمس وتُعدّ مرحلة ما قبل سرطانية، قابلة للتحول لسرطان إذا تُركت دون علاج.",
465
+ "recommendations": [
466
+ "استشر طبيب جلدية لتقييم المنطقة وتحديد العلاج المناسب.",
467
+ "من خيارات العلاج: التجميد بالنيتروجين أو كريمات طبية موضعية.",
468
+ "الحد من التعرض للشمس المباشرة أمر بالغ الأهمية.",
469
+ "استخدم واقي الشمس يومياً وأعد وضعه كل ساعتين عند الخروج.",
470
+ "متابعة دورية مع الطبيب كل 6-12 شهراً.",
471
+ ],
472
+ },
473
+ "BCC": {
474
+ "disease_name": "BCC — Basal Cell Carcinoma",
475
+ "description": "أكثر أنواع سرطان الجلد شيوعاً، ينمو ببطء ونادراً ما ينتشر، لكنه يحتاج علاجاً طبياً سريعاً لمنع تلف الأنسجة المحيطة.",
476
+ "recommendations": [
477
+ "راجع طبيب جلدية في أقرب وقت لتقييم المنطقة.",
478
+ "الطبيب على الأرجح سيقترح إزالة جراحية للمنطقة المصابة.",
479
+ "ابتعد عن التعرض المباشر للشمس خصوصاً بين 10 صباحاً و4 عصراً.",
480
+ "ضع واقي شمس يومياً حتى في الأيام الغائمة.",
481
+ "راقب أي تغير في الحجم أو اللون أو النزيف.",
482
+ ],
483
+ },
484
+ "BKL": {
485
+ "disease_name": "BKL — Benign Keratosis-like Lesion",
486
+ "description": "نتوءات جلدية حميدة شائعة خاصةً مع التقدم في السن. ليست خطرة ولا تنتشر، لكنها قد تكون مزعجة من الناحية التجميلية.",
487
+ "recommendations": [
488
+ "الحالة حميدة في الغالب ولا تستدعي قلقاً فورياً.",
489
+ "راقب المنطقة — إذا تغير الشكل أو اللون أو نزّت دماً راجع الطبيب.",
490
+ "يمكن إزالتها جمالياً عند طبيب جلدية إذا أزعجتك.",
491
+ "اعتنِ بنظافة المنطقة وتجنب حكّها.",
492
+ "زيارة روتينية للطبيب سنوياً للاطمئنان.",
493
+ ],
494
+ },
495
+ "DF": {
496
+ "disease_name": "DF — Dermatofibroma",
497
+ "description": "ورم جلدي حميد صلب يظهر عادةً في الساقين، ويُعتقد أنه يتكون بعد إصابة بسيطة كلسعة حشرة. نادراً ما يسبب مشاكل.",
498
+ "recommendations": [
499
+ "الورم الليفي حميد تماماً ولا يحتاج علاجاً في معظم الحالات.",
500
+ "تجنب الضغط عليه أو العبث به.",
501
+ "إذا كبر بسرعة أو سبّب ألماً اعرضه على الطبيب.",
502
+ "يمكن إزالته جراحياً إذا أزعجك مظهره.",
503
+ "مراجعة الطبيب سنوياً للاطمئنان.",
504
+ ],
505
+ },
506
+ "MEL": {
507
+ "disease_name": "MEL — Melanoma",
508
+ "description": "نوع خطير من سرطان الجلد ينشأ من الخلايا المنتجة للميلانين. يُعدّ من أشد أنواع سرطان الجلد خطورةً إذا لم يُكتشف مبكراً.",
509
+ "recommendations": [
510
+ "اذهب لطبيب جلدية أو أورام فوراً — لا تتأخر.",
511
+ "لا تتعرض لأشعة الشمس المباشرة على المنطقة المصابة.",
512
+ "التقط صوراً دورية للمنطقة لمتابعة أي تغير في الشكل أو الحجم.",
513
+ "استخدم واقي الشمس بعامل حماية SPF 50+ يومياً.",
514
+ "قد يحتاج الطبيب لأخذ خزعة (biopsy) للتأكد من التشخيص.",
515
+ ],
516
+ },
517
+ "NV": {
518
+ "disease_name": "NV — Melanocytic Nevi",
519
+ "description": "الشامات الشائعة الحميدة الناتجة عن تجمّع الخلايا المنتجة للميلانين. معظمها غير ضار لكن يجب مراقبتها باستمرار.",
520
+ "recommendations": [
521
+ "الشامة الطبيعية غير ضارة — لكن المراقبة المستمرة مهمة.",
522
+ "اتبع قاعدة ABCDE: تغير في التماثل والحدود واللون والحجم والشكل العام.",
523
+ "التقط صورة شهرية لمتابعة أي تغيير.",
524
+ "واقي الشمس يومياً يقلل خطر تحوّلها.",
525
+ "إذا لاحظت أي تغيير مفاجئ راجع الطبيب فوراً.",
526
+ ],
527
+ },
528
+ "SCC": {
529
+ "disease_name": "SCC — Squamous Cell Carcinoma",
530
+ "description": "نوع من سرطان الجلد ينشأ من الطبقات الخارجية للجلد، وقد ينتشر للعقد الليمفاوية إذا أُهمل.",
531
+ "recommendations": [
532
+ "زيارة طبيب جلدية عاجلة ضرورية جداً.",
533
+ "الطبيب سيحدد ما إذا كانت الإزالة الجراحية أو العلاج الإشعاعي مناسباً.",
534
+ "تجنّب تماماً أجهزة تسمير الجلد (سولاريوم).",
535
+ "واقي شمس SPF 50+ يومياً إلزامي.",
536
+ "أخبر الطبيب بأي تاريخ مرضي سابق لأمراض الجلد.",
537
+ ],
538
+ },
539
+ "VASC": {
540
+ "disease_name": "VASC — Vascular Lesion",
541
+ "description": "آفات تنشأ من الأوعية الدموية تحت الجلد كالأورام الوعائية والشعيرات المتوسعة. معظمها حميد تماماً.",
542
+ "recommendations": [
543
+ "معظم الآفات الوعائية حميدة ولا تحتاج تدخلاً طبياً عاجلاً.",
544
+ "راقب المنطقة — أي نزيف أو ألم يستدعي زيارة الطبيب.",
545
+ "يمكن علاجها بالليزر لأغراض تجميلية.",
546
+ "حافظ على نظافة المنطقة وتجنب احتكاكها بالملابس.",
547
+ "زيارة طبيب جلدية للتأكد من التشخيص.",
548
+ ],
549
+ },
550
+
551
+ # ── Breast ────────────────────────────────────────────────────────────────
552
+ "benign": {
553
+ "disease_name": "Benign — ورم حميد",
554
+ "description": "الورم الحميد لا ينتشر ولا يشكّل خطراً مباشراً، لكن يجب متابعته طبياً بانتظام للتأكد من عدم تغيّره.",
555
+ "recommendations": [
556
+ "راجع طبيب مختص لتأكيد التشخيص بالخزعة إذا لزم.",
557
+ "متابعة دورية بالموجات فوق الصوتية كل 6 أشهر.",
558
+ "تجنّبي الضغط على المنطقة أو العبث بها.",
559
+ "أبلغي الطبيب فوراً إذا تغيّر الحجم أو ظهر ألم.",
560
+ "الفحص الذاتي الشهري للثدي مهم جداً للمتابعة.",
561
+ ],
562
+ },
563
+ "malignant": {
564
+ "disease_name": "Malignant — ورم خبيث",
565
+ "description": "الورم الخبيث (السرطاني) يحتاج تدخلاً طبياً عاجلاً. الاكتشاف المبكر يرفع نسبة الشفاء بشكل كبير.",
566
+ "recommendations": [
567
+ "توجّهي لطبيب أورام فوراً — لا تتأخري أبداً.",
568
+ "ستحتاجين لفحوصات إضافية: خزعة، تصوير بالرنين، سكان عظام.",
569
+ "خيارات العلاج تشمل: جراحة، كيماوي، إشعاع، علاج هرموني.",
570
+ "احرصي على دعم نفسي وعائلي خلال فترة العلاج.",
571
+ "لا تعتمدي على أي علاج شعبي بديل دون استشارة الطبيب.",
572
+ ],
573
+ },
574
+ "normal": {
575
+ "disease_name": "Normal — طبيعي",
576
+ "description": "لم تظهر علامات مرضية في الصورة. الثدي يبدو بصحة جيدة.",
577
+ "recommendations": [
578
+ "استمري في الفحص الذاتي الشهري للثدي.",
579
+ "احرصي على فحص دوري سنوي عند الطبيب بعد سن الـ 40.",
580
+ "الماموغرام مهم كل عامين للنساء فوق 40 سنة.",
581
+ "نظام غذائي صحي وممارسة الرياضة تقلل خطر سرطان الثدي.",
582
+ "أبلغي الطبيبة فوراً عند ملاحظة أي تغيير غير طبيعي.",
583
+ ],
584
+ },
585
+
586
+ # ── Eye ───────────────────────────────────────────────────────────────────
587
+ "Cataract": {
588
+ "disease_name": "Cataract — إعتام عدسة العين",
589
+ "description": "تعتّم في عدسة العين الطبيعية يؤدي إلى ضبابية في الرؤية. شائع مع التقدم في السن وقابل للعلاج الجراحي.",
590
+ "recommendations": [
591
+ "راجع طبيب عيون لتقييم مدى تأثيره على رؤيتك.",
592
+ "العملية الجراحية بسيطة وآمنة وتعيد الرؤية بشكل كامل في معظم الحالات.",
593
+ "استخدم نظارات شمسية لحماية العينين من الأشعة فوق البنفسجية.",
594
+ "تجنب قيادة السيارة ليلاً إذا كانت رؤيتك ضعيفة.",
595
+ "نظام غذائي غني بالفيتامينات C وE يحمي العينين.",
596
+ ],
597
+ },
598
+ "Diabetic Retinopathy": {
599
+ "disease_name": "Diabetic Retinopathy — اعتلال الشبكية السكري",
600
+ "description": "مضاعفة لمرض السكري تصيب الأوعية الدموية في شبكية العين وقد تؤدي لفقدان البصر إذا لم تُعالج.",
601
+ "recommendations": [
602
+ "تحكّم في مستوى السكر في الدم بشكل صارم.",
603
+ "راجع طبيب شبكية متخصص في أقرب وقت.",
604
+ "قد تحتاج ليزر أو حقن مضادة للـ VEGF حسب الحالة.",
605
+ "افحص عينيك سنوياً على الأقل إذا كنت مصاباً بالسكري.",
606
+ "التحكم في ضغط الدم والكوليسترول يبطئ تطور المرض.",
607
+ ],
608
+ },
609
+ "Glaucoma": {
610
+ "disease_name": "Glaucoma — المياه الزرقاء",
611
+ "description": "مجموعة من أمراض العين تصيب العصب البصري غالباً بسبب ارتفاع ضغط العين. قد تؤدي لفقدان البصر التدريجي.",
612
+ "recommendations": [
613
+ "راجع طبيب عيون فوراً لقياس ضغط العين.",
614
+ "العلاج المبكر بالقطرات أو الجراحة يوقف تطور المرض.",
615
+ "لا تتوقف عن استخدام قطرات الضغط دون استشارة الطبيب.",
616
+ "افحص عينيك دورياً خاصة إذا كان هناك تاريخ عائلي للمرض.",
617
+ "تجنب الأنشطة التي ترفع ضغط العين كرفع الأثقال الشديد.",
618
+ ],
619
+ },
620
+ "Normal": {
621
+ "disease_name": "Normal — طبيعي",
622
+ "description": "لم تظهر علامات لأمراض العين في الصورة. العين تبدو بصحة جيدة.",
623
+ "recommendations": [
624
+ "احرص على فحص العينين مرة كل عام أو عامين.",
625
+ "نظام غذائي غني بالفيتامينات A وC وE مفيد للعيون.",
626
+ "استخدم نظارات شمسية ذات حماية UV عند الخروج.",
627
+ "خذ استراحة كل 20 دقيقة عند استخدام الشاشات.",
628
+ "تجنب التدخين فإنه يزيد خطر أمراض العيون.",
629
+ ],
630
+ },
631
+
632
+ # ── Brain ─────────────────────────────────────────────────────────────────
633
+ "glioma": {
634
+ "disease_name": "Glioma — ورم الغليوما",
635
+ "description": "نوع من أورام المخ ينشأ من الخلايا الدبقية. يتراوح بين درجات منخفضة الخطورة وأخرى عالية الخطورة.",
636
+ "recommendations": [
637
+ "توجّه لطبيب أعصاب أو جراح مخ فوراً.",
638
+ "ستحتاج لتصوير بالرنين المغناطيسي المعزز لتحديد دقة الورم.",
639
+ "خيارات العلاج تشمل: جراحة، إشعاع، كيماوي حسب درجة الورم.",
640
+ "لا تتوقف عن أدوية الصرع إذا وُصفت لك.",
641
+ "الدعم النفسي والعائلي جزء أساسي من رحلة العلاج.",
642
+ ],
643
+ },
644
+ "meningioma": {
645
+ "disease_name": "Meningioma — ورم السحايا",
646
+ "description": "ورم ينشأ من أغشية المخ (السحايا). معظمه حميد وبطيء النمو، لكن قد يسبب ضغطاً على المخ.",
647
+ "recommendations": [
648
+ "راجع طبيب أعصاب لتقييم الورم ومتابعته.",
649
+ "الأورام الصغيرة غير المسببة لأعراض قد تحتاج مراقبة فقط.",
650
+ "الأورام الكبيرة أو المسببة لأعراض قد تحتاج جراحة أو إشعاع.",
651
+ "متابعة بالرنين المغناطيسي كل 6-12 شهراً حسب توجيه الطبيب.",
652
+ "أبلغ الطبيب فوراً عند ظهور صداع شديد أو تغيرات في الرؤية.",
653
+ ],
654
+ },
655
+ "notumor": {
656
+ "disease_name": "No Tumor — لا يوجد ورم",
657
+ "description": "لم تظهر علامات لوجود ورم في الصورة. الدماغ يبدو طبيعياً.",
658
+ "recommendations": [
659
+ "إذا كانت لديك أعراض استشر طبيب أعصاب للتأكد.",
660
+ "فحص دوري سنوي مهم خاصة عند وجود تاريخ عائلي.",
661
+ "نوم كافٍ وتجنب الضغط النفسي الشديد يحمي صحة الدماغ.",
662
+ "مارس الرياضة بانتظام لتحسين الدورة الدموية للمخ.",
663
+ "تجنب التدخين والكحول للحفاظ على صحة الجهاز العصبي.",
664
+ ],
665
+ },
666
+ "pituitary": {
667
+ "disease_name": "Pituitary — ورم الغدة النخامية",
668
+ "description": "ورم ينشأ في الغدة النخامية بقاعدة الدماغ. معظمه حميد لكن قد يؤثر على الهرمونات والرؤية.",
669
+ "recommendations": [
670
+ "راجع طبيب غدد صماء وطبيب أعصاب معاً.",
671
+ "ستحتاج لتحليل هرمونات شاملة لتقييم تأثير الورم.",
672
+ "بعض الأورام تُعالج بالدواء فقط دون جراحة.",
673
+ "تابع رؤيتك — ورم الغدة قد يضغط على العصب البصري.",
674
+ "الفحص الدوري بالرنين المغناطيسي ضروري لمتابعة حجم الورم.",
675
+ ],
676
+ },
677
+
678
+ # ── Heart ─────────────────────────────────────────────────────────────────
679
+ "normal": {
680
+ "disease_name": "Normal — حجم القلب طبيعي",
681
+ "description": "منطقة القلب المكتشفة في الصورة تقع ضمن النطاق الطبيعي المتوقع.",
682
+ "recommendations": [
683
+ "استمر في ممارسة الرياضة بانتظام للحفاظ على صحة القلب.",
684
+ "نظام غذائي صحي قليل الدهون المشبعة مفيد للقلب.",
685
+ "راقب ضغط الدم والكوليسترول بشكل دوري.",
686
+ "تجنب التدخين وتقليل التوتر النفسي.",
687
+ "فحص قلب سنوي مهم خاصة بعد سن الـ 40.",
688
+ ],
689
+ },
690
+ "slightly_large": {
691
+ "disease_name": "Slightly Large — تضخم بسيط في القلب",
692
+ "description": "تم اكتشاف منطقة قلب أكبر قليلاً من المعدل الطبيعي في الصورة. يستدعي مراجعة طبية.",
693
+ "recommendations": [
694
+ "راجع طبيب قلب لإجراء تخطيط قلب وإيكو.",
695
+ "تجنب المجهود الشديد حتى تأكيد التشخيص.",
696
+ "راقب أي أعراض: ضيق تنفس، خفقان، تورم في القدمين.",
697
+ "قلّل الملح في طعامك لتخفيف الضغط على القلب.",
698
+ "الالتزام بالأدوية إذا وصفها الطبيب أمر ضروري.",
699
+ ],
700
+ },
701
+ "abnormally_large": {
702
+ "disease_name": "Abnormally Large — تضخم غير طبيعي في القلب",
703
+ "description": "تم اكتشاف منطقة قلب أكبر بكثير من المعدل الطبيعي. يستدعي تدخلاً طبياً عاجلاً.",
704
+ "recommendations": [
705
+ "اذهب لطبيب قلب أو طوارئ فوراً.",
706
+ "أجرِ تخطيط قلب (ECG) وإيكو قلب (Echocardiogram) عاجلاً.",
707
+ "تجنب أي مجهود بدني حتى تحصل على تقييم طبي.",
708
+ "أخبر الطبيب بكل الأدوية التي تتناولها.",
709
+ "التضخم القلبي قابل للعلاج إذا اكتُشف مبكراً.",
710
+ ],
711
+ },
712
+ "not_detected": {
713
+ "disease_name": "Not Detected — لم يتم اكتشاف القلب",
714
+ "description": "لم يتمكن الموديل من تحديد منطقة القلب بوضوح في الصورة. قد يكون بسبب جودة الصورة.",
715
+ "recommendations": [
716
+ "تأكد من جودة الصورة وأنها CT scan واضحة.",
717
+ "أعد رفع صورة بجودة أعلى وزاوية مناسبة.",
718
+ "راجع طبيب متخصص لإجراء الفحص اللازم.",
719
+ ],
720
+ },
721
+
722
+ # ── Lung ──────────────────────────────────────────────────────────────────
723
+ "Tuberculosis": {
724
+ "disease_name": "Tuberculosis — السل الرئوي",
725
+ "description": "عدوى بكتيرية تصيب الرئتين وتنتشر عن طريق الهواء. من أعراضها السعال المزمن ونزول الدم والتعرق الليلي وفقدان الوزن.",
726
+ "recommendations": [
727
+ "راجع طبيب أمراض صدر فوراً.",
728
+ "يحتاج علاج مضادات حيوية لمدة 6 أشهر على الأقل.",
729
+ "تجنب الاختلاط بالآخرين حتى تأكيد التشخيص.",
730
+ "أجرِ تحليل بلغم وأشعة مقطعية للتأكد.",
731
+ "الالتزام بجرعات الدواء كاملة بدون توقف أمر بالغ الأهمية.",
732
+ ],
733
+ },
734
+ "Pneumonia-Viral": {
735
+ "disease_name": "Pneumonia-Viral — التهاب رئوي فيروسي",
736
+ "description": "التهاب في أنسجة الرئة بسبب فيروس. يسبب حمى وسعالاً وضيق تنفس وإرهاداً شديداً.",
737
+ "recommendations": [
738
+ "راجع طبيب في أقرب وقت.",
739
+ "الراحة التامة وشرب السوائل بكثرة ضروريان.",
740
+ "المضادات الحيوية لا تفيد (فيروسي) — الطبيب سيحدد العلاج المناسب.",
741
+ "راقب مستوى الأكسجين في الدم.",
742
+ "اذهب للطوارئ إذا نزل الأكسجين عن 94% أو اشتد ضيق التنفس.",
743
+ ],
744
+ },
745
+ "Pneumonia-Bacterial": {
746
+ "disease_name": "Pneumonia-Bacterial — التهاب رئوي بكتيري",
747
+ "description": "التهاب رئوي بسبب بكتيريا. أعراضه أشد من الفيروسي وتشمل حمى عالية وسعال بلغمي وألم في الصدر.",
748
+ "recommendations": [
749
+ "اذهب للطبيب فوراً — يحتاج مضادات حيوية.",
750
+ "لا تأخذ مضادات حيوية بدون وصفة طبية.",
751
+ "الراحة التامة والسوائل الكافية ضرورية للتعافي.",
752
+ "في الحالات الشديدة قد تحتاج دخول مستشفى.",
753
+ "أكمل دورة المضادات الحيوية كاملة حتى لو تحسّنت.",
754
+ ],
755
+ },
756
+ "Normal": {
757
+ "disease_name": "Normal — رئة طبيعية",
758
+ "description": "الأشعة لا تظهر أي تغيرات مرضية. الرئتان تبدوان بصحة جيدة.",
759
+ "recommendations": [
760
+ "استمر في الاهتمام بصحتك.",
761
+ "ابتعد عن التدخين والأماكن الملوثة.",
762
+ "مارس الرياضة بانتظام لتقوية الرئتين.",
763
+ "إذا كانت عندك أعراض رغم النتيجة استشر طبيب.",
764
+ "فحص سنوي مهم خاصة لمن يعملون في بيئات ملوثة.",
765
+ ],
766
+ },
767
+ "Emphysema": {
768
+ "disease_name": "Emphysema — انتفاخ الرئة",
769
+ "description": "تلف في الحويصلات الهوائية بالرئة يجعل التنفس صعباً. غالباً مرتبط بالتدخين لفترات طويلة.",
770
+ "recommendations": [
771
+ "راجع طبيب أمراض صدر.",
772
+ "التوقف عن التدخين فوراً هو أهم خطوة يمكنك اتخاذها.",
773
+ "قد تحتاج موسّع للشعب الهوائية (بخاخ).",
774
+ "تمارين التنفس مع متخصص تساعد كثيراً.",
775
+ "تجنب الأماكن الملوثة والغبار والدخان.",
776
+ ],
777
+ },
778
+ "Covid-19": {
779
+ "disease_name": "Covid-19 — كوفيد-19",
780
+ "description": "عدو�� فيروسية تسببها فيروس كورونا. تظهر في الأشعة كبقع زجاجية في الرئتين. أعراضها تتراوح من خفيفة لشديدة.",
781
+ "recommendations": [
782
+ "أجرِ تحليل PCR لتأكيد الإصابة.",
783
+ "عزل فوري لمنع انتشار العدوى.",
784
+ "راجع طبيب لتقييم الحالة وتحديد العلاج.",
785
+ "راقب مستوى الأكسجين — إذا نزل عن 94% اذهب للطوارئ.",
786
+ "الراحة والسوائل والتغذية الجيدة تسرّع التعافي.",
787
+ ],
788
+ },
789
+
790
+ # ── Kidney ────────────────────────────────────────────────────────────────
791
+ "Cyst": {
792
+ "disease_name": "Cyst — كيس في الكلى",
793
+ "description": "تجمّع سائل في الكلى يشكّل كيساً. معظم الأكياس حميدة ولا تسبب أعراضاً، لكن تحتاج متابعة.",
794
+ "recommendations": [
795
+ "راجع طبيب كلى لتقييم حجم الكيس وطبيعته.",
796
+ "معظم الأكياس لا تحتاج علاجاً — متابعة بالأشعة كافية.",
797
+ "اشرب كميات كافية من الماء يومياً (2-3 لتر).",
798
+ "راقب أي أعراض: ألم في الجانب، دم في البول، ارتفاع ضغط.",
799
+ "متابعة بالأشعة فوق الصوتية كل 6-12 شهراً.",
800
+ ],
801
+ },
802
+ "Normal": {
803
+ "disease_name": "Normal — كلية طبيعية",
804
+ "description": "لم تظهر علامات مرضية في صورة الكلى. الكلية تبدو بصحة جيدة.",
805
+ "recommendations": [
806
+ "اشرب كميات كافية من الماء يومياً للحفاظ على صحة الكلى.",
807
+ "قلّل الملح والبروتين الزائد في نظامك الغذائي.",
808
+ "راقب ضغط الدم والسكر لأنهما يؤثران على الكلى.",
809
+ "تجنب الإفراط في مسكنات الألم (مثل الإيبوبروفين).",
810
+ "فحص وظائف الكلى سنوياً مهم خاصة بعد سن الـ 40.",
811
+ ],
812
+ },
813
+ "Stone": {
814
+ "disease_name": "Stone — حصى في الكلى",
815
+ "description": "تراكم معادن وأملاح داخل الكلى يشكّل حصوات. قد تسبب ألماً شديداً عند تحرّكها.",
816
+ "recommendations": [
817
+ "راجع طبيب كلى أو مسالك بولية لتحديد حجم الحصى.",
818
+ "اشرب 3 لتر ماء يومياً على الأقل لمساعدة الحصى على الخروج.",
819
+ "تجنب الأطعمة الغنية بالأوكسالات كالسبانخ والشوكولاتة.",
820
+ "الحصى الصغيرة تخرج وحدها — الكبيرة تحتاج تفتيت أو جراحة.",
821
+ "اذهب للطوارئ إذا اشتد الألم أو ظهر دم في البول مع حمى.",
822
+ ],
823
+ },
824
+ "Tumor": {
825
+ "disease_name": "Tumor — ورم في الكلى",
826
+ "description": "تم اكتشاف كتلة غير طبيعية في الكلى. يحتاج تقييماً طبياً عاجلاً لتحديد طبيعة الورم.",
827
+ "recommendations": [
828
+ "توجّه لطبيب أورام أو كلى فوراً — لا تتأخر.",
829
+ "ستحتاج لأشعة مقطعية (CT) بصبغة لتحديد طبيعة الورم.",
830
+ "خيارات العلاج تشمل: جراحة، استئصال جزئي أو كلي للكلى، أو علاج موضعي.",
831
+ "الاكتشاف المبكر يرفع نسبة الشفاء بشكل كبير.",
832
+ "لا تتجاهل أي أعراض: دم في البول، ألم في الجانب، فقدان وزن.",
833
+ ],
834
+ },
835
+ }
836
+
837
+
838
+ @app.get("/")
839
+ def root():
840
+ return {
841
+ "message": "Medical AI API — 7 models",
842
+ "loaded_models": list(MODELS.keys()),
843
+ "endpoints": [
844
+ "/predict/skin",
845
+ "/predict/breast",
846
+ "/predict/eye",
847
+ "/predict/brain",
848
+ "/predict/heart",
849
+ "/predict/lung",
850
+ "/predict/kidney",
851
+ ],
852
+ }
853
+
854
+
855
+ @app.get("/health")
856
+ def health():
857
+ return {"status": "ok", "loaded_models": list(MODELS.keys())}
858
+
859
+
860
+ # ══════════════════════════════════════════════════════════════════════════════
861
+ # 1 ─ SKIN
862
+ # ══════════════════════════════════════════════════════════════════════════════
863
+ SKIN_LABELS = ["AK", "BCC", "BKL", "DF", "MEL", "NV", "SCC", "VASC"]
864
+ SKIN_NAMES = {
865
+ "AK": "Actinic Keratosis",
866
+ "BCC": "Basal Cell Carcinoma",
867
+ "BKL": "Benign Keratosis",
868
+ "DF": "Dermatofibroma",
869
+ "MEL": "Melanoma",
870
+ "NV": "Melanocytic Nevi",
871
+ "SCC": "Squamous Cell Carcinoma",
872
+ "VASC": "Vascular Lesion",
873
+ }
874
+ SKIN_SEVERITY = {
875
+ "MEL": "high", "BCC": "high", "SCC": "high",
876
+ "AK": "medium",
877
+ "BKL": "low", "DF": "low", "NV": "low", "VASC": "low",
878
+ }
879
+
880
+
881
+ @app.post("/predict/skin")
882
+ async def predict_skin(
883
+ file: UploadFile = File(...),
884
+ age: Optional[int] = Form(None),
885
+ gender: Optional[str] = Form(None),
886
+ symptoms: Optional[str] = Form(None),
887
+ chronic_diseases: Optional[str] = Form(None),
888
+ medications: Optional[str] = Form(None),
889
+ ):
890
+ """
891
+ Classify skin lesion from a dermatoscopy image.
892
+
893
+ Returns: predicted class, confidence, severity, all probabilities, AI medical report.
894
+
895
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
896
+ """
897
+ _require("skin")
898
+ img = await read_image(file)
899
+ arr = np.array(img.convert("RGB").resize((28, 28)), dtype=np.float32)
900
+ arr = np.expand_dims(arr, 0)
901
+
902
+ probs = MODELS["skin"].predict(arr, verbose=0)[0]
903
+ idx = int(np.argmax(probs))
904
+ label = SKIN_LABELS[idx]
905
+ confidence = round(float(probs[idx]) * 100, 2)
906
+
907
+ rec = RECOMMENDATIONS.get(label, {})
908
+ report = generate_medical_report(
909
+ model_type="skin",
910
+ predicted_class=label,
911
+ disease_name=rec.get("disease_name", label),
912
+ confidence=confidence,
913
+ age=age, gender=gender, symptoms=symptoms,
914
+ chronic_diseases=chronic_diseases, medications=medications,
915
+ )
916
+ return {
917
+ "model": "skin",
918
+ "predicted_class": label,
919
+ "predicted_name": SKIN_NAMES[label],
920
+ "confidence": confidence,
921
+ "severity": SKIN_SEVERITY[label],
922
+ "all_probabilities": {
923
+ SKIN_LABELS[i]: round(float(p) * 100, 2)
924
+ for i, p in enumerate(probs)
925
+ },
926
+ "disease_info": {
927
+ "disease_name": rec.get("disease_name", ""),
928
+ "description": rec.get("description", ""),
929
+ "recommendations": rec.get("recommendations", []),
930
+ },
931
+ "medical_report": report,
932
+ }
933
+
934
+
935
+ # ══════════════════════════════════════════════════════════════════════════════
936
+ # 2 ─ BREAST
937
+ # ══════════════════════════════════════════════════════════════════════════════
938
+ BREAST_CLASSES = ["benign", "malignant", "normal"]
939
+
940
+
941
+ @app.post("/predict/breast")
942
+ async def predict_breast(
943
+ file: UploadFile = File(...),
944
+ age: Optional[int] = Form(None),
945
+ gender: Optional[str] = Form(None),
946
+ symptoms: Optional[str] = Form(None),
947
+ chronic_diseases: Optional[str] = Form(None),
948
+ medications: Optional[str] = Form(None),
949
+ ):
950
+ """
951
+ Breast ultrasound — runs segmentation + classification.
952
+
953
+ Returns:
954
+ - classification result (benign / malignant / normal)
955
+ - segmentation mask stats (mean activation, coverage %)
956
+ - AI medical report
957
+
958
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
959
+ """
960
+ _require("breast_seg")
961
+ _require("breast_cls")
962
+
963
+ img = await read_image(file)
964
+
965
+ # ── segmentation ──
966
+ seg_in = np.array(img.convert("RGB").resize((256, 256)), dtype=np.float32) / 255.0
967
+ seg_in = np.expand_dims(seg_in, 0)
968
+ mask = MODELS["breast_seg"].predict(seg_in, verbose=0)[0, :, :, 0]
969
+ coverage = round(float(np.mean(mask > 0.5)) * 100, 2)
970
+
971
+ # ── classification ──
972
+ cls_in = np.array(img.convert("RGB").resize((224, 224)), dtype=np.float32)
973
+ cls_in = np.expand_dims(cls_in, 0)
974
+ probs = MODELS["breast_cls"].predict(cls_in, verbose=0)[0]
975
+ idx = int(np.argmax(probs))
976
+ confidence = round(float(probs[idx]) * 100, 2)
977
+
978
+ mask_b64 = _mask_to_base64(mask, colormap="plasma")
979
+ overlay_b64 = _overlay_to_base64(img, mask, size=(256, 256))
980
+ rec = RECOMMENDATIONS.get(BREAST_CLASSES[idx], {})
981
+ report = generate_medical_report(
982
+ model_type="breast",
983
+ predicted_class=BREAST_CLASSES[idx],
984
+ disease_name=rec.get("disease_name", BREAST_CLASSES[idx]),
985
+ confidence=confidence,
986
+ age=age, gender=gender, symptoms=symptoms,
987
+ chronic_diseases=chronic_diseases, medications=medications,
988
+ )
989
+ return {
990
+ "model": "breast",
991
+ "predicted_class": BREAST_CLASSES[idx],
992
+ "confidence": confidence,
993
+ "all_probabilities": {
994
+ BREAST_CLASSES[i]: round(float(p) * 100, 2)
995
+ for i, p in enumerate(probs)
996
+ },
997
+ "segmentation": {
998
+ "mask_mean_activation": round(float(np.mean(mask)), 4),
999
+ "lesion_coverage_percent": coverage,
1000
+ "mask_image_base64": mask_b64,
1001
+ "overlay_image_base64": overlay_b64,
1002
+ },
1003
+ "disease_info": {
1004
+ "disease_name": rec.get("disease_name", ""),
1005
+ "description": rec.get("description", ""),
1006
+ "recommendations": rec.get("recommendations", []),
1007
+ },
1008
+ "medical_report": report,
1009
+ }
1010
+
1011
+
1012
+ # ══════════════════════════════════════════════════════════════════════════════
1013
+ # 3 ─ EYE
1014
+ # ══════════════════════════════════════════════════════════════════════════════
1015
+ EYE_CLASSES = ["Cataract", "Diabetic Retinopathy", "Glaucoma", "Normal"]
1016
+
1017
+
1018
+ @app.post("/predict/eye")
1019
+ async def predict_eye(
1020
+ file: UploadFile = File(...),
1021
+ age: Optional[int] = Form(None),
1022
+ gender: Optional[str] = Form(None),
1023
+ symptoms: Optional[str] = Form(None),
1024
+ chronic_diseases: Optional[str] = Form(None),
1025
+ medications: Optional[str] = Form(None),
1026
+ ):
1027
+ """
1028
+ Classify eye disease from a fundus / eye image.
1029
+
1030
+ Returns: predicted class, confidence, all probabilities, AI medical report.
1031
+
1032
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
1033
+ """
1034
+ _require("eye")
1035
+ from tensorflow.keras.applications.efficientnet import preprocess_input
1036
+
1037
+ img = await read_image(file)
1038
+ arr = np.array(img.convert("RGB").resize((224, 224)), dtype=np.float32)
1039
+ arr = preprocess_input(arr)
1040
+ arr = np.expand_dims(arr, 0)
1041
+
1042
+ probs = MODELS["eye"].predict(arr, verbose=0)[0]
1043
+ idx = int(np.argmax(probs))
1044
+ confidence = round(float(probs[idx]) * 100, 2)
1045
+
1046
+ rec = RECOMMENDATIONS.get(EYE_CLASSES[idx], {})
1047
+ report = generate_medical_report(
1048
+ model_type="eye",
1049
+ predicted_class=EYE_CLASSES[idx],
1050
+ disease_name=rec.get("disease_name", EYE_CLASSES[idx]),
1051
+ confidence=confidence,
1052
+ age=age, gender=gender, symptoms=symptoms,
1053
+ chronic_diseases=chronic_diseases, medications=medications,
1054
+ )
1055
+ return {
1056
+ "model": "eye",
1057
+ "predicted_class": EYE_CLASSES[idx],
1058
+ "confidence": confidence,
1059
+ "all_probabilities": {
1060
+ EYE_CLASSES[i]: round(float(p) * 100, 2)
1061
+ for i, p in enumerate(probs)
1062
+ },
1063
+ "disease_info": {
1064
+ "disease_name": rec.get("disease_name", ""),
1065
+ "description": rec.get("description", ""),
1066
+ "recommendations": rec.get("recommendations", []),
1067
+ },
1068
+ "medical_report": report,
1069
+ }
1070
+
1071
+
1072
+ # ══════════════════════════════════════════════════════════════════════════════
1073
+ # 4 ─ BRAIN
1074
+ # ══════════════════════════════════════════════════════════════════════════════
1075
+ BRAIN_CLASSES = ["glioma", "meningioma", "notumor", "pituitary"]
1076
+
1077
+
1078
+ @app.post("/predict/brain")
1079
+ async def predict_brain(
1080
+ file: UploadFile = File(...),
1081
+ age: Optional[int] = Form(None),
1082
+ gender: Optional[str] = Form(None),
1083
+ symptoms: Optional[str] = Form(None),
1084
+ chronic_diseases: Optional[str] = Form(None),
1085
+ medications: Optional[str] = Form(None),
1086
+ ):
1087
+ """
1088
+ Classify brain tumor type from an MRI image.
1089
+
1090
+ Returns: predicted class, confidence, all probabilities, AI medical report.
1091
+
1092
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
1093
+ """
1094
+ _require("brain")
1095
+ import cv2
1096
+
1097
+ img_pil = await read_image(file)
1098
+ img_np = np.array(img_pil.convert("RGB"))
1099
+ img_np = cv2.resize(img_np, (224, 224))
1100
+ arr = img_np.astype(np.float32) / 255.0
1101
+ arr = np.expand_dims(arr, 0)
1102
+
1103
+ probs = MODELS["brain"].predict(arr, verbose=0)[0]
1104
+ idx = int(np.argmax(probs))
1105
+ confidence = round(float(probs[idx]) * 100, 2)
1106
+
1107
+ rec = RECOMMENDATIONS.get(BRAIN_CLASSES[idx], {})
1108
+ report = generate_medical_report(
1109
+ model_type="brain",
1110
+ predicted_class=BRAIN_CLASSES[idx],
1111
+ disease_name=rec.get("disease_name", BRAIN_CLASSES[idx]),
1112
+ confidence=confidence,
1113
+ age=age, gender=gender, symptoms=symptoms,
1114
+ chronic_diseases=chronic_diseases, medications=medications,
1115
+ )
1116
+ return {
1117
+ "model": "brain",
1118
+ "predicted_class": BRAIN_CLASSES[idx],
1119
+ "confidence": confidence,
1120
+ "all_probabilities": {
1121
+ BRAIN_CLASSES[i]: round(float(p) * 100, 2)
1122
+ for i, p in enumerate(probs)
1123
+ },
1124
+ "disease_info": {
1125
+ "disease_name": rec.get("disease_name", ""),
1126
+ "description": rec.get("description", ""),
1127
+ "recommendations": rec.get("recommendations", []),
1128
+ },
1129
+ "medical_report": report,
1130
+ }
1131
+
1132
+
1133
+ # ══════════════════════════════════════════════════════════════════════════════
1134
+ # 5 ─ HEART
1135
+ # ══════════════════════════════════════════════════════════════════════════════
1136
+ @app.post("/predict/heart")
1137
+ async def predict_heart(
1138
+ file: UploadFile = File(...),
1139
+ age: Optional[int] = Form(None),
1140
+ gender: Optional[str] = Form(None),
1141
+ symptoms: Optional[str] = Form(None),
1142
+ chronic_diseases: Optional[str] = Form(None),
1143
+ medications: Optional[str] = Form(None),
1144
+ ):
1145
+ """
1146
+ Heart segmentation from a CT scan image.
1147
+
1148
+ Returns:
1149
+ - heart_area_ratio (%)
1150
+ - assessment (normal / slightly_large / abnormally_large / not_detected)
1151
+ - mask stats
1152
+ - AI medical report
1153
+
1154
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
1155
+ """
1156
+ _require("heart")
1157
+
1158
+ img = await read_image(file)
1159
+ arr = np.array(img.convert("L").resize((128, 128)), dtype=np.float32) / 255.0
1160
+ arr = np.expand_dims(arr, (0, 3))
1161
+
1162
+ mask = MODELS["heart"].predict(arr, verbose=0)[0, :, :, 0]
1163
+ mask_bin = (mask > 0.5).astype(np.uint8)
1164
+ ratio = float(np.sum(mask_bin)) / (128 * 128)
1165
+
1166
+ if ratio < 0.01:
1167
+ assessment = "not_detected"
1168
+ elif ratio < 0.15:
1169
+ assessment = "normal"
1170
+ elif ratio < 0.35:
1171
+ assessment = "slightly_large"
1172
+ else:
1173
+ assessment = "abnormally_large"
1174
+
1175
+ confidence = round(float(np.max(mask)) * 100, 2)
1176
+ mask_b64 = _mask_to_base64(mask, colormap="hot")
1177
+ overlay_b64 = _overlay_to_base64(img, mask, size=(128, 128))
1178
+ rec = RECOMMENDATIONS.get(assessment, {})
1179
+ report = generate_medical_report(
1180
+ model_type="heart",
1181
+ predicted_class=assessment,
1182
+ disease_name=rec.get("disease_name", assessment),
1183
+ confidence=confidence,
1184
+ age=age, gender=gender, symptoms=symptoms,
1185
+ chronic_diseases=chronic_diseases, medications=medications,
1186
+ )
1187
+ return {
1188
+ "model": "heart",
1189
+ "task": "segmentation",
1190
+ "heart_area_ratio_percent": round(ratio * 100, 2),
1191
+ "assessment": assessment,
1192
+ "mask_stats": {
1193
+ "mean_activation": round(float(np.mean(mask)), 4),
1194
+ "max_activation": round(float(np.max(mask)), 4),
1195
+ "detected_pixels": int(np.sum(mask_bin)),
1196
+ },
1197
+ "segmentation": {
1198
+ "mask_image_base64": mask_b64,
1199
+ "overlay_image_base64": overlay_b64,
1200
+ },
1201
+ "disease_info": {
1202
+ "disease_name": rec.get("disease_name", ""),
1203
+ "description": rec.get("description", ""),
1204
+ "recommendations": rec.get("recommendations", []),
1205
+ },
1206
+ "medical_report": report,
1207
+ }
1208
+
1209
+
1210
+ # ══════════════════════════════════════════════════════════════════════════════
1211
+ # 6 ─ LUNG (PyTorch)
1212
+ # ══════════════════════════════════════════════════════════════════════════════
1213
+ LUNG_CLASSES = [
1214
+ "Tuberculosis", "Pneumonia-Viral", "Pneumonia-Bacterial",
1215
+ "Normal", "Emphysema", "Covid-19",
1216
+ ]
1217
+
1218
+
1219
+ @app.post("/predict/lung")
1220
+ async def predict_lung(
1221
+ file: UploadFile = File(...),
1222
+ age: Optional[int] = Form(None),
1223
+ gender: Optional[str] = Form(None),
1224
+ symptoms: Optional[str] = Form(None),
1225
+ chronic_diseases: Optional[str] = Form(None),
1226
+ medications: Optional[str] = Form(None),
1227
+ ):
1228
+ """
1229
+ Classify chest X-ray disease (6 classes).
1230
+
1231
+ Returns: predicted class, confidence, all probabilities, AI medical report.
1232
+
1233
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
1234
+ """
1235
+ _require("lung")
1236
+ import torch
1237
+ import albumentations as A
1238
+ from albumentations.pytorch import ToTensorV2
1239
+
1240
+ transform = A.Compose([
1241
+ A.Resize(224, 224),
1242
+ A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
1243
+ ToTensorV2(),
1244
+ ])
1245
+
1246
+ img = await read_image(file)
1247
+ arr = np.array(img.convert("RGB"))
1248
+ t = transform(image=arr)["image"].unsqueeze(0)
1249
+
1250
+ with torch.no_grad():
1251
+ probs = torch.softmax(MODELS["lung"](t), dim=1).squeeze().numpy()
1252
+
1253
+ idx = int(np.argmax(probs))
1254
+ confidence = round(float(probs[idx]) * 100, 2)
1255
+
1256
+ rec = RECOMMENDATIONS.get(LUNG_CLASSES[idx], {})
1257
+ report = generate_medical_report(
1258
+ model_type="lung",
1259
+ predicted_class=LUNG_CLASSES[idx],
1260
+ disease_name=rec.get("disease_name", LUNG_CLASSES[idx]),
1261
+ confidence=confidence,
1262
+ age=age, gender=gender, symptoms=symptoms,
1263
+ chronic_diseases=chronic_diseases, medications=medications,
1264
+ )
1265
+ return {
1266
+ "model": "lung",
1267
+ "predicted_class": LUNG_CLASSES[idx],
1268
+ "confidence": confidence,
1269
+ "all_probabilities": {
1270
+ LUNG_CLASSES[i]: round(float(p) * 100, 2)
1271
+ for i, p in enumerate(probs)
1272
+ },
1273
+ "disease_info": {
1274
+ "disease_name": rec.get("disease_name", ""),
1275
+ "description": rec.get("description", ""),
1276
+ "recommendations": rec.get("recommendations", []),
1277
+ },
1278
+ "medical_report": report,
1279
+ }
1280
+
1281
+
1282
+ # ══════════════════════════════════════════════════════════════════════════════
1283
+ # 7 ─ KIDNEY
1284
+ # ══════════════════════════════════════════════════════════════════════════════
1285
+ KIDNEY_CLASSES = ["Cyst", "Normal", "Stone", "Tumor"]
1286
+
1287
+
1288
+ @app.post("/predict/kidney")
1289
+ async def predict_kidney(
1290
+ file: UploadFile = File(...),
1291
+ age: Optional[int] = Form(None),
1292
+ gender: Optional[str] = Form(None),
1293
+ symptoms: Optional[str] = Form(None),
1294
+ chronic_diseases: Optional[str] = Form(None),
1295
+ medications: Optional[str] = Form(None),
1296
+ ):
1297
+ """
1298
+ Classify kidney disease from a CT scan image.
1299
+
1300
+ Returns: predicted class, confidence, all probabilities, AI medical report.
1301
+
1302
+ Optional form fields: age, gender, symptoms, chronic_diseases, medications
1303
+ """
1304
+ _require("kidney")
1305
+
1306
+ img = await read_image(file)
1307
+ arr = np.array(img.convert("L").resize((200, 200)), dtype=np.float32) / 255.0
1308
+ arr = arr.reshape(1, 200, 200, 1)
1309
+
1310
+ probs = MODELS["kidney"].predict(arr, verbose=0)[0]
1311
+ idx = int(np.argmax(probs))
1312
+ confidence = round(float(probs[idx]) * 100, 2)
1313
+
1314
+ rec = RECOMMENDATIONS.get(KIDNEY_CLASSES[idx], {})
1315
+ report = generate_medical_report(
1316
+ model_type="kidney",
1317
+ predicted_class=KIDNEY_CLASSES[idx],
1318
+ disease_name=rec.get("disease_name", KIDNEY_CLASSES[idx]),
1319
+ confidence=confidence,
1320
+ age=age, gender=gender, symptoms=symptoms,
1321
+ chronic_diseases=chronic_diseases, medications=medications,
1322
+ )
1323
+ return {
1324
+ "model": "kidney",
1325
+ "predicted_class": KIDNEY_CLASSES[idx],
1326
+ "confidence": confidence,
1327
+ "all_probabilities": {
1328
+ KIDNEY_CLASSES[i]: round(float(p) * 100, 2)
1329
+ for i, p in enumerate(probs)
1330
+ },
1331
+ "disease_info": {
1332
+ "disease_name": rec.get("disease_name", ""),
1333
+ "description": rec.get("description", ""),
1334
+ "recommendations": rec.get("recommendations", []),
1335
+ },
1336
+ "medical_report": report,
1337
+ }
model_lung.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torchvision import models
4
+
5
+
6
+ class AttentionBlock(nn.Module):
7
+ def __init__(self, in_features: int):
8
+ super().__init__()
9
+ self.attention = nn.Sequential(
10
+ nn.Linear(in_features, in_features // 8),
11
+ nn.ReLU(inplace=True),
12
+ nn.Linear(in_features // 8, in_features),
13
+ nn.Sigmoid(),
14
+ )
15
+
16
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
17
+ return x * self.attention(x)
18
+
19
+
20
+ class HybridModel(nn.Module):
21
+ """ResNet-152 + EfficientNet-B5 hybrid with attention classifier."""
22
+
23
+ def __init__(self, num_classes: int = 6):
24
+ super().__init__()
25
+
26
+ # ── ResNet-152 backbone ──
27
+ self.resnet = models.resnet152(weights=None)
28
+ resnet_features = self.resnet.fc.in_features
29
+ self.resnet.fc = nn.Identity()
30
+
31
+ # ── EfficientNet-B5 backbone ──
32
+ self.effnet = models.efficientnet_b5(weights=None)
33
+ effnet_features = self.effnet.classifier[1].in_features
34
+ self.effnet.classifier = nn.Identity()
35
+
36
+ combined = resnet_features + effnet_features
37
+
38
+ self.classifier = nn.Sequential(
39
+ nn.Dropout(p=0.5),
40
+ nn.Linear(combined, 1024),
41
+ nn.BatchNorm1d(1024),
42
+ nn.ReLU(inplace=True),
43
+ AttentionBlock(1024),
44
+ nn.Dropout(p=0.4),
45
+ nn.Linear(1024, 512),
46
+ nn.BatchNorm1d(512),
47
+ nn.ReLU(inplace=True),
48
+ nn.Dropout(p=0.3),
49
+ nn.Linear(512, num_classes),
50
+ )
51
+
52
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
53
+ feats = torch.cat((self.resnet(x), self.effnet(x)), dim=1)
54
+ return self.classifier(feats)
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn[standard]>=0.29.0
3
+ python-multipart>=0.0.9
4
+ Pillow>=10.3.0
5
+ numpy>=1.26.0
6
+ opencv-python-headless>=4.9.0
7
+ tensorflow==2.15.0
8
+ torch>=2.2.0
9
+ torchvision>=0.17.0
10
+ albumentations>=1.4.0
11
+ pydantic>=2.7.0
12
+ annotated-doc>=0.0.2
13
+ huggingface_hub>=0.20.0
14
+ matplotlib>=3.7.0
15
+