開発book-knowledge-base

きっかけ:「このページ番号って何の意味があるんですか?」

引き継ぎセッションを開いて、まずは前日 restructure をかけ直した あるスクショ取込本 の DB が Web UI でちゃんと読めるか確認しに行った。 書籍ビューアを開いて、ページのドロップダウンをスクロールした瞬間に手が止まった。

表紙が 1、「はじめに」が 2、その次のページが 53

何だこの番号は、と引っかかった。 紙の本ならページが飛ぶことは普通にあるが、これは Kindle Windows アプリのスクリーンショットを OCR して取り込んだ本だ。 紙のページ番号は持っていない。

ということは、この 153PNG の連番でしかない。 表紙の PNG が1枚目、「はじめに」の PNG が2枚目、本文最初の PNG が53枚目。 紙の本のページ番号でもなければ、Kindle の Location 番号でもない。 ただの撮影時のシャッター回数。

読者にとっても自分にとっても、この数字は完全に意味がない。 むしろ「ページ53」と表示されると「え、52ページ分どこ行った?」と無駄な認知負荷を生む。

Phase 0:判定基準を確定する

最初にやったのは「スクショ取込本」をどう判定するかの確定。 DB のスキーマを Claude Code に確認させたら、books.source_path を見れば一発で識別できると分かった。

-- スクショ取込本: source_path が "kindle:" で始まる
-- 通常の PDF/EPUB 取込本: source_path がファイルパス("C:/..." 等)
SELECT id, title FROM books WHERE source_path LIKE 'kindle:%';

これで「PNG 連番ページ番号を持つ本」を 1 クエリで全部引ける。 判定が prefix match だけで済むので、Web UI 側の computed もシンプルに書ける。

Phase 1:Web UI 側でページ番号バッジを非表示にする

意味のない番号は、まずは見せないようにする。 web/app/pages/books/[bookId]/[page].vuehasMeaningfulPageNumbers という computed を1個生やして、 book.source_pathkindle: で始まるときは false を返す。

ページ番号バッジの v-if にそれを噛ませて非表示にした。 画像と本文だけが残るので、見た目もスッキリ。 ドロップダウンの選択肢は内部的には連番のままだが、表示上は「画像N枚目」という意味合いになる。

これで「ページ53って何?」問題は消えた。

ところが…DB をブラウジングしていたら別の問題が見えてきた

UI 修正後、確認のために本の章末をブラウジングしていたら、 今度は 本文中にランニングヘッダー(書籍タイトル)そのものが混入しているのが目に入った。 ページの本文の合間に、ヘッダー由来の短い行が定期的に挟まっている状態。

これは Kindle アプリのページ上部に表示される ランニングヘッダーを OCR が拾ってしまった結果。 スクショ取込本だと避けられない構造的なノイズ。 書籍タイトルだけじゃなく、章名やページのフッタ UI(「< 戻る」みたいなボタンラベル)まで紛れ込んでいる本もあった。

UI で隠せばいいというレベルじゃない。 これは全文検索(FTS)の精度を下げるし、AI に書籍内容を引かせるときにもノイズになる。 DB 側から消すべきだと判断した。

Phase 2:ノイズ集計(dry-run)

Claude Code に依頼して、スクショ取込本 52冊から「ほぼ全頁に出現する短い行」を集計させた。 書籍タイトルやランニングヘッダーは、その本のほぼ全ページに出現する性質を持つ。 逆に本文はそうそう同じ行が繰り返されない。 この性質を使って「全頁出現率 ≧ N%」の行をノイズ候補として抽出する素朴な方法を取った。

dry-run の結果は memo/2026-06-24/kindle-noise-candidates.md に 273 行で出力。 それを目で眺めたら、

  • 書籍タイトル100%混入の本が 4冊
  • Kindle UI 由来の固定ノイズ(「マイライブラリ」「もっと見る」みたいな文字列)が多数
  • 章タイトルが [第3章] のような prefix として section 名に紛れている本もあった

という構図が見えた。

Phase 3:「再 restructure」ではなく「独立 cleanup フェーズ」に切る

当初の計画書では、52冊を Workflow で並列に 再 restructure する案を書いていた。 が、書きながら気づいた。 restructure はセクション統合まで含む重めの手作業(章構造の再構築、見出し階層の整理)で、それを 52 冊全部やり直すのは現実的じゃない。 既存の restructure 結果(章構造)は活かしたい。

そこで方針を切り替えた。 apply_kindle_noise_filter という独立フェーズを実装して、行レベル削除 + section prefix → tags 移管だけをやる。 章構造には触らない。本文の chunk 単位で「ノイズ候補リストにマッチする行」だけを抜く。 section 名の [第3章] みたいな prefix は tags に移管して、section 名自体をクリーンにする。

これなら既存資産が全部活きる。

Phase 4:単体適用 → サンプル目視 → 一括適用

まず あるスクショ取込本 単体で適用させた。 dry-run 出力で、151 chunks 全件で content が変わり、104 chunks の section から [XXX] prefix が tags へ移った。 2,168 行削除。

before/after サンプルを目視確認。 page 1 の冒頭16行あった UI ノイズと書籍タイトル混入が消えて、奥付相当の最小限の情報だけが残る。 page 53「はじめに」の5行ノイズも消滅。本文だけが残った。 読みやすい。

問題なさそうなので、残り 51 冊を --all でバックグラウンド一括適用させた。

Phase 5:結果

完了通知を待っている間に他のことをやり、戻ってきたら全 52 冊終わっていた。 累計で 8,948 行のノイズ削除。 書籍タイトル100%混入だった 4 冊もすべてクリーンアップ。

検証クエリを回して、ノイズが残っていないかを確認。 2冊だけ少数残っていたが、内容を見たら「本文の正当な言及」(書籍タイトルがそのまま本文中で言及されているケース)で、消すと逆に情報が消える。これは許容範囲として残した。

FTS の rebuild も走らせて、全文検索インデックスも最新化。

コミットは2本に分けた。

  • Web UI 改修(hasMeaningfulPageNumbers computed 追加 + ページ番号バッジの v-if 制御)
  • ノイズフィルタ実装(apply_kindle_noise_filter モジュール + CLI)

scratchpad の作業ファイルは除外して、必要なものだけ。

学び

  • 「再 restructure 全部やり直す」よりも「ノイズ除去だけ独立フェーズで切り出す」方が既存資産が活きる。 章構造の再構築は重い。本文の行レベル削除と section 名のクリーンアップだけなら、独立した小さい関数で完結する。 「全部やり直す案」を最初に書いたが、書いている途中で「重すぎる」と気づいて方針を切り替えられたのが大きかった。
  • Web UI の違和感を起点に、DB 側のノイズ問題まで芋づる式に拾えた。 「ページ番号が意味不明」という UI の違和感を直すために DB をブラウジングしたら、本文に書籍タイトルが混入しているのが目に入った。 最初から「DB のノイズを掃除しよう」と思って入ったわけじゃない。UI を触っているうちに視界に入った。
  • 読んでて気持ち悪い違和感を放置せず拾うのは筆者本人の係。 「ページ53って何?」「本文の途中に書籍タイトルが挟まってる」は、AI には違和感として認識されにくい。 実装の係はAIに振れるが、違和感の検知はこっちで持つしかない。
  • 判定基準が1個の prefix match で済むと、後続の実装が劇的にシンプルになるsource_path LIKE 'kindle:%' だけで全部分岐できるので、Web UI の computed もノイズフィルタの対象抽出も、同じ1行で済んだ。 スキーマ設計の段階で source_path に prefix 規約を入れていた過去の自分に感謝。

関連ファイル

  • 計画書: memo/2026-06-24/kindle-screenshot-noise-removal-plan.md
  • dry-run 出力: memo/2026-06-24/kindle-noise-candidates.md(273行)
  • Web UI: web/app/pages/books/[bookId]/[page].vue
  • ノイズフィルタ実装: apply_kindle_noise_filter モジュール + CLI