Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| from typing import List, Any | |
| import gradio as gr | |
| from PIL import Image | |
| from pipeline import SmartCBC | |
| # ------------------------------------------------- | |
| # Initialize pipeline ONCE (cached in Spaces) | |
| # ------------------------------------------------- | |
| cbc = SmartCBC() # loads YOLO + classifier once | |
| # ------------------------------------------------- | |
| # Helper: convert uploaded files to PIL Images | |
| # ------------------------------------------------- | |
| def files_to_pil_list(files: List[Any]) -> List[Image.Image]: | |
| """ | |
| Gradio Files (file_count='multiple') returns a list of file objects or dicts. | |
| Each item commonly looks like: | |
| - {"name": "/tmp/....png", "orig_name": "...", ...} | |
| - or a file-like object with .name | |
| This helper normalizes them into a list of RGB PIL Images. | |
| """ | |
| pil_list: List[Image.Image] = [] | |
| if files is None: | |
| return pil_list | |
| for f in files: | |
| # Newer gradio often returns dicts with "name" | |
| if isinstance(f, dict) and "name" in f: | |
| path = f["name"] | |
| # Older style: File object with .name | |
| elif hasattr(f, "name"): | |
| path = f.name | |
| # Fallback: assume it's already a path-like | |
| else: | |
| path = str(f) | |
| if not os.path.isfile(path): | |
| raise FileNotFoundError(f"Uploaded file not found on disk: {path}") | |
| img = Image.open(path).convert("RGB") | |
| pil_list.append(img) | |
| return pil_list | |
| # ------------------------------------------------- | |
| # Core Gradio wrapper | |
| # ------------------------------------------------- | |
| def analyze_images(files, age, gender, output_mode): | |
| """ | |
| Wrapper for SmartCBC.analyze(). | |
| - Accepts one or multiple images from a Gradio Files input. | |
| - If a single image -> sends a single PIL.Image | |
| If multiple -> sends a list[Image.Image] (SmartCBC can route to analyze_batch). | |
| - Returns either a human-readable text report or full JSON. | |
| """ | |
| if files is None or len(files) == 0: | |
| return "Please upload at least one image.", None | |
| pil_images = files_to_pil_list(files) | |
| if len(pil_images) == 1: | |
| image_input = pil_images[0] | |
| else: | |
| image_input = pil_images | |
| # Run SmartCBC pipeline | |
| result = cbc.analyze( | |
| image=image_input, | |
| age=age, | |
| gender=gender, | |
| ) | |
| # Choose output mode | |
| if output_mode == "Text Report": | |
| return result.get("report_text", "No report generated."), None | |
| else: | |
| return None, result | |
| # ------------------------------------------------- | |
| # Gradio UI Layout (compatible with Gradio 4.0.0) | |
| # ------------------------------------------------- | |
| with gr.Blocks(title="SmartCBC - Multimodal Blood Analysis") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🩸 SmartCBC — Multimodal AI Blood Smear Analysis | |
| Upload **one or multiple** peripheral smear FOV images and get: | |
| - RBC / WBC / Platelet counts | |
| - WBC subtype classification | |
| - Aggregated multi-FOV differential | |
| - Age-specific reference comparisons | |
| - Clinical insights (non-diagnostic) | |
| """ | |
| ) | |
| with gr.Row(): | |
| # Use Files for multi-image upload (works on Gradio 4.0.0) | |
| img_in = gr.Files( | |
| label="Upload 1 or Multiple Blood Smear Images (FOVs)", | |
| file_count="multiple", | |
| file_types=["image"], | |
| ) | |
| with gr.Column(): | |
| age_in = gr.Number(label="Age (years)", value=30) | |
| gender_in = gr.Dropdown( | |
| ["", "M", "F"], | |
| label="Gender (optional)", | |
| value="" | |
| ) | |
| output_mode = gr.Radio( | |
| ["Text Report", "Structured JSON"], | |
| value="Text Report", | |
| label="Output Format", | |
| ) | |
| btn = gr.Button("Analyze") | |
| # OUTPUT AREAS | |
| txt_out = gr.Textbox( | |
| label="Report (Human Readable)", | |
| visible=True, | |
| lines=30, | |
| interactive=False, | |
| ) | |
| json_out = gr.JSON( | |
| label="Structured Output (JSON)", | |
| visible=False, | |
| ) | |
| # Toggle visibility based on output mode | |
| def toggle_output(mode): | |
| return ( | |
| gr.update(visible=(mode == "Text Report")), | |
| gr.update(visible=(mode == "Structured JSON")), | |
| ) | |
| output_mode.change(toggle_output, [output_mode], [txt_out, json_out]) | |
| # Button Binding | |
| btn.click( | |
| analyze_images, | |
| inputs=[img_in, age_in, gender_in, output_mode], | |
| outputs=[txt_out, json_out], | |
| ) | |
| # ------------------------------------------------- | |
| # HF Spaces entrypoint | |
| # ------------------------------------------------- | |
| if __name__ == "__main__": | |
| demo.launch() | |