emmafar commited on
Commit
35ee1cf
·
verified ·
1 Parent(s): 28f6709

TMA-1 4B public demo: ZeroGPU chat + mini-RAG (TMT + fact sheets) + EN disclaimer

Browse files
Files changed (5) hide show
  1. README.md +25 -7
  2. app.py +210 -0
  3. requirements.txt +6 -0
  4. tma_fact_sheets_v1.json +0 -0
  5. tmt_lookup.json +0 -0
README.md CHANGED
@@ -1,13 +1,31 @@
1
  ---
2
- title: Tma1 Medgemma 4b Demo
3
- emoji: 📊
4
- colorFrom: red
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TMA medgemma-4b demo
3
+ emoji: 🩺
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.9.1
 
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Thai Medical Assistant demo — DRAFT, verify before use
11
  ---
12
 
13
+ # TMA · tma1-medgemma-4b public demo (ZeroGPU)
14
+
15
+ Public demo of [`icdevelop/tma1-medgemma-4b`](https://huggingface.co/icdevelop/tma1-medgemma-4b) —
16
+ a consumer-facing **Thai Medical Assistant** (general health education, OTC drugs, supplements,
17
+ home medical devices). Multimodal: text + optional image (drug package / label / prescription).
18
+
19
+ > ⚠️ **DRAFT — not pharmacist-certified.** The model's knowledge base has **not** been formally
20
+ > reviewed or certified by licensed pharmacists/physicians. Answers may be inaccurate or outdated —
21
+ > **verify any information with authoritative sources or licensed professionals before real-world
22
+ > use.** Not a medical device; it does not diagnose. Emergency (Thailand): **1669** ·
23
+ > Mental-health hotline: **1323**.
24
+
25
+ Grounding in this demo is a bundled KB subset (TMT trade-name lookup + TMA fact sheets for
26
+ supplements/devices, knowledge cutoff Jul 2026); production deployments use the full retrieval KB.
27
+
28
+ ## Stateless API
29
+
30
+ `POST /gradio_api/call/chat` with `data=[message, history_json, image_b64]` — the caller owns the
31
+ conversation history (pass `"[]"` to start a new chat).
app.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ZeroGPU demo Space for TMA-1 — tma1-medgemma-4b (Thai Medical Assistant, multimodal, multi-turn).
2
+
3
+ Public demo on HuggingFace ZeroGPU (H200 slice). Cloned from PAI's space_demo pattern.
4
+
5
+ Two surfaces:
6
+ • Browser UI — Chatbot with in-page memory.
7
+ • Stateless API `/chat` — (message, history_json, image_b64) → reply. The caller owns history.
8
+
9
+ Grounding (mini-RAG bundled with the Space): TMT trade-name lookup + TMA fact sheets
10
+ (supplements/vitamins + home medical devices) injected as reference facts; strict
11
+ anti-hallucination system prompt. Production uses the full KB — this is a demo subset.
12
+ """
13
+ import json
14
+ import os
15
+ import re
16
+
17
+ import gradio as gr
18
+ import spaces
19
+ import torch
20
+ from transformers import AutoModelForImageTextToText, AutoProcessor
21
+
22
+ MODEL_ID = "icdevelop/tma1-medgemma-4b"
23
+ TOKEN = os.environ.get("HF_TOKEN")
24
+
25
+ SYSTEM = (
26
+ "คุณคือ TMA ผู้ช่วยสุขภาพ AI สำหรับประชาชนในประเทศไทย ให้ความรู้เรื่องสุขภาพทั่วไป ยา วิตามิน อาหารเสริม "
27
+ "และอุปกรณ์การแพทย์ที่ใช้ในบ้าน ตอบเป็นภาษาไทยที่สุภาพ เข้าใจง่าย ถูกต้องตามหลักการแพทย์ "
28
+ "จำบทสนทนาก่อนหน้าและตอบต่อเนื่องอย่างเป็นธรรมชาติ (อย่าทักทายหรือถามข้อมูลซ้ำถ้าผู้ใช้ตอบไปแล้ว)\n"
29
+ "ขอบเขต (สำคัญมาก):\n"
30
+ "- คุณ 'ไม่ใช่' แพทย์และไม่วินิจฉัยโรค ไม่อ่านภาพเอกซเรย์ ผลแล็บ ฟิล์ม MRI/CT หรือภาพบาดแผล/ผิวหนังเพื่อวินิจฉัย "
31
+ "ถ้าผู้ใช้ขอสิ่งเหล่านี้ให้ตอบว่า 'ขออภัยค่ะ TMA ให้ข้อมูลสุขภาพทั่วไปเท่านั้น ไม่สามารถอ่านภาพทางการแพทย์หรือวินิจฉัยโรคได้ "
32
+ "กรุณาปรึกษาแพทย์ค่ะ' และห้ามเดาว่าภาพนั้นเป็นยา\n"
33
+ "- ภาพที่รับได้คือ 'ซองยา/ฉลากยา/ฉลากอาหารเสริม/ใบสั่งยา' เพื่อบอกว่าเป็นผลิตภัณฑ์อะไรเท่านั้น\n"
34
+ "- ไม่สั่งหรือปรับขนาดยาที่ต้องมีใบสั่งแพทย์ และห้ามแนะนำให้หยุดยาที่แพทย์สั่งเอง\n"
35
+ "กฎเหล็กด้านความปลอดภัย:\n"
36
+ "1. ห้ามเดา/แต่งข้อมูลผลิตภัณฑ์โดยเด็ดขาด (ชื่อการค้า ตัวยา ความแรง รูปแบบ สรรพคุณ)\n"
37
+ "2. ใช้ข้อเท็จจริงของผลิตภัณฑ์เฉพาะจาก '[ข้อมูลอ้างอิง]' ที่แนบมาเท่านั้น "
38
+ "ถ้าไม่พบให้บอกว่า 'ไม่พบข้อมูลในคลังอ้างอิงของ demo นี้ กรุณาสอบถามเภสัชกร' ห้ามคาดเดา\n"
39
+ "3. อาหารเสริม/วิตามินไม่ใช่ยารักษาโรค — ห้ามกล่าวอ้างสรรพคุณเกินจริง และแก้ความเข้าใจเมื่อผู้ใช้ได้ข้อมูลเกินจริงมา\n"
40
+ "4. ซักถามข้อมูลที่จำเป็น (อาการ ระยะเวลา โรคประจำตัว ยาที่ใช้ ตั้งครรภ์/ให้นม) ก่อนแนะนำ "
41
+ "และแนะนำให้แจ้งข้อมูลเหล่านี้กับเภสัชกรที่ร้านด้วย\n"
42
+ "5. พบสัญญาณอันตรายให้แนะนำไปโรงพยาบาลหรือโทร 1669 ทันที · ปัญหาสุขภาพจิตรุนแรงแนะนำสายด่วน 1323"
43
+ )
44
+
45
+ _D = os.path.dirname(__file__)
46
+ with open(os.path.join(_D, "tmt_lookup.json"), encoding="utf-8") as f:
47
+ TMT = json.load(f)
48
+ TMT_KEYS = sorted(TMT.keys(), key=len, reverse=True)
49
+
50
+ with open(os.path.join(_D, "tma_fact_sheets_v1.json"), encoding="utf-8") as f:
51
+ SHEETS = json.load(f)
52
+
53
+
54
+ def _t(v) -> str:
55
+ if isinstance(v, list):
56
+ return " • ".join(_t(x) for x in v if _t(x))
57
+ if isinstance(v, dict):
58
+ return " • ".join(f"{k}: {_t(x)}" if not str(k).isdigit() else _t(x) for k, x in v.items() if _t(x))
59
+ return str(v or "").strip()
60
+
61
+
62
+ def _norm(s: str) -> str:
63
+ return re.sub(r"[^0-9A-Za-zก-๙]+", "", (s or "").upper())
64
+
65
+
66
+ # fact-sheet name index: full name + the English part + significant Thai tokens
67
+ SHEET_KEYS = []
68
+ for _s in SHEETS:
69
+ name = _s["name"]
70
+ keys = {_norm(name)}
71
+ m = re.search(r"\(([^)]+)\)", name)
72
+ if m:
73
+ keys.add(_norm(m.group(1)))
74
+ keys.add(_norm(re.sub(r"\([^)]*\)", "", name)))
75
+ for k in keys:
76
+ if len(k) >= 4:
77
+ SHEET_KEYS.append((k, _s))
78
+ SHEET_KEYS.sort(key=lambda x: len(x[0]), reverse=True)
79
+
80
+
81
+ def sheet_line(s: dict) -> str:
82
+ if s["kind"] == "supplement":
83
+ return (f"- {s['name']} (อาหารเสริม — ไม่ใช่ยารักษาโรค): {_t(s.get('benefits'))[:220]} | "
84
+ f"ขนาดทั่วไป: {_t(s.get('typical_dose'))[:120]} | ระวัง: {_t(s.get('cautions'))[:150]}")
85
+ return (f"- {s['name']} (อุปกรณ์ใช้ในบ้าน): {_t(s.get('what_for'))[:180]} | "
86
+ f"การแปลผล: {_t(s.get('reading_interpretation'))[:200]} | พบแพทย์เมื่อ: {_t(s.get('when_to_see_doctor'))[:150]}")
87
+
88
+
89
+ def retrieve(text: str, limit: int = 5) -> str:
90
+ q = _norm(text)
91
+ if not q:
92
+ return ""
93
+ hits, used = [], []
94
+ for k in TMT_KEYS:
95
+ if len(hits) >= limit:
96
+ break
97
+ if k in q and not any(k in u for u in used):
98
+ used.append(k)
99
+ d = TMT[k]
100
+ line = f"- {d['name']}: ตัวยา {d['active']}"
101
+ if d.get("strength"):
102
+ line += f" | ความแรง {d['strength']}"
103
+ if d.get("form"):
104
+ line += f" | รูปแบบ {d['form']}"
105
+ if d.get("legal"):
106
+ line += f" | ประเภท {d['legal']}"
107
+ hits.append(line)
108
+ seen = set()
109
+ for k, s in SHEET_KEYS:
110
+ if len(hits) >= limit + 3:
111
+ break
112
+ if k in q and s["name"] not in seen:
113
+ seen.add(s["name"])
114
+ hits.append(sheet_line(s))
115
+ return "[ข้อมูลอ้างอิง — ทะเบียนยา TMT และ fact sheets]\n" + "\n".join(hits) if hits else ""
116
+
117
+
118
+ processor = AutoProcessor.from_pretrained(MODEL_ID, token=TOKEN)
119
+ model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, token=TOKEN)
120
+
121
+
122
+ @spaces.GPU(duration=120)
123
+ def _gen(messages):
124
+ model.to("cuda")
125
+ inputs = processor.apply_chat_template(
126
+ messages, add_generation_prompt=True, tokenize=True,
127
+ return_dict=True, return_tensors="pt",
128
+ ).to("cuda", dtype=torch.bfloat16)
129
+ with torch.inference_mode():
130
+ out = model.generate(**inputs, max_new_tokens=512, do_sample=True, temperature=0.7)
131
+ return processor.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True).strip()
132
+
133
+
134
+ def _decode_image(image_b64: str):
135
+ if not image_b64 or not str(image_b64).strip():
136
+ return None
137
+ try:
138
+ import base64
139
+ import io
140
+
141
+ from PIL import Image
142
+ s = str(image_b64).strip()
143
+ if s.startswith("data:"):
144
+ s = s.split(",", 1)[1]
145
+ return Image.open(io.BytesIO(base64.b64decode(s))).convert("RGB")
146
+ except Exception:
147
+ return None
148
+
149
+
150
+ def _reply(message: str, history: list, image=None) -> str:
151
+ message = str(message or "").strip()
152
+ if not message and image is None:
153
+ return ""
154
+ msgs = [{"role": "system", "content": [{"type": "text", "text": SYSTEM}]}]
155
+ for h in history or []:
156
+ role, c = h.get("role"), h.get("content")
157
+ if role in ("user", "assistant") and isinstance(c, str) and c.strip():
158
+ msgs.append({"role": role, "content": [{"type": "text", "text": c}]})
159
+ convo = " ".join([message] + [h.get("content", "") for h in (history or []) if isinstance(h.get("content"), str)])
160
+ ref = retrieve(convo)
161
+ user_text = (ref + "\n\n[คำถาม] " + message) if ref else (message or "ผลิตภัณฑ์นี้คืออะไร")
162
+ content = []
163
+ if image is not None:
164
+ content.append({"type": "image", "image": image})
165
+ content.append({"type": "text", "text": user_text})
166
+ msgs.append({"role": "user", "content": content})
167
+ return _gen(msgs)
168
+
169
+
170
+ def api_chat(message: str, history_json: str = "[]", image_b64: str = "") -> str:
171
+ try:
172
+ history = json.loads(history_json) if isinstance(history_json, str) and history_json.strip() else (history_json or [])
173
+ except Exception:
174
+ history = []
175
+ return _reply(message, history, _decode_image(image_b64))
176
+
177
+
178
+ DISCLAIMER = (
179
+ "⚠️ **DRAFT — not pharmacist-certified.** TMA-1 is an AI health assistant under evaluation. "
180
+ "Its knowledge base has **not** been formally reviewed or certified by licensed pharmacists/physicians, "
181
+ "and answers may be inaccurate or outdated. **Please verify any information with authoritative sources "
182
+ "or licensed professionals before real-world use.** Not a medical device — no diagnosis. "
183
+ "Emergency (Thailand): **1669** · Mental-health hotline: **1323** · Demo uses a KB subset (knowledge cutoff Jul 2026)."
184
+ )
185
+
186
+ with gr.Blocks(title="TMA · tma1-medgemma-4b demo") as demo:
187
+ gr.Markdown(f"# TMA · tma1-medgemma-4b — Thai Medical Assistant\n{DISCLAIMER}")
188
+ chatbot = gr.Chatbot(type="messages", height=460, label="TMA")
189
+ msg = gr.Textbox(label="พิมพ์คำถาม", placeholder="เช่น วิตามินซีกับซิงค์กินด้วยกันได้มั้ย / เครื่องวัดความดันวัดได้ 150/95 แปลว่าอะไร")
190
+ with gr.Row():
191
+ clear = gr.Button("🗑️ เริ่มแชทใหม่")
192
+
193
+ def ui_submit(user_msg, chat_history):
194
+ chat_history = chat_history or []
195
+ reply = _reply(user_msg, chat_history)
196
+ chat_history = chat_history + [{"role": "user", "content": user_msg},
197
+ {"role": "assistant", "content": reply}]
198
+ return "", chat_history
199
+
200
+ msg.submit(ui_submit, [msg, chatbot], [msg, chatbot])
201
+ clear.click(lambda: (None, []), None, [msg, chatbot])
202
+
203
+ api_msg = gr.Textbox(visible=False)
204
+ api_hist = gr.Textbox(visible=False, value="[]")
205
+ api_img = gr.Textbox(visible=False, value="")
206
+ api_out = gr.Textbox(visible=False)
207
+ api_msg.submit(api_chat, [api_msg, api_hist, api_img], api_out, api_name="chat")
208
+
209
+ if __name__ == "__main__":
210
+ demo.queue(max_size=20).launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spaces
2
+ torch
3
+ transformers>=4.53.0
4
+ accelerate
5
+ pillow
6
+ gradio>=5.0.0
tma_fact_sheets_v1.json ADDED
The diff for this file is too large to render. See raw diff
 
tmt_lookup.json ADDED
The diff for this file is too large to render. See raw diff