Spaces:
Sleeping
Sleeping
| """ | |
| π©Έ LeukemiaScope Agentic - Multi-Agent Blood Cell Analysis | |
| HuggingFace Spaces deployment with multi-step patient flow and PDF export | |
| """ | |
| import os | |
| import sys | |
| import gradio as gr | |
| from PIL import Image | |
| from datetime import datetime | |
| # Add current dir to path | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from graph.workflow import run_analysis | |
| from tools.medgemma_predictor import get_predictor | |
| from tools.pdf_generator import generate_pdf_report | |
| # Global state | |
| current_patient = {} | |
| def get_progress_html(active_step=1): | |
| """Generate progress bar HTML with the given step highlighted""" | |
| def step_style(n): | |
| if n == active_step: | |
| return "background: rgba(255,255,255,0.95); color: #dc2626;" | |
| elif n < active_step: | |
| return "background: rgba(255,255,255,0.5); color: #dc2626;" | |
| else: | |
| return "background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.7);" | |
| def label_style(n): | |
| if n == active_step: | |
| return "color: white; font-weight: 600;" | |
| elif n < active_step: | |
| return "color: rgba(255,255,255,0.7);" | |
| else: | |
| return "color: rgba(255,255,255,0.5);" | |
| def line_style(n): | |
| if n < active_step: | |
| return "background: rgba(255,255,255,0.6);" | |
| else: | |
| return "background: rgba(255,255,255,0.3);" | |
| labels = {1: "Patient Info", 2: "Upload Image", 3: "Report"} | |
| return f""" | |
| <div class="ls-header" style="text-align: center; padding: 14px 20px; background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); color: white; border-radius: 12px; margin-bottom: 0;"> | |
| <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;"> | |
| <h2 style="margin: 0; font-size: 22px; color: white;">π©Έ LeukemiaScope</h2> | |
| <div style="display: flex; align-items: center; gap: 6px;"> | |
| <div style="width: 26px; height: 26px; border-radius: 50%; {step_style(1)} display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px;">1</div> | |
| <span style="{label_style(1)} font-size: 11px;">{labels[1]}</span> | |
| <div style="width: 24px; height: 2px; {line_style(1)}"></div> | |
| <div style="width: 26px; height: 26px; border-radius: 50%; {step_style(2)} display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px;">2</div> | |
| <span style="{label_style(2)} font-size: 11px;">{labels[2]}</span> | |
| <div style="width: 24px; height: 2px; {line_style(2)}"></div> | |
| <div style="width: 26px; height: 26px; border-radius: 50%; {step_style(3)} display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px;">3</div> | |
| <span style="{label_style(3)} font-size: 11px;">{labels[3]}</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def validate_patient_info(name, dob, gender): | |
| """Validate patient information before proceeding""" | |
| if not name or len(name.strip()) < 2: | |
| return False, "Please enter patient name (at least 2 characters)" | |
| return True, "" | |
| def save_patient_info(name, dob, gender): | |
| """Save patient info and move to next step""" | |
| global current_patient | |
| is_valid, error_msg = validate_patient_info(name, dob, gender) | |
| if not is_valid: | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| f"β {error_msg}", | |
| get_progress_html(1) | |
| ) | |
| current_patient = { | |
| "name": name.strip(), | |
| "dob": dob, | |
| "gender": gender, | |
| "id": f"LS-{datetime.now().strftime('%Y%m%d%H%M%S')}" | |
| } | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| f"β Patient registered: {name}", | |
| get_progress_html(2) | |
| ) | |
| def go_back_to_step1(): | |
| """Go back to patient info step""" | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| get_progress_html(1) | |
| ) | |
| def analyze_image_workflow(image): | |
| """Run the agentic workflow on uploaded image""" | |
| global current_patient | |
| if image is None: | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| "β Please upload an image", | |
| "", | |
| "", | |
| None, | |
| get_progress_html(2) | |
| ) | |
| # Convert to PIL if needed | |
| if not isinstance(image, Image.Image): | |
| image = Image.fromarray(image).convert("RGB") | |
| else: | |
| image = image.convert("RGB") | |
| try: | |
| # Run the agentic workflow | |
| result = run_analysis( | |
| image=image, | |
| patient_id=current_patient.get("id", "Anonymous"), | |
| patient_context=f"Name: {current_patient.get('name')}, DOB: {current_patient.get('dob')}, Gender: {current_patient.get('gender')}" | |
| ) | |
| result["patient_name"] = current_patient.get("name", "") | |
| result["patient_dob"] = current_patient.get("dob", "") | |
| result["patient_gender"] = current_patient.get("gender", "") | |
| result["patient_id"] = current_patient.get("id", "") | |
| # Generate enhanced report | |
| from agents.report_generator import generate_report | |
| report = generate_report( | |
| patient_name=result["patient_name"], | |
| patient_dob=result["patient_dob"], | |
| patient_gender=result["patient_gender"], | |
| classification=result.get("classification", "Unknown"), | |
| confidence=result.get("confidence", 0.0), | |
| clinical_advice=result.get("clinical_advice"), | |
| next_steps=result.get("next_steps"), | |
| severity=result.get("severity"), | |
| patient_id=result["patient_id"] | |
| ) | |
| # Generate PDF | |
| pdf_path = generate_pdf_report( | |
| patient_name=result["patient_name"], | |
| patient_dob=result["patient_dob"], | |
| patient_id=result["patient_id"], | |
| classification=result.get("classification", "Unknown"), | |
| confidence=result.get("confidence", 0.0), | |
| clinical_advice=result.get("clinical_advice"), | |
| next_steps=result.get("next_steps"), | |
| severity=result.get("severity"), | |
| patient_gender=result.get("patient_gender") | |
| ) | |
| # Workflow trace | |
| classification = result.get("classification", "Unknown") | |
| confidence = result.get("confidence", 0.0) | |
| trace = f""" | |
| ### Workflow Execution | |
| | Step | Agent | Status | | |
| |------|-------|--------| | |
| | 1 | π¬ Image Analyzer | β Complete | | |
| | 2 | π©Ί Clinical Advisor | {"β Complete" if result.get("clinical_complete") else "βοΈ Skipped"} | | |
| | 3 | π Report Generator | β Complete | | |
| **Classification**: {classification} ({confidence:.1%} confidence) | |
| """ | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| "β Analysis complete!", | |
| report, | |
| trace, | |
| pdf_path, | |
| get_progress_html(3) | |
| ) | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| f"β Error: {str(e)}", | |
| "", | |
| "", | |
| None, | |
| get_progress_html(2) | |
| ) | |
| def start_new_analysis(): | |
| """Reset and start a new analysis""" | |
| global current_patient | |
| current_patient = {} | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| "", | |
| "", | |
| "Not specified", | |
| None, | |
| "", | |
| "", | |
| None, | |
| get_progress_html(1) | |
| ) | |
| # ==================== Build Gradio UI ==================== | |
| custom_css = """ | |
| .gradio-container { max-width: 1000px !important; } | |
| .gradio-container .gap { gap: 4px !important; } | |
| .step-header { | |
| background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); | |
| color: white; | |
| padding: 15px 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| } | |
| /* Dark mode: force all HTML content to keep readable text on their light backgrounds */ | |
| .dark .ls-card { | |
| color: #1e293b !important; | |
| } | |
| .dark .ls-card * { | |
| color: inherit !important; | |
| } | |
| .dark .ls-card h1, .dark .ls-card h2, | |
| .dark .ls-card h3, .dark .ls-card h4 { | |
| color: inherit !important; | |
| } | |
| .dark .ls-card code { | |
| color: #1e40af !important; | |
| background: rgba(0,0,0,0.06) !important; | |
| } | |
| /* Header stays white text */ | |
| .dark .ls-header, .dark .ls-header * { | |
| color: white !important; | |
| } | |
| /* Footer */ | |
| .dark .ls-footer { | |
| background: #1e293b !important; | |
| } | |
| .dark .ls-footer p { | |
| color: #94a3b8 !important; | |
| } | |
| .dark .ls-footer code { | |
| color: #60a5fa !important; | |
| } | |
| """ | |
| custom_css += """ | |
| /* Spinner animation */ | |
| @keyframes ls-spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes ls-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .ls-spinner { | |
| width: 48px; height: 48px; | |
| border: 4px solid rgba(220,38,38,0.2); | |
| border-top-color: #dc2626; | |
| border-radius: 50%; | |
| animation: ls-spin 1s linear infinite; | |
| } | |
| .ls-pulse { animation: ls-pulse 2s ease-in-out infinite; } | |
| """ | |
| def accept_disclaimer(): | |
| """Hide disclaimer and show the main app""" | |
| return gr.update(visible=False), gr.update(visible=True) | |
| with gr.Blocks( | |
| title="LeukemiaScope - AI Blood Cell Analysis", | |
| theme=gr.themes.Soft(), | |
| css=custom_css | |
| ) as demo: | |
| # ==================== DISCLAIMER POPUP ==================== | |
| with gr.Group(visible=True) as disclaimer_section: | |
| gr.HTML(""" | |
| <div class="ls-card" style="background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.08); margin-bottom: 24px; color: #1e293b;"> | |
| <!-- Hero Header --> | |
| <div class="ls-header" style="text-align: center; padding: 40px 30px 30px; background: linear-gradient(135deg, #dc2626 0%, #991b1b 50%, #7f1d1d 100%); color: white; position: relative;"> | |
| <div style="font-size: 48px; margin-bottom: 8px;">π©Έ</div> | |
| <h1 style="margin: 0; font-size: 36px; font-weight: 700; color: white; letter-spacing: -0.5px;">LeukemiaScope</h1> | |
| <p style="margin: 8px 0 0; font-size: 16px; color: rgba(255,255,255,0.9); font-weight: 300;">AI-Powered Blood Cell Analysis with Multi-Agent Workflow</p> | |
| <div style="margin-top: 16px; display: inline-flex; gap: 12px;"> | |
| <span style="background: rgba(255,255,255,0.15); padding: 4px 14px; border-radius: 20px; font-size: 12px; color: white; backdrop-filter: blur(4px);">MedGemma 1.5 4B</span> | |
| <span style="background: rgba(255,255,255,0.15); padding: 4px 14px; border-radius: 20px; font-size: 12px; color: white; backdrop-filter: blur(4px);">LangGraph Agents</span> | |
| <span style="background: rgba(255,255,255,0.15); padding: 4px 14px; border-radius: 20px; font-size: 12px; color: white; backdrop-filter: blur(4px);">Gemini 3 Flash</span> | |
| </div> | |
| </div> | |
| <!-- Medical Disclaimer --> | |
| <div style="padding: 24px 30px; background: #fef2f2; border-bottom: 1px solid #fecaca;"> | |
| <div style="display: flex; align-items: flex-start; gap: 14px;"> | |
| <div style="font-size: 28px; flex-shrink: 0; margin-top: 2px;">βοΈ</div> | |
| <div> | |
| <h2 style="margin: 0 0 10px; font-size: 20px; color: #991b1b;">Medical Disclaimer</h2> | |
| <p style="margin: 0 0 8px; color: #7f1d1d; line-height: 1.7; font-size: 14px;"> | |
| LeukemiaScope is an <strong>AI research prototype</strong> developed for the MedGemma Impact Challenge 2026. | |
| It is designed to <strong>assist β not replace</strong> β trained medical professionals in screening blood cell images for signs of Acute Lymphoblastic Leukemia (ALL). | |
| </p> | |
| <div style="background: #fee2e2; border-radius: 8px; padding: 12px 16px; margin-top: 8px;"> | |
| <p style="margin: 0; color: #991b1b; font-size: 13px; line-height: 1.6;"> | |
| <strong>β οΈ This is NOT a certified medical device.</strong> All results require confirmation through standard laboratory procedures | |
| (CBC, bone marrow biopsy, flow cytometry) by a qualified hematologist or oncologist. Do not make clinical decisions based solely on this tool. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- How It Works --> | |
| <div style="padding: 24px 30px; border-bottom: 1px solid #e2e8f0;"> | |
| <h2 style="margin: 0 0 16px; font-size: 20px; color: #1e293b;">π Agentic AI Workflow</h2> | |
| <p style="color: #475569; font-size: 14px; margin: 0 0 16px; line-height: 1.6;"> | |
| Your image is processed through a <strong>3-step intelligent pipeline</strong> built with LangGraph. Each agent specializes in a specific task and passes its findings to the next: | |
| </p> | |
| <div style="display: flex; gap: 16px; flex-wrap: wrap;"> | |
| <!-- Agent 1 --> | |
| <div style="flex: 1; min-width: 200px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; position: relative;"> | |
| <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"> | |
| <div style="background: #dc2626; color: white !important; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;">1</div> | |
| <strong style="color: #1e293b; font-size: 14px;">π¬ Image Analyzer</strong> | |
| </div> | |
| <p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.5;">Fine-tuned <strong>MedGemma 1.5 4B</strong> with LoRA classifies cells as Normal or Leukemia.</p> | |
| </div> | |
| <!-- Agent 2 --> | |
| <div style="flex: 1; min-width: 200px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px;"> | |
| <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"> | |
| <div style="background: #dc2626; color: white !important; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;">2</div> | |
| <strong style="color: #1e293b; font-size: 14px;">π©Ί Clinical Advisor</strong> | |
| </div> | |
| <p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.5;">If leukemia is detected, <strong>Gemini 3 Flash</strong> generates clinical advice and risk assessment.</p> | |
| </div> | |
| <!-- Agent 3 --> | |
| <div style="flex: 1; min-width: 200px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px;"> | |
| <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"> | |
| <div style="background: #dc2626; color: white !important; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;">3</div> | |
| <strong style="color: #1e293b; font-size: 14px;">π Report Generator</strong> | |
| </div> | |
| <p style="margin: 0; color: #64748b; font-size: 13px; line-height: 1.5;">Compiles a structured HTML report with patient data, results, and downloadable PDF.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Image Requirements + Examples --> | |
| <div style="padding: 24px 30px; border-bottom: 1px solid #e2e8f0;"> | |
| <h2 style="margin: 0 0 16px; font-size: 20px; color: #1e293b;">π· Supported Image Types</h2> | |
| <p style="color: #475569; font-size: 14px; margin: 0 0 16px; line-height: 1.6;"> | |
| The model was trained on the <a href="https://www.kaggle.com/datasets/andrewmvd/leukemia-classification" target="_blank" style="color: #dc2626; text-decoration: underline; font-weight: 600;">Kaggle Leukemia Classification Dataset</a> (ALL-IDB). | |
| For best results, upload images similar to the examples below: | |
| </p> | |
| </div> | |
| </div> | |
| """) | |
| # Example images using Gradio components | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML(""" | |
| <div class="ls-card" style="text-align:center; background:#f8fafc; border-radius:12px; padding:12px; color: #1e293b;"> | |
| <p style="margin:0 0 4px; font-weight:600; color:#22c55e;">β Normal Blood Cell</p> | |
| <p style="margin:0; font-size:12px; color:#64748b;">Healthy lymphocyte β round, well-defined</p> | |
| </div> | |
| """) | |
| gr.Image( | |
| value="examples/normal_cell.png", | |
| label="Normal Cell Example", | |
| show_label=False, | |
| height=220, | |
| interactive=False | |
| ) | |
| with gr.Column(): | |
| gr.HTML(""" | |
| <div class="ls-card" style="text-align:center; background:#f8fafc; border-radius:12px; padding:12px; color: #1e293b;"> | |
| <p style="margin:0 0 4px; font-weight:600; color:#ef4444;">β οΈ Leukemia Blast Cell</p> | |
| <p style="margin:0; font-size:12px; color:#64748b;">Abnormal blast β irregular, large nucleus</p> | |
| </div> | |
| """) | |
| gr.Image( | |
| value="examples/leukemia_cell.png", | |
| label="Leukemia Cell Example", | |
| show_label=False, | |
| height=220, | |
| interactive=False | |
| ) | |
| gr.HTML(""" | |
| <div class="ls-card" style="background: #ffffff; border-radius: 12px; padding: 16px 30px; margin-top: 16px; margin-bottom: 16px; border: 1px solid #e2e8f0; color: #1e293b;"> | |
| <div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; font-size: 13px; color: #64748b;"> | |
| <span>βοΈ <strong>Dark background</strong></span> | |
| <span>βοΈ <strong>Single cell crops</strong></span> | |
| <span>βοΈ <strong>Wright/Giemsa stain</strong></span> | |
| <span>βοΈ <strong>JPEG or PNG</strong></span> | |
| <span>βοΈ <strong>Clear, in-focus</strong></span> | |
| </div> | |
| </div> | |
| """) | |
| accept_btn = gr.Button( | |
| "β I Understand & Accept β Proceed to App", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| # ==================== MAIN APP (hidden until disclaimer accepted) ==================== | |
| with gr.Group(visible=False) as main_app: | |
| # Dynamic progress bar | |
| progress_bar = gr.HTML(value=get_progress_html(1)) | |
| status_msg = gr.Markdown("") | |
| # Step 1: Patient Information | |
| with gr.Group(visible=True) as step1: | |
| gr.Markdown("## π Step 1: Patient Information") | |
| gr.Markdown("Please enter the patient details before proceeding with the analysis.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| patient_name = gr.Textbox(label="Full Name *", placeholder="Enter patient's full name", max_lines=1) | |
| patient_dob = gr.Textbox(label="Date of Birth", placeholder="YYYY-MM-DD", max_lines=1) | |
| patient_gender = gr.Dropdown(label="Gender", choices=["Not specified", "Male", "Female", "Other"], value="Not specified") | |
| gr.HTML(""" | |
| <div style="background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 14px 16px; margin: 12px 0 4px;"> | |
| <p style="margin: 0; color: #0369a1; font-size: 13px; line-height: 1.6;"> | |
| π <strong>Privacy Notice:</strong> Your personal information is used solely to generate this screening report. | |
| <strong>No data is stored, retained, or shared</strong> β all information is discarded after the session ends. | |
| </p> | |
| </div> | |
| """) | |
| privacy_consent = gr.Checkbox(label="I acknowledge that my information will not be stored and consent to proceed.", value=False) | |
| next_btn_1 = gr.Button("Continue to Image Upload β", variant="primary", size="lg", interactive=False) | |
| # Step 2: Image Upload | |
| with gr.Group(visible=False) as step2: | |
| gr.Markdown("## π· Step 2: Upload Blood Cell Image") | |
| gr.Markdown("Upload a microscopy image of blood cells for analysis.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| image_input = gr.Image(label="Blood Cell Image", type="pil", height=350) | |
| with gr.Row(): | |
| back_btn = gr.Button("β Back", size="lg") | |
| analyze_btn = gr.Button("π¬ Analyze Image", variant="primary", size="lg") | |
| # Processing overlay (hidden by default) | |
| with gr.Group(visible=False) as processing_overlay: | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 40px 20px;"> | |
| <div style="display: inline-block; width: 48px; height: 48px; border: 4px solid rgba(220,38,38,0.2); border-top-color: #dc2626; border-radius: 50%; animation: ls-spin 1s linear infinite;"></div> | |
| <h3 style="margin: 20px 0 8px; color: #1e293b; font-size: 20px;">π¬ Analyzing Blood Cell Image...</h3> | |
| <p style="color: #64748b; font-size: 14px; margin: 0 0 20px;">Multi-agent workflow in progress. This may take 15β30 seconds.</p> | |
| <div style="display: flex; flex-direction: column; gap: 8px; max-width: 360px; margin: 0 auto; text-align: left;"> | |
| <div style="display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: #fef2f2; border-radius: 8px; animation: ls-pulse 2s ease-in-out infinite;"> | |
| <span style="font-size: 16px;">π¬</span> | |
| <span style="color: #991b1b; font-size: 13px; font-weight: 500;">Agent 1: Image Analyzer running MedGemma...</span> | |
| </div> | |
| <div style="display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: #f8fafc; border-radius: 8px; opacity: 0.5;"> | |
| <span style="font-size: 16px;">π©Ί</span> | |
| <span style="color: #64748b; font-size: 13px;">Agent 2: Clinical Advisor (pending)</span> | |
| </div> | |
| <div style="display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: #f8fafc; border-radius: 8px; opacity: 0.5;"> | |
| <span style="font-size: 16px;">π</span> | |
| <span style="color: #64748b; font-size: 13px;">Agent 3: Report Generator (pending)</span> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # Step 3: Results | |
| with gr.Group(visible=False) as step3: | |
| gr.HTML(""" | |
| <div class="ls-card" style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 16px 20px; margin-bottom: 8px; color: #166534; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 18px; color: #166534;">β Analysis Complete β Report Ready</h3> | |
| </div> | |
| """) | |
| # Action buttons β prominent at top | |
| with gr.Row(): | |
| pdf_download = gr.File(label="π₯ Download PDF Report", interactive=False) | |
| with gr.Row(): | |
| new_analysis_btn = gr.Button("π Start New Analysis", variant="primary", size="lg") | |
| # Workflow trace (collapsible) | |
| with gr.Accordion("π‘ Workflow Execution Trace", open=False): | |
| trace_output = gr.Markdown(label="Workflow Trace") | |
| # Full-width report | |
| gr.HTML(""" | |
| <div style="margin-top: 8px;"> | |
| <h3 style="margin: 0 0 8px; color: #1e293b; font-size: 16px;">π Full Medical Report</h3> | |
| </div> | |
| """) | |
| report_output = gr.HTML(label="Medical Report") | |
| # Footer (inside main_app so it hides with disclaimer) | |
| gr.HTML(""" | |
| <div class="ls-footer" style="margin-top: 30px; padding: 20px; background: #f8fafc; border-radius: 10px; text-align: center;"> | |
| <p style="margin: 0; color: #64748b; font-size: 14px;"> | |
| Powered by <strong style="color: #64748b;">LangGraph</strong> Multi-Agent Workflow | | |
| Model: <code style="color: #3b82f6;">chaudhrysuleman/medgemma-1.5-4b-it-leukemia-lora</code> | |
| </p> | |
| <p style="margin: 10px 0 0 0; color: #94a3b8; font-size: 12px;"> | |
| Built for the MedGemma Impact Challenge 2026 | By Chaudhry Muhammad Suleman & Muhammad Idnan | |
| </p> | |
| </div> | |
| """) | |
| # ==================== Event Handlers ==================== | |
| # Disclaimer accept | |
| accept_btn.click(accept_disclaimer, [], [disclaimer_section, main_app]) | |
| # Privacy consent toggles Continue button | |
| privacy_consent.change( | |
| fn=lambda checked: gr.update(interactive=checked), | |
| inputs=[privacy_consent], | |
| outputs=[next_btn_1] | |
| ) | |
| # Step 1 -> Step 2 | |
| next_btn_1.click(save_patient_info, [patient_name, patient_dob, patient_gender], [step1, step2, step3, status_msg, progress_bar]) | |
| back_btn.click(go_back_to_step1, [], [step1, step2, step3, progress_bar]) | |
| # Analyze: chain events β show overlay first, then run analysis | |
| def show_processing(): | |
| return gr.update(visible=False), gr.update(visible=True), get_progress_html(2) | |
| def run_and_finish(image): | |
| # Run the actual analysis | |
| result = analyze_image_workflow(image) | |
| # result is (step2_vis, step3_vis, status, report, trace, pdf, progress) | |
| # We also need to hide the processing overlay | |
| return ( | |
| result[0], # step2 | |
| result[1], # step3 | |
| gr.update(visible=False), # hide processing overlay | |
| result[2], # status_msg | |
| result[3], # report_output | |
| result[4], # trace_output | |
| result[5], # pdf_download | |
| result[6], # progress_bar | |
| ) | |
| analyze_btn.click( | |
| show_processing, [], [step2, processing_overlay, progress_bar] | |
| ).then( | |
| run_and_finish, [image_input], [step2, step3, processing_overlay, status_msg, report_output, trace_output, pdf_download, progress_bar] | |
| ) | |
| new_analysis_btn.click(start_new_analysis, [], [step1, step2, step3, patient_name, patient_dob, patient_gender, image_input, report_output, trace_output, pdf_download, progress_bar]) | |
| # Pre-load model at startup | |
| print("=" * 60) | |
| print("π©Έ LeukemiaScope - Agentic AI Workflow") | |
| print("=" * 60) | |
| print("π₯ Pre-loading MedGemma model...") | |
| predictor = get_predictor() | |
| predictor.load() | |
| print("β Model loaded! Launching app...") | |
| # Launch for HuggingFace Spaces | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |