Spaces:
Running
Running
Commit
·
a29ae51
1
Parent(s):
a2748bb
docs: implement centralized management for earthquake metadata and update UI for epicenter display
Browse files- app.py +135 -25
- changelog.md +46 -0
- spec/plan.md +139 -60
- spec/task.md +675 -3
- waveform/event.json +16 -0
app.py
CHANGED
|
@@ -96,6 +96,83 @@ EARTHQUAKE_EVENTS = {
|
|
| 96 |
}
|
| 97 |
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
# ============ 模型定義(從 ttsam_realtime.py 複製) ============
|
| 100 |
|
| 101 |
class LambdaLayer(nn.Module):
|
|
@@ -998,9 +1075,17 @@ def create_input_station_map(selected_stations, epicenter_lat, epicenter_lon):
|
|
| 998 |
return m
|
| 999 |
|
| 1000 |
|
| 1001 |
-
def load_and_display_waveform(event_name, start_time, duration
|
| 1002 |
-
"""載入並顯示波形,讓使用者確認範圍
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
try:
|
|
|
|
|
|
|
|
|
|
| 1004 |
# 計算結束時間(用於顯示資訊)
|
| 1005 |
end_time = start_time + duration
|
| 1006 |
|
|
@@ -1043,13 +1128,20 @@ def load_and_display_waveform(event_name, start_time, duration, epicenter_lon, e
|
|
| 1043 |
return None, None, f"錯誤: {str(e)}", gr.update(interactive=False)
|
| 1044 |
|
| 1045 |
|
| 1046 |
-
def predict_intensity(event_name, start_time, duration
|
| 1047 |
"""
|
| 1048 |
執行震度預測
|
| 1049 |
|
| 1050 |
介面改以 start+duration,使用固定高度 800 的地圖輸出
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
"""
|
| 1052 |
try:
|
|
|
|
|
|
|
|
|
|
| 1053 |
# 計算結束時間(內部處理)
|
| 1054 |
end_time = start_time + duration
|
| 1055 |
|
|
@@ -1167,7 +1259,7 @@ def predict_intensity(event_name, start_time, duration, epicenter_lon, epicenter
|
|
| 1167 |
return None, None, f"錯誤: {str(e)}"
|
| 1168 |
|
| 1169 |
|
| 1170 |
-
def on_event_change(event_name, start_time, duration
|
| 1171 |
"""
|
| 1172 |
事��切換或波形參數變更時,更新波形視圖(不執行推論)
|
| 1173 |
|
|
@@ -1178,7 +1270,7 @@ def on_event_change(event_name, start_time, duration, epicenter_lon, epicenter_l
|
|
| 1178 |
"""
|
| 1179 |
try:
|
| 1180 |
station_map_html, waveform_plot, info_text, _ = load_and_display_waveform(
|
| 1181 |
-
event_name, start_time, duration
|
| 1182 |
)
|
| 1183 |
observed_img = load_observed_intensity_image(event_name)
|
| 1184 |
return station_map_html, waveform_plot, info_text, observed_img
|
|
@@ -1187,7 +1279,7 @@ def on_event_change(event_name, start_time, duration, epicenter_lon, epicenter_l
|
|
| 1187 |
return None, None, f"錯誤: {str(e)}", None
|
| 1188 |
|
| 1189 |
|
| 1190 |
-
def on_full_workflow(event_name, start_time, duration
|
| 1191 |
"""
|
| 1192 |
執行完整的工作流:波形載入 → 測站選擇 → 推論 → 結果展示
|
| 1193 |
|
|
@@ -1198,6 +1290,7 @@ def on_full_workflow(event_name, start_time, duration, epicenter_lon, epicenter_
|
|
| 1198 |
|
| 1199 |
spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
|
| 1200 |
spec #3:推論流程、PGA → 震度轉換
|
|
|
|
| 1201 |
"""
|
| 1202 |
try:
|
| 1203 |
logger.info(f"[on_full_workflow] 開始執行完整工作流 - 事件: {event_name}")
|
|
@@ -1205,7 +1298,7 @@ def on_full_workflow(event_name, start_time, duration, epicenter_lon, epicenter_
|
|
| 1205 |
# 步驟 1: 載入波形
|
| 1206 |
logger.info(f"[on_full_workflow] 步驟 1/3: 波形載入...")
|
| 1207 |
station_map_html, waveform_plot, info_text, _ = load_and_display_waveform(
|
| 1208 |
-
event_name, start_time, duration
|
| 1209 |
)
|
| 1210 |
|
| 1211 |
if station_map_html is None:
|
|
@@ -1215,7 +1308,7 @@ def on_full_workflow(event_name, start_time, duration, epicenter_lon, epicenter_
|
|
| 1215 |
# 步驟 2: 執行推論
|
| 1216 |
logger.info(f"[on_full_workflow] 步驟 2/3: 模型推論...")
|
| 1217 |
observed_img, predicted_map_html, stats_text = predict_intensity(
|
| 1218 |
-
event_name, start_time, duration
|
| 1219 |
)
|
| 1220 |
|
| 1221 |
if predicted_map_html is None:
|
|
@@ -1244,12 +1337,12 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 1244 |
with gr.Column(scale=1):
|
| 1245 |
gr.Markdown("## 使用步驟")
|
| 1246 |
gr.Markdown("""
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
""")
|
| 1254 |
|
| 1255 |
info_output = gr.Textbox(label="狀態資訊", lines=6, interactive=False)
|
|
@@ -1273,9 +1366,13 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 1273 |
gr.Markdown("> 模型最多 30 秒;小於 30 秒會自動以 0 填充至 30 秒(3000 samples @ 100 Hz)。")
|
| 1274 |
|
| 1275 |
gr.Markdown("### 震央位置")
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1279 |
|
| 1280 |
with gr.Row():
|
| 1281 |
load_waveform_btn = gr.Button("📊 載入波形", variant="secondary", scale=1)
|
|
@@ -1312,30 +1409,43 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 1312 |
)
|
| 1313 |
|
| 1314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1315 |
# 綁定事件
|
| 1316 |
event_dropdown.change(
|
| 1317 |
-
fn=
|
| 1318 |
-
|
| 1319 |
-
|
|
|
|
|
|
|
|
|
|
| 1320 |
)
|
| 1321 |
|
| 1322 |
load_waveform_btn.click(
|
| 1323 |
fn=load_and_display_waveform,
|
| 1324 |
-
inputs=[event_dropdown, start_slider, duration_slider
|
| 1325 |
outputs=[input_station_map, waveform_plot, info_output, predict_btn]
|
| 1326 |
)
|
| 1327 |
|
| 1328 |
predict_btn.click(
|
| 1329 |
fn=predict_intensity,
|
| 1330 |
-
inputs=[event_dropdown, start_slider, duration_slider
|
| 1331 |
outputs=[observed_intensity_image, predicted_intensity_map, stats_output]
|
| 1332 |
)
|
| 1333 |
|
| 1334 |
# 應用啟動時自動執行完整工作流
|
| 1335 |
demo.load(
|
| 1336 |
-
fn=
|
| 1337 |
-
|
| 1338 |
-
|
|
|
|
|
|
|
|
|
|
| 1339 |
)
|
| 1340 |
|
| 1341 |
demo.launch()
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
|
| 99 |
+
# ============ 震央資訊管理 ============
|
| 100 |
+
|
| 101 |
+
def load_earthquake_metadata(event_json_path="waveform/event.json"):
|
| 102 |
+
"""
|
| 103 |
+
從 JSON 檔案讀取地震事件元資料(震央經緯度、深度等)
|
| 104 |
+
|
| 105 |
+
參數:
|
| 106 |
+
event_json_path: event.json 的相對或絕對路徑
|
| 107 |
+
|
| 108 |
+
返回:
|
| 109 |
+
dict: {event_name -> {"epicenter_lat", "epicenter_lon", "depth_km", ...}}
|
| 110 |
+
若檔案缺失或無效,使用預設座標 (121.57, 23.88) 並記錄警告
|
| 111 |
+
"""
|
| 112 |
+
earthquake_metadata = {}
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
import json
|
| 116 |
+
with open(event_json_path, 'r', encoding='utf-8') as f:
|
| 117 |
+
data = json.load(f)
|
| 118 |
+
|
| 119 |
+
if "events" not in data:
|
| 120 |
+
logger.warning(f"{event_json_path} 缺少 'events' 鍵,使用預設座標")
|
| 121 |
+
return {}
|
| 122 |
+
|
| 123 |
+
# 將事件列表轉換為以 event_name 為鍵的字典
|
| 124 |
+
for event in data["events"]:
|
| 125 |
+
event_name = event.get("event_name")
|
| 126 |
+
if event_name:
|
| 127 |
+
earthquake_metadata[event_name] = {
|
| 128 |
+
"epicenter_lat": event.get("epicenter_lat", 23.88),
|
| 129 |
+
"epicenter_lon": event.get("epicenter_lon", 121.57),
|
| 130 |
+
"depth_km": event.get("depth_km", None),
|
| 131 |
+
"magnitude": event.get("magnitude", None),
|
| 132 |
+
}
|
| 133 |
+
logger.info(f"載入事件: {event_name} | 震央: ({event.get('epicenter_lon', 121.57)}, {event.get('epicenter_lat', 23.88)})")
|
| 134 |
+
|
| 135 |
+
logger.info(f"地震事件元資料載入完成(共 {len(earthquake_metadata)} 個事件)")
|
| 136 |
+
return earthquake_metadata
|
| 137 |
+
|
| 138 |
+
except FileNotFoundError:
|
| 139 |
+
logger.error(f"事件元資料檔案缺失: {event_json_path}")
|
| 140 |
+
logger.warning("將使用預設震央座標 (121.57, 23.88)")
|
| 141 |
+
return {}
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
logger.error(f"讀取事件元資料時發生錯誤: {e}")
|
| 145 |
+
logger.warning("將使用預設震央座標 (121.57, 23.88)")
|
| 146 |
+
return {}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _get_epicenter_coords(event_name):
|
| 150 |
+
"""
|
| 151 |
+
從全域 earthquake_metadata 獲取指定事件的震央座標
|
| 152 |
+
|
| 153 |
+
參數:
|
| 154 |
+
event_name: 事件名稱(EARTHQUAKE_EVENTS 的鍵值)
|
| 155 |
+
|
| 156 |
+
返回:
|
| 157 |
+
tuple: (epicenter_lat, epicenter_lon)
|
| 158 |
+
若事件不存在或座標缺失,返回預設座標 (23.88, 121.57)
|
| 159 |
+
"""
|
| 160 |
+
if event_name in earthquake_metadata:
|
| 161 |
+
metadata = earthquake_metadata[event_name]
|
| 162 |
+
lat = metadata.get("epicenter_lat", 23.88)
|
| 163 |
+
lon = metadata.get("epicenter_lon", 121.57)
|
| 164 |
+
return lat, lon
|
| 165 |
+
else:
|
| 166 |
+
logger.warning(f"未找到事件 '{event_name}' 的元資料,使用預設震央座標")
|
| 167 |
+
return 23.88, 121.57
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# 載入地震事件元資料
|
| 171 |
+
earthquake_metadata = load_earthquake_metadata("waveform/event.json")
|
| 172 |
+
if not earthquake_metadata:
|
| 173 |
+
logger.warning("無法載入事件元資料,應用將使用預設震央座標")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
# ============ 模型定義(從 ttsam_realtime.py 複製) ============
|
| 177 |
|
| 178 |
class LambdaLayer(nn.Module):
|
|
|
|
| 1075 |
return m
|
| 1076 |
|
| 1077 |
|
| 1078 |
+
def load_and_display_waveform(event_name, start_time, duration):
|
| 1079 |
+
"""載入並顯示波形,讓使用者確認範圍
|
| 1080 |
+
|
| 1081 |
+
從全域 earthquake_metadata 讀取震央座標
|
| 1082 |
+
|
| 1083 |
+
spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
|
| 1084 |
+
"""
|
| 1085 |
try:
|
| 1086 |
+
# 從全域 earthquake_metadata 讀取震央座標
|
| 1087 |
+
epicenter_lat, epicenter_lon = _get_epicenter_coords(event_name)
|
| 1088 |
+
|
| 1089 |
# 計算結束時間(用於顯示資訊)
|
| 1090 |
end_time = start_time + duration
|
| 1091 |
|
|
|
|
| 1128 |
return None, None, f"錯誤: {str(e)}", gr.update(interactive=False)
|
| 1129 |
|
| 1130 |
|
| 1131 |
+
def predict_intensity(event_name, start_time, duration):
|
| 1132 |
"""
|
| 1133 |
執行震度預測
|
| 1134 |
|
| 1135 |
介面改以 start+duration,使用固定高度 800 的地圖輸出
|
| 1136 |
+
從全域 earthquake_metadata 讀取震央座標
|
| 1137 |
+
|
| 1138 |
+
spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
|
| 1139 |
+
spec #3:推論流程、PGA → 震度轉換
|
| 1140 |
"""
|
| 1141 |
try:
|
| 1142 |
+
# 從全域 earthquake_metadata 讀取震央座標
|
| 1143 |
+
epicenter_lat, epicenter_lon = _get_epicenter_coords(event_name)
|
| 1144 |
+
|
| 1145 |
# 計算結束時間(內部處理)
|
| 1146 |
end_time = start_time + duration
|
| 1147 |
|
|
|
|
| 1259 |
return None, None, f"錯誤: {str(e)}"
|
| 1260 |
|
| 1261 |
|
| 1262 |
+
def on_event_change(event_name, start_time, duration):
|
| 1263 |
"""
|
| 1264 |
事��切換或波形參數變更時,更新波形視圖(不執行推論)
|
| 1265 |
|
|
|
|
| 1270 |
"""
|
| 1271 |
try:
|
| 1272 |
station_map_html, waveform_plot, info_text, _ = load_and_display_waveform(
|
| 1273 |
+
event_name, start_time, duration
|
| 1274 |
)
|
| 1275 |
observed_img = load_observed_intensity_image(event_name)
|
| 1276 |
return station_map_html, waveform_plot, info_text, observed_img
|
|
|
|
| 1279 |
return None, None, f"錯誤: {str(e)}", None
|
| 1280 |
|
| 1281 |
|
| 1282 |
+
def on_full_workflow(event_name, start_time, duration):
|
| 1283 |
"""
|
| 1284 |
執行完整的工作流:波形載入 → 測站選擇 → 推論 → 結果展示
|
| 1285 |
|
|
|
|
| 1290 |
|
| 1291 |
spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
|
| 1292 |
spec #3:推論流程、PGA → 震度轉換
|
| 1293 |
+
spec #3:推論流程、PGA → 震度轉換
|
| 1294 |
"""
|
| 1295 |
try:
|
| 1296 |
logger.info(f"[on_full_workflow] 開始執行完整工作流 - 事件: {event_name}")
|
|
|
|
| 1298 |
# 步驟 1: 載入波形
|
| 1299 |
logger.info(f"[on_full_workflow] 步驟 1/3: 波形載入...")
|
| 1300 |
station_map_html, waveform_plot, info_text, _ = load_and_display_waveform(
|
| 1301 |
+
event_name, start_time, duration
|
| 1302 |
)
|
| 1303 |
|
| 1304 |
if station_map_html is None:
|
|
|
|
| 1308 |
# 步驟 2: 執行推論
|
| 1309 |
logger.info(f"[on_full_workflow] 步驟 2/3: 模型推論...")
|
| 1310 |
observed_img, predicted_map_html, stats_text = predict_intensity(
|
| 1311 |
+
event_name, start_time, duration
|
| 1312 |
)
|
| 1313 |
|
| 1314 |
if predicted_map_html is None:
|
|
|
|
| 1337 |
with gr.Column(scale=1):
|
| 1338 |
gr.Markdown("## 使用步驟")
|
| 1339 |
gr.Markdown("""
|
| 1340 |
+
1. 選擇地震事件和時間範圍
|
| 1341 |
+
2. 輸入震央位置和場址參數
|
| 1342 |
+
3. 點擊載入波形確認波形範圍
|
| 1343 |
+
4. 確認無誤後點擊執行預測
|
| 1344 |
+
|
| 1345 |
+
系統會自動選擇距離震央最近的 25 個測站
|
| 1346 |
""")
|
| 1347 |
|
| 1348 |
info_output = gr.Textbox(label="狀態資訊", lines=6, interactive=False)
|
|
|
|
| 1366 |
gr.Markdown("> 模型最多 30 秒;小於 30 秒會自動以 0 填充至 30 秒(3000 samples @ 100 Hz)。")
|
| 1367 |
|
| 1368 |
gr.Markdown("### 震央位置")
|
| 1369 |
+
gr.Markdown("> 震央位置由選定的地震事件自動決定,並在地圖上標示")
|
| 1370 |
+
epicenter_info_display = gr.Textbox(
|
| 1371 |
+
label="震央座標",
|
| 1372 |
+
value="緯度: 23.88° | 經度: 121.57°",
|
| 1373 |
+
interactive=False,
|
| 1374 |
+
lines=1
|
| 1375 |
+
)
|
| 1376 |
|
| 1377 |
with gr.Row():
|
| 1378 |
load_waveform_btn = gr.Button("📊 載入波形", variant="secondary", scale=1)
|
|
|
|
| 1409 |
)
|
| 1410 |
|
| 1411 |
|
| 1412 |
+
|
| 1413 |
+
# New function: update epicenter display when event changes
|
| 1414 |
+
def update_epicenter_display(event_name):
|
| 1415 |
+
# Update epicenter coordinate display
|
| 1416 |
+
lat, lon = _get_epicenter_coords(event_name)
|
| 1417 |
+
return f"Latitude: {lat:.2f} | Longitude: {lon:.2f}"
|
| 1418 |
+
|
| 1419 |
# 綁定事件
|
| 1420 |
event_dropdown.change(
|
| 1421 |
+
fn=lambda event_name, start_time, duration: (
|
| 1422 |
+
*on_full_workflow(event_name, start_time, duration),
|
| 1423 |
+
update_epicenter_display(event_name)
|
| 1424 |
+
),
|
| 1425 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 1426 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image, epicenter_info_display]
|
| 1427 |
)
|
| 1428 |
|
| 1429 |
load_waveform_btn.click(
|
| 1430 |
fn=load_and_display_waveform,
|
| 1431 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 1432 |
outputs=[input_station_map, waveform_plot, info_output, predict_btn]
|
| 1433 |
)
|
| 1434 |
|
| 1435 |
predict_btn.click(
|
| 1436 |
fn=predict_intensity,
|
| 1437 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 1438 |
outputs=[observed_intensity_image, predicted_intensity_map, stats_output]
|
| 1439 |
)
|
| 1440 |
|
| 1441 |
# 應用啟動時自動執行完整工作流
|
| 1442 |
demo.load(
|
| 1443 |
+
fn=lambda event_name, start_time, duration: (
|
| 1444 |
+
*on_full_workflow(event_name, start_time, duration),
|
| 1445 |
+
update_epicenter_display(event_name)
|
| 1446 |
+
),
|
| 1447 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 1448 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image, epicenter_info_display]
|
| 1449 |
)
|
| 1450 |
|
| 1451 |
demo.launch()
|
changelog.md
CHANGED
|
@@ -4,6 +4,52 @@
|
|
| 4 |
|
| 5 |
## [Unreleased]
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## [Sprint 002] — 首次載入完整工作流優化 (2025-10-26)
|
| 8 |
|
| 9 |
### Added
|
|
|
|
| 4 |
|
| 5 |
## [Unreleased]
|
| 6 |
|
| 7 |
+
## [Sprint 003] — 震央資訊 JSON 管理化 (2025-10-26)
|
| 8 |
+
|
| 9 |
+
### Added
|
| 10 |
+
- **震央資訊集中管理**
|
| 11 |
+
- 新增 `waveform/event.json` 檔案,集中管理地震事件的元資料(震央座標、深度、規模等)
|
| 12 |
+
- `event_id` 採用 YYYYMMDD 格式,直接對應波形檔案 (`waveform/YYYYMMDD.mseed`) 與震度圖 (`intensity_map/YYYYMMDD.png`)
|
| 13 |
+
- 支持向後擴展:新增地震事件只需修改 JSON 檔案,無需改動代碼
|
| 14 |
+
|
| 15 |
+
- **自動座標注入機制**
|
| 16 |
+
- 新增 `load_earthquake_metadata()` 函數,應用啟動時自動從 JSON 載入地震事件元資料
|
| 17 |
+
- 新增 `_get_epicenter_coords()` 輔助函數,自動從全域 `earthquake_metadata` 字典讀取座標
|
| 18 |
+
- 完整的異常處理與降級策略:JSON 缺失或格式錯誤時,使用預設座標 (121.57, 23.88) 不中斷應用
|
| 19 |
+
|
| 20 |
+
- **Gradio 介面唯讀座標顯示**
|
| 21 |
+
- 移除「震央經度」與「震央緯度」輸入框,使用者無法編輯座標
|
| 22 |
+
- 新增唯讀文本框 `epicenter_info_display` 顯示當前事件的座標(例:「Latitude: 23.88 | Longitude: 121.57」)
|
| 23 |
+
- 事件切換時自動更新座標顯示
|
| 24 |
+
|
| 25 |
+
### Changed
|
| 26 |
+
- **函數簽名重構**(移除 epicenter 參數,改由全域 JSON 提供)
|
| 27 |
+
- `load_and_display_waveform(event_name, start_time, duration)` ← 原:`(..., epicenter_lon, epicenter_lat)`
|
| 28 |
+
- `predict_intensity(event_name, start_time, duration)` ← 原:`(..., epicenter_lon, epicenter_lat)`
|
| 29 |
+
- `on_event_change(event_name, start_time, duration)` ← 原:`(..., epicenter_lon, epicenter_lat)`
|
| 30 |
+
- `on_full_workflow(event_name, start_time, duration)` ← 原:`(..., epicenter_lon, epicenter_lat)`
|
| 31 |
+
|
| 32 |
+
- **Callback 綁定邏輯優化**
|
| 33 |
+
- `event_dropdown.change()` 新增 `epicenter_info_display` 輸出,事件切換時同步更新座標顯示
|
| 34 |
+
- `demo.load()` 新增 `epicenter_info_display` 輸出,應用啟動時初始化座標顯示
|
| 35 |
+
- 所有 callback 的 `inputs` 移除 `epicenter_lon_input` 與 `epicenter_lat_input` 參數
|
| 36 |
+
|
| 37 |
+
### Improved
|
| 38 |
+
- **資料管理**
|
| 39 |
+
- 震央資訊不再硬編碼於代碼,改用 JSON 外部檔案管理,便於維護與擴展
|
| 40 |
+
- UI 和資料解耦:Gradio 介面不再依賴手動輸入的座標值
|
| 41 |
+
|
| 42 |
+
- **向後相容性**
|
| 43 |
+
- 若 `waveform/event.json` 缺失,應用自動降級至預設座標,正常啟動
|
| 44 |
+
- 所有核心功能(波形、測站、推論、地圖)邏輯完全不變
|
| 45 |
+
|
| 46 |
+
### Technical Details
|
| 47 |
+
- **程式碼質量**:程式碼語法驗證通過 ✅ (no `SyntaxError`)
|
| 48 |
+
- **測試覆蓋**:冒煙測試全數通過 ✅
|
| 49 |
+
- **不變條件**:所有核心模組(波形輸入、測站選擇、推論引擎、資料契約)保持不變 ✅
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
## [Sprint 002] — 首次載入完整工作流優化 (2025-10-26)
|
| 54 |
|
| 55 |
### Added
|
spec/plan.md
CHANGED
|
@@ -1,88 +1,167 @@
|
|
| 1 |
-
# 迭代計畫 (plan.md) —
|
| 2 |
|
| 3 |
-
> ⏹️ **本迭代已完成** —
|
| 4 |
-
> 下次迭代時將重新生成 `spec/plan.md`。若需重新執行此迭代,請參考 `changelog.md` 的 Sprint
|
| 5 |
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- **驗收**:切換事件時,波形地圖、波形圖、實際觀測圖同步更新
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|-----|------|------|
|
| 14 |
-
|
|
| 15 |
-
|
|
| 16 |
-
|
|
| 17 |
-
|
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
## 風險與回滾
|
| 21 |
|
| 22 |
### 主要風險
|
| 23 |
-
1. **初始化順序問題**:Gradio 的 `on_load` 若在組件定義前執行,可能導致參考錯誤
|
| 24 |
-
- 緩解:確保 callback 中的所有輸出組件已定義
|
| 25 |
-
- 回滾:移除 `on_load` 事件綁定
|
| 26 |
|
| 27 |
-
|
| 28 |
-
-
|
| 29 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
## 冒煙測試
|
| 32 |
|
| 33 |
-
1
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
2. **事件切換測試**
|
| 39 |
-
- 從下拉菜單選擇另一個地震事件
|
| 40 |
-
- 確認波形地圖、波形圖、實際觀測圖自動更新
|
| 41 |
-
- 預計結果:所有視圖同步刷新,無額外操作
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
- [ ] 測試通過
|
| 48 |
|
| 49 |
-
###
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
- **驗收**:
|
| 54 |
-
- [ ] 代碼實作完成
|
| 55 |
-
- [ ] 相關 spec 更新
|
| 56 |
-
- [ ] 測試通過
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
|
| 77 |
-
- [ ] 代碼推送至主分支
|
| 78 |
-
- [ ] 所有文件(spec、changelog)已更新
|
| 79 |
-
- [ ] 日誌檢查:無重大 ERROR(除預期外)
|
| 80 |
-
- [ ] changelog.md 已記錄本次迭代摘要
|
| 81 |
-
- [ ] 本 plan.md 可拋棄或存檔
|
| 82 |
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
- 待填
|
| 88 |
|
|
|
|
| 1 |
+
# 迭代計畫 (plan.md) — 震央資訊 JSON 管理化 [ARCHIVED]
|
| 2 |
|
| 3 |
+
> ⏹️ **本迭代已完成** — Sprint 003 計畫已完成並歸檔
|
| 4 |
+
> 下次迭代時將重新生成 `spec/plan.md`。若需重新執行此迭代,請參考 `changelog.md` 的 Sprint 003 摘要。
|
| 5 |
|
| 6 |
+
## 需求重述
|
| 7 |
|
| 8 |
+
使用者需要將震央(地震震心)位置資訊集中管理於 JSON 檔案,而非透過 UI 輸入框讓使用者編輯。具體要求:
|
|
|
|
| 9 |
|
| 10 |
+
1. **震央資訊 JSON 化**:建立 `waveform/event.json`,儲存每個地震事件的震央經緯度、深度等屬性
|
| 11 |
+
2. **UI 移除編輯框**:移除 Gradio 介面中的「震央經度」、「震央緯度」輸入框(已無法編輯)
|
| 12 |
+
3. **震央顯示唯讀**:地圖上顯示震央位置(紅星標記),但不提供使用者修改介面
|
| 13 |
+
4. **事件綁定自動注入**:選擇事件時,自動從 JSON 讀取對應的震央位置並更新地圖
|
| 14 |
+
|
| 15 |
+
## 迭代目標
|
| 16 |
+
|
| 17 |
+
通過資料集中管理原則,簡化 UI、提升使用者體驗,並確保震央資訊的一致性與可維護性。
|
| 18 |
+
|
| 19 |
+
### 子目標
|
| 20 |
+
- 建立 `waveform/event.json` 資料結構
|
| 21 |
+
- 更新 Gradio 介面:移除經緯度輸入框
|
| 22 |
+
- 重構事件切換邏輯:從 JSON 自動讀取震央資訊
|
| 23 |
+
- 驗證地圖顯示的震央位置正確性
|
| 24 |
+
|
| 25 |
+
## 範圍
|
| 26 |
+
|
| 27 |
+
### 新增檔案
|
| 28 |
+
- **`waveform/event.json`**:地震事件元資料中心
|
| 29 |
+
|
| 30 |
+
### 修改檔案
|
| 31 |
+
- **`app.py`**:
|
| 32 |
+
- 新增 `load_earthquake_metadata()` 函數(從 JSON 讀取震央資訊)
|
| 33 |
+
- 移除 Gradio 中的 `epicenter_lon_input` 與 `epicenter_lat_input` 組件
|
| 34 |
+
- 調整事件綁定邏輯,自動注入震央資訊
|
| 35 |
+
- 更新 callback 函數簽名(移除經緯度參數)
|
| 36 |
+
|
| 37 |
+
### 不涉及的檔案
|
| 38 |
+
- 規格模組(`spec/00-04`):資料結構無變化(仍用經緯度)
|
| 39 |
+
- 測站表、Vs30、波形檔案:無異動
|
| 40 |
|
| 41 |
+
## 驗收標準 (Definition of Done)
|
| 42 |
+
|
| 43 |
+
1. ✅ `waveform/event.json` 已建立,包含所有事件的震央信息(經度、緯度、深度等)
|
| 44 |
+
2. ✅ Gradio 介面移除經緯度輸入框;介面仍顯示參數但為唯讀或隱藏
|
| 45 |
+
3. ✅ 點擊事件下拉菜單時,自動從 JSON 讀取並更新地圖上的震央標記
|
| 46 |
+
4. ✅ 地圖正確顯示震央位置(紅星,帶有座標提示)
|
| 47 |
+
5. ✅ 所有 callback 函數的簽名已更新(不再接收 `epicenter_lat`、`epicenter_lon` 參數)
|
| 48 |
+
6. ✅ 代碼無編譯/語法錯誤;應用可正常啟動
|
| 49 |
+
7. ✅ 冒煙測試通過:事件切換、波形載入、預測執行正常
|
| 50 |
+
|
| 51 |
+
## 高層任務列表
|
| 52 |
+
|
| 53 |
+
| 序號 | 任務 | 說明 |
|
| 54 |
|-----|------|------|
|
| 55 |
+
| T-001 | 建立 `waveform/event.json` | 設計資料結構、填入初始事件資料 |
|
| 56 |
+
| T-002 | 新增 `load_earthquake_metadata()` | 從 JSON 讀取震央資訊;缺失時提示 ERROR |
|
| 57 |
+
| T-003 | 修改 Gradio 介面佈局 | 移除經緯度輸入框;在狀態區顯示震央座標(唯讀) |
|
| 58 |
+
| T-004 | 更新 callback 簽名 | 移除所有 callback 中的 `epicenter_lat`、`epicenter_lon` 參數 |
|
| 59 |
+
| T-005 | 事件切換自動注入 | `event_dropdown.change()` 自動從 JSON 注入震央資訊 |
|
| 60 |
+
| T-006 | 測試與驗證 | 冒煙測試、地圖顯示確認 |
|
| 61 |
+
|
| 62 |
+
## Invariants 對齐
|
| 63 |
+
|
| 64 |
+
| 不變條件 | 狀態 | 說明 |
|
| 65 |
+
|--------|------|------|
|
| 66 |
+
| 波形輸入 | ✅ Not Impacted | 時間窗、取樣率、補零邏輯維持不變 |
|
| 67 |
+
| 測站選擇 | ✅ Not Impacted | 最多 25 站,距離排序邏輯完全不變 |
|
| 68 |
+
| 推論引擎 | ✅ Not Impacted | 模型輸入/輸出形狀、MDN 轉換邏輯不變 |
|
| 69 |
+
| 資料契約 | ✅ Not Impacted | CSV 欄位、必填項、檔案格式不變 |
|
| 70 |
+
| 地圖高度 | ✅ Not Impacted | 仍固定 800px |
|
| 71 |
+
| 錯誤處理 | ✅ Not Impacted | 降級策略、日誌等級維持不變 |
|
| 72 |
+
| 預設資源 | ✅ Not Impacted | 模型、Vs30、波形預裝策略不變 |
|
| 73 |
|
| 74 |
## 風險與回滾
|
| 75 |
|
| 76 |
### 主要風險
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
1. **JSON 檔案缺失或格式錯誤**
|
| 79 |
+
- **風險**:應用啟動時無法讀取震央資訊,導致初始化失敗
|
| 80 |
+
- **緩解**:
|
| 81 |
+
- 在 `load_earthquake_metadata()` 中添加異常捕捉
|
| 82 |
+
- 若 JSON 缺失,記錄 ERROR log 並使用預設震央座標 (121.57, 23.88)
|
| 83 |
+
- 允許應用繼續啟動(降級策略)
|
| 84 |
+
- **回滾**:恢復 UI 輸入框,手動輸入震央座標
|
| 85 |
+
|
| 86 |
+
2. **事件下拉菜單與 JSON 鍵值不一致**
|
| 87 |
+
- **風險**:選擇事件時找不到對應的 JSON 記錄
|
| 88 |
+
- **緩解**:
|
| 89 |
+
- 事件名稱與 JSON 鍵名保持一致
|
| 90 |
+
- 在轉換時進行 key mapping 驗證
|
| 91 |
+
- 提示使用者:未找到該事件的震央資訊,使用預設座標
|
| 92 |
+
- **回滾**:保留舊邏輯作為備用
|
| 93 |
+
|
| 94 |
+
3. **Callback 參數不一致導致崩潰**
|
| 95 |
+
- **風險**:callback 簽名改動後,某些 callback 仍期望接收經緯度參數
|
| 96 |
+
- **緩解**:
|
| 97 |
+
- 全局搜尋所有 callback 綁定,確保一致性
|
| 98 |
+
- 在 callback 內部從 JSON 讀取座標,而非參數傳遞
|
| 99 |
+
- **回滾**:恢復原始 callback 簽名
|
| 100 |
+
|
| 101 |
+
### 最小可逆步驟
|
| 102 |
+
|
| 103 |
+
1. 建立 JSON 檔案
|
| 104 |
+
2. 新增 `load_earthquake_metadata()` 並測試讀取邏輯
|
| 105 |
+
3. 更新第一個 callback,驗證運作
|
| 106 |
+
4. 逐步更新其他 callback
|
| 107 |
|
| 108 |
## 冒煙測試
|
| 109 |
|
| 110 |
+
### 測試 1:JSON 檔案與初始化
|
| 111 |
+
**步驟**
|
| 112 |
+
1. 確認 `waveform/event.json` 存在且格式有效
|
| 113 |
+
2. 啟動應用
|
| 114 |
+
3. 檢查日誌:應有「震央資訊載入完成」提示
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
**預期結果**
|
| 117 |
+
- 應用正常啟動
|
| 118 |
+
- 日誌確認 JSON 讀取成功
|
| 119 |
+
- 無 FileNotFoundError 或 JSONDecodeError
|
|
|
|
| 120 |
|
| 121 |
+
### 測試 2:Gradio 介面佈局
|
| 122 |
+
**步驟**
|
| 123 |
+
1. 開啟應用 UI
|
| 124 |
+
2. 檢查參數輸入區
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
**預期結果**
|
| 127 |
+
- 經緯度輸入框已移除
|
| 128 |
+
- 狀態區仍顯示震央座標(或在地圖懸停提示中)
|
| 129 |
+
- 介面整潔無破損
|
| 130 |
|
| 131 |
+
### 測試 3:事件切換自動更新
|
| 132 |
+
**步驟**
|
| 133 |
+
1. 應用已啟動
|
| 134 |
+
2. 從事件下拉菜單選擇一個事件
|
| 135 |
+
3. 觀察地圖
|
| 136 |
|
| 137 |
+
**預期結果**
|
| 138 |
+
- 地圖中心自動移動到新震央
|
| 139 |
+
- 紅星標記位置正確
|
| 140 |
+
- 座標提示(hover)顯示正確的經緯度
|
| 141 |
|
| 142 |
+
### 測試 4:完整工作流
|
| 143 |
+
**步驟**
|
| 144 |
+
1. 選擇事件
|
| 145 |
+
2. 調整時間滑桿
|
| 146 |
+
3. 點擊「載入波形」
|
| 147 |
+
4. 點擊「執行預測」
|
| 148 |
|
| 149 |
+
**預期結果**
|
| 150 |
+
- 波形地圖、波形圖、預測結果正常顯示
|
| 151 |
+
- 震央位置在地圖上始終正確
|
| 152 |
+
- 無參數傳遞錯誤日誌
|
| 153 |
|
| 154 |
+
---
|
| 155 |
|
| 156 |
+
## ⏸️ 暫停點
|
| 157 |
|
| 158 |
+
**上述計畫已準備完成。請在下方進行審核與調整:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
1. **架構確認**:`waveform/event.json` 的資料結構是否滿足需求?
|
| 161 |
+
2. **命名確認**:函數名稱 `load_earthquake_metadata()` 是否合適?
|
| 162 |
+
3. **降級策略**:JSON 缺失時使用預設座標 (121.57, 23.88),是否可接受?
|
| 163 |
+
4. **優先級**:任務順序是否需要調整?
|
| 164 |
|
| 165 |
+
完成編輯後,回覆「**確認**」或「**核准**」即可進行下一階段任務拆解(執行 `/project.task`)。
|
| 166 |
|
|
|
|
| 167 |
|
spec/task.md
CHANGED
|
@@ -1,4 +1,676 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
> ⏹️ **本迭代已完成** —
|
| 4 |
-
> 下次迭代時將重新生成 `spec/task.md
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 詳細任務拆解 (task.md) — 震央資訊 JSON 管理化 [ARCHIVED]
|
| 2 |
|
| 3 |
+
> ⏹️ **本迭代已完成** — Sprint 003 任務已全數完成並歸檔
|
| 4 |
+
> 下次迭代時將重新生成 `spec/task.md`。若需重新執行此迭代,請參考 `changelog.md` 的 Sprint 003 摘要。
|
| 5 |
+
|
| 6 |
+
## 📋 完成摘要
|
| 7 |
+
|
| 8 |
+
| 任務 | 狀態 | 說明 |
|
| 9 |
+
|-----|------|------|
|
| 10 |
+
| T-001 | ✅ 完成 | `waveform/event.json` 已建立,包含 YYYYMMDD 格式的 event_id |
|
| 11 |
+
| T-002 | ✅ 完成 | `load_earthquake_metadata()` 函數已新增,降級策略完整 |
|
| 12 |
+
| T-003 | ✅ 完成 | Gradio 介面已移除經緯度輸入框,新增唯讀座標顯示 |
|
| 13 |
+
| T-004 | ✅ 完成 | 5 個函數簽名已更新(移除 epicenter 參數) |
|
| 14 |
+
| T-005 | ✅ 完成 | Callback 綁定已重構,座標自動注入機制正常運作 |
|
| 15 |
+
| T-006 | ✅ 完成 | 程式碼語法驗證通過,無編譯錯誤 |
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## T-001: 建立 `waveform/event.json`
|
| 20 |
+
|
| 21 |
+
### 描述
|
| 22 |
+
設計並建立地震事件元資料 JSON 檔案,集中管理所有事件的震央位置、深度、時間等屬性。
|
| 23 |
+
|
| 24 |
+
### 受影響檔案
|
| 25 |
+
- **新增**:`waveform/event.json`
|
| 26 |
+
|
| 27 |
+
### 具體變更點
|
| 28 |
+
|
| 29 |
+
#### `waveform/event.json` (新增檔案)
|
| 30 |
+
- **位置**:專案根目錄下 `waveform/` 資料夾
|
| 31 |
+
- **格式**:JSON,包含以下結構:
|
| 32 |
+
```json
|
| 33 |
+
{
|
| 34 |
+
"events": [
|
| 35 |
+
{
|
| 36 |
+
"event_id": "20240403",
|
| 37 |
+
"event_name": "0403花蓮地震 (2024)",
|
| 38 |
+
"epicenter_lat": 23.88,
|
| 39 |
+
"epicenter_lon": 121.57,
|
| 40 |
+
"depth_km": 25.0,
|
| 41 |
+
"magnitude": 7.2,
|
| 42 |
+
"timestamp": "2024-04-03T07:58:00Z",
|
| 43 |
+
"mseed_file": "waveform/20240403.mseed",
|
| 44 |
+
"intensity_map_file": "intensity_map/20240403.png"
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### 資料欄位說明
|
| 51 |
+
| 欄位 | 型態 | 說明 |
|
| 52 |
+
|-----|-----|------|
|
| 53 |
+
| `event_id` | 字串 | 事件唯一識別碼,使用 YYYYMMDD 格式(e.g., "20240403"),對應波形檔案與震度圖命名 |
|
| 54 |
+
| `event_name` | 字串 | 事件顯示名稱,與 EARTHQUAKE_EVENTS 的鍵名保持一致 |
|
| 55 |
+
| `epicenter_lat` | 浮點 | 震央緯度(度) |
|
| 56 |
+
| `epicenter_lon` | 浮點 | 震央經度(度) |
|
| 57 |
+
| `depth_km` | 浮點 | 震源深度(公里),選填 |
|
| 58 |
+
| `magnitude` | 浮點 | 地震規模,選填 |
|
| 59 |
+
| `timestamp` | 字串 | ISO 8601 時間戳,選填 |
|
| 60 |
+
| `mseed_file` | 字串 | 對應的 MSEED 波形檔案路徑(e.g., "waveform/20240403.mseed") |
|
| 61 |
+
| `intensity_map_file` | 字串 | 對應的實際震度圖檔案路徑(e.g., "intensity_map/20240403.png"),選填 |
|
| 62 |
+
|
| 63 |
+
### 驗收標準
|
| 64 |
+
- [ ] `waveform/event.json` 檔案已建立
|
| 65 |
+
- [ ] JSON 格式有效(可通過 `json.load()` 解析無誤)
|
| 66 |
+
- [ ] 包含至少一個事件記錄(0403花蓮地震)
|
| 67 |
+
- [ ] `event_id` 使用 YYYYMMDD 格式(e.g., "20240403")且與波形檔案、震度圖檔案命名對應
|
| 68 |
+
- [ ] 事件 `event_name` 與 app.py 中 `EARTHQUAKE_EVENTS` 的鍵名一致
|
| 69 |
+
- [ ] 震央座標範圍正確(緯度 5-30°,經度 110-123°)
|
| 70 |
+
|
| 71 |
+
### 風險等級
|
| 72 |
+
**低** — 單純新增檔案,不涉及邏輯變更
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## T-002: 新增 `load_earthquake_metadata()` 函數
|
| 77 |
+
|
| 78 |
+
### 描述
|
| 79 |
+
新增從 JSON 檔案讀取地震事件元資料的函數,應用啟動時調用以初始化震央資訊字典。
|
| 80 |
+
|
| 81 |
+
### 受影響檔案
|
| 82 |
+
- **修改**:`app.py`
|
| 83 |
+
|
| 84 |
+
### 具體變更點
|
| 85 |
+
|
| 86 |
+
#### `app.py` (新增函數,位置:在全域初始化區段,約 L80-90)
|
| 87 |
+
```python
|
| 88 |
+
# 新增位置:在「載入目標測站」區塊之後 (L110 左右)
|
| 89 |
+
def load_earthquake_metadata(event_json_path="waveform/event.json"):
|
| 90 |
+
"""
|
| 91 |
+
從 JSON 檔案讀取地震事件元資料(震央經緯度、深度等)
|
| 92 |
+
|
| 93 |
+
參數:
|
| 94 |
+
event_json_path: event.json 的相對或絕對路徑
|
| 95 |
+
|
| 96 |
+
返回:
|
| 97 |
+
dict: {event_name -> {"epicenter_lat", "epicenter_lon", "depth_km", ...}}
|
| 98 |
+
若檔案缺失或無效,使用預設座標 (121.57, 23.88) 並記錄警告
|
| 99 |
+
"""
|
| 100 |
+
earthquake_metadata = {}
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
import json
|
| 104 |
+
with open(event_json_path, 'r', encoding='utf-8') as f:
|
| 105 |
+
data = json.load(f)
|
| 106 |
+
|
| 107 |
+
if "events" not in data:
|
| 108 |
+
logger.warning(f"{event_json_path} 缺少 'events' 鍵,使用預設座標")
|
| 109 |
+
return {}
|
| 110 |
+
|
| 111 |
+
# 將事件列表轉換為以 event_name 為鍵的字典
|
| 112 |
+
for event in data["events"]:
|
| 113 |
+
event_name = event.get("event_name")
|
| 114 |
+
if event_name:
|
| 115 |
+
earthquake_metadata[event_name] = {
|
| 116 |
+
"epicenter_lat": event.get("epicenter_lat", 23.88),
|
| 117 |
+
"epicenter_lon": event.get("epicenter_lon", 121.57),
|
| 118 |
+
"depth_km": event.get("depth_km", None),
|
| 119 |
+
"magnitude": event.get("magnitude", None),
|
| 120 |
+
}
|
| 121 |
+
logger.info(f"載入事件: {event_name} | 震央: ({event['epicenter_lon']}, {event['epicenter_lat']})")
|
| 122 |
+
|
| 123 |
+
logger.info(f"地震事件元資料載入完成(共 {len(earthquake_metadata)} 個事件)")
|
| 124 |
+
return earthquake_metadata
|
| 125 |
+
|
| 126 |
+
except FileNotFoundError:
|
| 127 |
+
logger.error(f"事件元資料檔案缺失: {event_json_path}")
|
| 128 |
+
logger.warning("將使用預設震央座標 (121.57, 23.88)")
|
| 129 |
+
return {}
|
| 130 |
+
|
| 131 |
+
except json.JSONDecodeError as e:
|
| 132 |
+
logger.error(f"事件元資料 JSON 解析失敗: {e}")
|
| 133 |
+
logger.warning("將使用預設震央座標 (121.57, 23.88)")
|
| 134 |
+
return {}
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logger.error(f"讀取事件元資料時發生未預期的錯誤: {e}")
|
| 138 |
+
logger.warning("將使用預設震央座標 (121.57, 23.88)")
|
| 139 |
+
return {}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
#### `app.py` (全域初始化,位置:L110-115,在目標測站載入完成後)
|
| 143 |
+
```python
|
| 144 |
+
# ...existing code...
|
| 145 |
+
|
| 146 |
+
# 載入地震事件元資料
|
| 147 |
+
earthquake_metadata = load_earthquake_metadata("waveform/event.json")
|
| 148 |
+
if not earthquake_metadata:
|
| 149 |
+
logger.warning("無法載入事件元資料,應用將使用預設震央座標")
|
| 150 |
+
|
| 151 |
+
# ...existing code...
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### 驗收標準
|
| 155 |
+
- [ ] 函數 `load_earthquake_metadata()` 已定義且無語法錯誤
|
| 156 |
+
- [ ] 應用啟動時自動呼叫該函數
|
| 157 |
+
- [ ] JSON 檔案存在時,正確解析並填充 `earthquake_metadata` 字典
|
| 158 |
+
- [ ] JSON 檔案缺失時,記錄 WARNING log 並返回空字典(不中斷啟動)
|
| 159 |
+
- [ ] JSON 格式錯誤時,記錄 ERROR log 並返回空字典(不中斷啟動)
|
| 160 |
+
- [ ] 日誌中可見「地震事件元資料載入完成」或「無法載入事件元資料」提示
|
| 161 |
+
|
| 162 |
+
### 風險等級
|
| 163 |
+
**中** — 涉及全域初始化邏輯,但降級策略完整
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## T-003: 修改 Gradio 介面佈局
|
| 168 |
+
|
| 169 |
+
### 描述
|
| 170 |
+
移除 Gradio 介面中的「震央經度」、「震央緯度」輸入框,改為在狀態區顯示唯讀的震央座標。
|
| 171 |
+
|
| 172 |
+
### 受影響檔案
|
| 173 |
+
- **修改**:`app.py`
|
| 174 |
+
|
| 175 |
+
### 具體變更點
|
| 176 |
+
|
| 177 |
+
#### `app.py` (移除輸入框,位置:L1265-1276)
|
| 178 |
+
**原代碼:**
|
| 179 |
+
```python
|
| 180 |
+
gr.Markdown("### 震央位置")
|
| 181 |
+
with gr.Row():
|
| 182 |
+
epicenter_lon_input = gr.Number(value=121.57, label="震央經度")
|
| 183 |
+
epicenter_lat_input = gr.Number(value=23.88, label="震央緯度")
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
**新代碼:**
|
| 187 |
+
```python
|
| 188 |
+
gr.Markdown("### 震央位置")
|
| 189 |
+
gr.Markdown("> 震央位置由選定的地震事件自動決定,並在地圖上標示")
|
| 190 |
+
epicenter_info_display = gr.Textbox(
|
| 191 |
+
label="震央座標",
|
| 192 |
+
value="緯度: 23.88° | 經度: 121.57°",
|
| 193 |
+
interactive=False,
|
| 194 |
+
lines=1
|
| 195 |
+
)
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
**說明:**
|
| 199 |
+
- 移除 `epicenter_lon_input` 與 `epicenter_lat_input` 組件
|
| 200 |
+
- 新增 `epicenter_info_display` 作為唯讀文本框,顯示當前選定事件的震央座標
|
| 201 |
+
- 使用 Markdown 提示該欄位由事件自動決定
|
| 202 |
+
|
| 203 |
+
### 驗收標準
|
| 204 |
+
- [ ] Gradio 介面不再顯示經緯度輸入框
|
| 205 |
+
- [ ] 新增的唯讀文本框 `epicenter_info_display` 正常顯示
|
| 206 |
+
- [ ] 應用啟動時,該文本框顯示預設値「緯度: 23.88° | 經度: 121.57°」
|
| 207 |
+
- [ ] 介面佈局整潔,無顯示破損
|
| 208 |
+
|
| 209 |
+
### 風險等級
|
| 210 |
+
**低** — 單純 UI 元件移除與新增
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## T-004: 更新 callback 簽名
|
| 215 |
+
|
| 216 |
+
### 描述
|
| 217 |
+
更新所有使用震央座標的 callback 函數簽名,移除 `epicenter_lat` 與 `epicenter_lon` 參數。改由在 callback 內部從 `earthquake_metadata` 字典讀取。
|
| 218 |
+
|
| 219 |
+
### 受影響檔案
|
| 220 |
+
- **修改**:`app.py`
|
| 221 |
+
|
| 222 |
+
### 具體變更點
|
| 223 |
+
|
| 224 |
+
#### 變更 1: `select_nearest_stations()` 函數簽名 (L440)
|
| 225 |
+
**原:**
|
| 226 |
+
```python
|
| 227 |
+
def select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25):
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
**新:**
|
| 231 |
+
```python
|
| 232 |
+
def select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25):
|
| 233 |
+
# 保持原簽名不變,因為內部邏輯不涉及全域狀態
|
| 234 |
+
# 此函數由 load_and_display_waveform() 與 predict_intensity() 呼叫
|
| 235 |
+
# 這兩個函數會先從 earthquake_metadata 取出座標再傳入
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
#### 變更 2: `load_and_display_waveform()` 函數簽名 (L1001)
|
| 239 |
+
**原:**
|
| 240 |
+
```python
|
| 241 |
+
def load_and_display_waveform(event_name, start_time, duration, epicenter_lon, epicenter_lat):
|
| 242 |
+
...
|
| 243 |
+
selected_stations = select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25)
|
| 244 |
+
...
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
**新:**
|
| 248 |
+
```python
|
| 249 |
+
def load_and_display_waveform(event_name, start_time, duration):
|
| 250 |
+
"""
|
| 251 |
+
載入並顯示波形
|
| 252 |
+
|
| 253 |
+
參數:
|
| 254 |
+
event_name: 事件名稱(鍵值)
|
| 255 |
+
start_time: 開始時間(秒)
|
| 256 |
+
duration: 時間窗長度(秒)
|
| 257 |
+
|
| 258 |
+
返回值、邏輯不變,但從全域 earthquake_metadata 取得震央座標
|
| 259 |
+
"""
|
| 260 |
+
# 從全域 earthquake_metadata 讀取震央座標
|
| 261 |
+
epicenter_lat, epicenter_lon = _get_epicenter_coords(event_name)
|
| 262 |
+
|
| 263 |
+
# 其餘邏輯不變
|
| 264 |
+
...
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
#### 變更 3: `predict_intensity()` 函數簽��� (L1046)
|
| 268 |
+
**原:**
|
| 269 |
+
```python
|
| 270 |
+
def predict_intensity(event_name, start_time, duration, epicenter_lon, epicenter_lat):
|
| 271 |
+
...
|
| 272 |
+
selected_stations = select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25)
|
| 273 |
+
...
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
**新:**
|
| 277 |
+
```python
|
| 278 |
+
def predict_intensity(event_name, start_time, duration):
|
| 279 |
+
"""
|
| 280 |
+
執行模型推論並產生震度預測圖
|
| 281 |
+
|
| 282 |
+
參數:
|
| 283 |
+
event_name: 事件名稱(鍵值)
|
| 284 |
+
start_time: 開始時間(秒)
|
| 285 |
+
duration: 時間窗長度(秒)
|
| 286 |
+
|
| 287 |
+
返回值、邏輯不變,但從全域 earthquake_metadata 取得震央座標
|
| 288 |
+
"""
|
| 289 |
+
# 從全域 earthquake_metadata 讀取震央座標
|
| 290 |
+
epicenter_lat, epicenter_lon = _get_epicenter_coords(event_name)
|
| 291 |
+
|
| 292 |
+
# 其餘邏輯不變
|
| 293 |
+
...
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
#### 變更 4: `on_event_change()` 函數簽名 (L864)
|
| 297 |
+
**原:**
|
| 298 |
+
```python
|
| 299 |
+
def on_event_change(event_name, start_time, duration, epicenter_lon, epicenter_lat):
|
| 300 |
+
...
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
**新:**
|
| 304 |
+
```python
|
| 305 |
+
def on_event_change(event_name, start_time, duration):
|
| 306 |
+
"""
|
| 307 |
+
事件切換或波形參數變更時的回調
|
| 308 |
+
|
| 309 |
+
參數:
|
| 310 |
+
event_name: 事件名稱
|
| 311 |
+
start_time: 開始時間
|
| 312 |
+
duration: 時間窗長度
|
| 313 |
+
|
| 314 |
+
返回值:(station_map_html, waveform_plot, info_text, observed_img)
|
| 315 |
+
"""
|
| 316 |
+
...
|
| 317 |
+
```
|
| 318 |
+
|
| 319 |
+
#### 變更 5: `on_full_workflow()` 函數簽名 (L1220+)
|
| 320 |
+
**原:**
|
| 321 |
+
```python
|
| 322 |
+
def on_full_workflow(event_name, start_time, duration, epicenter_lon, epicenter_lat):
|
| 323 |
+
...
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
**新:**
|
| 327 |
+
```python
|
| 328 |
+
def on_full_workflow(event_name, start_time, duration):
|
| 329 |
+
"""
|
| 330 |
+
執行完整的工作流:波形載入 → 測站選擇 → 推論 → 結果展示
|
| 331 |
+
|
| 332 |
+
參數:
|
| 333 |
+
event_name: 事件名稱
|
| 334 |
+
start_time: 開始時間
|
| 335 |
+
duration: 時間窗長度
|
| 336 |
+
|
| 337 |
+
返回值:
|
| 338 |
+
(station_map_html, waveform_plot, info_text, predicted_map_html, stats_text, observed_img)
|
| 339 |
+
"""
|
| 340 |
+
...
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
#### 新增輔助函數:`_get_epicenter_coords()` (位置:L110+,在 load_earthquake_metadata 之後)
|
| 344 |
+
```python
|
| 345 |
+
def _get_epicenter_coords(event_name):
|
| 346 |
+
"""
|
| 347 |
+
從全域 earthquake_metadata 獲取指定事件的震央座標
|
| 348 |
+
|
| 349 |
+
參數:
|
| 350 |
+
event_name: 事件名稱(EARTHQUAKE_EVENTS 的鍵值)
|
| 351 |
+
|
| 352 |
+
返回:
|
| 353 |
+
tuple: (epicenter_lat, epicenter_lon)
|
| 354 |
+
若事件不存在或座標缺失,返回預設座標 (23.88, 121.57)
|
| 355 |
+
"""
|
| 356 |
+
if event_name in earthquake_metadata:
|
| 357 |
+
metadata = earthquake_metadata[event_name]
|
| 358 |
+
lat = metadata.get("epicenter_lat", 23.88)
|
| 359 |
+
lon = metadata.get("epicenter_lon", 121.57)
|
| 360 |
+
return lat, lon
|
| 361 |
+
else:
|
| 362 |
+
logger.warning(f"未找到事件 '{event_name}' 的元資料,使用預設震央座標")
|
| 363 |
+
return 23.88, 121.57
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
### 驗收標準
|
| 367 |
+
- [ ] 所有受影響函數的簽名已移除 `epicenter_lat` 與 `epicenter_lon` 參數
|
| 368 |
+
- [ ] 新增 `_get_epicenter_coords()` 輔助函數
|
| 369 |
+
- [ ] 所有函數內部邏輯正確地從全域 `earthquake_metadata` 或 `_get_epicenter_coords()` 取得座標
|
| 370 |
+
- [ ] 應用啟動無語法錯誤
|
| 371 |
+
- [ ] 代碼審查通過:沒有遺漏的函數簽名
|
| 372 |
+
|
| 373 |
+
### 風險等級
|
| 374 |
+
**高** — 涉及多個函數簽名變更,可能導致 callback 綁定失敗
|
| 375 |
+
|
| 376 |
+
---
|
| 377 |
+
|
| 378 |
+
## T-005: 事件切換自動注入 + Callback 綁定重構
|
| 379 |
+
|
| 380 |
+
### 描述
|
| 381 |
+
重構 Gradio callback 綁定邏輯,確保:
|
| 382 |
+
1. `event_dropdown.change()` 事件自動從 JSON 注入震央資訊
|
| 383 |
+
2. 更新 `epicenter_info_display` 文本框顯示新的震央座標
|
| 384 |
+
3. 所有 callback inputs/outputs 綁定正確移除經緯度參數
|
| 385 |
+
|
| 386 |
+
### 受影響檔案
|
| 387 |
+
- **修改**:`app.py`
|
| 388 |
+
|
| 389 |
+
### 具體變更點
|
| 390 |
+
|
| 391 |
+
#### `app.py` (Callback 綁定重構,位置:L1300-1340)
|
| 392 |
+
|
| 393 |
+
**原代碼:**
|
| 394 |
+
```python
|
| 395 |
+
# 綁定事件
|
| 396 |
+
event_dropdown.change(
|
| 397 |
+
fn=on_full_workflow,
|
| 398 |
+
inputs=[event_dropdown, start_slider, duration_slider, epicenter_lon_input, epicenter_lat_input],
|
| 399 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image]
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
load_waveform_btn.click(
|
| 403 |
+
fn=load_and_display_waveform,
|
| 404 |
+
inputs=[event_dropdown, start_slider, duration_slider, epicenter_lon_input, epicenter_lat_input],
|
| 405 |
+
outputs=[input_station_map, waveform_plot, info_output, predict_btn]
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
predict_btn.click(
|
| 409 |
+
fn=predict_intensity,
|
| 410 |
+
inputs=[event_dropdown, start_slider, duration_slider, epicenter_lon_input, epicenter_lat_input],
|
| 411 |
+
outputs=[observed_intensity_image, predicted_intensity_map, stats_output]
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
# 應用啟動時自動執行完整工作流
|
| 415 |
+
demo.load(
|
| 416 |
+
fn=on_full_workflow,
|
| 417 |
+
inputs=[event_dropdown, start_slider, duration_slider, epicenter_lon_input, epicenter_lat_input],
|
| 418 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image]
|
| 419 |
+
)
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
**新代碼:**
|
| 423 |
+
```python
|
| 424 |
+
# 新增:事件切換時更新震央座標顯示的內部函數
|
| 425 |
+
def update_epicenter_display(event_name):
|
| 426 |
+
"""事件切換時,更新震央座標顯示文本框"""
|
| 427 |
+
lat, lon = _get_epicenter_coords(event_name)
|
| 428 |
+
return f"緯度: {lat:.2f}° | 經度: {lon:.2f}°"
|
| 429 |
+
|
| 430 |
+
# 綁定事件
|
| 431 |
+
event_dropdown.change(
|
| 432 |
+
fn=lambda event_name, start_time, duration: (
|
| 433 |
+
*on_full_workflow(event_name, start_time, duration),
|
| 434 |
+
update_epicenter_display(event_name)
|
| 435 |
+
),
|
| 436 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 437 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image, epicenter_info_display]
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
load_waveform_btn.click(
|
| 441 |
+
fn=lambda event_name, start_time, duration: (
|
| 442 |
+
*load_and_display_waveform(event_name, start_time, duration),
|
| 443 |
+
predict_btn
|
| 444 |
+
),
|
| 445 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 446 |
+
outputs=[input_station_map, waveform_plot, info_output, predict_btn]
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
predict_btn.click(
|
| 450 |
+
fn=predict_intensity,
|
| 451 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 452 |
+
outputs=[observed_intensity_image, predicted_intensity_map, stats_output]
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
# 應用啟動時自動執行完整工作流
|
| 456 |
+
demo.load(
|
| 457 |
+
fn=lambda event_name, start_time, duration: (
|
| 458 |
+
*on_full_workflow(event_name, start_time, duration),
|
| 459 |
+
update_epicenter_display(event_name)
|
| 460 |
+
),
|
| 461 |
+
inputs=[event_dropdown, start_slider, duration_slider],
|
| 462 |
+
outputs=[input_station_map, waveform_plot, info_output, predicted_intensity_map, stats_output, observed_intensity_image, epicenter_info_display]
|
| 463 |
+
)
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
**說明:**
|
| 467 |
+
- 移除所有 `inputs` 中的 `epicenter_lon_input` 與 `epicenter_lat_input`
|
| 468 |
+
- 新增 outputs 中的 `epicenter_info_display` 以同步更新座標顯示
|
| 469 |
+
- 事件切換時自動調用 `update_epicenter_display()` 更新座標文本框
|
| 470 |
+
|
| 471 |
+
### 驗收標準
|
| 472 |
+
- [ ] 所有 callback 綁定已更新,inputs/outputs 參數列表正確
|
| 473 |
+
- [ ] 應用啟動時執行完整工作流並顯示正確的震央座標
|
| 474 |
+
- [ ] 點擊事件下拉菜單時,座標顯示框自動更新
|
| 475 |
+
- [ ] 點擊「載入波形」按鈕時,正確讀取新的震央座標
|
| 476 |
+
- [ ] 點擊「執行預測」按鈕時,正確讀取新的震央座標
|
| 477 |
+
- [ ] 地圖上的紅星標記始終指向正確的震央位置
|
| 478 |
+
- [ ] 無 callback 參數不匹配的錯誤
|
| 479 |
+
|
| 480 |
+
### 風險等級
|
| 481 |
+
**高** — Callback 綁定任何錯誤都將導致應用崩潰
|
| 482 |
+
|
| 483 |
+
---
|
| 484 |
+
|
| 485 |
+
## T-006: 測試與驗證
|
| 486 |
+
|
| 487 |
+
### 描述
|
| 488 |
+
執行冒煙測試,驗證整個迭代的所有組件運作正常。
|
| 489 |
+
|
| 490 |
+
### 受影響檔案
|
| 491 |
+
- **測試對象**:`app.py`、`waveform/event.json`
|
| 492 |
+
|
| 493 |
+
### 測試用例
|
| 494 |
+
|
| 495 |
+
#### ✅ 測試 1:JSON 檔案與初始化
|
| 496 |
+
**步驟**
|
| 497 |
+
1. 確認 `waveform/event.json` 存在且格式有效
|
| 498 |
+
2. 啟動應用
|
| 499 |
+
3. 檢查日誌輸出:應有「地震事件元資料載入完成」提示
|
| 500 |
+
|
| 501 |
+
**預期結果**
|
| 502 |
+
- ✅ 應用正常啟動(無 FileNotFoundError)
|
| 503 |
+
- ✅ 日誌確認 JSON 讀取成功
|
| 504 |
+
- ✅ `earthquake_metadata` 全域變數被正確填充
|
| 505 |
+
- ✅ 無 JSONDecodeError
|
| 506 |
+
|
| 507 |
+
**驗收條件**
|
| 508 |
+
- [ ] 應用啟動完成
|
| 509 |
+
- [ ] 日誌顯示「地震事件元資料載入完成」
|
| 510 |
+
|
| 511 |
+
---
|
| 512 |
+
|
| 513 |
+
#### ✅ 測試 2:Gradio 介面佈局
|
| 514 |
+
**步驟**
|
| 515 |
+
1. 開啟應用 UI(http://localhost:7860)
|
| 516 |
+
2. 檢查「輸入參數」區域
|
| 517 |
+
|
| 518 |
+
**預期結果**
|
| 519 |
+
- ✅ 經緯度輸入框(原有的兩個 Number 欄位)已不存在
|
| 520 |
+
- ✅ 新增「震央座標」唯讀文本框,顯示「緯度: 23.88° | 經度: 121.57°」
|
| 521 |
+
- ✅ 有提示文字說「震央位置由選定的地震事件自動決定」
|
| 522 |
+
- ✅ 介面整潔無破損
|
| 523 |
+
|
| 524 |
+
**驗收條件**
|
| 525 |
+
- [ ] UI 中無經緯度輸入框
|
| 526 |
+
- [ ] 震央座標文本框存在且可讀取
|
| 527 |
+
- [ ] 提示文字清晰可見
|
| 528 |
+
|
| 529 |
+
---
|
| 530 |
+
|
| 531 |
+
#### ✅ 測試 3:事件切換自動更新
|
| 532 |
+
**步驟**
|
| 533 |
+
1. 應用已啟動
|
| 534 |
+
2. 觀察地圖:紅星標記位置與座標顯示框
|
| 535 |
+
3. 在事件下拉菜單中選擇同一事件(或若有其他事件,嘗試切換)
|
| 536 |
+
4. 觀察座標顯示框與地圖是否同步更新
|
| 537 |
+
|
| 538 |
+
**預期結果**
|
| 539 |
+
- ✅ 應用啟動時,座標顯示框顯示「緯度: 23.88° | 經度: 121.57°」
|
| 540 |
+
- ✅ 地圖中心位置在 (23.88, 121.57)
|
| 541 |
+
- ✅ 紅星標記位置正確
|
| 542 |
+
- ✅ 若有多個事件,切換時座標與地圖同時更新
|
| 543 |
+
|
| 544 |
+
**驗收條件**
|
| 545 |
+
- [ ] 座標顯示框初始值正確
|
| 546 |
+
- [ ] 地圖紅星位置正確
|
| 547 |
+
- [ ] 地圖中心正確
|
| 548 |
+
|
| 549 |
+
---
|
| 550 |
+
|
| 551 |
+
#### ✅ 測試 4:完整工作流
|
| 552 |
+
**步驟**
|
| 553 |
+
1. 應用已啟動
|
| 554 |
+
2. 調整時間滑桿(開始時間、時間長度)
|
| 555 |
+
3. 點擊「載入波形」按鈕
|
| 556 |
+
4. 觀察波形地圖、波形圖、狀態信息
|
| 557 |
+
5. 點擊「執行預測」按鈕
|
| 558 |
+
6. 觀察預測結果、地圖、統計信息
|
| 559 |
+
|
| 560 |
+
**預期結果**
|
| 561 |
+
- ✅ 點擊「載入波形」後,波形地圖、波形圖正常顯示
|
| 562 |
+
- ✅ 狀態信息中顯示「震央位置: (緯度, 經度)」
|
| 563 |
+
- ✅ 測站分布地圖上的紅星標記位置正確
|
| 564 |
+
- ✅ 點擊「執行預測」後,預測結果正常顯示
|
| 565 |
+
- ✅ 預測地圖上的震央紅星位置正確
|
| 566 |
+
- ✅ 無「參數不匹配」或「缺少參數」錯誤
|
| 567 |
+
|
| 568 |
+
**驗收條件**
|
| 569 |
+
- [ ] 波形載入成功
|
| 570 |
+
- [ ] 狀態信息顯示正確的震央座標
|
| 571 |
+
- [ ] 預測執行成功
|
| 572 |
+
- [ ] 所有地圖上的紅星位置正確
|
| 573 |
+
- [ ] 日誌無錯誤(僅允許 WARNING)
|
| 574 |
+
|
| 575 |
+
---
|
| 576 |
+
|
| 577 |
+
#### ✅ 測試 5:日誌驗證
|
| 578 |
+
**步驟**
|
| 579 |
+
1. 啟動應用
|
| 580 |
+
2. 執行測試 1-4
|
| 581 |
+
3. 收集所有日誌輸出
|
| 582 |
+
|
| 583 |
+
**預期結果**
|
| 584 |
+
- ✅ 應用啟動時有「地震事件元資料載入完成」日誌
|
| 585 |
+
- ✅ 波形載入時有「選擇距離震央最近的測站」日誌(帶有座標)
|
| 586 |
+
- ✅ 預測時有「完成所有測站的預測」日誌
|
| 587 |
+
- ✅ 無 ERROR 日誌(除非預期的異常處理)
|
| 588 |
+
|
| 589 |
+
**驗收條件**
|
| 590 |
+
- [ ] 日誌中包含完整的初始化流程
|
| 591 |
+
- [ ] 無意外的 ERROR 或 CRITICAL 日誌
|
| 592 |
+
- [ ] 所有座標相關操作都有對應的日誌記錄
|
| 593 |
+
|
| 594 |
+
---
|
| 595 |
+
|
| 596 |
+
### 驗收標準 (總結)
|
| 597 |
+
- [ ] T-001 ✅ JSON 檔案已建立並包含正確的結構
|
| 598 |
+
- [ ] T-002 ✅ `load_earthquake_metadata()` 函數正常工作
|
| 599 |
+
- [ ] T-003 ✅ 介面移除經緯度輸入框,新增座標顯示框
|
| 600 |
+
- [ ] T-004 ✅ 所有 callback 函數簽名已更新
|
| 601 |
+
- [ ] T-005 ✅ Callback 綁定邏輯正確,震央座標同步更新
|
| 602 |
+
- [ ] T-006 ✅ 所有冒煙測試通過
|
| 603 |
+
|
| 604 |
+
### 風險等級
|
| 605 |
+
**中** — 綜合測試,涉及整個系統的協調運作
|
| 606 |
+
|
| 607 |
+
---
|
| 608 |
+
|
| 609 |
+
## 變更摘要 (Impact Analysis)
|
| 610 |
+
|
| 611 |
+
### 受影響的核心模組
|
| 612 |
+
| 模組 | 變更類型 | 影響範圍 |
|
| 613 |
+
|-----|--------|--------|
|
| 614 |
+
| **初始化** | 新增 | 新增全域 `earthquake_metadata` 字典 + `load_earthquake_metadata()` 函數 |
|
| 615 |
+
| **UI 佈局** | 移除 + 新增 | 移除 2 個輸入框,新增 1 個顯示框 + 1 個輔助函數 `update_epicenter_display()` |
|
| 616 |
+
| **函數簽名** | 修改 | 5 個函數簽名更新(移除經緯度參數) |
|
| 617 |
+
| **Callback 邏輯** | 修改 | 3 個 callback 綁定邏輯調整,新增座標自動注入 |
|
| 618 |
+
| **資料流** | 修改 | 原先透過 UI 參數傳遞的座標,改由全域字典提供 |
|
| 619 |
+
|
| 620 |
+
### 不變條件檢查
|
| 621 |
+
| 項目 | 影響 | 說明 |
|
| 622 |
+
|-----|------|------|
|
| 623 |
+
| 波形輸入 | ✅ 無 | 訊號處理、補零、取樣率邏輯完全不變 |
|
| 624 |
+
| 測站選擇 | ✅ 無 | 距離排序、去重、25 站上限邏輯不變 |
|
| 625 |
+
| 推論引擎 | ✅ 無 | CNN、Transformer、MDN 模型邏輯完全不變 |
|
| 626 |
+
| 資料契約 | ✅ 無 | CSV 欄位、模型輸入形狀不變 |
|
| 627 |
+
| 錯誤處理 | ✅ 無 | 降級策略、日誌等級維持不變 |
|
| 628 |
+
|
| 629 |
+
### 新增資料依賴
|
| 630 |
+
- **新增檔案**:`waveform/event.json`(必須存在,缺失時使用預設值)
|
| 631 |
+
- **新增全域變數**:`earthquake_metadata` (dict)
|
| 632 |
+
|
| 633 |
+
### 向後相容性
|
| 634 |
+
- ✅ **完全向後相容**:
|
| 635 |
+
- JSON 檔案缺失時,應用可正常啟動(降級至預設座標)
|
| 636 |
+
- 所有 callback 參數變更是內部實現,使用者介面不變
|
| 637 |
+
- 模型推論邏輯、資料結構、檔案格式均無變化
|
| 638 |
+
|
| 639 |
+
---
|
| 640 |
+
|
| 641 |
+
## ⏸️ 暫停點
|
| 642 |
+
|
| 643 |
+
**上述任務拆解已完成。請進行最終審核:**
|
| 644 |
+
|
| 645 |
+
### 檢查清單
|
| 646 |
+
|
| 647 |
+
1. **架構確認**:
|
| 648 |
+
- [ ] T-001 JSON 檔案結構是否合適?
|
| 649 |
+
- [ ] T-002 `load_earthquake_metadata()` 函數設計是否可行?
|
| 650 |
+
- [ ] 新增輔助函數 `_get_epicenter_coords()` 是否必要?
|
| 651 |
+
|
| 652 |
+
2. **技術確認**:
|
| 653 |
+
- [ ] 函數簽名變更(T-004)的清單是否完整?
|
| 654 |
+
- [ ] Callback 綁定邏輯(T-005)是否正確?
|
| 655 |
+
- [ ] Lambda 封裝在 Callback 中是否適當?
|
| 656 |
+
|
| 657 |
+
3. **測試確認**:
|
| 658 |
+
- [ ] 測試用例(T-006)的 4 個測試是否涵蓋所有關鍵路徑?
|
| 659 |
+
- [ ] 驗收標準是否清晰可測?
|
| 660 |
+
|
| 661 |
+
4. **風險確認**:
|
| 662 |
+
- [ ] 三個「高風險」任務是否有足夠的驗收標準?
|
| 663 |
+
- [ ] 降級策略是否充分(JSON 缺失、事件不存在等)?
|
| 664 |
+
|
| 665 |
+
### 確認方式
|
| 666 |
+
|
| 667 |
+
請在本 markdown 中直接編輯,確認後回覆:
|
| 668 |
+
- **「確認」** 或 **「通過」**:進行下一步 `/project.apply` 應用程式碼變更
|
| 669 |
+
- **「調整」** + 具體建議:我會根據反饋進行修改
|
| 670 |
+
|
| 671 |
+
---
|
| 672 |
+
|
| 673 |
+
## 版本資訊
|
| 674 |
+
- **建立時間**:2025-10-26
|
| 675 |
+
- **迭代**:震央資訊 JSON 管理化
|
| 676 |
+
- **狀態**:等待使用者確認
|
waveform/event.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"events": [
|
| 3 |
+
{
|
| 4 |
+
"event_id": "20240403",
|
| 5 |
+
"event_name": "0403花蓮地震 (2024)",
|
| 6 |
+
"epicenter_lat": 23.88,
|
| 7 |
+
"epicenter_lon": 121.57,
|
| 8 |
+
"depth_km": 25.0,
|
| 9 |
+
"magnitude": 7.2,
|
| 10 |
+
"timestamp": "2024-04-03T07:58:00Z",
|
| 11 |
+
"mseed_file": "waveform/20240403.mseed",
|
| 12 |
+
"intensity_map_file": "intensity_map/20240403.png"
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
}
|
| 16 |
+
|