cwadayi commited on
Commit
b12cac7
ยท
verified ยท
1 Parent(s): 9824e85
Files changed (1) hide show
  1. app.py +0 -523
app.py DELETED
@@ -1,523 +0,0 @@
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
10
- from datetime import datetime, timedelta, timezone
11
-
12
- from flask import Flask, request, abort, send_from_directory
13
-
14
- from linebot.v3 import WebhookHandler
15
- from linebot.v3.exceptions import InvalidSignatureError
16
- from linebot.v3.messaging import (
17
- Configuration, ApiClient, MessagingApi,
18
- ReplyMessageRequest, TextMessage
19
- )
20
- from linebot.v3.messaging.models import ImageMessage
21
- from linebot.v3.webhooks import MessageEvent, TextMessageContent
22
-
23
- import requests
24
- import pandas as pd
25
-
26
- # Matplotlib๏ผˆ็„ก้ ญ๏ผ‰
27
- import matplotlib
28
- matplotlib.use("Agg")
29
- import matplotlib.pyplot as plt
30
- from matplotlib.colors import Normalize
31
- import matplotlib.cm as cm
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:
46
- if "T" in s or s.endswith("Z") or "+" in s:
47
- dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
48
- else:
49
- dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
50
- dt = dt.replace(tzinfo=timezone(timedelta(hours=8)))
51
- tw = dt.astimezone(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M")
52
- utc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
53
- return (tw, utc)
54
- except Exception:
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()
61
- payload = r.json()
62
- except Exception as e:
63
- return f"โŒ ๅœฐ้œ‡้ ่ญฆๆŸฅ่ฉขๅคฑๆ•—๏ผš{e}"
64
-
65
- items = None
66
- if isinstance(payload, dict):
67
- items = payload.get("data") or payload.get("records") or payload.get("list") or payload.get("items")
68
- if items is None and isinstance(payload, list):
69
- items = payload
70
- if not items:
71
- return "โœ… ็›ฎๅ‰ๆฒ’ๆœ‰ๅœฐ้œ‡้ ่ญฆใ€‚"
72
-
73
- def _key(it):
74
- s = it.get("originTime") or ""
75
- try:
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)
83
-
84
- try:
85
- items = sorted(items, key=_key, reverse=True)
86
- except Exception:
87
- pass
88
-
89
- def _num(x):
90
- xs = str(x)
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 "โ€”"
98
- msg_type = it.get("msgType") or "โ€”"
99
- msg_no = it.get("msgNo") or it.get("msgSeq") 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
-
106
- lat = _num(it.get("epicenterLat") or it.get("latitude") or it.get("lat"))
107
- lon = _num(it.get("epicenterLon") or it.get("longitude") or it.get("lon"))
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):
115
- areas_txt = "ใ€".join(str(a) for a in areas if a)
116
- elif isinstance(areas, str):
117
- areas_txt = areas
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"
152
- _t.set_num_threads(max(1, int(os.getenv("LLM_THREADS", "1"))))
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:
163
- device = "cpu"
164
- mdl = mdl.to(device)
165
-
166
- _LLM.update({"ok": True, "model": mdl, "tokenizer": tok, "device": device})
167
- return True, None
168
- except Exception as e:
169
- _LLM["err"] = f"{e}"
170
- _LLM["ok"] = False
171
- return False, _LLM["err"]
172
-
173
- def generate_ai_text(user_prompt: str) -> str:
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"]
184
-
185
- max_new = int(os.getenv("LLM_MAX_NEW_TOKENS", "120"))
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(
199
- input_ids=inputs["input_ids"],
200
- attention_mask=inputs.get("attention_mask"),
201
- max_new_tokens=max_new,
202
- do_sample=True,
203
- top_k=top_k,
204
- temperature=temperature,
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",
221
- "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
222
- "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
223
- "/usr/share/fonts/truetype/arphic/ukai.ttc",
224
- ]:
225
- if os.path.exists(fp):
226
- matplotlib.rcParams["font.family"] = fm.FontProperties(fname=fp).get_name()
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"
241
- else:
242
- HF_SPACE_URL = ""
243
-
244
- STATIC_DIR = os.getenv("STATIC_DIR", os.path.join(tempfile.gettempdir(), "static"))
245
- os.makedirs(STATIC_DIR, exist_ok=True)
246
-
247
- app = Flask(__name__)
248
- configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
249
- handler = WebhookHandler(CHANNEL_SECRET)
250
-
251
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
252
- # ้ฆ–้ ๏ผˆๆŒ‡ไปค/Webhook/้œๆ…‹ๆช”่ชชๆ˜Ž๏ผ‰
253
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
254
- @app.route("/", methods=["GET"])
255
- def home():
256
- base = (os.getenv("SPACEURL") or request.url_root).rstrip("/")
257
- webhook_url = f"{base}/callback"
258
- static_hint = f"{base}/static/<ๆช”ๅ>"
259
- channel_ok = "โœ…" if os.getenv("CHANNEL_ACCESS_TOKEN") and os.getenv("CHANNEL_SECRET") else "โš ๏ธ"
260
- space_ok = "โœ…" if os.getenv("SPACEURL") or os.getenv("SPACE_ID") else "โ„น๏ธ"
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():
311
- return "ok"
312
-
313
- @app.route("/static/<path:filename>")
314
- def serve_static(filename):
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
-
322
- def _iso(dt: datetime) -> str:
323
- return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
324
-
325
- def fetch_global_last24h_text(min_mag=5.0, limit=10) -> str:
326
- now_utc = datetime.now(timezone.utc)
327
- since = now_utc - timedelta(hours=24)
328
- params = {
329
- "format": "geojson",
330
- "starttime": _iso(since),
331
- "endtime": _iso(now_utc),
332
- "minmagnitude": float(min_mag),
333
- "limit": int(limit),
334
- "orderby": "time",
335
- }
336
- try:
337
- r = requests.get(USGS_API_BASE_URL, params=params, timeout=15)
338
- r.raise_for_status()
339
- features = r.json().get("features", [])
340
- if not features:
341
- return "โœ… ้ŽๅŽป 24 ๅฐๆ™‚ๅ…ง๏ผŒๅ…จ็ƒ็„ก่ฆๆจก 5.0 ไปฅไธŠ็š„้กฏ่‘—ๅœฐ้œ‡ใ€‚"
342
- lines = ["๐Ÿšจ ่ฟ‘ 24 ๅฐๆ™‚ๅ…จ็ƒ้กฏ่‘—ๅœฐ้œ‡ (Mโ‰ฅ5.0):", "-" * 20]
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}"
350
-
351
- def fetch_taiwan_df_this_year(min_mag=5.0) -> pd.DataFrame | str:
352
- now_utc = datetime.now(timezone.utc)
353
- start_of_year_utc = datetime(now_utc.year, 1, 1, tzinfo=timezone.utc)
354
- params = {
355
- "format": "geojson", "starttime": _iso(start_of_year_utc), "endtime": _iso(now_utc),
356
- "minmagnitude": float(min_mag),
357
- "minlatitude": 21, "maxlatitude": 26,
358
- "minlongitude": 119, "maxlongitude": 123,
359
- "limit": 250, "orderby": "time",
360
- }
361
- try:
362
- r = requests.get(USGS_API_BASE_URL, params=params, timeout=20)
363
- r.raise_for_status()
364
- features = r.json().get("features", [])
365
- if not features:
366
- return f"โœ… ไปŠๅนด ({now_utc.year} ๅนด) ไปฅไพ†๏ผŒๅฐ็ฃๅ€ๅŸŸ็„ก Mโ‰ฅ{min_mag:.1f} ็š„้กฏ่‘—ๅœฐ้œ‡ใ€‚"
367
- rows = []
368
- for f in features:
369
- p = f["properties"]
370
- lon, lat, *_ = f["geometry"]["coordinates"]
371
- rows.append({
372
- "latitude": lat, "longitude": lon, "magnitude": p["mag"],
373
- "place": p.get("place", ""), "time_utc": datetime.fromtimestamp(p["time"]/1000, tz=timezone.utc)
374
- })
375
- return pd.DataFrame(rows)
376
- except Exception as 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)
384
- ax.set_xlim(118.5, 123.5)
385
- ax.set_ylim(20.5, 26.8)
386
- ax.set_xlabel("Longitude (ยฐE)")
387
- ax.set_ylabel("Latitude (ยฐN)")
388
- ax.set_title(f"ไปŠๅนด ({datetime.now(timezone.utc).year}) ๅฐ็ฃๅ€ๅŸŸ้กฏ่‘—ๅœฐ้œ‡ (Mโ‰ฅ5.0) โ€” UTC")
389
- ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4)
390
-
391
- mags = df["magnitude"].astype(float).clip(lower=0)
392
- norm = Normalize(vmin=max(4.5, mags.min()), vmax=max(6.5, mags.max()))
393
- cmap = cm.get_cmap("YlOrRd")
394
- colors = cmap(norm(mags.values))
395
- sizes = 15 + (mags - mags.min()) * 25
396
-
397
- ax.scatter(df["longitude"].values, df["latitude"].values,
398
- s=sizes, c=colors, edgecolor="k", linewidths=0.4, alpha=0.9)
399
-
400
- fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, pad=0.02).set_label("Magnitude")
401
-
402
- filename = f"map_{uuid.uuid4().hex}.png"
403
- filepath = os.path.join(STATIC_DIR, filename)
404
- fig.tight_layout()
405
- fig.savefig(filepath)
406
- plt.close(fig)
407
- return filename
408
-
409
- def _base_url_for_images() -> str:
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():
417
- signature = request.headers.get("X-Line-Signature")
418
- body = request.get_data(as_text=True)
419
- try:
420
- handler.handle(body, signature)
421
- except InvalidSignatureError:
422
- abort(400)
423
- return "OK"
424
-
425
- @handler.add(MessageEvent, message=TextMessageContent)
426
- def handle_message(event):
427
- user_message_raw = (event.message.text or "").strip()
428
- user_message = user_message_raw.lower()
429
-
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(
437
- ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=reply_text)])
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):
445
- filename = create_and_save_map(result)
446
- image_url = f"{_base_url_for_images()}/static/{filename}"
447
- reply = ReplyMessageRequest(
448
- reply_token=event.reply_token,
449
- messages=[
450
- TextMessage(text="๐Ÿ—บ๏ธ ๅทฒ็‚บๆ‚จ็นช่ฃฝไปŠๅนดๅฐ็ฃๅ€ๅŸŸ Mโ‰ฅ5.0 ๅœฐ้œ‡ๅˆ†ไฝˆๅœ–๏ผˆUTC๏ผ‰ใ€‚"),
451
- ImageMessage(original_content_url=image_url, preview_image_url=image_url),
452
- ],
453
- )
454
- else:
455
- reply = ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=result)])
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):
480
- count = len(result)
481
- lines = [f"๐Ÿ‡น๐Ÿ‡ผ ไปŠๅนด ({datetime.now(timezone.utc).year} ๅนด) ๅฐ็ฃๅ€ๅŸŸ้กฏ่‘—ๅœฐ้œ‡ (Mโ‰ฅ5.0)๏ผŒๅ…ฑ {count} ็ญ†:", "-" * 20]
482
- for _, row in result.head(15).iterrows():
483
- t = row["time_utc"].strftime("%Y-%m-%d %H:%M")
484
- lines.append(f"้œ‡็ดš: {row['magnitude']:.1f} | ๆ—ฅๆœŸๆ™‚้–“: {t} (UTC)\nๅœฐ้ปž: {row['place']}")
485
- if count > 15:
486
- lines.append(f"... (้‚„ๆœ‰ {count - 15} ็ญ†๏ผŒๅฏ็”จใ€Œ่‡บ็ฃๅœฐ้œ‡็•ซๅœ–ใ€ๆŸฅ็œ‹ๅ…จ้ƒจ)")
487
- reply_text = "\n\n".join(lines)
488
- else:
489
- reply_text = result
490
- line_bot_api.reply_message_with_http_info(
491
- ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=reply_text)])
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(
499
- ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=reply_text)])
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)])
523
- )