""" Medical AI FastAPI — 7 Models in One API ========================================== Models: 1. Skin Cancer Classification → /predict/skin 2. Breast Cancer Seg + Class → /predict/breast 3. Eye Disease Classification → /predict/eye 4. Brain Tumor Classification → /predict/brain 5. Heart CT Segmentation → /predict/heart 6. Lung/Chest X-Ray Classification → /predict/lung 7. Kidney Disease Classification → /predict/kidney """ from fastapi import FastAPI, File, UploadFile, HTTPException, Form from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager import numpy as np from PIL import Image import io, os, logging import base64 import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.cm as cm from groq import Groq from typing import Optional # ── Groq setup ──────────────────────────────────────────────────────────────── GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") _groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None GROQ_MODEL = "llama-3.3-70b-versatile" # نوع الصورة لكل موديل — بيساعد Gemini يفهم السياق MODEL_IMAGE_TYPE = { "skin": "صورة جلدية (ديرماتوسكوبي)", "breast": "أشعة موجات فوق صوتية للثدي", "eye": "صورة قاع العين (فانداس)", "brain": "صورة رنين مغناطيسي للمخ", "heart": "أشعة مقطعية CT للصدر", "lung": "أشعة X-Ray للصدر", "kidney": "أشعة مقطعية CT للكلى", } def generate_medical_report( model_type: str, predicted_class: str, disease_name: str, confidence: float, age: Optional[int], gender: Optional[str], symptoms: Optional[str], chronic_diseases: Optional[str], medications: Optional[str], ) -> str: """ يبعت بيانات المريض والتشخيص لـ Gemini ويرجع تقرير طبي مخصص بالعربي. لو Gemini مش متاح يرجع None وهنستخدم الـ recommendations الثابتة. """ if not _groq_client: return None # تحديد مستوى الـ confidence بلغة مفهومة if confidence >= 85: confidence_note = "نتيجة ذات ثقة عالية" elif confidence >= 65: confidence_note = "نتيجة تحتاج تأكيداً من الطبيب" else: confidence_note = "نتيجة غير حاسمة — الفحص الطبي ضروري للتأكيد" gender_ar = "ذكر" if gender and gender.lower() in ("male", "m", "ذكر") else "أنثى" if gender else "غير محدد" patient_info = f"العمر: {age} سنة" if age else "العمر: غير محدد" patient_info += f" | الجنس: {gender_ar}" if symptoms: patient_info += f" | الأعراض: {symptoms}" if chronic_diseases: patient_info += f" | الأمراض المزمنة: {chronic_diseases}" if medications: patient_info += f" | الأدوية الحالية: {medications}" prompt = f"""أنت مساعد طبي متعاطف ومتوازن. مهمتك كتابة رسالة طبية قصيرة ومطمئنة بالعربي للمريض. [بيانات الحالة] - نوع الفحص: {MODEL_IMAGE_TYPE.get(model_type, model_type)} - نتيجة التحليل: {disease_name} - مستوى الثقة في النتيجة: {confidence:.1f}% ({confidence_note}) - {patient_info} [قواعد صارمة لا تخالفها أبداً] - لا تذكر أرقام أو إحصائيات عن نسب الوفاة أو الانتشار - لا تجزم بالتشخيص النهائي — هذا حق الطبيب المختص فقط - لا تذكر أسماء أدوية أو جرعات أو بروتوكولات علاج بالتفصيل - لا تستخدم عبارات تخويف مثل: "قد يتطور إلى"، "في الحالات الشديدة"، "خطر الانتشار" - لا تكرر نسبة الـ confidence بالأرقام في التقرير - إذا كانت الثقة أقل من 65% ابدأ بجملة تؤكد ضرورة الفحص الطبي للتأكد - إذا كانت الحالة طبيعية (Normal / No Tumor) ركز على الطمأنينة والوقاية [تعليمات الأسلوب] - النبرة: دافئة، واضحة، غير مبالغة - الطول: 4 إلى 6 جمل فقط لا أكثر - اللغة: عربية فصحى مبسطة بدون مصطلحات طبية معقدة [هيكل التقرير] 1. جملة افتتاحية مطمئنة تذكر فيها نتيجة الفحص بشكل محايد 2. ملاحظة واحدة مخصصة لبيانات المريض (العمر أو الجنس أو الأعراض إذا كانت متوفرة) 3. الخطوة العملية الواحدة القادمة (مراجعة طبيب — أيّ نوع) 4. جملة ختامية داعمة ومشجعة اكتب التقرير مباشرة بدون عناوين أو نقاط أو مقدمات.""" try: response = _groq_client.chat.completions.create( model=GROQ_MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.4, max_tokens=350, ) return response.choices[0].message.content.strip() except Exception as e: logger.warning(f"⚠️ Groq report generation failed: {e}") return None def _mask_to_base64(mask: np.ndarray, colormap: str = "plasma") -> str: """Convert a 2-D float mask → PNG base64 string.""" import io norm = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8) cmap = cm.get_cmap(colormap) rgba = (cmap(norm) * 255).astype(np.uint8) img = Image.fromarray(rgba) buf = io.BytesIO() img.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode("utf-8") def _overlay_to_base64(orig: Image.Image, mask: np.ndarray, size: tuple, alpha: float = 0.45) -> str: """Blend original image with coloured mask → PNG base64 string.""" import io img_rgb = np.array(orig.convert("RGB").resize(size)) norm = (mask - mask.min()) / (mask.max() - mask.min() + 1e-8) cmap = cm.get_cmap("plasma") colored = (cmap(norm)[:, :, :3] * 255).astype(np.uint8) overlay = (img_rgb * (1 - alpha) + colored * alpha).astype(np.uint8) buf = io.BytesIO() Image.fromarray(overlay).save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode("utf-8") logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # ────────────────────────────────────────────────────────────────────────────── # Global model store # ────────────────────────────────────────────────────────────────────────────── MODELS: dict = {} def _build_skin_cnn(): """Custom CNN — same architecture used during skin cancer training (28x28x3, 8 classes).""" import tensorflow as tf from tensorflow.keras.regularizers import l2 model = tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3,3), activation='relu', padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(0.01), input_shape=(28, 28, 3)), tf.keras.layers.MaxPooling2D(), tf.keras.layers.BatchNormalization(), tf.keras.layers.Conv2D(64, (3,3), activation='relu', padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(0.01)), tf.keras.layers.MaxPooling2D(), tf.keras.layers.BatchNormalization(), tf.keras.layers.Conv2D(128, (3,3), activation='relu', padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(0.01)), tf.keras.layers.MaxPooling2D(), tf.keras.layers.BatchNormalization(), tf.keras.layers.Flatten(), tf.keras.layers.Dropout(0.5), tf.keras.layers.Dense(256, activation='relu', kernel_initializer='he_normal', kernel_regularizer=l2(0.01)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(128, activation='relu', kernel_initializer='he_normal', kernel_regularizer=l2(0.01)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(64, activation='relu', kernel_initializer='he_normal', kernel_regularizer=l2(0.01)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dense(8, activation='softmax'), ]) return model def _build_eye_model(): """EfficientNetB3 + Dense — same architecture used during eye disease training (224x224x3, 4 classes).""" import tensorflow as tf from tensorflow.keras import regularizers base_model = tf.keras.applications.EfficientNetB3( include_top=False, weights=None, input_shape=(224, 224, 3), pooling='max' ) model = tf.keras.Sequential([ base_model, tf.keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001), tf.keras.layers.Dense(256, kernel_regularizer=regularizers.l2(0.016), activity_regularizer=regularizers.l1(0.006), bias_regularizer=regularizers.l1(0.006), activation='relu'), tf.keras.layers.Dropout(rate=0.45, seed=123), tf.keras.layers.Dense(4, activation='softmax'), ]) return model def _build_brain_cnn(): """VGG-like CNN — same architecture used during brain tumor training.""" import tensorflow as tf model = tf.keras.Sequential([ tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu", input_shape=(224,224,3)), tf.keras.layers.Conv2D(64, (3,3), padding="same", activation="relu"), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Conv2D(128, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(128, (3,3), padding="same", activation="relu"), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(256, (3,3), padding="same", activation="relu"), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.Conv2D(512, (3,3), padding="same", activation="relu"), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(256, activation="relu"), tf.keras.layers.Dense(64, activation="relu"), tf.keras.layers.Dense(4, activation="softmax"), ]) return model def _build_kidney_cnn(): """CNN architecture used during kidney disease training.""" import tensorflow as tf model = tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3,3), activation="relu", input_shape=(200,200,1)), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(32, (3,3), activation="relu"), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(64, (3,3), activation="relu"), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(64, (3,3), activation="relu"), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(128, (3,3), activation="relu"), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Conv2D(128, (3,3), activation="relu"), tf.keras.layers.MaxPooling2D(2), tf.keras.layers.Flatten(), tf.keras.layers.Dense(512, activation="relu"), tf.keras.layers.Dense(4, activation="softmax"), ]) return model HF_REPO = "morefaat69/medical-ai-models" WEIGHTS_DIR = "weights" def _download_weight(filename: str) -> str: """Download a model file from Hugging Face if not already cached locally.""" from huggingface_hub import hf_hub_download local_path = os.path.join(WEIGHTS_DIR, filename) if not os.path.exists(local_path): logger.info(f"⬇️ Downloading {filename} from Hugging Face...") os.makedirs(WEIGHTS_DIR, exist_ok=True) downloaded = hf_hub_download( repo_id=HF_REPO, filename=filename, local_dir=WEIGHTS_DIR, ) logger.info(f"✅ {filename} downloaded") return downloaded logger.info(f"📦 {filename} already cached locally") return local_path def load_all_models(): """Download weights from Hugging Face (if needed) then load all models.""" import tensorflow as tf # 1 ─ Skin try: path = _download_weight("skin_model.h5") MODELS["skin"] = tf.keras.models.load_model(path, compile=False) logger.info("✅ Skin model loaded") except Exception as e: logger.warning(f"⚠️ Skin model failed: {e}") # 2 ─ Breast (two sub-models) try: seg_path = _download_weight("Final Final Breast Cancer Segmentation.h5") cls_path = _download_weight("bes__model.h5") MODELS["breast_seg"] = tf.keras.models.load_model(seg_path, compile=False) MODELS["breast_cls"] = tf.keras.models.load_model(cls_path, compile=False) logger.info("✅ Breast models loaded") except Exception as e: logger.warning(f"⚠️ Breast models failed: {e}") # 3 ─ Eye try: path = _download_weight("eye_model_fixed.h5") MODELS["eye"] = tf.keras.models.load_model(path, compile=False) logger.info("✅ Eye model loaded") except Exception as e: logger.warning(f"⚠️ Eye model failed: {e}") # 4 ─ Brain try: path = _download_weight("brain_model.h5") try: MODELS["brain"] = tf.keras.models.load_model(path, compile=False) except TypeError: brain_model = _build_brain_cnn() brain_model.load_weights(path) MODELS["brain"] = brain_model logger.info("✅ Brain model loaded") except Exception as e: logger.warning(f"⚠️ Brain model failed: {e}") # 5 ─ Heart try: path = _download_weight("heart_segmentation_model.h5") MODELS["heart"] = _build_unet() MODELS["heart"].load_weights(path) logger.info("✅ Heart model loaded") except Exception as e: logger.warning(f"⚠️ Heart model failed: {e}") # 6 ─ Lung (PyTorch) try: import torch from model_lung import HybridModel path = _download_weight("final_ChestX6_hybrid_model.pth") m = HybridModel(num_classes=6) state = torch.load(path, map_location="cpu") clean = {k.replace("_orig_mod.", ""): v for k, v in state.items()} m.load_state_dict(clean) m.eval() MODELS["lung"] = m logger.info("✅ Lung model loaded") except Exception as e: logger.warning(f"⚠️ Lung model failed: {e}") # 7 ─ Kidney try: path = _download_weight("kidney_model1.h5") try: MODELS["kidney"] = tf.keras.models.load_model(path, compile=False) except TypeError: kidney_model = _build_kidney_cnn() kidney_model.load_weights(path) MODELS["kidney"] = kidney_model logger.info("✅ Kidney model loaded") except Exception as e: logger.warning(f"⚠️ Kidney model failed: {e}") # ────────────────────────────────────────────────────────────────────────────── # U-Net builder (Heart) # ────────────────────────────────────────────────────────────────────────────── def _build_unet(input_shape=(128, 128, 1)): import tensorflow as tf L = tf.keras.layers inputs = L.Input(shape=input_shape) c1 = L.Conv2D(16, 3, activation="relu", padding="same")(inputs) c1 = L.Conv2D(16, 3, activation="relu", padding="same")(c1) p1 = L.MaxPooling2D()(c1) c2 = L.Conv2D(32, 3, activation="relu", padding="same")(p1) c2 = L.Conv2D(32, 3, activation="relu", padding="same")(c2) p2 = L.MaxPooling2D()(c2) c3 = L.Conv2D(64, 3, activation="relu", padding="same")(p2) c3 = L.Conv2D(64, 3, activation="relu", padding="same")(c3) p3 = L.MaxPooling2D()(c3) c4 = L.Conv2D(128, 3, activation="relu", padding="same")(p3) c4 = L.Conv2D(128, 3, activation="relu", padding="same")(c4) p4 = L.MaxPooling2D()(c4) c5 = L.Conv2D(256, 3, activation="relu", padding="same")(p4) c5 = L.Conv2D(256, 3, activation="relu", padding="same")(c5) u6 = L.Conv2DTranspose(128, 2, strides=2, padding="same")(c5) u6 = L.concatenate([u6, c4]) c6 = L.Conv2D(128, 3, activation="relu", padding="same")(u6) c6 = L.Conv2D(128, 3, activation="relu", padding="same")(c6) u7 = L.Conv2DTranspose(64, 2, strides=2, padding="same")(c6) u7 = L.concatenate([u7, c3]) c7 = L.Conv2D(64, 3, activation="relu", padding="same")(u7) c7 = L.Conv2D(64, 3, activation="relu", padding="same")(c7) u8 = L.Conv2DTranspose(32, 2, strides=2, padding="same")(c7) u8 = L.concatenate([u8, c2]) c8 = L.Conv2D(32, 3, activation="relu", padding="same")(u8) c8 = L.Conv2D(32, 3, activation="relu", padding="same")(c8) u9 = L.Conv2DTranspose(16, 2, strides=2, padding="same")(c8) u9 = L.concatenate([u9, c1]) c9 = L.Conv2D(16, 3, activation="relu", padding="same")(u9) c9 = L.Conv2D(16, 3, activation="relu", padding="same")(c9) outputs = L.Conv2D(1, 1, activation="sigmoid")(c9) return tf.keras.models.Model(inputs, outputs) # ────────────────────────────────────────────────────────────────────────────── # App lifecycle # ────────────────────────────────────────────────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): load_all_models() yield MODELS.clear() app = FastAPI( title="Medical AI API", description="7 medical AI models in one unified API", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ────────────────────────────────────────────────────────────────────────────── # Utility # ────────────────────────────────────────────────────────────────────────────── async def read_image(file: UploadFile) -> Image.Image: data = await file.read() return Image.open(io.BytesIO(data)) def _require(key: str): if key not in MODELS: raise HTTPException(503, f"Model '{key}' is not loaded. Check weight file.") # ────────────────────────────────────────────────────────────────────────────── # Root # ────────────────────────────────────────────────────────────────────────────── # ══════════════════════════════════════════════════════════════════════════════ # RECOMMENDATIONS — Arabic medical info for every disease # ══════════════════════════════════════════════════════════════════════════════ RECOMMENDATIONS = { # ── Skin ────────────────────────────────────────────────────────────────── "AK": { "disease_name": "AK — Actinic Keratosis", "description": "آفة جلدية تظهر نتيجة التعرض المفرط للشمس وتُعدّ مرحلة ما قبل سرطانية، قابلة للتحول لسرطان إذا تُركت دون علاج.", "recommendations": [ "استشر طبيب جلدية لتقييم المنطقة وتحديد العلاج المناسب.", "من خيارات العلاج: التجميد بالنيتروجين أو كريمات طبية موضعية.", "الحد من التعرض للشمس المباشرة أمر بالغ الأهمية.", "استخدم واقي الشمس يومياً وأعد وضعه كل ساعتين عند الخروج.", "متابعة دورية مع الطبيب كل 6-12 شهراً.", ], }, "BCC": { "disease_name": "BCC — Basal Cell Carcinoma", "description": "أكثر أنواع سرطان الجلد شيوعاً، ينمو ببطء ونادراً ما ينتشر، لكنه يحتاج علاجاً طبياً سريعاً لمنع تلف الأنسجة المحيطة.", "recommendations": [ "راجع طبيب جلدية في أقرب وقت لتقييم المنطقة.", "الطبيب على الأرجح سيقترح إزالة جراحية للمنطقة المصابة.", "ابتعد عن التعرض المباشر للشمس خصوصاً بين 10 صباحاً و4 عصراً.", "ضع واقي شمس يومياً حتى في الأيام الغائمة.", "راقب أي تغير في الحجم أو اللون أو النزيف.", ], }, "BKL": { "disease_name": "BKL — Benign Keratosis-like Lesion", "description": "نتوءات جلدية حميدة شائعة خاصةً مع التقدم في السن. ليست خطرة ولا تنتشر، لكنها قد تكون مزعجة من الناحية التجميلية.", "recommendations": [ "الحالة حميدة في الغالب ولا تستدعي قلقاً فورياً.", "راقب المنطقة — إذا تغير الشكل أو اللون أو نزّت دماً راجع الطبيب.", "يمكن إزالتها جمالياً عند طبيب جلدية إذا أزعجتك.", "اعتنِ بنظافة المنطقة وتجنب حكّها.", "زيارة روتينية للطبيب سنوياً للاطمئنان.", ], }, "DF": { "disease_name": "DF — Dermatofibroma", "description": "ورم جلدي حميد صلب يظهر عادةً في الساقين، ويُعتقد أنه يتكون بعد إصابة بسيطة كلسعة حشرة. نادراً ما يسبب مشاكل.", "recommendations": [ "الورم الليفي حميد تماماً ولا يحتاج علاجاً في معظم الحالات.", "تجنب الضغط عليه أو العبث به.", "إذا كبر بسرعة أو سبّب ألماً اعرضه على الطبيب.", "يمكن إزالته جراحياً إذا أزعجك مظهره.", "مراجعة الطبيب سنوياً للاطمئنان.", ], }, "MEL": { "disease_name": "MEL — Melanoma", "description": "نوع خطير من سرطان الجلد ينشأ من الخلايا المنتجة للميلانين. يُعدّ من أشد أنواع سرطان الجلد خطورةً إذا لم يُكتشف مبكراً.", "recommendations": [ "اذهب لطبيب جلدية أو أورام فوراً — لا تتأخر.", "لا تتعرض لأشعة الشمس المباشرة على المنطقة المصابة.", "التقط صوراً دورية للمنطقة لمتابعة أي تغير في الشكل أو الحجم.", "استخدم واقي الشمس بعامل حماية SPF 50+ يومياً.", "قد يحتاج الطبيب لأخذ خزعة (biopsy) للتأكد من التشخيص.", ], }, "NV": { "disease_name": "NV — Melanocytic Nevi", "description": "الشامات الشائعة الحميدة الناتجة عن تجمّع الخلايا المنتجة للميلانين. معظمها غير ضار لكن يجب مراقبتها باستمرار.", "recommendations": [ "الشامة الطبيعية غير ضارة — لكن المراقبة المستمرة مهمة.", "اتبع قاعدة ABCDE: تغير في التماثل والحدود واللون والحجم والشكل العام.", "التقط صورة شهرية لمتابعة أي تغيير.", "واقي الشمس يومياً يقلل خطر تحوّلها.", "إذا لاحظت أي تغيير مفاجئ راجع الطبيب فوراً.", ], }, "SCC": { "disease_name": "SCC — Squamous Cell Carcinoma", "description": "نوع من سرطان الجلد ينشأ من الطبقات الخارجية للجلد، وقد ينتشر للعقد الليمفاوية إذا أُهمل.", "recommendations": [ "زيارة طبيب جلدية عاجلة ضرورية جداً.", "الطبيب سيحدد ما إذا كانت الإزالة الجراحية أو العلاج الإشعاعي مناسباً.", "تجنّب تماماً أجهزة تسمير الجلد (سولاريوم).", "واقي شمس SPF 50+ يومياً إلزامي.", "أخبر الطبيب بأي تاريخ مرضي سابق لأمراض الجلد.", ], }, "VASC": { "disease_name": "VASC — Vascular Lesion", "description": "آفات تنشأ من الأوعية الدموية تحت الجلد كالأورام الوعائية والشعيرات المتوسعة. معظمها حميد تماماً.", "recommendations": [ "معظم الآفات الوعائية حميدة ولا تحتاج تدخلاً طبياً عاجلاً.", "راقب المنطقة — أي نزيف أو ألم يستدعي زيارة الطبيب.", "يمكن علاجها بالليزر لأغراض تجميلية.", "حافظ على نظافة المنطقة وتجنب احتكاكها بالملابس.", "زيارة طبيب جلدية للتأكد من التشخيص.", ], }, # ── Breast ──────────────────────────────────────────────────────────────── "benign": { "disease_name": "Benign — ورم حميد", "description": "الورم الحميد لا ينتشر ولا يشكّل خطراً مباشراً، لكن يجب متابعته طبياً بانتظام للتأكد من عدم تغيّره.", "recommendations": [ "راجع طبيب مختص لتأكيد التشخيص بالخزعة إذا لزم.", "متابعة دورية بالموجات فوق الصوتية كل 6 أشهر.", "تجنّبي الضغط على المنطقة أو العبث بها.", "أبلغي الطبيب فوراً إذا تغيّر الحجم أو ظهر ألم.", "الفحص الذاتي الشهري للثدي مهم جداً للمتابعة.", ], }, "malignant": { "disease_name": "Malignant — ورم خبيث", "description": "الورم الخبيث (السرطاني) يحتاج تدخلاً طبياً عاجلاً. الاكتشاف المبكر يرفع نسبة الشفاء بشكل كبير.", "recommendations": [ "توجّهي لطبيب أورام فوراً — لا تتأخري أبداً.", "ستحتاجين لفحوصات إضافية: خزعة، تصوير بالرنين، سكان عظام.", "خيارات العلاج تشمل: جراحة، كيماوي، إشعاع، علاج هرموني.", "احرصي على دعم نفسي وعائلي خلال فترة العلاج.", "لا تعتمدي على أي علاج شعبي بديل دون استشارة الطبيب.", ], }, "normal": { "disease_name": "Normal — طبيعي", "description": "لم تظهر علامات مرضية في الصورة. الثدي يبدو بصحة جيدة.", "recommendations": [ "استمري في الفحص الذاتي الشهري للثدي.", "احرصي على فحص دوري سنوي عند الطبيب بعد سن الـ 40.", "الماموغرام مهم كل عامين للنساء فوق 40 سنة.", "نظام غذائي صحي وممارسة الرياضة تقلل خطر سرطان الثدي.", "أبلغي الطبيبة فوراً عند ملاحظة أي تغيير غير طبيعي.", ], }, # ── Eye ─────────────────────────────────────────────────────────────────── "Cataract": { "disease_name": "Cataract — إعتام عدسة العين", "description": "تعتّم في عدسة العين الطبيعية يؤدي إلى ضبابية في الرؤية. شائع مع التقدم في السن وقابل للعلاج الجراحي.", "recommendations": [ "راجع طبيب عيون لتقييم مدى تأثيره على رؤيتك.", "العملية الجراحية بسيطة وآمنة وتعيد الرؤية بشكل كامل في معظم الحالات.", "استخدم نظارات شمسية لحماية العينين من الأشعة فوق البنفسجية.", "تجنب قيادة السيارة ليلاً إذا كانت رؤيتك ضعيفة.", "نظام غذائي غني بالفيتامينات C وE يحمي العينين.", ], }, "Diabetic Retinopathy": { "disease_name": "Diabetic Retinopathy — اعتلال الشبكية السكري", "description": "مضاعفة لمرض السكري تصيب الأوعية الدموية في شبكية العين وقد تؤدي لفقدان البصر إذا لم تُعالج.", "recommendations": [ "تحكّم في مستوى السكر في الدم بشكل صارم.", "راجع طبيب شبكية متخصص في أقرب وقت.", "قد تحتاج ليزر أو حقن مضادة للـ VEGF حسب الحالة.", "افحص عينيك سنوياً على الأقل إذا كنت مصاباً بالسكري.", "التحكم في ضغط الدم والكوليسترول يبطئ تطور المرض.", ], }, "Glaucoma": { "disease_name": "Glaucoma — المياه الزرقاء", "description": "مجموعة من أمراض العين تصيب العصب البصري غالباً بسبب ارتفاع ضغط العين. قد تؤدي لفقدان البصر التدريجي.", "recommendations": [ "راجع طبيب عيون فوراً لقياس ضغط العين.", "العلاج المبكر بالقطرات أو الجراحة يوقف تطور المرض.", "لا تتوقف عن استخدام قطرات الضغط دون استشارة الطبيب.", "افحص عينيك دورياً خاصة إذا كان هناك تاريخ عائلي للمرض.", "تجنب الأنشطة التي ترفع ضغط العين كرفع الأثقال الشديد.", ], }, "Normal": { "disease_name": "Normal — طبيعي", "description": "لم تظهر علامات لأمراض العين في الصورة. العين تبدو بصحة جيدة.", "recommendations": [ "احرص على فحص العينين مرة كل عام أو عامين.", "نظام غذائي غني بالفيتامينات A وC وE مفيد للعيون.", "استخدم نظارات شمسية ذات حماية UV عند الخروج.", "خذ استراحة كل 20 دقيقة عند استخدام الشاشات.", "تجنب التدخين فإنه يزيد خطر أمراض العيون.", ], }, # ── Brain ───────────────────────────────────────────────────────────────── "glioma": { "disease_name": "Glioma — ورم الغليوما", "description": "نوع من أورام المخ ينشأ من الخلايا الدبقية. يتراوح بين درجات منخفضة الخطورة وأخرى عالية الخطورة.", "recommendations": [ "توجّه لطبيب أعصاب أو جراح مخ فوراً.", "ستحتاج لتصوير بالرنين المغناطيسي المعزز لتحديد دقة الورم.", "خيارات العلاج تشمل: جراحة، إشعاع، كيماوي حسب درجة الورم.", "لا تتوقف عن أدوية الصرع إذا وُصفت لك.", "الدعم النفسي والعائلي جزء أساسي من رحلة العلاج.", ], }, "meningioma": { "disease_name": "Meningioma — ورم السحايا", "description": "ورم ينشأ من أغشية المخ (السحايا). معظمه حميد وبطيء النمو، لكن قد يسبب ضغطاً على المخ.", "recommendations": [ "راجع طبيب أعصاب لتقييم الورم ومتابعته.", "الأورام الصغيرة غير المسببة لأعراض قد تحتاج مراقبة فقط.", "الأورام الكبيرة أو المسببة لأعراض قد تحتاج جراحة أو إشعاع.", "متابعة بالرنين المغناطيسي كل 6-12 شهراً حسب توجيه الطبيب.", "أبلغ الطبيب فوراً عند ظهور صداع شديد أو تغيرات في الرؤية.", ], }, "notumor": { "disease_name": "No Tumor — لا يوجد ورم", "description": "لم تظهر علامات لوجود ورم في الصورة. الدماغ يبدو طبيعياً.", "recommendations": [ "إذا كانت لديك أعراض استشر طبيب أعصاب للتأكد.", "فحص دوري سنوي مهم خاصة عند وجود تاريخ عائلي.", "نوم كافٍ وتجنب الضغط النفسي الشديد يحمي صحة الدماغ.", "مارس الرياضة بانتظام لتحسين الدورة الدموية للمخ.", "تجنب التدخين والكحول للحفاظ على صحة الجهاز العصبي.", ], }, "pituitary": { "disease_name": "Pituitary — ورم الغدة النخامية", "description": "ورم ينشأ في الغدة النخامية بقاعدة الدماغ. معظمه حميد لكن قد يؤثر على الهرمونات والرؤية.", "recommendations": [ "راجع طبيب غدد صماء وطبيب أعصاب معاً.", "ستحتاج لتحليل هرمونات شاملة لتقييم تأثير الورم.", "بعض الأورام تُعالج بالدواء فقط دون جراحة.", "تابع رؤيتك — ورم الغدة قد يضغط على العصب البصري.", "الفحص الدوري بالرنين المغناطيسي ضروري لمتابعة حجم الورم.", ], }, # ── Heart ───────────────────────────────────────────────────────────────── "normal": { "disease_name": "Normal — حجم القلب طبيعي", "description": "منطقة القلب المكتشفة في الصورة تقع ضمن النطاق الطبيعي المتوقع.", "recommendations": [ "استمر في ممارسة الرياضة بانتظام للحفاظ على صحة القلب.", "نظام غذائي صحي قليل الدهون المشبعة مفيد للقلب.", "راقب ضغط الدم والكوليسترول بشكل دوري.", "تجنب التدخين وتقليل التوتر النفسي.", "فحص قلب سنوي مهم خاصة بعد سن الـ 40.", ], }, "slightly_large": { "disease_name": "Slightly Large — تضخم بسيط في القلب", "description": "تم اكتشاف منطقة قلب أكبر قليلاً من المعدل الطبيعي في الصورة. يستدعي مراجعة طبية.", "recommendations": [ "راجع طبيب قلب لإجراء تخطيط قلب وإيكو.", "تجنب المجهود الشديد حتى تأكيد التشخيص.", "راقب أي أعراض: ضيق تنفس، خفقان، تورم في القدمين.", "قلّل الملح في طعامك لتخفيف الضغط على القلب.", "الالتزام بالأدوية إذا وصفها الطبيب أمر ضروري.", ], }, "abnormally_large": { "disease_name": "Abnormally Large — تضخم غير طبيعي في القلب", "description": "تم اكتشاف منطقة قلب أكبر بكثير من المعدل الطبيعي. يستدعي تدخلاً طبياً عاجلاً.", "recommendations": [ "اذهب لطبيب قلب أو طوارئ فوراً.", "أجرِ تخطيط قلب (ECG) وإيكو قلب (Echocardiogram) عاجلاً.", "تجنب أي مجهود بدني حتى تحصل على تقييم طبي.", "أخبر الطبيب بكل الأدوية التي تتناولها.", "التضخم القلبي قابل للعلاج إذا اكتُشف مبكراً.", ], }, "not_detected": { "disease_name": "Not Detected — لم يتم اكتشاف القلب", "description": "لم يتمكن الموديل من تحديد منطقة القلب بوضوح في الصورة. قد يكون بسبب جودة الصورة.", "recommendations": [ "تأكد من جودة الصورة وأنها CT scan واضحة.", "أعد رفع صورة بجودة أعلى وزاوية مناسبة.", "راجع طبيب متخصص لإجراء الفحص اللازم.", ], }, # ── Lung ────────────────────────────────────────────────────────────────── "Tuberculosis": { "disease_name": "Tuberculosis — السل الرئوي", "description": "عدوى بكتيرية تصيب الرئتين وتنتشر عن طريق الهواء. من أعراضها السعال المزمن ونزول الدم والتعرق الليلي وفقدان الوزن.", "recommendations": [ "راجع طبيب أمراض صدر فوراً.", "يحتاج علاج مضادات حيوية لمدة 6 أشهر على الأقل.", "تجنب الاختلاط بالآخرين حتى تأكيد التشخيص.", "أجرِ تحليل بلغم وأشعة مقطعية للتأكد.", "الالتزام بجرعات الدواء كاملة بدون توقف أمر بالغ الأهمية.", ], }, "Pneumonia-Viral": { "disease_name": "Pneumonia-Viral — التهاب رئوي فيروسي", "description": "التهاب في أنسجة الرئة بسبب فيروس. يسبب حمى وسعالاً وضيق تنفس وإرهاداً شديداً.", "recommendations": [ "راجع طبيب في أقرب وقت.", "الراحة التامة وشرب السوائل بكثرة ضروريان.", "المضادات الحيوية لا تفيد (فيروسي) — الطبيب سيحدد العلاج المناسب.", "راقب مستوى الأكسجين في الدم.", "اذهب للطوارئ إذا نزل الأكسجين عن 94% أو اشتد ضيق التنفس.", ], }, "Pneumonia-Bacterial": { "disease_name": "Pneumonia-Bacterial — التهاب رئوي بكتيري", "description": "التهاب رئوي بسبب بكتيريا. أعراضه أشد من الفيروسي وتشمل حمى عالية وسعال بلغمي وألم في الصدر.", "recommendations": [ "اذهب للطبيب فوراً — يحتاج مضادات حيوية.", "لا تأخذ مضادات حيوية بدون وصفة طبية.", "الراحة التامة والسوائل الكافية ضرورية للتعافي.", "في الحالات الشديدة قد تحتاج دخول مستشفى.", "أكمل دورة المضادات الحيوية كاملة حتى لو تحسّنت.", ], }, "Normal": { "disease_name": "Normal — رئة طبيعية", "description": "الأشعة لا تظهر أي تغيرات مرضية. الرئتان تبدوان بصحة جيدة.", "recommendations": [ "استمر في الاهتمام بصحتك.", "ابتعد عن التدخين والأماكن الملوثة.", "مارس الرياضة بانتظام لتقوية الرئتين.", "إذا كانت عندك أعراض رغم النتيجة استشر طبيب.", "فحص سنوي مهم خاصة لمن يعملون في بيئات ملوثة.", ], }, "Emphysema": { "disease_name": "Emphysema — انتفاخ الرئة", "description": "تلف في الحويصلات الهوائية بالرئة يجعل التنفس صعباً. غالباً مرتبط بالتدخين لفترات طويلة.", "recommendations": [ "راجع طبيب أمراض صدر.", "التوقف عن التدخين فوراً هو أهم خطوة يمكنك اتخاذها.", "قد تحتاج موسّع للشعب الهوائية (بخاخ).", "تمارين التنفس مع متخصص تساعد كثيراً.", "تجنب الأماكن الملوثة والغبار والدخان.", ], }, "Covid-19": { "disease_name": "Covid-19 — كوفيد-19", "description": "عدوى فيروسية تسببها فيروس كورونا. تظهر في الأشعة كبقع زجاجية في الرئتين. أعراضها تتراوح من خفيفة لشديدة.", "recommendations": [ "أجرِ تحليل PCR لتأكيد الإصابة.", "عزل فوري لمنع انتشار العدوى.", "راجع طبيب لتقييم الحالة وتحديد العلاج.", "راقب مستوى الأكسجين — إذا نزل عن 94% اذهب للطوارئ.", "الراحة والسوائل والتغذية الجيدة تسرّع التعافي.", ], }, # ── Kidney ──────────────────────────────────────────────────────────────── "Cyst": { "disease_name": "Cyst — كيس في الكلى", "description": "تجمّع سائل في الكلى يشكّل كيساً. معظم الأكياس حميدة ولا تسبب أعراضاً، لكن تحتاج متابعة.", "recommendations": [ "راجع طبيب كلى لتقييم حجم الكيس وطبيعته.", "معظم الأكياس لا تحتاج علاجاً — متابعة بالأشعة كافية.", "اشرب كميات كافية من الماء يومياً (2-3 لتر).", "راقب أي أعراض: ألم في الجانب، دم في البول، ارتفاع ضغط.", "متابعة بالأشعة فوق الصوتية كل 6-12 شهراً.", ], }, "Normal": { "disease_name": "Normal — كلية طبيعية", "description": "لم تظهر علامات مرضية في صورة الكلى. الكلية تبدو بصحة جيدة.", "recommendations": [ "اشرب كميات كافية من الماء يومياً للحفاظ على صحة الكلى.", "قلّل الملح والبروتين الزائد في نظامك الغذائي.", "راقب ضغط الدم والسكر لأنهما يؤثران على الكلى.", "تجنب الإفراط في مسكنات الألم (مثل الإيبوبروفين).", "فحص وظائف الكلى سنوياً مهم خاصة بعد سن الـ 40.", ], }, "Stone": { "disease_name": "Stone — حصى في الكلى", "description": "تراكم معادن وأملاح داخل الكلى يشكّل حصوات. قد تسبب ألماً شديداً عند تحرّكها.", "recommendations": [ "راجع طبيب كلى أو مسالك بولية لتحديد حجم الحصى.", "اشرب 3 لتر ماء يومياً على الأقل لمساعدة الحصى على الخروج.", "تجنب الأطعمة الغنية بالأوكسالات كالسبانخ والشوكولاتة.", "الحصى الصغيرة تخرج وحدها — الكبيرة تحتاج تفتيت أو جراحة.", "اذهب للطوارئ إذا اشتد الألم أو ظهر دم في البول مع حمى.", ], }, "Tumor": { "disease_name": "Tumor — ورم في الكلى", "description": "تم اكتشاف كتلة غير طبيعية في الكلى. يحتاج تقييماً طبياً عاجلاً لتحديد طبيعة الورم.", "recommendations": [ "توجّه لطبيب أورام أو كلى فوراً — لا تتأخر.", "ستحتاج لأشعة مقطعية (CT) بصبغة لتحديد طبيعة الورم.", "خيارات العلاج تشمل: جراحة، استئصال جزئي أو كلي للكلى، أو علاج موضعي.", "الاكتشاف المبكر يرفع نسبة الشفاء بشكل كبير.", "لا تتجاهل أي أعراض: دم في البول، ألم في الجانب، فقدان وزن.", ], }, } @app.get("/") def root(): return { "message": "Medical AI API — 7 models", "loaded_models": list(MODELS.keys()), "endpoints": [ "/predict/skin", "/predict/breast", "/predict/eye", "/predict/brain", "/predict/heart", "/predict/lung", "/predict/kidney", ], } @app.get("/health") def health(): return {"status": "ok", "loaded_models": list(MODELS.keys())} # ══════════════════════════════════════════════════════════════════════════════ # 1 ─ SKIN # ══════════════════════════════════════════════════════════════════════════════ SKIN_LABELS = ["AK", "BCC", "BKL", "DF", "MEL", "NV", "SCC", "VASC"] SKIN_NAMES = { "AK": "Actinic Keratosis", "BCC": "Basal Cell Carcinoma", "BKL": "Benign Keratosis", "DF": "Dermatofibroma", "MEL": "Melanoma", "NV": "Melanocytic Nevi", "SCC": "Squamous Cell Carcinoma", "VASC": "Vascular Lesion", } SKIN_SEVERITY = { "MEL": "high", "BCC": "high", "SCC": "high", "AK": "medium", "BKL": "low", "DF": "low", "NV": "low", "VASC": "low", } @app.post("/predict/skin") async def predict_skin( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Classify skin lesion from a dermatoscopy image. Returns: predicted class, confidence, severity, all probabilities, AI medical report. Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("skin") img = await read_image(file) arr = np.array(img.convert("RGB").resize((28, 28)), dtype=np.float32) arr = np.expand_dims(arr, 0) probs = MODELS["skin"].predict(arr, verbose=0)[0] idx = int(np.argmax(probs)) label = SKIN_LABELS[idx] confidence = round(float(probs[idx]) * 100, 2) rec = RECOMMENDATIONS.get(label, {}) report = generate_medical_report( model_type="skin", predicted_class=label, disease_name=rec.get("disease_name", label), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "skin", "predicted_class": label, "predicted_name": SKIN_NAMES[label], "confidence": confidence, "severity": SKIN_SEVERITY[label], "all_probabilities": { SKIN_LABELS[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 2 ─ BREAST # ══════════════════════════════════════════════════════════════════════════════ BREAST_CLASSES = ["benign", "malignant", "normal"] @app.post("/predict/breast") async def predict_breast( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Breast ultrasound — runs segmentation + classification. Returns: - classification result (benign / malignant / normal) - segmentation mask stats (mean activation, coverage %) - AI medical report Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("breast_seg") _require("breast_cls") img = await read_image(file) # ── segmentation ── seg_in = np.array(img.convert("RGB").resize((256, 256)), dtype=np.float32) / 255.0 seg_in = np.expand_dims(seg_in, 0) mask = MODELS["breast_seg"].predict(seg_in, verbose=0)[0, :, :, 0] coverage = round(float(np.mean(mask > 0.5)) * 100, 2) # ── classification ── cls_in = np.array(img.convert("RGB").resize((224, 224)), dtype=np.float32) cls_in = np.expand_dims(cls_in, 0) probs = MODELS["breast_cls"].predict(cls_in, verbose=0)[0] idx = int(np.argmax(probs)) confidence = round(float(probs[idx]) * 100, 2) mask_b64 = _mask_to_base64(mask, colormap="plasma") overlay_b64 = _overlay_to_base64(img, mask, size=(256, 256)) rec = RECOMMENDATIONS.get(BREAST_CLASSES[idx], {}) report = generate_medical_report( model_type="breast", predicted_class=BREAST_CLASSES[idx], disease_name=rec.get("disease_name", BREAST_CLASSES[idx]), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "breast", "predicted_class": BREAST_CLASSES[idx], "confidence": confidence, "all_probabilities": { BREAST_CLASSES[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "segmentation": { "mask_mean_activation": round(float(np.mean(mask)), 4), "lesion_coverage_percent": coverage, "mask_image_base64": mask_b64, "overlay_image_base64": overlay_b64, }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 3 ─ EYE # ══════════════════════════════════════════════════════════════════════════════ EYE_CLASSES = ["Cataract", "Diabetic Retinopathy", "Glaucoma", "Normal"] @app.post("/predict/eye") async def predict_eye( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Classify eye disease from a fundus / eye image. Returns: predicted class, confidence, all probabilities, AI medical report. Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("eye") from tensorflow.keras.applications.efficientnet import preprocess_input img = await read_image(file) arr = np.array(img.convert("RGB").resize((224, 224)), dtype=np.float32) arr = preprocess_input(arr) arr = np.expand_dims(arr, 0) probs = MODELS["eye"].predict(arr, verbose=0)[0] idx = int(np.argmax(probs)) confidence = round(float(probs[idx]) * 100, 2) rec = RECOMMENDATIONS.get(EYE_CLASSES[idx], {}) report = generate_medical_report( model_type="eye", predicted_class=EYE_CLASSES[idx], disease_name=rec.get("disease_name", EYE_CLASSES[idx]), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "eye", "predicted_class": EYE_CLASSES[idx], "confidence": confidence, "all_probabilities": { EYE_CLASSES[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 4 ─ BRAIN # ══════════════════════════════════════════════════════════════════════════════ BRAIN_CLASSES = ["glioma", "meningioma", "notumor", "pituitary"] @app.post("/predict/brain") async def predict_brain( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Classify brain tumor type from an MRI image. Returns: predicted class, confidence, all probabilities, AI medical report. Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("brain") import cv2 img_pil = await read_image(file) img_np = np.array(img_pil.convert("RGB")) img_np = cv2.resize(img_np, (224, 224)) arr = img_np.astype(np.float32) / 255.0 arr = np.expand_dims(arr, 0) probs = MODELS["brain"].predict(arr, verbose=0)[0] idx = int(np.argmax(probs)) confidence = round(float(probs[idx]) * 100, 2) rec = RECOMMENDATIONS.get(BRAIN_CLASSES[idx], {}) report = generate_medical_report( model_type="brain", predicted_class=BRAIN_CLASSES[idx], disease_name=rec.get("disease_name", BRAIN_CLASSES[idx]), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "brain", "predicted_class": BRAIN_CLASSES[idx], "confidence": confidence, "all_probabilities": { BRAIN_CLASSES[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 5 ─ HEART # ══════════════════════════════════════════════════════════════════════════════ @app.post("/predict/heart") async def predict_heart( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Heart segmentation from a CT scan image. Returns: - heart_area_ratio (%) - assessment (normal / slightly_large / abnormally_large / not_detected) - mask stats - AI medical report Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("heart") img = await read_image(file) arr = np.array(img.convert("L").resize((128, 128)), dtype=np.float32) / 255.0 arr = np.expand_dims(arr, (0, 3)) mask = MODELS["heart"].predict(arr, verbose=0)[0, :, :, 0] mask_bin = (mask > 0.5).astype(np.uint8) ratio = float(np.sum(mask_bin)) / (128 * 128) if ratio < 0.01: assessment = "not_detected" elif ratio < 0.15: assessment = "normal" elif ratio < 0.35: assessment = "slightly_large" else: assessment = "abnormally_large" confidence = round(float(np.max(mask)) * 100, 2) mask_b64 = _mask_to_base64(mask, colormap="hot") overlay_b64 = _overlay_to_base64(img, mask, size=(128, 128)) rec = RECOMMENDATIONS.get(assessment, {}) report = generate_medical_report( model_type="heart", predicted_class=assessment, disease_name=rec.get("disease_name", assessment), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "heart", "task": "segmentation", "heart_area_ratio_percent": round(ratio * 100, 2), "assessment": assessment, "mask_stats": { "mean_activation": round(float(np.mean(mask)), 4), "max_activation": round(float(np.max(mask)), 4), "detected_pixels": int(np.sum(mask_bin)), }, "segmentation": { "mask_image_base64": mask_b64, "overlay_image_base64": overlay_b64, }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 6 ─ LUNG (PyTorch) # ══════════════════════════════════════════════════════════════════════════════ LUNG_CLASSES = [ "Tuberculosis", "Pneumonia-Viral", "Pneumonia-Bacterial", "Normal", "Emphysema", "Covid-19", ] @app.post("/predict/lung") async def predict_lung( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Classify chest X-ray disease (6 classes). Returns: predicted class, confidence, all probabilities, AI medical report. Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("lung") import torch import albumentations as A from albumentations.pytorch import ToTensorV2 transform = A.Compose([ A.Resize(224, 224), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2(), ]) img = await read_image(file) arr = np.array(img.convert("RGB")) t = transform(image=arr)["image"].unsqueeze(0) with torch.no_grad(): probs = torch.softmax(MODELS["lung"](t), dim=1).squeeze().numpy() idx = int(np.argmax(probs)) confidence = round(float(probs[idx]) * 100, 2) rec = RECOMMENDATIONS.get(LUNG_CLASSES[idx], {}) report = generate_medical_report( model_type="lung", predicted_class=LUNG_CLASSES[idx], disease_name=rec.get("disease_name", LUNG_CLASSES[idx]), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "lung", "predicted_class": LUNG_CLASSES[idx], "confidence": confidence, "all_probabilities": { LUNG_CLASSES[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, } # ══════════════════════════════════════════════════════════════════════════════ # 7 ─ KIDNEY # ══════════════════════════════════════════════════════════════════════════════ KIDNEY_CLASSES = ["Cyst", "Normal", "Stone", "Tumor"] @app.post("/predict/kidney") async def predict_kidney( file: UploadFile = File(...), age: Optional[int] = Form(None), gender: Optional[str] = Form(None), symptoms: Optional[str] = Form(None), chronic_diseases: Optional[str] = Form(None), medications: Optional[str] = Form(None), ): """ Classify kidney disease from a CT scan image. Returns: predicted class, confidence, all probabilities, AI medical report. Optional form fields: age, gender, symptoms, chronic_diseases, medications """ _require("kidney") img = await read_image(file) arr = np.array(img.convert("L").resize((200, 200)), dtype=np.float32) / 255.0 arr = arr.reshape(1, 200, 200, 1) probs = MODELS["kidney"].predict(arr, verbose=0)[0] idx = int(np.argmax(probs)) confidence = round(float(probs[idx]) * 100, 2) rec = RECOMMENDATIONS.get(KIDNEY_CLASSES[idx], {}) report = generate_medical_report( model_type="kidney", predicted_class=KIDNEY_CLASSES[idx], disease_name=rec.get("disease_name", KIDNEY_CLASSES[idx]), confidence=confidence, age=age, gender=gender, symptoms=symptoms, chronic_diseases=chronic_diseases, medications=medications, ) return { "model": "kidney", "predicted_class": KIDNEY_CLASSES[idx], "confidence": confidence, "all_probabilities": { KIDNEY_CLASSES[i]: round(float(p) * 100, 2) for i, p in enumerate(probs) }, "disease_info": { "disease_name": rec.get("disease_name", ""), "description": rec.get("description", ""), "recommendations": rec.get("recommendations", []), }, "medical_report": report, }