きっかけ:「このページ番号って何の意味があるんですか?」
引き継ぎセッションを開いて、まずは前日 restructure をかけ直した あるスクショ取込本 の DB が Web UI でちゃんと読めるか確認しに行った。 書籍ビューアを開いて、ページのドロップダウンをスクロールした瞬間に手が止まった。
表紙が 1、「はじめに」が 2、その次のページが 53。
何だこの番号は、と引っかかった。 紙の本ならページが飛ぶことは普通にあるが、これは Kindle Windows アプリのスクリーンショットを OCR して取り込んだ本だ。 紙のページ番号は持っていない。
ということは、この 1 も 53 も PNG の連番でしかない。
表紙の 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].vue に hasMeaningfulPageNumbers という computed を1個生やして、
book.source_path が kindle: で始まるときは 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 改修(
hasMeaningfulPageNumberscomputed 追加 + ページ番号バッジの 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