sugan04 commited on
Commit
9f42ce1
Β·
verified Β·
1 Parent(s): ba5ff39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -160
app.py CHANGED
@@ -11,7 +11,6 @@ import modal
11
  from PIL import Image
12
  import numpy as np
13
  import io
14
- import base64
15
  import datetime
16
  from huggingface_hub import InferenceClient
17
  from reportlab.lib.pagesizes import A4
@@ -19,20 +18,9 @@ from reportlab.lib import colors
19
  from reportlab.lib.units import cm
20
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle, HRFlowable
21
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
22
- from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
23
- from reportlab.pdfgen import canvas
24
- from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
25
 
26
- os.environ["MODAL_TOKEN_ID"] = os.environ.get("MODAL_TOKEN_ID", "")
27
- os.environ["MODAL_TOKEN_SECRET"] = os.environ.get("MODAL_TOKEN_SECRET", "")
28
-
29
- SurgiSightDetector = modal.Cls.from_name("surgisight", "SurgiSightDetector")
30
- detector = SurgiSightDetector()
31
-
32
- client = InferenceClient(
33
- model="meta-llama/Llama-3.1-8B-Instruct",
34
- token=os.environ.get("HF_TOKEN")
35
- )
36
 
37
  CLASS_NAMES = [
38
  "Black Background", "Abdominal Wall", "Liver", "Gastrointestinal Tract",
@@ -40,9 +28,23 @@ CLASS_NAMES = [
40
  "L-hook Electrocautery", "Gallbladder", "Hepatic Vein", "Liver Ligament"
41
  ]
42
  DANGER_CLASSES = ["Hepatic Vein", "Cystic Duct", "Blood"]
43
-
44
- # Store last result globally for PDF generation
45
  last_result = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
 
48
  def pil_to_bytes(pil_image):
@@ -51,69 +53,67 @@ def pil_to_bytes(pil_image):
51
  return buf.getvalue()
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def generate_pdf(original_image, annotated_image, seen, alert, explanation):
55
  pdf_path = "/tmp/surgisight_report.pdf"
56
  doc = SimpleDocTemplate(
57
- pdf_path,
58
- pagesize=A4,
59
- rightMargin=2*cm,
60
- leftMargin=2*cm,
61
- topMargin=2*cm,
62
- bottomMargin=2*cm
63
  )
64
-
65
  styles = getSampleStyleSheet()
66
-
67
- # Custom styles
68
- title_style = ParagraphStyle(
69
- 'Title', parent=styles['Title'],
70
  fontSize=22, textColor=colors.HexColor('#0f2027'),
71
- spaceAfter=4, fontName='Helvetica-Bold', alignment=TA_CENTER
72
- )
73
- subtitle_style = ParagraphStyle(
74
- 'Subtitle', parent=styles['Normal'],
75
  fontSize=10, textColor=colors.HexColor('#888888'),
76
- spaceAfter=2, alignment=TA_CENTER
77
- )
78
- section_style = ParagraphStyle(
79
- 'Section', parent=styles['Normal'],
80
  fontSize=13, textColor=colors.HexColor('#203a43'),
81
- spaceAfter=6, spaceBefore=12, fontName='Helvetica-Bold'
82
- )
83
- body_style = ParagraphStyle(
84
- 'Body', parent=styles['Normal'],
85
  fontSize=10, textColor=colors.HexColor('#1a1a1a'),
86
- spaceAfter=4, leading=16
87
- )
88
- danger_style = ParagraphStyle(
89
- 'Danger', parent=styles['Normal'],
90
  fontSize=10, textColor=colors.HexColor('#cc0000'),
91
  spaceAfter=4, leading=16, fontName='Helvetica-Bold',
92
- backColor=colors.HexColor('#fff0f0'),
93
- borderPadding=(6, 8, 6, 8)
94
- )
95
- safe_style = ParagraphStyle(
96
- 'Safe', parent=styles['Normal'],
97
  fontSize=10, textColor=colors.HexColor('#1a7a40'),
98
  spaceAfter=4, leading=16, fontName='Helvetica-Bold',
99
- backColor=colors.HexColor('#f0fff4'),
100
- borderPadding=(6, 8, 6, 8)
101
- )
102
- caption_style = ParagraphStyle(
103
- 'Caption', parent=styles['Normal'],
104
  fontSize=8, textColor=colors.HexColor('#888888'),
105
- alignment=TA_CENTER, spaceAfter=4
106
- )
107
- footer_style = ParagraphStyle(
108
- 'Footer', parent=styles['Normal'],
109
- fontSize=8, textColor=colors.HexColor('#aaaaaa'),
110
- alignment=TA_CENTER
111
- )
112
 
113
  story = []
114
  timestamp = datetime.datetime.now().strftime("%B %d, %Y at %H:%M")
115
 
116
- # ── Header ────────────────────────────────────────────────────────────────
117
  story.append(Spacer(1, 0.3*cm))
118
  story.append(Paragraph("SurgiSight", title_style))
119
  story.append(Paragraph("Surgical Anatomy Analysis Report", subtitle_style))
@@ -122,100 +122,80 @@ def generate_pdf(original_image, annotated_image, seen, alert, explanation):
122
  story.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#203a43')))
123
  story.append(Spacer(1, 0.4*cm))
124
 
125
- # ── Images side by side ───────────────────────────────────────────────────
126
  story.append(Paragraph("Segmentation Output", section_style))
127
-
128
  img_width = 8.5*cm
129
  img_height = 6.5*cm
130
-
131
  orig_buf = io.BytesIO()
132
  original_image.save(orig_buf, format="PNG")
133
  orig_buf.seek(0)
134
- orig_rl = RLImage(orig_buf, width=img_width, height=img_height)
135
-
136
  ann_buf = io.BytesIO()
137
  annotated_image.save(ann_buf, format="PNG")
138
  ann_buf.seek(0)
139
- ann_rl = RLImage(ann_buf, width=img_width, height=img_height)
140
-
141
  img_table = Table(
142
- [[orig_rl, ann_rl],
 
143
  [Paragraph("Original Frame", caption_style), Paragraph("AI Segmented Output", caption_style)]],
144
  colWidths=[img_width + 0.5*cm, img_width + 0.5*cm]
145
  )
146
  img_table.setStyle(TableStyle([
147
- ('ALIGN', (0,0), (-1,-1), 'CENTER'),
148
- ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
149
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#f5f5f5')),
150
- ('ROUNDEDCORNERS', [6, 6, 6, 6]),
151
  ('BOX', (0,0), (0,0), 0.5, colors.HexColor('#dddddd')),
152
  ('BOX', (1,0), (1,0), 0.5, colors.HexColor('#dddddd')),
153
  ]))
154
  story.append(img_table)
155
  story.append(Spacer(1, 0.5*cm))
156
 
157
- # ── Safety Alert ──────────────────────────────────────────────────────────
158
  story.append(Paragraph("Safety Assessment", section_style))
159
  if any(d in alert for d in DANGER_CLASSES):
160
- story.append(Paragraph(f"⚠ {alert}", danger_style))
161
  else:
162
- story.append(Paragraph(f"βœ“ {alert}", safe_style))
163
  story.append(Spacer(1, 0.4*cm))
164
 
165
- # ── Detection Results Table ───────────────────────────────────────────────
166
  story.append(Paragraph("Detected Tissues & Instruments", section_style))
167
-
168
  table_data = [["Structure", "Confidence", "Risk Level"]]
 
169
  for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
170
  if name == "Black Background":
171
  continue
172
- risk = "⚠ DANGER" if name in DANGER_CLASSES else "Safe"
173
- risk_color = colors.HexColor('#cc0000') if name in DANGER_CLASSES else colors.HexColor('#1a7a40')
174
- conf_pct = f"{conf:.1%}"
175
- bar = "β–ˆ" * int(conf * 10) + "β–‘" * (10 - int(conf * 10))
176
- table_data.append([name, f"{conf_pct} {bar}", risk])
177
-
178
  det_table = Table(table_data, colWidths=[6*cm, 7*cm, 3.5*cm])
179
- det_table_style = [
180
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#203a43')),
181
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
182
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
183
  ('FONTSIZE', (0,0), (-1,0), 10),
184
- ('ALIGN', (0,0), (-1,-1), 'LEFT'),
185
- ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
186
  ('FONTSIZE', (0,1), (-1,-1), 9),
187
  ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#f9f9f9'), colors.white]),
188
  ('GRID', (0,0), (-1,-1), 0.3, colors.HexColor('#dddddd')),
189
- ('TOPPADDING', (0,0), (-1,-1), 6),
190
- ('BOTTOMPADDING', (0,0), (-1,-1), 6),
191
  ('LEFTPADDING', (0,0), (-1,-1), 8),
192
  ]
193
- # Color danger rows red
194
- for i, (name, conf) in enumerate(sorted(seen.items(), key=lambda x: -x[1])):
195
- if name == "Black Background":
196
- continue
197
- if name in DANGER_CLASSES:
198
- det_table_style.append(('TEXTCOLOR', (2, i+1), (2, i+1), colors.HexColor('#cc0000')))
199
- det_table_style.append(('FONTNAME', (2, i+1), (2, i+1), 'Helvetica-Bold'))
200
- else:
201
- det_table_style.append(('TEXTCOLOR', (2, i+1), (2, i+1), colors.HexColor('#1a7a40')))
202
-
203
- det_table.setStyle(TableStyle(det_table_style))
204
  story.append(det_table)
205
  story.append(Spacer(1, 0.5*cm))
206
 
207
- # ── Anatomy Explanation ───────────────────────────────────────────────────
208
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cccccc')))
209
  story.append(Spacer(1, 0.3*cm))
210
  story.append(Paragraph("Anatomy Teaching Note", section_style))
211
  story.append(Paragraph(explanation, body_style))
212
  story.append(Spacer(1, 0.5*cm))
213
 
214
- # ── Model Info ────────────────────────────────────────────────────────────
215
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cccccc')))
216
  story.append(Spacer(1, 0.3*cm))
217
  model_data = [
218
- ["Detection Model", "YOLOv26-seg fine-tuned on CholecSeg8k (MICCAI 2020)"],
219
  ["LLM", "Meta Llama 3.1 8B Instruct"],
220
  ["Inference", "Modal GPU (T4)"],
221
  ["Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"],
@@ -223,38 +203,36 @@ def generate_pdf(original_image, annotated_image, seen, alert, explanation):
223
  ]
224
  model_table = Table(model_data, colWidths=[4.5*cm, 12*cm])
225
  model_table.setStyle(TableStyle([
226
- ('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'),
227
- ('FONTSIZE', (0,0), (-1,-1), 8),
228
  ('TEXTCOLOR', (0,0), (0,-1), colors.HexColor('#203a43')),
229
  ('TEXTCOLOR', (1,0), (1,-1), colors.HexColor('#555555')),
230
  ('VALIGN', (0,0), (-1,-1), 'TOP'),
231
- ('TOPPADDING', (0,0), (-1,-1), 3),
232
- ('BOTTOMPADDING', (0,0), (-1,-1), 3),
233
  ('ROWBACKGROUNDS', (0,0), (-1,-1), [colors.HexColor('#f5f5f5'), colors.white]),
234
  ]))
235
  story.append(model_table)
236
  story.append(Spacer(1, 0.4*cm))
237
-
238
- # ── Disclaimer ────────────────────────────────────────────────────────────
239
  story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#203a43')))
240
  story.append(Spacer(1, 0.2*cm))
241
  story.append(Paragraph(
242
- "⚠ DISCLAIMER: This report is generated by an AI research prototype for educational purposes only. "
243
  "It is NOT a medical device and must not be used for clinical decision-making. "
244
  "All demo footage is from the publicly available CholecSeg8k research dataset β€” no real patient data involved.",
245
- footer_style
246
- ))
247
- story.append(Paragraph("Built for Build Small Hackathon 2026 Β· huggingface.co/build-small-hackathon", footer_style))
248
-
249
  doc.build(story)
250
  return pdf_path
251
 
252
 
253
  def segment_image(input_image, conf_threshold=0.25):
254
- global last_result
255
  if input_image is None:
256
- return None, "Please upload a surgical frame to begin.", "No image provided.", "No explanation yet.", gr.update(visible=False)
 
 
 
257
 
 
258
  image_bytes = pil_to_bytes(input_image)
259
  result = detector.run.remote(image_bytes, conf_threshold)
260
 
@@ -284,50 +262,103 @@ def segment_image(input_image, conf_threshold=0.25):
284
  else:
285
  alert = "All clear β€” no critical structures flagged."
286
 
287
- if seen:
288
- tissue_list = [n for n in seen.keys() if n != "Black Background"]
289
- if tissue_list:
290
- prompt = (
291
- f"You are a surgical anatomy teacher helping a junior medical resident. "
292
- f"These tissues and instruments were detected in a laparoscopic cholecystectomy frame: {', '.join(tissue_list)}. "
293
- f"In 3 sentences, explain what the resident should know about these structures β€” "
294
- f"what they are, why they matter, and what to be careful about."
295
- )
296
- try:
297
- messages = [{"role": "user", "content": prompt}]
298
- response = client.chat_completion(messages, max_tokens=180, temperature=0.4)
299
- explanation = response.choices[0].message.content.strip()
300
- except Exception as e:
301
- explanation = f"Explanation unavailable: {str(e)}"
302
- else:
303
- explanation = "Only background detected β€” no tissue explanation needed."
304
- else:
305
- explanation = "No tissues detected to explain."
 
 
306
 
307
- # Store for PDF generation
308
  last_result = {
309
- "original": input_image,
310
- "annotated": annotated_image,
311
- "seen": seen,
312
- "alert": alert,
313
- "explanation": explanation
314
  }
315
 
316
- return annotated_image, summary, alert, explanation, gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
 
319
  def export_pdf():
320
  global last_result
321
  if not last_result:
322
  return None
323
- pdf_path = generate_pdf(
324
- last_result["original"],
325
- last_result["annotated"],
326
- last_result["seen"],
327
- last_result["alert"],
328
- last_result["explanation"]
329
  )
330
- return pdf_path
331
 
332
 
333
  css = """
@@ -342,7 +373,8 @@ body { font-family: 'Inter', sans-serif; }
342
  #danger-box textarea { font-family: 'Courier New', monospace !important; font-size: 0.9rem !important; font-weight: bold !important; background: #1a0a0a !important; color: #ff6b6b !important; border-radius: 10px !important; border: 1px solid #ff4444 !important; }
343
  #explain-box textarea { font-family: 'Inter', sans-serif !important; font-size: 0.88rem !important; line-height: 1.7 !important; background: #0a1a0f !important; color: #6ee7b7 !important; border-radius: 10px !important; border: 1px solid #34d399 !important; }
344
  .footer-note { text-align: center; font-size: 0.78rem; color: #888; margin-top: 16px; padding-bottom: 24px; }
345
- #pdf-btn { background: linear-gradient(135deg, #203a43, #2c5364) !important; color: white !important; border-radius: 10px !important; margin-top: 8px; }
 
346
  """
347
 
348
  header_html = """
@@ -350,7 +382,7 @@ header_html = """
350
  <h1>πŸ”¬ Surgical Tissue Segmentation</h1>
351
  <p>Upload a laparoscopic surgery frame β€” or click any example below. The AI identifies tissues, flags danger zones, and explains anatomy for surgical trainees.</p>
352
  <div class="badge-row">
353
- <span class="badge">YOLO26n-seg</span>
354
  <span class="badge">Llama 3.1 8B</span>
355
  <span class="badge">Modal GPU</span>
356
  <span class="badge">CholecSeg8k Dataset</span>
@@ -369,6 +401,8 @@ footer_html = """
369
 
370
  with gr.Blocks(title="SurgiSight β€” Surgical Tissue Segmentation") as demo:
371
  gr.HTML(header_html)
 
 
372
  with gr.Row():
373
  with gr.Column(scale=1):
374
  input_img = gr.Image(type="pil", label="Input β€” Laparoscopic Frame", height=350)
@@ -383,14 +417,40 @@ with gr.Blocks(title="SurgiSight β€” Surgical Tissue Segmentation") as demo:
383
  placeholder="Safety alert will appear here after running segmentation...")
384
  output_text = gr.Textbox(label="Detected Tissues & Confidence", lines=6, elem_id="output-text",
385
  placeholder="Tissue detection results will appear here...")
386
- explain_box = gr.Textbox(label="🧠 Anatomy Explanation for Surgical Trainees",
387
- lines=5, elem_id="explain-box",
388
  placeholder="AI anatomy explanation will appear here after segmentation...")
389
 
 
390
  with gr.Row():
391
- pdf_btn = gr.Button("πŸ“„ Download Full Report (PDF)", visible=False, elem_id="pdf-btn", size="lg")
392
  pdf_output = gr.File(label="Your Report", visible=False)
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  gr.Examples(
395
  examples=[
396
  ["examples/frame_80_endo.png"],
@@ -402,20 +462,28 @@ with gr.Blocks(title="SurgiSight β€” Surgical Tissue Segmentation") as demo:
402
  examples_per_page=3
403
  )
404
 
 
 
 
405
  run_btn.click(
406
  fn=segment_image,
407
  inputs=[input_img, conf_slider],
408
- outputs=[output_img, output_text, danger_box, explain_box, pdf_btn],
 
409
  show_progress="full"
410
  )
411
 
412
- pdf_btn.click(
413
- fn=export_pdf,
414
- inputs=[],
415
- outputs=[pdf_output]
416
  ).then(fn=lambda: gr.update(visible=True), outputs=[pdf_output])
417
 
418
- gr.HTML(footer_html)
 
 
 
 
 
 
 
419
 
420
  if __name__ == "__main__":
421
- demo.launch(css=css, inbrowser=True)
 
11
  from PIL import Image
12
  import numpy as np
13
  import io
 
14
  import datetime
15
  from huggingface_hub import InferenceClient
16
  from reportlab.lib.pagesizes import A4
 
18
  from reportlab.lib.units import cm
19
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle, HRFlowable
20
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
21
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT
 
 
22
 
23
+ print("All imports OK")
 
 
 
 
 
 
 
 
 
24
 
25
  CLASS_NAMES = [
26
  "Black Background", "Abdominal Wall", "Liver", "Gastrointestinal Tract",
 
28
  "L-hook Electrocautery", "Gallbladder", "Hepatic Vein", "Liver Ligament"
29
  ]
30
  DANGER_CLASSES = ["Hepatic Vein", "Cystic Duct", "Blood"]
 
 
31
  last_result = {}
32
+ chat_context = {} # stores detected tissues for chat context
33
+
34
+ try:
35
+ client = InferenceClient(
36
+ model="meta-llama/Llama-3.1-8B-Instruct",
37
+ token=os.environ.get("HF_TOKEN")
38
+ )
39
+ print("InferenceClient OK")
40
+ except Exception as e:
41
+ client = None
42
+ print(f"InferenceClient failed (non-fatal): {e}")
43
+
44
+
45
+ def get_detector():
46
+ SurgiSightDetector = modal.Cls.from_name("surgisight", "SurgiSightDetector")
47
+ return SurgiSightDetector()
48
 
49
 
50
  def pil_to_bytes(pil_image):
 
53
  return buf.getvalue()
54
 
55
 
56
+ def generate_suggested_questions(tissue_list):
57
+ """Generate 3 contextual suggested questions based on detected tissues."""
58
+ questions = []
59
+ # Danger-specific questions first
60
+ for t in tissue_list:
61
+ if t == "Hepatic Vein":
62
+ questions.append("Why is the hepatic vein so dangerous to nick?")
63
+ elif t == "Cystic Duct":
64
+ questions.append("How do I safely identify the cystic duct?")
65
+ elif t == "Blood":
66
+ questions.append("What are the steps to control unexpected bleeding?")
67
+ elif t == "Gallbladder":
68
+ questions.append("What is the critical view of safety for gallbladder removal?")
69
+ elif t == "L-hook Electrocautery":
70
+ questions.append("What are the risks of using electrocautery near the bile duct?")
71
+ elif t == "Liver":
72
+ questions.append("How does liver retraction affect visibility in laparoscopic surgery?")
73
+ if len(questions) >= 2:
74
+ break
75
+ # Always add a general fallback
76
+ questions.append("What are common complications in laparoscopic cholecystectomy?")
77
+ return questions[:3]
78
+
79
+
80
  def generate_pdf(original_image, annotated_image, seen, alert, explanation):
81
  pdf_path = "/tmp/surgisight_report.pdf"
82
  doc = SimpleDocTemplate(
83
+ pdf_path, pagesize=A4,
84
+ rightMargin=2*cm, leftMargin=2*cm,
85
+ topMargin=2*cm, bottomMargin=2*cm
 
 
 
86
  )
 
87
  styles = getSampleStyleSheet()
88
+ title_style = ParagraphStyle('Title', parent=styles['Title'],
 
 
 
89
  fontSize=22, textColor=colors.HexColor('#0f2027'),
90
+ spaceAfter=4, fontName='Helvetica-Bold', alignment=TA_CENTER)
91
+ subtitle_style = ParagraphStyle('Subtitle', parent=styles['Normal'],
 
 
92
  fontSize=10, textColor=colors.HexColor('#888888'),
93
+ spaceAfter=2, alignment=TA_CENTER)
94
+ section_style = ParagraphStyle('Section', parent=styles['Normal'],
 
 
95
  fontSize=13, textColor=colors.HexColor('#203a43'),
96
+ spaceAfter=6, spaceBefore=12, fontName='Helvetica-Bold')
97
+ body_style = ParagraphStyle('Body', parent=styles['Normal'],
 
 
98
  fontSize=10, textColor=colors.HexColor('#1a1a1a'),
99
+ spaceAfter=4, leading=16)
100
+ danger_style = ParagraphStyle('Danger', parent=styles['Normal'],
 
 
101
  fontSize=10, textColor=colors.HexColor('#cc0000'),
102
  spaceAfter=4, leading=16, fontName='Helvetica-Bold',
103
+ backColor=colors.HexColor('#fff0f0'), borderPadding=(6,8,6,8))
104
+ safe_style = ParagraphStyle('Safe', parent=styles['Normal'],
 
 
 
105
  fontSize=10, textColor=colors.HexColor('#1a7a40'),
106
  spaceAfter=4, leading=16, fontName='Helvetica-Bold',
107
+ backColor=colors.HexColor('#f0fff4'), borderPadding=(6,8,6,8))
108
+ caption_style = ParagraphStyle('Caption', parent=styles['Normal'],
 
 
 
109
  fontSize=8, textColor=colors.HexColor('#888888'),
110
+ alignment=TA_CENTER, spaceAfter=4)
111
+ footer_style = ParagraphStyle('Footer', parent=styles['Normal'],
112
+ fontSize=8, textColor=colors.HexColor('#aaaaaa'), alignment=TA_CENTER)
 
 
 
 
113
 
114
  story = []
115
  timestamp = datetime.datetime.now().strftime("%B %d, %Y at %H:%M")
116
 
 
117
  story.append(Spacer(1, 0.3*cm))
118
  story.append(Paragraph("SurgiSight", title_style))
119
  story.append(Paragraph("Surgical Anatomy Analysis Report", subtitle_style))
 
122
  story.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#203a43')))
123
  story.append(Spacer(1, 0.4*cm))
124
 
 
125
  story.append(Paragraph("Segmentation Output", section_style))
 
126
  img_width = 8.5*cm
127
  img_height = 6.5*cm
 
128
  orig_buf = io.BytesIO()
129
  original_image.save(orig_buf, format="PNG")
130
  orig_buf.seek(0)
 
 
131
  ann_buf = io.BytesIO()
132
  annotated_image.save(ann_buf, format="PNG")
133
  ann_buf.seek(0)
 
 
134
  img_table = Table(
135
+ [[RLImage(orig_buf, width=img_width, height=img_height),
136
+ RLImage(ann_buf, width=img_width, height=img_height)],
137
  [Paragraph("Original Frame", caption_style), Paragraph("AI Segmented Output", caption_style)]],
138
  colWidths=[img_width + 0.5*cm, img_width + 0.5*cm]
139
  )
140
  img_table.setStyle(TableStyle([
141
+ ('ALIGN', (0,0), (-1,-1), 'CENTER'), ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
 
142
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#f5f5f5')),
 
143
  ('BOX', (0,0), (0,0), 0.5, colors.HexColor('#dddddd')),
144
  ('BOX', (1,0), (1,0), 0.5, colors.HexColor('#dddddd')),
145
  ]))
146
  story.append(img_table)
147
  story.append(Spacer(1, 0.5*cm))
148
 
 
149
  story.append(Paragraph("Safety Assessment", section_style))
150
  if any(d in alert for d in DANGER_CLASSES):
151
+ story.append(Paragraph(f"WARNING: {alert}", danger_style))
152
  else:
153
+ story.append(Paragraph(f"SAFE: {alert}", safe_style))
154
  story.append(Spacer(1, 0.4*cm))
155
 
 
156
  story.append(Paragraph("Detected Tissues & Instruments", section_style))
 
157
  table_data = [["Structure", "Confidence", "Risk Level"]]
158
+ rows_danger = []
159
  for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
160
  if name == "Black Background":
161
  continue
162
+ risk = "DANGER" if name in DANGER_CLASSES else "Safe"
163
+ bar = "\u2588" * int(conf * 10) + "\u2591" * (10 - int(conf * 10))
164
+ table_data.append([name, f"{conf:.1%} {bar}", risk])
165
+ rows_danger.append(name in DANGER_CLASSES)
 
 
166
  det_table = Table(table_data, colWidths=[6*cm, 7*cm, 3.5*cm])
167
+ det_style = [
168
  ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#203a43')),
169
  ('TEXTCOLOR', (0,0), (-1,0), colors.white),
170
  ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
171
  ('FONTSIZE', (0,0), (-1,0), 10),
172
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'), ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
 
173
  ('FONTSIZE', (0,1), (-1,-1), 9),
174
  ('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.HexColor('#f9f9f9'), colors.white]),
175
  ('GRID', (0,0), (-1,-1), 0.3, colors.HexColor('#dddddd')),
176
+ ('TOPPADDING', (0,0), (-1,-1), 6), ('BOTTOMPADDING', (0,0), (-1,-1), 6),
 
177
  ('LEFTPADDING', (0,0), (-1,-1), 8),
178
  ]
179
+ for i, is_danger in enumerate(rows_danger):
180
+ r = i + 1
181
+ col = colors.HexColor('#cc0000') if is_danger else colors.HexColor('#1a7a40')
182
+ det_style.append(('TEXTCOLOR', (2,r), (2,r), col))
183
+ if is_danger:
184
+ det_style.append(('FONTNAME', (2,r), (2,r), 'Helvetica-Bold'))
185
+ det_table.setStyle(TableStyle(det_style))
 
 
 
 
186
  story.append(det_table)
187
  story.append(Spacer(1, 0.5*cm))
188
 
 
189
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cccccc')))
190
  story.append(Spacer(1, 0.3*cm))
191
  story.append(Paragraph("Anatomy Teaching Note", section_style))
192
  story.append(Paragraph(explanation, body_style))
193
  story.append(Spacer(1, 0.5*cm))
194
 
 
195
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cccccc')))
196
  story.append(Spacer(1, 0.3*cm))
197
  model_data = [
198
+ ["Detection Model", "YOLOv8-seg fine-tuned on CholecSeg8k (MICCAI 2020)"],
199
  ["LLM", "Meta Llama 3.1 8B Instruct"],
200
  ["Inference", "Modal GPU (T4)"],
201
  ["Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"],
 
203
  ]
204
  model_table = Table(model_data, colWidths=[4.5*cm, 12*cm])
205
  model_table.setStyle(TableStyle([
206
+ ('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'), ('FONTSIZE', (0,0), (-1,-1), 8),
 
207
  ('TEXTCOLOR', (0,0), (0,-1), colors.HexColor('#203a43')),
208
  ('TEXTCOLOR', (1,0), (1,-1), colors.HexColor('#555555')),
209
  ('VALIGN', (0,0), (-1,-1), 'TOP'),
210
+ ('TOPPADDING', (0,0), (-1,-1), 3), ('BOTTOMPADDING', (0,0), (-1,-1), 3),
 
211
  ('ROWBACKGROUNDS', (0,0), (-1,-1), [colors.HexColor('#f5f5f5'), colors.white]),
212
  ]))
213
  story.append(model_table)
214
  story.append(Spacer(1, 0.4*cm))
 
 
215
  story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#203a43')))
216
  story.append(Spacer(1, 0.2*cm))
217
  story.append(Paragraph(
218
+ "DISCLAIMER: This report is generated by an AI research prototype for educational purposes only. "
219
  "It is NOT a medical device and must not be used for clinical decision-making. "
220
  "All demo footage is from the publicly available CholecSeg8k research dataset β€” no real patient data involved.",
221
+ footer_style))
222
+ story.append(Paragraph("Built for Build Small Hackathon 2026", footer_style))
 
 
223
  doc.build(story)
224
  return pdf_path
225
 
226
 
227
  def segment_image(input_image, conf_threshold=0.25):
228
+ global last_result, chat_context
229
  if input_image is None:
230
+ return (None, "Please upload a surgical frame to begin.", "No image provided.",
231
+ "No explanation yet.", gr.update(visible=False),
232
+ gr.update(visible=False), gr.update(visible=False),
233
+ gr.update(visible=False), gr.update(visible=False), [])
234
 
235
+ detector = get_detector()
236
  image_bytes = pil_to_bytes(input_image)
237
  result = detector.run.remote(image_bytes, conf_threshold)
238
 
 
262
  else:
263
  alert = "All clear β€” no critical structures flagged."
264
 
265
+ tissue_list = [n for n in seen.keys() if n != "Black Background"]
266
+ explanation = "No tissues detected to explain."
267
+ if tissue_list and client:
268
+ prompt = (
269
+ f"You are a surgical anatomy teacher helping a junior medical resident. "
270
+ f"These tissues and instruments were detected in a laparoscopic cholecystectomy frame: {', '.join(tissue_list)}. "
271
+ f"In 3 sentences, explain what the resident should know about these structures β€” "
272
+ f"what they are, why they matter, and what to be careful about."
273
+ )
274
+ try:
275
+ response = client.chat_completion([{"role": "user", "content": prompt}], max_tokens=180, temperature=0.4)
276
+ explanation = response.choices[0].message.content.strip()
277
+ except Exception as e:
278
+ explanation = f"Explanation unavailable: {str(e)}"
279
+
280
+ # Store context for chat
281
+ chat_context = {
282
+ "tissue_list": tissue_list,
283
+ "alert": alert,
284
+ "initial_explanation": explanation
285
+ }
286
 
 
287
  last_result = {
288
+ "original": input_image, "annotated": annotated_image,
289
+ "seen": seen, "alert": alert, "explanation": explanation
 
 
 
290
  }
291
 
292
+ # Generate suggested questions
293
+ suggested = generate_suggested_questions(tissue_list) if tissue_list else []
294
+ q1 = suggested[0] if len(suggested) > 0 else ""
295
+ q2 = suggested[1] if len(suggested) > 1 else ""
296
+ q3 = suggested[2] if len(suggested) > 2 else ""
297
+
298
+ # Initial chat message from assistant
299
+ initial_chat = [{"role": "assistant", "content": f"I've analysed the frame. I detected: **{', '.join(tissue_list) if tissue_list else 'no tissues'}**.\n\n{explanation}\n\nFeel free to ask me anything about these structures or general laparoscopic surgery!"}]
300
+
301
+ return (annotated_image, summary, alert, explanation,
302
+ gr.update(visible=True), # pdf_btn
303
+ gr.update(visible=True), # chat section
304
+ gr.update(value=q1, visible=bool(q1)),
305
+ gr.update(value=q2, visible=bool(q2)),
306
+ gr.update(value=q3, visible=bool(q3)),
307
+ initial_chat)
308
+
309
+
310
+ def chat_response(message, history):
311
+ global chat_context
312
+ if not message.strip():
313
+ return history, ""
314
+
315
+ tissue_list = chat_context.get("tissue_list", [])
316
+ alert = chat_context.get("alert", "")
317
+
318
+ if tissue_list:
319
+ system_prompt = (
320
+ f"You are SurgiSight, an expert surgical anatomy assistant helping medical trainees. "
321
+ f"The current laparoscopic frame shows these detected tissues/instruments: {', '.join(tissue_list)}. "
322
+ f"Safety status: {alert}. "
323
+ f"Answer questions about these specific structures or general laparoscopic/surgical questions. "
324
+ f"Be concise, educational, and clinically accurate. Use 2-4 sentences max per response."
325
+ )
326
+ else:
327
+ system_prompt = (
328
+ "You are SurgiSight, an expert surgical anatomy assistant for medical trainees. "
329
+ "Answer general laparoscopic surgery and anatomy questions concisely and accurately. "
330
+ "Use 2-4 sentences max per response."
331
+ )
332
+
333
+ messages = [{"role": "system", "content": system_prompt}]
334
+ for msg in history:
335
+ messages.append({"role": msg["role"], "content": msg["content"]})
336
+ messages.append({"role": "user", "content": message})
337
+
338
+ try:
339
+ response = client.chat_completion(messages, max_tokens=200, temperature=0.5)
340
+ reply = response.choices[0].message.content.strip()
341
+ except Exception as e:
342
+ reply = f"Sorry, I couldn't process that: {str(e)}"
343
+
344
+ history.append({"role": "user", "content": message})
345
+ history.append({"role": "assistant", "content": reply})
346
+ return history, ""
347
+
348
+
349
+ def use_suggested(question, history):
350
+ """Fill chat with a suggested question and immediately get response."""
351
+ return chat_response(question, history)
352
 
353
 
354
  def export_pdf():
355
  global last_result
356
  if not last_result:
357
  return None
358
+ return generate_pdf(
359
+ last_result["original"], last_result["annotated"],
360
+ last_result["seen"], last_result["alert"], last_result["explanation"]
 
 
 
361
  )
 
362
 
363
 
364
  css = """
 
373
  #danger-box textarea { font-family: 'Courier New', monospace !important; font-size: 0.9rem !important; font-weight: bold !important; background: #1a0a0a !important; color: #ff6b6b !important; border-radius: 10px !important; border: 1px solid #ff4444 !important; }
374
  #explain-box textarea { font-family: 'Inter', sans-serif !important; font-size: 0.88rem !important; line-height: 1.7 !important; background: #0a1a0f !important; color: #6ee7b7 !important; border-radius: 10px !important; border: 1px solid #34d399 !important; }
375
  .footer-note { text-align: center; font-size: 0.78rem; color: #888; margin-top: 16px; padding-bottom: 24px; }
376
+ #suggested-q button { background: rgba(32,58,67,0.08) !important; border: 1px solid #203a43 !important; color: #203a43 !important; border-radius: 999px !important; font-size: 0.82rem !important; padding: 6px 14px !important; margin: 2px !important; }
377
+ #suggested-q button:hover { background: #203a43 !important; color: white !important; }
378
  """
379
 
380
  header_html = """
 
382
  <h1>πŸ”¬ Surgical Tissue Segmentation</h1>
383
  <p>Upload a laparoscopic surgery frame β€” or click any example below. The AI identifies tissues, flags danger zones, and explains anatomy for surgical trainees.</p>
384
  <div class="badge-row">
385
+ <span class="badge">YOLOv8-seg</span>
386
  <span class="badge">Llama 3.1 8B</span>
387
  <span class="badge">Modal GPU</span>
388
  <span class="badge">CholecSeg8k Dataset</span>
 
401
 
402
  with gr.Blocks(title="SurgiSight β€” Surgical Tissue Segmentation") as demo:
403
  gr.HTML(header_html)
404
+
405
+ # ── Input / Output ──────────────────────────────────────────────────────
406
  with gr.Row():
407
  with gr.Column(scale=1):
408
  input_img = gr.Image(type="pil", label="Input β€” Laparoscopic Frame", height=350)
 
417
  placeholder="Safety alert will appear here after running segmentation...")
418
  output_text = gr.Textbox(label="Detected Tissues & Confidence", lines=6, elem_id="output-text",
419
  placeholder="Tissue detection results will appear here...")
420
+ explain_box = gr.Textbox(label="🧠 Initial Anatomy Explanation",
421
+ lines=4, elem_id="explain-box",
422
  placeholder="AI anatomy explanation will appear here after segmentation...")
423
 
424
+ # ── PDF button ──────────────────────────────────────────────────────────
425
  with gr.Row():
426
+ pdf_btn = gr.Button("πŸ“„ Download Full Report (PDF)", visible=False, size="lg")
427
  pdf_output = gr.File(label="Your Report", visible=False)
428
 
429
+ # ── Chat Section ────────────────────────────────────────────────────────
430
+ with gr.Column(visible=False) as chat_section:
431
+ gr.HTML("<hr style='margin:20px 0;border-color:#e0e0e0'><h3 style='margin-bottom:8px'>πŸ’¬ Ask the AI β€” Follow-up Questions</h3><p style='font-size:0.85rem;color:#666;margin-bottom:12px'>Ask anything about the detected tissues or general laparoscopic surgery</p>")
432
+
433
+ # Suggested question chips
434
+ with gr.Row(elem_id="suggested-q"):
435
+ sq1 = gr.Button("", visible=False, size="sm")
436
+ sq2 = gr.Button("", visible=False, size="sm")
437
+ sq3 = gr.Button("", visible=False, size="sm")
438
+
439
+ chatbot = gr.Chatbot(
440
+ label="SurgiSight Chat",
441
+ type="messages",
442
+ height=350,
443
+ show_label=False,
444
+ avatar_images=(None, "https://huggingface.co/front/assets/huggingface_logo-noborder.svg")
445
+ )
446
+
447
+ with gr.Row():
448
+ chat_input = gr.Textbox(
449
+ placeholder="Ask about detected tissues, surgical anatomy, or any surgery question...",
450
+ show_label=False, scale=5, container=False
451
+ )
452
+ send_btn = gr.Button("Send ➀", variant="primary", scale=1)
453
+
454
  gr.Examples(
455
  examples=[
456
  ["examples/frame_80_endo.png"],
 
462
  examples_per_page=3
463
  )
464
 
465
+ gr.HTML(footer_html)
466
+
467
+ # ── Event handlers ──────────────────────────────────────────────────────
468
  run_btn.click(
469
  fn=segment_image,
470
  inputs=[input_img, conf_slider],
471
+ outputs=[output_img, output_text, danger_box, explain_box,
472
+ pdf_btn, chat_section, sq1, sq2, sq3, chatbot],
473
  show_progress="full"
474
  )
475
 
476
+ pdf_btn.click(fn=export_pdf, inputs=[], outputs=[pdf_output]
 
 
 
477
  ).then(fn=lambda: gr.update(visible=True), outputs=[pdf_output])
478
 
479
+ # Send button / Enter key
480
+ send_btn.click(fn=chat_response, inputs=[chat_input, chatbot], outputs=[chatbot, chat_input])
481
+ chat_input.submit(fn=chat_response, inputs=[chat_input, chatbot], outputs=[chatbot, chat_input])
482
+
483
+ # Suggested question chips β†’ send directly into chat
484
+ sq1.click(fn=use_suggested, inputs=[sq1, chatbot], outputs=[chatbot, chat_input])
485
+ sq2.click(fn=use_suggested, inputs=[sq2, chatbot], outputs=[chatbot, chat_input])
486
+ sq3.click(fn=use_suggested, inputs=[sq3, chatbot], outputs=[chatbot, chat_input])
487
 
488
  if __name__ == "__main__":
489
+ demo.launch(css=css)