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())