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 """
πŸ”¬
Run analysis on a surgical frame, then ask anything about the anatomy.
""" items = [] for i, msg in enumerate(history): role = msg["role"] text = msg["display"] text_html = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text_html = text_html.replace("\n\n", "

").replace("\n", "
") text_html = f"

{text_html}

" if role == "user": items.append(f"""
{text_html}
""") else: audio_b64 = tts_to_b64(text, lang_code) audio_html = spk_btn = "" if audio_b64: aid = f"aud{i}" audio_html = f'' spk_btn = ( f'' ) items.append(f"""
S
SurgiSight AI
{text_html}
{spk_btn}
{audio_html}
""") scroll_js = "" return f"""
{''.join(items)}
{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 """
πŸ”¬ Run analysis to see results
""" 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'{"DANGER" if is_d else "SAFE"}') tissue_rows += ( f'
' f'
{name}
' f'
' f'
' f'
{pct}%
' f'{badge}
') return ( f'
' f'
' f'{alert_icon}' f'{alert}
' f'
' f'
Detected Structures
' f'{tissue_rows if tissue_rows else "
No structures detected
"}' f'
' f'
' f'
πŸ“– Anatomy Brief
' f'
{explanation}
' f'
') 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 = '
Analysing Surgical Frame
YOLOv8 Β· Modal GPU Β· Llama 3.1
'; 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 = """
πŸ”¬
SurgiSight
Surgical Anatomy AI Β· Laparoscopic Training
Bile duct injuries occur in 1 in 300 laparoscopic cholecystectomies. SurgiSight identifies danger zones in real time and explains anatomy for surgical trainees.
YOLOv8n-seg Llama 3.1 8B Modal GPU Β· T4 CholecSeg8k Β· 13 classes Β· mAP50: 0.581
""" FLASH_CARDS_HTML = """
⚑ Surgical Intelligence Feed
πŸ”¬ DID YOU KNOW?
1 / 10
"
Loading...
""" with gr.Blocks(title="SurgiSight β€” Surgical AI", css=css) as demo: gr.HTML(HEADER_HTML) gr.HTML(f'') 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='
πŸ”¬ Run analysis to see results
') with gr.Column(scale=1, min_width=300, visible=False) as chat_col: gr.HTML('
πŸ’¬ AI Consult
Ask anything about the detected anatomy
') 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('
CholecSeg8k Β· MICCAI 2020 Β· No patient data Β· Research prototype only Β· Build Small Hackathon 2026
') 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()