自炊した中学受験向け理科参考書をyomitoku OCRでTurso蔵書DBに取り込み、理科学習ページを4トピック増やした

開発book-knowledge-base

自炊した理科参考書を蔵書DBに取り込み、理科学習ページを4トピック増やした

「これ、まだ取り込んでないんでしたっけ?」から始まった

自炊済みの中学受験向け理科参考書のPDFが手元にあって、蔵書DB(Turso の book-knowledge-base)に入れた気がしていたが、思い出せなかった。Claude Code に books テーブルを調べさせたら、書名の一部で検索してヒットしたのは別の1冊だけ。未取り込みだと確定した。

この参考書は理科の頻出論点を100トピックにまとめた構成で、開発中の理科学習ページ(/science/ 配下)の categories.ts は、最初からこの100トピックの枠組みに対応させて設計してあった。既に6トピック実装済み。素材のDBが空のままでは先に進めないので、「取り込みからリストラクチャーまで」を一気にやらせることにした。

Phase 0: yomitoku OCR → Markdown → トピック単位に統合

313ページのスキャンPDFを yomitoku(日本語特化のAI OCR)にかけた。GPUで1ページ約1.5秒、10〜15分ほどで Markdown 313ページ分と図1,565枚が出力された。

DB格納は1回こけた。scratchpad に置いたスクリプトを絶対パスで実行したせいで src/db.py が見つからなかった、という import パスの問題で、sys.pathsrc を追加させて再実行したら通った。

リストラクチャーは、ページ単位の263チャンクを目次に基づいてトピック単位のセクションに統合する処理。「263チャンクすべてがセクション定義に過不足なく含まれるか」の完全性検証をゲートにして、NGなら中断する設計にした。

作業はセッションをまたいだ。次のセッションに貼った引き継ぎプロンプトは一部が文字化けしていたが、記憶やプロンプトの文面ではなく、ディスク上の実態(OCR出力の有無・DBの格納状態)を確認させてから続きに入った。その上で照合ゲートを流したら一発合格。書籍のメタデータ紐付けまで含めて Phase 0 が完了した。

途中で全文検索の仕様にひとつ引っかかった。この蔵書DBのFTSは trigram tokenizer なので、日本語は3文字以上でないと検索が効かない。「さび」は2文字なのでFTSでは拾えず、LIKE に切り替えた。すると今度は解答編のチャンクまで LIKE に引っかかってきたので、解答編を除外する条件を足した。

公開コンテンツにするための線引き

学習ページは公開する予定なので、計画段階で著作権の扱いを詰めた。Codex にレビューさせたら計3巡で5指摘が出て、「事実チェックのルールと著作権ルールが矛盾している」という指摘はもっともだったので計画書を直した。

方針はこうだ。扱うのは一般的な理科の知識そのものなので、書籍の並びや表現をなぞらず、構成もタイトルも意図的に変える。トピックのタイトルは「実際の書籍と合っていると困る。どんどん変えてほしい」と指示した。書籍の代替物にならないことを、生成時のゲートとして計画書に書き込んだ。

先に検査網を張った(コミット f8a5f3f4)

バッチ生成を始める前に、2つの仕掛けを整備させた。

  • レジストリ整合性テスト: categories.ts の登録内容とコンポーネントの実在が食い違っていないかを検証するテスト
  • 原文混入チェッカー: 生成したコンテンツに書籍の原文がそのまま混ざっていないかを機械的に照合するチェッカー

エージェントを並列で走らせると、生成物を1本ずつ目視する時間はない。人間の目の代わりに機械の網を先に張っておく、という順番にした。

バッチ1: 燃焼カテゴリの残り4トピック(コミット 13a921df)

「物の燃え方」カテゴリの残り4トピック(炭作り・空気と燃焼・金属の燃焼・さび)を、生成エージェント4体の並列で作らせた。各エージェントからは「10問クイズ+4コンポーネント生成、原文混入チェック1回目で合格」「計算問題は全問検算済み」「混合物のつるかめ算まで検算済み」といった報告が順に返ってきた。

申し送りも上がってきた。相互参照のトピック名が「ろうそく」と「ロウソク」で揺れている可能性、還元(酸化銅の還元)は本トピックのスコープ外として意図的に除外した判断。こういう判断メモが残ると、登録時の突き合わせと後続バッチの計画がやりやすい。

レートリミットで中断

4体完了して検証ステップに入った直後、画面に「You've hit your session limit · resets 4:10pm」の同じ行が4回並んだ。ここは待つしかないので、解除されてから「続きお願いします」で再開した。

セクションが空に見えるバグの切り分け

dev サーバーで表示確認をしたら、新規トピックのセクション2が空に見えた。コンソールログを grep させ、既存トピック(ろうそく)では正常に遷移することを確認し、モジュール単体の import も成功。クリック時のエラー本文をフックして捕まえようとしたら、フックを仕掛けた画面が既存トピックのページのままだった、という切り分けミスに気づいて対象ページに戻る一幕もあった。最終的に4トピックとも表示を確認できた。

事実チェックで1件訂正

生成物の事実チェックで、線香花火の火花を「鉄」と説明している箇所を見つけた。あの火花は炭素由来なので訂正した。原文混入チェックを通っても、理科的な誤りは別の網で拾う必要がある。

現在地を計画書に残して終了(コミット ea80072f)

今日はここで切り上げると決めて、計画書に現在地セクションを追記させた。Phase 0(書籍のTurso取り込み+照合ゲート合格)→ 事前整備(整合性テスト+原文混入チェッカー)→ バッチ1(燃焼4トピック、検証・事実チェック・コミットまで)が完了。次はバッチ2、「力と運動」カテゴリの8トピック。

学び

  • trigram tokenizer のFTSは日本語3文字以上が前提。2文字キーワードは LIKE に切り替え、解答編など不要チャンクの除外条件をセットで考える
  • 一括生成の前に検査網(整合性テスト+原文混入チェッカー)を張ると、エージェント並列でも生成物を信用して受け取れる
  • 原文混入チェックと事実チェックは別物。線香花火の誤りは後者でしか拾えなかった
  • 公開コンテンツは「書籍の代替物にならない構成」を計画書のゲートに明文化しておくと、生成エージェントへの指示が迷わない
  • 画面の違和感を拾うのは自分、切り分けと修正はAI係。ただし切り分け中に「どのページを見ているか」を取り違えると、AIも自分も同じ穴に落ちる
  • セッションをまたぐときは、引き継ぎプロンプトの文面よりディスク上の実態を先に確認させる。文字化けしていても実態が合っていれば続行できる

次にやること

  • バッチ2: 「力と運動」カテゴリの8トピックを生成する
  • 相互参照のトピック名表記(「ろうそく」と「ロウソク」)の揺れを横断で確認する
  • スコープ外として除外した還元(酸化銅の還元)を、後続バッチのどこで扱うか決める