sugan04 commited on
Commit
5663dd7
·
verified ·
1 Parent(s): 037e21a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -113
app.py CHANGED
@@ -7,7 +7,7 @@ sys.modules["pyaudioop"] = audioop_mock
7
  import gradio as gr
8
  import modal
9
  from PIL import Image
10
- import io, datetime
11
  from huggingface_hub import InferenceClient
12
  from reportlab.lib.pagesizes import A4
13
  from reportlab.lib import colors
@@ -27,24 +27,23 @@ except ImportError:
27
  print("gTTS not available")
28
 
29
  CLASS_NAMES = [
30
- "Black Background", "Abdominal Wall", "Liver", "Gastrointestinal Tract",
31
- "Fat", "Grasper", "Connective Tissue", "Blood", "Cystic Duct",
32
- "L-hook Electrocautery", "Gallbladder", "Hepatic Vein", "Liver Ligament"
33
  ]
34
- DANGER_CLASSES = ["Hepatic Vein", "Cystic Duct", "Blood"]
35
 
36
  LANGUAGES = {
37
- "English": {"code": "en", "prompt": "Respond in English."},
38
- "French": {"code": "fr", "prompt": "Réponds en français."},
39
- "Spanish": {"code": "es", "prompt": "Responde en español."},
40
- "German": {"code": "de", "prompt": "Antworte auf Deutsch."},
41
- "Arabic": {"code": "ar", "prompt": "أجب باللغة العربية."},
42
- "Hindi": {"code": "hi", "prompt": "हिंदी में उत्तर दें।"},
43
  }
44
 
45
  last_result = {}
46
  chat_context = {}
47
- last_ai_reply = {"text": "", "lang": "en"}
48
 
49
  try:
50
  client = InferenceClient(model="meta-llama/Llama-3.1-8B-Instruct", token=os.environ.get("HF_TOKEN"))
@@ -61,16 +60,15 @@ def get_detector():
61
  def pil_to_bytes(img):
62
  buf = io.BytesIO(); img.save(buf, format="PNG"); return buf.getvalue()
63
 
64
- def text_to_speech(text, lang_code):
65
- if not TTS_AVAILABLE or not text: return None
66
  try:
67
  tts = gTTS(text=text, lang=lang_code, slow=False)
68
- path = "/tmp/surgisight_reply.mp3"
69
- tts.save(path)
70
- return path
71
  except Exception as e:
72
- print(f"TTS error: {e}")
73
- return None
74
 
75
  def generate_suggested_questions(tissue_list):
76
  questions = []
@@ -85,6 +83,88 @@ def generate_suggested_questions(tissue_list):
85
  questions.append("What are common complications in laparoscopic cholecystectomy?")
86
  return questions[:3]
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  def generate_pdf(original_image, annotated_image, seen, alert, explanation):
89
  pdf_path = "/tmp/surgisight_report.pdf"
90
  doc = SimpleDocTemplate(pdf_path, pagesize=A4,
@@ -161,12 +241,17 @@ def generate_pdf(original_image, annotated_image, seen, alert, explanation):
161
  return pdf_path
162
 
163
 
 
 
 
164
  def segment_image(input_image, conf_threshold=0.25):
165
- global last_result, chat_context, last_ai_reply
 
166
  if input_image is None:
167
  return (None, "Upload a frame to begin analysis.", "——", "——",
168
  gr.update(visible=False), gr.update(visible=False),
169
- gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), [])
 
170
  detector = get_detector()
171
  result = detector.run.remote(pil_to_bytes(input_image), conf_threshold)
172
  annotated_image = Image.open(io.BytesIO(result["annotated_bytes"]))
@@ -200,25 +285,26 @@ def segment_image(input_image, conf_threshold=0.25):
200
  chat_context = {"tissue_list": tissue_list, "alert": alert}
201
  last_result = {"original": input_image, "annotated": annotated_image,
202
  "seen": seen, "alert": alert, "explanation": explanation}
 
 
 
203
  suggested = generate_suggested_questions(tissue_list) if tissue_list else []
204
  q1 = suggested[0] if len(suggested) > 0 else ""
205
  q2 = suggested[1] if len(suggested) > 1 else ""
206
  q3 = suggested[2] if len(suggested) > 2 else ""
207
- intro = (f"Analysis complete. Detected: **{', '.join(tissue_list) if tissue_list else 'no structures'}**.\n\n"
208
- f"{explanation}\n\nAsk me anything about these structures or laparoscopic surgery.")
209
- last_ai_reply = {"text": intro, "lang": "en"}
210
- initial_chat = [{"role": "assistant", "content": intro}]
211
  return (annotated_image, summary, alert, explanation,
212
  gr.update(visible=True), gr.update(visible=True),
213
  gr.update(value=q1, visible=bool(q1)),
214
  gr.update(value=q2, visible=bool(q2)),
215
- gr.update(value=q3, visible=bool(q3)), initial_chat)
 
216
 
217
 
218
- def chat_response(message, history, language):
219
- global chat_context, last_ai_reply
220
  if not message.strip():
221
- return history, "", gr.update(visible=False), gr.update(visible=False)
 
222
  tissue_list = chat_context.get("tissue_list", [])
223
  alert = chat_context.get("alert", "")
224
  lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
@@ -227,31 +313,15 @@ def chat_response(message, history, language):
227
  + (f"Current frame detected: {', '.join(tissue_list)}. Safety status: {alert}. " if tissue_list else "")
228
  + "Answer concisely in 2-4 sentences. "
229
  + lang_cfg["prompt"])
230
- messages = [{"role": "system", "content": system_prompt}]
231
- for msg in history:
232
- messages.append({"role": msg["role"], "content": msg["content"]})
233
- messages.append({"role": "user", "content": message})
234
  try:
235
  response = client.chat_completion(messages, max_tokens=200, temperature=0.5)
236
  reply = response.choices[0].message.content.strip()
237
  except Exception as e:
238
  reply = f"Error: {str(e)}"
239
- new_history = history + [
240
- {"role": "user", "content": message},
241
- {"role": "assistant", "content": reply}
242
- ]
243
- last_ai_reply = {"text": reply, "lang": lang_cfg["code"]}
244
- audio_path = text_to_speech(reply, lang_cfg["code"])
245
- audio_update = gr.update(value=audio_path, visible=True) if audio_path else gr.update(visible=False)
246
- return new_history, "", audio_update, gr.update(visible=True)
247
-
248
-
249
- def replay_last(language):
250
- global last_ai_reply
251
- lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
252
- text = last_ai_reply.get("text", "")
253
- audio_path = text_to_speech(text, lang_cfg["code"])
254
- return gr.update(value=audio_path, visible=True) if audio_path else gr.update(visible=False)
255
 
256
 
257
  def export_pdf():
@@ -260,7 +330,6 @@ def export_pdf():
260
  last_result["seen"], last_result["alert"], last_result["explanation"])
261
 
262
 
263
- # CSS passed to launch() in Gradio 6
264
  css = """
265
  .gradio-container { max-width: 1120px !important; margin: 0 auto !important; }
266
  .generating, .progress-text, .progress-bar-wrap, .eta-bar, .eta-text { display: none !important; }
@@ -270,48 +339,28 @@ css = """
270
  flex-direction:column; align-items:center; justify-content:center;
271
  }
272
  #surgi-overlay.active { display:flex !important; }
273
- .or-spinner {
274
- width:48px; height:48px;
275
- border:4px solid rgba(255,255,255,0.2);
276
- border-top-color:#fff; border-radius:50%;
277
- animation:spin 0.8s linear infinite; margin-bottom:16px;
278
- }
279
  @keyframes spin { to { transform:rotate(360deg) } }
280
- .or-text { color:#fff; font-size:1rem; font-weight:600; text-align:center; }
281
- .or-sub { color:rgba(255,255,255,0.6); font-size:0.82rem; margin-top:6px; }
282
  """
283
 
284
- # JS runs once on page load — safe place for scripts in Gradio 6
285
  overlay_js = """
286
  () => {
287
- // Inject overlay HTML
288
  const div = document.createElement('div');
289
  div.id = 'surgi-overlay';
290
- div.innerHTML = `
291
- <div class="or-spinner"></div>
292
- <div class="or-text">Analysing Surgical Frame…</div>
293
- <div class="or-sub">YOLOv8 · Modal GPU · Llama 3.1</div>
294
- `;
295
  document.body.appendChild(div);
296
-
297
- // Attach click listener to run button
298
  function attachBtn() {
299
  const btn = document.querySelector('#run-btn button');
300
- if (btn) {
301
- btn.addEventListener('click', () => div.classList.add('active'));
302
- } else {
303
- setTimeout(attachBtn, 500);
304
- }
305
  }
306
  attachBtn();
307
-
308
- // Hide overlay when output image appears
309
  new MutationObserver(() => {
310
  const img = document.querySelector('#output-frame img');
311
  if (img && img.src && img.src.length > 80) div.classList.remove('active');
312
- }).observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
313
-
314
- // Safety timeout
315
  setTimeout(() => div.classList.remove('active'), 90000);
316
  }
317
  """
@@ -319,59 +368,50 @@ overlay_js = """
319
  with gr.Blocks(title="SurgiSight — Surgical AI", js=overlay_js) as demo:
320
 
321
  gr.Markdown("""
322
- # SurgiSight — Surgical Tissue Segmentation
323
- **AI-powered anatomy detection for laparoscopic training** · YOLOv26n-seg · Llama 3.1 8B · Modal GPU · CholecSeg8k (MICCAI 2020) · mAP50: 0.581
324
 
325
- > Research prototype — No real patient data.
326
  """)
327
 
328
  with gr.Row(equal_height=True):
329
  with gr.Column(scale=1):
330
  input_img = gr.Image(type="pil", label="Input Frame", height=340, elem_id="input-frame")
331
- conf_slider = gr.Slider(0.1, 0.9, value=0.25, step=0.05,
332
- label="Detection Confidence Threshold",
333
- info="Lower = more detections · Higher = high-confidence only")
334
- run_btn = gr.Button("▶ Run Analysis", variant="primary", size="lg", elem_id="run-btn")
335
  with gr.Column(scale=1):
336
- output_img = gr.Image(type="pil", label="Segmented Output", height=340, elem_id="output-frame")
337
 
338
  with gr.Row():
339
  danger_box = gr.Textbox(label="⚠ Safety Assessment", lines=2, placeholder="Awaiting analysis…")
340
- output_text = gr.Textbox(label="🔍 Detected Structures", lines=6, placeholder="Detection results will appear here…")
341
 
342
- explain_box = gr.Textbox(label="📖 Anatomy Brief", lines=4,
343
- placeholder="AI anatomy explanation will appear after analysis…")
344
 
345
  with gr.Row():
346
  pdf_btn = gr.Button("⬇ Export Full Report (PDF)", visible=False)
347
  pdf_output = gr.File(label="Download Report", visible=False)
348
 
 
349
  with gr.Column(visible=False) as chat_section:
350
  gr.Markdown("---\n### 💬 AI Anatomy Consult")
351
 
352
  with gr.Row():
353
  lang_select = gr.Dropdown(
354
  choices=list(LANGUAGES.keys()), value="English",
355
- label="🌍 Response Language", scale=1, min_width=180)
356
- replay_btn = gr.Button("🔊 Replay Last Reply", scale=0, min_width=180, variant="secondary")
357
 
358
  with gr.Row():
359
  sq1 = gr.Button("", visible=False, size="sm")
360
  sq2 = gr.Button("", visible=False, size="sm")
361
  sq3 = gr.Button("", visible=False, size="sm")
362
 
363
- chatbot = gr.Chatbot(height=340, show_label=False)
364
-
365
- # Audio player — autoplay, appears after each reply
366
- audio_out = gr.Audio(
367
- label="🔊 AI Voice Reply",
368
- visible=False,
369
- autoplay=True,
370
- format="mp3"
371
- )
372
 
373
  with gr.Row():
374
- chat_input = gr.Textbox(placeholder="Ask about anatomy, safety, or technique…",
 
375
  show_label=False, scale=5, container=False)
376
  send_btn = gr.Button("Send", variant="primary", scale=1)
377
 
@@ -382,26 +422,22 @@ with gr.Blocks(title="SurgiSight — Surgical AI", js=overlay_js) as demo:
382
 
383
  gr.Markdown("---\n*CholecSeg8k · MICCAI 2020 · No patient data · Built for Build Small Hackathon 2026*")
384
 
385
- # Wiring
386
  run_btn.click(fn=segment_image, inputs=[input_img, conf_slider],
387
  outputs=[output_img, output_text, danger_box, explain_box,
388
- pdf_btn, chat_section, sq1, sq2, sq3, chatbot])
389
 
390
  pdf_btn.click(fn=export_pdf, outputs=[pdf_output]).then(
391
  fn=lambda: gr.update(visible=True), outputs=[pdf_output])
392
 
393
- send_btn.click(fn=chat_response,
394
- inputs=[chat_input, chatbot, lang_select],
395
- outputs=[chatbot, chat_input, audio_out, replay_btn])
396
- chat_input.submit(fn=chat_response,
397
- inputs=[chat_input, chatbot, lang_select],
398
- outputs=[chatbot, chat_input, audio_out, replay_btn])
399
-
400
- replay_btn.click(fn=replay_last, inputs=[lang_select], outputs=[audio_out])
401
 
402
- sq1.click(fn=chat_response, inputs=[sq1, chatbot, lang_select], outputs=[chatbot, chat_input, audio_out, replay_btn])
403
- sq2.click(fn=chat_response, inputs=[sq2, chatbot, lang_select], outputs=[chatbot, chat_input, audio_out, replay_btn])
404
- sq3.click(fn=chat_response, inputs=[sq3, chatbot, lang_select], outputs=[chatbot, chat_input, audio_out, replay_btn])
405
 
406
  if __name__ == "__main__":
407
  demo.launch(css=css)
 
7
  import gradio as gr
8
  import modal
9
  from PIL import Image
10
+ import io, datetime, base64
11
  from huggingface_hub import InferenceClient
12
  from reportlab.lib.pagesizes import A4
13
  from reportlab.lib import colors
 
27
  print("gTTS not available")
28
 
29
  CLASS_NAMES = [
30
+ "Black Background","Abdominal Wall","Liver","Gastrointestinal Tract",
31
+ "Fat","Grasper","Connective Tissue","Blood","Cystic Duct",
32
+ "L-hook Electrocautery","Gallbladder","Hepatic Vein","Liver Ligament"
33
  ]
34
+ DANGER_CLASSES = ["Hepatic Vein","Cystic Duct","Blood"]
35
 
36
  LANGUAGES = {
37
+ "English": {"code":"en","prompt":"Respond in English."},
38
+ "French": {"code":"fr","prompt":"Réponds en français."},
39
+ "Spanish": {"code":"es","prompt":"Responde en español."},
40
+ "German": {"code":"de","prompt":"Antworte auf Deutsch."},
41
+ "Arabic": {"code":"ar","prompt":"أجب باللغة العربية."},
42
+ "Hindi": {"code":"hi","prompt":"हिंदी में उत्तर दें।"},
43
  }
44
 
45
  last_result = {}
46
  chat_context = {}
 
47
 
48
  try:
49
  client = InferenceClient(model="meta-llama/Llama-3.1-8B-Instruct", token=os.environ.get("HF_TOKEN"))
 
60
  def pil_to_bytes(img):
61
  buf = io.BytesIO(); img.save(buf, format="PNG"); return buf.getvalue()
62
 
63
+ def tts_to_b64(text, lang_code):
64
+ if not TTS_AVAILABLE or not text: return ""
65
  try:
66
  tts = gTTS(text=text, lang=lang_code, slow=False)
67
+ buf = io.BytesIO()
68
+ tts.write_to_fp(buf)
69
+ return base64.b64encode(buf.getvalue()).decode()
70
  except Exception as e:
71
+ print(f"TTS error: {e}"); return ""
 
72
 
73
  def generate_suggested_questions(tissue_list):
74
  questions = []
 
83
  questions.append("What are common complications in laparoscopic cholecystectomy?")
84
  return questions[:3]
85
 
86
+ def render_chat_html(messages, lang_code):
87
+ """Render chat messages as clean Perplexity-style HTML with inline TTS."""
88
+ if not messages:
89
+ return '<p style="color:#888;text-align:center;padding:32px 0;">Run analysis first, then ask questions.</p>'
90
+
91
+ items = []
92
+ for i, msg in enumerate(messages):
93
+ role = msg["role"]
94
+ text = msg["content"]
95
+ # Convert **bold** markdown
96
+ import re
97
+ text_html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
98
+ text_html = text_html.replace("\n\n", "</p><p>").replace("\n", "<br>")
99
+ text_html = f"<p>{text_html}</p>"
100
+
101
+ if role == "user":
102
+ items.append(f"""
103
+ <div style="display:flex;justify-content:flex-end;margin:12px 0 4px;">
104
+ <div style="background:#2563eb;color:#fff;border-radius:20px 20px 4px 20px;
105
+ padding:10px 16px;max-width:72%;font-size:0.9rem;line-height:1.6;">
106
+ {text_html}
107
+ </div>
108
+ </div>""")
109
+ else:
110
+ # AI message — generate TTS inline as base64 data URI
111
+ audio_b64 = tts_to_b64(text, lang_code)
112
+ audio_tag = ""
113
+ speaker_btn = ""
114
+ if audio_b64:
115
+ audio_id = f"audio-{i}"
116
+ audio_tag = f'<audio id="{audio_id}" src="data:audio/mp3;base64,{audio_b64}" preload="auto"></audio>'
117
+ speaker_btn = f"""
118
+ <button
119
+ onclick="(function(){{
120
+ var a=document.getElementById('{audio_id}');
121
+ var b=this;
122
+ if(!a.paused){{a.pause();a.currentTime=0;b.textContent='🔊';return;}}
123
+ document.querySelectorAll('audio').forEach(function(x){{x.pause();x.currentTime=0;}});
124
+ document.querySelectorAll('.spk-btn').forEach(function(x){{x.textContent='🔊';}});
125
+ a.play();b.textContent='⏸';
126
+ a.onended=function(){{b.textContent='🔊';}};
127
+ }}).call(this)"
128
+ class="spk-btn"
129
+ title="Listen"
130
+ style="background:none;border:none;cursor:pointer;font-size:1rem;
131
+ padding:2px 6px;border-radius:6px;color:#6b7280;
132
+ transition:color 0.15s;flex-shrink:0;margin-top:2px;"
133
+ onmouseover="this.style.color='#2563eb'"
134
+ onmouseout="this.style.color='#6b7280'">🔊</button>"""
135
+
136
+ items.append(f"""
137
+ <div style="display:flex;justify-content:flex-start;margin:12px 0 4px;">
138
+ <div style="max-width:82%;">
139
+ <div style="font-size:0.75rem;color:#6b7280;margin-bottom:4px;font-weight:500;">
140
+ SurgiSight AI
141
+ </div>
142
+ <div style="display:flex;align-items:flex-start;gap:8px;">
143
+ <div style="font-size:0.9rem;line-height:1.7;color:var(--body-text-color,#e0e0e0);">
144
+ {text_html}
145
+ </div>
146
+ {speaker_btn}
147
+ </div>
148
+ {audio_tag}
149
+ </div>
150
+ </div>""")
151
+
152
+ html = f"""
153
+ <div id="chat-scroll"
154
+ style="height:420px;overflow-y:auto;padding:16px 8px;">
155
+ {''.join(items)}
156
+ <div id="chat-end"></div>
157
+ </div>
158
+ <script>
159
+ (function(){{
160
+ var el = document.getElementById('chat-end');
161
+ if(el) el.scrollIntoView({{behavior:'smooth'}});
162
+ }})();
163
+ </script>
164
+ """
165
+ return html
166
+
167
+
168
  def generate_pdf(original_image, annotated_image, seen, alert, explanation):
169
  pdf_path = "/tmp/surgisight_report.pdf"
170
  doc = SimpleDocTemplate(pdf_path, pagesize=A4,
 
241
  return pdf_path
242
 
243
 
244
+ # In-memory chat history (list of {"role":..,"content":..})
245
+ _chat_history = []
246
+
247
  def segment_image(input_image, conf_threshold=0.25):
248
+ global last_result, chat_context, _chat_history
249
+ _chat_history = []
250
  if input_image is None:
251
  return (None, "Upload a frame to begin analysis.", "——", "——",
252
  gr.update(visible=False), gr.update(visible=False),
253
+ gr.update(visible=False), gr.update(visible=False), gr.update(visible=False),
254
+ render_chat_html([], "en"))
255
  detector = get_detector()
256
  result = detector.run.remote(pil_to_bytes(input_image), conf_threshold)
257
  annotated_image = Image.open(io.BytesIO(result["annotated_bytes"]))
 
285
  chat_context = {"tissue_list": tissue_list, "alert": alert}
286
  last_result = {"original": input_image, "annotated": annotated_image,
287
  "seen": seen, "alert": alert, "explanation": explanation}
288
+ intro = (f"Analysis complete. Detected: **{', '.join(tissue_list) if tissue_list else 'no structures'}**.\n\n"
289
+ f"{explanation}\n\nAsk me anything about these structures or laparoscopic surgery.")
290
+ _chat_history = [{"role": "assistant", "content": intro}]
291
  suggested = generate_suggested_questions(tissue_list) if tissue_list else []
292
  q1 = suggested[0] if len(suggested) > 0 else ""
293
  q2 = suggested[1] if len(suggested) > 1 else ""
294
  q3 = suggested[2] if len(suggested) > 2 else ""
 
 
 
 
295
  return (annotated_image, summary, alert, explanation,
296
  gr.update(visible=True), gr.update(visible=True),
297
  gr.update(value=q1, visible=bool(q1)),
298
  gr.update(value=q2, visible=bool(q2)),
299
+ gr.update(value=q3, visible=bool(q3)),
300
+ render_chat_html(_chat_history, "en"))
301
 
302
 
303
+ def send_message(message, language):
304
+ global chat_context, _chat_history
305
  if not message.strip():
306
+ lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
307
+ return render_chat_html(_chat_history, lang_cfg["code"]), ""
308
  tissue_list = chat_context.get("tissue_list", [])
309
  alert = chat_context.get("alert", "")
310
  lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
 
313
  + (f"Current frame detected: {', '.join(tissue_list)}. Safety status: {alert}. " if tissue_list else "")
314
  + "Answer concisely in 2-4 sentences. "
315
  + lang_cfg["prompt"])
316
+ messages = [{"role": "system", "content": system_prompt}] + _chat_history + [{"role": "user", "content": message}]
 
 
 
317
  try:
318
  response = client.chat_completion(messages, max_tokens=200, temperature=0.5)
319
  reply = response.choices[0].message.content.strip()
320
  except Exception as e:
321
  reply = f"Error: {str(e)}"
322
+ _chat_history.append({"role": "user", "content": message})
323
+ _chat_history.append({"role": "assistant", "content": reply})
324
+ return render_chat_html(_chat_history, lang_cfg["code"]), ""
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
 
327
  def export_pdf():
 
330
  last_result["seen"], last_result["alert"], last_result["explanation"])
331
 
332
 
 
333
  css = """
334
  .gradio-container { max-width: 1120px !important; margin: 0 auto !important; }
335
  .generating, .progress-text, .progress-bar-wrap, .eta-bar, .eta-text { display: none !important; }
 
339
  flex-direction:column; align-items:center; justify-content:center;
340
  }
341
  #surgi-overlay.active { display:flex !important; }
342
+ .or-spinner { width:48px;height:48px;border:4px solid rgba(255,255,255,0.2);border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:16px; }
 
 
 
 
 
343
  @keyframes spin { to { transform:rotate(360deg) } }
344
+ .or-text { color:#fff;font-size:1rem;font-weight:600;text-align:center; }
345
+ .or-sub { color:rgba(255,255,255,0.6);font-size:0.82rem;margin-top:6px; }
346
  """
347
 
 
348
  overlay_js = """
349
  () => {
 
350
  const div = document.createElement('div');
351
  div.id = 'surgi-overlay';
352
+ div.innerHTML = '<div class="or-spinner"></div><div class="or-text">Analysing Surgical Frame…</div><div class="or-sub">YOLOv8 · Modal GPU · Llama 3.1</div>';
 
 
 
 
353
  document.body.appendChild(div);
 
 
354
  function attachBtn() {
355
  const btn = document.querySelector('#run-btn button');
356
+ if (btn) { btn.addEventListener('click', () => div.classList.add('active')); }
357
+ else { setTimeout(attachBtn, 500); }
 
 
 
358
  }
359
  attachBtn();
 
 
360
  new MutationObserver(() => {
361
  const img = document.querySelector('#output-frame img');
362
  if (img && img.src && img.src.length > 80) div.classList.remove('active');
363
+ }).observe(document.body, {childList:true,subtree:true,attributes:true,attributeFilter:['src']});
 
 
364
  setTimeout(() => div.classList.remove('active'), 90000);
365
  }
366
  """
 
368
  with gr.Blocks(title="SurgiSight — Surgical AI", js=overlay_js) as demo:
369
 
370
  gr.Markdown("""
371
+ # 🔬 SurgiSight — Surgical Tissue Segmentation
372
+ **AI-powered anatomy detection for laparoscopic training** · YOLOv8n-seg · Llama 3.1 8B · Modal GPU · CholecSeg8k · mAP50: 0.581
373
 
374
+ > ⚠️ Research prototype — not a medical device. No real patient data.
375
  """)
376
 
377
  with gr.Row(equal_height=True):
378
  with gr.Column(scale=1):
379
  input_img = gr.Image(type="pil", label="Input Frame", height=340, elem_id="input-frame")
380
+ conf_slider = gr.Slider(0.1, 0.9, value=0.25, step=0.05, label="Confidence Threshold")
381
+ run_btn = gr.Button("▶ Run Analysis", variant="primary", size="lg", elem_id="run-btn")
 
 
382
  with gr.Column(scale=1):
383
+ output_img = gr.Image(type="pil", label="Segmented Output", height=340, elem_id="output-frame")
384
 
385
  with gr.Row():
386
  danger_box = gr.Textbox(label="⚠ Safety Assessment", lines=2, placeholder="Awaiting analysis…")
387
+ output_text = gr.Textbox(label="🔍 Detected Structures", lines=6, placeholder="Results will appear here…")
388
 
389
+ explain_box = gr.Textbox(label="📖 Anatomy Brief", lines=4, placeholder="AI explanation will appear after analysis…")
 
390
 
391
  with gr.Row():
392
  pdf_btn = gr.Button("⬇ Export Full Report (PDF)", visible=False)
393
  pdf_output = gr.File(label="Download Report", visible=False)
394
 
395
+ # ── Chat ──────────────────────────────────────────────────────────────────
396
  with gr.Column(visible=False) as chat_section:
397
  gr.Markdown("---\n### 💬 AI Anatomy Consult")
398
 
399
  with gr.Row():
400
  lang_select = gr.Dropdown(
401
  choices=list(LANGUAGES.keys()), value="English",
402
+ label="🌍 Response Language", scale=0, min_width=200)
 
403
 
404
  with gr.Row():
405
  sq1 = gr.Button("", visible=False, size="sm")
406
  sq2 = gr.Button("", visible=False, size="sm")
407
  sq3 = gr.Button("", visible=False, size="sm")
408
 
409
+ # Clean HTML chat display — no Gradio chatbot widget
410
+ chat_display = gr.HTML(value="", label="")
 
 
 
 
 
 
 
411
 
412
  with gr.Row():
413
+ chat_input = gr.Textbox(
414
+ placeholder="Ask about anatomy, safety, or technique…",
415
  show_label=False, scale=5, container=False)
416
  send_btn = gr.Button("Send", variant="primary", scale=1)
417
 
 
422
 
423
  gr.Markdown("---\n*CholecSeg8k · MICCAI 2020 · No patient data · Built for Build Small Hackathon 2026*")
424
 
425
+ # ── Wiring ────────────────────────────────────────────────────────────────
426
  run_btn.click(fn=segment_image, inputs=[input_img, conf_slider],
427
  outputs=[output_img, output_text, danger_box, explain_box,
428
+ pdf_btn, chat_section, sq1, sq2, sq3, chat_display])
429
 
430
  pdf_btn.click(fn=export_pdf, outputs=[pdf_output]).then(
431
  fn=lambda: gr.update(visible=True), outputs=[pdf_output])
432
 
433
+ send_btn.click(fn=send_message, inputs=[chat_input, lang_select],
434
+ outputs=[chat_display, chat_input])
435
+ chat_input.submit(fn=send_message, inputs=[chat_input, lang_select],
436
+ outputs=[chat_display, chat_input])
 
 
 
 
437
 
438
+ sq1.click(fn=send_message, inputs=[sq1, lang_select], outputs=[chat_display, chat_input])
439
+ sq2.click(fn=send_message, inputs=[sq2, lang_select], outputs=[chat_display, chat_input])
440
+ sq3.click(fn=send_message, inputs=[sq3, lang_select], outputs=[chat_display, chat_input])
441
 
442
  if __name__ == "__main__":
443
  demo.launch(css=css)