cwadayi commited on
Commit
647c085
ยท
verified ยท
1 Parent(s): 33e131c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -159
app.py CHANGED
@@ -1,6 +1,9 @@
1
  # app.py
2
  import os
3
- os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib" # ้ฟๅ…ๅ”ฏ่ฎ€็›ฎ้Œ„่ญฆๅ‘Š
 
 
 
4
 
5
  import uuid
6
  import tempfile
@@ -29,16 +32,14 @@ import matplotlib.cm as cm
29
  from matplotlib import font_manager as fm
30
 
31
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
- # 1) ๅ˜—่ฉฆ่ผ‰ๅ…ฅใ€Œๅœฐ้œ‡้ ่ญฆใ€ๅค–้ƒจๆจก็ต„๏ผ›่‹ฅไธๅญ˜ๅœจๅฐฑ็”จๅ…งๅปบๅ‚™ๆด็‰ˆๆœฌ
33
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
34
  try:
35
- # ่‹ฅไฝ ๆœ‰ๅปบ็ซ‹ fetch_cwa_alarm_list.py๏ผŒๆœƒๅ„ชๅ…ˆไฝฟ็”จ
36
- from fetch_cwa_alarm_list import fetch_cwa_alarm_list # noqa: F401
37
  except Exception:
38
  CWA_ALARM_API = "https://app-2.cwa.gov.tw/api/v1/earthquake/alarm/list"
39
 
40
  def _parse_cwa_time(s: str):
41
- """ๅ›žๅ‚ณ(ๅฐ็ฃๆ™‚้–“, UTC)ใ€‚่‹ฅ็„กๆ™‚ๅ€๏ผŒ่ฆ–็‚บๅฐ็ฃๆ™‚้–“(UTC+8)ใ€‚"""
42
  if not s:
43
  return ("ๆœช็Ÿฅ", "ๆœช็Ÿฅ")
44
  try:
@@ -54,7 +55,6 @@ except Exception:
54
  return (s, "ๆœช็Ÿฅ")
55
 
56
  def fetch_cwa_alarm_list(limit: int = 5) -> str:
57
- """CWA ๅœฐ้œ‡้ ่ญฆ๏ผˆๅ‚™ๆด็‰ˆ๏ผ‰ใ€‚"""
58
  try:
59
  r = requests.get(CWA_ALARM_API, timeout=10)
60
  r.raise_for_status()
@@ -76,9 +76,7 @@ except Exception:
76
  if "T" in s or s.endswith("Z") or "+" in s:
77
  dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
78
  else:
79
- dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(
80
- tzinfo=timezone(timedelta(hours=8))
81
- )
82
  return dt.astimezone(timezone.utc)
83
  except Exception:
84
  return datetime.min.replace(tzinfo=timezone.utc)
@@ -93,7 +91,7 @@ except Exception:
93
  ok = xs.replace(".", "", 1).replace("-", "", 1).isdigit()
94
  return float(xs) if ok else None
95
 
96
- lines = ["๐Ÿšจ ๅœฐ้œ‡้ ่ญฆ๏ผˆๆœ€ๆ–ฐ๏ผ‰:", "-" * 20]
97
  for it in items[:limit]:
98
  identifier = it.get("identifier") or it.get("eventId") or it.get("id") or "โ€”"
99
  status = it.get("status") or "โ€”"
@@ -102,7 +100,6 @@ except Exception:
102
 
103
  mag = _num(it.get("magnitudeValue") or it.get("magnitude") or it.get("ml") or it.get("mw"))
104
  mag_str = f"{mag:.1f}" if mag is not None else "โ€”"
105
-
106
  depth = _num(it.get("depth"))
107
  depth_str = f"{depth:.0f}" if depth is not None else "โ€”"
108
 
@@ -111,8 +108,7 @@ except Exception:
111
  lat_str = f"{lat:.2f}" if lat is not None else "โ€”"
112
  lon_str = f"{lon:.2f}" if lon is not None else "โ€”"
113
 
114
- origin = it.get("originTime") or ""
115
- tw_str, utc_str = _parse_cwa_time(origin)
116
 
117
  areas = it.get("locationDesc") or it.get("areas") or it.get("alertAreas")
118
  if isinstance(areas, list):
@@ -122,43 +118,34 @@ except Exception:
122
  else:
123
  areas_txt = "โ€”"
124
 
125
- lines.append(
126
- f"ไบ‹ไปถ: {identifier} | ็‹€ๆ…‹: {status} | ้กžๅž‹: {msg_type}#{msg_no}\n"
127
  f"้œ‡็ดš/ๆทฑๅบฆ: M{mag_str} / {depth_str} km\n"
128
  f"้œ‡ไธญ: lat {lat_str}, lon {lon_str}\n"
129
  f"ๆ™‚้–“: {tw_str}๏ผˆๅฐ็ฃ๏ผ‰ / {utc_str}๏ผˆUTC๏ผ‰\n"
130
- f"้ ่ญฆๅœฐๅ€: {areas_txt}"
 
131
  )
132
- lines.append("")
133
-
134
  if len(items) > limit:
135
- lines.append(f"... ๅฆๆœ‰ {len(items) - limit} ็ญ†ใ€‚")
136
- return "\n".join(lines).strip()
137
 
138
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
139
- # 2) AI ๅฐ่ฉฑ๏ผˆTransformers๏ผ‰โ€” ๅปถ้ฒ่ผ‰ๅ…ฅ่ˆ‡ๅฎน้Œฏ
140
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
141
  _LLM = {"loaded": False, "ok": False, "err": None, "model": None, "tokenizer": None, "device": "cpu"}
142
 
143
  def _ensure_llm():
144
- """
145
- ๆ‡ถ่ผ‰ๅ…ฅ LLMใ€‚้ ่จญไธญๆ–‡ๆจกๅž‹ ckiplab/gpt2-base-chinese๏ผŒๅฏ็”จ็’ฐๅขƒ่ฎŠๆ•ธ่ฆ†ๅฏซ๏ผš
146
- - LLM_MODEL (้ ่จญ ckiplab/gpt2-base-chinese)
147
- - LLM_DEVICE (cuda / cpu)
148
- ๅ›žๅ‚ณ (ok: bool, err: str|None)
149
- """
150
  if _LLM["loaded"]:
151
  return _LLM["ok"], _LLM["err"]
152
-
153
  _LLM["loaded"] = True
154
  try:
155
- # ๅŒฏๅ…ฅๆ”พๅœจๅ‡ฝๅผๅ…ง๏ผŒ้ฟๅ…ๆœๅ‹™ๅ•Ÿๅ‹•้Žๆ…ขๆˆ–ๅฅ—ไปถ็ผบๅคฑๆ™‚็›ดๆŽฅๅดฉๆฝฐ
156
- import torch # noqa: F401
157
- from transformers import AutoTokenizer, AutoModelForCausalLM # noqa: F401
158
 
159
  device = os.getenv("LLM_DEVICE")
160
  if device not in ("cuda", "cpu"):
161
- # ่‡ชๅ‹•ๅตๆธฌ
162
  try:
163
  import torch as _t
164
  device = "cuda" if _t.cuda.is_available() else "cpu"
@@ -166,10 +153,10 @@ def _ensure_llm():
166
  except Exception:
167
  device = "cpu"
168
 
169
- model_name = os.getenv("LLM_MODEL", "ckiplab/gpt2-base-chinese") # ๅฏๆ”นๆˆ "gpt2"
170
- from transformers import AutoTokenizer, AutoModelForCausalLM
171
- tok = AutoTokenizer.from_pretrained(model_name)
172
- mdl = AutoModelForCausalLM.from_pretrained(model_name)
173
  try:
174
  mdl = mdl.to(device)
175
  except Exception:
@@ -187,9 +174,10 @@ def generate_ai_text(user_prompt: str) -> str:
187
  ok, err = _ensure_llm()
188
  if not ok:
189
  return ("๐Ÿค– AI ๅฐšๆœชๅ•Ÿ็”จ๏ผš็ผบๅฐ‘ไพ่ณดๆˆ–ๆจกๅž‹ๆœชไธ‹่ผ‰ใ€‚\n"
190
- "่ซ‹ๅœจ requirements.txt ๅŠ ๅ…ฅ๏ผštransformersใ€torchใ€accelerateใ€safetensorsใ€‚\n"
191
  f"่ฉณ็ดฐ้Œฏ่ชค๏ผš{err}")
192
 
 
193
  tok = _LLM["tokenizer"]
194
  mdl = _LLM["model"]
195
  device = _LLM["device"]
@@ -198,16 +186,13 @@ def generate_ai_text(user_prompt: str) -> str:
198
  top_k = int(os.getenv("LLM_TOP_K", "50"))
199
  temperature = float(os.getenv("LLM_TEMPERATURE", "0.7"))
200
 
201
- # ๅปบ็ซ‹็ณป็ตฑๅŒ–ๆ็คบ๏ผŒ่ฎ“ๅ›ž็ญ”ๆ›ด่ฒผ่ฟ‘ๆฉŸๅ™จไบบไธป้กŒ
202
- system_prefix = (
203
- "ไฝ ๆ˜ฏไธ€ๅ€‹ๅœฐ้œ‡่ณ‡่จŠ่ˆ‡ไธ€่ˆฌๅ•็ญ”็š„ LINE ๅŠฉ็†ใ€‚ๅ›ž็ญ”่ฆ็ฒพ็ฐกใ€ๆธ…ๆฅš๏ผŒ"
204
- "่‹ฅ่ˆ‡ๅœฐ้œ‡็›ธ้—œๅฏๅŠ ๅ…ฅๆณจๆ„ไบ‹้ …ใ€‚่‹ฅๅ•้กŒ่ˆ‡ๅœฐ้œ‡็„ก้—œ๏ผŒๅฐฑไธ€่ˆฌๅ›ž่ฆ†ใ€‚\n\n"
205
- "ไฝฟ็”จ่€…๏ผš"
206
  )
207
- prompt = system_prefix + user_prompt
208
 
209
  try:
210
- import torch
211
  inputs = tok(prompt, return_tensors="pt").to(device)
212
  with torch.no_grad():
213
  output = mdl.generate(
@@ -220,19 +205,16 @@ def generate_ai_text(user_prompt: str) -> str:
220
  pad_token_id=tok.eos_token_id,
221
  )
222
  text = tok.decode(output[0], skip_special_tokens=True)
223
- # ๅ˜—่ฉฆๆŠŠ system_prefix ๅŽปๆމ๏ผŒ็•™ไธ‹ไฝฟ็”จ่€…ๅพŒ็š„ๅ›žๆ‡‰
224
- if system_prefix in text:
225
- text = text.split(system_prefix, 1)[-1]
226
- # GPT-2 ๅฏ่ƒฝๆœƒๆŠŠ prompt ไนŸๅธถๅ›ž๏ผŒ็ฐกๅ–ฎ่ฃๅ‰ช
227
  if user_prompt in text:
228
  text = text.split(user_prompt, 1)[-1].strip()
229
- # ๅฎ‰ๅ…จ้•ทๅบฆๆŽงๅˆถ
230
- return text.strip()[:1200] if text else "๏ผˆๆฒ’ๆœ‰็”ข็”Ÿๅ…งๅฎน๏ผ‰"
231
  except Exception as e:
232
  return f"AI ็”ข็”Ÿ็™ผ็”Ÿ้Œฏ่ชค๏ผš{e}"
233
 
234
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
235
- # 3) ไธญๆ–‡ๅญ—ๅž‹๏ผˆ่‹ฅๅฎนๅ™จๆœ‰ Noto/WenQuanYi ็ญ‰ๆœƒ่‡ชๅ‹•ๅฅ—็”จ๏ผ‰
236
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
237
  for fp in [
238
  "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
@@ -245,15 +227,14 @@ for fp in [
245
  break
246
 
247
  # โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
248
- # 4) ๅŸบๆœฌ่จญๅฎš
249
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
250
  CHANNEL_ACCESS_TOKEN = os.getenv("CHANNEL_ACCESS_TOKEN")
251
  CHANNEL_SECRET = os.getenv("CHANNEL_SECRET")
252
 
253
- # HF Space ๅ…ฌ็ถฒไฝๅ€๏ผˆSPACEURL > SPACE_ID ๆŽจ่ซ–๏ผ‰
254
  HF_SPACE_URL = os.getenv("SPACEURL")
255
  if not HF_SPACE_URL:
256
- sid = os.getenv("SPACE_ID") # ๅฝขๅฆ‚ user/space
257
  if sid and "/" in sid:
258
  a, n = sid.split("/", 1)
259
  HF_SPACE_URL = f"https://{a.replace('_','-')}-{n.replace('_','-')}.hf.space"
@@ -268,7 +249,7 @@ configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
268
  handler = WebhookHandler(CHANNEL_SECRET)
269
 
270
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
271
- # 5) ้ฆ–้ ๏ผˆ็พŽ่ง€่ชชๆ˜Ž้ ๏ผ‰
272
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
273
  @app.route("/", methods=["GET"])
274
  def home():
@@ -280,83 +261,50 @@ def home():
280
 
281
  return f"""
282
  <!doctype html>
283
- <html lang="zh-Hant">
284
- <head>
285
- <meta charset="utf-8" />
286
- <meta name="viewport" content="width=device-width,initial-scale=1" />
287
  <title>ๅœฐ้œ‡้ ่ญฆ dayichen โ€“ LINE Bot Server</title>
288
  <style>
289
- :root {{
290
- --bg:#0f1115; --card:#151821; --text:#e6e8ef; --muted:#9aa4b2; --accent:#23c55e; --border:rgba(255,255,255,.08);
291
- }}
292
- * {{ box-sizing:border-box; }}
293
- body {{
294
- margin:0; background:var(--bg); color:var(--text);
295
- font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans TC","PingFang TC",sans-serif;
296
- padding:32px 16px; display:flex; justify-content:center;
297
- }}
298
- .wrap {{ width:100%; max-width:980px; }}
299
- .hero {{
300
- background:linear-gradient(135deg,#1f2937,#0f172a);
301
- border:1px solid var(--border); border-radius:16px; padding:28px; margin-bottom:20px;
302
- box-shadow:0 8px 30px rgba(0,0,0,.25);
303
- }}
304
- .title {{ margin:0 0 6px; font-size:28px; font-weight:800; letter-spacing:.3px; }}
305
- .subtitle {{ margin:0; color:var(--muted); }}
306
- .grid {{ display:grid; gap:16px; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); margin-top:18px; }}
307
- .card {{ background:#151821; border:1px solid var(--border); border-radius:14px; padding:16px 18px; }}
308
- h3 {{ margin:0 0 8px; font-size:18px; }}
309
- code, .mono {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
310
- .badge {{ display:inline-block; padding:2px 8px; border-radius:999px; background:#1f2937; border:1px solid var(--border); font-size:12px; color:#9aa4b2; }}
311
- ul {{ padding-left:18px; margin:10px 0 0; }}
312
- li + li {{ margin-top:6px; }}
313
- a {{ color:#60a5fa; text-decoration:none; }}
314
- a:hover {{ text-decoration:underline; }}
315
- .kbd {{ padding:2px 6px; border:1px solid var(--border); border-radius:6px; background:#0b0e14; }}
316
- .foot {{ color:#9aa4b2; font-size:13px; margin-top:18px; text-align:center; }}
317
- </style>
318
- </head>
319
- <body>
320
- <div class="wrap">
321
- <section class="hero">
322
- <div class="badge">็‹€ๆ…‹๏ผš<span style="color:{'#86efac' if channel_ok=='โœ…' else '#fbbf24'}">{channel_ok}</span> LINE ้‡‘้‘ฐใ€€ยทใ€€HF Space๏ผš{space_ok}</div>
323
- <h1 class="title">ๅœฐ้œ‡้ ่ญฆ dayichen โ€“ LINE Bot</h1>
324
- <p class="subtitle">ๆŒ‡ไปค๏ผš/helpใ€ๅœฐ้œ‡/quakeใ€่‡บ็ฃๅœฐ้œ‡/ๅฐ็ฃๅœฐ้œ‡ใ€่‡บ็ฃๅœฐ้œ‡็•ซๅœ–/ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–ใ€ๅœฐ้œ‡้ ่ญฆใ€AI ๅฐ่ฉฑ๏ผˆai + ๅ•้กŒ๏ผ‰ใ€‚</p>
325
- <div class="grid">
326
- <div class="card">
327
- <h3>๐Ÿš€ ๅฟซ้€Ÿ้–‹ๅง‹</h3>
328
- <ul>
329
- <li><span class="kbd">/help</span>๏ผš้กฏ็คบๆ‰€ๆœ‰ๆŒ‡ไปค</li>
330
- <li><span class="kbd">ๅœฐ้œ‡</span>/<span class="kbd">quake</span>๏ผšๅ…จ็ƒ่ฟ‘ 24 ๅฐๆ™‚ Mโ‰ฅ5.0</li>
331
- <li><span class="kbd">่‡บ็ฃๅœฐ้œ‡</span>/<span class="kbd">ๅฐ็ฃๅœฐ้œ‡</span>๏ผšไปŠๅนดๅฐ็ฃๅ€ๅŸŸๆธ…ๅ–ฎ๏ผˆๅซๆ—ฅๆœŸๆ™‚้–“๏ผ‰</li>
332
- <li><span class="kbd">่‡บ็ฃๅœฐ้œ‡็•ซๅœ–</span>/<span class="kbd">ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–</span>๏ผšๅ›žๅ‚ณๅœฐๅœ–ๅœ–็‰‡</li>
333
- <li><span class="kbd">ๅœฐ้œ‡้ ่ญฆ</span>๏ผšCWA ๅœฐ้œ‡้ ่ญฆ๏ผˆๆœ€ๆ–ฐ 5 ็ญ†๏ผ‰</li>
334
- <li><span class="kbd">ai ไฝ ็š„ๅ•้กŒ</span>๏ผšๅ•Ÿ็”จ AI ๅฐ่ฉฑ๏ผŒไพ‹ๅฆ‚ <span class="kbd">ai ไปŠๆ—ฅๅฐ็ฃๅœฐ้œ‡้ขจ้šช้œ€่ฆๆณจๆ„ไป€้บผ๏ผŸ</span></li>
335
- </ul>
336
- </div>
337
- <div class="card">
338
- <h3>๐Ÿ› ๏ธ Webhook ่ˆ‡้œๆ…‹ๆช”</h3>
339
- <ul>
340
- <li>Webhook๏ผš<span class="mono"><a href="{webhook_url}">{webhook_url}</a></span></li>
341
- <li>้œๆ…‹ๅœ–็‰‡๏ผš<span class="mono">{static_hint}</span></li>
342
- <li>ๅฅๅบทๆชขๆŸฅ๏ผš<span class="mono"><a href="{base}/healthz">{base}/healthz</a></span></li>
343
- </ul>
344
- </div>
345
- <div class="card">
346
- <h3>โ„น๏ธ ๅ‚™่จป</h3>
347
- <ul>
348
- <li>AI ้œ€่ฆๅฎ‰่ฃ <span class="mono">transformers / torch</span> ็ญ‰ไพ่ณด๏ผ›ๆœชๅฎ‰่ฃๆ™‚ๆœๅ‹™ๆœƒ็ตฆๅ‡บๆ็คบใ€‚</li>
349
- <li>ๆจกๅž‹๏ผš<span class="mono">{os.getenv("LLM_MODEL","ckiplab/gpt2-base-chinese")}</span> ยท ่ฃ็ฝฎ๏ผš<span class="mono">{os.getenv("LLM_DEVICE","auto")}</span></li>
350
- <li>ๅœฐๅœ–ไปฅ Matplotlib ็”ข็”Ÿๆ–ผ <span class="mono">/tmp/static</span>๏ผŒ็”ฑ <span class="mono">/static/</span> ๆไพ›ใ€‚</li>
351
- </ul>
352
- </div>
353
- </div>
354
- <p class="foot">ยฉ {datetime.now().year} dayichen ยท server: {base}</p>
355
- </section>
356
- </div>
357
- </body>
358
- </html>
359
- """
360
 
361
  @app.route("/healthz")
362
  def healthz():
@@ -367,7 +315,7 @@ def serve_static(filename):
367
  return send_from_directory(STATIC_DIR, filename)
368
 
369
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
370
- # 6) ่ณ‡ๆ–™ๆŸฅ่ฉข๏ผˆUSGS๏ผ‰
371
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
372
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
373
 
@@ -395,9 +343,7 @@ def fetch_global_last24h_text(min_mag=5.0, limit=10) -> str:
395
  for f in features:
396
  p = f["properties"]
397
  t_utc = datetime.fromtimestamp(p["time"] / 1000, tz=timezone.utc)
398
- lines.append(
399
- f"้œ‡็ดš: {p['mag']:.1f} | ๆ—ฅๆœŸๆ™‚้–“: {t_utc.strftime('%Y-%m-%d %H:%M')} (UTC)\nๅœฐ้ปž: {p.get('place','')}"
400
- )
401
  return "\n\n".join(lines)
402
  except Exception as e:
403
  return f"โŒ ๆŸฅ่ฉขๅคฑๆ•—: {e}"
@@ -431,7 +377,7 @@ def fetch_taiwan_df_this_year(min_mag=5.0) -> pd.DataFrame | str:
431
  return f"โŒ ๆŸฅ่ฉขๅคฑๆ•—: {e}"
432
 
433
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
434
- # 7) ่ฃฝๅœ–
435
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
436
  def create_and_save_map(df: pd.DataFrame) -> str:
437
  fig, ax = plt.subplots(figsize=(9, 6), dpi=150)
@@ -464,7 +410,7 @@ def _base_url_for_images() -> str:
464
  return HF_SPACE_URL.rstrip("/") if HF_SPACE_URL else request.url_root.rstrip("/")
465
 
466
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
467
- # 8) LINE Webhook
468
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
469
  @app.route("/callback", methods=["POST"])
470
  def callback():
@@ -484,7 +430,7 @@ def handle_message(event):
484
  with ApiClient(configuration) as api_client:
485
  line_bot_api = MessagingApi(api_client)
486
 
487
- # 1) ๅœฐ้œ‡้ ่ญฆ๏ผˆCWA๏ผ‰
488
  if "ๅœฐ้œ‡้ ่ญฆ" in user_message:
489
  reply_text = fetch_cwa_alarm_list(limit=5)
490
  line_bot_api.reply_message_with_http_info(
@@ -492,7 +438,7 @@ def handle_message(event):
492
  )
493
  return
494
 
495
- # 2) ่‡บ็ฃๅœฐ้œ‡็•ซๅœ–
496
  if ("่‡บ็ฃๅœฐ้œ‡็•ซๅœ–" in user_message) or ("ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–" in user_message):
497
  result = fetch_taiwan_df_this_year()
498
  if isinstance(result, pd.DataFrame):
@@ -510,24 +456,24 @@ def handle_message(event):
510
  line_bot_api.reply_message_with_http_info(reply)
511
  return
512
 
513
- # 3) ่ชชๆ˜Ž
514
  if user_message == "/help":
515
  text = (
516
- "๐Ÿ“– ๅœฐ้œ‡้ ่ญฆ dayichen ๆŒ‡ไปค\n\n"
517
- "โžก๏ธ /help\n"
518
- "โžก๏ธ ๅœฐ้œ‡ / quake๏ผˆๅ…จ็ƒ่ฟ‘24ๅฐๆ™‚๏ผŒๅซๆ—ฅๆœŸๆ™‚้–“๏ผ‰\n"
519
- "โžก๏ธ ่‡บ็ฃๅœฐ้œ‡ / ๅฐ็ฃๅœฐ้œ‡๏ผˆไปŠๅนดๅฐ็ฃๅ€ๅŸŸๆธ…ๅ–ฎ๏ผ‰\n"
520
- "โžก๏ธ ่‡บ็ฃๅœฐ้œ‡็•ซๅœ– / ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–๏ผˆไปŠๅนดๅฐ็ฃๅ€ๅŸŸๅˆ†ไฝˆๅœ–๏ผ‰\n"
521
- "โžก๏ธ ๅœฐ้œ‡้ ่ญฆ๏ผˆCWA ๆœ€ๆ–ฐ 5 ็ญ†๏ผ‰\n"
522
- "โžก๏ธ ai ไฝ ็š„ๅ•้กŒ๏ผˆAI ๅฐ่ฉฑ๏ผ‰\n"
523
- "โžก๏ธ ไฝ ๅฅฝ"
524
  )
525
  line_bot_api.reply_message_with_http_info(
526
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=text)])
527
  )
528
  return
529
 
530
- # 4) ๅฐ็ฃๆธ…ๅ–ฎ
531
  if ("่‡บ็ฃๅœฐ้œ‡" in user_message) or ("ๅฐ็ฃๅœฐ้œ‡" in user_message):
532
  result = fetch_taiwan_df_this_year()
533
  if isinstance(result, pd.DataFrame):
@@ -546,7 +492,7 @@ def handle_message(event):
546
  )
547
  return
548
 
549
- # 5) ๅ…จ็ƒ 24 ๅฐๆ™‚
550
  if ("ๅœฐ้œ‡" in user_message) or ("quake" in user_message):
551
  reply_text = fetch_global_last24h_text()
552
  line_bot_api.reply_message_with_http_info(
@@ -554,28 +500,23 @@ def handle_message(event):
554
  )
555
  return
556
 
557
- # 6) AI ๅฐ่ฉฑ๏ผˆ่งธ็™ผ่ฉž๏ผšไปฅ "ai "ใ€"ai๏ผš"ใ€"ai:" ้–‹้ ญ๏ผ›ๆˆ–ๆœ€ๅพŒๅ…œๅบ•๏ผ‰
558
  if user_message.startswith("ai ") or user_message.startswith("ai:") or user_message.startswith("ai๏ผš"):
559
- prompt = user_message_raw[2:].lstrip(" ๏ผš:").strip()
560
- if not prompt:
561
- prompt = "่ซ‹ไฝœ็‚บไธ€ๅ€‹ๅœฐ้œ‡่ณ‡่จŠๅŠฉ็†๏ผŒ็ฐก่ฆไป‹็ดนไฝ ็š„ๅŠŸ่ƒฝใ€‚"
562
  ai_text = generate_ai_text(prompt)
563
  line_bot_api.reply_message_with_http_info(
564
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=ai_text)])
565
  )
566
  return
567
 
568
- # 7) ๆ‹›ๅ‘ผ
569
  if ("ไฝ ๅฅฝ" in user_message) or ("hi" in user_message):
570
  line_bot_api.reply_message_with_http_info(
571
- ReplyMessageRequest(
572
- reply_token=event.reply_token,
573
- messages=[TextMessage(text="๐Ÿ‘‹ ไฝ ๅฅฝ๏ผ่ผธๅ…ฅ /help ๆŸฅ็œ‹ๆŒ‡ไปคใ€‚")]
574
- )
575
  )
576
  return
577
 
578
- # 8) ๅ…œๅบ•๏ผš่‹ฅ้žไธŠ่ฟฐไปปไฝ•ๆŒ‡ไปค๏ผŒไบค็ตฆ AI ็”ข็”Ÿๅ›ž่ฆ†๏ผˆๆ›ดๆ™บ่ƒฝ๏ผ‰
579
  fallback = generate_ai_text(user_message_raw)
580
  line_bot_api.reply_message_with_http_info(
581
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=fallback)])
 
1
  # app.py
2
  import os
3
+ # ๅฐ‡ Matplotlib ่ˆ‡ Hugging Face ๆจกๅž‹ๅฟซๅ–ๅˆฐๅฏๅฏซ็›ฎ้Œ„๏ผˆHF Spaces ๅธธ่ฆ‹ๅšๆณ•๏ผ‰
4
+ os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
5
+ os.environ.setdefault("TRANSFORMERS_CACHE", "/tmp/huggingface")
6
+ os.makedirs(os.environ["TRANSFORMERS_CACHE"], exist_ok=True)
7
 
8
  import uuid
9
  import tempfile
 
32
  from matplotlib import font_manager as fm
33
 
34
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
+ # CWA ๅœฐ้œ‡้ ่ญฆ๏ผš่‹ฅๅค–้ƒจๆจก็ต„ๅญ˜ๅœจๅฐฑ็”จ๏ผ›ๅฆๅ‰‡ไฝฟ็”จๅ‚™ๆดๅฏฆไฝœ
36
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
37
  try:
38
+ from fetch_cwa_alarm_list import fetch_cwa_alarm_list # type: ignore
 
39
  except Exception:
40
  CWA_ALARM_API = "https://app-2.cwa.gov.tw/api/v1/earthquake/alarm/list"
41
 
42
  def _parse_cwa_time(s: str):
 
43
  if not s:
44
  return ("ๆœช็Ÿฅ", "ๆœช็Ÿฅ")
45
  try:
 
55
  return (s, "ๆœช็Ÿฅ")
56
 
57
  def fetch_cwa_alarm_list(limit: int = 5) -> str:
 
58
  try:
59
  r = requests.get(CWA_ALARM_API, timeout=10)
60
  r.raise_for_status()
 
76
  if "T" in s or s.endswith("Z") or "+" in s:
77
  dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
78
  else:
79
+ dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone(timedelta(hours=8)))
 
 
80
  return dt.astimezone(timezone.utc)
81
  except Exception:
82
  return datetime.min.replace(tzinfo=timezone.utc)
 
91
  ok = xs.replace(".", "", 1).replace("-", "", 1).isdigit()
92
  return float(xs) if ok else None
93
 
94
+ out = ["๐Ÿšจ ๅœฐ้œ‡้ ่ญฆ๏ผˆๆœ€ๆ–ฐ๏ผ‰:", "-" * 20]
95
  for it in items[:limit]:
96
  identifier = it.get("identifier") or it.get("eventId") or it.get("id") or "โ€”"
97
  status = it.get("status") or "โ€”"
 
100
 
101
  mag = _num(it.get("magnitudeValue") or it.get("magnitude") or it.get("ml") or it.get("mw"))
102
  mag_str = f"{mag:.1f}" if mag is not None else "โ€”"
 
103
  depth = _num(it.get("depth"))
104
  depth_str = f"{depth:.0f}" if depth is not None else "โ€”"
105
 
 
108
  lat_str = f"{lat:.2f}" if lat is not None else "โ€”"
109
  lon_str = f"{lon:.2f}" if lon is not None else "โ€”"
110
 
111
+ tw_str, utc_str = _parse_cwa_time(it.get("originTime") or "")
 
112
 
113
  areas = it.get("locationDesc") or it.get("areas") or it.get("alertAreas")
114
  if isinstance(areas, list):
 
118
  else:
119
  areas_txt = "โ€”"
120
 
121
+ out.append(
122
+ f"{status} | ้กžๅž‹: {msg_type}#{msg_no}\n"
123
  f"้œ‡็ดš/ๆทฑๅบฆ: M{mag_str} / {depth_str} km\n"
124
  f"้œ‡ไธญ: lat {lat_str}, lon {lon_str}\n"
125
  f"ๆ™‚้–“: {tw_str}๏ผˆๅฐ็ฃ๏ผ‰ / {utc_str}๏ผˆUTC๏ผ‰\n"
126
+ f"้ ่ญฆๅœฐๅ€: {areas_txt}\n"
127
+ f"ไบ‹ไปถ: {identifier}"
128
  )
129
+ out.append("")
 
130
  if len(items) > limit:
131
+ out.append(f"... ๅฆๆœ‰ {len(items) - limit} ็ญ†ใ€‚")
132
+ return "\n".join(out).strip()
133
 
134
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
135
+ # AI ็”ŸๆˆๅŠŸ่ƒฝ๏ผˆTransformers๏ผ‰๏ผšๆ‡ถ่ผ‰ๅ…ฅ + ๆฌŠ้™ๅฎ‰ๅ…จๅฟซๅ–
136
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
  _LLM = {"loaded": False, "ok": False, "err": None, "model": None, "tokenizer": None, "device": "cpu"}
138
 
139
  def _ensure_llm():
 
 
 
 
 
 
140
  if _LLM["loaded"]:
141
  return _LLM["ok"], _LLM["err"]
 
142
  _LLM["loaded"] = True
143
  try:
144
+ import torch # noqa
145
+ from transformers import AutoTokenizer, AutoModelForCausalLM
 
146
 
147
  device = os.getenv("LLM_DEVICE")
148
  if device not in ("cuda", "cpu"):
 
149
  try:
150
  import torch as _t
151
  device = "cuda" if _t.cuda.is_available() else "cpu"
 
153
  except Exception:
154
  device = "cpu"
155
 
156
+ model_name = os.getenv("LLM_MODEL", "ckiplab/gpt2-base-chinese")
157
+ cache_dir = os.environ["TRANSFORMERS_CACHE"]
158
+ tok = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir)
159
+ mdl = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
160
  try:
161
  mdl = mdl.to(device)
162
  except Exception:
 
174
  ok, err = _ensure_llm()
175
  if not ok:
176
  return ("๐Ÿค– AI ๅฐšๆœชๅ•Ÿ็”จ๏ผš็ผบๅฐ‘ไพ่ณดๆˆ–ๆจกๅž‹ๆœชไธ‹่ผ‰ใ€‚\n"
177
+ "่ซ‹ๅœจ requirements.txt ๅŠ ๅ…ฅ transformersใ€torchใ€accelerateใ€safetensorsใ€sentencepieceใ€‚\n"
178
  f"่ฉณ็ดฐ้Œฏ่ชค๏ผš{err}")
179
 
180
+ import torch # type: ignore
181
  tok = _LLM["tokenizer"]
182
  mdl = _LLM["model"]
183
  device = _LLM["device"]
 
186
  top_k = int(os.getenv("LLM_TOP_K", "50"))
187
  temperature = float(os.getenv("LLM_TEMPERATURE", "0.7"))
188
 
189
+ sys_prefix = (
190
+ "ไฝ ๆ˜ฏไธ€ๅ€‹ๅœฐ้œ‡่ณ‡่จŠ่ˆ‡ไธ€่ˆฌๅ•็ญ”็š„ LINE ๅŠฉ็†ใ€‚ๅ›ž็ญ”่ฆ็ฒพ็ฐกใ€ๆธ…ๆฅš๏ผ›"
191
+ "่‹ฅ่ˆ‡ๅœฐ้œ‡็›ธ้—œๅฏๅŠ ๅ…ฅๆณจๆ„ไบ‹้ …๏ผ›่‹ฅ็„ก้—œๅ‰‡ไธ€่ˆฌๅ›ž่ฆ†ใ€‚\n\nไฝฟ็”จ่€…๏ผš"
 
 
192
  )
193
+ prompt = sys_prefix + user_prompt
194
 
195
  try:
 
196
  inputs = tok(prompt, return_tensors="pt").to(device)
197
  with torch.no_grad():
198
  output = mdl.generate(
 
205
  pad_token_id=tok.eos_token_id,
206
  )
207
  text = tok.decode(output[0], skip_special_tokens=True)
208
+ if sys_prefix in text:
209
+ text = text.split(sys_prefix, 1)[-1]
 
 
210
  if user_prompt in text:
211
  text = text.split(user_prompt, 1)[-1].strip()
212
+ return (text or "๏ผˆๆฒ’ๆœ‰็”ข็”Ÿๅ…งๅฎน๏ผ‰")[:1200]
 
213
  except Exception as e:
214
  return f"AI ็”ข็”Ÿ็™ผ็”Ÿ้Œฏ่ชค๏ผš{e}"
215
 
216
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
217
+ # ไธญๆ–‡ๅญ—ๅž‹๏ผˆๆœ‰ๅฐฑๅฅ—็”จ๏ผ‰
218
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
219
  for fp in [
220
  "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
 
227
  break
228
 
229
  # โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
230
+ # ๅŸบๆœฌ่จญๅฎš
231
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
232
  CHANNEL_ACCESS_TOKEN = os.getenv("CHANNEL_ACCESS_TOKEN")
233
  CHANNEL_SECRET = os.getenv("CHANNEL_SECRET")
234
 
 
235
  HF_SPACE_URL = os.getenv("SPACEURL")
236
  if not HF_SPACE_URL:
237
+ sid = os.getenv("SPACE_ID")
238
  if sid and "/" in sid:
239
  a, n = sid.split("/", 1)
240
  HF_SPACE_URL = f"https://{a.replace('_','-')}-{n.replace('_','-')}.hf.space"
 
249
  handler = WebhookHandler(CHANNEL_SECRET)
250
 
251
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
252
+ # ้ฆ–้ ๏ผˆๆŒ‡ไปค/Webhook/้œๆ…‹ๆช”่ชชๆ˜Ž๏ผ‰
253
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
254
  @app.route("/", methods=["GET"])
255
  def home():
 
261
 
262
  return f"""
263
  <!doctype html>
264
+ <html lang="zh-Hant"><head>
265
+ <meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
 
 
266
  <title>ๅœฐ้œ‡้ ่ญฆ dayichen โ€“ LINE Bot Server</title>
267
  <style>
268
+ :root{{--bg:#0f1115;--card:#151821;--text:#e6e8ef;--muted:#9aa4b2;--border:rgba(255,255,255,.08)}}
269
+ *{{box-sizing:border-box}} body{{margin:0;background:#0f1115;color:#e6e8ef;
270
+ font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans TC","PingFang TC",sans-serif;
271
+ padding:32px 16px;display:flex;justify-content:center}}
272
+ .wrap{{width:100%;max-width:980px}} .hero{{background:linear-gradient(135deg,#1f2937,#0f172a);
273
+ border:1px solid var(--border);border-radius:16px;padding:28px;margin-bottom:20px;box-shadow:0 8px 30px rgba(0,0,0,.25)}}
274
+ .title{{margin:0 0 6px;font-size:28px;font-weight:800}} .subtitle{{margin:0;color:#9aa4b2}}
275
+ .grid{{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));margin-top:18px}}
276
+ .card{{background:#151821;border:1px solid var(--border);border-radius:14px;padding:16px 18px}}
277
+ h3{{margin:0 0 8px;font-size:18px}} .kbd{{padding:2px 6px;border:1px solid var(--border);border-radius:6px;background:#0b0e14}}
278
+ .mono,code{{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#e6e8ef}}
279
+ a{{color:#60a5fa;text-decoration:none}} a:hover{{text-decoration:underline}}
280
+ .badge{{display:inline-block;padding:2px 8px;border-radius:999px;background:#1f2937;border:1px solid var(--border);font-size:12px;color:#9aa4b2}}
281
+ .foot{{color:#9aa4b2;font-size:13px;margin-top:18px;text-align:center}}
282
+ </style></head>
283
+ <body><div class="wrap"><section class="hero">
284
+ <div class="badge">็‹€ๆ…‹๏ผš<span style="color:{'#86efac' if channel_ok=='โœ…' else '#fbbf24'}">{channel_ok}</span> LINE ้‡‘้‘ฐใ€€ยทใ€€HF Space๏ผš{space_ok}</div>
285
+ <h1 class="title">ๅœฐ้œ‡้ ่ญฆ dayichen โ€“ LINE Bot</h1>
286
+ <p class="subtitle">ๆŒ‡ไปค๏ผš/helpใ€ๅœฐ้œ‡/quakeใ€่‡บ็ฃๅœฐ้œ‡/ๅฐ็ฃๅœฐ้œ‡ใ€่‡บ็ฃๅœฐ้œ‡็•ซๅœ–/ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–ใ€ๅœฐ้œ‡้ ่ญฆใ€AI๏ผˆai + ๅ•้กŒ๏ผ‰ใ€‚</p>
287
+ <div class="grid">
288
+ <div class="card"><h3>๐Ÿš€ ๅฟซ้€Ÿ้–‹ๅง‹</h3><ul>
289
+ <li><span class="kbd">/help</span>๏ผš้กฏ็คบๆ‰€ๆœ‰ๆŒ‡ไปค</li>
290
+ <li><span class="kbd">ๅœฐ้œ‡</span>/<span class="kbd">quake</span>๏ผšๅ…จ็ƒ่ฟ‘ 24 ๅฐๆ™‚ Mโ‰ฅ5.0</li>
291
+ <li><span class="kbd">่‡บ็ฃๅœฐ้œ‡</span>/<span class="kbd">ๅฐ็ฃๅœฐ้œ‡</span>๏ผšไปŠๅนดๅฐ็ฃๅ€ๅŸŸๆธ…ๅ–ฎ๏ผˆๅซๆ—ฅๆœŸๆ™‚้–“๏ผ‰</li>
292
+ <li><span class="kbd">่‡บ็ฃๅœฐ้œ‡็•ซๅœ–</span>/<span class="kbd">ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–</span>๏ผšๅ›žๅ‚ณๅœฐๅœ–ๅœ–็‰‡</li>
293
+ <li><span class="kbd">ๅœฐ้œ‡้ ่ญฆ</span>๏ผšCWA ๅœฐ้œ‡้ ่ญฆ๏ผˆๆœ€ๆ–ฐ 5 ็ญ†๏ผ‰</li>
294
+ <li><span class="kbd">ai ไฝ ็š„ๅ•้กŒ</span>๏ผšAI ๅฐ่ฉฑ๏ผˆๆจกๅž‹๏ผš<span class="mono">{os.getenv("LLM_MODEL","ckiplab/gpt2-base-chinese")}</span>๏ผ‰</li>
295
+ </ul></div>
296
+ <div class="card"><h3>๐Ÿ› ๏ธ Webhook / ้œๆ…‹ๆช”</h3><ul>
297
+ <li>Webhook๏ผš<span class="mono"><a href="{webhook_url}">{webhook_url}</a></span></li>
298
+ <li>้œๆ…‹ๅœ–็‰‡๏ผš<span class="mono">{static_hint}</span></li>
299
+ <li>ๅฅๅบทๆชขๆŸฅ๏ผš<span class="mono"><a href="{base}/healthz">{base}/healthz</a></span></li>
300
+ </ul></div>
301
+ <div class="card"><h3>โ„น๏ธ ๅ‚™่จป</h3><ul>
302
+ <li>AI ๅฟซๅ–ไฝ็ฝฎ๏ผš<span class="mono">{os.environ["TRANSFORMERS_CACHE"]}</span></li>
303
+ <li>่‹ฅ AI ๆœชๅฎ‰่ฃไพ่ณด๏ผŒๆฉŸๅ™จไบบๆœƒๆ็คบๅฎ‰่ฃ๏ผŒไธๆœƒๅฝฑ้Ÿฟๅ…ถไป–ๅŠŸ่ƒฝใ€‚</li>
304
+ </ul></div>
305
+ </div>
306
+ <p class="foot">ยฉ {datetime.now().year} dayichen ยท server: {base}</p>
307
+ </section></div></body></html>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
  @app.route("/healthz")
310
  def healthz():
 
315
  return send_from_directory(STATIC_DIR, filename)
316
 
317
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
318
+ # USGS ๆŸฅ่ฉข
319
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
320
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
321
 
 
343
  for f in features:
344
  p = f["properties"]
345
  t_utc = datetime.fromtimestamp(p["time"] / 1000, tz=timezone.utc)
346
+ lines.append(f"้œ‡็ดš: {p['mag']:.1f} | ๆ—ฅๆœŸๆ™‚้–“: {t_utc.strftime('%Y-%m-%d %H:%M')} (UTC)\nๅœฐ้ปž: {p.get('place','')}")
 
 
347
  return "\n\n".join(lines)
348
  except Exception as e:
349
  return f"โŒ ๆŸฅ่ฉขๅคฑๆ•—: {e}"
 
377
  return f"โŒ ๆŸฅ่ฉขๅคฑๆ•—: {e}"
378
 
379
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
380
+ # ่ฃฝๅœ–
381
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
382
  def create_and_save_map(df: pd.DataFrame) -> str:
383
  fig, ax = plt.subplots(figsize=(9, 6), dpi=150)
 
410
  return HF_SPACE_URL.rstrip("/") if HF_SPACE_URL else request.url_root.rstrip("/")
411
 
412
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
413
+ # LINE Webhook
414
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
415
  @app.route("/callback", methods=["POST"])
416
  def callback():
 
430
  with ApiClient(configuration) as api_client:
431
  line_bot_api = MessagingApi(api_client)
432
 
433
+ # ๅœฐ้œ‡้ ่ญฆ
434
  if "ๅœฐ้œ‡้ ่ญฆ" in user_message:
435
  reply_text = fetch_cwa_alarm_list(limit=5)
436
  line_bot_api.reply_message_with_http_info(
 
438
  )
439
  return
440
 
441
+ # ่‡บ็ฃๅœฐ้œ‡็•ซๅœ–
442
  if ("่‡บ็ฃๅœฐ้œ‡็•ซๅœ–" in user_message) or ("ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–" in user_message):
443
  result = fetch_taiwan_df_this_year()
444
  if isinstance(result, pd.DataFrame):
 
456
  line_bot_api.reply_message_with_http_info(reply)
457
  return
458
 
459
+ # ่ชชๆ˜Ž
460
  if user_message == "/help":
461
  text = (
462
+ "๐Ÿ“– ๆŒ‡ไปค\n\n"
463
+ "โ€ข /help\n"
464
+ "โ€ข ๅœฐ้œ‡ / quake๏ผˆๅ…จ็ƒ่ฟ‘24ๅฐๆ™‚๏ผŒๅซๆ—ฅๆœŸๆ™‚้–“๏ผ‰\n"
465
+ "โ€ข ่‡บ็ฃๅœฐ้œ‡ / ๅฐ็ฃๅœฐ้œ‡๏ผˆไปŠๅนดๅฐ็ฃๅ€ๅŸŸๆธ…ๅ–ฎ๏ผ‰\n"
466
+ "โ€ข ่‡บ็ฃๅœฐ้œ‡็•ซๅœ– / ๅฐ็ฃๅœฐ้œ‡็•ซๅœ–๏ผˆไปŠๅนดๅฐ็ฃๅ€ๅŸŸๅˆ†ไฝˆๅœ–๏ผ‰\n"
467
+ "โ€ข ๅœฐ้œ‡้ ่ญฆ๏ผˆCWA ๆœ€ๆ–ฐ 5 ็ญ†๏ผ‰\n"
468
+ "โ€ข ai ไฝ ็š„ๅ•้กŒ๏ผˆAI ๅฐ่ฉฑ๏ผ‰\n"
469
+ "โ€ข ไฝ ๅฅฝ"
470
  )
471
  line_bot_api.reply_message_with_http_info(
472
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=text)])
473
  )
474
  return
475
 
476
+ # ๅฐ็ฃๆธ…ๅ–ฎ
477
  if ("่‡บ็ฃๅœฐ้œ‡" in user_message) or ("ๅฐ็ฃๅœฐ้œ‡" in user_message):
478
  result = fetch_taiwan_df_this_year()
479
  if isinstance(result, pd.DataFrame):
 
492
  )
493
  return
494
 
495
+ # ๅ…จ็ƒ 24 ๅฐๆ™‚
496
  if ("ๅœฐ้œ‡" in user_message) or ("quake" in user_message):
497
  reply_text = fetch_global_last24h_text()
498
  line_bot_api.reply_message_with_http_info(
 
500
  )
501
  return
502
 
503
+ # AI ๅฐ่ฉฑ๏ผˆๆ˜Ž็ขบ่งธ็™ผ่ฉž๏ผ‰
504
  if user_message.startswith("ai ") or user_message.startswith("ai:") or user_message.startswith("ai๏ผš"):
505
+ prompt = user_message_raw[2:].lstrip(" ๏ผš:").strip() or "่ซ‹็ฐก่ฆไป‹็ดนไฝ ็š„ๅŠŸ่ƒฝใ€‚"
 
 
506
  ai_text = generate_ai_text(prompt)
507
  line_bot_api.reply_message_with_http_info(
508
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=ai_text)])
509
  )
510
  return
511
 
512
+ # ๆ‹›ๅ‘ผ
513
  if ("ไฝ ๅฅฝ" in user_message) or ("hi" in user_message):
514
  line_bot_api.reply_message_with_http_info(
515
+ ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text="๐Ÿ‘‹ ไฝ ๅฅฝ๏ผ่ผธๅ…ฅ /help ๆŸฅ็œ‹ๆŒ‡ไปคใ€‚")])
 
 
 
516
  )
517
  return
518
 
519
+ # ๅ…œๅบ•๏ผšไบค็ตฆ AI ๅ›ž่ฆ†๏ผŒ่ฎ“็ณป็ตฑๆ›ดๆ™บ่ƒฝ
520
  fallback = generate_ai_text(user_message_raw)
521
  line_bot_api.reply_message_with_http_info(
522
  ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=fallback)])