紙の教材をOCRでナレッジベース化する4段チェーンと、出力先ディレクトリの既定値を直した話

開発book-knowledge-base

朝いちばんに蔵書DBを検索した。 目的の教材が出てこない。 Kindle 側にも無い。 「じゃあ今からスキャンします」と決めて、保存ディレクトリに PDF を1本放り込んだ。

あとはいつもの多段チェーンに載せるだけのはずだった。

PDFを通す4段のチェーン

保存ディレクトリに置いた PDF は、4つのコマンドを順に通す。

  • /rename-pdf:pymupdf で表紙まわりの画像を抜き、画像認識でタイトルを判定してファイル名を付け替える
  • /yomitoku:日本語特化の AI OCR で Markdown に変換し、図表を画像として切り出して Turso に格納する
  • /restructure-book:ページ単位のチャンクを、目次に沿ってセクション単位へ統合する
  • /book-cleanup:DB に入ったコンテンツを整理して、検索品質と可読性を上げる

表紙と裏表紙の判定が一致したのでリネームは一発で通り、そのまま OCR に入った。 1冊目は227ページある。 44ページまで進んだところで、完了まで約20分と見込みが返ってきた。

待つあいだに書誌情報を確定させて book_id を決めていたら、ふと引っかかった。 このシリーズ、他の科目も持っていた気がする。 そう投げて、蔵書メタデータと DB の両方を照合させた。

見つからない1冊の行方

照合すると、同じシリーズが蔵書に3冊あり、そのうち1冊だけ未取り込みだと分かった。 ついでなので取り込む列に足した。 別系統の検定テキストも2冊出てきたので、そちらも並べた。 1冊のつもりが5冊になった。

ただ、最初に探していた科目のテキストだけがどこにも出てこない。 DB にも、Kindle にも、スキャン済みの原本置き場にも無い。 買った覚えはある。

これが解けたのは、1冊目の巻末を読んでいるときだった。 シリーズのラインナップページに、公式テキストがあるのは一部の科目だけだと書いてあった。 探していた科目の公式テキストは、最初から作られていない。 記憶にあったのは別の版元から出ている演習書のほうで、そちらは蔵書番号付きで棚にあった。

これで、無いものを探さずに済むようになった。

章扉の巻き込みと装飾図の選別

再構造化は章扉ページを目印にしてセクションを切る。 2冊目でこれが空振りした。 章扉が前のページのチャンクに巻き込まれていて、独立したページとして立っていない。

実物のページ画像を確認させて、統合のときに切り離す処理を足した。 扉12件を分離して、224チャンクが58チャンクにまとまった。 他の巻も同じように圧縮され、218が80、264が89、269が46、218が34になっている。 ページ単位のままだと検索結果が文の途中で切れるので、この統合が効く。

cleanup のほうは図の選別だった。 OCR は図表を片端から切り出すので、セクションのバッジや章番号の数字、矢印、ロゴまでファイルになる。 コンタクトシートを作らせて、装飾かどうかはサブエージェントに並列で判定させた。 1冊目は205枚のうち81枚が装飾図だった。 別の1冊では122件を落とした。 図がローカルに残っていない巻は、R2 から266枚を取り直してから判定した。

別セッションから改名の依頼が来る

作業の途中で、別のセッションから連絡が入った。 2冊の book_id を既存の命名に揃えて、handoff.json を出してほしいという。

付け替えの範囲は R2、DB、本文中の図パス、履歴ファイルの4か所にまたがる。 一括で走らせようとしたらブロックされた。 工程を分けても、R2 のコピーでまたブロック。 ここで粘っても進まないので改名は保留にして、自分が頼んだぶんの仕上げに戻した。 しばらくして依頼元から「改名不要」と返ってきたので、その線で handoff.json を書き直させた。

最後に画面で見た。 サイドバーに新規5冊が「整」「済」バッジ付きで並び、図の入る本文ページもコンソールエラーなしで出ていた。

2ヶ月前のディレクトリが上書きされていた

完了報告のなかに、一行だけ引っかかる記述があった。 「audit ツールが 2026-07-16 の memo ファイルを上書きしていたので戻します」。

なんで7月16日のディレクトリにあるファイルを上書きするのか。 今日の作業と7月の memo は何の関係もない。 戻したという結果より先に、そこが理解できなかった。

推測で答えさせず、スクリプトの既定値を確認させた。 audit_figure_references.py の出力先の既定値に、7/16 に調査したときのディレクトリが直書きされていた。 引数なしで実行すれば、2ヶ月前の成果物を毎回上書きする。

原本は復元できた。 実行日時: 2026-07-16T11:00:40+09:00 の中身に戻り、memo/ の差分はゼロになった。

再発防止の案が2つ出てきた。 選んだのは2番、既定の出力先を実行日のフォルダにする案である。 出力日でフォルダを切ってしまえば、そもそも重複しない。 実行日が変われば行き先も変わるので、既定値が置き去りになることもない。

直させたあと、実際に走らせて今日の日付フォルダに出ること、7/16 が無傷であることまで確かめた。 誤読のもとになっていたスクリプト冒頭の説明もあわせて直させた。

コミットのついでに出てきた2件

コミットとプッシュを頼んだら、origin と1コミットずつ食い違っていた。 中身を見てから、先にコミットしてリベースした。 コミットは2つに分けて 6c7196b..d4b3d6d で通った。

そのあと、dev サーバーが落ちていたと報告が上がってきた。 pnpm dev 2>&1 | head -25 で起動したのが原因で、head が読み終えた時点でパイプを閉じ、SIGPIPE でサーバーごと落ちる。 自分の CLAUDE.md に書いてある罠がそのまま踏まれていた。 直前に受け取った「起動したままにしてあります」という報告も、そのぶん誤りだった。

改行コードの正規化も残っていたので片付けた。 対象は CSV が1本だけで、中身は完全に同一、行末だけの変換だった。 .gitattributes で LF に固定して 2660341 でプッシュした。

残していること

既定値の直書きを直したのは audit_figure_references.py の1本だけで、他のスクリプトに同じ書き方が残っていないかは、まだ見ていない。 今日これに気づけたのは、たまたま7月の memo が git 管理下にあって差分に出たからだ。 差分に出ない場所へ書き出すスクリプトがあったら、上書きは続いたままになる。

#OCR#PDF#Turso#ナレッジベース#スクリプト設計