cwadayi's picture
Update app.py
f2e9915 verified
import gradio as gr
import random
import textwrap
import folium
from obspy.clients.fdsn import Client
from obspy import UTCDateTime
import ast # 用於安全地解析字典
# -------------------------------------------------------------------
# 範例 1 (基礎) 的預設資料
# -------------------------------------------------------------------
TUTORIAL_BASIC_INITIAL_DICT = {
"板塊構造 (Plate Tectonics)": "地球的岩石圈被分成許多稱為「板塊」的巨大板塊,這些板塊在軟流圈上緩慢移動,彼此碰撞或分離,形成了火山、地震和山脈。",
"地震波 (Seismic Wave)": "地震時從震源向四面八方傳播的能量波。主要分為 P 波(壓縮波,速度快)和 S 波(剪力波,速度慢),它們是我們研究地球內部的主要工具。",
"海嘯 (Tsunami)": "由海底地震、火山爆發或山崩引起的巨大海浪。它在深海中傳播速度極快(可達時速 800 公里),但波高很低;當它靠近淺海岸時,速度減慢,波高則急遽增加。"
}
TUTORIAL_CODE_BASIC = textwrap.dedent(f"""
# --- 這是 Level 1 範例的完整程式碼 ---
# 試試看:
# 1. 在 geo_dict 中新增一個你自己的詞條。
# 2. 修改 "海嘯" 的解釋。
# 3. 按下方的 "套用變更" 按鈕。
# 4. 回到 "Live Demo" 分頁看看你的修改!
# 1. 知識庫 (你的地科字典)
geo_dict = {TUTORIAL_BASIC_INITIAL_DICT!r}
# 2. 定義介面函式
def get_definition(term):
# geo_dict 會從這個程式碼的 "全域" 範圍內被找到
return geo_dict.get(term, "查無此名詞")
""")
# -------------------------------------------------------------------
# 範例 1 (基礎) 的動態執行 Wrapper
# -------------------------------------------------------------------
def run_basic_code(term, code_string):
# [***** 修復 *****]
# 建立一個同時作為 global 和 local 範圍的 scope
# 這樣在 `exec` 中定義的函式才能找到在同一個 scope 中定義的全域變數
execution_scope = {"__builtins__": {}}
try:
# [修復] 將 global 和 local 都設置為同一個 execution_scope
exec(code_string, execution_scope, execution_scope)
get_definition_func = execution_scope.get("get_definition")
if not callable(get_definition_func):
return "程式碼錯誤:找不到 `get_definition` 函式。"
# 呼叫學生程式碼中定義的函式
return get_definition_func(term)
except Exception as e:
return f"執行時發生錯誤:\n{e}"
# -------------------------------------------------------------------
# 範例 2 (進階) 的預設程式碼
# -------------------------------------------------------------------
TUTORIAL_CODE_ADVANCED = textwrap.dedent("""
# --- 這是 Level 2 範例的完整程式碼 ---
# 試試看:
# 1. 找到 "color = 'green'",把它改成 "color = 'blue'"。
# 2. 找到 "radius=mag * 1.5",把它改成 "radius=mag * 3"。
# 3. 按下方的 "套用變更" 按鈕。
# 4. 回到 "Live Demo" 分頁,搜尋地震看看你的修改!
# 1. 定義核心函式:搜尋並繪圖
def fetch_and_plot_events(min_mag, days_ago):
try:
# Folium, Client, UTCDateTime 已經被安全地匯入
client = Client("IRIS")
endtime = UTCDateTime.now()
starttime = endtime - (days_ago * 24 * 3600)
catalog = client.get_events(
starttime=starttime,
endtime=endtime,
minmagnitude=min_mag
)
m = folium.Map(location=[0, 0], zoom_start=2)
if len(catalog) == 0:
return "<p>在此條件下查無地震資料,請嘗試放寬搜尋條件。</p>"
for event in catalog:
origin = event.preferred_origin()
mag = event.preferred_magnitude().mag
lat = origin.latitude
lon = origin.longitude
depth_km = origin.depth / 1000.0
if depth_km < 70:
color = "green" # <-- 試著修改這裡!
elif depth_km < 300:
color = "orange"
else:
color = "red"
popup_html = f'''
<b>時間:</b> {origin.time.strftime('%Y-%m-%d %H:%M:%S')}<br>
<b>規模 (M):</b> {mag:.1f}<br>
<b>深度 (km):</b> {depth_km:.1f}<br>
<b>位置:</b> ({lat:.2f}, {lon:.2f})
'''
folium.CircleMarker(
location=[lat, lon],
radius=mag * 1.5, # <-- 試著修改這裡!
popup=folium.Popup(popup_html, max_width=300),
color=color,
fill=True,
fill_color=color,
fill_opacity=0.6
).add_to(m)
return m._repr_html_()
except Exception as e:
return f"<p>發生錯誤:{e}</p>"
""")
# -------------------------------------------------------------------
# 範例 2 (進階) 的安全沙盒與動態執行 Wrapper
# -------------------------------------------------------------------
def run_advanced_code(min_mag, days_ago, code_string):
# 建立一個安全的 "全域" 環境,只允許必要的模組
# 這個設計是正確的,因為函式(fetch_and_plot_events)需要存取
# 這些 "預先匯入" 的全域模組。
safe_globals = {
"__builtins__": {
"print": print, "Exception": Exception, "len": len, "str": str,
"float": float, "int": int, "list": list, "dict": dict, "range": range,
"True": True, "False": False, "None": None
},
"folium": folium,
"Client": Client,
"UTCDateTime": UTCDateTime
}
local_scope = {}
try:
# 在安全沙盒中執行學生程式碼
exec(code_string, safe_globals, local_scope)
fetch_func = local_scope.get("fetch_and_plot_events")
if not callable(fetch_func):
return "<p>程式碼錯誤:找不到 `fetch_and_plot_events` 函式。</p>"
# 呼叫學生程式碼中定義的函式
return fetch_func(min_mag, days_ago)
except Exception as e:
return f"<p>執行時發生錯誤:\n{e}</p>"
# -------------------------------------------------------------------
# 專案總覽的資料
# -------------------------------------------------------------------
projects_data = {
"🌊 地震與波形展示": [
{"title": "📡 地震目錄搜尋 API", "url": "https://cwadayi-python-app.hf.space/"},
{"title": "⚠️ 地震預警訊息顯示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_alarm_Taiwan"},
{"title": "🗺️ PyGMT 震央繪圖展示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_gmt_demo_new"},
{"title": "📈 ObsPy 地震資料處理", "url": "https://huggingface.co/spaces/cwadayi/streamlit_obspy"}
],
"🛰️ 地震監測與資料應用": [
{"title": "📍 Geiger 法地震定位展示", "url": "https://huggingface.co/spaces/cwadayi/streamlit_earthquake_location"},
{"title": "🔍 地震監測流程 (擷取-撿拾-定位)", "url": "https://huggingface.co/spaces/cwadayi/earthquake_monitoring"},
{"title": "📊 Google Sheet 資料顯示 (類Grafana)", "url": "https://huggingface.co/spaces/cwadayi/Grafana_like_2"},
{"title": "🏠 PWS 地震預警顯示", "url": "https://cwadayi-app-show-pws.hf.space/cwa-earthquakes"},
{"title": "🌍 全球地震顯示 (Streamlit)", "url": "https://huggingface.co/spaces/cwadayi/streamlit"}
],
"💻 資料擷取與 API 應用": [
{"title": "💬 LINE ROBOT 伺服器", "url": "https://huggingface.co/spaces/cwadayi/LINE-ROBOT"},
{"title": "🗺️ 震央分佈圖 (MCP-2)", "url": "https://huggingface.co/spaces/cwadayi/MCP-2"},
{"title": "📥 PWS 資訊擷取", "url": "https://huggingface.co/spaces/cwadayi/MCP-pws"},
{"title": "🏛️ USGS 地震資料 API", "url": "https://huggingface.co/spaces/cwadayi/Usgs_api_gemini"},
{"title": "🇹🇼 CWA 地震資料 API", "url": "https://huggingface.co/spaces/cwadayi/CWA_API_chatgpt5_1"}
],
"🏫 地球物理課程與教材": [
{"title": "🎓 地球物理課程 (總覽)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_class"},
{"title": "📗 地物課程網站 (Day1)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_day1"},
{"title": "📘 地物課程網站 (Day2)", "url": "https://huggingface.co/spaces/cwadayi/Geophysics_day2"},
{"title": "📏 折射震測展示 (SDD)", "url": "https://huggingface.co/spaces/cwadayi/Refraction_sdd"},
{"title": "📐 折射震測展示 (Refraction 3)", "url": "https://huggingface.co/spaces/cwadayi/Refraction_3"},
{"title": "⚖️ 重力異常展示 (Bouguer & Free-air)", "url": "https://huggingface.co/spaces/cwadayi/Gravity"}
]
}
def recommend_project():
all_projects = [item for sublist in projects_data.values() for item in sublist]
recommendation = random.choice(all_projects)
return f"""
### {recommendation['title']}
[**點此立即前往 🚀**]({recommendation['url']})
"""
TUTORIAL_REQUIREMENTS = textwrap.dedent("""
gradio
obspy
folium
""")
# -------------------------------------------------------------------
# 自訂 CSS
# -------------------------------------------------------------------
css = """
/* 標頭樣式 */
#main-header {
background: linear-gradient(135deg, #2c3e50, #34495e);
padding: 2rem 1.5rem;
border-bottom: 5px solid #1abc9c;
}
#main-header h1 {
font-size: 2.8rem;
font-weight: 700;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
margin-bottom: 0.5rem;
text-align: center;
}
#main-header p {
font-size: 1.2rem;
color: #ecf0f1;
max-width: 600px;
margin: 0 auto;
text-align: center;
}
#header-image {
width: 100%;
max-height: 250px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 1rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
/* Tab 按鈕美化 */
#main-tabs > .tab-buttons > button {
background-color: #f0f2f5;
color: #555;
font-size: 1.05rem !important;
font-weight: 600 !important;
border: 2px solid #e0e0e0 !important;
border-radius: 10px !important;
margin: 0 5px !important;
padding: 10px 20px !important;
transition: all 0.3s ease !important;
}
#main-tabs > .tab-buttons > button:hover:not(.selected) {
background-color: #e8e8e8;
border-color: #ccc;
transform: translateY(-2px);
}
#main-tabs > .tab-buttons > button.selected {
background-color: #1abc9c !important;
color: white !important;
border-color: #1abc9c !important;
box-shadow: 0 4px 10px rgba(22, 160, 133, 0.3);
}
/* 推薦按鈕 */
#recommend-button {
background: linear-gradient(135deg, #1abc9c, #16a085);
color: white !important;
font-size: 1.1rem !important;
font-weight: bold !important;
border: none !important;
border-radius: 10px !important;
transition: all 0.3s ease !important;
}
#recommend-button:hover {
background: linear-gradient(135deg, #16a085, #1abc9c);
transform: scale(1.02);
box-shadow: 0 5px 15px rgba(22, 160, 133, 0.4);
}
/* 推薦輸出框 */
#recommend-output {
border: 2px dashed #1abc9c;
border-radius: 10px;
padding: 1.5rem;
text-align: center;
background-color: #f8fdfc;
}
#recommend-output h3 { margin-top: 0.5rem; margin-bottom: 1.2rem; color: #2c3e50; font-size: 1.4rem; }
#recommend-output a { font-size: 1.1rem; font-weight: bold; color: #2980b9; text-decoration: none; transition: color 0.3s ease; }
#recommend-output a:hover { color: #3498db; text-decoration: underline; }
/* 專案標題 */
.project-title h3 { margin: 0; padding: 0.5rem 0; font-size: 1.2rem; color: #2c3e50; }
/* 專案按鈕 */
.project-button a {
background-color: #3498db !important;
color: #ffffff !important;
border-radius: 8px !important;
transition: background-color 0.3s ease, transform 0.2s ease !important;
}
.project-button a:hover { background-color: #2980b9 !important; transform: scale(1.03); }
/* 頁腳 */
#footer { text-align: center; color: #777; font-size: 0.9rem; margin-top: 2rem; }
"""
# -------------------------------------------------------------------
# Gradio 應用程式主體
# -------------------------------------------------------------------
with gr.Blocks(css=css, title="地球物理 x Hugging Face 專案展示") as demo:
# 標頭區
with gr.Row(elem_id="main-header"):
gr.Markdown(
"""
<img id="header-image" src="https://images.pexels.com/photos/220201/pexels-photo-220201.jpeg" alt="Abstract seismic waves banner">
<h1>地球物理 x Hugging Face 專案展示</h1>
<p>一個地球物理學家的數位百寶箱:探索一系列由 cwadayi 打造的地震與地科應用!</p>
"""
)
# 主內容區:Tab 介面
with gr.Tabs(elem_id="main-tabs") as tabs:
# --- Tab 1: 專案總覽 ---
with gr.TabItem("📚 專案總覽", id=0):
# 幸運推薦區
gr.Markdown("---")
with gr.Blocks():
gr.Markdown("## ✨ 幸運推薦 ✨")
recommend_btn = gr.Button("不知道從何逛起? 點我隨機推薦一個專案!", elem_id="recommend-button")
recommend_out = gr.Markdown("點擊上方按鈕,讓我為您挑選一個有趣的專案!", elem_id="recommend-output")
gr.Markdown("---")
# 專案列表區
gr.Markdown("## 🗂️ 專案列表")
for category, items in projects_data.items():
with gr.Accordion(category, open=True):
for item in items:
with gr.Row(variant="panel", equal_height=True):
with gr.Column(scale=4):
gr.Markdown(f"### {item['title']}", elem_classes="project-title")
with gr.Column(scale=1, min_width=150):
gr.Button(
"前往應用 ↗",
link=item['url'],
elem_classes="project-button"
)
# --- Tab 2: 範例 1 (即時編輯) ---
with gr.TabItem("📖 範例 1: 地科字典", id=1):
gr.Markdown("# 🚀 範例 1: 地科字典 (Live Demo & Editor)")
basic_code_state = gr.State(value=TUTORIAL_CODE_BASIC)
with gr.Tabs():
with gr.TabItem("🚀 Live Demo"):
gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡查看結果。")
dropdown = gr.Dropdown(
label="請選擇一個地科名詞",
choices=list(TUTORIAL_BASIC_INITIAL_DICT.keys()) # 從預設字典載入
)
output_textbox = gr.Textbox(label="名詞解釋", lines=5, interactive=False)
with gr.TabItem("✏️ Edit Code"):
gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
basic_code_editor = gr.Code(
label="app.py (Level 1 範例)",
value=TUTORIAL_CODE_BASIC,
language="python",
interactive=True,
lines=20
)
apply_btn_basic = gr.Button("套用變更", variant="primary")
apply_msg_basic = gr.Markdown("")
# --- 範例 1 的邏輯綁定 ---
def update_basic_code(new_code):
local_scope = {}
try:
parsed_code = ast.parse(new_code)
exec(compile(parsed_code, "<string>", "exec"), {"__builtins__": {}}, local_scope)
new_dict = local_scope.get("geo_dict")
if not isinstance(new_dict, dict):
raise ValueError("找不到 `geo_dict` 變數或其不是一個字典。")
new_choices = list(new_dict.keys())
return {
basic_code_state: new_code,
dropdown: gr.Dropdown(choices=new_choices, label="請選擇一個地科名詞"),
apply_msg_basic: gr.Markdown("✅ 變更已套用!請回到 'Live Demo' 分頁查看。")
}
except Exception as e:
# 保持原狀,並顯示錯誤
return {
basic_code_state: basic_code_state,
dropdown: dropdown,
apply_msg_basic: gr.Markdown(f"❌ 程式碼錯誤,未套用:\n```\n{e}\n```")
}
apply_btn_basic.click(
fn=update_basic_code,
inputs=[basic_code_editor],
outputs=[basic_code_state, dropdown, apply_msg_basic]
)
dropdown.change(
fn=run_basic_code,
inputs=[dropdown, basic_code_state],
outputs=output_textbox
)
# --- Tab 3: 範例 2 (即時編輯) ---
with gr.TabItem("🌍 範例 2: 地震地圖", id=2):
gr.Markdown("# 🚀 範例 2: 進階地震地圖 (Live Demo & Editor)")
advanced_code_state = gr.State(value=TUTORIAL_CODE_ADVANCED)
with gr.Tabs():
with gr.TabItem("🚀 Live Demo"):
gr.Markdown("在這裡測試你的程式碼!修改「Edit Code」分頁中的內容,按下「套用變更」,然後在這裡點擊「搜尋」。")
with gr.Row():
mag_slider = gr.Slider(minimum=4.0, maximum=8.0, value=5.0, step=0.1, label="最小地震規模 (M)")
days_slider = gr.Slider(minimum=1, maximum=30, value=7, step=1, label="搜尋天數 (過去幾天)")
search_button = gr.Button("搜尋並繪製地圖 🌍", variant="primary")
map_output = gr.HTML(label="地震分佈圖")
with gr.TabItem("✏️ Edit Code"):
gr.Markdown("在這裡修改 Python 程式碼,然後按下「套用變更」儲存。")
advanced_code_editor = gr.Code(
label="app.py (Level 2 範例)",
value=TUTORIAL_CODE_ADVANCED,
language="python",
interactive=True,
lines=40
)
apply_btn_advanced = gr.Button("套用變更", variant="primary")
apply_msg_advanced = gr.Markdown("")
# --- 範例 2 的邏輯綁定 ---
def update_advanced_code(new_code):
# 僅儲存,我們在點擊 "Search" 時才做完整測試
return new_code, "✅ 變更已儲存!請回到 'Live Demo' 分頁並點擊 '搜尋' 來執行新程式碼。"
apply_btn_advanced.click(
fn=update_advanced_code,
inputs=[advanced_code_editor],
outputs=[advanced_code_state, apply_msg_advanced]
)
search_button.click(
fn=run_advanced_code,
inputs=[mag_slider, days_slider, advanced_code_state],
outputs=map_output
)
# --- Tab 4: AI 創客空間 (教學) ---
with gr.TabItem("🎓 AI 創客空間", id=3):
gr.Markdown(
"""
<div style="padding: 1rem 0;">
<h2>🚀 打造你自己的地科 App!</h2>
<p>這是一個教你如何「跟 AI (例如 Gemini) 協作」來快速打造並部署應用程式的SOP:</p>
<p><strong>最好的學習方式就是動手修改!</strong> 請前往「範例 1」和「範例 2」分頁中的 <strong>"✏️ Edit Code"</strong> 標籤,直接修改程式碼並查看即時結果!</p>
</div>
"""
)
with gr.Accordion("Level 1: 基礎字典 App 📖 (點我展開)", open=False):
gr.Markdown("### 部署你自己的版本:")
gr.Markdown(
"""
<ol>
<li>到 Hugging Face 網站,點選你的頭像,選擇 <strong>"New Space"</strong>。</li>
<li>為你的 Space 命名 (例如: <code>my-geo-dictionary</code>)。</li>
<li>在 "Space SDK" 中選擇 <strong>"Gradio"</strong> (很重要!)。</li>
<li>點擊 "Create Space"。</li>
<li>在你的 Space 頁面,點選 "Files and versions" -> "Add file" -> "Create new file"。</li>
<li>將檔案命名為 <strong><code>app.py</code></strong> (必須是這個名字!)。</li>
<li><strong>將下面的整段程式碼,完整貼到 `app.py` 的編輯器中。</strong></li>
<li>點擊 "Commit new file" (提交檔案)。</li>
</ol>
"""
)
# [修復] 修正教學程式碼,使其可以獨立運作
standalone_basic_code = (
"import gradio as gr\n\n" +
TUTORIAL_CODE_BASIC +
"\n\nwith gr.Blocks() as demo:\n" +
" gr.Markdown('# 🚀 我的第一個地科 App (Level 1)')\n" +
" dropdown = gr.Dropdown(label='請選擇一個地科名詞', choices=list(geo_dict.keys()))\n" +
" output_textbox = gr.Textbox(label='名詞解釋', lines=5, interactive=False)\n" +
" dropdown.change(fn=get_definition, inputs=dropdown, outputs=output_textbox)\n\n" +
"demo.launch()\n"
)
gr.Code(
label="app.py (Level 1 範例)",
value=standalone_basic_code,
language="python",
interactive=False
)
with gr.Accordion("Level 2: 進階地震地圖 App 🌍 (點我展開)", open=False):
gr.Markdown("### 部署你自己的版本:")
gr.Markdown(
"""
<p>這次你需要上傳 <strong>2 個檔案</strong>到你自己的 Hugging Face Space:</p>
<h4>1. <code>requirements.txt</code></h4>
<p>建立這個檔案並貼上下面的內容,告訴 Hugging Face 你需要 `obspy` 和 `folium`。</p>
"""
)
gr.Code(
label="requirements.txt (必備)",
value=TUTORIAL_REQUIREMENTS,
interactive=False
)
gr.Markdown("<h4>2. <code>app.py</code></h4><p>建立 `app.py` 檔案並貼上下面的內容。</p>")
# [修復] 修正教學程式碼,使其可以獨立運作
standalone_advanced_code = (
"import gradio as gr\nimport folium\nfrom obspy.clients.fdsn import Client\nfrom obspy import UTCDateTime\n\n" +
TUTORIAL_CODE_ADVANCED +
"\n\nwith gr.Blocks() as demo:\n" +
" gr.Markdown('# 🚀 我的進階地科 App (Level 2)')\n" +
" gr.Markdown('使用 ObsPy 從 IRIS FDSN 搜尋即時地震資料,並用 Folium 繪製互動式地圖。')\n" +
" with gr.Row():\n" +
" mag_slider = gr.Slider(minimum=4.0, maximum=8.0, value=5.0, step=0.1, label='最小地震規模 (M)')\n" +
" days_slider = gr.Slider(minimum=1, maximum=30, value=7, step=1, label='搜尋天數 (過去幾天)')\n" +
" search_button = gr.Button('搜尋並繪製地圖 🌍', variant='primary')\n" +
" map_output = gr.HTML(label='地震分佈圖')\n" +
" search_button.click(fn=fetch_and_plot_events, inputs=[mag_slider, days_slider], outputs=map_output)\n\n" +
"demo.launch()\n"
)
gr.Code(
label="app.py (Level 2 範例)",
value=standalone_advanced_code,
language="python",
interactive=False
)
# --- Tab 5: 關於 Spaces ---
with gr.TabItem("ℹ️ 關於 Spaces", id=4):
with gr.Row(equal_height=True, variant="panel"):
with gr.Column():
gr.Markdown(
"""
### 什麼是 Hugging Face Spaces?
Hugging Face Spaces 是一個由 Hugging Face 提供的免費平台,讓開發者和創作者可以輕鬆地建立、分享和展示他們的機器學習 Demo 或網頁應用程式。
它就像是 AI 應用的「YouTube」!您可以使用 Gradio、Streamlit 等 Python 套件快速打造互動介面,或者部署靜態網頁 (HTML/CSS/JS),甚至運行 Docker 容器。
"""
)
with gr.Column():
gr.Markdown(
"""
### 🚀 如何部署 Gradio 應用程式?
**這本身就是一個 Gradio 應用程式!** 部署它非常簡單:
1. 在 Hugging Face 建立一個 new Space。
2. C選擇 SDK:選擇 **"Gradio"** (這也是預設選項)。
3. 建立 Space 後,點選 "Files and versions" 分頁。
4. 建立一個名為 `app.py` 的檔案。
5. **將這份 Python 程式碼**貼到 `app.py` 中並提交。
6. (進階) 如果你的 App 需要 `obspy` 這樣的額外函式庫,請再建立一個 `requirements.txt` 檔案並列出它們。
7. **完成!** 您的Gradio應用程式將自動建置並上線。
"""
)
# 頁腳
gr.Markdown(
"""
<hr>
<p id="footer">此頁面由 Gemini 協助生成,展示 "1022 中央地科演講" 中的 Hugging Face 專案。</p>
"""
)
# 7. 綁定按鈕點擊事件
recommend_btn.click(
fn=recommend_project,
inputs=None,
outputs=recommend_out
)
# 8. 啟動應用程式
if __name__ == "__main__":
demo.launch()