File size: 49,012 Bytes
842d762
fbea569
 
 
 
 
106237e
ea7385f
0c64177
8fb9b8c
ea7385f
3436668
 
 
 
 
56f05d5
56f5584
 
 
 
 
3436668
de72a23
 
 
 
 
 
48b651f
5663dd7
 
 
48b651f
5663dd7
de72a23
fa9be09
 
de72a23
 
48b651f
970acca
8fb9b8c
 
9f42ce1
842d762
9f42ce1
 
842d762
9f42ce1
 
 
 
48b651f
842d762
 
48b651f
5663dd7
 
7bdad65
a239b88
 
 
 
cce64c3
a239b88
 
cce64c3
f424041
7bdad65
8fb9b8c
f424041
 
8fb9b8c
f424041
fa9be09
f424041
 
 
8fb9b8c
f424041
 
8fb9b8c
9f42ce1
 
 
f424041
 
 
 
 
 
 
 
 
 
 
 
 
 
9f42ce1
 
 
8fb9b8c
 
fa9be09
 
c853c34
fa9be09
c853c34
fa9be09
 
 
5663dd7
8fb9b8c
5663dd7
8fb9b8c
5663dd7
f424041
8fb9b8c
5663dd7
 
fa9be09
 
 
 
5663dd7
 
 
 
 
cce64c3
5663dd7
cce64c3
8fb9b8c
cce64c3
 
 
 
 
 
 
 
 
 
 
 
8fb9b8c
cce64c3
 
 
 
 
 
 
 
 
 
 
 
 
fa9be09
cce64c3
 
 
 
 
8fb9b8c
 
fa9be09
5663dd7
8fb9b8c
fa9be09
8fb9b8c
 
 
 
f424041
8fb9b8c
 
 
f424041
8fb9b8c
5663dd7
48b651f
 
f424041
48b651f
f424041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8fb9b8c
f424041
 
 
 
 
 
 
fa9be09
f424041
878087d
f424041
 
 
 
 
48b651f
56f5584
 
 
 
f424041
 
fa9be09
f424041
 
56f5584
 
f424041
fa9be09
f424041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56f5584
f424041
 
 
 
 
 
 
 
56f5584
f424041
 
 
 
 
 
 
 
 
 
 
878087d
f424041
 
56f5584
f424041
 
 
 
56f5584
c853c34
 
 
 
 
 
f424041
 
c853c34
f424041
c853c34
 
f424041
 
c853c34
f424041
c853c34
 
 
 
 
 
 
 
 
 
 
 
 
7c68a59
c853c34
 
 
 
 
 
 
 
 
 
7c68a59
6fa11ec
c853c34
 
 
 
 
48b651f
5663dd7
 
48b651f
f424041
9f42ce1
842d762
970acca
48b651f
 
c853c34
 
f424041
 
c853c34
f424041
c853c34
 
9f42ce1
 
f424041
 
c853c34
9f42ce1
c853c34
 
f424041
 
da17147
f424041
da17147
 
f424041
c853c34
 
 
 
f424041
9f42ce1
5663dd7
 
8fb9b8c
037e21a
5663dd7
c853c34
f424041
 
 
 
 
 
9f42ce1
f424041
c853c34
9f42ce1
c853c34
f424041
 
5663dd7
5b7aaf3
48b651f
f424041
 
 
48b651f
56f5584
f424041
 
 
48b651f
 
fa9be09
 
c853c34
 
f424041
 
 
c853c34
f424041
c853c34
f424041
c853c34
f424041
 
 
fa9be09
f424041
 
 
 
c853c34
 
f424041
5b7aaf3
f424041
c853c34
 
 
55eaa55
0c64177
037e21a
 
c853c34
6fa11ec
037e21a
c853c34
 
 
 
 
037e21a
c853c34
 
 
 
 
037e21a
 
de72a23
fa9be09
 
 
f424041
fa9be09
 
f424041
fa9be09
 
 
 
 
 
 
c853c34
 
 
 
fa9be09
 
 
 
8bfbf60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73f4351
fa9be09
73f4351
fa9be09
 
7c68a59
f424041
 
fa9be09
f424041
fa9be09
 
f424041
fa9be09
7c68a59
fa9be09
f424041
fa9be09
f424041
 
fa9be09
 
 
 
 
f424041
 
de72a23
f424041
8bfbf60
f424041
de72a23
f424041
 
 
 
 
8fb9b8c
5663dd7
 
 
825832c
087edbe
73f4351
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
import sys, types, os

audioop_mock = types.ModuleType("audioop")
sys.modules["audioop"] = audioop_mock
sys.modules["pyaudioop"] = audioop_mock

import gradio as gr
import modal
from PIL import Image
import io, datetime, base64, re
from huggingface_hub import InferenceClient
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

try:
    from gtts import gTTS
    TTS_AVAILABLE = True
except ImportError:
    TTS_AVAILABLE = False

CLASS_NAMES = [
    "Black Background","Abdominal Wall","Liver","Gastrointestinal Tract",
    "Fat","Grasper","Connective Tissue","Blood","Cystic Duct",
    "L-hook Electrocautery","Gallbladder","Hepatic Vein","Liver Ligament"
]
DANGER_CLASSES = ["Hepatic Vein","Cystic Duct","Blood"]
LANGUAGES = {
    "English": {"code":"en","prompt":"Respond in English."},
    "French":  {"code":"fr","prompt":"RΓ©ponds en franΓ§ais."},
}

last_result = {}
chat_context = {}
_chat_history = []

try:
    client = InferenceClient(model="meta-llama/Llama-3.1-8B-Instruct", token=os.environ.get("HF_TOKEN"))
except Exception as e:
    client = None
    print(f"InferenceClient failed: {e}")

def get_detector():
    SurgiSightDetector = modal.Cls.from_name("surgisight", "SurgiSightDetector")
    return SurgiSightDetector()

def pil_to_bytes(img):
    buf = io.BytesIO(); img.save(buf, format="PNG"); return buf.getvalue()

def tts_to_b64(text, lang_code):
    if not TTS_AVAILABLE or not text: return ""
    try:
        tts = gTTS(text=text[:500], lang=lang_code, slow=False)
        buf = io.BytesIO()
        tts.write_to_fp(buf)
        data = buf.getvalue()
        if len(data) < 100: return ""
        return base64.b64encode(data).decode()
    except Exception as e:
        print(f"TTS error: {e}")
        return ""

def translate_to(text, lang_cfg):
    if lang_cfg["code"] == "en" or not client:
        return text
    try:
        lang_name = [k for k, v in LANGUAGES.items() if v["code"] == lang_cfg["code"]][0]
        resp = client.chat_completion(
            [{"role": "user", "content": f"Translate to {lang_name}. Output ONLY the translation.\n\n{text}"}],
            max_tokens=300, temperature=0.1
        )
        return resp.choices[0].message.content.strip()
    except:
        return text

def generate_suggested_questions(tissue_list):
    questions = []
    for t in tissue_list:
        if t == "Hepatic Vein":
            questions.append("Why is the hepatic vein dangerous to nick?")
        elif t == "Cystic Duct":
            questions.append("How do I safely identify the cystic duct?")
        elif t == "Blood":
            questions.append("What are steps to control unexpected bleeding?")
        elif t == "Gallbladder":
            questions.append("What is the critical view of safety?")
        elif t == "L-hook Electrocautery":
            questions.append("What are risks of electrocautery near bile duct?")
        elif t == "Liver":
            questions.append("How does liver retraction affect visibility?")
        if len(questions) >= 2:
            break
    questions.append("What are common complications in laparoscopic cholecystectomy?")
    return questions[:3]

def render_chat_html(history, lang_code):
    if not history:
        return """

        <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;

                    height:300px;gap:12px;">

          <div style="font-size:2rem;">πŸ”¬</div>

          <div style="color:#475569;font-size:0.85rem;text-align:center;max-width:260px;line-height:1.6;">

            Run analysis on a surgical frame, then ask anything about the anatomy.

          </div>

        </div>"""
    items = []
    for i, msg in enumerate(history):
        role = msg["role"]
        text = msg["display"]
        text_html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
        text_html = text_html.replace("\n\n", "</p><p style='margin:6px 0;'>").replace("\n", "<br>")
        text_html = f"<p style='margin:0;'>{text_html}</p>"
        if role == "user":
            items.append(f"""

            <div style="display:flex;justify-content:flex-end;margin:10px 0;">

              <div style="background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;

                          border-radius:18px 18px 4px 18px;padding:10px 15px;max-width:75%;

                          font-size:0.875rem;line-height:1.6;box-shadow:0 2px 8px rgba(37,99,235,0.3);">

                {text_html}

              </div>

            </div>""")
        else:
            audio_b64 = tts_to_b64(text, lang_code)
            audio_html = spk_btn = ""
            if audio_b64:
                aid = f"aud{i}"
                audio_html = f'<audio id="{aid}" src="data:audio/mp3;base64,{audio_b64}" preload="auto"></audio>'
                spk_btn = (
                    f'<button onclick="(function(b){{var a=document.getElementById(\'{aid}\');'
                    f'if(!a.paused){{a.pause();a.currentTime=0;b.textContent=\'πŸ”Š\';return;}}'
                    f'document.querySelectorAll(\'audio\').forEach(function(x){{x.pause();x.currentTime=0;}});'
                    f'document.querySelectorAll(\'.spkb\').forEach(function(x){{x.textContent=\'πŸ”Š\';}});'
                    f'a.play();b.textContent=\'⏸\';a.onended=function(){{b.textContent=\'πŸ”Š\';}};'
                    f'}})(this)" class="spkb" title="Listen" '
                    f'style="background:rgba(99,102,241,0.1);border:none;cursor:pointer;font-size:0.8rem;'
                    f'padding:4px 8px;border-radius:20px;color:#6366f1;flex-shrink:0;margin-top:2px;'
                    f'transition:all 0.15s;font-weight:500;" '
                    f'onmouseover="this.style.background=\'rgba(99,102,241,0.2)\'" '
                    f'onmouseout="this.style.background=\'rgba(99,102,241,0.1)\'">πŸ”Š</button>'
                )
            items.append(f"""

            <div style="display:flex;justify-content:flex-start;margin:10px 0;">

              <div style="max-width:85%;">

                <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">

                  <div style="width:20px;height:20px;border-radius:50%;

                              background:linear-gradient(135deg,#6366f1,#8b5cf6);

                              display:flex;align-items:center;justify-content:center;

                              font-size:0.6rem;color:white;font-weight:700;flex-shrink:0;">S</div>

                  <span style="font-size:0.7rem;color:#475569;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;">SurgiSight AI</span>

                </div>

                <div style="display:flex;align-items:flex-start;gap:8px;">

                  <div style="font-size:0.875rem;line-height:1.7;color:#cbd5e1;flex:1;">

                    {text_html}

                  </div>

                  {spk_btn}

                </div>

                {audio_html}

              </div>

            </div>""")
    scroll_js = "<script>setTimeout(function(){var e=document.getElementById('ce');if(e)e.scrollIntoView({behavior:'smooth'});},80);</script>"
    return f"""

    <div style="height:380px;overflow-y:auto;padding:8px 4px;">

      {''.join(items)}

      <div id="ce"></div>

    </div>{scroll_js}"""

def retranslate_history(language):
    global _chat_history
    if not _chat_history:
        return render_chat_html([], LANGUAGES.get(language, LANGUAGES["English"])["code"])
    lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
    for msg in _chat_history:
        if msg["role"] == "assistant":
            msg["display"] = msg["en"] if lang_cfg["code"] == "en" else translate_to(msg["en"], lang_cfg)
    return render_chat_html(_chat_history, lang_cfg["code"])

def generate_pdf(original_image, annotated_image, seen, alert, explanation):
    pdf_path = "/tmp/surgisight_report.pdf"
    doc = SimpleDocTemplate(pdf_path, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
    styles = getSampleStyleSheet()
    ts = ParagraphStyle('T2', parent=styles['Title'], fontSize=22, textColor=colors.HexColor('#1a3a5c'), spaceAfter=4, fontName='Helvetica-Bold', alignment=TA_CENTER)
    ss = ParagraphStyle('S2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#888'), spaceAfter=2, alignment=TA_CENTER)
    ses = ParagraphStyle('Se2', parent=styles['Normal'], fontSize=13, textColor=colors.HexColor('#1a3a5c'), spaceAfter=6, spaceBefore=12, fontName='Helvetica-Bold')
    bs = ParagraphStyle('B2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#1a1a1a'), spaceAfter=4, leading=16)
    ds = ParagraphStyle('D2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#cc0000'), spaceAfter=4, leading=16, fontName='Helvetica-Bold', backColor=colors.HexColor('#fff0f0'), borderPadding=(6, 8, 6, 8))
    sas = ParagraphStyle('Sa2', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#1a7a40'), spaceAfter=4, leading=16, fontName='Helvetica-Bold', backColor=colors.HexColor('#f0fff4'), borderPadding=(6, 8, 6, 8))
    cs = ParagraphStyle('C2', parent=styles['Normal'], fontSize=8, textColor=colors.HexColor('#888'), alignment=TA_CENTER, spaceAfter=4)
    fs = ParagraphStyle('F2', parent=styles['Normal'], fontSize=8, textColor=colors.HexColor('#aaa'), alignment=TA_CENTER)
    story = []
    tstamp = datetime.datetime.now().strftime("%B %d, %Y at %H:%M")
    story += [Spacer(1, .3*cm), Paragraph("SurgiSight", ts), Paragraph("Surgical Anatomy Analysis Report", ss), Paragraph(f"Generated on {tstamp}", ss), Spacer(1, .2*cm), HRFlowable(width="100%", thickness=2, color=colors.HexColor('#1a3a5c')), Spacer(1, .4*cm), Paragraph("Segmentation Output", ses)]
    iw, ih = 8.5*cm, 6.5*cm
    ob = io.BytesIO(); original_image.save(ob, format="PNG"); ob.seek(0)
    ab = io.BytesIO(); annotated_image.save(ab, format="PNG"); ab.seek(0)
    it = Table([[RLImage(ob, width=iw, height=ih), RLImage(ab, width=iw, height=ih)], [Paragraph("Original Frame", cs), Paragraph("AI Segmented Output", cs)]], colWidths=[iw + .5*cm, iw + .5*cm])
    it.setStyle(TableStyle([('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f5f5f5')), ('BOX', (0, 0), (0, 0), .5, colors.HexColor('#ddd')), ('BOX', (1, 0), (1, 0), .5, colors.HexColor('#ddd'))]))
    story += [it, Spacer(1, .5*cm), Paragraph("Safety Assessment", ses)]
    if any(d in alert for d in DANGER_CLASSES):
        story.append(Paragraph(f"WARNING: {alert}", ds))
    else:
        story.append(Paragraph(f"SAFE: {alert}", sas))
    story += [Spacer(1, .4*cm), Paragraph("Detected Tissues & Instruments", ses)]
    td = [["Structure", "Confidence", "Risk Level"]]; rd = []
    for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
        if name == "Black Background":
            continue
        bar = "\u2588" * int(conf * 10) + "\u2591" * (10 - int(conf * 10))
        td.append([name, f"{conf:.1%}  {bar}", "DANGER" if name in DANGER_CLASSES else "Safe"])
        rd.append(name in DANGER_CLASSES)
    dt = Table(td, colWidths=[6*cm, 7*cm, 3.5*cm])
    dts = [('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1a3a5c')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('FONTSIZE', (0, 1), (-1, -1), 9), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.HexColor('#f9f9f9'), colors.white]), ('GRID', (0, 0), (-1, -1), .3, colors.HexColor('#ddd')), ('TOPPADDING', (0, 0), (-1, -1), 6), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ('LEFTPADDING', (0, 0), (-1, -1), 8)]
    for i, isd in enumerate(rd):
        r = i + 1; c = colors.HexColor('#cc0000') if isd else colors.HexColor('#1a7a40')
        dts.append(('TEXTCOLOR', (2, r), (2, r), c))
        if isd:
            dts.append(('FONTNAME', (2, r), (2, r), 'Helvetica-Bold'))
    dt.setStyle(TableStyle(dts))
    story += [dt, Spacer(1, .5*cm), HRFlowable(width="100%", thickness=.5, color=colors.HexColor('#ccc')), Spacer(1, .3*cm), Paragraph("Anatomy Teaching Note", ses), Paragraph(explanation, bs), Spacer(1, .5*cm), HRFlowable(width="100%", thickness=.5, color=colors.HexColor('#ccc')), Spacer(1, .3*cm)]
    md = [["Detection Model", "YOLOv8-seg fine-tuned on CholecSeg8k (MICCAI 2020)"], ["LLM", "Meta Llama 3.1 8B Instruct"], ["Inference", "Modal GPU (T4)"], ["Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"], ["mAP50", "0.581"]]
    mt = Table(md, colWidths=[4.5*cm, 12*cm])
    mt.setStyle(TableStyle([('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 8), ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#1a3a5c')), ('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#555')), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('TOPPADDING', (0, 0), (-1, -1), 3), ('BOTTOMPADDING', (0, 0), (-1, -1), 3), ('ROWBACKGROUNDS', (0, 0), (-1, -1), [colors.HexColor('#f5f5f5'), colors.white])]))
    story += [mt, Spacer(1, .4*cm), HRFlowable(width="100%", thickness=1, color=colors.HexColor('#1a3a5c')), Spacer(1, .2*cm), Paragraph("DISCLAIMER: Research prototype only. Not a medical device. No real patient data.", fs), Paragraph("Built for Build Small Hackathon 2026", fs)]
    doc.build(story)
    return pdf_path

def generate_word(original_image, annotated_image, seen, alert, explanation):
    docx_path = "/tmp/surgisight_report.docx"
    doc = Document()
    for section in doc.sections:
        section.top_margin = Inches(1); section.bottom_margin = Inches(1)
        section.left_margin = Inches(1.1); section.right_margin = Inches(1.1)
    t = doc.add_heading("SurgiSight", 0); t.alignment = WD_ALIGN_PARAGRAPH.CENTER
    for run in t.runs:
        run.font.color.rgb = RGBColor(0x1a, 0x3a, 0x5c)
    sub = doc.add_paragraph("Surgical Anatomy Analysis Report")
    sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
    sub.runs[0].font.size = Pt(11); sub.runs[0].font.color.rgb = RGBColor(0x88, 0x88, 0x88)
    tsp = doc.add_paragraph(datetime.datetime.now().strftime("Generated on %B %d, %Y at %H:%M"))
    tsp.alignment = WD_ALIGN_PARAGRAPH.CENTER; tsp.runs[0].font.size = Pt(9); tsp.runs[0].font.color.rgb = RGBColor(0x88, 0x88, 0x88)
    doc.add_paragraph(); doc.add_heading("Segmentation Output", 2)
    img_tbl = doc.add_table(rows=2, cols=2); img_tbl.style = "Table Grid"
    for ci, (pil_img, cap) in enumerate([(original_image, "Original Frame"), (annotated_image, "AI Segmented Output")]):
        buf = io.BytesIO(); pil_img.save(buf, format="PNG"); buf.seek(0)
        cell = img_tbl.cell(0, ci); cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
        cell.paragraphs[0].add_run().add_picture(buf, width=Inches(2.9))
        cc = img_tbl.cell(1, ci); cc.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
        cr = cc.paragraphs[0].add_run(cap); cr.font.size = Pt(9); cr.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
    doc.add_paragraph(); doc.add_heading("Safety Assessment", 2)
    is_danger = any(d in alert for d in DANGER_CLASSES)
    p = doc.add_paragraph(); run = p.add_run(("⚠ WARNING: " if is_danger else "βœ“ SAFE: ") + alert)
    run.bold = True; run.font.size = Pt(11)
    run.font.color.rgb = RGBColor(0xcc, 0, 0) if is_danger else RGBColor(0x1a, 0x7a, 0x40)
    doc.add_paragraph(); doc.add_heading("Detected Tissues & Instruments", 2)
    rows = [(n, c) for n, c in sorted(seen.items(), key=lambda x: -x[1]) if n != "Black Background"]
    if rows:
        tbl = doc.add_table(rows=1 + len(rows), cols=3); tbl.style = "Table Grid"
        hdr = tbl.rows[0].cells
        for i, h in enumerate(["Structure", "Confidence", "Risk Level"]):
            hdr[i].text = h; hdr[i].paragraphs[0].runs[0].bold = True
            hdr[i].paragraphs[0].runs[0].font.size = Pt(10)
            hdr[i].paragraphs[0].runs[0].font.color.rgb = RGBColor(0xff, 0xff, 0xff)
            tc = hdr[i]._tc; tcPr = tc.get_or_add_tcPr()
            shd = OxmlElement('w:shd'); shd.set(qn('w:val'), 'clear'); shd.set(qn('w:color'), 'auto'); shd.set(qn('w:fill'), '1a3a5c')
            tcPr.append(shd)
        for ri, (name, conf) in enumerate(rows):
            row = tbl.rows[ri + 1].cells; row[0].text = name; row[1].text = f"{conf:.1%}"
            is_d = name in DANGER_CLASSES
            rr = row[2].paragraphs[0].add_run("DANGER" if is_d else "Safe")
            rr.bold = is_d; rr.font.size = Pt(9)
            rr.font.color.rgb = RGBColor(0xcc, 0, 0) if is_d else RGBColor(0x1a, 0x7a, 0x40)
            for c in [row[0], row[1]]:
                if c.paragraphs[0].runs:
                    c.paragraphs[0].runs[0].font.size = Pt(9)
    doc.add_paragraph(); doc.add_heading("Anatomy Teaching Note", 2)
    doc.add_paragraph(explanation); doc.add_paragraph(); doc.add_heading("Model Information", 2)
    for label, value in [("Detection Model", "YOLOv8-seg fine-tuned on CholecSeg8k (MICCAI 2020)"), ("LLM", "Meta Llama 3.1 8B Instruct"), ("Inference", "Modal GPU (T4)"), ("Dataset", "CholecSeg8k β€” 8,080 frames, 13 classes"), ("mAP50", "0.581")]:
        p = doc.add_paragraph(); rl = p.add_run(f"{label}: "); rl.bold = True; rl.font.size = Pt(9); rl.font.color.rgb = RGBColor(0x1a, 0x3a, 0x5c)
        rv = p.add_run(value); rv.font.size = Pt(9)
    doc.add_paragraph()
    disc = doc.add_paragraph("DISCLAIMER: Research prototype only. Not a medical device. No real patient data. Built for Build Small Hackathon 2026.")
    disc.runs[0].font.size = Pt(8); disc.runs[0].font.color.rgb = RGBColor(0xaa, 0xaa, 0xaa)
    doc.save(docx_path)
    return docx_path

def build_results_html(seen, alert, explanation, danger_detected):
    if seen is None:
        return """<div style="display:flex;align-items:center;justify-content:center;height:300px;

                    color:#475569;font-size:0.85rem;gap:8px;">

                    <span>πŸ”¬</span>&nbsp;Run analysis to see results</div>"""
    is_danger = bool(danger_detected)
    alert_color = "#ef4444" if is_danger else "#22c55e"
    alert_bg = "rgba(239,68,68,0.08)" if is_danger else "rgba(34,197,94,0.08)"
    alert_border = "#fca5a5" if is_danger else "#86efac"
    alert_icon = "⚠" if is_danger else "βœ“"
    tissue_rows = ""
    for name, conf in sorted(seen.items(), key=lambda x: -x[1]):
        if name == "Black Background":
            continue
        is_d = name in DANGER_CLASSES
        pct = int(conf * 100)
        bar_color = "#ef4444" if is_d else "#6366f1"
        badge = (f'<span style="font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:20px;'
                 f'background:{"rgba(239,68,68,0.12)" if is_d else "rgba(34,197,94,0.1)"};'
                 f'color:{"#ef4444" if is_d else "#22c55e"};">{"DANGER" if is_d else "SAFE"}</span>')
        tissue_rows += (
            f'<div style="display:flex;align-items:center;gap:10px;padding:8px 0;'
            f'border-bottom:1px solid rgba(255,255,255,0.05);">'
            f'<div style="flex:1;font-size:0.82rem;font-weight:500;">{name}</div>'
            f'<div style="width:80px;background:rgba(255,255,255,0.06);border-radius:20px;height:5px;overflow:hidden;">'
            f'<div style="width:{pct}%;height:100%;background:{bar_color};border-radius:20px;"></div></div>'
            f'<div style="font-size:0.75rem;color:#94a3b8;width:34px;text-align:right;">{pct}%</div>'
            f'{badge}</div>')
    return (
        f'<div style="display:flex;flex-direction:column;gap:12px;">'
        f'<div style="padding:12px 16px;border-radius:10px;border:1px solid {alert_border};'
        f'background:{alert_bg};display:flex;align-items:center;gap:10px;">'
        f'<span style="font-size:1.1rem;">{alert_icon}</span>'
        f'<span style="font-size:0.85rem;font-weight:600;color:{alert_color};">{alert}</span></div>'
        f'<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);'
        f'border-radius:10px;padding:14px 16px;">'
        f'<div style="font-size:0.7rem;font-weight:700;color:#475569;letter-spacing:0.08em;'
        f'text-transform:uppercase;margin-bottom:8px;">Detected Structures</div>'
        f'{tissue_rows if tissue_rows else "<div style=color:#475569;font-size:0.85rem;>No structures detected</div>"}'
        f'</div>'
        f'<div style="background:rgba(99,102,241,0.05);border:1px solid rgba(99,102,241,0.15);'
        f'border-radius:10px;padding:14px 16px;">'
        f'<div style="font-size:0.7rem;font-weight:700;color:#6366f1;letter-spacing:0.08em;'
        f'text-transform:uppercase;margin-bottom:8px;">πŸ“– Anatomy Brief</div>'
        f'<div style="font-size:0.85rem;line-height:1.7;color:#cbd5e1;">{explanation}</div>'
        f'</div></div>')

def segment_image(input_image, conf_threshold=0.25):
    global last_result, chat_context, _chat_history
    _chat_history = []
    if input_image is None:
        return (None, build_results_html(None, None, None, None), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), render_chat_html([], "en"))
    detector = get_detector()
    result = detector.run.remote(pil_to_bytes(input_image), conf_threshold)
    annotated_image = Image.open(io.BytesIO(result["annotated_bytes"]))
    seen = {}
    for det in result["detections"]:
        cls_id, conf = det["cls_id"], det["conf"]
        name = CLASS_NAMES[cls_id] if cls_id < len(CLASS_NAMES) else f"Class {cls_id}"
        if name not in seen or conf > seen[name]:
            seen[name] = conf
    danger_detected = [n for n in seen if n in DANGER_CLASSES]
    alert = (f"⚠ DANGER ZONE: {', '.join(danger_detected)} β€” Extreme caution required." if danger_detected else "βœ“ ALL CLEAR β€” No critical structures flagged.")
    tissue_list = [n for n in seen if n != "Black Background"]
    explanation = "No tissues detected."
    if tissue_list and client:
        try:
            prompt = (f"You are a surgical anatomy teacher for a junior resident. Detected in a laparoscopic cholecystectomy frame: {', '.join(tissue_list)}. In 3 sentences, explain what the resident should know.")
            resp = client.chat_completion([{"role": "user", "content": prompt}], max_tokens=180, temperature=0.4)
            explanation = resp.choices[0].message.content.strip()
        except Exception as e:
            explanation = f"Explanation unavailable: {str(e)}"
    chat_context = {"tissue_list": tissue_list, "alert": alert}
    last_result = {"original": input_image, "annotated": annotated_image, "seen": seen, "alert": alert, "explanation": explanation}
    danger_note = (f" I flagged **{', '.join(danger_detected)}** as high-risk β€” want me to explain why?" if danger_detected else " No critical structures flagged this time.")
    intro = (f"I can see **{', '.join(tissue_list[:3]) if tissue_list else 'no structures'}**"
             f"{' and more' if len(tissue_list) > 3 else ''} in this frame.{danger_note}\n\n"
             f"What would you like to know? You can ask me about safe dissection technique, "
             f"what to watch out for, or anything about the anatomy here. πŸ‘‡")
    _chat_history = [{"role": "assistant", "en": intro, "display": intro}]
    suggested = generate_suggested_questions(tissue_list) if tissue_list else []
    q1 = suggested[0] if len(suggested) > 0 else ""
    q2 = suggested[1] if len(suggested) > 1 else ""
    q3 = suggested[2] if len(suggested) > 2 else ""
    return (annotated_image, build_results_html(seen, alert, explanation, danger_detected), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(value=q1, visible=bool(q1)), gr.update(value=q2, visible=bool(q2)), gr.update(value=q3, visible=bool(q3)), render_chat_html(_chat_history, "en"))

def send_message(message, language):
    global chat_context, _chat_history
    lang_cfg = LANGUAGES.get(language, LANGUAGES["English"])
    if not message.strip():
        return render_chat_html(_chat_history, lang_cfg["code"]), ""
    tissue_list = chat_context.get("tissue_list", [])
    alert = chat_context.get("alert", "")
    system_prompt = ("You are SurgiSight, an expert surgical anatomy assistant for medical trainees. " + (f"Current frame detected: {', '.join(tissue_list)}. Safety status: {alert}. " if tissue_list else "") + "Answer concisely in 2-4 sentences. " + lang_cfg["prompt"])
    msgs = [{"role": "system", "content": system_prompt}]
    for m in _chat_history:
        msgs.append({"role": m["role"], "content": m["en"]})
    msgs.append({"role": "user", "content": message})
    try:
        resp = client.chat_completion(msgs, max_tokens=200, temperature=0.5)
        reply = resp.choices[0].message.content.strip()
    except Exception as e:
        reply = f"Error: {str(e)}"
    _chat_history.append({"role": "user", "en": message, "display": message})
    _chat_history.append({"role": "assistant", "en": reply, "display": reply})
    return render_chat_html(_chat_history, lang_cfg["code"]), ""

def export_pdf():
    if not last_result:
        return None
    return generate_pdf(last_result["original"], last_result["annotated"], last_result["seen"], last_result["alert"], last_result["explanation"])

def export_word():
    if not last_result:
        return None
    return generate_word(last_result["original"], last_result["annotated"], last_result["seen"], last_result["alert"], last_result["explanation"])

css = """

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

* { font-family: 'Inter', sans-serif !important; }

.gradio-container { max-width: 1200px !important; margin: 0 auto !important; background: #0a0f1e !important; }

.generating, .progress-text, .progress-bar-wrap, .eta-bar, .eta-text, footer { display: none !important; }

#app-header { background: linear-gradient(135deg,#0f172a 0%,#1e1b4b 50%,#0f172a 100%); border-bottom: 1px solid rgba(99,102,241,0.2); padding: 24px 32px 20px; position: relative; overflow: hidden; }

#app-header::before { content:''; position:absolute; top:-50%; right:-10%; width:400px; height:400px; background: radial-gradient(circle,rgba(99,102,241,0.12) 0%,transparent 70%); pointer-events:none; }

#run-btn button { background: linear-gradient(135deg,#6366f1,#8b5cf6) !important; border: none !important; border-radius: 10px !important; font-weight: 600 !important; font-size: 0.95rem !important; letter-spacing: 0.02em !important; padding: 14px 28px !important; box-shadow: 0 4px 20px rgba(99,102,241,0.35) !important; transition: all 0.2s !important; }

#run-btn button:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 28px rgba(99,102,241,0.5) !important; }

.export-btn button { background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 0.83rem !important; color: #94a3b8 !important; transition: all 0.15s !important; }

.export-btn button:hover { background: rgba(255,255,255,0.08) !important; color: #e2e8f0 !important; }

.sq-btn button { background: rgba(99,102,241,0.08) !important; border: 1px solid rgba(99,102,241,0.2) !important; border-radius: 20px !important; color: #a5b4fc !important; font-size: 0.78rem !important; font-weight: 500 !important; padding: 6px 14px !important; transition: all 0.15s !important; white-space: normal !important; text-align: left !important; height: auto !important; min-height: 0 !important; }

.sq-btn button:hover { background: rgba(99,102,241,0.18) !important; color: #c7d2fe !important; }

#chat-input { width: 100% !important; margin-top: 10px !important; }

#chat-input textarea { width: 100% !important; min-height: 92px !important; background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 10px !important; color: #e2e8f0 !important; font-size: 0.88rem !important; padding: 14px 16px !important; resize: none !important; }

#chat-input textarea:focus { border-color: rgba(99,102,241,0.5) !important; box-shadow: 0 0 0 3px rgba(99,102,241,0.10) !important; }

#chat-input textarea::placeholder { color: #475569 !important; }

#send-btn { width: 100% !important; margin-top: 10px !important; }

#send-btn button { width: 100% !important; background: #f97316 !important; border: none !important; border-radius: 10px !important; font-weight: 700 !important; min-height: 52px !important; }

#send-btn button:hover { background: #ea580c !important; }

.lang-select select, .lang-select .wrap { background: rgba(255,255,255,0.04) !important; border: 1px solid rgba(255,255,255,0.1) !important; border-radius: 8px !important; font-size: 0.83rem !important; }

.file-download { background: rgba(255,255,255,0.03) !important; border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 8px !important; }

input[type=range] { accent-color: #6366f1 !important; }

#surgi-overlay { display:none; position:fixed; inset:0; background:rgba(2,6,23,0.85); backdrop-filter:blur(6px); z-index:99999; flex-direction:column; align-items:center; justify-content:center; }

#surgi-overlay.active { display:flex !important; }

.or-ring { width:56px; height:56px; border-radius:50%; border:3px solid rgba(99,102,241,0.2); border-top-color:#6366f1; animation:spin 0.9s linear infinite; margin-bottom:20px; }

@keyframes spin { to { transform:rotate(360deg) } }

.or-text { color:#e2e8f0; font-size:1rem; font-weight:600; letter-spacing:0.02em; }

.or-sub  { color:#475569; font-size:0.8rem; margin-top:6px; }

"""

overlay_js = """

() => {

  const div = document.createElement('div'); div.id = 'surgi-overlay';

  div.innerHTML = '<div class="or-ring"></div><div class="or-text">Analysing Surgical Frame</div><div class="or-sub">YOLOv8 Β· Modal GPU Β· Llama 3.1</div>';

  document.body.appendChild(div);

  function attachBtn() {

    const btn = document.querySelector('#run-btn button');

    if (btn) { btn.addEventListener('click', () => div.classList.add('active')); }

    else { setTimeout(attachBtn, 500); }

  }

  attachBtn();

  new MutationObserver(() => {

    const img = document.querySelector('#output-frame img');

    if (img && img.src && img.src.length > 80) div.classList.remove('active');

  }).observe(document.body, {childList:true,subtree:true,attributes:true,attributeFilter:['src']});

  setTimeout(() => div.classList.remove('active'), 90000);

}

"""

HEADER_HTML = """

<div id="app-header">

  <div style="display:flex;align-items:center;gap:14px;margin-bottom:10px;">

    <div style="width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:1.2rem;box-shadow:0 4px 16px rgba(99,102,241,0.4);">πŸ”¬</div>

    <div>

      <div style="font-size:1.35rem;font-weight:700;color:#e2e8f0;letter-spacing:-0.01em;">SurgiSight</div>

      <div style="font-size:0.75rem;color:#475569;font-weight:500;letter-spacing:0.04em;text-transform:uppercase;margin-top:1px;">Surgical Anatomy AI Β· Laparoscopic Training</div>

    </div>

  </div>

  <div style="font-size:0.83rem;color:#64748b;max-width:600px;line-height:1.6;margin-bottom:14px;">

    Bile duct injuries occur in <strong style="color:#a5b4fc;">1 in 300</strong> laparoscopic cholecystectomies.

    SurgiSight identifies danger zones in real time and explains anatomy for surgical trainees.

  </div>

  <div style="display:flex;flex-wrap:wrap;gap:6px;">

    <span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(99,102,241,0.12);color:#a5b4fc;border:1px solid rgba(99,102,241,0.2);">YOLOv8n-seg</span>

    <span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(139,92,246,0.12);color:#c4b5fd;border:1px solid rgba(139,92,246,0.2);">Llama 3.1 8B</span>

    <span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(34,197,94,0.08);color:#86efac;border:1px solid rgba(34,197,94,0.15);">Modal GPU Β· T4</span>

    <span style="font-size:0.7rem;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(255,255,255,0.05);color:#64748b;border:1px solid rgba(255,255,255,0.08);">CholecSeg8k Β· 13 classes Β· mAP50: 0.581</span>

  </div>

</div>

"""


FLASH_CARDS_HTML = """

<div id="flashcard-section" style="max-width:960px;margin:32px auto 8px;padding:0 16px;">

  <div style="display:flex;align-items:center;gap:10px;margin-bottom:18px;">

    <div style="height:2px;flex:1;background:linear-gradient(90deg,rgba(99,102,241,0.5),transparent);"></div>

    <span style="font-size:0.68rem;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#6366f1;">⚑ Surgical Intelligence Feed</span>

    <div style="height:2px;flex:1;background:linear-gradient(90deg,transparent,rgba(99,102,241,0.5));"></div>

  </div>

  <div style="position:relative;">

    <div style="position:absolute;inset:-2px;border-radius:20px;background:linear-gradient(135deg,rgba(99,102,241,0.25),rgba(139,92,246,0.15),rgba(236,72,153,0.1));filter:blur(12px);z-index:0;"></div>

    <div id="fc-card" style="position:relative;z-index:1;background:linear-gradient(135deg,#0f1729 0%,#131a2e 60%,#0e1628 100%);border:1px solid rgba(99,102,241,0.25);border-radius:18px;padding:28px 32px 22px;overflow:hidden;min-height:200px;">

      <div style="position:absolute;top:0;right:0;width:160px;height:160px;background:radial-gradient(circle at top right,rgba(99,102,241,0.12),transparent 70%);pointer-events:none;"></div>

      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">

        <div id="fc-tag" style="display:inline-flex;align-items:center;gap:6px;font-size:0.65rem;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;padding:4px 12px;border-radius:20px;background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;">πŸ”¬ DID YOU KNOW?</div>

        <div id="fc-counter" style="font-size:0.72rem;color:#475569;font-weight:600;letter-spacing:0.05em;">1 / 10</div>

      </div>

      <div style="font-size:4rem;line-height:0.6;color:rgba(99,102,241,0.15);font-family:Georgia,serif;font-weight:700;margin-bottom:4px;user-select:none;">"</div>

      <div id="fc-text" style="font-size:1.05rem;font-weight:500;line-height:1.75;color:#e2e8f0;min-height:60px;transition:opacity 0.4s ease,transform 0.3s ease;">Loading...</div>

      <div id="fc-source" style="margin-top:14px;font-size:0.72rem;color:#475569;font-style:italic;letter-spacing:0.02em;"></div>

      <div id="fc-link-wrap" style="margin-top:8px;display:none;">

        <a id="fc-link" href="#" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:5px;font-size:0.72rem;color:#6366f1;text-decoration:none;font-weight:600;padding:4px 10px;border-radius:20px;background:rgba(99,102,241,0.1);border:1px solid rgba(99,102,241,0.25);transition:all 0.15s;" onmouseover="this.style.background='rgba(99,102,241,0.2)'" onmouseout="this.style.background='rgba(99,102,241,0.1)'">πŸ”— Read more β†’</a>

      </div>

      <div style="margin-top:18px;">

        <div id="fc-progress" style="height:3px;border-radius:20px;background:rgba(255,255,255,0.06);overflow:hidden;margin-bottom:12px;">

          <div id="fc-progress-bar" style="height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:20px;transition:width linear;"></div>

        </div>

        <div style="display:flex;align-items:center;justify-content:space-between;">

          <div id="fc-dots" style="display:flex;gap:5px;flex-wrap:wrap;max-width:200px;"></div>

          <div style="display:flex;gap:8px;">

            <button onclick="fcPrev()" style="background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#94a3b8;border-radius:8px;padding:5px 12px;font-size:0.75rem;cursor:pointer;transition:all 0.15s;font-weight:600;" onmouseover="this.style.background='rgba(99,102,241,0.2)';this.style.color='#a5b4fc'" onmouseout="this.style.background='rgba(255,255,255,0.05)';this.style.color='#94a3b8'">← Prev</button>

            <button onclick="fcNext()" style="background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;border-radius:8px;padding:5px 12px;font-size:0.75rem;cursor:pointer;transition:all 0.15s;font-weight:600;" onmouseover="this.style.background='rgba(99,102,241,0.3)';this.style.color='#c7d2fe'" onmouseout="this.style.background='rgba(99,102,241,0.15)';this.style.color='#a5b4fc'">Next β†’</button>

          </div>

        </div>

      </div>

    </div>

  </div>

</div>

<script>

(function(){

  const CARDS=[

    {

      tag:"⚠️ The Hidden Danger",

      tagColor:"rgba(239,68,68,0.15)",tagBorder:"rgba(239,68,68,0.35)",tagText:"#fca5a5",

      text:"Bile duct injuries occur in <strong style='color:#f87171;'>1 in every 300</strong> laparoscopic gallbladder removals β€” a rare but life-altering complication that can lead to multiple re-operations, liver failure, and lifelong health issues.",

      source:"β€” Strasberg et al., Journal of the American College of Surgeons, 1995",

      link:""

    },

    {

      tag:"πŸ“° New Research 2025",

      tagColor:"rgba(251,191,36,0.12)",tagBorder:"rgba(251,191,36,0.3)",tagText:"#fde68a",

      text:"A 2025 study of <strong style='color:#fde68a;'>737,908 Medicare patients</strong> found that robotic-assisted cholecystectomy has a bile duct injury rate <strong style='color:#fde68a;'>3Γ— higher</strong> than standard laparoscopic surgery β€” highlighting how even newer technology can increase risk without better training.",

      source:"β€” Medscape, March 2025",

      link:"https://www.medscape.com/viewarticle/bile-duct-injury-risk-higher-robotic-cholecystectomy-2025a10007bv"

    },

    {

      tag:"πŸ”¬ What SurgiSight Detects",

      tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",

      text:"SurgiSight identifies <strong style='color:#c7d2fe;'>13 structures</strong> inside a live surgical frame β€” including the liver, gallbladder, fat, grasper instruments, and three <strong style='color:#f87171;'>DANGER-flagged structures</strong>: the Hepatic Vein, Cystic Duct, and Blood.",

      source:"β€” CholecSeg8k, MICCAI 2020 Β· SurgiSight, 2026",

      link:"https://arxiv.org/abs/2012.12503"

    },

    {

      tag:"πŸ“Š The Dataset Behind It",

      tagColor:"rgba(34,197,94,0.12)",tagBorder:"rgba(34,197,94,0.3)",tagText:"#86efac",

      text:"<strong style='color:#86efac;'>CholecSeg8k</strong> is the world's only publicly available annotated dataset for laparoscopic cholecystectomy segmentation β€” 8,080 real surgical frames hand-labelled by medical experts across 13 anatomical classes.",

      source:"β€” Hong et al., MICCAI 2020 Workshop",

      link:"https://arxiv.org/abs/2012.12503"

    },

    {

      tag:"πŸ›‘οΈ The Golden Rule: CVS",

      tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",

      text:"The <strong style='color:#c7d2fe;'>Critical View of Safety (CVS)</strong> is the surgical gold standard: before cutting anything, a surgeon must confirm the hepatocystic triangle is clear and only two structures enter the gallbladder. The American College of Surgeons now mandates CVS documentation.",

      source:"β€” ACS Bulletin, May 2025",

      link:"https://www.facs.org/for-medical-professionals/news-publications/news-and-articles/bulletin/2025/may-2025-volume-110-issue-5/critical-view-of-safety/"

    },

    {

      tag:"πŸ€– AI Is Entering the OR",

      tagColor:"rgba(139,92,246,0.15)",tagBorder:"rgba(139,92,246,0.35)",tagText:"#c4b5fd",

      text:"Researchers have shown that <strong style='color:#c4b5fd;'>AI can recognise surgical phases</strong> in laparoscopic cholecystectomy videos with over 90% accuracy β€” paving the way for real-time intraoperative guidance systems like SurgiSight.",

      source:"β€” PubMed, AI in Laparoscopic Surgery, 2024",

      link:"https://pmc.ncbi.nlm.nih.gov/articles/PMC11599821/"

    },

    {

      tag:"🌍 Scale of the Problem",

      tagColor:"rgba(251,191,36,0.12)",tagBorder:"rgba(251,191,36,0.3)",tagText:"#fde68a",

      text:"Over <strong style='color:#fde68a;'>1.2 million</strong> gallbladder removal surgeries are performed in the US every year. Even a small improvement in trainee safety awareness could prevent hundreds of devastating complications annually.",

      source:"β€” American College of Surgeons",

      link:""

    },

    {

      tag:"πŸ₯ Surgical AI Goes Enterprise",

      tagColor:"rgba(34,197,94,0.12)",tagBorder:"rgba(34,197,94,0.3)",tagText:"#86efac",

      text:"Surgical Safety Technologies (SST) was named to <strong style='color:#86efac;'>TIME's World's Top HealthTech Companies of 2025</strong> and joined the EU AI Pact β€” signalling that AI-powered surgical safety is moving from research labs into real hospitals.",

      source:"β€” SST Press Release, 2025",

      link:"https://www.surgicalsafety.com/company/news/sst-marks-a-defining-year-of-enterprise-execution-and-data-driven-impact"

    },

    {

      tag:"⚑ How Fast Is SurgiSight?",

      tagColor:"rgba(99,102,241,0.15)",tagBorder:"rgba(99,102,241,0.35)",tagText:"#a5b4fc",

      text:"SurgiSight analyses a full surgical frame and generates an AI anatomy explanation in <strong style='color:#c7d2fe;'>under 2 seconds</strong> β€” powered by a Modal T4 GPU for segmentation and Meta Llama 3.1 8B for the teaching note.",

      source:"β€” SurgiSight benchmark, Build Small Hackathon 2026",

      link:""

    },

    {

      tag:"πŸ“‹ New 2025 Guidelines",

      tagColor:"rgba(239,68,68,0.15)",tagBorder:"rgba(239,68,68,0.35)",tagText:"#fca5a5",

      text:"SAGES and AHPBA published new 2025 guidelines for managing bile duct injuries β€” recommending <strong style='color:#f87171;'>delayed repair (6+ weeks)</strong> to reduce re-operation rates. Prevention through better training, as SurgiSight enables, remains the first priority.",

      source:"β€” SAGES-AHPBA 2025 Guidelines, PubMed",

      link:"https://pubmed.ncbi.nlm.nih.gov/41266841/"

    }

  ];

  let cur=0,timer=null;

  const IV=7000;

  function dots(){const d=document.getElementById('fc-dots');if(!d)return;d.innerHTML='';CARDS.forEach((_,i)=>{const e=document.createElement('div');e.style.cssText=`width:${i===cur?20:6}px;height:6px;border-radius:20px;transition:all 0.3s ease;background:${i===cur?'#6366f1':'rgba(255,255,255,0.12)'};`;d.appendChild(e);});}

  function show(idx,dir){

    const tx=document.getElementById('fc-text'),tg=document.getElementById('fc-tag'),sr=document.getElementById('fc-source'),ct=document.getElementById('fc-counter'),br=document.getElementById('fc-progress-bar'),lw=document.getElementById('fc-link-wrap'),la=document.getElementById('fc-link');

    const c=CARDS[idx];if(!tx)return;

    tx.style.opacity='0';tx.style.transform=`translateX(${dir>0?'24px':'-24px'})`;

    setTimeout(()=>{

      tx.innerHTML=c.text;sr.textContent=c.source;ct.textContent=`${idx+1} / ${CARDS.length}`;

      tg.innerHTML=c.tag;tg.style.background=c.tagColor;tg.style.borderColor=c.tagBorder;tg.style.color=c.tagText;

      if(c.link){lw.style.display='block';la.href=c.link;}else{lw.style.display='none';}

      dots();

      tx.style.opacity='1';tx.style.transform='translateX(0)';

      br.style.transition='none';br.style.width='0%';

      setTimeout(()=>{br.style.transition=`width ${IV}ms linear`;br.style.width='100%';},30);

    },280);

  }

  window.fcNext=function(){cur=(cur+1)%CARDS.length;show(cur,1);reset();};

  window.fcPrev=function(){cur=(cur-1+CARDS.length)%CARDS.length;show(cur,-1);reset();};

  function reset(){clearInterval(timer);timer=setInterval(()=>{cur=(cur+1)%CARDS.length;show(cur,1);},IV);}

  function init(){if(!document.getElementById('fc-text')){setTimeout(init,400);return;}show(0,1);reset();}

  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();

})();

</script>

"""

with gr.Blocks(title="SurgiSight β€” Surgical AI", css=css) as demo:
    gr.HTML(HEADER_HTML)
    gr.HTML(f'<script>{overlay_js.strip()}</script>')
    with gr.Row(equal_height=False):
        with gr.Column(scale=1, min_width=320):
            input_img = gr.Image(type="pil", label="Surgical Frame", height=260, elem_id="input-frame")
            conf_slider = gr.Slider(0.1, 0.9, value=0.25, step=0.05, label="Detection Confidence")
            run_btn = gr.Button("β–Ά  Run Analysis", variant="primary", size="lg", elem_id="run-btn")
            with gr.Row():
                pdf_btn = gr.Button("⬇ PDF", visible=False, elem_classes=["export-btn"])
                word_btn = gr.Button("⬇ Word", visible=False, elem_classes=["export-btn"])
            with gr.Row():
                pdf_output = gr.File(label="PDF", visible=False, elem_classes=["file-download"])
                word_output = gr.File(label="Word", visible=False, elem_classes=["file-download"])
            output_img = gr.Image(type="pil", label="Segmented Output", height=260, elem_id="output-frame")
        with gr.Column(scale=1, min_width=320):
            results_display = gr.HTML(value='<div style="display:flex;align-items:center;justify-content:center;height:160px;color:#475569;font-size:0.85rem;gap:8px;"><span>πŸ”¬</span>&nbsp;Run analysis to see results</div>')
        with gr.Column(scale=1, min_width=300, visible=False) as chat_col:
            gr.HTML('<div style="padding:0 0 10px;"><div style="font-size:0.95rem;font-weight:700;color:#e2e8f0;margin-bottom:2px;">πŸ’¬ AI Consult</div><div style="font-size:0.75rem;color:#475569;">Ask anything about the detected anatomy</div></div>')
            lang_select = gr.Dropdown(choices=list(LANGUAGES.keys()), value="English", show_label=False, elem_classes=["lang-select"])
            chat_display = gr.HTML(render_chat_html([], "en"))
            with gr.Row():
                sq1 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
                sq2 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
            sq3 = gr.Button("", visible=False, size="sm", elem_classes=["sq-btn"])
            chat_input = gr.Textbox(placeholder="Ask about anatomy, safety, or technique…", show_label=False, lines=3, max_lines=5, container=False, elem_id="chat-input")
            send_btn = gr.Button("Send", variant="primary", elem_id="send-btn")

    gr.Examples(examples=[["examples/frame_80_endo.png"], ["examples/frame_912_endo.png"], ["examples/frame_2176_endo.png"], ["examples/frame_939_endo.png"]], inputs=input_img, label="Example frames β€” CholecSeg8k dataset", examples_per_page=4)
    gr.HTML(FLASH_CARDS_HTML)
    gr.HTML('<div style="text-align:center;padding:16px;font-size:0.72rem;color:#1e293b;">CholecSeg8k Β· MICCAI 2020 Β· No patient data Β· Research prototype only Β· Build Small Hackathon 2026</div>')

    run_btn.click(fn=segment_image, inputs=[input_img, conf_slider], outputs=[output_img, results_display, pdf_btn, word_btn, chat_col, sq1, sq2, sq3, chat_display])
    pdf_btn.click(fn=export_pdf, outputs=[pdf_output]).then(fn=lambda: gr.update(visible=True), outputs=[pdf_output])
    word_btn.click(fn=export_word, outputs=[word_output]).then(fn=lambda: gr.update(visible=True), outputs=[word_output])
    send_btn.click(fn=send_message, inputs=[chat_input, lang_select], outputs=[chat_display, chat_input])
    chat_input.submit(fn=send_message, inputs=[chat_input, lang_select], outputs=[chat_display, chat_input])
    lang_select.change(fn=retranslate_history, inputs=[lang_select], outputs=[chat_display])
    sq1.click(fn=send_message, inputs=[sq1, lang_select], outputs=[chat_display, chat_input])
    sq2.click(fn=send_message, inputs=[sq2, lang_select], outputs=[chat_display, chat_input])
    sq3.click(fn=send_message, inputs=[sq3, lang_select], outputs=[chat_display, chat_input])

if __name__ == "__main__":
    demo.launch()