Phanarin commited on
Commit
a324535
·
verified ·
1 Parent(s): e860bd9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -312
app.py CHANGED
@@ -1,382 +1,99 @@
1
- import os
2
- import sys
3
- import cv2
4
- import numpy as np
5
- import gradio as gr
6
- import plotly.graph_objects as go
7
- from ultralytics import YOLO
8
- import google.generativeai as genai
9
- from PIL import Image
10
- from gtts import gTTS
11
- import tempfile
12
- import datetime
13
- import requests
14
- import shutil
15
-
16
- # --- ReportLab Imports (PDF) ---
17
- from reportlab.pdfgen import canvas
18
- from reportlab.lib.pagesizes import A4
19
- from reportlab.lib.units import cm, mm
20
- from reportlab.pdfbase import pdfmetrics
21
- from reportlab.pdfbase.ttfonts import TTFont
22
-
23
- # ============================================
24
  # 1) Configuration & Setup
25
  # ============================================
26
 
27
- # 🔑 API KEY
28
- GOOGLE_API_KEY = os.getenv("AIzaSyBYksOq03N5V2MjSYicHdsk4ESdyR9FABw")
29
  if GOOGLE_API_KEY:
30
  genai.configure(api_key=GOOGLE_API_KEY)
31
 
32
- # 📂 Model Path
33
- MODEL_PATH = "otu_multiclass_yolo11s_v2.pt"
34
-
35
- LOGO_KMUTNB_URL = "https://www.mou.kmutnb.ac.th/logo_kmutnb.png"
36
- LOGO_RAMA_URL ="https://www.rama.mahidol.ac.th/nursing/sites/default/files/public/Rama_Logo.png"
37
- INTRO_SOUND_URL = "https://cdn.pixabay.com/download/audio/2022/03/24/audio_c8c8a73467.mp3?filename=cinematic-atmosphere-score-2-22136.mp3"
38
-
39
- CLASS_NAMES = {
40
- 0: "Chocolate cyst", 1: "Serous cystadenoma", 2: "Teratoma", 3: "Theca cell tumor",
41
- 4: "Simple cyst", 5: "Normal ovary", 6: "Mucinous cystadenoma", 7: "High grade serous"
42
- }
43
-
44
- # ---------------------------------------------------------
45
- # 🛠️ AUTO-DOWNLOAD FONTS
46
- # ---------------------------------------------------------
47
- def force_download_font(url, filename):
48
  if not os.path.exists(filename):
49
  print(f"📥 Downloading {filename}...")
50
  try:
51
- r = requests.get(url, allow_redirects=True)
52
- with open(filename, 'wb') as f:
53
- f.write(r.content)
 
 
 
 
 
 
54
  except Exception as e:
55
  print(f"❌ Error downloading {filename}: {e}")
56
  return False
57
  return True
58
 
59
  font_urls = [
60
- ("https://github.com/nutjunkie/thaifonts_sipa/raw/master/sipa_fonts/THSarabunNew/THSarabunNew.ttf", "THSarabunNew.ttf"),
61
- ("https://github.com/nutjunkie/thaifonts_sipa/raw/master/sipa_fonts/THSarabunNew/THSarabunNew%20Bold.ttf", "THSarabunNew-Bold.ttf")
62
  ]
63
 
64
  for url, fname in font_urls:
65
- force_download_font(url, fname)
66
-
67
- try:
68
- if os.path.exists("THSarabunNew.ttf"):
69
- pdfmetrics.registerFont(TTFont('THSarabun', 'THSarabunNew.ttf'))
70
- if os.path.exists("THSarabunNew-Bold.ttf"):
71
- pdfmetrics.registerFont(TTFont('THSarabun-Bold', 'THSarabunNew-Bold.ttf'))
72
- except Exception as e:
73
- print(f"⚠️ Font Registration Error: {e}")
74
-
75
- # ============================================
76
- # 2) Helper Functions
77
- # ============================================
78
- def text_to_speech(text):
79
- try:
80
- tts = gTTS(text, lang='th')
81
- f = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
82
- tts.save(f.name)
83
- return f.name
84
- except: return None
85
-
86
- def generate_led_html(score, diagnosis):
87
- color = "#ff4444" if "High grade" in diagnosis else "#00C851" if "Normal" in diagnosis else "#ffbb33"
88
- return f"""
89
- <div style='background-color: #2b2b2b; border: 3px solid {color}; border-radius: 12px; padding: 15px; text-align: center; box-shadow: 0 4px 8px rgba(0,0,0,0.2);'>
90
- <span style='color: #e0e0e0; font-size: 14px; text-transform: uppercase;'>Primary Diagnosis</span><br>
91
- <span style='color: {color}; font-size: 24px; font-weight: 800;'>{diagnosis}</span><br>
92
- <hr style='border-color: #444; margin: 10px 0;'>
93
- <span style='color: white; font-size: 32px; font-weight: bold;'>{score}%</span>
94
- </div>
95
- """
96
-
97
- def create_medical_report(pt_name, pt_id, diagnosis, conf):
98
  try:
99
  filename = tempfile.mktemp(suffix=".pdf")
100
  c = canvas.Canvas(filename, pagesize=A4)
101
-
102
- # Use registered font if available, else standard
103
  font_name = 'THSarabun-Bold' if 'THSarabun-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'
104
 
105
  c.setFont(font_name, 24)
106
- c.drawString(2*cm, 27*cm, "Medical Image Analysis Report")
107
-
108
- c.setFont(font_name, 16)
109
- c.drawString(2*cm, 25*cm, f"Patient Name: {pt_name}")
110
- c.drawString(2*cm, 24*cm, f"Patient ID: {pt_id}")
111
- c.drawString(2*cm, 22*cm, f"Diagnosis Result: {diagnosis}")
112
- c.drawString(2*cm, 21*cm, f"Confidence Score: {conf}%")
113
- c.drawString(2*cm, 20*cm, f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
114
-
115
- c.save()
116
- return filename
117
- except Exception as e:
118
- print(f"PDF Error: {e}")
119
  return None
120
 
121
  # ============================================
122
- # [FIXED] Chat Function
123
  # ============================================
124
  def chat_fn(message, history, crop_img, info_text, diagnosis):
125
- # เปลี่ยนจาก history = [] เป็นการรับค่า list ของ dict
126
  if history is None: history = []
127
 
128
- # 1. เช็ค API Key
129
  if not GOOGLE_API_KEY:
130
- response = "❌ ไม่พบ API KEY: กรุณาไปที่ Settings > Secrets แล้วตั้งค่า 'GOOGLE_API_KEY' จากนั้นกด Restart Space"
131
- # เพิ่มข้อความลง history แบบ Dictionary (ตามมาตรฐานใหม่ Huggingface/Gradio)
132
  history.append({"role": "user", "content": message})
133
  history.append({"role": "assistant", "content": response})
134
  return history, ""
135
 
136
  try:
137
- # 2. สร้าง Prompt
138
  context_prompt = f"""
139
  บทบาท: คุณคือผู้ช่วยทางการแพทย์อัจฉริยะ (AI Medical Assistant)
140
 
141
- ข้อมูลบริบททางการแพทย์ของผู้ป่วยรายนี้:
142
- - ผลการวินิจฉัยหลัก (Diagnosis): {diagnosis if diagnosis else "ยังไม่มีการวินิจฉัย"}
143
- - ข้อมูลเพิ่มเติม: {info_text if info_text else "ไม่มี"}
144
-
145
- คำถามจากผู้ใช้: {message}
146
-
147
- คำแนะนำในการตอบ:
148
- - ตอบเป็นภาษาไทย ให้กระชับ เข้าใจง่าย
149
- - ถ้าเกี่ยวกับเรื่องซีสต์หรือเนื้องอก ให้ข้อมูลตามหลักการแพทย์
150
  - *สำคัญ*: ต้องลงท้ายเสมอว่า "ควรปรึกษาแพทย์ผู้เชี่ยวชาญเพื่อการวินิจฉัยที่แม่นยำที่สุด"
151
  """
152
 
153
- # 3. เรียกใช้ Gemini
154
- model = genai.GenerativeModel('gemini-1.5-flash')
155
  response = model.generate_content(context_prompt)
156
  bot_reply = response.text
157
 
158
- except Exception as e:
159
  bot_reply = f"เกิดข้อผิดพลาด (System Error): {str(e)}"
160
  print(f"DEBUG ERROR: {e}")
161
 
162
- # [FIX] เพิ่มข้อความลงใน History แบบ Dictionary
163
  history.append({"role": "user", "content": message})
164
  history.append({"role": "assistant", "content": bot_reply})
165
 
166
- # คืนค่า history และ string ว่าง ("") เพื่อลบข้อความในช่องพิมพ์
167
- return history, ""
168
-
169
- # ============================================
170
- # 3) Main Inference Logic
171
- # ============================================
172
- def analyze_image(image, history_list):
173
- if history_list is None: history_list = []
174
- if image is None:
175
- return [None]*15
176
-
177
- if not os.path.exists(MODEL_PATH):
178
- error_msg = f"⚠️ Model file not found at {MODEL_PATH}. Please upload the .pt file."
179
- return image, image, image, go.Figure(), error_msg, "", None, image, "Error", 0, history_list, history_list, image, image, image
180
-
181
- history_list.append(image)
182
-
183
- lab = cv2.cvtColor(image, cv2.COLOR_RGB2LAB)
184
- l, a, b = cv2.split(lab)
185
- clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
186
- cl = clahe.apply(l)
187
- enhanced_img = cv2.merge((cl,a,b))
188
- enhanced_img = cv2.cvtColor(enhanced_img, cv2.COLOR_LAB2RGB)
189
 
190
- try:
191
- model = YOLO(MODEL_PATH)
192
- results = model.predict(
193
- enhanced_img,
194
- imgsz=640,
195
- conf=0.25,
196
- iou=0.45,
197
- augment=True,
198
- verbose=False
199
- )[0]
200
- except Exception as e:
201
- return image, image, image, go.Figure(), f"Inference Error: {e}", "", None, image, "Error", 0, history_list, history_list, image, image, image
202
-
203
- orig = image.copy()
204
- seg_overlay = image.copy()
205
- crop_img = np.zeros_like(image)
206
- info_log = "Analysis Results:\n" + "-"*20 + "\n"
207
- max_conf = 0
208
- primary_diag = "Normal / Not Found"
209
- fig = go.Figure()
210
-
211
- if results.boxes and len(results.boxes) > 0:
212
- boxes = results.boxes.data.cpu().numpy()
213
-
214
- for i, box in enumerate(boxes):
215
- x1, y1, x2, y2, conf, cls_id = box
216
- cls_name = CLASS_NAMES.get(int(cls_id), "Unknown")
217
-
218
- if conf > max_conf:
219
- max_conf = conf
220
- primary_diag = cls_name
221
- crop_img = image[int(y1):int(y2), int(x1):int(x2)]
222
-
223
- color = (0, 165, 255)
224
- if "High grade" in cls_name: color = (255, 0, 0)
225
- if "Normal" in cls_name: color = (0, 255, 0)
226
-
227
- cv2.rectangle(orig, (int(x1), int(y1)), (int(x2), int(y2)), color, 3)
228
- label = f"{cls_name} {conf*100:.1f}%"
229
- cv2.putText(orig, label, (int(x1), int(y1)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
230
-
231
- info_log += f"Found #{i+1}: {cls_name} ({conf*100:.1f}%)\n"
232
-
233
- if results.masks:
234
- mask_combined = np.zeros(image.shape[:2], dtype=np.float32)
235
- for m_raw in results.masks.data.cpu().numpy():
236
- m_resized = cv2.resize(m_raw, (image.shape[1], image.shape[0]))
237
- mask_combined = np.maximum(mask_combined, m_resized)
238
-
239
- mask_bool = mask_combined > 0.5
240
- mask_uint8 = (mask_bool * 255).astype(np.uint8)
241
-
242
- colored_mask = np.zeros_like(seg_overlay)
243
- colored_mask[mask_bool] = (0, 255, 0)
244
- seg_overlay = cv2.addWeighted(seg_overlay, 1.0, colored_mask, 0.4, 0)
245
-
246
- contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
247
- cv2.drawContours(seg_overlay, contours, -1, (255, 255, 255), 2)
248
-
249
- dist_map = cv2.distanceTransform(mask_uint8, cv2.DIST_L2, 5)
250
- y_idx, x_idx = np.where(mask_bool)
251
- if len(x_idx) > 0:
252
- step = max(1, len(x_idx) // 1000)
253
- fig.add_trace(go.Scatter3d(
254
- x=x_idx[::step], y=image.shape[0]-y_idx[::step], z=dist_map[y_idx, x_idx][::step],
255
- mode='markers', marker=dict(size=2, color=dist_map[y_idx, x_idx][::step], colorscale='Hot', opacity=0.8)
256
- ))
257
- else:
258
- info_log = "ไม่พบความผิดปกติในภาพนี้ (No Lesion Detected)"
259
- crop_img = image
260
-
261
- fig.update_layout(scene=dict(xaxis_title='Width', yaxis_title='Height', zaxis_title='Density'), margin=dict(l=0,r=0,b=0,t=0), height=300)
262
-
263
- score_percent = int(max_conf * 100)
264
- led_html = generate_led_html(score_percent, primary_diag)
265
- audio_path = text_to_speech(f"วิเคราะห์เสร็จสิ้น ตรวจพบ {primary_diag} ความมั่นใจ {score_percent} เปอร์เซ็นต์")
266
-
267
- return orig, seg_overlay, crop_img, fig, info_log, led_html, audio_path, crop_img, primary_diag, score_percent, history_list, history_list, image, orig, seg_overlay
268
 
269
  # ============================================
270
- # 4) Gradio UI
271
- # ============================================
272
- css = """
273
- @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700;900&display=swap');
274
- .logo-container { display: flex; justify-content: flex-end; align-items: center; gap: 20px; }
275
- #intro-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #000; z-index: 99999; display: flex; flex-direction: column; justify-content: center; align-items: center; animation: fadeOutOverlay 1s ease-in-out 4.5s forwards; pointer-events: none; }
276
- .intro-content { display: flex; gap: 40px; align-items: center; animation: zoomInLogos 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
277
- .intro-logo { height: 120px; width: auto; filter: drop-shadow(0 0 10px rgba(255,255,255,0.3)); }
278
- .intro-text-container { margin-top: 40px; text-align: center; opacity: 0; animation: textSlideUp 1.5s ease-out 1.2s forwards; }
279
- .intro-title { color: #ffffff; font-family: 'Montserrat', sans-serif; font-size: 2.5rem; font-weight: 900; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 20px rgba(255, 255, 255, 0.6); line-height: 1.2; margin-bottom: 10px; }
280
- .intro-subtitle { color: #b3b3b3; font-family: 'Montserrat', sans-serif; font-size: 1.2rem; font-weight: 400; letter-spacing: 4px; }
281
- @keyframes zoomInLogos { 0% { transform: scale(0.8); opacity: 0; } 50% { transform: scale(1.05); opacity: 1; } 100% { transform: scale(1.0); opacity: 1; } }
282
- @keyframes textSlideUp { 0% { transform: translateY(30px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }
283
- @keyframes fadeOutOverlay { to { opacity: 0; visibility: hidden; z-index: -1; } }
284
-
285
- /* Floating Chatbot CSS */
286
- #floating_container { position: fixed; bottom: 25px; left: 25px; z-index: 9999; display: flex; flex-direction: column; align-items: flex-start; }
287
- #chat_window { width: 380px; height: 550px; background: white; border-radius: 20px; box-shadow: 0 15px 50px rgba(0,0,0,0.25); margin-bottom: 15px; display: none; flex-direction: column; border: 1px solid #eee; overflow: hidden; }
288
- .show-chat #chat_window { display: flex !important; }
289
- #chat_btn { width: 80px; height: 80px; background: white; border-radius: 50%; cursor: pointer; display: flex; justify-content: center; align-items: center; box-shadow: 0 8px 30px rgba(0,0,0,0.2); transition: 0.3s; border: 2px solid #0072ff; }
290
- #chat_btn:hover { transform: scale(1.1); }
291
- #chat_btn img { width: 65px; height: 65px; object-fit: contain; border-radius: 50%; }
292
- """
293
-
294
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Ovarian Tumor AI") as demo:
295
- # --- Intro Overlay ---
296
- gr.HTML(f"""<div id="intro-overlay"><audio autoplay><source src="{INTRO_SOUND_URL}" type="audio/mpeg"></audio><div class="intro-content"><img src="{LOGO_KMUTNB_URL}" class="intro-logo"><img src="{LOGO_RAMA_URL}" class="intro-logo"></div><div class="intro-text-container"><div class="intro-title">Deep Learning for<br>Ovarian Tumor Detection</div><div class="intro-title" style="font-size: 1.8rem; color: #E50914;">in Ultrasound Images</div><div class="intro-subtitle">AI MEDICAL DIAGNOSIS SYSTEM</div></div></div>""")
297
-
298
- # --- Header ---
299
- with gr.Row(variant="panel"):
300
- with gr.Column(scale=3):
301
- gr.Markdown("# 🏥 Ovarian Tumor Diagnosis System")
302
- gr.Markdown("AI System for Ovarian Tumor Detection & Diagnosis")
303
- gr.Markdown("จัดทำโดย นายภานรินทร์ เปียกบุตร & นางสาวภาพิมล ไพจิตโรจนา")
304
- with gr.Column(scale=2):
305
- with gr.Row(elem_classes="logo-container"):
306
- gr.Image(LOGO_KMUTNB_URL, show_label=False, container=False, height=65)
307
- gr.Image(LOGO_RAMA_URL, show_label=False, container=False, height=65)
308
-
309
- # State Variables
310
- state_crop = gr.State(None)
311
- state_info = gr.State("")
312
- state_diag = gr.State("")
313
- state_conf = gr.State(0)
314
- state_gallery = gr.State([])
315
- state_img_orig = gr.State(None)
316
- state_img_det = gr.State(None)
317
- state_img_seg = gr.State(None)
318
- state_fig = gr.State(None)
319
-
320
- # --- Main UI ---
321
- with gr.Tabs():
322
- with gr.Tab("1. Detection Analysis"):
323
- with gr.Row():
324
- with gr.Column(scale=2):
325
- img_in = gr.Image(label="Upload Ultrasound Image", type="numpy", height=400)
326
- btn_analyze = gr.Button("🔍 Analyze Image", variant="primary")
327
- with gr.Column(scale=1):
328
- html_led = gr.HTML()
329
- aud = gr.Audio(label="Voice Assistant", autoplay=True)
330
- txt_log = gr.Textbox(label="Detailed Findings", lines=8)
331
-
332
- with gr.Row():
333
- img_det = gr.Image(label="AI Detection", interactive=False)
334
- img_seg = gr.Image(label="Segmentation", interactive=False)
335
- img_crop = gr.Image(label="Focused Lesion", interactive=False)
336
-
337
- with gr.Tab("2. Medical Report"):
338
- with gr.Row():
339
- inp_pt_name = gr.Textbox(label="Patient Name")
340
- inp_pt_id = gr.Textbox(label="Patient ID (HN)")
341
- btn_pdf = gr.Button("🖨️ Generate PDF Report", variant="primary")
342
- out_pdf = gr.File(label="Download Report")
343
-
344
  with gr.Tab("3. Gallery History"):
345
  gallery_ui = gr.Gallery(columns=4, height=600)
346
 
347
- # --- [FIXED] Floating Chatbot ---
348
  with gr.Column(elem_id="floating_container"):
349
  with gr.Column(elem_id="chat_window"):
350
  gr.HTML(f"<div style='background:linear-gradient(90deg, #0072ff, #00c6ff); color:white; padding:15px; border-radius:15px 15px 0 0;'><b>💬 ปรึกษาน้องดูแล</b></div>")
351
 
352
- # [FIX] เพิ่ม type="messages" เพื่อบอก Gradio ว่าใช้ format ใหม่
353
- chatbot = gr.Chatbot(height=400, show_label=False, avatar_images=(None, LOGO_RAMA_URL), type="messages")
354
 
355
  msg = gr.Textbox(placeholder="พิมพ์คำถามที่นี่...", show_label=False)
356
  btn_send = gr.Button("ส่งข้อความ", variant="primary")
357
 
358
- gr.HTML(f"""
359
- <div id="chat_btn" onclick="document.getElementById('floating_container').classList.toggle('show-chat')">
360
- <img src="{LOGO_RAMA_URL}" />
361
- </div>
362
- """)
363
-
364
- # --- Interactions ---
365
- btn_analyze.click(
366
- analyze_image,
367
- [img_in, state_gallery],
368
- [img_det, img_seg, img_crop, state_fig, txt_log, html_led, aud, state_crop, state_diag, state_conf, gallery_ui, state_gallery, state_img_orig, state_img_det, state_img_seg]
369
- )
370
-
371
- def pdf_wrapper(name, pid, diag, conf):
372
- if not diag: return None
373
- return create_medical_report(name, pid, diag, conf)
374
-
375
  btn_pdf.click(pdf_wrapper, [inp_pt_name, inp_pt_id, state_diag, state_conf], out_pdf)
376
 
377
- # [FIX] Chat interaction: เพิ่ม outputs ตัวที่ 2 (msg) เพื่อล้างข้อความ
378
  btn_send.click(chat_fn, [msg, chatbot, state_crop, state_info, state_diag], [chatbot, msg])
379
  msg.submit(chat_fn, [msg, chatbot, state_crop, state_info, state_diag], [chatbot, msg])
380
-
381
- if __name__ == "__main__":
382
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # 1) Configuration & Setup
2
  # ============================================
3
 
4
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
5
+
6
  if GOOGLE_API_KEY:
7
  genai.configure(api_key=GOOGLE_API_KEY)
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  if not os.path.exists(filename):
10
  print(f"📥 Downloading {filename}...")
11
  try:
12
+ session = requests.Session()
13
+ session.headers.update({'User-Agent': 'Mozilla/5.0'})
14
+ r = session.get(url, allow_redirects=True)
15
+ if r.status_code == 200 and len(r.content) > 1000:
16
+ with open(filename, 'wb') as f:
17
+ f.write(r.content)
18
+ else:
19
+ print(f"❌ Failed to download {filename} (Status: {r.status_code})")
20
+ return False
21
  except Exception as e:
22
  print(f"❌ Error downloading {filename}: {e}")
23
  return False
24
  return True
25
 
26
  font_urls = [
27
+ ("https://raw.githubusercontent.com/nutjunkie/thaifonts_sipa/master/sipa_fonts/THSarabunNew/THSarabunNew.ttf", "THSarabunNew.ttf"),
28
+ ("https://raw.githubusercontent.com/nutjunkie/thaifonts_sipa/master/sipa_fonts/THSarabunNew/THSarabunNew%20Bold.ttf", "THSarabunNew-Bold.ttf")
29
  ]
30
 
31
  for url, fname in font_urls:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  try:
33
  filename = tempfile.mktemp(suffix=".pdf")
34
  c = canvas.Canvas(filename, pagesize=A4)
35
+
36
+
37
  font_name = 'THSarabun-Bold' if 'THSarabun-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'
38
 
39
  c.setFont(font_name, 24)
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return None
41
 
42
  # ============================================
43
+ # [FIXED] Chat Function (Dictionary Format)
44
  # ============================================
45
  def chat_fn(message, history, crop_img, info_text, diagnosis):
46
+
47
  if history is None: history = []
48
 
49
+ # 1. API Key Check
50
  if not GOOGLE_API_KEY:
51
+ response = "❌ ไม่พบ API KEY: กรุณาตรวจสอบการตั้งค่า GOOGLE_API_KEY ในไฟล์ app.py"
52
+
53
  history.append({"role": "user", "content": message})
54
  history.append({"role": "assistant", "content": response})
55
  return history, ""
56
 
57
  try:
58
+ # 2. Context Prompt
59
  context_prompt = f"""
60
  บทบาท: คุณคือผู้ช่วยทางการแพทย์อัจฉริยะ (AI Medical Assistant)
61
 
 
 
 
 
 
 
 
 
 
62
  - *สำคัญ*: ต้องลงท้ายเสมอว่า "ควรปรึกษาแพทย์ผู้เชี่ยวชาญเพื่อการวินิจฉัยที่แม่นยำที่สุด"
63
  """
64
 
65
+ # 3. Call Gemini
66
+ model = genai.GenerativeModel('gemini-2.5-flash')
67
  response = model.generate_content(context_prompt)
68
  bot_reply = response.text
69
 
 
70
  bot_reply = f"เกิดข้อผิดพลาด (System Error): {str(e)}"
71
  print(f"DEBUG ERROR: {e}")
72
 
73
+ # Append Dictionary format
74
  history.append({"role": "user", "content": message})
75
  history.append({"role": "assistant", "content": bot_reply})
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
+ return history, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  # ============================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  with gr.Tab("3. Gallery History"):
82
  gallery_ui = gr.Gallery(columns=4, height=600)
83
 
84
+ # --- Floating Chatbot ---
85
  with gr.Column(elem_id="floating_container"):
86
  with gr.Column(elem_id="chat_window"):
87
  gr.HTML(f"<div style='background:linear-gradient(90deg, #0072ff, #00c6ff); color:white; padding:15px; border-radius:15px 15px 0 0;'><b>💬 ปรึกษาน้องดูแล</b></div>")
88
 
89
+ # Chatbot: No type param needed here, but data passed will be dicts
90
+ chatbot = gr.Chatbot(height=400, show_label=False, avatar_images=(None, LOGO_RAMA_URL))
91
 
92
  msg = gr.Textbox(placeholder="พิมพ์คำถามที่นี่...", show_label=False)
93
  btn_send = gr.Button("ส่งข้อความ", variant="primary")
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  btn_pdf.click(pdf_wrapper, [inp_pt_name, inp_pt_id, state_diag, state_conf], out_pdf)
96
 
97
+ # Chat interactions
98
  btn_send.click(chat_fn, [msg, chatbot, state_crop, state_info, state_diag], [chatbot, msg])
99
  msg.submit(chat_fn, [msg, chatbot, state_crop, state_info, state_diag], [chatbot, msg])