手書き数値表42枚のOCRで、マルチモーダルLLMをレート制限ごとに乗り換えた記録
地域密着のサービス業の記録を電子化する作業を続けている。 紙に手書きで残っていた数値表を撮った画像が42枚あり、これを機械が読める CSV に起こしたい。 前日は Gemini 3.1 Pro に読ませて9枚まで進み、429 が返ったところで止まっていた。
朝いちばんに思ったのは、モデルを最新の Flash に替えれば枚数が伸びるだろう、ということだった。 レート制限も一晩たてば解除されているはずだ、と踏んでいた。
外れた。
一晩で枠は戻っているはず、という読み
疎通の1発目が2分待っても返ってこない。 枠が切れているのか、単にモデルが遅いのかを切り分けさせたら、制限はまだ効いたままだった。 回復予定は3日後の朝。
待つと決めた。 ただし枠が開いた瞬間に動けるよう、Flash 対応と枠切れの早期検知をスクリプトへ入れさせておいた。 枠が切れている今なら、その検知が本当に働くかを実地で確かめられる。 走らせたら、想定どおり止まった。
ここで気になったのは、確かめ方そのものだった。 叩いて確かめる行為は、それ自体が枠を1回食う。 公表されているトークン単価と画像の実寸から計算できるなら、消費せずに見積もれるはずではないか。 そう聞いてみた。
実測せずに枠を見積もれないか
画像は 2268x4032 で、ここからトークン数は出せる。 ただし肝心なのは、使っている CLI 側の枠が何で測られているかだ。 公表仕様を当たらせたところ、試算とは別のものが引っかかった。
モデル一覧が「Gemini」と「Claude / GPT」で節に分かれている。 枠も分かれているのではないか、という見立てが立った。 叩かせたら、Gemini 系だけが切れていて、Claude / GPT 系は生きていた。
基準にしている1枚を Sonnet 4.6 に読ませると、136件すべてが正解データと一致した。 2分弱で終わっている。 残り33枚を投入した。 1枚あたり約35秒で流れていく。
14枚を読んだところで、朝入れておいた枠切れ検知が働いて止まった。 それでも合計は23枚まで来ている。
途中、1枚だけ278件と突出した画像があった。 他は130件前後なので、読み間違いを疑った。 中身を確認させたら、同じ CSV を2回出力していただけだった。 2ブロックは完全に一致していて、データとしては無傷だった。 まとめスクリプトを重複ブロックに対応させて先へ進めた。
枠は世代ごとにも分かれているのか
Gemini 側の枠は、世代ごとにも分かれていた。 3.8 Flash と 3.1 Pro は枯れているのに、3.7 と 3.6 の Flash は生きている。 前日に立てた「アカウント共通の枠」という見立ては、そもそも粗すぎた。
3.7 Flash にも基準の1枚を読ませたら、こちらも136件すべてが一致した。 残り19枚を回させた。
ここで実装ミスが出た。 処理済みディレクトリの指定に Sonnet 分を入れ損ねていて、すでに読み終えた14枚を 3.7 Flash が読み直している。 完走を待ってから直すことにした。 捨てるつもりだった重複が、結果として別系統2モデルの突き合わせ材料になる。
一致率3分の1の正体
突き合わせてみると、件数は同じなのに一致が3分の1しかない画像があった。
最初に疑ったのは並び順だった。 この表は先頭の数字の昇順で印刷されているように見えるので、片方だけ並びが崩れているのではないか。 機械的に全数チェックさせたら、この仮説は消えた。 目視で作った正解データ自身にも同じ逆行が出てくるので、順序は判定基準に使えない。
順序を無視して中身だけを比較する形に切り替えた。 結果は、1,883件すべてで中身が一致した。 違いは並び順だけで、残る食い違いは1枚の7件にとどまった。 独立に読ませた2モデルが同じ答えを返したのだから、読み取り精度の裏付けとしては強い。 この比較をその場かぎりにせず、正式なスクリプトとして残させた。
3.6 Flash も 3.7 と同じプールで、すでに枠が切れていた。 使えるモデルが尽きて、35枚で打ち止めになった。 通し番号は0から83まで連番で埋まっている。 残り7枚は3日後の朝を待つしかない。
残り7枚をブラウザから入れる
画像ファイルのフルパスを聞いて、HEIC ではなく JPEG に変換したディレクトリを作らせた。 42枚をまとめて変換し、全部が正常に読めることまで確認させている。
そこから1枚を自分でブラウザの AI Studio に貼り、出てきた CSV をセッションへ貼り付けた。 突き合わせさせたら、手元にあった4つ(目視で作った正解データを含む)すべてと136件が一致した。 一致率100%。
なら、この貼り付け作業自体を渡してしまえばいい。 自分が手で貼るのと、ブラウザを操作させるのとで、手間は大して変わらない。 気になったのはコンテキストのほうで、画像を7枚も扱ってセッションが重くならないかと聞いた。
杞憂だった。 画像は読ませるのではなくブラウザへアップロードするだけで、返ってくるのはテキスト136行、1枚あたり2KB程度でしかない。 むしろコピー&ペーストより、DOM から直接抜いたほうが取りこぼしがないという。
自分が開いていたタブは触らせず、設定だけ揃えた新しいタブで進めさせた。 ファイル入力が隠れているので一時的に見えるようにする、バックグラウンドタブではメニューが開かないので前面に出す、といった細かい迂回が続く。
送信のたびに403が返る
画像を入れてプロンプトを送ったら、内部エラーが返った。 OCR に不要な検索グラウンディングが干渉している疑いで切らせたが、2回とも同じところで落ちる。
ネットワークとコンソールを見させて、原因が出た。 生成リクエストが 403 で、中身は権限がないという応答だった。 画像がサーバー側に登録されていない疑いが出たので、アップロードの通信まで遡らせたら、そちらは200で成功している。
権限の伝播待ちを疑って、時間を置いてから再実行させたら通った。 アップロード直後の1回目は必ず403で、再実行すれば通る。 癖が分かってしまえば、あとは同じ手順の繰り返しだった。
7枚が順に片付いて、42枚すべてが揃った。 通し番号は0から99まで欠けがなく、範囲外の数字も出ていない。 合計5,666件で、既存の CSV より388件多い。 つまり、これまで拾えていなかった行がその分あった。
差分277件の出どころ
上書きはしたくない。 日付を入れた新しいファイルとして残し、目視で確認できる正式版を1本作ってほしい。 差分も先に見ておいてほしい、と頼んだ。
差分を掘らせると、既存にしかない277件が特定の画像へ集中していた。 その画像では、既存 CSV が通し番号を取り違えている。 ある番号に80件、その次の番号に27件が入っていて、手前にあるはずの番号が存在しない。 件数の足し算も整合した。 既存が正しくて新規が抜けているのではなく、既存側の誤りだった。
ところが、出てきたファイルを開いても、どれが正式版なのか分からない。
比較用の列まで並んでいて、5000行を目で追う形になっていなかった。
列を削ぎ落としたものを作り直させた。
Excel で前のファイルを開いたままだったので、閉じずに済むよう頭に final を付けた別ファイルにしてもらった。
1行だけずれて見える
その final を Excel で開いたら、見出しのあたりが1行ずれて見える。
スクリーンショットを撮って貼り、ずれていないかと聞いた。
ずれではなく文字化けだった。 BOM が無いので Excel が UTF-8 と認識せず、見出し行が壊れて表示されていた。 データ自体は最初から正しい。 既存の CSV も BOM なしのままだった。
BOM を付けて CRLF に統一し、全行が6列になっていることを確認させた。 確認の仕方も、画面を開いて目で見るのではなく Excel 自身に読ませる形にしてある。 行数も列数も正しく、5667行×6列と出た。 PowerShell 経由では日本語が化けるので、Excel が読んだ値を UTF-8 で書き出させて文字まで確かめた。
最後にコミットとプッシュまで回して、今日の分は終わり。
学び
- 利用枠はアカウント単位とは限らない。今回はモデルの系統(Gemini 系と Claude / GPT 系)でも、Gemini の世代(3.8 と 3.7)でも分かれていた。1つが429を返しても、全部が止まったとは限らない
- 同じ画像を別系統の2モデルへ独立に読ませると、それだけで精度の裏取りになる。今回の材料は、処理済み判定の実装ミスが生んだ読み直しだった
- 一致率が低いときは、データより先に比較の仕方を疑う。並び順の違いを中身の違いとして数えていた
- ブラウザ越しに別の LLM を使わせても、返ってくるのがテキストならコンテキストはほとんど食わない。1枚2KBなら7枚でも軽い
- Excel で開いた CSV の「行がずれて見える」は、BOM が無いだけのことがある。データを直す前に文字コードを見る