Spaces:
Build error
Build error
| %%writefile app.py | |
| import gradio as gr | |
| import os, sys, subprocess, re, json, base64, io, contextlib | |
| from io import BytesIO | |
| from PIL import Image | |
| from huggingface_hub import hf_hub_download | |
| # ββ HARDWARE ββββββββββββββββββββββββββββββββββ | |
| try: | |
| result = subprocess.run(['nvidia-smi'], capture_output=True) | |
| HAS_GPU = result.returncode == 0 | |
| except: | |
| HAS_GPU = False | |
| # ββ INSTALL βββββββββββββββββββββββββββββββββββ | |
| try: | |
| import llama_cpp | |
| import argostranslate.package | |
| import argostranslate.translate | |
| except ImportError: | |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', | |
| 'argostranslate', 'huggingface_hub', 'pillow']) | |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', | |
| 'llama-cpp-python', | |
| '--extra-index-url', | |
| 'https://abetlen.github.io/llama-cpp-python/whl/cu121' | |
| if HAS_GPU else | |
| 'https://abetlen.github.io/llama-cpp-python/whl/cpu' | |
| ]) | |
| import llama_cpp | |
| import argostranslate.package | |
| import argostranslate.translate | |
| from llama_cpp import Llama | |
| from llama_cpp.llama_chat_format import Llava15ChatHandler | |
| LANGUAGES = { | |
| "English": "en", "Urdu": "ur", "Arabic": "ar", | |
| "Hindi": "hi", "French": "fr", "Spanish": "es" | |
| } | |
| # ββ LOAD MODEL ββββββββββββββββββββββββββββββββ | |
| def load_model(): | |
| print("π₯ Downloading MedGemma GGUF...") | |
| text_gguf = hf_hub_download( | |
| repo_id="unsloth/medgemma-4b-it-GGUF", | |
| filename="medgemma-4b-it-Q4_K_M.gguf" | |
| ) | |
| vision_gguf = hf_hub_download( | |
| repo_id="kelkalot/medgemma-4b-it-GGUF", | |
| filename="mmproj-medgemma-4b-it-F16.gguf" | |
| ) | |
| chat_handler = Llava15ChatHandler(clip_model_path=vision_gguf) | |
| return Llama( | |
| model_path=text_gguf, | |
| chat_handler=chat_handler, | |
| chat_format="gemma", | |
| n_ctx=4096, | |
| n_threads=os.cpu_count() or 4, | |
| n_gpu_layers=-1 if HAS_GPU else 0, | |
| verbose=False | |
| ) | |
| model = load_model() | |
| # ββ TRANSLATION βββββββββββββββββββββββββββββββ | |
| def ensure_language_downloaded(target_code): | |
| if target_code == "en": return | |
| installed = argostranslate.package.get_installed_packages() | |
| if any(p.to_code == target_code and p.from_code == 'en' for p in installed): return | |
| argostranslate.package.update_package_index() | |
| available = argostranslate.package.get_available_packages() | |
| try: | |
| pkg = next(filter(lambda x: x.from_code == 'en' and x.to_code == target_code, available)) | |
| argostranslate.package.install_from_path(pkg.download()) | |
| except: pass | |
| def translate_text(text, target_code): | |
| if target_code == "en" or not text or text == "N/A": return text | |
| try: return argostranslate.translate.translate(text, "en", target_code) | |
| except: return text | |
| # ββ JSON HELPERS ββββββββββββββββββββββββββββββ | |
| def fix_json(raw): | |
| raw = re.sub(r'```json|```', '', raw).strip() | |
| raw = re.sub(r':\s*true\b', ': "True"', raw, flags=re.IGNORECASE) | |
| raw = re.sub(r':\s*false\b', ': "False"', raw, flags=re.IGNORECASE) | |
| raw = re.sub(r':\s*high\b', ': "High"', raw, flags=re.IGNORECASE) | |
| raw = re.sub(r':\s*medium\b', ': "Medium"', raw, flags=re.IGNORECASE) | |
| raw = re.sub(r':\s*low\b', ': "Low"', raw, flags=re.IGNORECASE) | |
| return raw | |
| def parse_output(raw): | |
| try: return json.loads(fix_json(raw)) | |
| except: pass | |
| try: | |
| match = re.search(r'\{.*?\}', raw, re.DOTALL) | |
| if match: return json.loads(fix_json(match.group())) | |
| except: pass | |
| def extract(p, t, d='N/A'): | |
| m = re.search(p, t, re.IGNORECASE | re.DOTALL) | |
| return m.group(1).strip() if m else d | |
| return { | |
| 'claim': extract(r'"claim"\s*:\s*"([^"]+)"', raw), | |
| 'verdict': extract(r'"verdict"\s*:\s*["]?([^",}\n]+)', raw), | |
| 'reason': extract(r'"reason"\s*:\s*"([^"]+)"', raw), | |
| 'doctor_summary': extract(r'"doctor_summary"\s*:\s*"([^"]+)"', raw), | |
| 'risk_level': extract(r'"risk_level"\s*:\s*["]?([^",}\n]+)',raw), | |
| 'confidence': extract(r'"confidence"\s*:\s*["]?([0-9.]+)', raw, '0.0'), | |
| } | |
| # ββ ANALYZE βββββββββββββββββββββββββββββββββββ | |
| def analyze(user_claim, symptoms, age, gender, condition, | |
| language, image): | |
| lang_code = LANGUAGES.get(language, "en") | |
| ensure_language_downloaded(lang_code) | |
| history = f"{age} Years Old {gender}, {condition}" | |
| image_b64 = None | |
| if image is not None: | |
| try: | |
| img = Image.fromarray(image).convert("RGB") | |
| buf = BytesIO() | |
| img.save(buf, format="JPEG") | |
| image_b64 = base64.b64encode(buf.getvalue()).decode('utf-8') | |
| except: pass | |
| raw_prompt = f"""<bos><start_of_turn>user | |
| You are an expert Clinical Triage Specialist and Medical Fact-Checker. | |
| Return ONLY a valid JSON object. No extra text, no markdown outside the JSON. | |
| SCHEMA: | |
| {{"claim":"...","verdict":"True|False|Mixed|Insufficient Evidence","reason":"...","doctor_summary":"...","risk_level":"Low|Medium|High","confidence":"0.0-1.0"}} | |
| EXAMPLE: | |
| {{"claim":"Drinking bleach cures covid","verdict":"False","reason":"Bleach is caustic and toxic with no antiviral properties.","doctor_summary":"HIGH ALERT: toxic ingestion risk. Consult toxicology.","risk_level":"High","confidence":"0.98"}} | |
| NOW EVALUATE: | |
| Symptoms: {symptoms} | |
| Claim: "{user_claim}" | |
| Patient: {history} | |
| Image: {"Provided" if image_b64 else "Not provided"} | |
| OUTPUT JSON:<end_of_turn> | |
| <start_of_turn>model | |
| """ | |
| if not image_b64: | |
| sink = io.StringIO() | |
| with contextlib.redirect_stdout(sink): | |
| response = model.create_completion( | |
| prompt=raw_prompt, | |
| max_tokens=350, | |
| temperature=0.1, | |
| stop=["<end_of_turn>"] | |
| ) | |
| raw = response['choices'][0]['text'] | |
| else: | |
| content = [ | |
| {"type": "text", "text": raw_prompt}, | |
| {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}} | |
| ] | |
| sink = io.StringIO() | |
| with contextlib.redirect_stdout(sink): | |
| response = model.create_chat_completion( | |
| messages=[{"role": "user", "content": content}], | |
| max_tokens=350, temperature=0.1, repeat_penalty=1.3 | |
| ) | |
| raw = response['choices'][0]['message']['content'] | |
| parsed = parse_output(raw) | |
| if lang_code != "en": | |
| parsed['reason'] = translate_text(parsed.get('reason', ''), lang_code) | |
| parsed['doctor_summary'] = translate_text(parsed.get('doctor_summary', ''), lang_code) | |
| # ββ Format output βββββββββββββββββββββββββ | |
| verdict = parsed.get('verdict', 'N/A').strip().strip('"') | |
| risk = parsed.get('risk_level', 'N/A').strip().strip('"') | |
| reason = parsed.get('reason', 'N/A') | |
| doc_note = parsed.get('doctor_summary', 'N/A') | |
| confidence = parsed.get('confidence', '0.0') | |
| verdict_map = { | |
| "True": "β TRUE", | |
| "False": "β FALSE", | |
| "Mixed": "β οΈ MIXED", | |
| "Insufficient Evidence": "π INSUFFICIENT EVIDENCE" | |
| } | |
| risk_map = {"High": "π΄ High", "Medium": "π‘ Medium", "Low": "π’ Low"} | |
| verdict_str = verdict_map.get(verdict, f"β {verdict}") | |
| risk_str = risk_map.get(risk, f"βͺ {risk}") | |
| try: | |
| conf_pct = f"{float(confidence):.0%}" | |
| except: | |
| conf_pct = confidence | |
| return ( | |
| verdict_str, | |
| risk_str, | |
| conf_pct, | |
| reason, | |
| doc_note | |
| ) | |
| # ββ EXAMPLES ββββββββββββββββββββββββββββββββββ | |
| EXAMPLES = [ | |
| ["Drinking Dettol cures COVID-19", "Fever, cough", 30, "Male", "No chronic illness", "English", None], | |
| ["Eating oranges gives vitamin C and boosts immunity","Fatigue, frequent colds", 25, "Female", "No known conditions", "English", None], | |
| ["Jeera water permanently cures diabetes", "High blood sugar, fatigue", 70, "Male", "Type 2 Diabetes on Insulin", "English", None], | |
| ["Turmeric milk cures pneumonia completely", "Fever, chest pain", 60, "Female", "Hypertension", "English", None], | |
| ["Drinking cold water causes cancer", "No symptoms", 40, "Male", "Generally healthy", "English", None], | |
| ] | |
| # ββ GRADIO UI βββββββββββββββββββββββββββββββββ | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(primary_hue="blue"), | |
| title="π‘οΈ MedGuard" | |
| ) as demo: | |
| gr.HTML(""" | |
| <div style="background:linear-gradient(135deg,#0077b6,#023e8a); | |
| border-radius:16px; padding:32px; text-align:center; margin-bottom:20px;"> | |
| <h1 style="color:white; font-size:2.5em; margin:0;">π‘οΈ MedGuard</h1> | |
| <p style="color:#90e0ef; margin:8px 0 0;"> | |
| AI-Powered Medical Misinformation Defense Β· MedGemma 4B-IT Β· Edge AI | |
| </p> | |
| <div style="margin-top:12px;"> | |
| <span style="background:rgba(255,255,255,0.15); color:white; | |
| border-radius:20px; padding:4px 12px; font-size:0.8em; margin:3px;"> | |
| βοΈ MedGemma</span> | |
| <span style="background:rgba(255,255,255,0.15); color:white; | |
| border-radius:20px; padding:4px 12px; font-size:0.8em; margin:3px;"> | |
| π Edge AI</span> | |
| <span style="background:rgba(255,255,255,0.15); color:white; | |
| border-radius:20px; padding:4px 12px; font-size:0.8em; margin:3px;"> | |
| π Multilingual</span> | |
| <span style="background:rgba(255,255,255,0.15); color:white; | |
| border-radius:20px; padding:4px 12px; font-size:0.8em; margin:3px;"> | |
| π©Ί Triage Ready</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # ββ LEFT COLUMN βββββββββββββββββββββββ | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π Patient Information") | |
| user_claim = gr.Textbox( | |
| label="π Health Claim to Verify", | |
| placeholder="Paste a WhatsApp health claim, home remedy, or medical advice...", | |
| lines=3 | |
| ) | |
| symptoms = gr.Textbox( | |
| label="π€ Patient Symptoms", | |
| placeholder="e.g. fever, fatigue, chest pain", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| age = gr.Number(label="π Age", value=45, minimum=1, maximum=120) | |
| gender = gr.Dropdown( | |
| label="β§ Gender", | |
| choices=["Male", "Female", "Other"], | |
| value="Male" | |
| ) | |
| condition = gr.Textbox( | |
| label="π₯ Medical History", | |
| value="No known conditions" | |
| ) | |
| language = gr.Dropdown( | |
| label="π Output Language", | |
| choices=list(LANGUAGES.keys()), | |
| value="English" | |
| ) | |
| image = gr.Image( | |
| label="πΌοΈ Upload X-Ray or Medical Image (Optional)", | |
| type="numpy" | |
| ) | |
| analyze_btn = gr.Button( | |
| "π‘οΈ Run MedGuard Analysis", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| gr.Markdown("### π‘ Quick Examples") | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=[user_claim, symptoms, age, gender, condition, language, image], | |
| label="Click to load" | |
| ) | |
| # ββ RIGHT COLUMN ββββββββββββββββββββββ | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π MedGuard Analysis Result") | |
| verdict_out = gr.Textbox(label="βοΈ Verdict", interactive=False) | |
| risk_out = gr.Textbox(label="π¨ Risk Level", interactive=False) | |
| confidence_out = gr.Textbox(label="π Confidence", interactive=False) | |
| reason_out = gr.Textbox(label="π§ Medical Reasoning", | |
| lines=6, interactive=False) | |
| doctor_out = gr.Textbox(label="π¨ββοΈ Doctor Triage Note", | |
| lines=5, interactive=False) | |
| gr.HTML(""" | |
| <div style="background:#fff8e1; border-left:4px solid #ffa000; | |
| border-radius:8px; padding:12px; margin-top:16px; | |
| font-size:0.85em; color:#555;"> | |
| β οΈ MedGuard is an AI triage support tool. | |
| It does <b>not</b> replace qualified medical professionals. | |
| Always consult a doctor. | |
| </div> | |
| """) | |
| analyze_btn.click( | |
| fn=analyze, | |
| inputs=[user_claim, symptoms, age, gender, | |
| condition, language, image], | |
| outputs=[verdict_out, risk_out, confidence_out, | |
| reason_out, doctor_out] | |
| ) | |
| demo.launch() |