NotionエクスポートのTurso取り込みとyomitoku OCRパイプラインで専門書をDB化した日

書籍ナレッジベース(book-knowledge-base)の充填を1日かけて回した。午前はNotionのExport機能で出した税務実務書のデータを /notion-import でTurso DBに取り込み、その後はyomitoku(日本語特化AI OCR)→ /restructure-book のパイプラインで財務デューデリジェンス系の専門書をDB化。夕方には、ずっと気になっていたTurso Embedded Replicaのsync詰まりをClaude Codeに調査させて根治までいった。

Notionエクスポートの取り込み(/notion-import)

「Notionのデータを取り込む方法ってどうやるんでしたっけ」から始まった。手順は /notion-import スラッシュコマンドに残してあった。エクスポート形式はPDFではなくHTML+画像+ルートCSV、「サブページのフォルダーを作成」をONにする設定で正解。この設定画面のスクリーンショットもコマンドのassetsに保存させて、次回迷わないようにした。

最初のZIP(76MB)を渡したら、Claude Codeが既存DBと突き合わせて「これは取り込み済みの所得税実務書と同じ書籍です」と返してきた。ルートHTMLのUUIDが一致していたのが根拠。二重取り込みを水際で止めた。

次の法人税実務書のZIPでは、エクスポート構造が想定と違った。「サブページのフォルダーを作成」ONだと各HTMLが独自フォルダを持ち、画像がその中に入る。既存の取り込みスクリプトは非再帰探索だったので拾えない。Claude Codeに3箇所改修させた。

  • build_parsed_files を再帰探索化
  • copy_images をディレクトリ構造保持に変更
  • rewrite_images_in_soup にHTML相対ディレクトリ引数を追加

dry-runで42チャンクの認識と画像srcの書き換えをサンプル確認してから本実行。42チャンク・画像61枚が入った。

取り込み後に踏んだバグ2つ

  • ファイル名に ( ) が含まれてMarkdownのリンク構文が壊れる → URLエンコードで回避
  • 日本語を含む画像パスでAPIが404 → getRouterParam がエンコード済みのまま返していた。decodeURIComponent を入れたら200に

最終的に内部画像61/61が200で配信されるところまで検証。ちなみにdev server確認のとき、Claude Codeがポート3000で起動しようとしたので「3000番台は使ってるんで別のやつにしてください」と止めて5050に変えさせた。

Notionで挫折 → PDFからyomitoku直行に切り替え

午前のもう1冊、コンサル系のドキュメント作成術の専門書はNotionエクスポートからの取り込みを試みたが、こちらはNotion DBではなく階層ページ+目次形式のエクスポートでルートCSVがない。標準フローに乗らないとClaude Codeが構造解析した時点で、「ごめん、これを取り込んで」と裁断スキャン済みのPDFを渡してOCRルートに切り替えた。

/yomitoku フローはこう進む。

  1. PDF冒頭ページから書誌情報を読み取り(奥付で出版社・出版年も確認)
  2. yomitokuをバックグラウンドで実行(222ページが約3分で完了した)
  3. 図表ファイルのリネーム → Turso DBにチャンク投入(203チャンク)
  4. /restructure-book で目次ベースのセクション単位に統合(203 → 35チャンク)

途中、DBに過去のNotion試行の残骸24件が残っていて、しかもタイトルが誤字っていたので削除してPDF版で上書きさせた。

OCRジャンクの除去は人間が見つけて依頼する

restructure後のページをブラウザで眺めていたら、本文の中に文脈の通らない短い断片がぽつぽつ挟まっているのに気づいた。「こういう意味がないところを全ページ見て削除してほしい」と依頼。Claude Codeがパターンを特定した。

  • 図表のラベル断片: <img> の直後に出る短い名詞断片の連続
  • ランニングヘッダー: 章名+節名のペアが本文中に紛れ込む

ただし消しすぎが怖い。図中ラベルと同じ単語が本文の説明にも出てくる箇所があったので、削除対象を検証させたら「本文中のinline説明は残し、図内のラベル断片だけ削除」になっていることを確認できた。28チャンクに適用して完了。

午後: 財務デューデリジェンスの専門書を流し込む

「FDDとかビジネスDDの本って入ってましたっけ?」とDBに聞いたら、63冊の中に正面から扱った専門書はゼロ。手元のM&A関連の専門書3冊を取り込むことにした。

3冊計1,280ページの大物。yomitokuをバックグラウンド逐次実行で開始したが、途中で1冊を別セッションに切り出すことにして「入門書は別のセッションでやります」と指示。Claude Codeは処理を止め、すでに235ページまで出力済みだった分厚い実務書を --pages 234-677 で途中から再開させた。最初からやり直しにならないのが効く。677ページ全体で約43分、582チャンクが入った。

「3部がないんですけど」

restructureで582チャンクを96セクションにマッピングした報告を見て、引っかかった。主要セクション例の一覧に第3部が見当たらない。「3部がないんですけど、なんでないんですか?本当にそれで合ってるんですか?」と突っ込んだ。

答えは、例示の表が第2部・第4部に偏っていただけで、第3部は28セクションぶんマッピング済みだった。全96セクションをチャンク数+PDFページ範囲付きで列挙させて、第3部がp376〜p490に並んでいるのを目で確認してから統合を承認した。報告のサマリーだけ見て進めると、こういう「見せ方の偏り」と「実際の欠落」を区別できない。疑ったら全件列挙させるのが正解だった。

夕方には入門書のほうも別セッションで取り込み(304ページ、約70分)、restructureで264 → 40チャンクに統合。目次相当ページがOCRで生成されていなかったため、Claude CodeがPDFの目次と突き合わせてセクション定義を自前で構築した。

Tursoレプリカ「いつもsyncで詰まってる気がする」を調査依頼

restructureのたびに「Embedded Replicaのsyncがハング → killしてHTTP直接接続に切り替え」という同じ退避を見ていた。今日だけで2回。「レプリカはいつもシンクで詰まっている気がするので、問題として確認してくれませんか」と調査を依頼した。

結果、気のせいではなく構造的な問題だった。

  • ローカルWALファイルが40MBまで膨張していた
  • 過去に9回も壊れて退避された履歴があった
  • 大量UPDATE(restructureの一括更新)をEmbedded Replica経由で書くと、syncがWALを送り切れずハングする

修正方針は読み書き分離。db.py の全writeメソッド(update/delete系)をHTTP直接接続に切り替え、Embedded Replicaは読み取り専用にした。

# write系は HTTP 直接接続(libsql の remote URL)に切り替え
# read系のみ Embedded Replica(_get_conn)を使う

詰まったWALを退避してから動作確認。書き込みは0.19秒で即時完了、20回連続書き込み後もWALは0 bytesのまま。今までrestructureのたびに40MB溜めてハングしていたものが、まったく溜まらなくなった。Issueにも調査結果と解消を記録させた。

今日の学び

  • Notionの「サブページのフォルダーを作成」ONエクスポートは画像が各ページのフォルダに散る。取り込みスクリプトは再帰探索が前提になる
  • Notionエクスポートが階層ページ形式(ルートCSVなし)の本は、Notion経由をあきらめてPDF → yomitokuに直行したほうが速い
  • yomitokuは途中停止しても --pages で再開できる。677ページの大物でも別セッション分割と組み合わせて回せる
  • AIの報告サマリーは例示が偏ることがある。「ない」と感じたら全件列挙させて自分の目で確認する
  • 「いつも詰まってる気がする」レベルの違和感でも調査を依頼すると、WAL 40MB膨張のような構造問題が出てくる。退避を繰り返す前に一度止まって聞く