faral commited on
Commit
bd2deef
·
verified ·
1 Parent(s): d8c36c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +770 -78
app.py CHANGED
@@ -1,112 +1,804 @@
1
  import gradio as gr
2
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
3
- from reportlab.lib.styles import getSampleStyleSheet
 
 
 
4
  import datetime
5
 
6
- def decision_logic(bladder, saddle, deficit_prog, myelopathy,
7
- cancer, fever, immuno,
8
- trauma,
9
- radiculopathy,
10
- duration,
11
- no_improvement,
12
- post_op,
13
- impact):
14
 
15
- if bladder or saddle or deficit_prog or myelopathy:
16
- return "🔴 MR PILNE", "Objawy alarmowe neurologiczne (CES/mielopatia)"
17
 
18
- if cancer or fever or immuno or post_op:
19
- return "🟠 MR PRZYSPIESZONE", "Podejrzenie nowotworu / infekcji / stan pooperacyjny"
20
 
21
- if radiculopathy and duration == ">6 tygodni" and no_improvement and impact:
22
- return "🟡 MR PLANOWE", "Radikulopatia >6 tygodni bez poprawy"
23
 
24
- if trauma:
25
- return "🔵 RTG/TK NAJPIERW", "Podejrzenie urazu"
26
 
27
- return "🟢 BRAK WSKAZAŃ", "Leczenie zachowawcze"
 
 
 
 
 
 
28
 
 
 
29
 
30
- def generate_pdf(rec, just):
31
- filename = "raport.pdf"
32
- doc = SimpleDocTemplate(filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  styles = getSampleStyleSheet()
34
- content = []
35
 
36
- content.append(Paragraph("Raport kwalifikacji do MR kręgosłupa", styles["Title"]))
37
- content.append(Spacer(1, 12))
38
- content.append(Paragraph(f"Data: {datetime.datetime.now()}", styles["Normal"]))
39
- content.append(Spacer(1, 12))
40
- content.append(Paragraph(f"Rekomendacja: {rec}", styles["Normal"]))
41
- content.append(Spacer(1, 12))
42
- content.append(Paragraph(f"Uzasadnienie: {just}", styles["Normal"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- doc.build(content)
45
  return filename
46
 
47
 
48
- def run(bladder, saddle, deficit_prog, myelopathy,
49
- cancer, fever, immuno,
50
- trauma,
51
- radiculopathy,
52
- duration,
53
- no_improvement,
54
- post_op,
55
- impact):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- rec, just = decision_logic(
58
- bladder, saddle, deficit_prog, myelopathy,
59
- cancer, fever, immuno,
60
- trauma,
61
- radiculopathy,
62
  duration,
63
- no_improvement,
 
64
  post_op,
65
- impact
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  )
67
 
68
- pdf = generate_pdf(rec, just)
69
- return rec, just, pdf
70
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- with gr.Blocks() as app:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- gr.Markdown("## 🧠 MR Spine Decision Support Tool")
75
- gr.Markdown("⚠️ Narzędzie wspomagające decyzję kliniczną — nie zastępuje lekarza")
 
 
 
 
 
 
76
 
77
- bladder = gr.Checkbox(label="Zaburzenia zwieraczy")
78
- saddle = gr.Checkbox(label="Znieczulenie krocza")
79
- deficit_prog = gr.Checkbox(label="Postępujący niedowład")
80
- myelopathy = gr.Checkbox(label="Mielopatia")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- cancer = gr.Checkbox(label="Nowotwór")
83
- fever = gr.Checkbox(label="Gorączka")
84
- immuno = gr.Checkbox(label="Immunosupresja")
 
 
 
 
85
 
86
- trauma = gr.Checkbox(label="Uraz")
 
 
 
 
 
 
 
 
 
87
 
88
- radiculopathy = gr.Checkbox(label="Radikulopatia")
89
- duration = gr.Radio(["<6 tygodni", ">6 tygodni"], label="Czas objawów")
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- no_improvement = gr.Checkbox(label="Brak poprawy")
92
- post_op = gr.Checkbox(label="Po operacji")
93
- impact = gr.Checkbox(label="Wpływ na leczenie")
94
 
95
- btn = gr.Button("🔍 Oceń")
 
 
 
 
 
 
 
 
 
 
96
 
97
- out1 = gr.Textbox(label="Rekomendacja")
98
- out2 = gr.Textbox(label="Uzasadnienie")
99
- out3 = gr.File(label="Pobierz PDF")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- btn.click(run,
102
- inputs=[bladder, saddle, deficit_prog, myelopathy,
103
- cancer, fever, immuno,
104
- trauma,
105
- radiculopathy,
106
- duration,
107
- no_improvement,
108
- post_op,
109
- impact],
110
- outputs=[out1, out2, out3])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  app.launch()
 
1
  import gradio as gr
2
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
3
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
4
+ from reportlab.lib import colors
5
+ from reportlab.lib.pagesizes import A4
6
+ from reportlab.lib.enums import TA_CENTER
7
  import datetime
8
 
 
 
 
 
 
 
 
 
9
 
10
+ AUTHOR_NAME = "prof. dr hab. n. med. Rafał Obuchowicz"
11
+ AUTHOR_AFFILIATION = "Luxmed Diagnostyka"
12
 
 
 
13
 
14
+ def yes_no(value: bool) -> str:
15
+ return "Tak" if value else "Nie"
16
 
 
 
17
 
18
+ def validate_form(
19
+ first_episode, recurrent_episode,
20
+ no_deficit, root_deficit, multilevel_bilateral, cord_signs,
21
+ conservative_none, conservative_lt6, conservative_ge6,
22
+ improved, no_improvement, worsening
23
+ ):
24
+ warnings = []
25
 
26
+ if first_episode and recurrent_episode:
27
+ warnings.append("Zaznaczono jednocześnie: 'Pierwszy epizod' i 'Nawrót / zaostrzenie przewlekłe'.")
28
 
29
+ neuro_flags = [no_deficit, root_deficit, multilevel_bilateral, cord_signs]
30
+ if no_deficit and any([root_deficit, multilevel_bilateral, cord_signs]):
31
+ warnings.append("Zaznaczono jednocześnie 'Brak deficytu' i obecność deficytu neurologicznego.")
32
+
33
+ treatment_flags = [conservative_none, conservative_lt6, conservative_ge6]
34
+ if sum(treatment_flags) > 1:
35
+ warnings.append("W sekcji leczenia zachowawczego wybrano więcej niż jedną sprzeczną opcję czasową.")
36
+
37
+ response_flags = [improved, no_improvement, worsening]
38
+ if sum(response_flags) > 1:
39
+ warnings.append("W sekcji odpowiedzi na leczenie zaznaczono więcej niż jedną sprzeczną opcję.")
40
+
41
+ return warnings
42
+
43
+
44
+ def build_recommendation(
45
+ spine_section,
46
+ duration,
47
+ first_episode,
48
+ recurrent_episode,
49
+ post_op,
50
+ axial_pain,
51
+ radicular_pain,
52
+ paresthesia,
53
+ weakness,
54
+ gait_balance,
55
+ sphincter,
56
+ saddle,
57
+ progressive_deficit,
58
+ myelopathy,
59
+ cancer_history,
60
+ weight_loss,
61
+ night_pain,
62
+ fever,
63
+ immunosuppression,
64
+ infection_suspicion,
65
+ trauma_high,
66
+ trauma_low_risk,
67
+ osteoporosis_steroids,
68
+ no_deficit,
69
+ root_deficit,
70
+ multilevel_bilateral,
71
+ cord_signs,
72
+ conservative_none,
73
+ conservative_lt6,
74
+ conservative_ge6,
75
+ improved,
76
+ no_improvement,
77
+ worsening,
78
+ suspect_disc,
79
+ suspect_cord,
80
+ suspect_tumor,
81
+ suspect_infection,
82
+ qualify_surgery,
83
+ qualify_injection,
84
+ treatment_impact,
85
+ ):
86
+ reasons = []
87
+ alternatives = []
88
+ urgency = "routine"
89
+ contrast = "nie"
90
+ imaging = "Brak wskazań do MR na tym etapie"
91
+ category = "🟢 BRAK WSKAZAŃ DO MR"
92
+ clinical_path = "Leczenie zachowawcze / obserwacja"
93
+
94
+ if sphincter or saddle or progressive_deficit or myelopathy or cord_signs:
95
+ category = "🔴 MR PILNE"
96
+ imaging = f"Pilne MR odcinka {spine_section}"
97
+ clinical_path = "Pilna konsultacja specjalistyczna / tryb pilny"
98
+ urgency = "pilne"
99
+ reasons.append("Stwierdzono objawy alarmowe neurologiczne")
100
+ if sphincter:
101
+ reasons.append("Zaburzenia zwieraczy")
102
+ if saddle:
103
+ reasons.append("Znieczulenie okolicy krocza")
104
+ if progressive_deficit:
105
+ reasons.append("Postępujący deficyt neurologiczny")
106
+ if myelopathy or cord_signs:
107
+ reasons.append("Objawy mielopatii / uszkodzenia rdzenia")
108
+
109
+ return {
110
+ "category": category,
111
+ "imaging": imaging,
112
+ "clinical_path": clinical_path,
113
+ "urgency": urgency,
114
+ "contrast": contrast,
115
+ "reasons": reasons,
116
+ "alternatives": alternatives,
117
+ "color": "#b91c1c",
118
+ }
119
+
120
+ if (
121
+ cancer_history
122
+ or weight_loss
123
+ or night_pain
124
+ or fever
125
+ or immunosuppression
126
+ or infection_suspicion
127
+ or suspect_tumor
128
+ or suspect_infection
129
+ or (post_op and (worsening or weakness or paresthesia or radicular_pain))
130
+ ):
131
+ category = "🟠 MR PRZYSPIESZONE"
132
+ imaging = f"MR odcinka {spine_section}"
133
+ clinical_path = "Przyspieszona diagnostyka specjalistyczna"
134
+ urgency = "przyspieszone"
135
+ contrast = "rozważyć"
136
+ reasons.append("Występują przesłanki nowotworowe, infekcyjne lub pooperacyjne")
137
+ if cancer_history:
138
+ reasons.append("Wywiad nowotworowy")
139
+ if weight_loss:
140
+ reasons.append("Niezamierzona utrata masy ciała")
141
+ if night_pain:
142
+ reasons.append("Ból nocny / niemechaniczny")
143
+ if fever:
144
+ reasons.append("Gorączka")
145
+ if immunosuppression:
146
+ reasons.append("Immunosupresja")
147
+ if infection_suspicion:
148
+ reasons.append("Podejrzenie infekcji")
149
+ if post_op:
150
+ reasons.append("Stan po operacji kręgosłupa")
151
+
152
+ alternatives.append("Rozważyć badania laboratoryjne: CRP, OB, morfologia")
153
+ if trauma_high or trauma_low_risk:
154
+ alternatives.append("Jeśli współistnieje uraz: rozważyć RTG/TK struktur kostnych")
155
+
156
+ return {
157
+ "category": category,
158
+ "imaging": imaging,
159
+ "clinical_path": clinical_path,
160
+ "urgency": urgency,
161
+ "contrast": contrast,
162
+ "reasons": reasons,
163
+ "alternatives": alternatives,
164
+ "color": "#ea580c",
165
+ }
166
+
167
+ if trauma_high or trauma_low_risk or osteoporosis_steroids:
168
+ category = "🔵 RTG / TK NAJPIERW"
169
+ imaging = f"RTG lub TK odcinka {spine_section}"
170
+ clinical_path = "Najpierw diagnostyka kostna; MR warunkowo"
171
+ urgency = "pilne" if trauma_high else "routine"
172
+ reasons.append("Dominują wskazania urazowe / kostne")
173
+ if trauma_high:
174
+ reasons.append("Uraz wysokiej energii")
175
+ if trauma_low_risk:
176
+ reasons.append("Uraz niskoenergetyczny z ryzykiem")
177
+ if osteoporosis_steroids:
178
+ reasons.append("Osteoporoza / przewlekła steroidoterapia")
179
+
180
+ alternatives.append("MR rozważyć przy deficycie neurologicznym lub podejrzeniu uszkodzeń tkanek miękkich")
181
+
182
+ return {
183
+ "category": category,
184
+ "imaging": imaging,
185
+ "clinical_path": clinical_path,
186
+ "urgency": urgency,
187
+ "contrast": contrast,
188
+ "reasons": reasons,
189
+ "alternatives": alternatives,
190
+ "color": "#2563eb",
191
+ }
192
+
193
+ radicular_pattern = radicular_pain or paresthesia or weakness or root_deficit or multilevel_bilateral
194
+
195
+ if (
196
+ radicular_pattern
197
+ and conservative_ge6
198
+ and (no_improvement or worsening)
199
+ and treatment_impact == "TAK"
200
+ ):
201
+ category = "🟡 MR PLANOWE"
202
+ imaging = f"MR odcinka {spine_section} bez kontrastu"
203
+ clinical_path = "Planowa diagnostyka obrazowa"
204
+ urgency = "planowe"
205
+ reasons.append("Objawy korzeniowe / neurologiczne utrzymują się mimo leczenia")
206
+ reasons.append("Leczenie zachowawcze ≥ 6 tygodni")
207
+ if no_improvement:
208
+ reasons.append("Brak poprawy")
209
+ if worsening:
210
+ reasons.append("Pogorszenie")
211
+ reasons.append("Wynik badania może wpłynąć na leczenie")
212
+
213
+ if qualify_surgery:
214
+ reasons.append("Pacjent potencjalnie kwalifikowany do zabiegu")
215
+ if qualify_injection:
216
+ reasons.append("Pacjent potencjalnie kwalifikowany do blokady / iniekcji")
217
+
218
+ return {
219
+ "category": category,
220
+ "imaging": imaging,
221
+ "clinical_path": clinical_path,
222
+ "urgency": urgency,
223
+ "contrast": contrast,
224
+ "reasons": reasons,
225
+ "alternatives": alternatives,
226
+ "color": "#ca8a04",
227
+ }
228
+
229
+ if (
230
+ axial_pain
231
+ and not radicular_pattern
232
+ and not weakness
233
+ and not gait_balance
234
+ and not conservative_ge6
235
+ and treatment_impact != "TAK"
236
+ ):
237
+ category = "🟢 BRAK WSKAZAŃ DO MR"
238
+ imaging = "Brak wskazań do MR na tym etapie"
239
+ clinical_path = "Leczenie zachowawcze / obserwacja / kontrola"
240
+ urgency = "routine"
241
+ reasons.append("Dominują objawy bólu osiowego bez czerwonych flag")
242
+ reasons.append("Brak przesłanek do pilnej diagnostyki obrazowej")
243
+ alternatives.append("Kontynuacja leczenia zachowawczego")
244
+ alternatives.append("Rozważyć fizjoterapię / farmakoterapię / kontrolę kliniczną")
245
+
246
+ return {
247
+ "category": category,
248
+ "imaging": imaging,
249
+ "clinical_path": clinical_path,
250
+ "urgency": urgency,
251
+ "contrast": contrast,
252
+ "reasons": reasons,
253
+ "alternatives": alternatives,
254
+ "color": "#15803d",
255
+ }
256
+
257
+ category = "🟡 WYMAGA DOPRECYZOWANIA"
258
+ imaging = "Rozważyć MR po uzupełnieniu oceny klinicznej"
259
+ clinical_path = "Uzupełnić wywiad / badanie / odpowiedź na leczenie"
260
+ urgency = "routine"
261
+ reasons.append("Dane nie są jednoznaczne dla automatycznej kwalifikacji")
262
+ if treatment_impact == "NIEPEWNE":
263
+ reasons.append("Niepewny wpływ wyniku MR na decyzję terapeutyczną")
264
+ alternatives.append("Zweryfikować czas leczenia zachowawczego")
265
+ alternatives.append("Uściślić deficyty neurologiczne i pytanie kliniczne")
266
+
267
+ return {
268
+ "category": category,
269
+ "imaging": imaging,
270
+ "clinical_path": clinical_path,
271
+ "urgency": urgency,
272
+ "contrast": contrast,
273
+ "reasons": reasons,
274
+ "alternatives": alternatives,
275
+ "color": "#a16207",
276
+ }
277
+
278
+
279
+ def generate_pdf(data, result, warnings):
280
+ filename = "raport_mr_kregoslup_pro_plus.pdf"
281
+ doc = SimpleDocTemplate(filename, pagesize=A4, rightMargin=36, leftMargin=36, topMargin=36, bottomMargin=36)
282
  styles = getSampleStyleSheet()
 
283
 
284
+ title_style = ParagraphStyle(
285
+ "CustomTitle",
286
+ parent=styles["Title"],
287
+ alignment=TA_CENTER,
288
+ fontSize=18,
289
+ spaceAfter=10
290
+ )
291
+
292
+ subtitle_style = ParagraphStyle(
293
+ "CustomSubtitle",
294
+ parent=styles["Normal"],
295
+ alignment=TA_CENTER,
296
+ fontSize=10,
297
+ textColor=colors.grey
298
+ )
299
+
300
+ story = []
301
+ story.append(Paragraph("Raport kwalifikacji do badania MR kręgosłupa", title_style))
302
+ story.append(Paragraph("Clinical Decision Support – PRO+", subtitle_style))
303
+ story.append(Spacer(1, 8))
304
+ story.append(Paragraph(f"Autor narzędzia: <b>{AUTHOR_NAME}</b>", styles["Normal"]))
305
+ story.append(Paragraph(f"Afiliacja: <b>{AUTHOR_AFFILIATION}</b>", styles["Normal"]))
306
+ story.append(Paragraph(f"Data wygenerowania: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}", styles["Normal"]))
307
+ story.append(Spacer(1, 14))
308
+
309
+ summary_table = Table(
310
+ [
311
+ ["Rekomendacja", result["category"]],
312
+ ["Sugerowane badanie", result["imaging"]],
313
+ ["Tryb", result["urgency"]],
314
+ ["Kontrast", result["contrast"]],
315
+ ["Ścieżka kliniczna", result["clinical_path"]],
316
+ ],
317
+ colWidths=[160, 350],
318
+ )
319
+ summary_table.setStyle(
320
+ TableStyle(
321
+ [
322
+ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAEAEA")),
323
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
324
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
325
+ ("FONTSIZE", (0, 0), (-1, -1), 10),
326
+ ("PADDING", (0, 0), (-1, -1), 6),
327
+ ]
328
+ )
329
+ )
330
+ story.append(summary_table)
331
+ story.append(Spacer(1, 14))
332
+
333
+ if warnings:
334
+ story.append(Paragraph("<b>Ostrzeżenia walidacyjne:</b>", styles["Heading3"]))
335
+ for w in warnings:
336
+ story.append(Paragraph(f"• {w}", styles["Normal"]))
337
+ story.append(Spacer(1, 10))
338
+
339
+ story.append(Paragraph("<b>Uzasadnienie:</b>", styles["Heading3"]))
340
+ for r in result["reasons"]:
341
+ story.append(Paragraph(f"• {r}", styles["Normal"]))
342
+ story.append(Spacer(1, 10))
343
+
344
+ if result["alternatives"]:
345
+ story.append(Paragraph("<b>Alternatywy / dalsze kroki:</b>", styles["Heading3"]))
346
+ for a in result["alternatives"]:
347
+ story.append(Paragraph(f"• {a}", styles["Normal"]))
348
+ story.append(Spacer(1, 10))
349
+
350
+ details = [
351
+ ["Odcinek", data["spine_section"]],
352
+ ["Czas objawów", data["duration"]],
353
+ ["Pierwszy epizod", data["first_episode"]],
354
+ ["Nawrót", data["recurrent_episode"]],
355
+ ["Stan po operacji", data["post_op"]],
356
+ ["Ból osiowy", data["axial_pain"]],
357
+ ["Ból promieniujący", data["radicular_pain"]],
358
+ ["Parestezje", data["paresthesia"]],
359
+ ["Osłabienie siły", data["weakness"]],
360
+ ["Zaburzenia chodu / równowagi", data["gait_balance"]],
361
+ ["Zaburzenia zwieraczy", data["sphincter"]],
362
+ ["Znieczulenie krocza", data["saddle"]],
363
+ ["Postępujący deficyt", data["progressive_deficit"]],
364
+ ["Objawy mielopatii", data["myelopathy"]],
365
+ ["Wywiad nowotworowy", data["cancer_history"]],
366
+ ["Utrata masy ciała", data["weight_loss"]],
367
+ ["Ból nocny", data["night_pain"]],
368
+ ["Gorączka", data["fever"]],
369
+ ["Immunosupresja", data["immunosuppression"]],
370
+ ["Podejrzenie infekcji", data["infection_suspicion"]],
371
+ ["Uraz wysokiej energii", data["trauma_high"]],
372
+ ["Uraz niskoenergetyczny + ryzyko", data["trauma_low_risk"]],
373
+ ["Osteoporoza / steroidy", data["osteoporosis_steroids"]],
374
+ ["Brak deficytu", data["no_deficit"]],
375
+ ["Deficyt korzeniowy", data["root_deficit"]],
376
+ ["Deficyt wielopoziomowy / obustronny", data["multilevel_bilateral"]],
377
+ ["Objawy rdzeniowe", data["cord_signs"]],
378
+ ["Brak leczenia zachowawczego", data["conservative_none"]],
379
+ ["Leczenie < 6 tygodni", data["conservative_lt6"]],
380
+ ["Leczenie ≥ 6 tygodni", data["conservative_ge6"]],
381
+ ["Poprawa", data["improved"]],
382
+ ["Brak poprawy", data["no_improvement"]],
383
+ ["Pogorszenie", data["worsening"]],
384
+ ["Podejrzenie dyskopatii", data["suspect_disc"]],
385
+ ["Podejrzenie ucisku rdzenia", data["suspect_cord"]],
386
+ ["Podejrzenie nowotworu", data["suspect_tumor"]],
387
+ ["Podejrzenie infekcji", data["suspect_infection"]],
388
+ ["Kwalifikacja do zabiegu", data["qualify_surgery"]],
389
+ ["Kwalifikacja do blokady", data["qualify_injection"]],
390
+ ["Czy wynik MR zmieni leczenie?", data["treatment_impact"]],
391
+ ]
392
+
393
+ details_table = Table(details, colWidths=[220, 290])
394
+ details_table.setStyle(
395
+ TableStyle(
396
+ [
397
+ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#F3F4F6")),
398
+ ("GRID", (0, 0), (-1, -1), 0.4, colors.lightgrey),
399
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
400
+ ("FONTSIZE", (0, 0), (-1, -1), 9),
401
+ ("PADDING", (0, 0), (-1, -1), 5),
402
+ ]
403
+ )
404
+ )
405
+ story.append(Paragraph("<b>Dane formularza:</b>", styles["Heading3"]))
406
+ story.append(details_table)
407
+ story.append(Spacer(1, 14))
408
+ story.append(Paragraph("Uwaga: narzędzie ma charakter wspomagający i nie zastępuje oceny lekarza.", styles["Italic"]))
409
 
410
+ doc.build(story)
411
  return filename
412
 
413
 
414
+ def render_html_banner(category, imaging, color):
415
+ return f"""
416
+ <div style="padding:16px;border-radius:14px;background:{color};color:white;
417
+ font-size:18px;font-weight:700;text-align:center;margin:8px 0;">
418
+ {category}<br>
419
+ <span style="font-size:14px;font-weight:500;">{imaging}</span>
420
+ </div>
421
+ """
422
+
423
+
424
+ def run_app(
425
+ spine_section,
426
+ duration,
427
+ first_episode,
428
+ recurrent_episode,
429
+ post_op,
430
+ axial_pain,
431
+ radicular_pain,
432
+ paresthesia,
433
+ weakness,
434
+ gait_balance,
435
+ sphincter,
436
+ saddle,
437
+ progressive_deficit,
438
+ myelopathy,
439
+ cancer_history,
440
+ weight_loss,
441
+ night_pain,
442
+ fever,
443
+ immunosuppression,
444
+ infection_suspicion,
445
+ trauma_high,
446
+ trauma_low_risk,
447
+ osteoporosis_steroids,
448
+ no_deficit,
449
+ root_deficit,
450
+ multilevel_bilateral,
451
+ cord_signs,
452
+ conservative_none,
453
+ conservative_lt6,
454
+ conservative_ge6,
455
+ improved,
456
+ no_improvement,
457
+ worsening,
458
+ suspect_disc,
459
+ suspect_cord,
460
+ suspect_tumor,
461
+ suspect_infection,
462
+ qualify_surgery,
463
+ qualify_injection,
464
+ treatment_impact,
465
+ ):
466
+ warnings = validate_form(
467
+ first_episode, recurrent_episode,
468
+ no_deficit, root_deficit, multilevel_bilateral, cord_signs,
469
+ conservative_none, conservative_lt6, conservative_ge6,
470
+ improved, no_improvement, worsening
471
+ )
472
 
473
+ result = build_recommendation(
474
+ spine_section,
 
 
 
475
  duration,
476
+ first_episode,
477
+ recurrent_episode,
478
  post_op,
479
+ axial_pain,
480
+ radicular_pain,
481
+ paresthesia,
482
+ weakness,
483
+ gait_balance,
484
+ sphincter,
485
+ saddle,
486
+ progressive_deficit,
487
+ myelopathy,
488
+ cancer_history,
489
+ weight_loss,
490
+ night_pain,
491
+ fever,
492
+ immunosuppression,
493
+ infection_suspicion,
494
+ trauma_high,
495
+ trauma_low_risk,
496
+ osteoporosis_steroids,
497
+ no_deficit,
498
+ root_deficit,
499
+ multilevel_bilateral,
500
+ cord_signs,
501
+ conservative_none,
502
+ conservative_lt6,
503
+ conservative_ge6,
504
+ improved,
505
+ no_improvement,
506
+ worsening,
507
+ suspect_disc,
508
+ suspect_cord,
509
+ suspect_tumor,
510
+ suspect_infection,
511
+ qualify_surgery,
512
+ qualify_injection,
513
+ treatment_impact,
514
+ )
515
+
516
+ reasons_text = "\n".join([f"- {x}" for x in result["reasons"]]) if result["reasons"] else "-"
517
+ alt_text = "\n".join([f"- {x}" for x in result["alternatives"]]) if result["alternatives"] else "-"
518
+ warnings_text = "\n".join([f"- {x}" for x in warnings]) if warnings else "Brak ostrzeżeń walidacyjnych."
519
+
520
+ form_data = {
521
+ "spine_section": spine_section,
522
+ "duration": duration,
523
+ "first_episode": yes_no(first_episode),
524
+ "recurrent_episode": yes_no(recurrent_episode),
525
+ "post_op": yes_no(post_op),
526
+ "axial_pain": yes_no(axial_pain),
527
+ "radicular_pain": yes_no(radicular_pain),
528
+ "paresthesia": yes_no(paresthesia),
529
+ "weakness": yes_no(weakness),
530
+ "gait_balance": yes_no(gait_balance),
531
+ "sphincter": yes_no(sphincter),
532
+ "saddle": yes_no(saddle),
533
+ "progressive_deficit": yes_no(progressive_deficit),
534
+ "myelopathy": yes_no(myelopathy),
535
+ "cancer_history": yes_no(cancer_history),
536
+ "weight_loss": yes_no(weight_loss),
537
+ "night_pain": yes_no(night_pain),
538
+ "fever": yes_no(fever),
539
+ "immunosuppression": yes_no(immunosuppression),
540
+ "infection_suspicion": yes_no(infection_suspicion),
541
+ "trauma_high": yes_no(trauma_high),
542
+ "trauma_low_risk": yes_no(trauma_low_risk),
543
+ "osteoporosis_steroids": yes_no(osteoporosis_steroids),
544
+ "no_deficit": yes_no(no_deficit),
545
+ "root_deficit": yes_no(root_deficit),
546
+ "multilevel_bilateral": yes_no(multilevel_bilateral),
547
+ "cord_signs": yes_no(cord_signs),
548
+ "conservative_none": yes_no(conservative_none),
549
+ "conservative_lt6": yes_no(conservative_lt6),
550
+ "conservative_ge6": yes_no(conservative_ge6),
551
+ "improved": yes_no(improved),
552
+ "no_improvement": yes_no(no_improvement),
553
+ "worsening": yes_no(worsening),
554
+ "suspect_disc": yes_no(suspect_disc),
555
+ "suspect_cord": yes_no(suspect_cord),
556
+ "suspect_tumor": yes_no(suspect_tumor),
557
+ "suspect_infection": yes_no(suspect_infection),
558
+ "qualify_surgery": yes_no(qualify_surgery),
559
+ "qualify_injection": yes_no(qualify_injection),
560
+ "treatment_impact": treatment_impact,
561
+ }
562
+
563
+ pdf_file = generate_pdf(form_data, result, warnings)
564
+ banner_html = render_html_banner(result["category"], result["imaging"], result["color"])
565
+
566
+ return (
567
+ banner_html,
568
+ result["category"],
569
+ result["imaging"],
570
+ result["clinical_path"],
571
+ result["urgency"],
572
+ result["contrast"],
573
+ reasons_text,
574
+ alt_text,
575
+ warnings_text,
576
+ pdf_file,
577
  )
578
 
 
 
579
 
580
+ with gr.Blocks(theme=gr.themes.Soft(), title="MR Spine CDS PRO+") as app:
581
+ gr.Markdown(
582
+ f"""
583
+ # MR Spine Clinical Decision Support – PRO+
584
+ **Autor:** {AUTHOR_NAME}
585
+ **Afiliacja:** {AUTHOR_AFFILIATION}
586
+
587
+ Ustrukturyzowany formularz kwalifikacji do badania MR kręgosłupa.
588
+ **Uwaga:** narzędzie wspomagające decyzję kliniczną, nie zastępuje oceny lekarza.
589
+ """
590
+ )
591
 
592
+ with gr.Group():
593
+ gr.Markdown("## A. Identyfikacja problemu")
594
+ with gr.Row():
595
+ spine_section = gr.Radio(
596
+ ["szyjnego", "piersiowego", "lędźwiowego"],
597
+ label="Odcinek kręgosłupa",
598
+ value="lędźwiowego",
599
+ )
600
+ duration = gr.Radio(
601
+ ["< 2 tygodni", "2–6 tygodni", "6–12 tygodni", "> 12 tygodni"],
602
+ label="Czas trwania objawów",
603
+ value="2–6 tygodni",
604
+ )
605
+ with gr.Row():
606
+ first_episode = gr.Checkbox(label="Pierwszy epizod")
607
+ recurrent_episode = gr.Checkbox(label="Nawrót / zaostrzenie przewlekłe")
608
+ post_op = gr.Checkbox(label="Pacjent po operacji kręgosłupa")
609
 
610
+ with gr.Group():
611
+ gr.Markdown("## B. Charakter objawów")
612
+ with gr.Row():
613
+ axial_pain = gr.Checkbox(label="Ból miejscowy (osiowy)")
614
+ radicular_pain = gr.Checkbox(label="Ból promieniujący (radikulopatia)")
615
+ paresthesia = gr.Checkbox(label="Parestezje / drętwienie")
616
+ weakness = gr.Checkbox(label="Osłabienie siły mięśniowej")
617
+ gait_balance = gr.Checkbox(label="Zaburzenia chodu / równowagi")
618
 
619
+ with gr.Group():
620
+ gr.Markdown("## C. Red flags")
621
+ with gr.Row():
622
+ sphincter = gr.Checkbox(label="Zaburzenia zwieraczy")
623
+ saddle = gr.Checkbox(label="Znieczulenie krocza")
624
+ progressive_deficit = gr.Checkbox(label="Postępujący niedowład")
625
+ myelopathy = gr.Checkbox(label="Objawy mielopatii")
626
+ with gr.Row():
627
+ cancer_history = gr.Checkbox(label="Wywiad nowotworowy")
628
+ weight_loss = gr.Checkbox(label="Utrata masy ciała")
629
+ night_pain = gr.Checkbox(label="Ból nocny / niemechaniczny")
630
+ with gr.Row():
631
+ fever = gr.Checkbox(label="Gorączka")
632
+ immunosuppression = gr.Checkbox(label="Immunosupresja")
633
+ infection_suspicion = gr.Checkbox(label="Podejrzenie infekcji")
634
+ with gr.Row():
635
+ trauma_high = gr.Checkbox(label="Uraz wysokiej energii")
636
+ trauma_low_risk = gr.Checkbox(label="Uraz niskoenergetyczny + ryzyko")
637
+ osteoporosis_steroids = gr.Checkbox(label="Osteoporoza / przewlekłe steroidy")
638
 
639
+ with gr.Group():
640
+ gr.Markdown("## D. Badanie neurologiczne")
641
+ with gr.Row():
642
+ no_deficit = gr.Checkbox(label="Brak deficytu")
643
+ root_deficit = gr.Checkbox(label="Deficyt korzeniowy")
644
+ multilevel_bilateral = gr.Checkbox(label="Deficyt wielopoziomowy / obustronny")
645
+ cord_signs = gr.Checkbox(label="Objawy rdzeniowe")
646
 
647
+ with gr.Group():
648
+ gr.Markdown("## E. Dotychczasowe leczenie")
649
+ with gr.Row():
650
+ conservative_none = gr.Checkbox(label="Brak leczenia zachowawczego")
651
+ conservative_lt6 = gr.Checkbox(label="Leczenie < 6 tygodni")
652
+ conservative_ge6 = gr.Checkbox(label="Leczenie ≥ 6 tygodni")
653
+ with gr.Row():
654
+ improved = gr.Checkbox(label="Poprawa")
655
+ no_improvement = gr.Checkbox(label="Brak poprawy")
656
+ worsening = gr.Checkbox(label="Pogorszenie")
657
 
658
+ with gr.Group():
659
+ gr.Markdown("## F. Pytanie kliniczne")
660
+ with gr.Row():
661
+ suspect_disc = gr.Checkbox(label="Podejrzenie dyskopatii")
662
+ suspect_cord = gr.Checkbox(label="Podejrzenie ucisku rdzenia")
663
+ suspect_tumor = gr.Checkbox(label="Podejrzenie nowotworu")
664
+ suspect_infection = gr.Checkbox(label="Podejrzenie infekcji")
665
+ with gr.Row():
666
+ qualify_surgery = gr.Checkbox(label="Kwalifikacja do zabiegu")
667
+ qualify_injection = gr.Checkbox(label="Kwalifikacja do blokady / iniekcji")
668
+ treatment_impact = gr.Radio(
669
+ ["TAK", "NIE", "NIEPEWNE"],
670
+ label="Czy wynik MR zmieni leczenie?",
671
+ value="NIEPEWNE",
672
+ )
673
 
674
+ with gr.Row():
675
+ run_button = gr.Button("Generuj rekomendację", variant="primary")
676
+ clear_button = gr.Button("Wyczyść formularz")
677
 
678
+ gr.Markdown("## Wynik")
679
+ out_banner = gr.HTML()
680
+ out_category = gr.Textbox(label="Kategoria rekomendacji")
681
+ out_imaging = gr.Textbox(label="Sugerowane badanie")
682
+ out_path = gr.Textbox(label="Ścieżka kliniczna")
683
+ out_urgency = gr.Textbox(label="Tryb")
684
+ out_contrast = gr.Textbox(label="Kontrast")
685
+ out_reasons = gr.Textbox(label="Uzasadnienie", lines=8)
686
+ out_alternatives = gr.Textbox(label="Alternatywy / dalsze kroki", lines=5)
687
+ out_warnings = gr.Textbox(label="Ostrzeżenia walidacyjne", lines=4)
688
+ out_pdf = gr.File(label="Pobierz raport PDF")
689
 
690
+ inputs = [
691
+ spine_section,
692
+ duration,
693
+ first_episode,
694
+ recurrent_episode,
695
+ post_op,
696
+ axial_pain,
697
+ radicular_pain,
698
+ paresthesia,
699
+ weakness,
700
+ gait_balance,
701
+ sphincter,
702
+ saddle,
703
+ progressive_deficit,
704
+ myelopathy,
705
+ cancer_history,
706
+ weight_loss,
707
+ night_pain,
708
+ fever,
709
+ immunosuppression,
710
+ infection_suspicion,
711
+ trauma_high,
712
+ trauma_low_risk,
713
+ osteoporosis_steroids,
714
+ no_deficit,
715
+ root_deficit,
716
+ multilevel_bilateral,
717
+ cord_signs,
718
+ conservative_none,
719
+ conservative_lt6,
720
+ conservative_ge6,
721
+ improved,
722
+ no_improvement,
723
+ worsening,
724
+ suspect_disc,
725
+ suspect_cord,
726
+ suspect_tumor,
727
+ suspect_infection,
728
+ qualify_surgery,
729
+ qualify_injection,
730
+ treatment_impact,
731
+ ]
732
 
733
+ outputs = [
734
+ out_banner,
735
+ out_category,
736
+ out_imaging,
737
+ out_path,
738
+ out_urgency,
739
+ out_contrast,
740
+ out_reasons,
741
+ out_alternatives,
742
+ out_warnings,
743
+ out_pdf,
744
+ ]
745
+
746
+ run_button.click(fn=run_app, inputs=inputs, outputs=outputs)
747
+
748
+ clear_button.click(
749
+ fn=lambda: (
750
+ "lędźwiowego",
751
+ "2–6 tygodni",
752
+ False,
753
+ False,
754
+ False,
755
+ False,
756
+ False,
757
+ False,
758
+ False,
759
+ False,
760
+ False,
761
+ False,
762
+ False,
763
+ False,
764
+ False,
765
+ False,
766
+ False,
767
+ False,
768
+ False,
769
+ False,
770
+ False,
771
+ False,
772
+ False,
773
+ False,
774
+ False,
775
+ False,
776
+ False,
777
+ False,
778
+ False,
779
+ False,
780
+ False,
781
+ False,
782
+ False,
783
+ False,
784
+ False,
785
+ False,
786
+ False,
787
+ False,
788
+ False,
789
+ "NIEPEWNE",
790
+ "",
791
+ "",
792
+ "",
793
+ "",
794
+ "",
795
+ "",
796
+ "",
797
+ "",
798
+ None,
799
+ ),
800
+ inputs=[],
801
+ outputs=inputs + outputs,
802
+ )
803
 
804
  app.launch()