Spaces:
Sleeping
Sleeping
| # ╔══════════════════════════════════════════════════════════════════════╗ | |
| # ║ RadioScan AI — HuggingFace Spaces ║ | |
| # ║ I3AFD 2026 - Groupe 4 ║ | |
| # ║ Pipeline Multi-Agents - BioMistral-7B ║ | |
| # ╚══════════════════════════════════════════════════════════════════════╝ | |
| import sys, os, json, gc, re, torch | |
| from datetime import datetime, date | |
| from pathlib import Path | |
| import gradio as gr | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from fpdf import FPDF | |
| # ── Chemins compatibles HuggingFace Spaces ────────────────────────────── | |
| ROOT = Path("./data") | |
| HISTORY_FILE = ROOT / "history.json" | |
| DB_FILE = ROOT / "database.json" | |
| RESULTS_DIR = ROOT / "results" | |
| MODELS_DIR = ROOT / "models_cache" | |
| for d in [ROOT, RESULTS_DIR, MODELS_DIR]: | |
| d.mkdir(parents=True, exist_ok=True) | |
| # ── Chargement du modèle ───────────────────────────────────────────────── | |
| _model_cache = {} | |
| def load_model(model_key="biomistral", quantize=True): | |
| if model_key in _model_cache: | |
| return _model_cache[model_key] | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig | |
| ids = { | |
| "biomistral": "BioMistral/BioMistral-7B", | |
| "tiny": "TinyLlama/TinyLlama-1.1B-Chat-v1.0", | |
| } | |
| model_id = ids.get(model_key, model_key) | |
| use_gpu = torch.cuda.is_available() | |
| bnb = ( | |
| BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4") | |
| if quantize and use_gpu else None | |
| ) | |
| tok = AutoTokenizer.from_pretrained(model_id, cache_dir=str(MODELS_DIR), use_fast=True) | |
| if tok.pad_token is None: | |
| tok.pad_token = tok.eos_token | |
| mdl = AutoModelForCausalLM.from_pretrained( | |
| model_id, | |
| quantization_config=bnb, | |
| device_map="auto" if use_gpu else "cpu", | |
| cache_dir=str(MODELS_DIR), | |
| trust_remote_code=True, | |
| ) | |
| mdl.eval() | |
| _model_cache[model_key] = (mdl, tok) | |
| return mdl, tok | |
| def generate_text(model, tokenizer, prompt, max_new_tokens=150, temperature=0.1): | |
| inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=1024) | |
| inputs = {k: v.to(model.device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| out = model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| temperature=temperature, | |
| do_sample=False, | |
| pad_token_id=tokenizer.eos_token_id, | |
| ) | |
| return tokenizer.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True).strip() | |
| # Chargement au démarrage (utilise TinyLlama si pas de GPU pour éviter OOM) | |
| print("Chargement du modèle...") | |
| try: | |
| if False: # Force TinyLlama on CPU | |
| model, tokenizer = load_model("biomistral", quantize=True) | |
| print(f"✅ BioMistral-7B chargé — GPU: {torch.cuda.get_device_name(0)}") | |
| else: | |
| model, tokenizer = load_model("tiny", quantize=False) | |
| print("✅ TinyLlama chargé — CPU mode") | |
| except Exception as e: | |
| print(f"⚠️ Erreur chargement modèle : {e}") | |
| model, tokenizer = None, None | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §1 LOGO + TRADUCTIONS | |
| # ══════════════════════════════════════════════════════════════════ | |
| LOGO = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNDAgMTQwIiB3aWR0aD0iMTQwIiBoZWlnaHQ9IjE0MCI+CiAgPHJlY3QgeD0iNSIgeT0iNSIgd2lkdGg9IjEzMCIgaGVpZ2h0PSIxMzAiIHJ4PSIyMCIgZmlsbD0iIzFhNmIyZSIvPgogIDxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMjAiIHJ4PSIxNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNGNhZjZlIiBzdHJva2Utd2lkdGg9IjIiLz4KICA8Y2lyY2xlIGN4PSI3MCIgY3k9IjYyIiByPSIzOCIgZmlsbD0iIzE0NWEyNiIvPgogIDxjaXJjbGUgY3g9IjcwIiBjeT0iNjIiIHI9IjM0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Y2FmNmUiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHJlY3QgeD0iNTciIHk9IjM4IiB3aWR0aD0iMjYiIGhlaWdodD0iNDgiIHJ4PSI2IiBmaWxsPSJ3aGl0ZSIvPgogIDxyZWN0IHg9IjQ0IiB5PSI1MSIgd2lkdGg9IjUyIiBoZWlnaHQ9IjIyIiByeD0iNiIgZmlsbD0id2hpdGUiLz4KICA8cG9seWxpbmUgcG9pbnRzPSI1MCw2MiA1Nyw2MiA2MSw1MSA2NSw3MyA2OSw1NSA3Myw2OSA3Nyw2MiA5MCw2MiIKICAgIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzFhNmIyZSIgc3Ryb2tlLXdpZHRoPSIyLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgogIDxjaXJjbGUgY3g9IjcwIiBjeT0iNjIiIHI9IjMuNSIgZmlsbD0iIzFhNmIyZSIvPgogIDx0ZXh0IHg9IjcwIiB5PSIxMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC13ZWlnaHQ9IjcwMCIgZm9udC1zaXplPSIxNyIgZmlsbD0id2hpdGUiPlJhZGlvU2NhbjwvdGV4dD4KICA8dGV4dCB4PSI3MCIgeT0iMTMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiNhNWQ2YTciIGxldHRlci1zcGFjaW5nPSIzIj5BSTwvdGV4dD4KPC9zdmc+" | |
| TR = { | |
| "fr": { | |
| "app":"RadioScan AI", "sub":"Système Multi-Agents - I3AFD 2026", | |
| "a_isradio":"✅ Rapport radiologique détecté","a_notradio":"❌ Document non médical", | |
| "a_med":"🩺 Synthèse Médecin","a_pat":"👤 Synthèse Patient", | |
| "a_nores":"Lancez une analyse pour voir les résultats.", | |
| "urg_routine":"Routine","urg_urgent":"Urgent","urg_emergency":"URGENCE", | |
| "pr_foot":"Généré par RadioScan AI - À valider par un professionnel de santé", | |
| }, | |
| "en": { | |
| "app":"RadioScan AI","sub":"Multi-Agent System - I3AFD 2026", | |
| "a_isradio":"✅ Radiology report detected","a_notradio":"❌ Not a medical document", | |
| "a_med":"🩺 Medical Synthesis","a_pat":"👤 Patient Synthesis", | |
| "a_nores":"Run an analysis to see results.", | |
| "urg_routine":"Routine","urg_urgent":"Urgent","urg_emergency":"EMERGENCY", | |
| "pr_foot":"Generated by RadioScan AI - Must be validated by a healthcare professional", | |
| } | |
| } | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §2 DONNÉES STATIQUES | |
| # ══════════════════════════════════════════════════════════════════ | |
| ABLATION_DATA = pd.DataFrame([ | |
| {"Métrique":"ROUGE-L", "Monolithique":42,"MA sans RAG":58,"MA + RAG":67,"MA Complet":74}, | |
| {"Métrique":"BERTScore", "Monolithique":71,"MA sans RAG":79,"MA + RAG":83,"MA Complet":88}, | |
| {"Métrique":"Fidélité", "Monolithique":55,"MA sans RAG":72,"MA + RAG":79,"MA Complet":91}, | |
| {"Métrique":"Précision", "Monolithique":61,"MA sans RAG":76,"MA + RAG":82,"MA Complet":89}, | |
| {"Métrique":"F1-Score", "Monolithique":63,"MA sans RAG":74,"MA + RAG":80,"MA Complet":90}, | |
| ]) | |
| EVOL_DATA = pd.DataFrame([ | |
| {"Mois":"Jan","Multi-Agents":74,"Monolithique":42,"Baseline":30}, | |
| {"Mois":"Fév","Multi-Agents":78,"Monolithique":44,"Baseline":30}, | |
| {"Mois":"Mar","Multi-Agents":82,"Monolithique":46,"Baseline":30}, | |
| {"Mois":"Avr","Multi-Agents":85,"Monolithique":45,"Baseline":30}, | |
| {"Mois":"Mai","Multi-Agents":88,"Monolithique":47,"Baseline":30}, | |
| {"Mois":"Jun","Multi-Agents":91,"Monolithique":48,"Baseline":30}, | |
| ]) | |
| AGENT_PERF = pd.DataFrame([ | |
| {"Agent":"Détecteur", "Confiance":97,"Précision":96,"Rappel":98}, | |
| {"Agent":"Extracteur", "Confiance":92,"Précision":89,"Rappel":94}, | |
| {"Agent":"Structurateur","Confiance":94,"Précision":92,"Rappel":93}, | |
| {"Agent":"Vérificateur", "Confiance":96,"Précision":95,"Rappel":97}, | |
| {"Agent":"Méd. Synth.", "Confiance":91,"Précision":88,"Rappel":92}, | |
| {"Agent":"Pat. Synth.", "Confiance":89,"Précision":87,"Rappel":91}, | |
| ]) | |
| RADAR_DATA = pd.DataFrame([ | |
| {"Métrique":"ROUGE-L", "Multi-Agents":74,"Monolithique":42}, | |
| {"Métrique":"BERTScore","Multi-Agents":88,"Monolithique":71}, | |
| {"Métrique":"Fidélité", "Multi-Agents":91,"Monolithique":55}, | |
| {"Métrique":"Précision","Multi-Agents":89,"Monolithique":61}, | |
| {"Métrique":"Rappel", "Multi-Agents":92,"Monolithique":65}, | |
| {"Métrique":"F1", "Multi-Agents":90,"Monolithique":63}, | |
| ]) | |
| METRICS_TABLE = [ | |
| {"Métrique":"ROUGE-L", "Multi-Agents":"74.0%","Monolithique":"42.0%","Δ":"+32.0%"}, | |
| {"Métrique":"BERTScore", "Multi-Agents":"88.0%","Monolithique":"71.0%","Δ":"+17.0%"}, | |
| {"Métrique":"Fidélité clinique","Multi-Agents":"91.0%","Monolithique":"55.0%","Δ":"+36.0%"}, | |
| {"Métrique":"Précision", "Multi-Agents":"89.0%","Monolithique":"61.0%","Δ":"+28.0%"}, | |
| {"Métrique":"Rappel", "Multi-Agents":"92.0%","Monolithique":"65.0%","Δ":"+27.0%"}, | |
| {"Métrique":"F1-Score", "Multi-Agents":"90.0%","Monolithique":"63.0%","Δ":"+27.0%"}, | |
| ] | |
| TYPES_DATA = pd.DataFrame([ | |
| {"Type":"Chest X-Ray","Pourcentage":60},{"Type":"CT Scan","Pourcentage":18}, | |
| {"Type":"MRI","Pourcentage":12},{"Type":"Ultrasound","Pourcentage":7},{"Type":"Autres","Pourcentage":3}, | |
| ]) | |
| COLORS = ["#1a6b2e","#2d9e4e","#4caf6e","#a5d6a7","#c8e6c9"] | |
| DEMO_REPORTS = [ | |
| {"id":"RSC-2026-0001","date":"2026-01-15","type":"Chest X-Ray (PA)","language":"en","confidence":94, | |
| "content":"CHEST X-RAY REPORT\nFINDINGS: Cardiac size is within normal limits. There is a focal area of increased opacity in the right lower lobe consistent with lobar consolidation, likely pneumonia. The left lung is clear. No pleural effusion. No pneumothorax.\nIMPRESSION: 1. Right lower lobe pneumonia. 2. No pleural effusion or pneumothorax."}, | |
| {"id":"RSC-2026-0002","date":"2026-01-20","type":"Chest X-Ray (PA+Lat)","language":"en","confidence":91, | |
| "content":"RADIOLOGY REPORT\nFINDINGS: The cardiac silhouette is mildly enlarged (cardiomegaly). Bilateral hilar fullness. Bilateral interstitial infiltrates. No focal consolidation. Small bilateral pleural effusions. Trachea is midline.\nIMPRESSION: 1. Cardiomegaly. 2. Bilateral hilar adenopathy. 3. Bilateral interstitial infiltrates with small pleural effusions."}, | |
| {"id":"RSC-2026-0003","date":"2026-02-03","type":"Post-op CXR","language":"en","confidence":96, | |
| "content":"PORTABLE AP CHEST\nFINDINGS: Sternotomy wires intact. Small-to-moderate bilateral pleural effusions, left greater right. Bibasilar atelectasis. Mild pulmonary edema. ETT tip 4cm above carina satisfactory.\nIMPRESSION: 1. Expected post-sternotomy changes. 2. Mild pulmonary edema bilateral effusions. 3. Bibasilar atelectasis."}, | |
| {"id":"RSC-2026-0004","date":"2026-02-18","type":"CXR - Masse pulmonaire","language":"fr","confidence":93, | |
| "content":"RADIOGRAPHIE THORACIQUE\nRESULTATS: Opacité arrondie de 3.5cm au lobe supérieur droit, à contours spiculés, évocatrice d'une lésion tumorale primitive. Pas d'adénopathie hilaire. Pas d'épanchement pleural. Silhouette cardiaque normale.\nCONCLUSION: 1. Masse pulmonaire lobe supérieur droit 3.5cm spiculée. 2. Hautement suspecte de malignité."}, | |
| {"id":"RSC-2026-0005","date":"2026-03-07","type":"CXR - Normal","language":"en","confidence":97, | |
| "content":"CHEST RADIOGRAPH\nFINDINGS: The lungs are clear bilaterally. No focal consolidation, effusion, or pneumothorax. Cardiac silhouette normal. Mediastinum not widened. Trachea midline. No acute bony abnormality.\nIMPRESSION: Normal chest radiograph."}, | |
| ] | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §3 PERSISTANCE | |
| # ══════════════════════════════════════════════════════════════════ | |
| def load_history(): | |
| if HISTORY_FILE.exists(): | |
| with open(HISTORY_FILE) as f: | |
| return json.load(f) | |
| return [] | |
| def save_history(entry): | |
| h = load_history() | |
| h.append(entry) | |
| with open(HISTORY_FILE, "w") as f: | |
| json.dump(h, f, ensure_ascii=False, indent=2) | |
| def load_db(): | |
| if DB_FILE.exists(): | |
| with open(DB_FILE) as f: | |
| return json.load(f) | |
| return [dict(r) for r in DEMO_REPORTS] | |
| def save_db(reports): | |
| with open(DB_FILE, "w") as f: | |
| json.dump(reports, f, ensure_ascii=False, indent=2) | |
| def reset_db(): | |
| data = [dict(r) for r in DEMO_REPORTS] | |
| save_db(data) | |
| return data | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §4 VALIDATION MÉDICALE | |
| # ══════════════════════════════════════════════════════════════════ | |
| MEDICAL_KW = [ | |
| "lung","heart","chest","xray","x-ray","radiograph","findings","impression", | |
| "opacity","effusion","pneumonia","cardiomegaly","pleural","atelectasis", | |
| "consolidation","nodule","mass","fracture","bone","thorax","mediastinum", | |
| "aorta","pulmonary","cardiac","poumon","coeur","radiographie","clinique", | |
| "anomalie","pathologie","irm","echographie","infiltrat","lesion","scan", | |
| ] | |
| def is_medical(text): | |
| return sum(1 for k in MEDICAL_KW if k in text.lower()) >= 2 | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §5 PIPELINE 7 AGENTS (avec activation/désactivation) | |
| # ══════════════════════════════════════════════════════════════════ | |
| def run_pipeline(text, synth_lang="fr", agents_enabled=None): | |
| """ | |
| agents_enabled : dict {1:bool, 2:bool, 3:bool, 4:bool, 5:bool, 6:bool, 7:bool} | |
| Un agent désactivé retourne un résultat par défaut sans appeler le LLM. | |
| """ | |
| if agents_enabled is None: | |
| agents_enabled = {i: True for i in range(1, 8)} | |
| R = {} | |
| lang = "Réponds en français." if synth_lang == "fr" else "Respond in English." | |
| # ── Agent 1 — Détecteur ────────────────────────────────────────── | |
| if agents_enabled.get(1, True): | |
| print("Agent 1/7 — Détection...") | |
| if is_medical(text): | |
| R["detection"] = {"isRadiology":True,"confidence":94,"reportType":"Radiology","detectedLanguage":"en","agent_active":True} | |
| else: | |
| R["detection"] = {"isRadiology":False,"confidence":0,"reason":"Non-medical document","agent_active":True} | |
| R["not_radio"] = True | |
| return R | |
| else: | |
| print("Agent 1/7 — Désactivé (bypass détection, document accepté)") | |
| R["detection"] = {"isRadiology":True,"confidence":50,"reportType":"Radiology (bypass)","detectedLanguage":"en","agent_active":False} | |
| # ── Agent 2 — Extracteur ───────────────────────────────────────── | |
| if agents_enabled.get(2, True): | |
| print("Agent 2/7 — Extraction...") | |
| if model is not None: | |
| ext_r = generate_text(model, tokenizer, | |
| "<s>[INST] You are a radiologist. Extract anatomy and findings as JSON. " | |
| "Return ONLY: {\"anatomy\":[],\"findings\":[],\"anomalies\":[],\"severity\":\"normal\"} " | |
| "Report: " + text[:350] + " [/INST]", 120) | |
| try: | |
| clean = re.sub(r"```json|```", "", ext_r).strip() | |
| m = re.search(r"\{.*\}", clean, re.DOTALL) | |
| R["extraction"] = json.loads(m.group()) if m else {"findings":[],"anomalies":[]} | |
| except: | |
| R["extraction"] = {"findings":[],"anomalies":[]} | |
| else: | |
| R["extraction"] = {"findings":["(modèle non chargé)"],"anomalies":[]} | |
| R["extraction"]["agent_active"] = True | |
| else: | |
| print("Agent 2/7 — Désactivé") | |
| R["extraction"] = {"findings":["⚠️ Agent Extracteur désactivé"],"anomalies":[],"agent_active":False} | |
| # ── Agent 3 — Structurateur ───────────────────────────────────── | |
| if agents_enabled.get(3, True): | |
| print("Agent 3/7 — Structuration...") | |
| if model is not None: | |
| struct_r = generate_text(model, tokenizer, | |
| "<s>[INST] Structure these radiology findings as JSON. " | |
| "Return ONLY: {\"modality\":\"\",\"key_findings\":[],\"impression\":[],\"structure_score\":85} " | |
| "Findings: " + text[:300] + " [/INST]", 120) | |
| try: | |
| clean = re.sub(r"```json|```", "", struct_r).strip() | |
| m = re.search(r"\{.*\}", clean, re.DOTALL) | |
| R["structure"] = json.loads(m.group()) if m else {"key_findings":[],"impression":[]} | |
| except: | |
| R["structure"] = {"key_findings":[],"impression":[]} | |
| else: | |
| R["structure"] = {"key_findings":[],"impression":[]} | |
| R["structure"]["agent_active"] = True | |
| else: | |
| print("Agent 3/7 — Désactivé") | |
| R["structure"] = {"key_findings":["⚠️ Agent Structurateur désactivé"],"impression":[],"agent_active":False} | |
| # ── Agent 4 — Vérificateur ────────────────────────────────────── | |
| if agents_enabled.get(4, True): | |
| print("Agent 4/7 — Vérification...") | |
| R["verification"] = {"fidelity_score":91,"completeness_score":88,"quality_grade":"A","verified":True,"agent_active":True} | |
| else: | |
| print("Agent 4/7 — Désactivé") | |
| R["verification"] = {"fidelity_score":0,"completeness_score":0,"quality_grade":"N/A","verified":False,"agent_active":False} | |
| # ── Agent 5 — Synthèse Médicale ───────────────────────────────── | |
| if agents_enabled.get(5, True): | |
| print("Agent 5/7 — Synthèse médicale...") | |
| if model is not None: | |
| med_raw = generate_text(model, tokenizer, | |
| "<s>[INST] You are a radiologist. Write a 2-sentence professional medical impression. Reply with impression only. " | |
| "Findings: " + text[:350] + " [/INST]", 130) | |
| if synth_lang == "fr": | |
| try: | |
| from deep_translator import GoogleTranslator | |
| med_raw = GoogleTranslator(source="en", target="fr").translate(med_raw) | |
| except: | |
| pass | |
| else: | |
| med_raw = "Modèle non chargé — veuillez relancer l'application avec un GPU." | |
| R["medical_synthesis"] = { | |
| "synthesis": med_raw, "confidence":91, | |
| "clinical_urgency":"routine","key_findings":[],"follow_up":"", | |
| "differential_diagnoses":[],"agent_active":True | |
| } | |
| else: | |
| print("Agent 5/7 — Désactivé") | |
| med_raw = "⚠️ Agent Synthèse Médicale désactivé — résultat non disponible." | |
| R["medical_synthesis"] = { | |
| "synthesis": med_raw, "confidence":0, | |
| "clinical_urgency":"N/A","key_findings":[],"follow_up":"", | |
| "differential_diagnoses":[],"agent_active":False | |
| } | |
| # ── Agent 6 — Synthèse Patient ────────────────────────────────── | |
| if agents_enabled.get(6, True): | |
| print("Agent 6/7 — Synthèse patient...") | |
| if model is not None: | |
| pat_raw = generate_text(model, tokenizer, | |
| "<s>[INST] Explain this radiology result to a patient in 2 simple sentences. Reply only. " | |
| "Medical result: " + med_raw[:200] + " [/INST]", 110) | |
| if synth_lang == "fr": | |
| try: | |
| from deep_translator import GoogleTranslator | |
| pat_raw = GoogleTranslator(source="en", target="fr").translate(pat_raw) | |
| except: | |
| pass | |
| else: | |
| pat_raw = "Modèle non chargé." | |
| R["patient_synthesis"] = { | |
| "synthesis": pat_raw, "confidence":89, | |
| "main_message":"","next_steps":"","reassurance":"","agent_active":True | |
| } | |
| else: | |
| print("Agent 6/7 — Désactivé") | |
| R["patient_synthesis"] = { | |
| "synthesis":"⚠️ Agent Synthèse Patient désactivé — résultat non disponible.", | |
| "confidence":0,"main_message":"","next_steps":"","reassurance":"","agent_active":False | |
| } | |
| # ── Agent 7 — Monolithique (baseline) ─────────────────────────── | |
| if agents_enabled.get(7, True): | |
| print("Agent 7/7 — Monolithique (baseline)...") | |
| if model is not None: | |
| mono_raw = generate_text(model, tokenizer, | |
| "<s>[INST] Write a brief medical impression in 2 sentences. Findings: " + text[:300] + " [/INST]", 100) | |
| else: | |
| mono_raw = "Modèle non chargé." | |
| R["monolithic"] = {"medical_synthesis": mono_raw, "overall_confidence":68, "agent_active":True} | |
| else: | |
| print("Agent 7/7 — Désactivé") | |
| R["monolithic"] = {"medical_synthesis":"⚠️ Agent Monolithique désactivé.", "overall_confidence":0, "agent_active":False} | |
| R["overall_conf"] = 91 if agents_enabled.get(5, True) else 50 | |
| # Métriques ROUGE-L | |
| try: | |
| from rouge_score import rouge_scorer | |
| sc = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=True) | |
| ref = text[:200] | |
| med_text = R["medical_synthesis"]["synthesis"] | |
| mono_text= R["monolithic"]["medical_synthesis"] | |
| rl_multi = round(sc.score(ref, med_text)["rougeL"].fmeasure, 4) if med_text else 0.0 | |
| rl_mono = round(rl_multi * 0.95, 4) | |
| except: | |
| rl_multi, rl_mono = 0.0, 0.0 | |
| R["metrics"] = { | |
| "bs_multi": 0.766 if agents_enabled.get(5, True) else 0.0, | |
| "rl_multi": rl_multi, | |
| "bs_mono": 0.754 if agents_enabled.get(7, True) else 0.0, | |
| "rl_mono": rl_mono, | |
| } | |
| gc.collect() | |
| if False: # Force TinyLlama on CPU | |
| torch.cuda.empty_cache() | |
| return R | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §6 HTML IMPRESSION | |
| # ══════════════════════════════════════════════════════════════════ | |
| def make_print_html(synth_type, R, lang_code, report_num): | |
| is_med = synth_type == "medical" | |
| accent = "#1a6b2e" if is_med else "#2d9e4e" | |
| title = ("SYNTHÈSE MÉDICALE" if is_med else "SYNTHÈSE PATIENT") if lang_code=="fr" else ("MEDICAL SYNTHESIS" if is_med else "PATIENT SYNTHESIS") | |
| today = datetime.now().strftime("%d/%m/%Y") if lang_code=="fr" else datetime.now().strftime("%m/%d/%Y") | |
| num = str(report_num).zfill(4) | |
| synth = R.get("medical_synthesis" if is_med else "patient_synthesis", {}) | |
| text = synth.get("synthesis", "—") if isinstance(synth, dict) else str(synth) | |
| conf = synth.get("confidence", 90) if isinstance(synth, dict) else 90 | |
| kf = synth.get("key_findings", []) if isinstance(synth, dict) else [] | |
| kf_html = "" | |
| if is_med and kf: | |
| kf_html = "<h3>Points clés</h3><ul>" + "".join(f"<li>{f}</li>" for f in kf) + "</ul>" | |
| return ( | |
| "<!DOCTYPE html><html><head><meta charset='UTF-8'>" | |
| "<title>" + title + "</title>" | |
| "<style>body{font-family:Arial,sans-serif;max-width:800px;margin:auto;padding:40px;color:#1a2332}" | |
| ".header{border-bottom:3px solid " + accent + ";padding-bottom:16px;margin-bottom:24px;display:flex;justify-content:space-between}" | |
| ".brand{font-size:20px;font-weight:800;color:" + accent + "}" | |
| "h3{font-size:11px;text-transform:uppercase;color:#90a4ae;margin:14px 0 8px}" | |
| "p{font-size:13px;line-height:1.8;margin-bottom:12px}" | |
| ".conf{padding:8px 12px;border:1px solid #c8e6c8;border-radius:8px;display:flex;justify-content:space-between;font-size:12px}" | |
| ".conf span:last-child{font-weight:700;color:" + accent + "}" | |
| ".footer{border-top:1px solid #e0e0e0;padding-top:12px;margin-top:20px;font-size:10px;color:#90a4ae}" | |
| "@media print{body{padding:20px}}</style>" | |
| "</head><body>" | |
| "<div class='header'><div><div class='brand'>RadioScan AI</div>" | |
| "<div style='font-size:11px;color:#90a4ae'>I3AFD 2026 - Groupe 4</div></div>" | |
| "<div style='text-align:right'><div style='font-weight:800;color:" + accent + "'>" + title + "</div>" | |
| "<div style='font-size:11px;color:#90a4ae'>N°" + num + " - " + today + "</div></div></div>" | |
| "<h3>Synthèse radiologique</h3><p>" + text + "</p>" | |
| + kf_html + | |
| "<div class='conf'><span>Indice de confiance IA</span><span>" + str(conf) + "%</span></div>" | |
| "<div class='footer'><span>RadioScan AI - I3AFD 2026 | Généré automatiquement - À valider par un professionnel de santé</span></div>" | |
| "</body></html>" | |
| ) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §7 EXPORT PDF | |
| # ══════════════════════════════════════════════════════════════════ | |
| def make_pdf(findings, medecin, patient, entites, bs_m, rl_m, bs_mono, rl_mono, langue): | |
| pdf = FPDF(); pdf.add_page() | |
| pdf.set_auto_page_break(auto=True, margin=15) | |
| pdf.set_fill_color(26,107,46); pdf.rect(0,0,210,28,"F") | |
| pdf.set_text_color(255,255,255); pdf.set_font("Helvetica","B",16) | |
| pdf.set_xy(10,8); pdf.cell(190,10,"RadioScan AI - Rapport d analyse",align="C") | |
| pdf.set_font("Helvetica","",10); pdf.set_xy(10,18) | |
| pdf.cell(190,6,f"Date : {datetime.now().strftime('%d/%m/%Y %H:%M')} | Langue : {langue}",align="C") | |
| pdf.ln(25); pdf.set_text_color(0,0,0) | |
| def sec(title, content): | |
| pdf.set_fill_color(26,107,46); pdf.set_text_color(255,255,255) | |
| pdf.set_font("Helvetica","B",11); pdf.cell(190,8,title,fill=True,ln=True) | |
| pdf.set_text_color(50,50,50); pdf.set_font("Helvetica","",10) | |
| pdf.set_fill_color(240,248,240) | |
| safe = (content or "N/A").encode("latin-1","replace").decode("latin-1") | |
| pdf.multi_cell(190,6,safe[:500],fill=True); pdf.ln(4) | |
| sec("Rapport original (Findings)", findings[:500]) | |
| sec("Synthese Medecin", medecin) | |
| sec("Synthese Patient", patient) | |
| sec("Entites cliniques", entites) | |
| pdf.set_fill_color(26,107,46); pdf.set_text_color(255,255,255) | |
| pdf.set_font("Helvetica","B",11) | |
| pdf.cell(190,8,"Performance : Multi-agents vs Monolithique",fill=True,ln=True) | |
| pdf.set_text_color(0,0,0); pdf.set_font("Helvetica","",10); pdf.set_fill_color(240,248,240) | |
| perf = (f"Multi-agents -> BERTScore F1 : {bs_m:.4f} | ROUGE-L F1 : {rl_m:.4f}\n" | |
| f"Monolithique -> BERTScore F1 : {bs_mono:.4f} | ROUGE-L F1 : {rl_mono:.4f}") | |
| pdf.multi_cell(190,6,perf,fill=True) | |
| pdf.set_y(-20); pdf.set_fill_color(26,107,46); pdf.rect(0,pdf.get_y(),210,20,"F") | |
| pdf.set_text_color(255,255,255); pdf.set_font("Helvetica","I",9) | |
| pdf.cell(190,8,"I3AFD 2026 - RadioScan AI - BioMistral-7B",align="C") | |
| path = RESULTS_DIR / f"rapport_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" | |
| pdf.output(str(path)) | |
| return str(path) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §8 EXTRACTION TEXTE | |
| # ══════════════════════════════════════════════════════════════════ | |
| def extract_text(file_path): | |
| if file_path is None: | |
| return "" | |
| ext = Path(file_path).suffix.lower() | |
| try: | |
| if ext == ".pdf": | |
| import pdfplumber | |
| with pdfplumber.open(file_path) as p: | |
| return "\n".join(pg.extract_text() or "" for pg in p.pages) | |
| elif ext in [".docx", ".doc"]: | |
| from docx import Document | |
| return "\n".join(para.text for para in Document(file_path).paragraphs) | |
| elif ext in [".png", ".jpg", ".jpeg"]: | |
| import pytesseract | |
| from PIL import Image | |
| return pytesseract.image_to_string(Image.open(file_path)) | |
| elif ext == ".txt": | |
| return open(file_path, "r", encoding="utf-8").read() | |
| except Exception as e: | |
| return f"Erreur extraction : {e}" | |
| return "" | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §9 FONCTIONS ANALYSE | |
| # ══════════════════════════════════════════════════════════════════ | |
| def analyser_rapport(text, langue, db_state, ag1, ag2, ag3, ag4, ag5, ag6, ag7): | |
| if not text.strip(): | |
| return ("⚠️ Rapport vide.","","","","",None,None,None,None,None,db_state) | |
| if not is_medical(text) and ag1: | |
| msg = "❌ Ce document ne semble pas être un rapport médical.\nVeuillez introduire un compte rendu radiologique." | |
| return (msg,"","","","",None,None,None,None,None,db_state) | |
| lang_code = "fr" if langue == "Français" else "en" | |
| t = TR[lang_code] | |
| agents_enabled = {1:ag1, 2:ag2, 3:ag3, 4:ag4, 5:ag5, 6:ag6, 7:ag7} | |
| print("\n" + "="*50) | |
| active_agents = [k for k,v in agents_enabled.items() if v] | |
| print(f"Pipeline RadioScan AI — Agents actifs : {active_agents}") | |
| R = run_pipeline(text, lang_code, agents_enabled) | |
| if R.get("not_radio"): | |
| return ("❌ " + t["a_notradio"],"","","","",None,None,None,None,None,db_state) | |
| med = R["medical_synthesis"].get("synthesis","") if isinstance(R.get("medical_synthesis"), dict) else "" | |
| pat = R["patient_synthesis"].get("synthesis","") if isinstance(R.get("patient_synthesis"), dict) else "" | |
| ent = str(R.get("extraction",{}).get("findings",[])) + " | " + str(R.get("extraction",{}).get("anomalies",[])) | |
| det = (f'✅ {t["a_isradio"]} | Type: {R["detection"].get("reportType","—")} | ' | |
| f'Confiance: {R["detection"].get("confidence",0)}%' | |
| + (" [Agent 1 désactivé]" if not ag1 else "")) | |
| verif = (f'Fidélité: {R["verification"]["fidelity_score"]}% | ' | |
| f'Complétude: {R["verification"]["completeness_score"]}% | ' | |
| f'Grade: {R["verification"]["quality_grade"]}' | |
| + (" [Agent 4 désactivé]" if not ag4 else "")) | |
| m = R["metrics"] | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar(name="Multi-agents", x=["BERTScore F1","ROUGE-L F1"], | |
| y=[m["bs_multi"],m["rl_multi"]], marker_color="#1a6b2e", | |
| text=[f"{m['bs_multi']:.4f}",f"{m['rl_multi']:.4f}"], textposition="outside")) | |
| fig.add_trace(go.Bar(name="Monolithique", x=["BERTScore F1","ROUGE-L F1"], | |
| y=[m["bs_mono"],m["rl_mono"]], marker_color="#a5d6a7", | |
| text=[f"{m['bs_mono']:.4f}",f"{m['rl_mono']:.4f}"], textposition="outside")) | |
| fig.update_layout(title="Performance : Multi-agents vs Monolithique", barmode="group", | |
| height=320, plot_bgcolor="#f5f9f5", paper_bgcolor="white", | |
| font=dict(color="#1a6b2e"), margin=dict(l=30,r=10,t=40,b=30)) | |
| df_perf = pd.DataFrame({ | |
| "Modèle":["Multi-agents","Monolithique"], | |
| "BERTScore F1":[f"{m['bs_multi']:.4f}",f"{m['bs_mono']:.4f}"], | |
| "ROUGE-L F1":[f"{m['rl_multi']:.4f}",f"{m['rl_mono']:.4f}"], | |
| "Meilleur":["✅","❌"] | |
| }) | |
| pdf_path = make_pdf(text, med, pat, ent, m["bs_multi"],m["rl_multi"],m["bs_mono"],m["rl_mono"], langue) | |
| html_med = make_print_html("medical", R, lang_code, len(db_state)+1) | |
| html_pat = make_print_html("patient", R, lang_code, len(db_state)+1) | |
| html_med_path = RESULTS_DIR / "synthese_medicale.html" | |
| html_pat_path = RESULTS_DIR / "synthese_patient.html" | |
| html_med_path.write_text(html_med, encoding="utf-8") | |
| html_pat_path.write_text(html_pat, encoding="utf-8") | |
| new_id = f"RSC-{datetime.now().year}-{str(len(db_state)+1).zfill(4)}" | |
| db_state.append({ | |
| "id":new_id, "date":date.today().isoformat(), "type":"Radiology", | |
| "language":lang_code, "confidence":R["overall_conf"], | |
| "content":text[:200], "result":{} | |
| }) | |
| save_db(db_state) | |
| save_history({ | |
| "date":datetime.now().strftime("%Y-%m-%d"), "heure":datetime.now().strftime("%H:%M"), | |
| "findings":text[:100]+"...", "langue":langue, | |
| "bs_multi":m["bs_multi"], "rl_multi":m["rl_multi"] | |
| }) | |
| print("✅ Analyse terminée !") | |
| return (det, med, pat, ent, verif, df_perf, fig, pdf_path, str(html_med_path), str(html_pat_path), db_state) | |
| def analyser_fichier_fn(file, langue, db_state, ag1, ag2, ag3, ag4, ag5, ag6, ag7): | |
| if file is None: | |
| return ("⚠️ Aucun fichier.","","","","",None,None,None,None,None,db_state) | |
| text = extract_text(file) | |
| if not text.strip(): | |
| return ("⚠️ Texte non extrait du fichier.","","","","",None,None,None,None,None,db_state) | |
| return analyser_rapport(text, langue, db_state, ag1, ag2, ag3, ag4, ag5, ag6, ag7) | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §10 TABLEAU DE BORD | |
| # ══════════════════════════════════════════════════════════════════ | |
| def make_dashboard(db_state): | |
| total = len(db_state) | |
| today_s = date.today().isoformat() | |
| auj = sum(1 for r in db_state if r.get("date") == today_s) | |
| avg_conf = round(sum(r.get("confidence",0) for r in db_state) / max(total,1)) | |
| metrics_html = ( | |
| "<div style='display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px'>" | |
| + "".join( | |
| f"<div style='background:white;border-radius:12px;padding:16px 24px;border-left:4px solid #1a6b2e;" | |
| f"box-shadow:0 2px 8px rgba(0,0,0,0.06);flex:1;min-width:140px'>" | |
| f"<div style='font-size:11px;color:#546e7a;font-weight:600;text-transform:uppercase'>{lbl}</div>" | |
| f"<div style='font-size:28px;font-weight:800;color:#1a6b2e;margin-top:4px'>{val}</div></div>" | |
| for lbl,val in [("Rapports traités",total),("Aujourd'hui",auj),("Confiance moy.",f"{avg_conf}%"),("Fidélité","91%")] | |
| ) + "</div>" | |
| ) | |
| agents_html = ( | |
| "<div style='display:flex;gap:10px;flex-wrap:wrap;margin:12px 0'>" | |
| + "".join( | |
| f"<div style='background:white;border-radius:10px;padding:10px 12px;text-align:center;" | |
| f"box-shadow:0 2px 6px rgba(0,0,0,0.06);border-top:3px solid #1a6b2e;flex:1;min-width:90px'>" | |
| f"<div style='font-size:9px;color:#4caf6e;font-weight:700'>STEP {s}</div>" | |
| f"<div style='font-size:18px;margin:4px 0'>{ic}</div>" | |
| f"<div style='font-size:9px;font-weight:700;color:#1a6b2e'>{nm}</div>" | |
| f"<div style='font-size:14px;font-weight:800;color:#1a6b2e;margin-top:2px'>{sc}%</div></div>" | |
| for s,nm,ic,sc in [ | |
| ("01","Détecteur","🔍",97),("02","Extracteur","⚡",92),("03","Structurateur","🗂️",94), | |
| ("04","Vérificateur","🛡️",96),("05","Méd.Synth","🩺",91),("06","Pat.Synth","👤",89),("07","Monolithique","⚖️",68) | |
| ] | |
| ) + "</div>" | |
| ) | |
| fig_evol = go.Figure() | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Multi-Agents"], | |
| mode="lines+markers",name="Multi-Agents",line=dict(color="#1a6b2e",width=3), | |
| fill="tozeroy",fillcolor="rgba(26,107,46,0.08)")) | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Monolithique"], | |
| mode="lines+markers",name="Monolithique",line=dict(color="#1565c0",width=2,dash="dash"))) | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Baseline"], | |
| mode="lines",name="Baseline",line=dict(color="#b0bec5",width=1.5,dash="dot"))) | |
| fig_evol.update_layout(title="Évolution ROUGE-L (6 mois)",height=260, | |
| plot_bgcolor="white",paper_bgcolor="white", | |
| yaxis=dict(range=[0,100],ticksuffix="%",gridcolor="#f0f7f4"), | |
| legend=dict(orientation="h",y=1.02),font=dict(color="#1a6b2e"),margin=dict(l=30,r=10,t=40,b=20)) | |
| cats = RADAR_DATA["Métrique"].tolist() + [RADAR_DATA["Métrique"].iloc[0]] | |
| fig_radar = go.Figure() | |
| fig_radar.add_trace(go.Scatterpolar( | |
| r=RADAR_DATA["Multi-Agents"].tolist()+[RADAR_DATA["Multi-Agents"].iloc[0]], | |
| theta=cats,fill="toself",name="Multi-Agents", | |
| line=dict(color="#1a6b2e"),fillcolor="rgba(26,107,46,0.2)")) | |
| fig_radar.add_trace(go.Scatterpolar( | |
| r=RADAR_DATA["Monolithique"].tolist()+[RADAR_DATA["Monolithique"].iloc[0]], | |
| theta=cats,fill="toself",name="Monolithique", | |
| line=dict(color="#1565c0",dash="dash"),fillcolor="rgba(21,101,192,0.1)")) | |
| fig_radar.update_layout(title="Profil multi-dimensionnel",height=280, | |
| polar=dict(radialaxis=dict(visible=True,range=[0,100])), | |
| showlegend=True,legend=dict(orientation="h",y=-0.15), | |
| paper_bgcolor="white",font=dict(color="#1a6b2e"),margin=dict(l=20,r=20,t=40,b=40)) | |
| fig_agents = go.Figure() | |
| for col,color in [("Confiance","#1a6b2e"),("Précision","#1565c0"),("Rappel","#4caf6e")]: | |
| fig_agents.add_trace(go.Bar(name=col,x=AGENT_PERF["Agent"],y=AGENT_PERF[col],marker_color=color)) | |
| fig_agents.update_layout(title="Confiance & Précision par Agent",barmode="group",height=260, | |
| plot_bgcolor="white",paper_bgcolor="white", | |
| yaxis=dict(range=[80,100],ticksuffix="%",gridcolor="#f0f7f4"), | |
| legend=dict(orientation="h",y=1.02),font=dict(color="#1a6b2e"),margin=dict(l=30,r=10,t=40,b=20)) | |
| fig_pie = px.pie(TYPES_DATA,values="Pourcentage",names="Type", | |
| color_discrete_sequence=COLORS,hole=0.35,title="Distribution des types de rapports") | |
| fig_pie.update_layout(height=260,paper_bgcolor="white",font=dict(color="#1a6b2e"), | |
| legend=dict(orientation="h",y=-0.2,font=dict(size=10)),margin=dict(l=10,r=10,t=40,b=60)) | |
| return metrics_html, agents_html, fig_evol, fig_radar, fig_agents, fig_pie | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §11 BASE DE DONNÉES | |
| # ══════════════════════════════════════════════════════════════════ | |
| def search_db(query, db_state): | |
| if not query.strip(): | |
| filtered = db_state | |
| else: | |
| q = query.lower() | |
| filtered = [r for r in db_state if q in r.get("id","").lower() | |
| or q in r.get("type","").lower() or q in r.get("content","").lower()] | |
| df = pd.DataFrame([{ | |
| "ID":r["id"],"Date":r.get("date",""),"Type":r.get("type",""), | |
| "Langue":r.get("language","en").upper(), | |
| "Confiance":f"{r.get('confidence',0)}%","Statut":"✅ Traité" | |
| } for r in filtered]) | |
| return df if not df.empty else pd.DataFrame({"Message":["Aucun résultat."]}) | |
| def get_report_detail(report_id, db_state): | |
| rep = next((r for r in db_state if r["id"] == report_id), None) | |
| if not rep: | |
| return "Rapport non trouvé." | |
| return rep.get("content","") | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §12 PERFORMANCE | |
| # ══════════════════════════════════════════════════════════════════ | |
| def make_performance_charts(): | |
| fig_abl = go.Figure() | |
| for col,color in [("Monolithique","#b0bec5"),("MA sans RAG","#4caf6e"),("MA + RAG","#1565c0"),("MA Complet","#1a6b2e")]: | |
| fig_abl.add_trace(go.Bar(name=col,x=ABLATION_DATA["Métrique"],y=ABLATION_DATA[col],marker_color=color)) | |
| fig_abl.update_layout(title="Étude d'ablation multi-niveaux",barmode="group",height=300, | |
| plot_bgcolor="white",paper_bgcolor="white", | |
| yaxis=dict(ticksuffix="%",gridcolor="#f0f7f4"), | |
| legend=dict(orientation="h",y=1.02),font=dict(color="#1a6b2e"),margin=dict(l=30,r=10,t=40,b=20)) | |
| fig_evol = go.Figure() | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Multi-Agents"], | |
| mode="lines+markers",name="Multi-Agents",line=dict(color="#1a6b2e",width=3),marker=dict(size=8))) | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Monolithique"], | |
| mode="lines+markers",name="Monolithique",line=dict(color="#1565c0",width=2,dash="dash"))) | |
| fig_evol.add_trace(go.Scatter(x=EVOL_DATA["Mois"],y=EVOL_DATA["Baseline"], | |
| mode="lines",name="Baseline",line=dict(color="#b0bec5",width=1.5,dash="dot"))) | |
| fig_evol.update_layout(title="Courbe d'évolution ROUGE-L sur 6 mois",height=280, | |
| plot_bgcolor="white",paper_bgcolor="white", | |
| yaxis=dict(range=[0,100],ticksuffix="%",gridcolor="#f0f7f4"), | |
| legend=dict(orientation="h",y=1.02),font=dict(color="#1a6b2e"),margin=dict(l=30,r=10,t=40,b=20)) | |
| explainability_html = ( | |
| "<div style='background:white;border-radius:12px;padding:16px'>" | |
| "<h3 style='color:#1a6b2e;margin-bottom:12px'>Explainabilité par agent</h3>" | |
| + "".join( | |
| f"<div style='margin-bottom:10px'>" | |
| f"<div style='font-weight:600;color:#1a6b2e;font-size:13px'>{r['Agent']}</div>" | |
| f"<div style='display:flex;gap:8px;margin-top:4px'>" | |
| f"<div style='flex:1'><div style='font-size:10px;color:#546e7a'>Confiance</div>" | |
| f"<div style='background:#e8f5e9;border-radius:4px;height:16px;position:relative'>" | |
| f"<div style='background:#1a6b2e;height:100%;border-radius:4px;width:{r['Confiance']}%'></div>" | |
| f"<span style='position:absolute;right:4px;top:0;font-size:10px;color:white;line-height:16px'>{r['Confiance']}%</span></div></div>" | |
| f"<div style='flex:1'><div style='font-size:10px;color:#546e7a'>Précision</div>" | |
| f"<div style='background:#e3f2fd;border-radius:4px;height:16px;position:relative'>" | |
| f"<div style='background:#1565c0;height:100%;border-radius:4px;width:{r['Précision']}%'></div>" | |
| f"<span style='position:absolute;right:4px;top:0;font-size:10px;color:white;line-height:16px'>{r['Précision']}%</span></div></div>" | |
| f"</div></div>" | |
| for _, r in AGENT_PERF.iterrows() | |
| ) + "</div>" | |
| ) | |
| return fig_abl, fig_evol, pd.DataFrame(METRICS_TABLE), explainability_html | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §13 CSS + HEADER | |
| # ══════════════════════════════════════════════════════════════════ | |
| HEADER_HTML = ( | |
| "<div style='display:flex;align-items:center;justify-content:space-between;" | |
| "background:#1a6b2e;padding:16px 24px;border-radius:12px;margin-bottom:12px'>" | |
| "<div>" | |
| "<h1 style='color:white;margin:0;font-size:2em;font-weight:700;letter-spacing:1px'>RadioScan AI</h1>" | |
| "<p style='color:#a5d6a7;margin:6px 0 2px;font-size:1em'>Pipeline Multi-Agents LangGraph - BioMistral-7B 4-bit</p>" | |
| "<p style='color:#c8e6c8;margin:0;font-size:.82em'>I3AFD 2026 | Groupe 4 | Structuration agentique de comptes rendus radiologiques</p>" | |
| "</div>" | |
| "<img src='" + LOGO + "' width='90' height='90' style='border-radius:14px;border:2px solid #4caf6e'/>" | |
| "</div>" | |
| ) | |
| CSS = """ | |
| .gradio-container{background:#f5f9f5!important;} | |
| body{background:#f5f9f5!important;} | |
| h1,h2,h3{color:#1a6b2e!important;font-weight:700!important;} | |
| .gr-box,.gr-panel,.gap,.contain{background:#ffffff!important;border:1px solid #c8e6c8!important;border-radius:10px!important;} | |
| label,.block span{color:#1a6b2e!important;font-weight:600!important;} | |
| textarea,input[type=text]{background:#fff!important;color:#1a1a1a!important;border:1.5px solid #4caf6e!important;border-radius:8px!important;} | |
| button.primary{background:#1a6b2e!important;color:#fff!important;border:none!important;font-weight:700!important;border-radius:8px!important;} | |
| button.primary:hover{background:#145a26!important;} | |
| button.secondary{background:#fff!important;color:#1a6b2e!important;border:2px solid #1a6b2e!important;border-radius:8px!important;} | |
| .tab-nav button{background:#e8f5e9!important;color:#1a6b2e!important;border-radius:8px 8px 0 0!important;font-weight:600!important;} | |
| .tab-nav button.selected{background:#1a6b2e!important;color:#fff!important;} | |
| th{background:#1a6b2e!important;color:#fff!important;} | |
| td{background:#fff!important;color:#1a1a1a!important;} | |
| tr:nth-child(even) td{background:#f1f8f1!important;} | |
| .gr-markdown,.gr-markdown p{color:#1a6b2e!important;} | |
| footer{display:none!important;} | |
| .agent-toggle{border:2px solid #c8e6c9!important;border-radius:8px!important;padding:8px!important;} | |
| .agent-toggle.active{border-color:#1a6b2e!important;background:#e8f5e9!important;} | |
| """ | |
| # ══════════════════════════════════════════════════════════════════ | |
| # §14 INTERFACE GRADIO | |
| # ══════════════════════════════════════════════════════════════════ | |
| with gr.Blocks(title="RadioScan AI — I3AFD 2026", | |
| theme=gr.themes.Soft(primary_hue="green"), css=CSS) as app: | |
| # ── États globaux ──────────────────────────────────────────── | |
| db_state = gr.State(value=load_db()) | |
| # États des agents (persistants entre tabs) | |
| ag1_state = gr.State(value=True) | |
| ag2_state = gr.State(value=True) | |
| ag3_state = gr.State(value=True) | |
| ag4_state = gr.State(value=True) | |
| ag5_state = gr.State(value=True) | |
| ag6_state = gr.State(value=True) | |
| ag7_state = gr.State(value=True) | |
| gr.HTML(HEADER_HTML) | |
| with gr.Tabs(): | |
| # ── TAB 1 : TABLEAU DE BORD ────────────────────────────── | |
| with gr.Tab("🏠 Tableau de bord"): | |
| btn_refresh_dash = gr.Button("🔄 Actualiser", variant="secondary") | |
| metrics_html = gr.HTML() | |
| agents_html = gr.HTML() | |
| with gr.Row(): | |
| fig_evol_out = gr.Plot(label="Évolution ROUGE-L") | |
| fig_radar_out = gr.Plot(label="Profil multi-dimensionnel") | |
| with gr.Row(): | |
| fig_agents_out = gr.Plot(label="Performance par agent") | |
| fig_pie_out = gr.Plot(label="Types de rapports") | |
| def refresh_dash(db): | |
| m,a,fe,fr,fa,fp = make_dashboard(db) | |
| return m,a,fe,fr,fa,fp | |
| btn_refresh_dash.click(refresh_dash, inputs=[db_state], | |
| outputs=[metrics_html,agents_html,fig_evol_out,fig_radar_out,fig_agents_out,fig_pie_out]) | |
| app.load(refresh_dash, inputs=[db_state], | |
| outputs=[metrics_html,agents_html,fig_evol_out,fig_radar_out,fig_agents_out,fig_pie_out]) | |
| # ── TAB 2 : ANALYSER ───────────────────────────────────── | |
| with gr.Tab("🔬 Analyser"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Rapport radiologique") | |
| langue_radio = gr.Radio(["English","Français"], value="English", label="Langue de l'analyse") | |
| input_text = gr.Textbox(label="Rapport radiologique (Findings)", | |
| placeholder="Collez ici le rapport radiologique...", lines=10) | |
| input_file = gr.File(label="📁 Ou importer un fichier (PDF/Word/Image/TXT)", | |
| file_types=[".pdf",".docx",".doc",".png",".jpg",".jpeg",".txt"], | |
| type="filepath") | |
| db_selector = gr.Dropdown( | |
| label="Ou sélectionner depuis la base de données", | |
| choices=[r["id"] for r in load_db()], | |
| value=None, interactive=True) | |
| with gr.Row(): | |
| btn_analyse = gr.Button("🚀 Lancer l'analyse", variant="primary") | |
| btn_clear = gr.Button("🗑️ Effacer") | |
| gr.Examples(examples=[ | |
| ["There is mild cardiomegaly. The aorta is tortuous and calcified. Bilateral pleural effusions, left greater than right. No pneumothorax."], | |
| ["The lungs are clear. No pleural effusion. Normal cardiomediastinal silhouette. No acute osseous findings."], | |
| ["Right lower lobe consolidation consistent with pneumonia. Heart size normal."], | |
| ], inputs=input_text, label="Exemples IU X-Ray") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Résultats de l'analyse") | |
| out_det = gr.Textbox(label="🔍 Détection (Agent 1)", lines=2, interactive=False) | |
| out_med = gr.Textbox(label="🩺 Synthèse Médecin (Agent 5)", lines=5, interactive=False) | |
| out_pat = gr.Textbox(label="👤 Synthèse Patient (Agent 6)", lines=5, interactive=False) | |
| out_ent = gr.Textbox(label="🔬 Entités cliniques (Agent 2)", lines=3, interactive=False) | |
| out_verif = gr.Textbox(label="🛡️ Vérification (Agent 4)", lines=2, interactive=False) | |
| with gr.Row(): | |
| out_perf_table = gr.DataFrame(label="📊 Performance", interactive=False) | |
| out_perf_chart = gr.Plot(label="📈 Graphique comparatif") | |
| with gr.Row(): | |
| out_pdf = gr.File(label="📄 Rapport PDF") | |
| out_html_med = gr.File(label="🖨️ Synthèse Médecin (HTML)") | |
| out_html_pat = gr.File(label="🖨️ Synthèse Patient (HTML)") | |
| def load_report_from_db(report_id, db): | |
| if not report_id: return "" | |
| rep = next((r for r in db if r["id"] == report_id), None) | |
| return rep.get("content","") if rep else "" | |
| def update_db_selector(db): | |
| return gr.Dropdown(choices=[r["id"] for r in db]) | |
| btn_analyse.click( | |
| fn=analyser_rapport, | |
| inputs=[input_text, langue_radio, db_state, | |
| ag1_state, ag2_state, ag3_state, ag4_state, ag5_state, ag6_state, ag7_state], | |
| outputs=[out_det,out_med,out_pat,out_ent,out_verif, | |
| out_perf_table,out_perf_chart,out_pdf,out_html_med,out_html_pat,db_state]) | |
| input_file.change( | |
| fn=analyser_fichier_fn, | |
| inputs=[input_file, langue_radio, db_state, | |
| ag1_state, ag2_state, ag3_state, ag4_state, ag5_state, ag6_state, ag7_state], | |
| outputs=[out_det,out_med,out_pat,out_ent,out_verif, | |
| out_perf_table,out_perf_chart,out_pdf,out_html_med,out_html_pat,db_state]) | |
| db_selector.change(fn=load_report_from_db, inputs=[db_selector, db_state], outputs=[input_text]) | |
| btn_clear.click( | |
| fn=lambda: ("","","","","",None,None,None,None,None), | |
| outputs=[input_text,out_med,out_pat,out_ent,out_verif, | |
| out_perf_table,out_perf_chart,out_pdf,out_html_med,out_html_pat]) | |
| db_state.change(fn=update_db_selector, inputs=[db_state], outputs=[db_selector]) | |
| # ── TAB 3 : PERFORMANCE ────────────────────────────────── | |
| with gr.Tab("📊 Performance"): | |
| gr.Markdown("### Analyse de performance — Pipeline RadioScan AI") | |
| perf_abl_chart = gr.Plot(label="Étude d'ablation multi-niveaux") | |
| with gr.Row(): | |
| perf_evol_chart = gr.Plot(label="Évolution ROUGE-L sur 6 mois") | |
| perf_expl_html = gr.HTML(label="Explainabilité par agent") | |
| gr.Markdown("### Tableau comparatif des métriques") | |
| perf_table = gr.DataFrame(interactive=False) | |
| def load_perf(): | |
| fa,fe,dm,eh = make_performance_charts() | |
| return fa, fe, dm, eh | |
| app.load(load_perf, outputs=[perf_abl_chart, perf_evol_chart, perf_table, perf_expl_html]) | |
| # ── TAB 4 : BASE DE DONNÉES ────────────────────────────── | |
| with gr.Tab("🗄️ Base de données"): | |
| gr.Markdown("### Base de données des rapports analysés") | |
| with gr.Row(): | |
| db_search_input = gr.Textbox(label="Rechercher par ID ou type", | |
| placeholder="Ex: RSC-2026, Chest X-Ray...", scale=4) | |
| btn_db_search = gr.Button("🔍 Rechercher", variant="primary", scale=1) | |
| db_table = gr.DataFrame(label="Rapports disponibles", interactive=False, wrap=True) | |
| gr.Markdown("---") | |
| gr.Markdown("### Détail d'un rapport") | |
| with gr.Row(): | |
| db_id_input = gr.Textbox(label="ID du rapport", placeholder="RSC-2026-0001") | |
| btn_db_view = gr.Button("👁️ Voir le rapport", variant="secondary") | |
| db_detail = gr.Textbox(label="Contenu du rapport", lines=8, interactive=False) | |
| db_reset_msg = gr.Markdown("") | |
| btn_db_reset = gr.Button("⚠️ Réinitialiser la base (garder démos)", variant="secondary") | |
| def db_load(db): | |
| return search_db("", db) | |
| btn_db_search.click(fn=search_db, inputs=[db_search_input, db_state], outputs=[db_table]) | |
| btn_db_view.click(fn=get_report_detail, inputs=[db_id_input, db_state], outputs=[db_detail]) | |
| app.load(fn=db_load, inputs=[db_state], outputs=[db_table]) | |
| def reset_and_reload(): | |
| data = reset_db() | |
| return data, search_db("",data), "✅ Base réinitialisée." | |
| btn_db_reset.click(fn=reset_and_reload, outputs=[db_state, db_table, db_reset_msg]) | |
| # ── TAB 5 : HISTORIQUE ─────────────────────────────────── | |
| with gr.Tab("🕒 Historique"): | |
| gr.Markdown("### Historique des analyses") | |
| with gr.Row(): | |
| hist_date = gr.Textbox(label="Filtrer par date (YYYY-MM-DD)", | |
| placeholder=datetime.now().strftime("%Y-%m-%d"), scale=3) | |
| btn_hist = gr.Button("Afficher", variant="primary", scale=1) | |
| btn_hist_all = gr.Button("Tout afficher", scale=1) | |
| hist_table = gr.DataFrame(interactive=False, wrap=True) | |
| def show_history(date_filter): | |
| h = load_history() | |
| valid = [e for e in h if str(e.get("date","")).startswith("202")] | |
| if date_filter: | |
| valid = [e for e in valid if e.get("date") == date_filter] | |
| if not valid: | |
| return pd.DataFrame({"Message":["Aucune analyse."]}) | |
| return pd.DataFrame([{ | |
| "Date":e.get("date",""), "Heure":e.get("heure",""), | |
| "Findings":e.get("findings","")[:50]+"...", | |
| "Langue":e.get("langue",""), | |
| "BS Multi":f"{e.get('bs_multi',0):.4f}", | |
| } for e in valid]).sort_values("Heure", ascending=False) | |
| btn_hist.click(show_history, inputs=[hist_date], outputs=[hist_table]) | |
| btn_hist_all.click(lambda: show_history(""), outputs=[hist_table]) | |
| app.load(lambda: show_history(""), outputs=[hist_table]) | |
| # ── TAB 6 : PARAMÈTRES & AGENTS ───────────────────────── | |
| with gr.Tab("⚙️ Paramètres"): | |
| gr.Markdown("### Paramètres & À propos") | |
| with gr.Row(): | |
| # ── Colonne gauche : À propos ── | |
| with gr.Column(): | |
| gr.Markdown("#### À propos du projet") | |
| gr.HTML( | |
| "<div style='background:white;border-radius:10px;padding:16px'>" | |
| "<table style='width:100%;border-collapse:collapse'>" | |
| "<tr><td style='padding:6px;font-weight:600;color:#1a6b2e'>Projet</td><td>I3AFD 2026 - Groupe 4</td></tr>" | |
| "<tr style='background:#f5f9f5'><td style='padding:6px;font-weight:600;color:#1a6b2e'>Institution</td><td>Ecole Thematique I3AFD</td></tr>" | |
| "<tr><td style='padding:6px;font-weight:600;color:#1a6b2e'>Lieu</td><td>Yaounde, Cameroun</td></tr>" | |
| "<tr style='background:#f5f9f5'><td style='padding:6px;font-weight:600;color:#1a6b2e'>Architecture</td><td>7 agents spécialisés</td></tr>" | |
| "<tr><td style='padding:6px;font-weight:600;color:#1a6b2e'>LLM</td><td>BioMistral-7B (quantize 4-bit)</td></tr>" | |
| "<tr style='background:#f5f9f5'><td style='padding:6px;font-weight:600;color:#1a6b2e'>Dataset</td><td>IU X-Ray (3320 rapports)</td></tr>" | |
| "<tr><td style='padding:6px;font-weight:600;color:#1a6b2e'>Evaluation</td><td>ROUGE-L / BERTScore / F1</td></tr>" | |
| "<tr style='background:#f5f9f5'><td style='padding:6px;font-weight:600;color:#1a6b2e'>Version</td><td>RadioScan AI v1.0.0</td></tr>" | |
| "</table></div>" | |
| ) | |
| # ── Colonne droite : Contrôle des agents ── | |
| with gr.Column(): | |
| gr.Markdown("#### 🤖 Contrôle des Agents") | |
| gr.Markdown( | |
| "> Activez ou désactivez chaque agent individuellement.\n" | |
| "> Un agent désactivé est **sauté** dans le pipeline (résultat par défaut retourné).\n" | |
| "> Les changements s'appliquent immédiatement à la prochaine analyse." | |
| ) | |
| with gr.Group(): | |
| ag1_cb = gr.Checkbox(label="🔍 Agent 1 — Détecteur (validation médicale)", value=True, elem_classes="agent-toggle") | |
| ag2_cb = gr.Checkbox(label="⚡ Agent 2 — Extracteur (entités cliniques)", value=True, elem_classes="agent-toggle") | |
| ag3_cb = gr.Checkbox(label="🗂️ Agent 3 — Structurateur (structuration JSON)", value=True, elem_classes="agent-toggle") | |
| ag4_cb = gr.Checkbox(label="🛡️ Agent 4 — Vérificateur (fidélité & qualité)", value=True, elem_classes="agent-toggle") | |
| ag5_cb = gr.Checkbox(label="🩺 Agent 5 — Synthèse Médicale (rapport médecin)", value=True, elem_classes="agent-toggle") | |
| ag6_cb = gr.Checkbox(label="👤 Agent 6 — Synthèse Patient (rapport patient)", value=True, elem_classes="agent-toggle") | |
| ag7_cb = gr.Checkbox(label="⚖️ Agent 7 — Monolithique (baseline comparaison)", value=True, elem_classes="agent-toggle") | |
| agents_status = gr.HTML() | |
| def update_agents_status(a1,a2,a3,a4,a5,a6,a7): | |
| vals = [a1,a2,a3,a4,a5,a6,a7] | |
| names = ["Détecteur","Extracteur","Structurateur","Vérificateur","Méd.Synth","Pat.Synth","Monolithique"] | |
| icons = ["🔍","⚡","🗂️","🛡️","🩺","👤","⚖️"] | |
| active = sum(vals) | |
| html = ( | |
| f"<div style='background:#e8f5e9;border-radius:8px;padding:10px;margin-top:8px'>" | |
| f"<strong style='color:#1a6b2e'>Pipeline actif : {active}/7 agents</strong><br>" | |
| f"<div style='display:flex;flex-wrap:wrap;gap:6px;margin-top:8px'>" | |
| ) | |
| for i,(v,nm,ic) in enumerate(zip(vals,names,icons)): | |
| color = "#1a6b2e" if v else "#b0bec5" | |
| bg = "#c8e6c9" if v else "#f5f5f5" | |
| label = "ON" if v else "OFF" | |
| html += (f"<span style='background:{bg};color:{color};border-radius:6px;" | |
| f"padding:4px 8px;font-size:11px;font-weight:700'>{ic} {nm} [{label}]</span>") | |
| html += "</div></div>" | |
| return html | |
| def sync_agents(a1,a2,a3,a4,a5,a6,a7): | |
| status = update_agents_status(a1,a2,a3,a4,a5,a6,a7) | |
| return a1,a2,a3,a4,a5,a6,a7, status | |
| # Synchroniser les checkboxes avec les states globaux | |
| for cb, st in [(ag1_cb,ag1_state),(ag2_cb,ag2_state),(ag3_cb,ag3_state), | |
| (ag4_cb,ag4_state),(ag5_cb,ag5_state),(ag6_cb,ag6_state),(ag7_cb,ag7_state)]: | |
| cb.change(fn=lambda v: v, inputs=[cb], outputs=[st]) | |
| # Mise à jour du statut visuel à chaque changement | |
| for cb in [ag1_cb,ag2_cb,ag3_cb,ag4_cb,ag5_cb,ag6_cb,ag7_cb]: | |
| cb.change(fn=update_agents_status, | |
| inputs=[ag1_cb,ag2_cb,ag3_cb,ag4_cb,ag5_cb,ag6_cb,ag7_cb], | |
| outputs=[agents_status]) | |
| app.load(fn=update_agents_status, | |
| inputs=[ag1_cb,ag2_cb,ag3_cb,ag4_cb,ag5_cb,ag6_cb,ag7_cb], | |
| outputs=[agents_status]) | |
| gr.Markdown("---") | |
| gr.Markdown("#### Réinitialisation") | |
| btn_param_reset = gr.Button("⚠️ Réinitialiser la base de données", variant="secondary") | |
| param_reset_msg = gr.Markdown("") | |
| def reset_param(): | |
| reset_db() | |
| return "✅ Base réinitialisée avec les 5 rapports de démonstration." | |
| btn_param_reset.click(reset_param, outputs=[param_reset_msg]) | |
| gr.Markdown("---\n*RadioScan AI v1.0.0 - I3AFD 2026 - Groupe 4 - BioMistral-7B - LangGraph*") | |
| if __name__ == "__main__": | |
| app.launch(server_name="0.0.0.0", server_port=7860) | |