Files changed (1) hide show
  1. app.py +222 -96
app.py CHANGED
@@ -1,98 +1,224 @@
1
- import gradio as gr
2
- from transformers import AutoImageProcessor, AutoModelForImageSegmentation
3
- from PIL import Image
4
- import torch
5
- import matplotlib.pyplot as plt
6
- import numpy as np
7
-
8
- # 1️⃣ โหลดโมเดล pre-trained จาก Hugging Face
9
- model_name = "MLforHealthcare/sam2rad" # MedSAM2
10
- processor = AutoImageProcessor.from_pretrained(model_name)
11
- model = AutoModelForImageSegmentation.from_pretrained(model_name)
12
-
13
- # 2️⃣ dictionary คำอธิบายอวัยวะ 50 ชิ้น
14
- organ_desc = {
15
- 0: "สมอง 🧠: ควบคุมการทำงานของร่างกาย",
16
- 1: "หัวใจ ❤️: สูบฉีดเลือด",
17
- 2: "ปอด 💨: แลกเปลี่ยนก๊าซ",
18
- 3: "ตับ 🍵: กำจัดสารพิษ",
19
- 4: "ไต 💧: กรองของเสียในเลือด",
20
- 5: "กระเพาะอาหาร 🥘: ย่อยอาหาร",
21
- 6: "ลำไส้เล็ก 🌾: ดูดซึมอาหาร",
22
- 7: "ลำไส้ใหญ่ 🌽: ดูดซึมและขับถ่าย",
23
- 8: "ตับอ่อน 🍭: สร้างอินซูลิน",
24
- 9: "กระเพาะปัสสาวะ 💦: เก็บปัสสาวะ",
25
- 10: "ม้าม 🔴: กรองเลือดและระบบภูมิคุ้มกัน",
26
- 11: "หลอดเลือดแดงใหญ่ 🔴: นำเลือดจากหัวใจไปยังร่างกาย",
27
- 12: "หลอดเลือดดำใหญ่ 🔵: นำเลือดกลับสู่หัวใจ",
28
- 13: "กระดูกสันหลัง 🦴: รองรับร่างกายและปกป้องไขสันหลัง",
29
- 14: "กล้ามเนื้อแขน 💪: ขยับแขน",
30
- 15: "กล้ามเนื้อขา 🦵: ขยับขาและเดิน",
31
- 16: "ผิวหนัง 🩸: ปกป้องร่างกาย",
32
- 17: "ลูกตา 👁️: รับภาพ",
33
- 18: "หู 👂: ได้ยินและรักษาสมดุล",
34
- 19: "จมูก 👃: ดมกลิ่น",
35
- 20: "ลิ้น 👅: ชิมอาหาร",
36
- 21: "ฟัน 🦷: เคี้ยวอาหาร",
37
- 22: "หูชั้นกลาง 🔊: นำเสียงเข้าโสตประสาท",
38
- 23: "กล่องเสียง 🗣️: สร้างเสียงพูด",
39
- 24: "หลอดลม 🌬️: นำอากาศเข้าสู่ปอด",
40
- 25: "หลอดอาหาร 🍴: นำอาหารสู่กระเพาะ",
41
- 26: "ไส้เลื่อน/เนื้อเยื่อใต้ผิว 🔹: ปกป้องอวัยวะภายใน",
42
- 27: "ต่อมไทรอยด์ 🦋: ควบคุมเมตาบอลิซึม",
43
- 28: "ต่อมหมวกไต 🏔️: ผลิตฮอร์โมน",
44
- 29: "อัณฑะ/รังไข่ 🔵: สร้างเซลล์สืบพันธุ์",
45
- 30: "มดลูก/อวัยวะสืบพันธุ์หญิง 🌸: ตั้งครรภ์",
46
- 31: "หลอดน้ำเหลือง 💛: ระบบภูมิคุ้มกัน",
47
- 32: "ต่อมน้ำเหลือง 💚: กรองเชื้อโรค",
48
- 33: "กระดูกเชิงกราน 🦴: รองรับอวัยวะภายใน",
49
- 34: "หัวเข่า 🦵: ขยับขา",
50
- 35: "ข้อศอก 💪: ขยับแขน",
51
- 36: "ไส้ติ่ง 🔺: อวัยวะเสริมย่อยอาหาร",
52
- 37: "เนื้อเยื่อไขมัน 🟡: เก็บพลังงาน",
53
- 38: "กล้ามเนื้อหน้าอก 💪: ช่วยหายใจและขยับแขน",
54
- 39: "กระดูกหน้าอก 🦴: ปกป้องหัวใจและปอด",
55
- 40: "ขากรรไกร 👄: เคี้ยวอาหาร",
56
- 41: "หลอดเลือดฝอย 🔴🔵: แลกเปลี่ยนสารอาหารและออกซิเจน",
57
- 42: "กระดูกสันคอ 🦴: ปกป้องไขสันหลังส่วนคอ",
58
- 43: "เส้นประสาท 🧬: ส่งสัญญาณร่างกาย",
59
- 44: "เส้นเอ็น/เอ็นกล้ามเนื้อ 🔗: เชื่อมกล้ามเนื้อกับกระดูก",
60
- 45: "กล้ามเนื้อหน้าท้อง 💪: รองรับอวัยวะภายใน",
61
- 46: "ผนังช่องท้อง 🩻: ปกป้องอวัยวะในช่องท้อง",
62
- 47: "ต่อมน้ำลาย 🟤: ผลิตน้ำลาย",
63
- 48: "ต่อมน้ำนม 🍼: สร้างน้ำนม (หญิง)",
64
- 49: "สมองน้อย 🧠: ควบคุมการทรงตัวและการเคลื่อนไหว"
65
- }
66
-
67
- # 3️⃣ ฟังก์ชันตรวจจับอวัยวะ
68
- def detect_organs(img):
69
- inputs = processor(images=img, return_tensors="pt")
70
- with torch.no_grad():
71
- outputs = model(**inputs)
72
- seg_map = outputs.logits.argmax(dim=1).squeeze().cpu().numpy()
73
-
74
- # วาด overlay
75
- plt.figure(figsize=(6,6))
76
- plt.imshow(img)
77
- plt.imshow(seg_map, alpha=0.5, cmap='jet')
78
- plt.axis('off')
79
- plt.tight_layout()
80
- plt.savefig("segmented.png", bbox_inches='tight', pad_inches=0)
81
- segmented_img = Image.open("segmented.png")
82
-
83
- # ดึงคำอธิบายอวัยวะที่พบ
84
- detected_organs = np.unique(seg_map)
85
- descriptions = [organ_desc.get(int(o), f"อวัยวะ {int(o)}") for o in detected_organs]
86
-
87
- return segmented_img, "\n".join(descriptions)
88
-
89
- # 4️⃣ สร้าง Gradio Interface
90
- iface = gr.Interface(
91
- fn=detect_organs,
92
- inputs=gr.Image(type="pil"),
93
- outputs=[gr.Image(type="pil"), gr.Textbox()],
94
- title="ตรวจจับอวัยวะ 50 ชิ้นด้วย MedSAM2",
95
- description="อัปโหลดภาพ AI ตรวจจับอวัยวะ → วาดกรอบ → แสดงคำอธิบาย"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  )
97
 
98
- iface.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import streamlit as st
5
+ from openai import OpenAI
6
+
7
+ from googleapiclient.discovery import build
8
+ from google.oauth2 import service_account
9
+
10
+
11
+ st.set_page_config(
12
+ page_title="회의록 요약 + 캘린더 등록",
13
+ page_icon="📅",
14
+ layout="wide"
15
+ )
16
+
17
+ SCOPES = ["https://www.googleapis.com/auth/calendar"]
18
+
19
+
20
+ def get_openai_client():
21
+ api_key = os.environ.get("OPENAI_API_KEY")
22
+
23
+ if not api_key:
24
+ st.error("OPENAI_API_KEY Secret이 설정되어 있지 않습니다.")
25
+ st.stop()
26
+
27
+ return OpenAI(api_key=api_key)
28
+
29
+
30
+ def get_calendar_service():
31
+ service_account_json = os.environ.get("GOOGLE_SERVICE_ACCOUNT_JSON")
32
+
33
+ if not service_account_json:
34
+ st.error("GOOGLE_SERVICE_ACCOUNT_JSON Secret이 설정되어 있지 않습니다.")
35
+ st.stop()
36
+
37
+ try:
38
+ service_account_info = json.loads(service_account_json)
39
+ except json.JSONDecodeError:
40
+ st.error("GOOGLE_SERVICE_ACCOUNT_JSON 값이 올바른 JSON 형식이 아닙니다.")
41
+ st.stop()
42
+
43
+ creds = service_account.Credentials.from_service_account_info(
44
+ service_account_info,
45
+ scopes=SCOPES
46
+ )
47
+
48
+ return build("calendar", "v3", credentials=creds)
49
+
50
+
51
+ def clean_json_text(text):
52
+ text = text.strip()
53
+ text = re.sub(r"^```json", "", text)
54
+ text = re.sub(r"^```", "", text)
55
+ text = re.sub(r"```$", "", text)
56
+ return text.strip()
57
+
58
+
59
+ def summarize_meeting(client, meeting_text):
60
+ prompt = f"""
61
+ 다음 회의록을 간략하게 요약해줘.
62
+
63
+ 너무 길게 쓰지 말고, 아래 형식으로만 정리해.
64
+
65
+ 형식:
66
+ 1. 회의 주제:
67
+ 2. 일시:
68
+ 3. 장소:
69
+ 4. 참석자:
70
+ 5. 주요 논의 내용:
71
+ 6. 주요 일정:
72
+ 7. 담당 업무:
73
+
74
+ 회의록:
75
+ {meeting_text}
76
+ """
77
+
78
+ response = client.responses.create(
79
+ model="gpt-4o-mini",
80
+ input=prompt
81
+ )
82
+
83
+ return response.output_text
84
+
85
+
86
+ def extract_calendar_events(client, meeting_text):
87
+ prompt = f"""
88
+ 다음 회의록에서 Google Calendar에 등록할 일정만 JSON 배열로 추출해줘.
89
+
90
+ 반드시 JSON 배열만 출력해.
91
+ 설명 문장 금지.
92
+ 마크다운 코드블록 금지.
93
+
94
+ 조건:
95
+ - 날짜가 명확한 일정만 추출
96
+ - 시간이 없으면 시작시간은 09:00, 종료시간은 10:00
97
+ - 날짜 형식은 YYYY-MM-DD
98
+ - 시간 형식은 HH:MM
99
+ - 한국 시간 기준
100
+ - 장소가 없으면 빈 문자열
101
+ - 담당자가 없으면 빈 문자열
102
+ - 회의 자체 일정도 날짜와 시간이 있으면 포함
103
+
104
+ 출력 형식:
105
+ [
106
+ {{
107
+ "제목": "",
108
+ "날짜": "YYYY-MM-DD",
109
+ "시작시간": "HH:MM",
110
+ "종료시간": "HH:MM",
111
+ "장소": "",
112
+ "설명": "",
113
+ "담당자": ""
114
+ }}
115
+ ]
116
+
117
+ 회의록:
118
+ {meeting_text}
119
+ """
120
+
121
+ response = client.responses.create(
122
+ model="gpt-4o-mini",
123
+ input=prompt
124
+ )
125
+
126
+ raw = clean_json_text(response.output_text)
127
+
128
+ try:
129
+ return json.loads(raw)
130
+ except json.JSONDecodeError:
131
+ st.error("일정 JSON 변환 실패")
132
+ st.text(raw)
133
+ return []
134
+
135
+
136
+ def add_events_to_calendar(events, calendar_id):
137
+ service = get_calendar_service()
138
+ created_links = []
139
+
140
+ for item in events:
141
+ date = item.get("날짜")
142
+ start_time = item.get("시작시간", "09:00")
143
+ end_time = item.get("종료시간", "10:00")
144
+
145
+ if not date:
146
+ continue
147
+
148
+ event = {
149
+ "summary": item.get("제목", "회의 일정"),
150
+ "location": item.get("장소", ""),
151
+ "description": f"담당자: {item.get('담당자', '')}\n\n{item.get('설명', '')}",
152
+ "start": {
153
+ "dateTime": f"{date}T{start_time}:00",
154
+ "timeZone": "Asia/Seoul",
155
+ },
156
+ "end": {
157
+ "dateTime": f"{date}T{end_time}:00",
158
+ "timeZone": "Asia/Seoul",
159
+ },
160
+ }
161
+
162
+ created_event = service.events().insert(
163
+ calendarId=calendar_id,
164
+ body=event
165
+ ).execute()
166
+
167
+ created_links.append(created_event.get("htmlLink"))
168
+
169
+ return created_links
170
+
171
+
172
+ st.title("회의록 요약 + Google Calendar 일정 등록")
173
+
174
+ st.info("txt 회의록을 업로드하면 내용을 요약하고 날짜가 있는 일정을 Google Calendar에 등록합니다.")
175
+
176
+ calendar_id = st.text_input(
177
+ "Google Calendar ID",
178
+ value="primary",
179
+ help="기본 캘린더는 primary를 그대로 두세요."
180
  )
181
 
182
+ uploaded_file = st.file_uploader("회의록 txt 파일 업로드", type=["txt"])
183
+
184
+ if uploaded_file is not None:
185
+ meeting_text = uploaded_file.read().decode("utf-8")
186
+
187
+ st.subheader("업로드한 회의록")
188
+ st.text_area("회의록 내용", meeting_text, height=250)
189
+
190
+ client = get_openai_client()
191
+
192
+ if st.button("1. 회의록 요약 및 일정 추출"):
193
+ with st.spinner("회의록을 분석하는 중입니다..."):
194
+ summary = summarize_meeting(client, meeting_text)
195
+ events = extract_calendar_events(client, meeting_text)
196
+
197
+ st.session_state["summary"] = summary
198
+ st.session_state["events"] = events
199
+
200
+ if "summary" in st.session_state:
201
+ st.subheader("회의록 요약")
202
+ st.write(st.session_state["summary"])
203
+
204
+ if "events" in st.session_state:
205
+ st.subheader("추출된 일정")
206
+
207
+ events = st.session_state["events"]
208
+
209
+ if len(events) == 0:
210
+ st.warning("등록할 일정이 없습니다.")
211
+ else:
212
+ st.dataframe(events, use_container_width=True)
213
+
214
+ if st.button("2. Google Calendar에 일정 등록"):
215
+ with st.spinner("Google Calendar에 등록하는 중입니다..."):
216
+ links = add_events_to_calendar(events, calendar_id)
217
+
218
+ st.success(f"총 {len(links)}개 일정 등록 완료")
219
+
220
+ for link in links:
221
+ st.write(link)
222
+
223
+ else:
224
+ st.warning("회의록 txt 파일을 업로드하세요.")