""" RetinaFlow — Dual-Model Retinal Disease Diagnostic System Author: Mohamed Mostafa Elbasyouni (markegyptian55-cloud) Repository: https://github.com/markegyptian55-cloud/RetinaFlow Space: https://huggingface.co/spaces/egyx/RetinaFlow License: MIT """ import os import sys import time import hashlib from typing import Dict, Tuple, Optional, Any import numpy as np from PIL import Image try: import spaces except ImportError: class _MockSpaces: @staticmethod def GPU(fn=None, **kwargs): if fn is not None and callable(fn): return fn return lambda f: f spaces = _MockSpaces() import onnxruntime as ort import gradio as gr # ============================================================================ # CONFIGURATION & METADATA # ============================================================================ HF_REPO_ID = "egyx/RetinaFlow" CLASS_NAMES = [ 'AMD', 'Cataract', 'Diabetic Retinopathy', 'Glaucoma', 'Hypertension', 'Myopia', 'Normal', 'Others' ] # Clinical data dictionary with full English & Arabic definitions CLINICAL_DATA = { 'Normal': { 'en': { 'name': 'Normal Retinal Fundus', 'badge': '🟢 ROUTINE / LOW RISK', 'desc': 'Optic nerve head, macula lutea, and retinal microvasculature exhibit physiological anatomical features with no visible signs of retinopathy or optic neuropathy.' }, 'ar': { 'name': 'شبكية سليمة وطبيعية', 'badge': '🟢 روتيني / طبيعي (منخفض الخطورة)', 'desc': 'المظهر العام لقاع العين سليم. القرص البصري، البقعة الصفراء، والتروية الشبكية ضمن المعايير التشريحية السليمة تماماً.' }, 'color': '#10b981', 'icon': '✅', }, 'AMD': { 'en': { 'name': 'Age-Related Macular Degeneration (AMD)', 'badge': '🟡 SPECIALIST REVIEW (MODERATE RISK)', 'desc': 'Degeneration of the central macular region impacting fine visual acuity. Prompt vitreoretinal specialist referral recommended to evaluate dry vs. wet progression.' }, 'ar': { 'name': 'ضمور البقعة الصفراء المرتبط بالعمر (AMD)', 'badge': '🟡 متابعة تخصصية (متوسط الخطورة)', 'desc': 'تنكس في منطقة اللطاخة الصفراء المسؤولة عن حدة الرؤية المركزية. يستدعي فحصاً دورياً مع أخصائي الشبكية لتفادي تطور الحالة.' }, 'color': '#f59e0b', 'icon': '👁️', }, 'Cataract': { 'en': { 'name': 'Cataract', 'badge': '🟡 OPHTHALMIC EVALUATION (MODERATE RISK)', 'desc': 'Progressive opacification of the ocular crystalline lens leading to visual degradation. Evaluated for surgical phacoemulsification and intraocular lens implantation.' }, 'ar': { 'name': 'المياه البيضاء (عتامة العدسة)', 'badge': '🟡 تقييم بصري وجراحي (متوسط الخطورة)', 'desc': 'عتامة في عدسة العين الطبيعية تؤدي لضبابية الرؤية وصعوبة فحص القاع بوضوح. تُعالج بنجاح عبر استبدال العدسة جراحياً.' }, 'color': '#f59e0b', 'icon': '🔍', }, 'Diabetic Retinopathy': { 'en': { 'name': 'Diabetic Retinopathy', 'badge': '🔴 URGENT CLINICAL REVIEW (HIGH RISK)', 'desc': 'Retinal microvascular pathology secondary to diabetes mellitus, with signs of microaneurysms, hemorrhages, hard exudates, or neovascularization. Dilated ophthalmic exam indicated.' }, 'ar': { 'name': 'اعتلال الشبكية السكري (DR)', 'badge': '🔴 تقييم سريري عاجل (عالي الخطورة)', 'desc': 'أضرار وعائية دقيقة ناجمة عن السكري (نزيف شبكي، ارتشاحات دهنية، أو نمو أوعية دموية جديدة). يتطلب فحصاً وتدخلاً عاجلاً.' }, 'color': '#ef4444', 'icon': '🩸', }, 'Glaucoma': { 'en': { 'name': 'Glaucoma (Optic Neuropathy)', 'badge': '🔴 URGENT CLINICAL REVIEW (HIGH RISK)', 'desc': 'Progressive optic neuropathy with neuroretinal rim thinning and increased optic disc cupping. Urgent intraocular pressure (IOP) tonometry and visual field testing required.' }, 'ar': { 'name': 'المياه الزرقاء (اعتلال العصب البصري)', 'badge': '🔴 تقييم سريري عاجل (عالي الخطورة)', 'desc': 'اعتلال تدريجي في ألياف العصب البصري وزيادة تقعر القرص البصري. يتطلب قياس ضغط العين وفحص المجال البصري لتفادي فقدان الرؤية.' }, 'color': '#ef4444', 'icon': '⚠️', }, 'Hypertension': { 'en': { 'name': 'Hypertensive Retinopathy', 'badge': '🚨 RARE CRITICAL FINDING (URGENT)', 'desc': 'Retinal vascular alterations caused by systemic arterial hypertension (arteriolar narrowing, AV nicking, or flame hemorrhages). Immediate cardiovascular and BP triage required.' }, 'ar': { 'name': 'اعتلال الشبكية الناتج عن ضغط الدم (HT)', 'badge': '🚨 حالة حرجة ونادرة (أولوية عاجلة)', 'desc': 'تغيرات وعائية شريانية ناتجة عن ارتفاع ضغط الدم (تصلب شرياني، نزف لهبي). يستدعي ضبطاً فورياً لضغط الدم ومتابعة قلبية وبصرية.' }, 'color': '#ff7849', 'icon': '❤️‍🩹', }, 'Myopia': { 'en': { 'name': 'Pathological Myopia', 'badge': '🟡 PERIODIC MONITORING (MODERATE RISK)', 'desc': 'High axial myopia associated with posterior staphyloma and chorioretinal thinning. Periodic peripheral fundus examination advised to monitor retinal detachment risk.' }, 'ar': { 'name': 'قصر النظر المرضي / الشديد', 'badge': '🟡 فحص شبكية دوري (متوسط الخطورة)', 'desc': 'استطالة محورية في مقلة العين مع ترقق الغشاء الشبكي وتغيرات في المشيمية. يستلزم مراقبة دورية لتجنب مخاطر تمزق أو انفصال الشبكية.' }, 'color': '#38bdf8', 'icon': '👓', }, 'Others': { 'en': { 'name': 'Other Retinal Lesions', 'badge': '🟡 CLINICAL CORRELATION (MODERATE RISK)', 'desc': 'Features suggestive of secondary or alternative fundus anomalies (e.g., epiretinal membrane, retinal vein occlusion, drusen). Direct ophthalmological examination recommended.' }, 'ar': { 'name': 'آفات / اعتلالات شبكية أخرى', 'badge': '🟡 فحص سريري إضافي (مراجعة مطلوبة)', 'desc': 'تم رصد شذوذ أو علامات لآفات شبكية أخرى (مثل الأغشية التليفية، أو انسداد وريدي، أو دروزن). يُوصى بالمطابقة السريرية المباشرة.' }, 'color': '#a855f7', 'icon': '🔬', } } MODEL_CONFIGS = { "General-Purpose (ResNet-50)": { "rel_path": "models/resnet50_m5a.onnx", "input_size": 224, "name": "ResNet-50 (M5a)", "badge": "🏆 Highest Overall Accuracy", "badge_ar": "🏆 أعلى دقة عامة", "blurb_en": "Optimized for overall classification across all 8 classes (71.7% Accuracy, Macro-F1 0.60).", "blurb_ar": "الأفضل للدقة الإجمالية الشاملة عبر كافة الفئات الـ 8 (دقة 71.7%، Macro-F1 0.60).", }, "High-Sensitivity (EfficientNet-B5)": { "rel_path": "models/efficientnet_b5_m6.onnx", "input_size": 300, "name": "EfficientNet-B5 (M6)", "badge": "🎯 High Hypertension Recall", "badge_ar": "🎯 حساسية فائقة للضغط", "blurb_en": "Specialized to screen rare, high-risk conditions — 38.5% Hypertension recall (vs 15.4% for general model).", "blurb_ar": "مخصص لالتقاط الحالات الحرجة النادرة مثل اعتلال ضغط الدم (38.5% Recall مقابل 15.4%).", }, } UI_TEXTS = { "en": { "lang_btn": "🌐 العربية", "top_status": "● Models ready · ONNX CPU", "title": "RetinaFlow", "subtitle": "Dual-Model Deep Learning Diagnostic System for Retinal Fundus Photography", "acc_val": "71.7%", "acc_lbl": "TEST ACCURACY", "ht_val": "38.5%", "ht_lbl": "HT RECALL (2.5×)", "accordion_title": "ℹ️ Model Architecture & Clinical Trade-Off Guide", "tradeoff_md": """### ⚖️ Why RetinaFlow Uses a Dual-Champion Architecture Our clinical dataset aggregates **11,839 fundus images** with extreme class skew: * **Hypertension** is ultra-rare (only 88 total images, a 53.4× imbalance vs Normal). * **ResNet-50 (M5a):** General-purpose champion with highest overall accuracy (**71.7%**) and balanced Macro-F1 (0.60), but 15.4% recall on Hypertension. * **EfficientNet-B5 (M6):** High-sensitivity champion achieving **38.5% recall on Hypertension** (2.5× higher), catching critical rare cases at the expense of lower overall accuracy (62.6%). * **Dual Consensus Mode:** Evaluates both engines concurrently and alerts the practitioner if a rare condition is detected. *Disclaimer: For research and educational decision-support only. Not an FDA/CE-certified diagnostic device.*""", "card1_num": "01", "card1_title": "Fundus image", "upload_label": "Upload Retinal Fundus Photograph", "mode_label": "Diagnostic Engine Mode", "mode_choices": [ "⚡ Dual Consensus (Both Models)", "General-Purpose (ResNet-50)", "High-Sensitivity (EfficientNet-B5)", ], "mode_blurb": "**Dual Consensus Mode:** Runs both ResNet-50 and EfficientNet-B5 in parallel. ResNet-50 provides overall diagnostic precision, while EfficientNet-B5 screens for rare, high-risk pathologies.", "btn_analyze": "⛶ Analyze image", "btn_reset": "🔄 Reset", "privacy_note": "🔒 Runs locally on device via ONNX. The image never leaves your machine.", "examples_label": "Select a sample fundus image to test", "card2_num": "02", "card2_title": "Model assessment", "placeholder_html": """
👁️

Awaiting Fundus Image

Upload a fundus photo or pick one of the sample images, then click Analyze image.

""", "label_m1": "Primary Probabilities (ResNet-50)", "label_m2": "High-Sensitivity Probabilities (EfficientNet-B5)", "footer_text": "RetinaFlow Research Platform • Built by Mohamed Mostafa Elbasyouni • GitHub: markegyptian55-cloud/RetinaFlow • Research & Educational Demonstration Only." }, "ar": { "lang_btn": "🌐 English", "top_status": "● النماذج جاهزة · ONNX CPU", "title": "RetinaFlow", "subtitle": "نظام الذكاء الاصطناعي السريري لتشخيص أمراض شبكية العين عبر صور قاع العين", "acc_val": "71.7%", "acc_lbl": "دقة الاختبار الإجمالية", "ht_val": "38.5%", "ht_lbl": "حساسية التقاط الضغط (2.5×)", "accordion_title": "ℹ️ دليل بنية النماذج والمفاضلة السريرية (Model Trade-Off Guide)", "tradeoff_md": """### ⚖️ لماذا يدمج RetinaFlow نموذجين بطلين معاً؟ تم تدريب النماذج على **11,839 صورة سريرية حقيقية**، حيث تعاني البيانات من اختلال حاد في التوزيع: * فئة **ارتفاع ضغط الدم (Hypertension)** نادرة جداً (88 حالة فقط في كامل قاعدة البيانات). * **ResNet-50 (M5a):** بطل الدقة الإجمالية (**71.7%**)، ولكنه يحقق 15.4% فقط في التقاط فئة الضغط. * **EfficientNet-B5 (M6):** بطل الحساسية العالية بحساسية تفوق **38.5%** للضغط (2.5 ضعف). * **وضع المقارنة المزدوج:** يُشغل النموذجين بالتوازي ويرصد أي تباين تشخيصي للحالات النادرة. *إخلاء مسؤولية: للأغراض البحثية والتعليمية والدعم التشخيصي فقط. ليس جهازاً طبياً معتمداً.*""", "card1_num": "01", "card1_title": "صورة قاع العين", "upload_label": "رفع صورة قاع العين (Fundus Image)", "mode_label": "وضع المحرك التشخيصي", "mode_choices": [ "⚡ مقارنة النموذجين معاً (Dual Consensus)", "General-Purpose (ResNet-50)", "High-Sensitivity (EfficientNet-B5)", ], "mode_blurb": "**الوضع المزدوج التوافقي:** يشغل النموذجين معاً بالتوازي؛ حيث يقدم ResNet-50 الدقة العامة بينما يدقق EfficientNet-B5 في الحالات النادرة والحرجة.", "btn_analyze": "⛶ بدء فحص وتحليل الصورة", "btn_reset": "🔄 إعادة ضبط", "privacy_note": "🔒 يتم الاستدلال محلياً عبر محرك ONNX. الصور لا تغادر جهازك.", "examples_label": "اختر إحدى عينات الفحص الجاهزة للاختبار", "card2_num": "02", "card2_title": "التقييم التشخيصي للنموذج", "placeholder_html": """
👁️

في انتظار تحميل الصورة وبدء الفحص

قم برفع صورة لقاع العين أو اختر إحدى العينات الجاهزة ثم اضغط على بدء فحص وتحليل الصورة.

""", "label_m1": "احتمالات النموذج العام (ResNet-50)", "label_m2": "احتمالات نموذج الحساسية العالية (EfficientNet-B5)", "footer_text": "منصة RetinaFlow البحثية • تطوير: محمد مصطفى البسيوني • GitHub: markegyptian55-cloud/RetinaFlow • للأغراض البحثية والتعليمية فقط." } } IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32) IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32) # ============================================================================ # STANDALONE MODEL RESOLUTION & CACHING # ============================================================================ _loaded_sessions: Dict[str, ort.InferenceSession] = {} _result_cache: Dict[Tuple[str, str], Dict[str, Any]] = {} def resolve_model_file(rel_path: str) -> str: """ Checks if model exists locally. If not found (e.g., when app.py is uploaded alone to a new Hugging Face Space), downloads it on-demand from the official Space. """ if os.path.exists(rel_path): return rel_path print(f"[*] Local model '{rel_path}' not found. Downloading from Space '{HF_REPO_ID}'...") try: from huggingface_hub import hf_hub_download cached_file = hf_hub_download( repo_id=HF_REPO_ID, repo_type="space", filename=rel_path ) print(f"[✓] Retrieved: {cached_file}") return cached_file except Exception as e: raise FileNotFoundError( f"Could not load '{rel_path}' locally and auto-download from '{HF_REPO_ID}' failed: {e}" ) def get_session(model_key: str) -> ort.InferenceSession: if model_key not in _loaded_sessions: cfg = MODEL_CONFIGS[model_key] resolved_path = resolve_model_file(cfg["rel_path"]) _loaded_sessions[model_key] = ort.InferenceSession( resolved_path, providers=['CPUExecutionProvider'] ) return _loaded_sessions[model_key] def _hash_image(image: Image.Image) -> str: return hashlib.sha256(image.tobytes()).hexdigest() def preprocess(image: Image.Image, size: int) -> np.ndarray: image = image.convert('RGB').resize((size, size), Image.BILINEAR) arr = np.asarray(image, dtype=np.float32) / 255.0 arr = (arr - IMAGENET_MEAN) / IMAGENET_STD arr = arr.transpose(2, 0, 1) # HWC -> CHW return np.expand_dims(arr, axis=0).astype(np.float32) def softmax(x: np.ndarray) -> np.ndarray: e_x = np.exp(x - np.max(x)) return e_x / e_x.sum() def run_inference_raw(image: Image.Image, model_key: str) -> Tuple[Dict[str, float], float]: config = MODEL_CONFIGS[model_key] session = get_session(model_key) input_tensor = preprocess(image, config["input_size"]) input_name = session.get_inputs()[0].name t0 = time.perf_counter() logits = session.run(None, {input_name: input_tensor})[0][0] lat_ms = (time.perf_counter() - t0) * 1000 probs = softmax(logits) prob_dict = {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))} return prob_dict, lat_ms # ============================================================================ # MCP & ZERO-GPU ENDPOINT # ============================================================================ @spaces.GPU def classify(image: Image.Image, model_choice: str = "General-Purpose (ResNet-50)") -> Dict[str, float]: """ Public tool endpoint for Model Context Protocol (MCP) and programmatic APIs. Preserves exact backward-compatible signature. """ if image is None: raise gr.Error("Please upload a fundus image before classifying.") # Normalize model choice string if passed from Arabic or alias if "EfficientNet" in model_choice: model_choice = "High-Sensitivity (EfficientNet-B5)" else: model_choice = "General-Purpose (ResNet-50)" cache_key = (_hash_image(image), model_choice) if cache_key in _result_cache: return _result_cache[cache_key]["probs"] probs, lat_ms = run_inference_raw(image, model_choice) _result_cache[cache_key] = {"probs": probs, "lat": lat_ms} return probs # ============================================================================ # CLINICAL REPORT GENERATION (BILINGUAL) # ============================================================================ def build_clinical_card_html(top_class: str, top_prob: float, lat_ms: float, model_name: str, consensus_note: str = "", lang: str = "en") -> str: data = CLINICAL_DATA.get(top_class, CLINICAL_DATA['Others']) lang_data = data[lang] pct = top_prob * 100 lbl_heading = "PREDICTED SEVERITY & FINDING" if lang == "en" else "التشخيص السريري الأرجح" lbl_conf = "CONFIDENCE" if lang == "en" else "درجة الثقة" lbl_latency = "LATENCY" if lang == "en" else "سرعة المعالجة" lbl_engine = "ACTIVE ENGINE" if lang == "en" else "المحرك التشخيصي" lbl_clinical = "CLINICAL INSIGHT:" if lang == "en" else "الدلالة السريرية:" consensus_section = "" if consensus_note: consensus_section = f"""
{consensus_note}
""" html = f"""
{lbl_heading}

{data['icon']} {lang_data['name']}

{f'
{top_class}
' if lang == 'ar' else ''}
{lang_data['badge']}
{lbl_conf}
{pct:.1f}%
{lbl_latency}
{lat_ms:.0f} ms
{lbl_engine}
{model_name}
{lbl_clinical} {lang_data['desc']}
{consensus_section}
""" return html # ============================================================================ # INTERACTIVE UI HANDLER # ============================================================================ def predict_ui(image: Optional[Image.Image], mode: str, lang: str): if image is None: err_msg = "Please upload a fundus image before running analysis." if lang == "en" else "يرجى رفع صورة لقاع العين أولاً قبل بدء الفحص." raise gr.Error(err_msg) # Detect if Dual Mode is selected in either language is_dual = "Dual" in mode or "مقارنة" in mode if is_dual: probs_m1, lat_m1 = run_inference_raw(image, "General-Purpose (ResNet-50)") probs_m2, lat_m2 = run_inference_raw(image, "High-Sensitivity (EfficientNet-B5)") top1_m1 = max(probs_m1, key=probs_m1.get) top1_m2 = max(probs_m2, key=probs_m2.get) # Clinical consensus logic if top1_m1 == top1_m2: if lang == "en": note = f""" 🤝 Model Consensus: Both engines independently agreed on {top1_m1} ({probs_m1[top1_m1]*100:.1f}% ResNet-50 · {probs_m2[top1_m2]*100:.1f}% EfficientNet-B5). """ else: note = f""" 🤝 توافق تشخيصي تام: كلا النموذجين متفقان على أرجحية تشخيص {top1_m1} ({CLINICAL_DATA[top1_m1]['ar']['name']}) بثقة {probs_m1[top1_m1]*100:.1f}% (ResNet) و {probs_m2[top1_m2]*100:.1f}% (EfficientNet). """ primary_class = top1_m1 primary_prob = probs_m1[top1_m1] else: if lang == "en": note = f""" ⚖️ Model Divergence Detected: General model favors {top1_m1} ({probs_m1[top1_m1]*100:.1f}%), while High-Sensitivity model flags {top1_m2} ({probs_m2[top1_m2]*100:.1f}%). Notice: Useful for alerting clinicians to rare high-risk cases like Hypertension. """ else: note = f""" ⚖️ تباين في التشخيص (Divergence): النموذج العام يرجح {top1_m1} ({probs_m1[top1_m1]*100:.1f}%)، بينما نموذج الحساسية العالية يرصد {top1_m2} ({probs_m2[top1_m2]*100:.1f}%). تنبيه: يفيد هذا التباين في لفت نظر الطبيب للحالات النادرة كضغط الدم. """ if top1_m2 in ['Hypertension', 'Glaucoma', 'Diabetic Retinopathy'] and probs_m2[top1_m2] > 0.30: primary_class = top1_m2 primary_prob = probs_m2[top1_m2] else: primary_class = top1_m1 primary_prob = probs_m1[top1_m1] total_lat = lat_m1 + lat_m2 engine_label = "Dual Consensus (ResNet-50 + EfficientNet-B5)" if lang == "en" else "المحرك المزدوج التوافقي" card_html = build_clinical_card_html(primary_class, primary_prob, total_lat, engine_label, note, lang=lang) return card_html, probs_m1, probs_m2, gr.update(visible=True) else: model_key = "High-Sensitivity (EfficientNet-B5)" if "EfficientNet" in mode else "General-Purpose (ResNet-50)" probs, lat = run_inference_raw(image, model_key) top1 = max(probs, key=probs.get) cfg = MODEL_CONFIGS[model_key] card_html = build_clinical_card_html(top1, probs[top1], lat, cfg["name"], lang=lang) return card_html, probs, None, gr.update(visible=False) def update_model_blurb(mode: str, lang: str) -> str: texts = UI_TEXTS[lang] if "Dual" in mode or "مقارنة" in mode: return texts["mode_blurb"] if "EfficientNet" in mode: return MODEL_CONFIGS["High-Sensitivity (EfficientNet-B5)"]["blurb_en" if lang == "en" else "blurb_ar"] return MODEL_CONFIGS["General-Purpose (ResNet-50)"]["blurb_en" if lang == "en" else "blurb_ar"] def clear_all(lang: str): texts = UI_TEXTS[lang] default_mode = texts["mode_choices"][0] return ( None, default_mode, texts["mode_blurb"], texts["placeholder_html"], None, None, gr.update(visible=False) ) # Language Toggle Function def toggle_language(current_lang: str): new_lang = "ar" if current_lang == "en" else "en" t = UI_TEXTS[new_lang] # Header HTML header_html = f"""
👁️

{t['title']}

{t['top_status']}

{t['subtitle']}

{t['acc_val']}
{t['acc_lbl']}
{t['ht_val']}
{t['ht_lbl']}
""" card1_title = f"""{t['card1_num']} {t['card1_title']}""" card2_title = f"""{t['card2_num']} {t['card2_title']}""" return ( new_lang, gr.update(value=t['lang_btn']), header_html, gr.update(label=t['accordion_title']), t['tradeoff_md'], card1_title, gr.update(label=t['upload_label']), gr.update(label=t['mode_label'], choices=t['mode_choices'], value=t['mode_choices'][0]), t['mode_blurb'], gr.update(value=t['btn_analyze']), gr.update(value=t['btn_reset']), f"""
{t['privacy_note']}
""", card2_title, t['placeholder_html'], gr.update(label=t['label_m1']), gr.update(label=t['label_m2']), f"""
{t['footer_text']}
""" ) # Discover examples EXAMPLES_DIR = "assets/samples" def discover_samples(): if not os.path.isdir(EXAMPLES_DIR): return [] valid = ('.jpg', '.jpeg', '.png') try: return [[os.path.join(EXAMPLES_DIR, f)] for f in sorted(os.listdir(EXAMPLES_DIR)) if f.lower().endswith(valid)] except Exception: return [] SAMPLE_LIST = discover_samples() # ============================================================================ # RETINAGRADE ULTRA-CLEAN MATTE OBSIDIAN DESIGN SYSTEM # ============================================================================ CUSTOM_CSS = """ :root { --bg-base: #090a0d; --bg-surface: #131418; --bg-container: #0d0e12; --border-color: #1e2027; --border-hover: #2d303b; --text-primary: #ffffff; --text-secondary: #808593; --text-dim: #525666; --accent-orange: #ff7849; --accent-orange-hover: #ff895e; --accent-teal: #10b981; } body, .gradio-container { background-color: var(--bg-base) !important; color: var(--text-primary) !important; font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif !important; } /* Outer Card Containers */ .rf-card { background: var(--bg-surface) !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 16px !important; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45) !important; } /* Header Banner */ #rf-top-header { background: #0d0e12 !important; border: 1px solid var(--border-color) !important; border-radius: 14px !important; padding: 16px 22px !important; margin-bottom: 12px !important; } /* Status Pill */ .rf-status-pill { display: inline-flex; align-items: center; background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.25); color: #10b981; font-size: 0.72rem; font-weight: 600; padding: 3px 10px; border-radius: 9999px; letter-spacing: 0.3px; } /* Language Toggle Button */ #rf-lang-btn { background: #14151a !important; border: 1px solid #2a2d38 !important; color: #ff7849 !important; border-radius: 8px !important; font-size: 0.85rem !important; font-weight: 700 !important; padding: 6px 14px !important; transition: all 0.15s ease !important; } #rf-lang-btn:hover { background: #1e2027 !important; border-color: #ff7849 !important; } /* Analyze Button (RetinaGrade Coral-Orange) */ #rf-btn-analyze { background: var(--accent-orange) !important; color: #090a0d !important; font-weight: 700 !important; font-size: 0.98rem !important; border: none !important; border-radius: 9px !important; padding: 11px 20px !important; box-shadow: 0 4px 18px rgba(255, 120, 73, 0.32) !important; transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1) !important; } #rf-btn-analyze:hover { background: var(--accent-orange-hover) !important; transform: translateY(-1px) !important; box-shadow: 0 6px 24px rgba(255, 120, 73, 0.45) !important; } /* Reset Button */ #rf-btn-reset { background: #1a1b22 !important; color: var(--text-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: 9px !important; font-weight: 600 !important; transition: all 0.15s ease !important; } #rf-btn-reset:hover { background: #22242c !important; color: #ffffff !important; } /* Image Upload Container */ #rf-upload-box { background: var(--bg-container) !important; border: 1px solid var(--border-color) !important; border-radius: 10px !important; } /* Radio Selection */ .gr-radio .wrap { gap: 8px !important; } /* Scrollbars */ ::-webkit-scrollbar { width: 7px; height: 7px; } ::-webkit-scrollbar-track { background: var(--bg-base); } ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #323542; } """ THEME = gr.themes.Base( primary_hue=gr.themes.colors.orange, secondary_hue=gr.themes.colors.neutral, neutral_hue=gr.themes.colors.neutral, font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], ).set( body_background_fill="#090a0d", background_fill_primary="#131418", background_fill_secondary="#0d0e12", border_color_primary="#1e2027", block_background_fill="#131418", block_border_color="#1e2027", block_label_text_color="#808593", body_text_color="#ffffff", body_text_color_subdued="#808593", ) # ============================================================================ # APP LAYOUT (ENGLISH BY DEFAULT) # ============================================================================ t_init = UI_TEXTS["en"] with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="RetinaFlow — Retinal AI Classifier") as demo: # Global Language State lang_state = gr.State("en") # Top Control Bar (Language Toggle + Quick Links) with gr.Row(): with gr.Column(scale=9): pass with gr.Column(scale=3, min_width=160): lang_toggle_btn = gr.Button(t_init["lang_btn"], elem_id="rf-lang-btn", size="sm") # Top Header Banner with gr.Column(elem_id="rf-top-header"): header_banner_html = gr.HTML(f"""
👁️

{t_init['title']}

{t_init['top_status']}

{t_init['subtitle']}

{t_init['acc_val']}
{t_init['acc_lbl']}
{t_init['ht_val']}
{t_init['ht_lbl']}
""") # Trade-off Advisory Collapsible Card with gr.Accordion(t_init["accordion_title"], open=False) as guide_accordion: guide_content = gr.Markdown(t_init["tradeoff_md"]) # Main Two-Column Workflow (Matching RetinaGrade Mockup) with gr.Row(equal_height=False): # Left Panel: 01 Fundus image with gr.Column(scale=5): with gr.Column(elem_classes=["rf-card"]): card1_title = gr.HTML( f"""{t_init['card1_num']} {t_init['card1_title']}""" ) image_input = gr.Image( type="pil", label=t_init["upload_label"], elem_id="rf-upload-box" ) model_selector = gr.Radio( choices=t_init["mode_choices"], value=t_init["mode_choices"][0], label=t_init["mode_label"], elem_id="rf-model-radio" ) model_info = gr.Markdown( t_init["mode_blurb"], elem_id="rf-model-info" ) with gr.Row(): submit_btn = gr.Button(t_init["btn_analyze"], variant="primary", elem_id="rf-btn-analyze", scale=3) reset_btn = gr.Button(t_init["btn_reset"], elem_id="rf-btn-reset", scale=1) privacy_html = gr.HTML( f"""
{t_init['privacy_note']}
""" ) # Sample Gallery if SAMPLE_LIST: gr.Examples( examples=SAMPLE_LIST, inputs=image_input, label=t_init["examples_label"] ) # Right Panel: 02 Model assessment with gr.Column(scale=6): with gr.Column(elem_classes=["rf-card"]): card2_title = gr.HTML( f"""{t_init['card2_num']} {t_init['card2_title']}""" ) clinical_report_box = gr.HTML( value=t_init["placeholder_html"], elem_id="rf-clinical-box" ) probs_m1_label = gr.Label( num_top_classes=8, label=t_init["label_m1"], elem_id="rf-probs-m1" ) with gr.Column(visible=True) as secondary_col: probs_m2_label = gr.Label( num_top_classes=8, label=t_init["label_m2"], elem_id="rf-probs-m2" ) # Invisible MCP bridge button mcp_bridge_btn = gr.Button(visible=False) mcp_bridge_btn.click( fn=classify, inputs=[image_input, model_selector], outputs=probs_m1_label, api_name="classify" ) # Event Handlers: Language Toggle lang_toggle_btn.click( fn=toggle_language, inputs=[lang_state], outputs=[ lang_state, lang_toggle_btn, header_banner_html, guide_accordion, guide_content, card1_title, image_input, model_selector, model_info, submit_btn, reset_btn, privacy_html, card2_title, clinical_report_box, probs_m1_label, probs_m2_label, footer_html := gr.HTML( f"""
{t_init['footer_text']}
""" ) ] ) # Event Handlers: Model Selector Change model_selector.change( fn=update_model_blurb, inputs=[model_selector, lang_state], outputs=model_info, api_name=False ) # Event Handlers: Submit submit_btn.click( fn=predict_ui, inputs=[image_input, model_selector, lang_state], outputs=[clinical_report_box, probs_m1_label, probs_m2_label, secondary_col], api_name=False ) # Event Handlers: Reset reset_btn.click( fn=clear_all, inputs=[lang_state], outputs=[image_input, model_selector, model_info, clinical_report_box, probs_m1_label, probs_m2_label, secondary_col], api_name=False ) if __name__ == "__main__": try: sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stderr.reconfigure(encoding='utf-8', errors='replace') except Exception: pass launch_kwargs = { "server_name": "0.0.0.0", "server_port": 7860, } import inspect if "mcp_server" in inspect.signature(demo.launch).parameters: launch_kwargs["mcp_server"] = True demo.launch(**launch_kwargs)