Spaces:
Running
Running
File size: 5,246 Bytes
6e649fa 35ae52e 6e649fa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | """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())
|