yomitokuで会計書籍3冊をOCR→Turso DBへ蔵書知識ベース化(再OCR・章再構造化・shelf改善まで)
book-knowledge-base リポジトリで、会計書籍3冊を yomitoku でOCRして Turso DB に格納し、章節整理(restructure)と /shelf のUI改善まで一通り通した。途中で WAL 書込衝突によるレプリカ詰まり、WebFetch の連続失敗、サイドバー表記の見にくさ、ケース境界での前ケース末尾混入、と4種類の壁にぶつかったが、それぞれ別アプローチで解決した。1日の終わりには /shelf に「📖 DB」「済」バッジと星評価4.3(39件)、Amazonリンクが揃って表示されるところまで持っていった。
1冊目: 参考書228ページを3.5分で完走(No866)
朝イチで小さめの参考書(228ページ)を yomitoku の GPU 逐次処理で流した。ScheduleWakeup でPCがスリープしないようにキープして、Monitor で進捗ログを横目に見ながら別作業をしていたら、3分30秒で OCR が完了した。1ページあたり1秒切るペース。
[完了] No866 228p / 図74件 / 188チャンク登録
図ファイル74件をリネームして yomitoku_import.py に通し、188チャンクが Turso に入った。チャンク数がページ数より少ないのは、目次・奥付・空白ページなどでチャンクがマージされたため。FTS で適当な見出しを引いてヒットすることを確認して、1冊目はあっさり完了した。
2冊目: 消費税の参考書624ページ(No865)
そのまま2冊目に入った。消費税系の専門書で624ページ。yomitokuの所要時間を1.5倍くらい見積もっていたが、ページ単位の図が多く OCR が重め。Monitor で進捗を見ていたら、1093ファイル出力・454図まで膨らんだ。
[完了] No865 624p / 1093ファイル / 454図 / 585チャンク登録
585チャンクを Turso に格納。/restructure-book を流す前に書籍メタデータ(書影URL、星評価、レビュー数、Amazonリンク)を埋める必要があるが、ここで Amazon 取得が止まった。
WebFetch連続失敗 → agent-browser フォールバック
amazon_metadata テーブルに格納するため、WebFetch で Amazon の書誌ページを取りに行ったら 連続でブロックを食らった。SSL 系のエラーかボット対策かは判別しなかったが、リトライしても通らない。
グローバルCLAUDE.mdに「WebFetchが失敗したら即座に agent-browser にフォールバックすること」と書いてあったのを思い出して、そのまま切り替えた。
agent-browser open "https://www.amazon.co.jp/dp/..." \
&& agent-browser wait --load networkidle
agent-browser eval 'document.body.innerText'
agent-browser close
ユーザーのChromeプロファイルを使うので 1回でメタデータが取れた。星評価4.3、レビュー39件、書影URL、著者名、出版社情報がそのまま抜けた。/shelf のカード表示に必要なフィールドが一式埋まった。
WAL書込衝突 → HTTP直接接続にリトライ
/restructure-book を Embedded Replica 経由で流したら、WAL ファイルがロックされたまま開放されない事象が起きた。前のセッションで残っていた Python subprocess が WAL を握っていたらしく、新しいトランザクションが書き込めない。
復旧手順:
-
data/replica/*.db-walと*.db-shmを一旦退避 - レプリカファイルを削除して再構築(
syncUrlから fresh sync) - HTTP 直接接続版の restructure に書き換えて再実行
Embedded Replica は読み取りキャッシュとしては効くが、書き込みが詰まると復旧コストが大きい。書き込みは HTTP 直接接続で叩く運用に倒すことにした。前日(4-30)の TAC テキスト一括取り込みで同じ判断をしていたので、スクリプト側のテンプレートはそのまま流用できた。
restructure: 305チャンク → 126セクション
並行で動かしていた3冊目(過去に上下分割マージを失敗した参考書、再OCR完了済み)の /restructure-book を回した。元は 305チャンクでページ単位に切れていたが、ケース毎・章扉・コラム単位でセクションを再定義して 126セクションに統合した。
[restructure] 305 chunks → 126 sections
- ケース本体: 100セクション(ケース1〜100)
- 章扉: 10セクション
- コラム: 16セクション
ケース毎に独立したセクションにしたかったので、目次から拾った「ケース1」「ケース2」…の見出しをセクション境界に置いた。章扉とコラムも独立セクションに切り出して、検索結果がケース単位で返ってくる粒度にした。
HTTP直書きの amazon_metadata がローカル Embedded Replica から読めるか
restructure と並行で書き込んだ amazon_metadata レコードが、ローカルの Embedded Replica から正しく読めるか確認した。HTTP直接接続で書き込むと、Embedded Replica は次回 sync まで古い状態のままになるリスクがある。
// HTTP 直書き直後にローカルレプリカで読む
await client.sync()
const { rows } = await client.execute({
sql: 'SELECT * FROM amazon_metadata WHERE file_no = ?',
args: [866],
})
client.sync() を明示的に挟めば、ローカルから新規レコードが読めることを確認した。同期OK。
古い書籍データの一括削除(No344)
3冊目の再OCR が成功したので、古いほうのレコードを丸ごと削除することにした。chunks 180件 / books 1件 / amazon_metadata 1件(file_no=257)が紐づいていたので、トランザクションで一括削除。
BEGIN;
DELETE FROM chunks WHERE book_id = 'no344-...';
DELETE FROM amazon_metadata WHERE file_no = 257;
DELETE FROM books WHERE id = 'no344-...';
COMMIT;
新しい再OCR版(chunks 305 → restructure 後126セクション)で置き換える形になる。file_no が変わるので、Amazonメタデータ側も新しい file_no で取り直した。
/shelf 画面で表示確認
3冊全部のDB登録が終わったので、/shelf を Chrome で開いて表示を確認した。
- 書影が3冊とも表示されている
- 「📖 DB」バッジ(青)が3冊に出ている
- 「済」バッジ(緑)も3冊揃って出ている
- 星評価 ★4.3 (39件) が表示されている
- Amazonリンクをクリックして商品ページに飛ぶ
/shelf の見た目はバッジが揃って気持ちよかった。前日に追加した「📖 DB」「済」のバッジ並びがそのまま効いている。
ユーザー指摘: サイドバーのページタイトルがわかりづらい
/shelf から書籍ページに飛んで、サイドバーの目次を見たユーザーから「これ、章のタイトルしか出てなくてどのケースか分からない」と指摘が来た。
確認すると、サイドバーは 章タイトルを大見出しとして並べていて、その下に節タイトル(=ケース番号)が小さく出ているだけだった。ユーザーが探しているのは「ケース23: 〇〇」のようなケース名なのに、視覚的には章タイトルが目立っていた。
優先順位を逆にした。ケース名(section)を主役にして、章はグループヘッダーに格下げした。
<!-- 修正後: ケース名が主役、章はグループヘッダー -->
<div v-for="chapter in chapters" :key="chapter.id">
<h3 class="chapter-header">{{ chapter.title }}</h3>
<ul>
<li v-for="section in chapter.sections" :key="section.id">
<a :href="section.path">{{ section.title }}</a>
</li>
</ul>
</div>
サイドバーをスクロールしたとき、目に飛び込んでくるのが「ケース23: 〇〇」になり、章タイトルは薄いグレーのグループヘッダーになった。ユーザー確認OK。
ケース8の境界チャンク混入 → サブエージェントで一括クリーンアップ
/shelf から飛んだケース8のページを開いたら、冒頭にケース7末尾のテキストが混じっていた。restructure でセクション境界を切ったときに、ケース7→ケース8の境界ページがケース8側に寄っていたが、そのページの上半分はケース7の続きだった。
「ケース8だけだろう」と思ってよく見たら、他のケースでも同じパターンが出る可能性があった。境界ページが両ケースにまたがっているケースは構造上どこでも起こりうる。
サブエージェントを起動して、全100ケースの先頭500文字を読み、前ケース末尾と判断できる段落を一括で削除するタスクを投げた。判定基準は:
- 段落の終わりに「次のケースで詳述する」「→ケースX参照」のような送り表現があれば前ケース末尾
- ケース番号の見出しより前にある本文は前ケース末尾と判断
- ケース冒頭の見出し直後に「事例の概要」「論点」のような節があれば、それ以前は削除
サブエージェントが100ケース分を直列で処理して、境界混入があったケースが14件見つかった。チャンク内容を更新する SQL を生成させて、HTTP 直接接続でまとめて UPDATE。修正後、ケース8のページを開き直したら、冒頭が「ケース8: 〇〇」の見出しから始まっていた。
学び
- WebFetch失敗時は即 agent-browser: SSL/ボット対策で WebFetch がブロックされたら、悩まず agent-browser に切り替えるのが早い。Chromeプロファイルを使うので Amazon みたいな認証/対策のあるサイトでも通る
- 書き込みは HTTP 直接接続、読み取りは Embedded Replica: WAL ロック詰まりからの復旧コストが大きいので、書き込み系の処理は最初から HTTP 直接接続で叩く。読み取りキャッシュとしての Embedded Replica は維持しつつ、書き込みパスだけ別に切る
- HTTP直書き後は
client.sync()を明示: HTTP直書きしたデータをローカルから読むなら、sync を明示的に呼ばないと古いキャッシュが返る。書いたら同期、を癖にする - サイドバーは「ユーザーが探している単位」を主役にする: 章/節のような階層構造があるとき、視覚的に目立つのは「ユーザーが探している粒度」であるべき。今回のケースでは章ではなくケースが探索対象だった
- 境界ページの混入はサブエージェントで一括判定: 100ケース分を目視確認するのは現実的ではない。判定基準を明文化してサブエージェントに投げれば、構造的な誤りを一気に潰せる
次にやりたいこと
- 残りの会計書籍(税効果会計、組織再編実務)を順次 yomitoku→Turso→restructure に流す
- サイドバーの「ケース名主役・章はグループヘッダー」パターンを他の問答形式書籍にも展開する
- 境界チャンク混入の自動検出スクリプトを
/restructure-bookの検証ステップに組み込む(restructure 直後に全セクション先頭500文字を機械チェック) - WebFetch失敗→agent-browser フォールバックを
amazon_metadata.pyのリトライロジックに組み込む