オチツカレサマ 〜 99%のゴミを消した昼と、rclone copy の罠
オチツカレサマ 〜 99%のゴミを消した昼と、rclone copy の罠
はじめに
前夜、OCR結果が元PDFの12倍に膨れていることに気づいた。99.4%が viz_page_*.jpg、つまり機械側の自己点検資料。保存しないのが正しいという結論に、思想(五蘊)と容量計算の両方が同じ場所を指していた。
朝、それを実装する。
ただし、Claude Codeにいきなり「viz画像を消すコードを書いて」と投げない。まず仕様を書く。これが昨夜立てたルールだった。
仕様を先に書いた朝
docs/specs/2026-04-27-viz画像抑止.md を作る。
含めたもの:
- 背景: 実測(590.6MB / 594MB = 99.4%)と、ネット切れ事故の事実
- 現状の動作: 1〜7のステップを箇条書き
- 要件 R1〜R5: アップしない/ローカルにも残さない/成功時のみwork_dir削除/必要ファイルは従来通り/既存viz掃除コマンド
- 影響範囲・触らない範囲: 元PDF、Vault Markdown、status判定は触らない
- 受け入れ基準: 数値で機械的に判定できる形に
- 実装の自由度: ハードコードでよい、設定切替不要
これを Claude Code に貼って「実装する前に CLAUDE.md と docs/specs/ を作って」と頼む。コードを触る前に、要件と作業規約を物理ファイルとして固定する。
夜の判断力で広い指示を出さない、ためのワークフローだった。
「結構描いてるじゃん」事件
ところがこの直後、私は早速ひとつやらかしている。
Claudeに「CLAUDE.md の内容は別途指示します」と言ってから渡したテンプレと、Claudeが先に作っていた草案で巨大な差分が出た。Claudeはモノレポ規約・五蘊思想・既知課題を「Claudeが迷わないための要点」というフレームで丁寧に書いていた。私はそれをそのまま全置換させかけた。
「差分大きすぎ。結構描いてるじゃん!CLAUDE.md。一旦落ち着こう。」
書いている途中で気づいて、止めた。前夜「夜は重要ファイルを触らない」と書いたばかりの教訓が、朝も同じように効いていた。判断力が落ちていなくても、テンプレ全置換は危険。差分が大きい時点で立ち止まる。
最終的に、Claudeが先に書いた CLAUDE.md のうち、私のテンプレが扱っていない部分(モノレポ規約・五蘊・既知課題)はそのまま残した。重複する部分は私のテンプレに寄せた。マージできる範囲だけマージ、できない範囲は無理にしない。
CLAUDE.md には、結果として:
- モノレポ構成と五蘊メタファー
- 規約・命名(フォルダ規約、
_v{n}バージョニング、エンジン名) - エンジニアリング上の決定事項(
aliasではなく function、Garage exists()のバグ修正履歴、viz_page画像の抑止) - 既知の課題(
statusコマンドの誤判定)
が並んだ。これは事故を起こさずに残った。
調査 — viz画像はOCRに使われていない
仕様には「実装の自由度」として「NDLOCR-Lite側で viz生成を抑止可能かどうかは Claude Code 側で調査して判断してよい」と書いた。
Claude Code が両エンジンのソースを読みにいく。
NDLOCR-Lite の src/ocr.py:122-137:
def inference_on_detector(args, inputname, npimage, outputpath, issaveimg=True):
detector = get_detector(args)
detections = detector.detect(npimage) # ← OCR検出はここで完了
classeslist = list(detector.classes.values())
if issaveimg: # ← 以降は描画&保存だけ
drawimage = npimage.copy()
pil_image = detector.draw_detections(...)
pil_image.save(output_filepath)
return detections, classeslist # ← 戻り値は detections のみ
detections は if issaveimg: の手前で確定している。viz画像は単に「検出結果を元画像に上から描画した人間用の確認画像」で、recognizerやテキスト化には1ミリも流れない。
入力画像 ──┬──> detector.detect() ──> detections ──> recognizer ──> page_*.{txt,json,xml}
│
└──> (viz=True のみ) draw_detections() ──> viz_page_*.jpg ← OCRには戻らない
NDLkotenOCR-Lite (src/ocr.py:50-63) も同じ構造。
そして両エンジンとも --viz のデフォルトが False だった:
parser.add_argument("--viz", type=bool, required=False, ..., default=False)
つまり、--viz を渡さなければ、エンジン側のデフォルトで viz は生成されない。「生成→削除」のワークアラウンドは要らない。生成しないですむ。
私が前夜「生成自体を抑止できるなら、それが望ましい」と書いた最良ケースが、最初から手の届く場所にあった。
余談:argparse の罠
両エンジンは type=bool を使っている。これはPython argparseの有名な落とし穴で、bool("False") == True(非空文字列はすべてTruthy)になるため、--viz False と書いてもviz は保存される。
幸い私たちのラッパーは「Falseのときは --viz フラグ自体を渡さない」という書き方をしていたので、この罠は踏んでいなかった。気づかずに踏まなかった罠は、気づいたら必ずメモする。
実装 — feat/viz-suppress
ブランチを切る。
git checkout -b feat/viz-suppress
変更点は最小限:
ocr_engines.py:run_ocr()からvisualize引数を削除、--vizを渡すコードを削除Config.visualizeフィールド・config.example.tomlの[output] visualize行を削除cli.py:config-showからvisualize表示を削除pipeline.pyのtry/finallyを撤去。成功時のみshutil.rmtree(work_root)する形に変える。失敗時は work_root を残してデバッグ可能にする(R3)ocr-pipeline/README.mdに R5 の rclone 掃除コマンドを「運用メモ」として追記
pipeline.py の try/finally を消したとき、最初に動かしたらSyntaxError: expected 'except' or 'finally' block で死んだ。try: の本体だけ消して try: 自体を残してしまっていた。try を撤去し、内側の処理をそのまま関数本体に格上げする。
例外は呼び出し元の process_engine がループで捕捉してくれるので、try を消しても挙動は変わらない。むしろ「成功時に消える、失敗時に残る」がコードの構造そのもので表現されるようになった。
1回目の検証 — 「変わってないかぁ」
feat/viz-suppress で同じPDF(日本語原学_104-203、100ページ)を --no-skip で再処理した。
5分。完走した。Vault にも Markdown が出ている。
$ rclone size garage:pdf-library/ocr-results/ndlocr/日本語原学_104-203_digidepo_1836174_0001/
Total objects: 401
Total size: 594.068 MiB (622925828 Byte)
594MB。
変わっていない。
「変わってないかぁ。別のやつでやってみたいな」とSlackに書いた(実態はClaude Codeとの対話)。
真因 — rclone copy は destination を消さない
落ち着いて考えると、これは新コードのバグではなかった。
pipeline.py の garage.upload_dir は rclone copy を使う。コピー元に無いファイルを destination から削除しない。rsyncの --delete 相当の挙動を持たない。
時系列でいうと:
- 過去の実行で、
ocr-results/.../viz_page_*.jpg100枚が Garage にアップ済み - 検証前、私が purge を提案したパスは
日本語原学_104-203/(digidepo接尾辞なし)→ 別フォルダなので何も消えなかった - 今回のクリーンな実行は viz を生成していない(新コード正常)
rclone copyは新しいpage_*.txt/.json/.xmlを上書きアップロードしたが、過去のviz_page_*.jpg100枚はそのまま残った- 合計サイズは「過去viz + 新page」で、元と同じ ~594MB に見える
つまり新コードは viz を上げていないけれど、過去の遺産が残ったまま。size比較では効果が見えない。
「rclone copy は destination 側を消さない」。書いてしまえば3秒で読めるが、リアルタイムで「サイズ変わってない」を見たときは一瞬うろたえた。基本仕様の確認は、検証コマンドそのものの解釈にも効く。
新規PDFでの確認 — 静かに通った
過去の遺産がない PDF を1冊、新規にGarageに上げて流した。6ページ、7.208 MiB。
$ rclone lsf garage:pdf-library/ocr-results/ndlocr/<新hash>/ | grep -c viz_
0
$ rclone size garage:pdf-library/ocr-results/ndlocr/<新hash>/
Total objects: 19
Total size: 229.199 KiB (234700 Byte)
| 受け入れ基準 | 期待 | 実測 |
|---|---|---|
| viz画像をGarageに上げない | 0件 | 0 |
| 必要ファイル揃う | page_*.{txt,json,xml} × 6 + text.txt = 19 | 19 |
| サイズ従来比1%未満 | 6ページなら ~360KB未満 | 229.199 KiB |
| 元PDFは触らない | 7.208 MiB が残存 | 同名・同サイズで残存 |
| work_dir 成功時消える | 当該分なし | 新規分は消えている |
全部通った。
サイズで言うと、6ページで 229 KiB。viz ありだったら100ページの実績から外挿して6ページで約36MB。削減率 99.4%。前夜書いた「600MB → 3MB、177倍」の試算と、思想(保存に値しない自己点検資料)が、数値で一致した。
元PDFはどこへ?
「元ファイルは7.6 MBあるんだけどそれはどこに行ったんだろう」と聞いた。
$ rclone size garage:pdf-library/ndlocr/<新hash>.pdf
Total objects: 1
Total size: 7.208 MiB (7558145 Byte)
そのまま pdf-library/ndlocr/ に残っていた。仕様の「触らないでほしい範囲」に書いた通り。
ストレージの全体像はこう:
| 場所 | 中身 | サイズ |
|---|---|---|
pdf-library/ndlocr/<file>.pdf |
元PDF(永久保存) | 7.2 MiB |
ocr-results/ndlocr/<stem>/page_*.* + text.txt |
OCR結果 | 229 KiB |
ローカル /tmp/ainsoph-ocr-work/ |
中間生成物 | 処理中だけ存在、成功時消える |
Vault Inbox/Sources/<engine>_<stem>_<date>.md |
人間用Markdown | 数十KB |
「元PDFは Garage、OCR結果は Garage、Markdown は Vault」と完全に分離されている。どこかが壊れても、元PDFから何度でも再OCRできる。
設計が思想を裏切っていない、と感じた。
夕方の追補:「完了」とは何か
viz の話が片付いて、残作業を眺めていた。
statusコマンドの判定ロジック改修(Phase 2)
これを「終わってるはず」と呟いたら、コードを開いてみると確かに text.txt の存在で判定する形に直っていた(cli.py:131)。前夜の「フォルダがあるだけで ✓ 表示」のバグはもう起きない。
ただし、当初の spec には:
本来は
text.txtの存在 + Vault 側の.mdの存在で判定すべき
と書いていた。つまり Vault 側のチェックが残っていた。これをやるかどうかで、「完了とは何か」をもう一度考え直すことになった。
Vault は唯一の真実か?
最初の問いは「Vault と Garage、どっちを基準に判定するか」だった。
私の感覚:
Garage はただの保存庫+保管庫。変わってもいいし、増えてもいい。
生きた知識、knowledge wisdom は Obsidian Vault かな。
そう答えてから、もう少し正確な言い方が降りてきた。
潜在意識と顕在意識の対比でいうと、Garage = 潜在意識かもしれないけど、何があるかは普段意識されないと思う。意識されるなら Vault かな。
「Vault が真実」より「Vault が顕在意識」の方が、自分の感覚に合う。Garage は普段意識されない潜在のストック。Vault は能動的に開いて、編集して、引用して、統合する場。
そうすると pipeline の役割が再定義される。潜在から顕在へ素材を運ぶのが pipeline の仕事。status が見たいのは「届いたか」、すなわち 顕在意識に取り込まれたか。
「完了」とは、アップしたかどうかではない。OCRが終わったかどうかでもない。Vault に取り込まれたかどうか。
けれど、顕在は揺れる
ここで一度引っかかった。
Vault は人間が日々編集する場だ。frontmatter status: archived になった .md は Inbox/Sources/ から Knowledge/Archive/ に動くかもしれない。複数の .md を MOC(Map of Content) に統合するかもしれない。不要だと思って削除するかもしれない。
これらは全部 Vault の健全な進化であって、エラーではない。
でも、もし status が Inbox/Sources/<engine>_<title>_<date>.md の存在を位置で判定していたら、ファイルを動かした瞬間に ◐(部分完了)表示になる。3ヶ月後にはほぼ全件 ◐ になり、本来検出したい「配達失敗」がノイズに埋もれる。
「Vault は真実」と言いつつ、「Vault は揺れる」のが現実。位置で判定すると、編集の自由を奪う。
位置ではなく存在で
Claude Code と話しながら答えに辿り着いた。
Vault のどこにあるか、ではなく、Vault のどこかに残っているか。
frontmatter の ocr_engine と title で検索すれば、ファイルが移動・統合・リネームされても検出できる。ユーザーが意図的に削除したときだけ ◐ になる。それは「顕在から消した」というユーザーの意思表示として、正しいシグナル。
実装としては、vault_dir 全体を walk して frontmatter を持つ .md から (ocr_engine, title) のセットを作り、各PDFがそのセットに帰属するかを判定する。Mac の SSD なら数千ファイルでも 0.4 秒程度で走査が終わる。
def _build_vault_ocr_index(vault_dir: Path) -> set[tuple[str, str]]:
"""位置(フォルダ)ではなく存在(frontmatter)で Vault Markdown を集める。"""
index = set()
for md_path in vault_dir.rglob("*.md"):
# 隠しディレクトリ(.obsidian, .trash 等)はスキップ
# frontmatter から ocr_engine + title を拾う
# ...
index.add((engine, title))
return index
判定基準は3状態:
✓ Garage の text.txt + Vault のどこかに対応する .md
◐ Garage の text.txt はあるが Vault に対応する .md なし(顕在から消えた/配達失敗)
· Garage の text.txt がない(未処理)
Vault 内のどこに動こうがリネームされようが、✓ のまま。ユーザーの編集権を尊重しつつ、配達状態を正確に通知する。
思想と実装が一致した
これは ainsoph-pipelines のある種の中核を確定する瞬間だった。pipeline は素材の生成器ではなく、顕在意識へのデリバリーサービス。届いたかどうかは frontmatter で確認する。届かなければ ◐ で通知し、再配達したければ --no-skip、放っておきたければ無視できる。機械が勝手に再生成しないことが、人間の編集権を守る最後の線になる。
実装は feat/status-vault-check ブランチで完了、main に merge した。実機テストで全7件 ✓ 表示。「失敗した3冊(204-303 / 304-329 / 4-103)も完了済み」が機械的に検証できた。Vault 全走査込みで実行 1.9秒。
判断の系譜は別の場所に置く
これだけの議論を、コードのコメントやコミットメッセージに収めるのは無理がある。設計を変える理由は、機能仕様(What)とは別の質をもっている。
そこで docs/design-log/ というディレクトリを新設した。docs/specs/ が「機能の要件」だとすれば、docs/design-log/ は「判断の系譜」。
docs/
├── specs/ ← 何を作るか(What)
└── design-log/ ← なぜそう作ったか(Why)
最初の住人は docs/design-log/2026-04-28-status判定の哲学.md。フォルダ判定 → text.txt 単独 → filename glob → frontmatter scan、という改修の系譜と、それぞれの段階で何を犠牲にし何を得たかが残っている。
3ヶ月後の自分が status の判定基準を見直したくなったとき、ここに戻れば1分で議論が再生される。コードのコメントを読みに行く必要はない。
オチツカレサマ
仕様を先に書いた。CLAUDE.md は事故を起こさずに済んだ。エンジンのソースを読んで、--viz を渡さなければよいと確信した。実装してテストして、混乱して、真因に行き着いて、新規PDFで確信した。
そこから一段深いところで、「完了とは何か」「Vault は揺れるが顕在意識である」「判断の系譜を残す場所」が決まった。
ここまで、淡々と。
「ちゃんと壊れた」夜(前夜)の続きとして、「ちゃんと回った」昼が来て、その先に「ちゃんと考え直した」夕方がついた、という感覚がある。設計→実装→検証→記録の輪が、思想とコードの両方で閉じる瞬間は、たぶん私がパイプラインを作る一番の理由だ。
Phase 1 設計書に書いた「色・受・想までを担い、行・識は人間に委ねる」が、容量設計と判定基準の両方で言葉のとおりになった日だった。pipeline は「想」の結晶を「行」のテーブルに置く配達人で、置けたかどうかを 位置ではなく存在で確認する。
今日の達成
残るもの
- 過去にアップ済みの viz 一括削除(R5 の
rclone delete)— 差し支えなければそのままでも可 audio-pipeline着手時にgarage_client.py/obsidian_writer.pyをshared/へ昇格
学び
仕様は、コードを書く前のClaude Codeへの最良の贈り物。
docs/specs/YYYY-MM-DD-<topic>.md を先に作ると、要件・受け入れ基準・触らない範囲が物理ファイルとして固定される。Claude Codeは「実装の自由度」の枠内で動き、ハルシネーションが入る余地が減る。夜の判断力に頼らないで済む。
既存ファイルがある時は、テンプレ全置換しない。
CLAUDE.md 全置換事故は前夜から朝にかけて2度起こりかけた。1度目は要件本文がまだ届く前の暫定版を書いた時、2度目は私が「テンプレで全置換して」と頼んだ時。両方ともClaudeが先に書いた中身を尊重して、差分が大きすぎる時点で立ち止まれた。差分の大きさは判断のシグナル。
rclone copy は destination を消さない。
これは基本仕様だが、検証コマンドの解釈にも効く。サイズが変わらなかったとき、「コードが効いていない」と早合点しそうになったが、真因は「過去の遺産が残っている」だった。新規PDFで確認するという検証戦略は、こういう時の救命具になる。
判断の系譜は、コードのコメントには収まらない。
status の判定基準を「位置 → 存在」へ移したのは、メタファーが「真実 → 顕在意識」へ精度を上げた結果だった。この道筋はコミットメッセージにも実装コメントにも書ききれない。docs/design-log/ という独立した場所に置いて初めて、3ヶ月後の自分に届く。設計を変える理由は、機能仕様とは別の質をもつ。
人間の編集権を侵さないために、機械は勝手に再生成しない。
◐ 状態(Vault に届かない / 削除された)を見たとき、システムが自動再OCRしないのは設計判断。pipeline は配達できなかったことを通知するだけで、復旧するかどうかは人間が決める。ユーザーが意識的に Vault から消したものを、pipeline が勝手に蘇らせるのは越権。これは技術的判断というより、ツールが人間の意思を尊重するための線引きだった。
オチツカレサマ。
明日も、ぼちぼち。
関連記事
- 2026-04-26-オチツカレサマGarageが立ち上がった朝の記録 — 第1夜、Garageが立ち上がった朝
- 2026-04-27-オチツカレサマ12倍に膨れたOCR結果と容量設計の罠 — 第2夜、99.4%がviz画像だと気づいた夜
- 2026-04-26-知性パイプラインPhase1設計書 — 五蘊メタファーの全体像
参考リンク
- NDLOCR-Lite
- NDLkotenOCR-Lite
- rclone copy マニュアル —
--delete系オプションの不在に注意 - argparse type=bool の罠 —
bool("False") == True