""" app.py — Hugging Face Space — Gradio 6 Compatible Medical AI Diagnostic System """ import subprocess, sys # Install LLaVA without overriding transformers subprocess.run([ sys.executable, "-m", "pip", "install", "--no-deps", "--quiet", "git+https://github.com/haotian-liu/LLaVA.git" ], check=True) import io, os, gc, re, uuid, base64 import spaces import torch import numpy as np import cv2 import gradio as gr from PIL import Image from peft import PeftModel from llava.model.builder import load_pretrained_model from llava.mm_utils import (get_model_name_from_path, process_images, tokenizer_image_token) from llava.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN from llava.conversation import conv_templates BASE_MODEL_ID = "microsoft/llava-med-v1.5-mistral-7b" LORA_MODEL_ID = "omaraboelmaaty/llava-med-lora-v2" LORA_SUBFOLDER = "checkpoints/llava-med-finetuned-v2/final" model = tokenizer = image_processor = None _loaded = False L = { "ar": { "title": "نظام التشخيص الطبي بالذكاء الاصطناعي", "sub": "رفع الأشعة • التقرير • خريطة الخطورة • محادثة", "upload": "ارفع الأشعة", "analyze": "تحليل الأشعة", "report": "التقرير الطبي", "overlay": "الأشعة المحللة", "heatmap": "خريطة الخطورة", "risk": "نسبة الخطورة", "tab1": "تحليل", "tab2": "محادثة", "q_title": "أسئلة مقترحة", "chat_ph": "اكتب سؤالك...", "send": "إرسال", "sid": "الجلسة", "hint": "اكتب شكراً أو thanks لإنهاء الجلسة", "no_img": "ارفع أشعة أولاً.", "no_rep": "حلّل الأشعة أولاً.", "welcome": "أهلاً دكتور — التحليل جاهز. يمكنك طرح أسئلتك.", "closed": "انتهت الجلسة. شكراً.", "rep_ph": "سيظهر التقرير هنا...", "lang_lbl": "اللغة", }, "en": { "title": "AI Medical Diagnostic System", "sub": "Upload Scan • Report • Risk Heatmap • Chat", "upload": "Upload Scan", "analyze": "Analyze Scan", "report": "Medical Report", "overlay": "Annotated Scan", "heatmap": "Risk Heatmap", "risk": "Risk Score", "tab1": "Analysis", "tab2": "Chat", "q_title": "Quick Questions", "chat_ph": "Type your question...", "send": "Send", "sid": "Session", "hint": "Type thanks to end session", "no_img": "Upload a scan first.", "no_rep": "Analyze the scan first.", "welcome": "Welcome Doctor — Analysis done. Ask your questions.", "closed": "Session ended. Thank you.", "rep_ph": "Report will appear here...", "lang_lbl": "Language", }, } AR_Q = ["ما أبرز الموجودات؟","هل توجد تغيّرات مرضية؟","ما الانطباع التشخيصي؟", "هل يلزم فحص إضافي؟","ما مدى الخطورة؟","ما التوصيات؟"] EN_Q = ["What are the key findings?","Are there pathological changes?", "What is the likely diagnosis?","Is further imaging needed?", "How severe is this?","What do you recommend?"] STOP_WORDS = {"شكرا","شكراً","thanks","thank you","bye","goodbye"} # ── CSS ─────────────────────────────────────────────────────────────────────── CSS = """ :root { --bg:#0b1120; --surface:#151f32; --surface2:#1c2a42; --border:#263450; --text:#dce8f5; --text2:#7b9bbf; --text3:#4a6484; --accent:#4d8ef0; --accent-h:#3b7de0; --lbl-bg:#1a3a7c; --lbl-text:#c8deff; --usr-msg:#162447; --usr-bdr:#2d5499; --bot-msg:#1c2a42; --bot-bdr:#263450; --shadow:0 1px 4px rgba(0,0,0,.35); --r:8px; } body[data-dark="false"] { --bg:#eef2f7; --surface:#ffffff; --surface2:#f4f7fb; --border:#d0d9e8; --text:#0f1e35; --text2:#3d5478; --text3:#8fa3be; --accent:#2563eb; --accent-h:#1a4fd6; --lbl-bg:#2563eb; --lbl-text:#ffffff; --usr-msg:#e8f0fe; --usr-bdr:#aac4f6; --bot-msg:#f4f7fb; --bot-bdr:#d0d9e8; --shadow:0 1px 4px rgba(15,30,53,.10); } footer { display:none!important } body, .gradio-container { background:var(--bg)!important; color:var(--text)!important; font-family:Inter,sans-serif!important; font-size:14px!important } .block,.panel,.form { background:var(--surface)!important; border-color:var(--border)!important; border-radius:var(--r)!important; box-shadow:var(--shadow)!important } .block .label-wrap { background:var(--lbl-bg)!important; border-radius:5px 5px 0 0!important; padding:3px 8px!important } .block .label-wrap span { color:var(--lbl-text)!important; font-size:11px!important; font-weight:600!important; text-transform:uppercase!important } textarea, input[type=text] { background:var(--surface2)!important; border:1px solid var(--border)!important; color:var(--text)!important } .tab-nav { background:var(--surface)!important; border-bottom:2px solid var(--border)!important } .tab-nav button { background:transparent!important; border:none!important; border-bottom:3px solid transparent!important; color:var(--text2)!important; box-shadow:none!important } .tab-nav button.selected { color:var(--accent)!important; border-bottom-color:var(--accent)!important; font-weight:700!important } button.primary { background:var(--accent)!important; color:#fff!important; border:none!important; border-radius:var(--r)!important; font-weight:600!important; box-shadow:none!important } button.primary:hover { background:var(--accent-h)!important } button.secondary { background:var(--surface2)!important; color:var(--text)!important; border:1px solid var(--border)!important; border-radius:var(--r)!important; box-shadow:none!important } .analyze-btn button { background:var(--accent)!important; color:#fff!important; border:none!important; border-radius:var(--r)!important; font-weight:700!important; width:100%!important; box-shadow:none!important } .q-btn button { background:var(--surface2)!important; color:var(--text2)!important; border:1px solid var(--border)!important; border-radius:6px!important; font-size:.8rem!important; width:100%!important; margin-bottom:4px!important; text-align:right!important; box-shadow:none!important } .med-header { background:var(--accent); border-radius:var(--r); padding:14px 20px; margin-bottom:10px; display:flex; align-items:center; gap:12px } .med-title { font-size:1rem; font-weight:700; color:#fff; margin:0 } .med-sub { font-size:.75rem; color:rgba(255,255,255,.78); margin:2px 0 0 } .rtl-box textarea { direction:rtl; text-align:right } .ltr-box textarea { direction:ltr; text-align:left } .toggle-row { display:flex; align-items:center; gap:6px; padding:4px 2px } .tog-icon { font-size:1rem; user-select:none } .tog-switch { position:relative; width:38px; height:21px; flex-shrink:0 } .tog-switch input { display:none } .tog-track { position:absolute; inset:0; background:var(--border); border-radius:21px; transition:background .2s } .tog-thumb { position:absolute; width:15px; height:15px; background:#fff; border-radius:50%; top:3px; left:3px; transition:left .2s; box-shadow:0 1px 3px rgba(0,0,0,.25) } .tog-switch input:checked ~ .tog-track { background:var(--accent) } .tog-switch input:checked ~ .tog-thumb { left:20px } .toggle-col, .toggle-col > *, .toggle-col .block { background:transparent!important; border:none!important; box-shadow:none!important; padding:0!important } .svelte-12ioyct, .wrap.svelte-12ioyct { border:none!important; box-shadow:none!important; background:transparent!important } .wrap.svelte-1kzox3m { background:transparent!important; border:none!important; box-shadow:none!important; padding:0!important; display:flex!important; gap:6px!important } label.svelte-1mhtq7j { background:var(--surface2)!important; border:1px solid var(--border)!important; border-radius:6px!important; padding:6px 14px!important; box-shadow:none!important } label.svelte-1mhtq7j.selected { background:var(--accent)!important; border-color:var(--accent)!important } label.svelte-1mhtq7j.selected span { color:#fff!important } .hint-txt { color:var(--text3)!important; font-size:.78rem!important; text-align:center } @media(max-width:768px) { .main-row > div { min-width:100%!important } } """ # JS for dark mode default (Gradio 6 uses js parameter in launch()) DARK_JS = """ () => { function applyDark() { document.body.setAttribute('data-dark', 'true'); } applyDark(); setTimeout(applyDark, 100); setTimeout(applyDark, 500); setTimeout(applyDark, 1500); } """ def _toggle_html(checked): chk = "checked" if checked else "" js = ( "var d=this.checked;" "document.body.setAttribute('data-dark',d?'true':'false');" "document.querySelectorAll('label span,.label-wrap span').forEach(function(el){" "el.style.color=d?'#c8deff':'#ffffff';});" "document.querySelectorAll('textarea,input[type=text]').forEach(function(el){" "el.style.background=d?'#1c2a42':'#f4f7fb';" "el.style.color=d?'#dce8f5':'#0f1e35';});" "document.getElementById('__mode_btn__').click();" ) return ( f'
{t["title"]}
' f'{t["sub"]}
{t["hint"]}
'), gr.update(label=t["lang_lbl"]), gr.update(value=_toggle_html(dark))) # ── Build UI ────────────────────────────────────────────────────────────────── def build_ui(): with gr.Blocks(title="Medical AI") as demo: gr.HTML(f"") gr.HTML("""""") state = gr.State(init_state()) header = gr.HTML(_header_html("ar")) with gr.Row(equal_height=True): lang_r = gr.Radio(choices=[("العربية","ar"),("English","en")], value="ar", label=L["ar"]["lang_lbl"], scale=2) sid_box = gr.Textbox(label=L["ar"]["sid"], value="—", interactive=False, scale=3) with gr.Column(scale=1, min_width=100, elem_classes="toggle-col"): mode_html = gr.HTML(_toggle_html(True)) mode_btn = gr.Button("m", visible=False, elem_id="__mode_btn__") with gr.Tabs(): with gr.Tab(L["ar"]["tab1"]) as tab1: with gr.Row(elem_classes="main-row"): with gr.Column(scale=1, min_width=200): img_in = gr.Image(label=L["ar"]["upload"], type="pil", height=240) an_btn = gr.Button(L["ar"]["analyze"], variant="primary", elem_classes="analyze-btn") risk = gr.Textbox(label=L["ar"]["risk"], value="—", interactive=False) with gr.Column(scale=1, min_width=200): ov_out = gr.Image(label=L["ar"]["overlay"], height=185, interactive=False) hm_out = gr.Image(label=L["ar"]["heatmap"], height=185, interactive=False) with gr.Column(scale=1, min_width=240): rep_out = gr.Textbox(label=L["ar"]["report"], lines=17, interactive=False, elem_classes="rtl-box", placeholder=L["ar"]["rep_ph"]) with gr.Tab(L["ar"]["tab2"]) as tab2: with gr.Row(): with gr.Column(scale=1, min_width=175): q_title = gr.Markdown(f"**{L['ar']['q_title']}**") with gr.Group(visible=True) as ar_grp: ar_btns = [gr.Button(q, elem_classes="q-btn", size="sm") for q in AR_Q] with gr.Group(visible=False) as en_grp: en_btns = [gr.Button(q, elem_classes="q-btn", size="sm") for q in EN_Q] with gr.Column(scale=3, min_width=280): chatbot = gr.Chatbot(height=380, avatar_images=("https://cdn-icons-png.flaticon.com/512/3774/3774299.png", "https://cdn-icons-png.flaticon.com/512/4712/4712035.png")) with gr.Row(): chat_in = gr.Textbox(label=L["ar"]["chat_ph"], lines=2, scale=5, placeholder="اكتب سؤالك...") with gr.Column(scale=1, min_width=100): send_btn = gr.Button(L["ar"]["send"], variant="primary", size="lg") hint_html = gr.HTML(f'{L["ar"]["hint"]}
') an_btn.click(fn=handle_analyze, inputs=[img_in, lang_r, state], outputs=[rep_out, ov_out, hm_out, risk, state, chatbot, sid_box]) def _cw(q,h,s): return handle_chat(q,h,s) send_btn.click(_cw, [chat_in,chatbot,state], [chatbot,state,chat_in]) chat_in.submit(_cw, [chat_in,chatbot,state], [chatbot,state,chat_in]) for btn in ar_btns+en_btns: btn.click(fn=quick_q, inputs=[gr.State(btn.value),chatbot,state], outputs=[chatbot,state,chat_in]) mode_btn.click(fn=toggle_mode, inputs=[state], outputs=[state,mode_html]) lang_r.change(fn=switch_lang, inputs=[lang_r,state], outputs=[state,header,img_in,an_btn,rep_out,ov_out,hm_out,risk,sid_box, tab1,tab2,chat_in,send_btn,q_title,ar_grp,en_grp,hint_html,lang_r,mode_html]) return demo demo = build_ui() if __name__ == "__main__": demo.launch(js=DARK_JS)