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"""
""")
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"""
""")
scroll_js = ""
return f"""
{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'
{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 = """
"""
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()