skyread / app.py
ericakcc's picture
Upload app.py with huggingface_hub
35ae52e verified
Raw
History Blame Contribute Delete
5.25 kB
"""SkyRead — Gradio app: sounding -> Skew-T plot + dual-layer interpretation.
Run locally:
uv run python app.py
"""
from __future__ import annotations
import threading
import gradio as gr
from matplotlib.figure import Figure
from skyread.indices import compute_indices
from skyread.interpret import interpret_rule_based
from skyread.live import STATIONS, latest_sounding
from skyread.llm import MODEL_ID, interpret_llm, warm_up
from skyread.plot import make_skewt
from skyread.sounding import Sounding, load_csv, load_sample
# Curated, demo-safe example soundings bundled with MetPy (zero network).
EXAMPLES: dict[str, str] = {
"1999-05-04 Oklahoma (強對流 / tornado outbreak)": "may4_sounding.txt",
"2010-01-20 winter case": "jan20_sounding.txt",
"2011-11-11 case": "nov11_sounding.txt",
}
SOURCE_LIVE = "🛰️ 即時探空(鄰近測站)"
SOURCE_EXAMPLE = "📚 經典個案"
SOURCE_UPLOAD = "📄 上傳 CSV"
_MODEL_NAME = MODEL_ID.split("/")[-1]
_BADGE_LLM = (
f"🧠 生活版由 **{_MODEL_NAME}**(本機推論)改寫;"
"同行版與所有數值由 MetPy 確定性計算。"
)
_BADGE_RULE = "📐 規則式判讀(fallback);所有數值由 MetPy 確定性計算。"
def _load_sounding(
source: str, station_label: str, example_label: str, uploaded: str | None
) -> Sounding:
"""Resolve the selected data source into a parsed Sounding."""
if source == SOURCE_LIVE:
return latest_sounding(STATIONS[station_label])
if source == SOURCE_UPLOAD:
if not uploaded:
raise ValueError("請先上傳 CSV 檔")
return load_csv(uploaded, name="uploaded")
return load_sample(EXAMPLES[example_label])
def analyze(
source: str,
station_label: str,
example_label: str,
uploaded: str | None,
use_llm: bool,
) -> tuple[Figure | None, str, str, str]:
"""Run the full chain and return (figure, pro_md, grandma_md, badge_md)."""
# The whole chain is guarded: a CSV can parse fine yet still blow up in
# index computation or plotting (empty profile, increasing pressure, …).
try:
snd = _load_sounding(source, station_label, example_label, uploaded)
indices = compute_indices(snd)
if use_llm:
cards, engine = interpret_llm(indices, snd.name)
else:
cards, engine = interpret_rule_based(indices, snd.name), "rule-based"
badge = _BADGE_LLM if engine == "llm" else _BADGE_RULE
return make_skewt(snd), cards["pro"], cards["grandma"], badge
except Exception as exc: # surface as a friendly message, never a crash
return None, f"⚠️ 讀取失敗:{exc}(可改選經典個案)", "", ""
def _analyze_fast(
source: str, station_label: str, example_label: str, uploaded: str | None
) -> tuple[Figure | None, str, str, str]:
"""Instant first paint on page load: skip the LLM, show rule-based cards."""
return analyze(source, station_label, example_label, uploaded, use_llm=False)
def build_ui() -> gr.Blocks:
"""Construct the Gradio interface."""
with gr.Blocks(title="SkyRead 探空白話判讀器") as demo:
gr.Markdown(
"# 🌤️ SkyRead — 探空白話判讀器\n"
"把艱深的 Skew-T 探空圖,翻成**同行看的指數**與**阿嬤看的帶傘建議**。\n"
"_數值由 MetPy 精確計算,AI 只負責把數字講成人話。_"
)
with gr.Row():
with gr.Column(scale=1):
source = gr.Radio(
choices=[SOURCE_LIVE, SOURCE_EXAMPLE, SOURCE_UPLOAD],
value=SOURCE_EXAMPLE,
label="資料來源",
)
station = gr.Dropdown(
choices=list(STATIONS),
value=list(STATIONS)[0],
label="即時測站(台灣探空未開放於 Wyoming 資料庫,取最近測站)",
)
example = gr.Dropdown(
choices=list(EXAMPLES), value=list(EXAMPLES)[0], label="範例探空"
)
upload = gr.File(
label="探空 CSV (pressure,temperature,dewpoint,direction,speed)",
file_types=[".csv"],
type="filepath",
)
use_llm = gr.Checkbox(
value=True,
label=f"🧠 用 {_MODEL_NAME} 潤飾生活版(CPU 上約半分鐘,但更像人話)",
)
btn = gr.Button("判讀 ☁️", variant="primary")
with gr.Column(scale=1):
plot = gr.Plot(label="Skew-T / Log-P")
pro = gr.Markdown()
grandma = gr.Markdown()
badge = gr.Markdown()
btn.click(
analyze,
inputs=[source, station, example, upload, use_llm],
outputs=[plot, pro, grandma, badge],
)
demo.load(
_analyze_fast,
inputs=[source, station, example, upload],
outputs=[plot, pro, grandma, badge],
)
return demo
if __name__ == "__main__":
threading.Thread(target=warm_up, daemon=True).start()
build_ui().launch(theme=gr.themes.Soft())