iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Modern Web

教練看不到的那六天|從 FIT 檔到 3D 軌跡,馬拉松訓練資料 Dashboard系列 第 13

Day 13|搞定 LINE 記事本裡的教練課表:讓 LLM 讀圖

  • 分享至 

  • xImage
  •  

Day 9 做自動排程的時候,我們已經發現教練課表是唯一還要人工搬運的一段

其他來源都有自己的接口—Garmin 有 API、體重機會廣播、重訓紀錄在 Notion。只有課表,教練發在 LINE 記事本,我得自己截圖、轉傳到 Telegram。

今天要處理這一段。而第一個決定是:不用 OCR。

OCR(Optical Character Recognition,光學文字辨識)就是「把圖片裡的字讀出來」的技術—掃描文件、手機的圖片轉文字都是它。既然課表是圖片,第一個念頭當然是先用 OCR 轉成文字,再寫程式解析。

但這條路不太適合我們。


先看這份課表長什麼樣

教練的課表不是一段自由書寫的訊息,是一張有結構的表:

第一、二組 4區1000m*3~4(組休180秒)

拆開來是:

部分 意思
第一、二組 分組(我在第一組)
4區 四區配速
1000m 每趟距離
*3~4 跑 3 到 4 趟
(組休180秒) 每趟之間休息 180 秒

而「幾區」對應的配速是另一張固定的表:

代號 配速 每 400m
一區 E 5:10/km ±10 秒 2:04
四區 A 4:00/km ±5 秒 1:36
六區 R 3:20/km ±5 秒 1:20

看到那個 1:36/400m 了嗎—那正是 Day 10 那張間歇表裡,Garmin 活動名稱標的目標配速。兩邊本來就對得起來,只是一邊在圖片裡,一邊在 FIT 檔裡。


有固定文法,那寫個解析器不就好了?

這是最直覺的反應,而且 Day 8 才剛用同樣的思路處理過重訓紀錄—找出格式,寫正則。

但課表跟那些不一樣的地方在於它是圖片。要先把圖變成文字,才輪得到正則。

問題不在認字,OCR 認字功能完善,4區1000m*3~4(組休180秒) 這幾個字元它讀得出來,

但認字之後,OCR 回給你的是一堆文字方塊,每個帶著座標,但課表是一張表格—哪一格屬於星期幾、哪一格跨了兩行、哪一段是備註,全部要自己從座標推回來。

換句話說,OCR 幫你跨過的是最簡單的那一步,剩下的表格重建才是麻煩的地方。

但有另一條方式可以完全跳過。


交給 LLM 解析

這條個方式在之前測試 Telegram bot 的時候,就加了圖片處理—當時只是想截圖訓練結果,完全沒想到後來會拿來讀課表:

def _photo_prompt(paths: list[Path], caption: str) -> str:
    """把圖片路徑組成給 claude 的 prompt,請它用 Read 工具看圖。"""
    listing = "\n".join(str(p) for p in paths)
    head = caption.strip() + "\n\n" if caption.strip() else ""
    return (
        f"{head}[使用者傳了 {len(paths)} 張圖片到本機,請先用 Read 工具逐張查看"
        f"再依 CLAUDE.md 規則處理]\n{listing}"
    )

流程是:圖片存到 coach/incoming/ → 告訴 Claude 路徑 → 它用 Read 工具看圖 → 依 CLAUDE.md 的規則處理。

昨天講過 CLI 內建了 Read 工具,而 Read 讀得了圖片。所以「把課表截圖交給 LLM」這件事,不需要任何新程式碼


那今天到底做了什麼

僅是寫了一份規則。

圖片能成功傳送給Claude 讀到,但它不知道我是第幾組、紅字雜訊要跳過,也不知道四區是配速是多少,這些每次都要在對話裡重講一遍。

所以寫進 coach/CLAUDE.md

## 只看第一組、只看黑字

- **只讀「第一組」**:每格第一行會標「第一、二組」之類,取第一組的量。
- **只讀黑字**:紅字是除了台北馬外的賽事專屬資訊,
  我的目標是 **12/20 臺北馬**,紅字那些不適用,直接略過。

加了說明和那張配速對照表。

這就是今天的全部產出。 沒有解析器、沒有新的 handler,只有一份寫給 AI 看的說明文件。


跟 Day 16 是同一個判準

這個做法之後還會再出現一次,Day 16 要做對話式紀錄—「今天 12K 有點喘」自動變成結構化資料,用的也是同一方式:規則寫在 markdown,不寫在 Python。

判準是:規則會不會變、以及變的時候誰要處理。

課表版面或寫法可能會更動改,也可能會換組。這些變動要是寫在正則裡,每次都得改程式碼,重新測試和部署,但寫在 CLAUDE.md 裡的話,改一行字就好。

代價是它還不穩定,資訊需要再做確認。


第一次實測:對照這四週,錯了一個單位

規則寫完,傳兩張課表截圖進去,一次讀出四週。跟實際紀錄一比,趟數全部對得上:

課表 實際做的
8/10 週 · 四區 1000m ×3~4 8/11 跑 4 趟
8/10 週 · 三區 1600m ×2~3 8/13 跑 3 趟
8/17 週 · 三區 1600m ×3~4 8/20 跑 4 趟
8/24 週 · 四區 1000m ×5~6 8/25 跑 5 趟

而且配速也對得上—四區是 1:36/400m,8/25 那場前四組跑 1:37.6~1:37.9,貼著目標下緣,達成率終於算得出來了。

但有一列明顯不對:

| 日 | 長跑 | 1 | 80~100km(週量) | - | - |

每週 80 到 100 公里。 我的實際最長單次是 16K,週量差得遠。

去對原圖才發現,那格寫的是 80~100 分鐘,被讀成了公里。

有意思的是這種錯不會讓程式壞掉。80~100 和 km 都是還算合理的數字跟單位,寫進 markdown 不會馬上被發現。

所以規則裡補了一段:

**課表裡時間與距離混用,讀圖時先確認單位**
不確定的時候寧可回報「這格看不清楚」,不要瞎猜。

這也是這個做法的真實成本—LLM 讀圖不是不會出錯,需要人工審閱。 趟數錯了能馬上會發現,因為它跟我實際跑的對不上;單位錯了則是因為那個數字太離譜才被抓到。要是它把 3~4 讀成 3~5,就很難發現了。


存去哪裡,以及為什麼要分開

解讀完的課表寫進 logs/plans.md,跟訓練紀錄分開存

## 2026-08-24 ~ 2026-08-30
| 日 | 內容 | 區 | 距離 | 趟數 | 組休 |
|---|---|---|---|---|---|
| 二 | 間歇 | 4 | 1000m | 3~4 | 180s |

分開的理由很簡單:課表是教練開的,訓練紀錄是實際做的。

Day 10 那個 planned 欄位的問題就是這麼來的—在規劃版面的時候假設會有課表可以比對,結果沒有,卡片只好從「本週課表達成」改名成「本週訓練」。

現在課表有地方放了,那張卡片之後就有機會改回去。


明天:AI 助教的背景是怎麼來的

課表成功進到系統,Garmin、體重、重訓紀錄也都有了,資料終於比較完整。

但資料齊了,不代表 AI 能有 sense 理解。Day 1 有提及這個系列不做 AI 教練—教練是真人,AI 是中間那層助教,既然是助教,怎麼讓他好好運行就是明天的事啦。

明天要講 NotebookLM 的部分—餵了哪些來源、怎麼驗證它沒有亂講,明天見啦!



上一篇
Day 12|讓 bot 變聰明:Agent SDK vs 自幹 subprocess
下一篇
Day 14|AI 助教的知識從哪來:使用 Gemini Notebook 生成助教人設
系列文
教練看不到的那六天|從 FIT 檔到 3D 軌跡,馬拉松訓練資料 Dashboard14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言