| 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> 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> 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() |