sugan04's picture
Update app.py
7c68a59 verified
Raw
History Blame Contribute Delete
49 kB
import sys, types, os
audioop_mock = types.ModuleType("audioop")
sys.modules["audioop"] = audioop_mock
sys.modules["pyaudioop"] = audioop_mock
import gradio as gr
import modal
from PIL import Image
import io, datetime, base64, re
from huggingface_hub import InferenceClient
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
try:
from gtts import gTTS
TTS_AVAILABLE = True
except ImportError:
TTS_AVAILABLE = False
CLASS_NAMES = [
"Black Background","Abdominal Wall","Liver","Gastrointestinal Tract",
"Fat","Grasper","Connective Tissue","Blood","Cystic Duct",
"L-hook Electrocautery","Gallbladder","Hepatic Vein","Liver Ligament"
]
DANGER_CLASSES = ["Hepatic Vein","Cystic Duct","Blood"]
LANGUAGES = {
"English": {"code":"en","prompt":"Respond in English."},
"French": {"code":"fr","prompt":"RΓ©ponds en franΓ§ais."},
}
last_result = {}
chat_context = {}
_chat_history = []
try:
client = InferenceClient(model="meta-llama/Llama-3.1-8B-Instruct", token=os.environ.get("HF_TOKEN"))
except Exception as e:
client = None
print(f"InferenceClient failed: {e}")
def get_detector():
SurgiSightDetector = modal.Cls.from_name("surgisight", "SurgiSightDetector")
return SurgiSightDetector()
def pil_to_bytes(img):
buf = io.BytesIO(); img.save(buf, format="PNG"); return buf.getvalue()
def tts_to_b64(text, lang_code):
if not TTS_AVAILABLE or not text: return ""
try:
tts = gTTS(text=text[:500], lang=lang_code, slow=False)
buf = io.BytesIO()
tts.write_to_fp(buf)
data = buf.getvalue()
if len(data) < 100: return ""
return base64.b64encode(data).decode()
except Exception as e:
print(f"TTS error: {e}")
return ""
def translate_to(text, lang_cfg):
if lang_cfg["code"] == "en" or not client:
return text
try:
lang_name = [k for k, v in LANGUAGES.items() if v["code"] == lang_cfg["code"]][0]
resp = client.chat_completion(
[{"role": "user", "content": f"Translate to {lang_name}. Output ONLY the translation.\n\n{text}"}],
max_tokens=300, temperature=0.1
)
return resp.choices[0].message.content.strip()
except:
return text
def generate_suggested_questions(tissue_list):
questions = []
for t in tissue_list:
if t == "Hepatic Vein":
questions.append("Why is the hepatic vein dangerous to nick?")
elif t == "Cystic Duct":
questions.append("How do I safely identify the cystic duct?")
elif t == "Blood":
questions.append("What are steps to control unexpected bleeding?")
elif t == "Gallbladder":
questions.append("What is the critical view of safety?")
elif t == "L-hook Electrocautery":
questions.append("What are risks of electrocautery near bile duct?")
elif t == "Liver":
questions.append("How does liver retraction affect visibility?")
if len(questions) >= 2:
break
questions.append("What are common complications in laparoscopic cholecystectomy?")
return questions[:3]
def render_chat_html(history, lang_code):
if not history:
return """
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
height:300px;gap:12px;">
<div style="font-size:2rem;">πŸ”¬</div>
<div style="color:#475569;font-size:0.85rem;text-align:center;max-width:260px;line-height:1.6;">
Run analysis on a surgical frame, then ask anything about the anatomy.
</div>
</div>"""
items = []
for i, msg in enumerate(history):
role = msg["role"]
text = msg["display"]
text_html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text_html = text_html.replace("\n\n", "</p><p style='margin:6px 0;'>").replace("\n", "<br>")
text_html = f"<p style='margin:0;'>{text_html}</p>"
if role == "user":
items.append(f"""
<div style="display:flex;justify-content:flex-end;margin:10px 0;">
<div style="background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;
border-radius:18px 18px 4px 18px;padding:10px 15px;max-width:75%;
font-size:0.875rem;line-height:1.6;box-shadow:0 2px 8px rgba(37,99,235,0.3);">
{text_html}
</div>
</div>""")
else:
audio_b64 = tts_to_b64(text, lang_code)
audio_html = spk_btn = ""
if audio_b64:
aid = f"aud{i}"
audio_html = f'<audio id="{aid}" src="data:audio/mp3;base64,{audio_b64}" preload="auto"></audio>'
spk_btn = (
f'<button onclick="(function(b){{var a=document.getElementById(\'{aid}\');'
f'if(!a.paused){{a.pause();a.currentTime=0;b.textContent=\'πŸ”Š\';return;}}'
f'document.querySelectorAll(\'audio\').forEach(function(x){{x.pause();x.currentTime=0;}});'
f'document.querySelectorAll(\'.spkb\').forEach(function(x){{x.textContent=\'πŸ”Š\';}});'
f'a.play();b.textContent=\'⏸\';a.onended=function(){{b.textContent=\'πŸ”Š\';}};'
f'}})(this)" class="spkb" title="Listen" '
f'style="background:rgba(99,102,241,0.1);border:none;cursor:pointer;font-size:0.8rem;'
f'padding:4px 8px;border-radius:20px;color:#6366f1;flex-shrink:0;margin-top:2px;'
f'transition:all 0.15s;font-weight:500;" '
f'onmouseover="this.style.background=\'rgba(99,102,241,0.2)\'" '
f'onmouseout="this.style.background=\'rgba(99,102,241,0.1)\'">πŸ”Š</button>'
)
items.append(f"""
<div style="display:flex;justify-content:flex-start;margin:10px 0;">
<div style="max-width:85%;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<div style="width:20px;height:20px;border-radius:50%;
background:linear-gradient(135deg,#6366f1,#8b5cf6);
display:flex;align-items:center;justify-content:center;
font-size:0.6rem;color:white;font-weight:700;flex-shrink:0;">S</div>
<span style="font-size:0.7rem;color:#475569;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;">SurgiSight AI</span>
</div>
<div style="display:flex;align-items:flex-start;gap:8px;">
<div style="font-size:0.875rem;line-height:1.7;color:#cbd5e1;flex:1;">
{text_html}
</div>
{spk_btn}
</div>
{audio_html}
</div>
</div>""")
scroll_js = "<script>setTimeout(function(){var e=document.getElementById('ce');if(e)e.scrollIntoView({behavior:'smooth'});},80);</script>"
return f"""
<div style="height:380px;overflow-y:auto;padding:8px 4px;">
{''.join(items)}
<div id="ce"></div>
</div>{scroll_js}"""
def retranslate_history(language):
global _chat_history
if not _chat_history:
return render_chat_html([], LANGUAGES.get(language, LANGUAGES["English"])["code"])
lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
for msg in _chat_history:
if msg["role"] == "assistant":
msg["display"] = msg["en"] if lang_cfg["code"] == "en" else translate_to(msg["en"], lang_cfg)
return render_chat_html(_chat_history, lang_cfg["code"])
def generate_pdf(original_image, annotated_image, seen, alert, explanation):
pdf_path = "/tmp/surgisight_report.pdf"
doc = SimpleDocTemplate(pdf_path, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
styles = getSampleStyleSheet()
ts = ParagraphStyle('T2', parent=styles['Title'], fontSize=22, textColor=colors.HexColor('#1a3a5c'), spaceAfter=4, fontName='Helvetica-Bold', alignment=TA_CENTER)
ss = ParagraphStyle('S2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#888'), spaceAfter=2, alignment=TA_CENTER)
ses = ParagraphStyle('Se2', parent=styles['Normal'], fontSize=13, textColor=colors.HexColor('#1a3a5c'), spaceAfter=6, spaceBefore=12, fontName='Helvetica-Bold')
bs = ParagraphStyle('B2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#1a1a1a'), spaceAfter=4, leading=16)
ds = ParagraphStyle('D2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#cc0000'), spaceAfter=4, leading=16, fontName='Helvetica-Bold', backColor=colors.HexColor('#fff0f0'), borderPadding=(6, 8, 6, 8))
sas = ParagraphStyle('Sa2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#1a7a40'), spaceAfter=4, leading=16, fontName='Helvetica-Bold', backColor=colors.HexColor('#f0fff4'), borderPadding=(6, 8, 6, 8))
cs = ParagraphStyle('C2', parent=styles['Normal'], fontSize=8, textColor=colors.HexColor('#888'), alignment=TA_CENTER, spaceAfter=4)
fs = ParagraphStyle('F2', parent=styles['Normal'], fontSize=8, textColor=colors.HexColor('#aaa'), alignment=TA_CENTER)
story = []
tstamp = datetime.datetime.now().strftime("%B %d, %Y at %H:%M")
story += [Spacer(1, .3*cm), Paragraph("SurgiSight", ts), Paragraph("Surgical Anatomy Analysis Report", ss), Paragraph(f"Generated on {tstamp}", ss), Spacer(1, .2*cm), HRFlowable(width="100%", thickness=2, color=colors.HexColor('#1a3a5c')), Spacer(1, .4*cm), Paragraph("Segmentation Output", ses)]
iw, ih = 8.5*cm, 6.5*cm
ob = io.BytesIO(); original_image.save(ob, format="PNG"); ob.seek(0)
ab = io.BytesIO(); annotated_image.save(ab, format="PNG"); ab.seek(0)
it = Table([[RLImage(ob, width=iw, height=ih), RLImage(ab, width=iw, height=ih)], [Paragraph("Original Frame", cs), Paragraph("AI Segmented Output", cs)]], colWidths=[iw + .5*cm, iw + .5*cm])
it.setStyle(TableStyle([('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f5f5f5')), ('BOX', (0, 0), (0, 0), .5, colors.HexColor('#ddd')), ('BOX', (1, 0), (1, 0), .5, colors.HexColor('#ddd'))]))
story += [it, Spacer(1, .5*cm), Paragraph("Safety Assessment", ses)]
if any(d in alert for d in DANGER_CLASSES):
story.append(Paragraph(f"WARNING: {alert}", ds))
else:
story.append(Paragraph(f"SAFE: {alert}", sas))
story += [Spacer(1, .4*cm), Paragraph("Detected Tissues & Instruments", ses)]
td = [["Structure", "Confidence", "Risk Level"]]; rd = []
for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
if name == "Black Background":
continue
bar = "\u2588" * int(conf * 10) + "\u2591" * (10 - int(conf * 10))
td.append([name, f"{conf:.1%} {bar}", "DANGER" if name in DANGER_CLASSES else "Safe"])
rd.append(name in DANGER_CLASSES)
dt = Table(td, colWidths=[6*cm, 7*cm, 3.5*cm])
dts = [('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1a3a5c')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('FONTSIZE', (0, 1), (-1, -1), 9), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.HexColor('#f9f9f9'), colors.white]), ('GRID', (0, 0), (-1, -1), .3, colors.HexColor('#ddd')), ('TOPPADDING', (0, 0), (-1, -1), 6), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ('LEFTPADDING', (0, 0), (-1, -1), 8)]
for i, isd in enumerate(rd):
r = i + 1; c = colors.HexColor('#cc0000') if isd else colors.HexColor('#1a7a40')
dts.append(('TEXTCOLOR', (2, r), (2, r), c))
if isd:
dts.append(('FONTNAME', (2, r), (2, r), 'Helvetica-Bold'))
dt.setStyle(TableStyle(dts))
story += [dt, Spacer(1, .5*cm), HRFlowable(width="100%", thickness=.5, color=colors.HexColor('#ccc')), Spacer(1, .3*cm), Paragraph("Anatomy Teaching Note", ses), Paragraph(explanation, bs), Spacer(1, .5*cm), HRFlowable(width="100%", thickness=.5, color=colors.HexColor('#ccc')), Spacer(1, .3*cm)]
md = [["Detection Model", "YOLOv8-seg fine-tuned on CholecSeg8k (MICCAI 2020)"], ["LLM", "Meta Llama 3.1 8B Instruct"], ["Inference", "Modal GPU (T4)"], ["Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"], ["mAP50", "0.581"]]
mt = Table(md, colWidths=[4.5*cm, 12*cm])
mt.setStyle(TableStyle([('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 8), ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#1a3a5c')), ('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#555')), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('TOPPADDING', (0, 0), (-1, -1), 3), ('BOTTOMPADDING', (0, 0), (-1, -1), 3), ('ROWBACKGROUNDS', (0, 0), (-1, -1), [colors.HexColor('#f5f5f5'), colors.white])]))
story += [mt, Spacer(1, .4*cm), HRFlowable(width="100%", thickness=1, color=colors.HexColor('#1a3a5c')), Spacer(1, .2*cm), Paragraph("DISCLAIMER: Research prototype only. Not a medical device. No real patient data.", fs), Paragraph("Built for Build Small Hackathon 2026", fs)]
doc.build(story)
return pdf_path
def generate_word(original_image, annotated_image, seen, alert, explanation):
docx_path = "/tmp/surgisight_report.docx"
doc = Document()
for section in doc.sections:
section.top_margin = Inches(1); section.bottom_margin = Inches(1)
section.left_margin = Inches(1.1); section.right_margin = Inches(1.1)
t = doc.add_heading("SurgiSight", 0); t.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in t.runs:
run.font.color.rgb = RGBColor(0x1a, 0x3a, 0x5c)
sub = doc.add_paragraph("Surgical Anatomy Analysis Report")
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
sub.runs[0].font.size = Pt(11); sub.runs[0].font.color.rgb = RGBColor(0x88, 0x88, 0x88)
tsp = doc.add_paragraph(datetime.datetime.now().strftime("Generated on %B %d, %Y at %H:%M"))
tsp.alignment = WD_ALIGN_PARAGRAPH.CENTER; tsp.runs[0].font.size = Pt(9); tsp.runs[0].font.color.rgb = RGBColor(0x88, 0x88, 0x88)
doc.add_paragraph(); doc.add_heading("Segmentation Output", 2)
img_tbl = doc.add_table(rows=2, cols=2); img_tbl.style = "Table Grid"
for ci, (pil_img, cap) in enumerate([(original_image, "Original Frame"), (annotated_image, "AI Segmented Output")]):
buf = io.BytesIO(); pil_img.save(buf, format="PNG"); buf.seek(0)
cell = img_tbl.cell(0, ci); cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
cell.paragraphs[0].add_run().add_picture(buf, width=Inches(2.9))
cc = img_tbl.cell(1, ci); cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
cr = cc.paragraphs[0].add_run(cap); cr.font.size = Pt(9); cr.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
doc.add_paragraph(); doc.add_heading("Safety Assessment", 2)
is_danger = any(d in alert for d in DANGER_CLASSES)
p = doc.add_paragraph(); run = p.add_run(("⚠ WARNING: " if is_danger else "βœ“ SAFE: ") + alert)
run.bold = True; run.font.size = Pt(11)
run.font.color.rgb = RGBColor(0xcc, 0, 0) if is_danger else RGBColor(0x1a, 0x7a, 0x40)
doc.add_paragraph(); doc.add_heading("Detected Tissues & Instruments", 2)
rows = [(n, c) for n, c in sorted(seen.items(), key=lambda x: -x[1]) if n != "Black Background"]
if rows:
tbl = doc.add_table(rows=1 + len(rows), cols=3); tbl.style = "Table Grid"
hdr = tbl.rows[0].cells
for i, h in enumerate(["Structure", "Confidence", "Risk Level"]):
hdr[i].text = h; hdr[i].paragraphs[0].runs[0].bold = True
hdr[i].paragraphs[0].runs[0].font.size = Pt(10)
hdr[i].paragraphs[0].runs[0].font.color.rgb = RGBColor(0xff, 0xff, 0xff)
tc = hdr[i]._tc; tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd'); shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto'); shd.set(qn('w:fill'), '1a3a5c')
tcPr.append(shd)
for ri, (name, conf) in enumerate(rows):
row = tbl.rows[ri + 1].cells; row[0].text = name; row[1].text = f"{conf:.1%}"
is_d = name in DANGER_CLASSES
rr = row[2].paragraphs[0].add_run("DANGER" if is_d else "Safe")
rr.bold = is_d; rr.font.size = Pt(9)
rr.font.color.rgb = RGBColor(0xcc, 0, 0) if is_d else RGBColor(0x1a, 0x7a, 0x40)
for c in [row[0], row[1]]:
if c.paragraphs[0].runs:
c.paragraphs[0].runs[0].font.size = Pt(9)
doc.add_paragraph(); doc.add_heading("Anatomy Teaching Note", 2)
doc.add_paragraph(explanation); doc.add_paragraph(); doc.add_heading("Model Information", 2)
for label, value in [("Detection Model", "YOLOv8-seg fine-tuned on CholecSeg8k (MICCAI 2020)"), ("LLM", "Meta Llama 3.1 8B Instruct"), ("Inference", "Modal GPU (T4)"), ("Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"), ("mAP50", "0.581")]:
p = doc.add_paragraph(); rl = p.add_run(f"{label}: "); rl.bold = True; rl.font.size = Pt(9); rl.font.color.rgb = RGBColor(0x1a, 0x3a, 0x5c)
rv = p.add_run(value); rv.font.size = Pt(9)
doc.add_paragraph()
disc = doc.add_paragraph("DISCLAIMER: Research prototype only. Not a medical device. No real patient data. Built for Build Small Hackathon 2026.")
disc.runs[0].font.size = Pt(8); disc.runs[0].font.color.rgb = RGBColor(0xaa, 0xaa, 0xaa)
doc.save(docx_path)
return docx_path
def build_results_html(seen, alert, explanation, danger_detected):
if seen is None:
return """<div style="display:flex;align-items:center;justify-content:center;height:300px;
color:#475569;font-size:0.85rem;gap:8px;">
<span>πŸ”¬</span>&nbsp;Run analysis to see results</div>"""
is_danger = bool(danger_detected)
alert_color = "#ef4444" if is_danger else "#22c55e"
alert_bg = "rgba(239,68,68,0.08)" if is_danger else "rgba(34,197,94,0.08)"
alert_border = "#fca5a5" if is_danger else "#86efac"
alert_icon = "⚠" if is_danger else "βœ“"
tissue_rows = ""
for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
if name == "Black Background":
continue
is_d = name in DANGER_CLASSES
pct = int(conf * 100)
bar_color = "#ef4444" if is_d else "#6366f1"
badge = (f'<span style="font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:20px;'
f'background:{"rgba(239,68,68,0.12)" if is_d else "rgba(34,197,94,0.1)"};'
f'color:{"#ef4444" if is_d else "#22c55e"};">{"DANGER" if is_d else "SAFE"}</span>')
tissue_rows += (
f'<div style="display:flex;align-items:center;gap:10px;padding:8px 0;'
f'border-bottom:1px solid rgba(255,255,255,0.05);">'
f'<div style="flex:1;font-size:0.82rem;font-weight:500;">{name}</div>'
f'<div style="width:80px;background:rgba(255,255,255,0.06);border-radius:20px;height:5px;overflow:hidden;">'
f'<div style="width:{pct}%;height:100%;background:{bar_color};border-radius:20px;"></div></div>'
f'<div style="font-size:0.75rem;color:#94a3b8;width:34px;text-align:right;">{pct}%</div>'
f'{badge}</div>')
return (
f'<div style="display:flex;flex-direction:column;gap:12px;">'
f'<div style="padding:12px 16px;border-radius:10px;border:1px solid {alert_border};'
f'background:{alert_bg};display:flex;align-items:center;gap:10px;">'
f'<span style="font-size:1.1rem;">{alert_icon}</span>'
f'<span style="font-size:0.85rem;font-weight:600;color:{alert_color};">{alert}</span></div>'
f'<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);'
f'border-radius:10px;padding:14px 16px;">'
f'<div style="font-size:0.7rem;font-weight:700;color:#475569;letter-spacing:0.08em;'
f'text-transform:uppercase;margin-bottom:8px;">Detected Structures</div>'
f'{tissue_rows if tissue_rows else "<div style=color:#475569;font-size:0.85rem;>No structures detected</div>"}'
f'</div>'
f'<div style="background:rgba(99,102,241,0.05);border:1px solid rgba(99,102,241,0.15);'
f'border-radius:10px;padding:14px 16px;">'
f'<div style="font-size:0.7rem;font-weight:700;color:#6366f1;letter-spacing:0.08em;'
f'text-transform:uppercase;margin-bottom:8px;">πŸ“– Anatomy Brief</div>'
f'<div style="font-size:0.85rem;line-height:1.7;color:#cbd5e1;">{explanation}</div>'
f'</div></div>')
def segment_image(input_image, conf_threshold=0.25):
global last_result, chat_context, _chat_history
_chat_history = []
if input_image is None:
return (None, build_results_html(None, None, None, None), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), render_chat_html([], "en"))
detector = get_detector()
result = detector.run.remote(pil_to_bytes(input_image), conf_threshold)
annotated_image = Image.open(io.BytesIO(result["annotated_bytes"]))
seen = {}
for det in result["detections"]:
cls_id, conf = det["cls_id"], det["conf"]
name = CLASS_NAMES[cls_id] if cls_id < len(CLASS_NAMES) else f"Class {cls_id}"
if name not in seen or conf > seen[name]:
seen[name] = conf
danger_detected = [n for n in seen if n in DANGER_CLASSES]
alert = (f"⚠ DANGER ZONE: {', '.join(danger_detected)} β€” Extreme caution required." if danger_detected else "βœ“ ALL CLEAR β€” No critical structures flagged.")
tissue_list = [n for n in seen if n != "Black Background"]
explanation = "No tissues detected."
if tissue_list and client:
try:
prompt = (f"You are a surgical anatomy teacher for a junior resident. Detected in a laparoscopic cholecystectomy frame: {', '.join(tissue_list)}. In 3 sentences, explain what the resident should know.")
resp = client.chat_completion([{"role": "user", "content": prompt}], max_tokens=180, temperature=0.4)
explanation = resp.choices[0].message.content.strip()
except Exception as e:
explanation = f"Explanation unavailable: {str(e)}"
chat_context = {"tissue_list": tissue_list, "alert": alert}
last_result = {"original": input_image, "annotated": annotated_image, "seen": seen, "alert": alert, "explanation": explanation}
danger_note = (f" I flagged **{', '.join(danger_detected)}** as high-risk β€” want me to explain why?" if danger_detected else " No critical structures flagged this time.")
intro = (f"I can see **{', '.join(tissue_list[:3]) if tissue_list else 'no structures'}**"
f"{' and more' if len(tissue_list) > 3 else ''} in this frame.{danger_note}\n\n"
f"What would you like to know? You can ask me about safe dissection technique, "
f"what to watch out for, or anything about the anatomy here. πŸ‘‡")
_chat_history = [{"role": "assistant", "en": intro, "display": intro}]
suggested = generate_suggested_questions(tissue_list) if tissue_list else []
q1 = suggested[0] if len(suggested) > 0 else ""
q2 = suggested[1] if len(suggested) > 1 else ""
q3 = suggested[2] if len(suggested) > 2 else ""
return (annotated_image, build_results_html(seen, alert, explanation, danger_detected), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(value=q1, visible=bool(q1)), gr.update(value=q2, visible=bool(q2)), gr.update(value=q3, visible=bool(q3)), render_chat_html(_chat_history, "en"))
def send_message(message, language):
global chat_context, _chat_history
lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
if not message.strip():
return render_chat_html(_chat_history, lang_cfg["code"]), ""
tissue_list = chat_context.get("tissue_list", [])
alert = chat_context.get("alert", "")
system_prompt = ("You are SurgiSight, an expert surgical anatomy assistant for medical trainees. " + (f"Current frame detected: {', '.join(tissue_list)}. Safety status: {alert}. " if tissue_list else "") + "Answer concisely in 2-4 sentences. " + lang_cfg["prompt"])
msgs = [{"role": "system", "content": system_prompt}]
for m in _chat_history:
msgs.append({"role": m["role"], "content": m["en"]})
msgs.append({"role": "user", "content": message})
try:
resp = client.chat_completion(msgs, max_tokens=200, temperature=0.5)
reply = resp.choices[0].message.content.strip()
except Exception as e:
reply = f"Error: {str(e)}"
_chat_history.append({"role": "user", "en": message, "display": message})
_chat_history.append({"role": "assistant", "en": reply, "display": reply})
return render_chat_html(_chat_history, lang_cfg["code"]), ""
def export_pdf():
if not last_result:
return None
return generate_pdf(last_result["original"], last_result["annotated"], last_result["seen"], last_result["alert"], last_result["explanation"])
def export_word():
if not last_result:
return None
return generate_word(last_result["original"], last_result["annotated"], last_result["seen"], last_result["alert"], last_result["explanation"])
css = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* { font-family: 'Inter', sans-serif !important; }
.gradio-container { max-width: 1200px !important; margin: 0 auto !important; background: #0a0f1e !important; }
.generating, .progress-text, .progress-bar-wrap, .eta-bar, .eta-text, footer { display: none !important; }
#app-header { background: linear-gradient(135deg,#0f172a 0%,#1e1b4b 50%,#0f172a 100%); border-bottom: 1px solid rgba(99,102,241,0.2); padding: 24px 32px 20px; position: relative; overflow: hidden; }
#app-header::before { content:''; position:absolute; top:-50%; right:-10%; width:400px; height:400px; background: radial-gradient(circle,rgba(99,102,241,0.12) 0%,transparent 70%); pointer-events:none; }
#run-btn button { background: linear-gradient(135deg,#6366f1,#8b5cf6) !important; border: none !important; border-radius: 10px !important; font-weight: 600 !important; font-size: 0.95rem !important; letter-spacing: 0.02em !important; padding: 14px 28px !important; box-shadow: 0 4px 20px rgba(99,102,241,0.35) !important; transition: all 0.2s !important; }
#run-btn button:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 28px rgba(99,102,241,0.5) !important; }
.export-btn button { background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 0.83rem !important; color: #94a3b8 !important; transition: all 0.15s !important; }
.export-btn button:hover { background: rgba(255,255,255,0.08) !important; color: #e2e8f0 !important; }
.sq-btn button { background: rgba(99,102,241,0.08) !important; border: 1px solid rgba(99,102,241,0.2) !important; border-radius: 20px !important; color: #a5b4fc !important; font-size: 0.78rem !important; font-weight: 500 !important; padding: 6px 14px !important; transition: all 0.15s !important; white-space: normal !important; text-align: left !important; height: auto !important; min-height: 0 !important; }
.sq-btn button:hover { background: rgba(99,102,241,0.18) !important; color: #c7d2fe !important; }
#chat-input { width: 100% !important; margin-top: 10px !important; }
#chat-input textarea { width: 100% !important; min-height: 92px !important; background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 10px !important; color: #e2e8f0 !important; font-size: 0.88rem !important; padding: 14px 16px !important; resize: none !important; }
#chat-input textarea:focus { border-color: rgba(99,102,241,0.5) !important; box-shadow: 0 0 0 3px rgba(99,102,241,0.10) !important; }
#chat-input textarea::placeholder { color: #475569 !important; }
#send-btn { width: 100% !important; margin-top: 10px !important; }
#send-btn button { width: 100% !important; background: #f97316 !important; border: none !important; border-radius: 10px !important; font-weight: 700 !important; min-height: 52px !important; }
#send-btn button:hover { background: #ea580c !important; }
.lang-select select, .lang-select .wrap { background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 8px !important; font-size: 0.83rem !important; }
.file-download { background: rgba(255,255,255,0.03) !important; border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 8px !important; }
input[type=range] { accent-color: #6366f1 !important; }
#surgi-overlay { display:none; position:fixed; inset:0; background:rgba(2,6,23,0.85); backdrop-filter:blur(6px); z-index:99999; flex-direction:column; align-items:center; justify-content:center; }
#surgi-overlay.active { display:flex !important; }
.or-ring { width:56px; height:56px; border-radius:50%; border:3px solid rgba(99,102,241,0.2); border-top-color:#6366f1; animation:spin 0.9s linear infinite; margin-bottom:20px; }
@keyframes spin { to { transform:rotate(360deg) } }
.or-text { color:#e2e8f0; font-size:1rem; font-weight:600; letter-spacing:0.02em; }
.or-sub { color:#475569; font-size:0.8rem; margin-top:6px; }
"""
overlay_js = """
() => {
const div = document.createElement('div'); div.id = 'surgi-overlay';
div.innerHTML = '<div class="or-ring"></div><div class="or-text">Analysing Surgical Frame</div><div class="or-sub">YOLOv8 Β· Modal GPU Β· Llama 3.1</div>';
document.body.appendChild(div);
function attachBtn() {
const btn = document.querySelector('#run-btn button');
if (btn) { btn.addEventListener('click', () => div.classList.add('active')); }
else { setTimeout(attachBtn, 500); }
}
attachBtn();
new MutationObserver(() => {
const img = document.querySelector('#output-frame img');
if (img && img.src && img.src.length > 80) div.classList.remove('active');
}).observe(document.body, {childList:true,subtree:true,attributes:true,attributeFilter:['src']});
setTimeout(() => div.classList.remove('active'), 90000);
}
"""
HEADER_HTML = """
<div id="app-header">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:10px;">
<div style="width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:1.2rem;box-shadow:0 4px 16px rgba(99,102,241,0.4);">πŸ”¬</div>
<div>
<div style="font-size:1.35rem;font-weight:700;color:#e2e8f0;letter-spacing:-0.01em;">SurgiSight</div>
<div style="font-size:0.75rem;color:#475569;font-weight:500;letter-spacing:0.04em;text-transform:uppercase;margin-top:1px;">Surgical Anatomy AI Β· Laparoscopic Training</div>
</div>
</div>
<div style="font-size:0.83rem;color:#64748b;max-width:600px;line-height:1.6;margin-bottom:14px;">
Bile duct injuries occur in <strong style="color:#a5b4fc;">1 in 300</strong> laparoscopic cholecystectomies.
SurgiSight identifies danger zones in real time and explains anatomy for surgical trainees.
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;">
<span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(99,102,241,0.12);color:#a5b4fc;border:1px solid rgba(99,102,241,0.2);">YOLOv8n-seg</span>
<span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(139,92,246,0.12);color:#c4b5fd;border:1px solid rgba(139,92,246,0.2);">Llama 3.1 8B</span>
<span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(34,197,94,0.08);color:#86efac;border:1px solid rgba(34,197,94,0.15);">Modal GPU Β· T4</span>
<span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(255,255,255,0.05);color:#64748b;border:1px solid rgba(255,255,255,0.08);">CholecSeg8k Β· 13 classes Β· mAP50: 0.581</span>
</div>
</div>
"""
FLASH_CARDS_HTML = """
<div id="flashcard-section" style="max-width:960px;margin:32px auto 8px;padding:0 16px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px;">
<div style="height:2px;flex:1;background:linear-gradient(90deg,rgba(99,102,241,0.5),transparent);"></div>
<span style="font-size:0.68rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#6366f1;">⚑ Surgical Intelligence Feed</span>
<div style="height:2px;flex:1;background:linear-gradient(90deg,transparent,rgba(99,102,241,0.5));"></div>
</div>
<div style="position:relative;">
<div style="position:absolute;inset:-2px;border-radius:20px;background:linear-gradient(135deg,rgba(99,102,241,0.25),rgba(139,92,246,0.15),rgba(236,72,153,0.1));filter:blur(12px);z-index:0;"></div>
<div id="fc-card" style="position:relative;z-index:1;background:linear-gradient(135deg,#0f1729 0%,#131a2e 60%,#0e1628 100%);border:1px solid rgba(99,102,241,0.25);border-radius:18px;padding:28px 32px 22px;overflow:hidden;min-height:200px;">
<div style="position:absolute;top:0;right:0;width:160px;height:160px;background:radial-gradient(circle at top right,rgba(99,102,241,0.12),transparent 70%);pointer-events:none;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<div id="fc-tag" style="display:inline-flex;align-items:center;gap:6px;font-size:0.65rem;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;padding:4px 12px;border-radius:20px;background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;">πŸ”¬ DID YOU KNOW?</div>
<div id="fc-counter" style="font-size:0.72rem;color:#475569;font-weight:600;letter-spacing:0.05em;">1 / 10</div>
</div>
<div style="font-size:4rem;line-height:0.6;color:rgba(99,102,241,0.15);font-family:Georgia,serif;font-weight:700;margin-bottom:4px;user-select:none;">"</div>
<div id="fc-text" style="font-size:1.05rem;font-weight:500;line-height:1.75;color:#e2e8f0;min-height:60px;transition:opacity 0.4s ease,transform 0.3s ease;">Loading...</div>
<div id="fc-source" style="margin-top:14px;font-size:0.72rem;color:#475569;font-style:italic;letter-spacing:0.02em;"></div>
<div id="fc-link-wrap" style="margin-top:8px;display:none;">
<a id="fc-link" href="#" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:5px;font-size:0.72rem;color:#6366f1;text-decoration:none;font-weight:600;padding:4px 10px;border-radius:20px;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.25);transition:all 0.15s;" onmouseover="this.style.background='rgba(99,102,241,0.2)'" onmouseout="this.style.background='rgba(99,102,241,0.1)'">πŸ”— Read more β†’</a>
</div>
<div style="margin-top:18px;">
<div id="fc-progress" style="height:3px;border-radius:20px;background:rgba(255,255,255,0.06);overflow:hidden;margin-bottom:12px;">
<div id="fc-progress-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:20px;transition:width linear;"></div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div id="fc-dots" style="display:flex;gap:5px;flex-wrap:wrap;max-width:200px;"></div>
<div style="display:flex;gap:8px;">
<button onclick="fcPrev()" style="background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#94a3b8;border-radius:8px;padding:5px 12px;font-size:0.75rem;cursor:pointer;transition:all 0.15s;font-weight:600;" onmouseover="this.style.background='rgba(99,102,241,0.2)';this.style.color='#a5b4fc'" onmouseout="this.style.background='rgba(255,255,255,0.05)';this.style.color='#94a3b8'">← Prev</button>
<button onclick="fcNext()" style="background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;border-radius:8px;padding:5px 12px;font-size:0.75rem;cursor:pointer;transition:all 0.15s;font-weight:600;" onmouseover="this.style.background='rgba(99,102,241,0.3)';this.style.color='#c7d2fe'" onmouseout="this.style.background='rgba(99,102,241,0.15)';this.style.color='#a5b4fc'">Next β†’</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
const CARDS=[
{
tag:"⚠️ The Hidden Danger",
tagColor:"rgba(239,68,68,0.15)",tagBorder:"rgba(239,68,68,0.35)",tagText:"#fca5a5",
text:"Bile duct injuries occur in <strong style='color:#f87171;'>1 in every 300</strong> laparoscopic gallbladder removals β€” a rare but life-altering complication that can lead to multiple re-operations, liver failure, and lifelong health issues.",
source:"β€” Strasberg et al., Journal of the American College of Surgeons, 1995",
link:""
},
{
tag:"πŸ“° New Research 2025",
tagColor:"rgba(251,191,36,0.12)",tagBorder:"rgba(251,191,36,0.3)",tagText:"#fde68a",
text:"A 2025 study of <strong style='color:#fde68a;'>737,908 Medicare patients</strong> found that robotic-assisted cholecystectomy has a bile duct injury rate <strong style='color:#fde68a;'>3Γ— higher</strong> than standard laparoscopic surgery β€” highlighting how even newer technology can increase risk without better training.",
source:"β€” Medscape, March 2025",
link:"https://www.medscape.com/viewarticle/bile-duct-injury-risk-higher-robotic-cholecystectomy-2025a10007bv"
},
{
tag:"πŸ”¬ What SurgiSight Detects",
tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",
text:"SurgiSight identifies <strong style='color:#c7d2fe;'>13 structures</strong> inside a live surgical frame β€” including the liver, gallbladder, fat, grasper instruments, and three <strong style='color:#f87171;'>DANGER-flagged structures</strong>: the Hepatic Vein, Cystic Duct, and Blood.",
source:"β€” CholecSeg8k, MICCAI 2020 Β· SurgiSight, 2026",
link:"https://arxiv.org/abs/2012.12503"
},
{
tag:"πŸ“Š The Dataset Behind It",
tagColor:"rgba(34,197,94,0.12)",tagBorder:"rgba(34,197,94,0.3)",tagText:"#86efac",
text:"<strong style='color:#86efac;'>CholecSeg8k</strong> is the world's only publicly available annotated dataset for laparoscopic cholecystectomy segmentation β€” 8,080 real surgical frames hand-labelled by medical experts across 13 anatomical classes.",
source:"β€” Hong et al., MICCAI 2020 Workshop",
link:"https://arxiv.org/abs/2012.12503"
},
{
tag:"πŸ›‘οΈ The Golden Rule: CVS",
tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",
text:"The <strong style='color:#c7d2fe;'>Critical View of Safety (CVS)</strong> is the surgical gold standard: before cutting anything, a surgeon must confirm the hepatocystic triangle is clear and only two structures enter the gallbladder. The American College of Surgeons now mandates CVS documentation.",
source:"β€” ACS Bulletin, May 2025",
link:"https://www.facs.org/for-medical-professionals/news-publications/news-and-articles/bulletin/2025/may-2025-volume-110-issue-5/critical-view-of-safety/"
},
{
tag:"πŸ€– AI Is Entering the OR",
tagColor:"rgba(139,92,246,0.15)",tagBorder:"rgba(139,92,246,0.35)",tagText:"#c4b5fd",
text:"Researchers have shown that <strong style='color:#c4b5fd;'>AI can recognise surgical phases</strong> in laparoscopic cholecystectomy videos with over 90% accuracy β€” paving the way for real-time intraoperative guidance systems like SurgiSight.",
source:"β€” PubMed, AI in Laparoscopic Surgery, 2024",
link:"https://pmc.ncbi.nlm.nih.gov/articles/PMC11599821/"
},
{
tag:"🌍 Scale of the Problem",
tagColor:"rgba(251,191,36,0.12)",tagBorder:"rgba(251,191,36,0.3)",tagText:"#fde68a",
text:"Over <strong style='color:#fde68a;'>1.2 million</strong> gallbladder removal surgeries are performed in the US every year. Even a small improvement in trainee safety awareness could prevent hundreds of devastating complications annually.",
source:"β€” American College of Surgeons",
link:""
},
{
tag:"πŸ₯ Surgical AI Goes Enterprise",
tagColor:"rgba(34,197,94,0.12)",tagBorder:"rgba(34,197,94,0.3)",tagText:"#86efac",
text:"Surgical Safety Technologies (SST) was named to <strong style='color:#86efac;'>TIME's World's Top HealthTech Companies of 2025</strong> and joined the EU AI Pact β€” signalling that AI-powered surgical safety is moving from research labs into real hospitals.",
source:"β€” SST Press Release, 2025",
link:"https://www.surgicalsafety.com/company/news/sst-marks-a-defining-year-of-enterprise-execution-and-data-driven-impact"
},
{
tag:"⚑ How Fast Is SurgiSight?",
tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",
text:"SurgiSight analyses a full surgical frame and generates an AI anatomy explanation in <strong style='color:#c7d2fe;'>under 2 seconds</strong> β€” powered by a Modal T4 GPU for segmentation and Meta Llama 3.1 8B for the teaching note.",
source:"β€” SurgiSight benchmark, Build Small Hackathon 2026",
link:""
},
{
tag:"πŸ“‹ New 2025 Guidelines",
tagColor:"rgba(239,68,68,0.15)",tagBorder:"rgba(239,68,68,0.35)",tagText:"#fca5a5",
text:"SAGES and AHPBA published new 2025 guidelines for managing bile duct injuries β€” recommending <strong style='color:#f87171;'>delayed repair (6+ weeks)</strong> to reduce re-operation rates. Prevention through better training, as SurgiSight enables, remains the first priority.",
source:"β€” SAGES-AHPBA 2025 Guidelines, PubMed",
link:"https://pubmed.ncbi.nlm.nih.gov/41266841/"
}
];
let cur=0,timer=null;
const IV=7000;
function dots(){const d=document.getElementById('fc-dots');if(!d)return;d.innerHTML='';CARDS.forEach((_,i)=>{const e=document.createElement('div');e.style.cssText=`width:${i===cur?20:6}px;height:6px;border-radius:20px;transition:all 0.3s ease;background:${i===cur?'#6366f1':'rgba(255,255,255,0.12)'};`;d.appendChild(e);});}
function show(idx,dir){
const tx=document.getElementById('fc-text'),tg=document.getElementById('fc-tag'),sr=document.getElementById('fc-source'),ct=document.getElementById('fc-counter'),br=document.getElementById('fc-progress-bar'),lw=document.getElementById('fc-link-wrap'),la=document.getElementById('fc-link');
const c=CARDS[idx];if(!tx)return;
tx.style.opacity='0';tx.style.transform=`translateX(${dir>0?'24px':'-24px'})`;
setTimeout(()=>{
tx.innerHTML=c.text;sr.textContent=c.source;ct.textContent=`${idx+1} / ${CARDS.length}`;
tg.innerHTML=c.tag;tg.style.background=c.tagColor;tg.style.borderColor=c.tagBorder;tg.style.color=c.tagText;
if(c.link){lw.style.display='block';la.href=c.link;}else{lw.style.display='none';}
dots();
tx.style.opacity='1';tx.style.transform='translateX(0)';
br.style.transition='none';br.style.width='0%';
setTimeout(()=>{br.style.transition=`width ${IV}ms linear`;br.style.width='100%';},30);
},280);
}
window.fcNext=function(){cur=(cur+1)%CARDS.length;show(cur,1);reset();};
window.fcPrev=function(){cur=(cur-1+CARDS.length)%CARDS.length;show(cur,-1);reset();};
function reset(){clearInterval(timer);timer=setInterval(()=>{cur=(cur+1)%CARDS.length;show(cur,1);},IV);}
function init(){if(!document.getElementById('fc-text')){setTimeout(init,400);return;}show(0,1);reset();}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
})();
</script>
"""
with gr.Blocks(title="SurgiSight β€” Surgical AI", css=css) as demo:
gr.HTML(HEADER_HTML)
gr.HTML(f'<script>{overlay_js.strip()}</script>')
with gr.Row(equal_height=False):
with gr.Column(scale=1, min_width=320):
input_img = gr.Image(type="pil", label="Surgical Frame", height=260, elem_id="input-frame")
conf_slider = gr.Slider(0.1, 0.9, value=0.25, step=0.05, label="Detection Confidence")
run_btn = gr.Button("β–Ά Run Analysis", variant="primary", size="lg", elem_id="run-btn")
with gr.Row():
pdf_btn = gr.Button("⬇ PDF", visible=False, elem_classes=["export-btn"])
word_btn = gr.Button("⬇ Word", visible=False, elem_classes=["export-btn"])
with gr.Row():
pdf_output = gr.File(label="PDF", visible=False, elem_classes=["file-download"])
word_output = gr.File(label="Word", visible=False, elem_classes=["file-download"])
output_img = gr.Image(type="pil", label="Segmented Output", height=260, elem_id="output-frame")
with gr.Column(scale=1, min_width=320):
results_display = gr.HTML(value='<div style="display:flex;align-items:center;justify-content:center;height:160px;color:#475569;font-size:0.85rem;gap:8px;"><span>πŸ”¬</span>&nbsp;Run analysis to see results</div>')
with gr.Column(scale=1, min_width=300, visible=False) as chat_col:
gr.HTML('<div style="padding:0 0 10px;"><div style="font-size:0.95rem;font-weight:700;color:#e2e8f0;margin-bottom:2px;">πŸ’¬ AI Consult</div><div style="font-size:0.75rem;color:#475569;">Ask anything about the detected anatomy</div></div>')
lang_select = gr.Dropdown(choices=list(LANGUAGES.keys()), value="English", show_label=False, elem_classes=["lang-select"])
chat_display = gr.HTML(render_chat_html([], "en"))
with gr.Row():
sq1 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
sq2 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
sq3 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
chat_input = gr.Textbox(placeholder="Ask about anatomy, safety, or technique…", show_label=False, lines=3, max_lines=5, container=False, elem_id="chat-input")
send_btn = gr.Button("Send", variant="primary", elem_id="send-btn")
gr.Examples(examples=[["examples/frame_80_endo.png"], ["examples/frame_912_endo.png"], ["examples/frame_2176_endo.png"], ["examples/frame_939_endo.png"]], inputs=input_img, label="Example frames β€” CholecSeg8k dataset", examples_per_page=4)
gr.HTML(FLASH_CARDS_HTML)
gr.HTML('<div style="text-align:center;padding:16px;font-size:0.72rem;color:#1e293b;">CholecSeg8k Β· MICCAI 2020 Β· No patient data Β· Research prototype only Β· Build Small Hackathon 2026</div>')
run_btn.click(fn=segment_image, inputs=[input_img, conf_slider], outputs=[output_img, results_display, pdf_btn, word_btn, chat_col, sq1, sq2, sq3, chat_display])
pdf_btn.click(fn=export_pdf, outputs=[pdf_output]).then(fn=lambda: gr.update(visible=True), outputs=[pdf_output])
word_btn.click(fn=export_word, outputs=[word_output]).then(fn=lambda: gr.update(visible=True), outputs=[word_output])
send_btn.click(fn=send_message, inputs=[chat_input, lang_select], outputs=[chat_display, chat_input])
chat_input.submit(fn=send_message, inputs=[chat_input, lang_select], outputs=[chat_display, chat_input])
lang_select.change(fn=retranslate_history, inputs=[lang_select], outputs=[chat_display])
sq1.click(fn=send_message, inputs=[sq1, lang_select], outputs=[chat_display, chat_input])
sq2.click(fn=send_message, inputs=[sq2, lang_select], outputs=[chat_display, chat_input])
sq3.click(fn=send_message, inputs=[sq3, lang_select], outputs=[chat_display, chat_input])
if __name__ == "__main__":
demo.launch()