何のための取り組みですか? 仕入台帳は紙のままだと、「あの月にいくら 仕入れたか」を調べるのに冊子をめくって探すしかありません。これをパソコンの表データに しておけば、検索や集計がすぐにできるようになります。まずは手元にある10冊分を 試しにスキャンし、読み取りの精度と、確認作業のやり方を検証しています。
読み取りの流れ
仕入台帳は、納品書のような1枚ものの伝票と違い、ノートを開いた見開き2ページに 何行も書き込んである帳簿です。行の数もページごとにバラバラなので、既存の納品書OCRの 仕組みをそのまま使うのではなく、帳簿向けに手順を組み直しました。
台帳をスキャンする
台帳を開いたまま、見開き2ページ分をまとめてスキャンします。中央には綴じ穴が 写り込みます。
左右のページに自動で分ける
見開きの真ん中あたり——綴じ穴のある帯——は、文字が書かれていない分、周りより 「インクの少ない場所」になります。これを目印にして、コンピューターが自動的に 左ページと右ページを切り分けます。


仕組み(技術)
基準の用紙に位置を合わせる(試験導入)
納品書OCRと同じ「お手本の用紙にぴったり重ねる」仕組みを流用しようとしましたが、 仕入台帳の用紙は同じ模様の罫線が等間隔に並ぶため、コンピューターにとって 「どこが基準の位置か」を見分けにくいことが分かりました。現状はこの位置合わせが うまくいかないページが多く、その場合は元のスキャン画像のまま次の工程へ 進めています。読み取り自体はページ全体をAIに見せる方式のため、この位置合わせが うまくいかなくても結果への影響は小さいことを確認済みです。
この位置合わせは今のところ「うまくいけば読み取りの精度が さらに上がるかもしれない」という位置づけの改善項目で、現在の読み取り結果の正しさには 影響していません。
AIがページ全体の表を読み取る
納品書OCRでは「この様式は必ず7行」のように行数を決め打ちできましたが、仕入台帳は ページによって書き込まれている行数がまちまちです。そこで今回は、 行数を決め打ちせず、AI(Google Gemini)に「書き込みのある行だけ、上から順に 読み取ってください」と指示する方式にしました。品名が2行にまたがって書かれている 場合は1行分にまとめて読み取るよう指示し、消費税・振込・締めの行なども種類分けして 読み取らせています。
計算が合っているか自動でチェックする
仕入台帳には、ひと月ごとなどの区切りに「締め」の行があり、そこにその期間の 仕入合計・支払合計・差引残高がまとめて書かれています。読み取った明細を1行ずつ 足し合わせて、この「締め」の数字と一致するかを自動で確かめます。一致しない場合は 画面上で赤く表示され、どこを見直すべきかがひと目で分かります。
人が画面で見比べて確認する
最後に、元のスキャン画像とAIの読み取り結果を左右に並べた画面で、 人が実際の帳簿と見比べながら確認します。
これまでの結果
手元にある10冊分のスキャンを、上記の流れで一通り処理しました。
| 項目 | 結果 |
|---|---|
| 処理した台帳の冊数 | 10冊 |
| 読み取れた行の合計 | 約 555 行 |
| 読み取りエラー(自動での読み取りに失敗し、要再確認) | 0 件 |
品名・数量・単価などの明細は、実際の帳簿と見比べた範囲では概ね正確に読み取れています。 1冊あたりの処理時間はおよそ1〜2分です。
今、確認をお願いしたいこと
「締め」の合計と、明細を足し合わせた金額が一致しないケースが多く見つかっています。 たとえば1冊目の最初の区切りでは、帳簿に書かれた「仕入金額計」は 161,849円でしたが、 その区切りに含まれる明細行をすべて足し合わせると 140,704円にしかならず、 21,145円分の差がありました。差引残高の欄は締めの仕入金額計と一致しているため、 AIの読み取りそのものが大きく外れているわけではなさそうですが、原因は まだ特定できていません。
考えられる理由としては、①このページに写っていない前のページからの繰り越しが 含まれている、②帳簿の余白に書かれた「合計の検算メモ」を明細の一部と誤認識している、 ③単純な読み取りミス、などが考えられますが、これは帳簿の運用に詳しい方に実物を 見ていただかないと判断が難しい部分です。次の「確認画面」で、金額が赤字になっている 区切りを実際の帳簿と見比べていただければ幸いです。
確認画面の見かた
読み取り結果を確認できる画面を、社内ネットワーク内に用意しています。会社のパソコンから、 ブラウザで次のアドレスを開いてください(社外からはアクセスできません)。
http://ledger-review.local/
台帳ごとに折りたたまれた一覧が表示されます。タイトルをクリックすると開閉できます。 赤字の「不一致」は上記「確認をお願いしたいこと」に対応する箇所です。
技術詳細(ITご担当者向け)
本機能は既存の納品書OCR(takahashi/services/ocr_ingest)と同じ設計方針
(剛体変換のみ・透視補正なし)を踏襲しつつ、帳簿特有の「行数が可変」
という性質に合わせて、別モジュール takahashi/services/ledger_backfill として
新規実装した。DB/記帳(買掛元帳への反映)は対象外で、現時点は
構造化データを人手レビューできる状態にするところまでのスコープ。
パイプライン全体
PDF (見開きスキャン) ──300dpi レンダリング (PyMuPDF, ocr_ingest.render_page を再利用)
→ split_spread 綴じ目をインク密度の谷として検出し左右ページへ分割 (新規)
→ FormRegistrar 仕入先フォーム整合と同じ仕組みを流用 (ocr_ingest, SIFT+RANSAC)
— 罫紙は反復模様が多くインライア0のケースが大半、無変換フォールバック
→ gemini_extract google.genai を直接呼び出し (ocr_form_kit.read は経由しない)
response_schema を「上限なし配列」にして可変行数に対応
→ records.normalize_rows 金額・数量パースは ocr_ingest.records の関数を再利用
→ reconcile.reconcile_blocks 締め行区切りブロック単位で仕入金額計を検算
→ __main__ split/extract/review/verify CLI
1. 見開き分割(frontend.split_spread)
納品書OCRの split_slips(緑スキャナ台を背景にした連結成分分割)は
流用できない — 本件はスキャナ台の色を使わない通常のブック平置きスキャンのため。
代わりに、グレースケール化した画像の列ごとの暗画素密度(ink = gray < 200
の列平均)を計算し、画像幅の35〜65%帯で密度最小の列を探索。谷が浅い
(最小値が帯平均の80%以上)場合は中央固定分割へフォールバックし、stderr
へ警告を出す。
2. 基準整合(FormRegistrar 流用)
納品書OCRの FormRegistrar(SIFT特徴点 + estimateAffinePartial2D/
RANSAC による相似変換)をそのまま再利用し、ledger_purchase
プロファイルを1件追加する形で対応。実データでは、罫紙特有の「等間隔・反復する罫線」が
SIFTの局所一意性の前提を崩し、インライア数が0(整合失敗)になるケースがほとんどだった。
FormRegistrar は整合失敗時に無変換で安全にフォールバックする設計のため、
読み取り自体は継続できている。将来、罫線のパターンマッチではなくページ全体の外形
(矩形検出)だけで軽くデスキューする、といった代替案は検討の余地がある。
3. Gemini 直接呼び出し(gemini_extract.py)
ocr_form_kit.read() は経由しない。同ライブラリのテンプレートは
PowerPointで手描きした「行数固定」の枠をプロンプトに焼き込む設計(納品書のような
既知の様式向け)で、行数が可変な帳簿には合わない。google.genai を直接呼び、
response_schema に上限なしの配列を指定することで可変行数に対応した。
モデルは gemini-3.5-flash(gemini-2.5-flash は導入時点で新規
アカウントから呼び出し不可になっていたため切替)。既定で有効な thinking(内部思考)
トークンが出力上限を食いつぶし、行数の多いページでJSONが途中で切れる不具合が
発生したため、thinking_budget=0 で無効化して解消した。
4. 検算(reconcile.py)
当初は当座勘定のように「前区切りの残高+仕入−支払=差引残高」という一般的な 運用を想定していたが、実データの締め行を複数突き合わせた結果、 差引残高は当該区切り自身の仕入金額計と一致する運用であることが分かった (支払金額は別建てで、残高計算には算入されない)。検算は次の2本に分解している。
balance_matches— 締め行の差引残高が、同じ締め行の仕入金額と一致するかline_items_match— 区切り内の明細行の仕入金額合計が、締め行の仕入金額と一致するか(現状ほぼ全件で不一致 — 本ページ「今、確認をお願いしたいこと」参照)
5. 障害耐性
1半ページの読み取り失敗(JSON解析エラー等)でバッチ全体を止めず、
<stem>.<side>.error.txt に理由を記録して次へ進む
(ocr_ingest.orchestrator の「1件の失敗でバッチを止めない」方針を踏襲)。
構成・実行例
| 役割 | 場所 |
|---|---|
| 見開き分割・整合・Gemini呼び出し・検算・CLI | takahashi/services/ledger_backfill/ |
| 基準用紙プロファイル | var/ocr_profiles/ledger_purchase/(非追跡) |
| 読み取り結果・確認用HTML | var/ledger_backfill/(非追跡) |
# 分割のみ(OCRなし。基準画像作成の素材出しにも使う) python -m takahashi.services.ledger_backfill split bin/docs/仕入台帳*.pdf -o var/ledger_backfill/ # 分割→整合→Gemini読取り→JSON/CSV(GEMINI_API_KEY が必要。--fake でネット無しダミー行) python -m takahashi.services.ledger_backfill extract bin/docs/仕入台帳*.pdf -o var/ledger_backfill/ # 確認用HTMLを生成(LANサービス lan-service init 済み: http://ledger-review.local/ で配信) python -m takahashi.services.ledger_backfill review var/ledger_backfill/ # 手直し済みCSVを再読込→検算のみ再実行 python -m takahashi.services.ledger_backfill verify var/ledger_backfill/*.csv
※ 現時点はスキャン→構造化データ→人手確認までのスコープで、買掛元帳(payable_entries)
への記帳は対象外。将来10年分すべてを取り込む際にどう反映するかは別途検討する。