• #yomitoku
  • #OCR
  • #Turso
  • #libSQL
  • #PDF
  • #書籍
  • #ナレッジベース
  • #バッチ処理
開発book-knowledge-baseメモ

yomitoku で専門書5冊・1181ページを一気にOCRして Turso DB に1057チャンク登録した

別リポジトリ book-knowledge-base で、日本語特化AI OCR の yomitoku を使って手元の専門書を一気に処理した。連結会計の入門書、税効果会計の教科書、財務数値ケース集、連結精算表の入門書、それから試運転として不動産業の漫画PDF。合計1181ページを yomitoku に通し、抽出したテキストと図を Turso DB に1057チャンク登録した。

処理した5冊

#種別ページ数DB登録チャンク抽出図
1不動産業の漫画(試運転10ページ)10-12
2連結会計の入門書352270121
3税効果会計の教科書36334148
4財務数値ケース集182180-
5連結精算表の入門書284266-
11811057181+

最初の漫画PDFは10ページのサンプルだけ流して、形式の確認に使った。16秒で完走した。1ページあたり1.6秒。Markdown 10ファイル + 図 12枚 + layout/ocr の可視化画像(バウンディングボックス付き)が一気に出てきた。

「漫画は縦書きセリフと効果音だらけで yomitoku が崩れるかもしれない」と身構えていたが、レイアウト認識は意外と素直に走った。コマ割りをブロックとして拾い、コマ内のセリフ枠を別ブロックとして識別して、横書きの説明テキストはそのまま読めた。縦書きセリフと効果音の擬音はさすがに崩れたが、地の文はほぼ拾えている。

漫画PDFのサマリー雛形を試作した

漫画PDFで「冒頭の話を1話単位で要約してマークダウンに保存」という雛形を試した。第1話の冒頭8ページを yomitoku に通したあと、抽出したセリフとト書きを 「物語の振り(フック)→ 問い → 答え → オチ」 の4ブロックで抽象化したマークダウンを書いた。

書籍名は伏せておくが、「不動産業の主人公が現場で何かに気付く → なぜそうなるのかという問いが立つ → 答えが提示される → 次回への引きでオチ」という4部構造で第1話冒頭が綺麗に切れた。本一冊(数十話)を同じ雛形で要約していけば、章ごとのストーリーが横並びで読める索引になる。

サマリーの粒度は「ストーリーの骨格だけ残してセリフは捨てる」にした。セリフを残すと著作権の問題に近づくし、骨格だけ残しておけば「この話は何を言いたかったか」が3行で読める。

DB登録時にぶつかった3つのエラー

1. WAL ロックエラー → --replace で抜ける

連結会計の入門書をDB登録する途中で、SQLITE_BUSY: database is locked が出て止まった。libSQL の Embedded Replica は、別のプロセスがWAL書き込み中だと一時的にロックされる。

最初の対処: 数秒待ってリトライ。多くの場合これで解消する。 それでも駄目だった時: --replace オプションを付けて再実行。途中まで入った中途半端なチャンクを上書きで再登録する形にした。

# WAL ロックで途中失敗した場合
pnpm tsx scripts/register-book.ts --book="連結会計入門" --replace

libSQL は SQLite 互換だが Embedded Replica モード特有の短時間ロックがあるので、バッチ処理の途中失敗は想定して --replace を最初から用意しておく方がよかった。

2. ファイル名の「100」が「1OO」に化けていた

抽出した図ファイルのファイル名を一覧していたら、「figure_100.png」と書いたつもりが figure_1OO.png(数字の0ではなく大文字のO、Unicode U+004F)になっていたケースを見つけた。シェルのワイルドカードで figure_1*.png と書くと拾えるが、figure_100.png を文字列リテラルで指定すると当たらない。

# 化けていない正規ファイル名を実物から確認
ls apps/web/public/figures/book-A/ | grep -E "1[O0]{2}"
# → figure_1OO.png ← O U+004F が混入

OCR時にページ番号らしきものを連番化する処理で 0O に化けた可能性がある。実ファイル名を ls で吐き出して目視確認するまで気付けなかった。

3. web/.output/public/figures.gitignore 漏れで追跡されていた

Nuxt の .output/public/figures 配下に yomitoku の抽出図がコピーされていたが、ここが .gitignore に入っていなかったgit status で181枚以上の図が「Untracked files」として並んでいて、コミット対象に紛れ込みそうになった。

# apps/web/.gitignore
+ .output/
+ .output/public/figures/

.output/ 自体は Nuxt の生成物なので、ルートの .gitignore で除外するのが筋。プロジェクト初期に .output/ を追加し忘れていたのが今回の遠因だった。

yomitoku の処理速度

GPU処理(CUDA)で 1ページあたり約1.6秒。1181ページを通した実測の合計時間は約30分(待機時間込み)。CPU処理だと10倍以上かかるので、GPUを積んだマシンで一気に流すのが正解だった。

バックグラウンド実行 + ScheduleWakeup(Windowsのタスクスケジューラからのスリープ復帰)の組み合わせで、夜間に自動で次の本を処理する運用に切り替えた。深夜にスリープから自動復帰してOCR → DB登録 → 次の本へ、という流れを5冊分パイプラインで回した。

学び

  • yomitoku は漫画も意外と読める: 縦書きセリフと効果音以外は、コマ内の横書きテキストと地の文をブロック単位で正しく拾う。「漫画は無理」と決めつけずに10ページサンプルで試す価値があった
  • WAL ロックは少し待てば解消する: libSQL Embedded Replica の短時間ロックは数秒で抜ける。バッチ処理側で --replace を初手から用意しておけば、リトライが楽になる
  • OCR成果物のファイル名は目視確認する: 「100」が「1OO」になる程度の文字化けは、シェル展開では気付きにくい。ls | grep で正規表現を当てて確認する習慣をつける
  • .output/ は最初から .gitignore する: Nuxt の生成物が181枚の画像と一緒に追跡対象になると、git add の事故が起きる。プロジェクト初期化時に .output/ を入れる
  • 漫画サマリーは「振り→問い→答え→オチ」で骨格だけ残す: セリフを残さず構造だけ抽出すると、章ごとの索引として使える形になる。本一冊全部やっても同じ雛形で読める

次にやりたいこと

  • 残りの専門書(あと8冊ほど積んである)を同じパイプラインで一気に流す
  • 漫画PDFの第2話以降を同じ「振り→問い→答え→オチ」雛形でサマリー化し、第1〜10話を横並びで読める索引ページを作る
  • Turso DB に登録した1057チャンクに対して、全文検索(FTS5)のインデックスを張って書籍ナレッジベース側で横断検索を試す
  • yomitoku の --replace 失敗時のリトライをスクリプト側で自動化(最大3回、間隔3秒)して手動再実行を消す