import gradio as gr from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.enums import TA_CENTER import datetime AUTHOR_NAME = "prof. dr hab. n. med. Rafał Obuchowicz" AUTHOR_AFFILIATION = "Luxmed Diagnostyka" def yes_no(value: bool) -> str: return "Tak" if value else "Nie" def validate_form( first_episode, recurrent_episode, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening ): warnings = [] if first_episode and recurrent_episode: warnings.append("Zaznaczono jednocześnie: 'Pierwszy epizod' i 'Nawrót / zaostrzenie przewlekłe'.") neuro_flags = [no_deficit, root_deficit, multilevel_bilateral, cord_signs] if no_deficit and any([root_deficit, multilevel_bilateral, cord_signs]): warnings.append("Zaznaczono jednocześnie 'Brak deficytu' i obecność deficytu neurologicznego.") treatment_flags = [conservative_none, conservative_lt6, conservative_ge6] if sum(treatment_flags) > 1: warnings.append("W sekcji leczenia zachowawczego wybrano więcej niż jedną sprzeczną opcję czasową.") response_flags = [improved, no_improvement, worsening] if sum(response_flags) > 1: warnings.append("W sekcji odpowiedzi na leczenie zaznaczono więcej niż jedną sprzeczną opcję.") return warnings def build_recommendation( spine_section, duration, first_episode, recurrent_episode, post_op, axial_pain, radicular_pain, paresthesia, weakness, gait_balance, sphincter, saddle, progressive_deficit, myelopathy, cancer_history, weight_loss, night_pain, fever, immunosuppression, infection_suspicion, trauma_high, trauma_low_risk, osteoporosis_steroids, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening, suspect_disc, suspect_cord, suspect_tumor, suspect_infection, qualify_surgery, qualify_injection, treatment_impact, ): reasons = [] alternatives = [] urgency = "routine" contrast = "nie" imaging = "Brak wskazań do MR na tym etapie" category = "🟢 BRAK WSKAZAŃ DO MR" clinical_path = "Leczenie zachowawcze / obserwacja" if sphincter or saddle or progressive_deficit or myelopathy or cord_signs: category = "🔴 MR PILNE" imaging = f"Pilne MR odcinka {spine_section}" clinical_path = "Pilna konsultacja specjalistyczna / tryb pilny" urgency = "pilne" reasons.append("Stwierdzono objawy alarmowe neurologiczne") if sphincter: reasons.append("Zaburzenia zwieraczy") if saddle: reasons.append("Znieczulenie okolicy krocza") if progressive_deficit: reasons.append("Postępujący deficyt neurologiczny") if myelopathy or cord_signs: reasons.append("Objawy mielopatii / uszkodzenia rdzenia") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#b91c1c", } if ( cancer_history or weight_loss or night_pain or fever or immunosuppression or infection_suspicion or suspect_tumor or suspect_infection or (post_op and (worsening or weakness or paresthesia or radicular_pain)) ): category = "🟠 MR PRZYSPIESZONE" imaging = f"MR odcinka {spine_section}" clinical_path = "Przyspieszona diagnostyka specjalistyczna" urgency = "przyspieszone" contrast = "rozważyć" reasons.append("Występują przesłanki nowotworowe, infekcyjne lub pooperacyjne") if cancer_history: reasons.append("Wywiad nowotworowy") if weight_loss: reasons.append("Niezamierzona utrata masy ciała") if night_pain: reasons.append("Ból nocny / niemechaniczny") if fever: reasons.append("Gorączka") if immunosuppression: reasons.append("Immunosupresja") if infection_suspicion: reasons.append("Podejrzenie infekcji") if post_op: reasons.append("Stan po operacji kręgosłupa") alternatives.append("Rozważyć badania laboratoryjne: CRP, OB, morfologia") if trauma_high or trauma_low_risk: alternatives.append("Jeśli współistnieje uraz: rozważyć RTG/TK struktur kostnych") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#ea580c", } if trauma_high or trauma_low_risk or osteoporosis_steroids: category = "🔵 RTG / TK NAJPIERW" imaging = f"RTG lub TK odcinka {spine_section}" clinical_path = "Najpierw diagnostyka kostna; MR warunkowo" urgency = "pilne" if trauma_high else "routine" reasons.append("Dominują wskazania urazowe / kostne") if trauma_high: reasons.append("Uraz wysokiej energii") if trauma_low_risk: reasons.append("Uraz niskoenergetyczny z ryzykiem") if osteoporosis_steroids: reasons.append("Osteoporoza / przewlekła steroidoterapia") alternatives.append("MR rozważyć przy deficycie neurologicznym lub podejrzeniu uszkodzeń tkanek miękkich") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#2563eb", } radicular_pattern = radicular_pain or paresthesia or weakness or root_deficit or multilevel_bilateral if ( radicular_pattern and conservative_ge6 and (no_improvement or worsening) and treatment_impact == "TAK" ): category = "🟡 MR PLANOWE" imaging = f"MR odcinka {spine_section} bez kontrastu" clinical_path = "Planowa diagnostyka obrazowa" urgency = "planowe" reasons.append("Objawy korzeniowe / neurologiczne utrzymują się mimo leczenia") reasons.append("Leczenie zachowawcze ≥ 6 tygodni") if no_improvement: reasons.append("Brak poprawy") if worsening: reasons.append("Pogorszenie") reasons.append("Wynik badania może wpłynąć na leczenie") if qualify_surgery: reasons.append("Pacjent potencjalnie kwalifikowany do zabiegu") if qualify_injection: reasons.append("Pacjent potencjalnie kwalifikowany do blokady / iniekcji") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#ca8a04", } if ( axial_pain and not radicular_pattern and not weakness and not gait_balance and not conservative_ge6 and treatment_impact != "TAK" ): category = "🟢 BRAK WSKAZAŃ DO MR" imaging = "Brak wskazań do MR na tym etapie" clinical_path = "Leczenie zachowawcze / obserwacja / kontrola" urgency = "routine" reasons.append("Dominują objawy bólu osiowego bez czerwonych flag") reasons.append("Brak przesłanek do pilnej diagnostyki obrazowej") alternatives.append("Kontynuacja leczenia zachowawczego") alternatives.append("Rozważyć fizjoterapię / farmakoterapię / kontrolę kliniczną") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#15803d", } category = "🟡 WYMAGA DOPRECYZOWANIA" imaging = "Rozważyć MR po uzupełnieniu oceny klinicznej" clinical_path = "Uzupełnić wywiad / badanie / odpowiedź na leczenie" urgency = "routine" reasons.append("Dane nie są jednoznaczne dla automatycznej kwalifikacji") if treatment_impact == "NIEPEWNE": reasons.append("Niepewny wpływ wyniku MR na decyzję terapeutyczną") alternatives.append("Zweryfikować czas leczenia zachowawczego") alternatives.append("Uściślić deficyty neurologiczne i pytanie kliniczne") return { "category": category, "imaging": imaging, "clinical_path": clinical_path, "urgency": urgency, "contrast": contrast, "reasons": reasons, "alternatives": alternatives, "color": "#a16207", } def generate_pdf(data, result, warnings): filename = "raport_mr_kregoslup_pro_plus.pdf" doc = SimpleDocTemplate(filename, pagesize=A4, rightMargin=36, leftMargin=36, topMargin=36, bottomMargin=36) styles = getSampleStyleSheet() title_style = ParagraphStyle( "CustomTitle", parent=styles["Title"], alignment=TA_CENTER, fontSize=18, spaceAfter=10 ) subtitle_style = ParagraphStyle( "CustomSubtitle", parent=styles["Normal"], alignment=TA_CENTER, fontSize=10, textColor=colors.grey ) story = [] story.append(Paragraph("Raport kwalifikacji do badania MR kręgosłupa", title_style)) story.append(Paragraph("Clinical Decision Support – PRO+", subtitle_style)) story.append(Spacer(1, 8)) story.append(Paragraph(f"Autor narzędzia: {AUTHOR_NAME}", styles["Normal"])) story.append(Paragraph(f"Afiliacja: {AUTHOR_AFFILIATION}", styles["Normal"])) story.append(Paragraph(f"Data wygenerowania: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", styles["Normal"])) story.append(Spacer(1, 14)) summary_table = Table( [ ["Rekomendacja", result["category"]], ["Sugerowane badanie", result["imaging"]], ["Tryb", result["urgency"]], ["Kontrast", result["contrast"]], ["Ścieżka kliniczna", result["clinical_path"]], ], colWidths=[160, 350], ) summary_table.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAEAEA")), ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("FONTSIZE", (0, 0), (-1, -1), 10), ("PADDING", (0, 0), (-1, -1), 6), ] ) ) story.append(summary_table) story.append(Spacer(1, 14)) if warnings: story.append(Paragraph("Ostrzeżenia walidacyjne:", styles["Heading3"])) for w in warnings: story.append(Paragraph(f"• {w}", styles["Normal"])) story.append(Spacer(1, 10)) story.append(Paragraph("Uzasadnienie:", styles["Heading3"])) for r in result["reasons"]: story.append(Paragraph(f"• {r}", styles["Normal"])) story.append(Spacer(1, 10)) if result["alternatives"]: story.append(Paragraph("Alternatywy / dalsze kroki:", styles["Heading3"])) for a in result["alternatives"]: story.append(Paragraph(f"• {a}", styles["Normal"])) story.append(Spacer(1, 10)) details = [ ["Odcinek", data["spine_section"]], ["Czas objawów", data["duration"]], ["Pierwszy epizod", data["first_episode"]], ["Nawrót", data["recurrent_episode"]], ["Stan po operacji", data["post_op"]], ["Ból osiowy", data["axial_pain"]], ["Ból promieniujący", data["radicular_pain"]], ["Parestezje", data["paresthesia"]], ["Osłabienie siły", data["weakness"]], ["Zaburzenia chodu / równowagi", data["gait_balance"]], ["Zaburzenia zwieraczy", data["sphincter"]], ["Znieczulenie krocza", data["saddle"]], ["Postępujący deficyt", data["progressive_deficit"]], ["Objawy mielopatii", data["myelopathy"]], ["Wywiad nowotworowy", data["cancer_history"]], ["Utrata masy ciała", data["weight_loss"]], ["Ból nocny", data["night_pain"]], ["Gorączka", data["fever"]], ["Immunosupresja", data["immunosuppression"]], ["Podejrzenie infekcji", data["infection_suspicion"]], ["Uraz wysokiej energii", data["trauma_high"]], ["Uraz niskoenergetyczny + ryzyko", data["trauma_low_risk"]], ["Osteoporoza / steroidy", data["osteoporosis_steroids"]], ["Brak deficytu", data["no_deficit"]], ["Deficyt korzeniowy", data["root_deficit"]], ["Deficyt wielopoziomowy / obustronny", data["multilevel_bilateral"]], ["Objawy rdzeniowe", data["cord_signs"]], ["Brak leczenia zachowawczego", data["conservative_none"]], ["Leczenie < 6 tygodni", data["conservative_lt6"]], ["Leczenie ≥ 6 tygodni", data["conservative_ge6"]], ["Poprawa", data["improved"]], ["Brak poprawy", data["no_improvement"]], ["Pogorszenie", data["worsening"]], ["Podejrzenie dyskopatii", data["suspect_disc"]], ["Podejrzenie ucisku rdzenia", data["suspect_cord"]], ["Podejrzenie nowotworu", data["suspect_tumor"]], ["Podejrzenie infekcji", data["suspect_infection"]], ["Kwalifikacja do zabiegu", data["qualify_surgery"]], ["Kwalifikacja do blokady", data["qualify_injection"]], ["Czy wynik MR zmieni leczenie?", data["treatment_impact"]], ] details_table = Table(details, colWidths=[220, 290]) details_table.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#F3F4F6")), ("GRID", (0, 0), (-1, -1), 0.4, colors.lightgrey), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("FONTSIZE", (0, 0), (-1, -1), 9), ("PADDING", (0, 0), (-1, -1), 5), ] ) ) story.append(Paragraph("Dane formularza:", styles["Heading3"])) story.append(details_table) story.append(Spacer(1, 14)) story.append(Paragraph("Uwaga: narzędzie ma charakter wspomagający i nie zastępuje oceny lekarza.", styles["Italic"])) doc.build(story) return filename def render_html_banner(category, imaging, color): return f"""
{category}
{imaging}
""" def run_app( spine_section, duration, first_episode, recurrent_episode, post_op, axial_pain, radicular_pain, paresthesia, weakness, gait_balance, sphincter, saddle, progressive_deficit, myelopathy, cancer_history, weight_loss, night_pain, fever, immunosuppression, infection_suspicion, trauma_high, trauma_low_risk, osteoporosis_steroids, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening, suspect_disc, suspect_cord, suspect_tumor, suspect_infection, qualify_surgery, qualify_injection, treatment_impact, ): warnings = validate_form( first_episode, recurrent_episode, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening ) result = build_recommendation( spine_section, duration, first_episode, recurrent_episode, post_op, axial_pain, radicular_pain, paresthesia, weakness, gait_balance, sphincter, saddle, progressive_deficit, myelopathy, cancer_history, weight_loss, night_pain, fever, immunosuppression, infection_suspicion, trauma_high, trauma_low_risk, osteoporosis_steroids, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening, suspect_disc, suspect_cord, suspect_tumor, suspect_infection, qualify_surgery, qualify_injection, treatment_impact, ) reasons_text = "\n".join([f"- {x}" for x in result["reasons"]]) if result["reasons"] else "-" alt_text = "\n".join([f"- {x}" for x in result["alternatives"]]) if result["alternatives"] else "-" warnings_text = "\n".join([f"- {x}" for x in warnings]) if warnings else "Brak ostrzeżeń walidacyjnych." form_data = { "spine_section": spine_section, "duration": duration, "first_episode": yes_no(first_episode), "recurrent_episode": yes_no(recurrent_episode), "post_op": yes_no(post_op), "axial_pain": yes_no(axial_pain), "radicular_pain": yes_no(radicular_pain), "paresthesia": yes_no(paresthesia), "weakness": yes_no(weakness), "gait_balance": yes_no(gait_balance), "sphincter": yes_no(sphincter), "saddle": yes_no(saddle), "progressive_deficit": yes_no(progressive_deficit), "myelopathy": yes_no(myelopathy), "cancer_history": yes_no(cancer_history), "weight_loss": yes_no(weight_loss), "night_pain": yes_no(night_pain), "fever": yes_no(fever), "immunosuppression": yes_no(immunosuppression), "infection_suspicion": yes_no(infection_suspicion), "trauma_high": yes_no(trauma_high), "trauma_low_risk": yes_no(trauma_low_risk), "osteoporosis_steroids": yes_no(osteoporosis_steroids), "no_deficit": yes_no(no_deficit), "root_deficit": yes_no(root_deficit), "multilevel_bilateral": yes_no(multilevel_bilateral), "cord_signs": yes_no(cord_signs), "conservative_none": yes_no(conservative_none), "conservative_lt6": yes_no(conservative_lt6), "conservative_ge6": yes_no(conservative_ge6), "improved": yes_no(improved), "no_improvement": yes_no(no_improvement), "worsening": yes_no(worsening), "suspect_disc": yes_no(suspect_disc), "suspect_cord": yes_no(suspect_cord), "suspect_tumor": yes_no(suspect_tumor), "suspect_infection": yes_no(suspect_infection), "qualify_surgery": yes_no(qualify_surgery), "qualify_injection": yes_no(qualify_injection), "treatment_impact": treatment_impact, } pdf_file = generate_pdf(form_data, result, warnings) banner_html = render_html_banner(result["category"], result["imaging"], result["color"]) return ( banner_html, result["category"], result["imaging"], result["clinical_path"], result["urgency"], result["contrast"], reasons_text, alt_text, warnings_text, pdf_file, ) with gr.Blocks(theme=gr.themes.Soft(), title="MR Spine CDS PRO+") as app: gr.Markdown( f""" # MR Spine Clinical Decision Support – PRO+ **Autor:** {AUTHOR_NAME} **Afiliacja:** {AUTHOR_AFFILIATION} Ustrukturyzowany formularz kwalifikacji do badania MR kręgosłupa. **Uwaga:** narzędzie wspomagające decyzję kliniczną, nie zastępuje oceny lekarza. """ ) with gr.Group(): gr.Markdown("## A. Identyfikacja problemu") with gr.Row(): spine_section = gr.Radio( ["szyjnego", "piersiowego", "lędźwiowego"], label="Odcinek kręgosłupa", value="lędźwiowego", ) duration = gr.Radio( ["< 2 tygodni", "2–6 tygodni", "6–12 tygodni", "> 12 tygodni"], label="Czas trwania objawów", value="2–6 tygodni", ) with gr.Row(): first_episode = gr.Checkbox(label="Pierwszy epizod") recurrent_episode = gr.Checkbox(label="Nawrót / zaostrzenie przewlekłe") post_op = gr.Checkbox(label="Pacjent po operacji kręgosłupa") with gr.Group(): gr.Markdown("## B. Charakter objawów") with gr.Row(): axial_pain = gr.Checkbox(label="Ból miejscowy (osiowy)") radicular_pain = gr.Checkbox(label="Ból promieniujący (radikulopatia)") paresthesia = gr.Checkbox(label="Parestezje / drętwienie") weakness = gr.Checkbox(label="Osłabienie siły mięśniowej") gait_balance = gr.Checkbox(label="Zaburzenia chodu / równowagi") with gr.Group(): gr.Markdown("## C. Red flags") with gr.Row(): sphincter = gr.Checkbox(label="Zaburzenia zwieraczy") saddle = gr.Checkbox(label="Znieczulenie krocza") progressive_deficit = gr.Checkbox(label="Postępujący niedowład") myelopathy = gr.Checkbox(label="Objawy mielopatii") with gr.Row(): cancer_history = gr.Checkbox(label="Wywiad nowotworowy") weight_loss = gr.Checkbox(label="Utrata masy ciała") night_pain = gr.Checkbox(label="Ból nocny / niemechaniczny") with gr.Row(): fever = gr.Checkbox(label="Gorączka") immunosuppression = gr.Checkbox(label="Immunosupresja") infection_suspicion = gr.Checkbox(label="Podejrzenie infekcji") with gr.Row(): trauma_high = gr.Checkbox(label="Uraz wysokiej energii") trauma_low_risk = gr.Checkbox(label="Uraz niskoenergetyczny + ryzyko") osteoporosis_steroids = gr.Checkbox(label="Osteoporoza / przewlekłe steroidy") with gr.Group(): gr.Markdown("## D. Badanie neurologiczne") with gr.Row(): no_deficit = gr.Checkbox(label="Brak deficytu") root_deficit = gr.Checkbox(label="Deficyt korzeniowy") multilevel_bilateral = gr.Checkbox(label="Deficyt wielopoziomowy / obustronny") cord_signs = gr.Checkbox(label="Objawy rdzeniowe") with gr.Group(): gr.Markdown("## E. Dotychczasowe leczenie") with gr.Row(): conservative_none = gr.Checkbox(label="Brak leczenia zachowawczego") conservative_lt6 = gr.Checkbox(label="Leczenie < 6 tygodni") conservative_ge6 = gr.Checkbox(label="Leczenie ≥ 6 tygodni") with gr.Row(): improved = gr.Checkbox(label="Poprawa") no_improvement = gr.Checkbox(label="Brak poprawy") worsening = gr.Checkbox(label="Pogorszenie") with gr.Group(): gr.Markdown("## F. Pytanie kliniczne") with gr.Row(): suspect_disc = gr.Checkbox(label="Podejrzenie dyskopatii") suspect_cord = gr.Checkbox(label="Podejrzenie ucisku rdzenia") suspect_tumor = gr.Checkbox(label="Podejrzenie nowotworu") suspect_infection = gr.Checkbox(label="Podejrzenie infekcji") with gr.Row(): qualify_surgery = gr.Checkbox(label="Kwalifikacja do zabiegu") qualify_injection = gr.Checkbox(label="Kwalifikacja do blokady / iniekcji") treatment_impact = gr.Radio( ["TAK", "NIE", "NIEPEWNE"], label="Czy wynik MR zmieni leczenie?", value="NIEPEWNE", ) with gr.Row(): run_button = gr.Button("Generuj rekomendację", variant="primary") clear_button = gr.Button("Wyczyść formularz") gr.Markdown("## Wynik") out_banner = gr.HTML() out_category = gr.Textbox(label="Kategoria rekomendacji") out_imaging = gr.Textbox(label="Sugerowane badanie") out_path = gr.Textbox(label="Ścieżka kliniczna") out_urgency = gr.Textbox(label="Tryb") out_contrast = gr.Textbox(label="Kontrast") out_reasons = gr.Textbox(label="Uzasadnienie", lines=8) out_alternatives = gr.Textbox(label="Alternatywy / dalsze kroki", lines=5) out_warnings = gr.Textbox(label="Ostrzeżenia walidacyjne", lines=4) out_pdf = gr.File(label="Pobierz raport PDF") inputs = [ spine_section, duration, first_episode, recurrent_episode, post_op, axial_pain, radicular_pain, paresthesia, weakness, gait_balance, sphincter, saddle, progressive_deficit, myelopathy, cancer_history, weight_loss, night_pain, fever, immunosuppression, infection_suspicion, trauma_high, trauma_low_risk, osteoporosis_steroids, no_deficit, root_deficit, multilevel_bilateral, cord_signs, conservative_none, conservative_lt6, conservative_ge6, improved, no_improvement, worsening, suspect_disc, suspect_cord, suspect_tumor, suspect_infection, qualify_surgery, qualify_injection, treatment_impact, ] outputs = [ out_banner, out_category, out_imaging, out_path, out_urgency, out_contrast, out_reasons, out_alternatives, out_warnings, out_pdf, ] run_button.click(fn=run_app, inputs=inputs, outputs=outputs) clear_button.click( fn=lambda: ( "lędźwiowego", "2–6 tygodni", False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, "NIEPEWNE", "", "", "", "", "", "", "", "", None, ), inputs=[], outputs=inputs + outputs, ) app.launch()