語源英単語4000語の画像ミスマッチをレビューした記録 — 重複バグの発見から作り直すフェーズへ

開発eurekapu-nuxt4

語源英単語4000語の画像ミスマッチをレビューした記録 — 重複バグの発見から作り直すフェーズへ

gogen-eitangoの語根ページclud-clus-closを開いて、単語ひとつひとつの画像と例文を見比べていたら、指が止まった。disclose自体は「布をめくって隠れていたものを見せる」絵で、語源のイメージには合っている。

だとしても、この1語だけを見て安心していいのかが分からない。同じ精度が、残り数千語にも及んでいる保証はどこにもなかった。ページ全体、いや語源英単語データベース全体をこの目で見て回るのは現実的ではない。サブエージェントかWorkflowを使って、全体をレビューしてもらうことにした。

パイロットで見つかったのは、指摘した箇所とは別の穴

まず語根データの構造を洗い出させ、画像を取得してReadツールでwebpを直接表示できるかを確認させた。判定基準も詰めた。「単語の核となる意味・語源イメージが伝わっているか」だけを見る。多義語のうちどちらの意味を描いているか、否定文が反転して描かれていないかは気にしない、というラインで統一した。

clud-clus-closファミリー11語をパイロットとして見比べさせると、指摘したdisclose自体は許容範囲だった。代わりに、enclose(例文は「申請書に領収書を同封してください」)で画像が「フェンスで菜園を囲う」絵になっている、という別のズレが浮かび上がった。同じ「囲う」でも、例文とは違う意味のほうが描かれている。指摘した箇所は無事だったのに、隣の単語で同じ種類の事故が起きていた。

Workflowの起動でも足踏みした。最初は対象マニフェストをargsに渡し忘れて空のまま起動してしまい、一旦止めて仕切り直した。次に80語分のマニフェストをargsへ丸ごと渡す形にすると、今度は起動はするのに完了しない。原因を切り分けると、巨大な配列をargsに渡すこと自体が失敗の引き金になっているとわかった。各エージェントがローカルのJSONファイルを直接読みに行く設計に作り直させ、それでも怪しさが残る箇所はモジュール名をスクリプト本体に直接書き込ませる形まで踏み込んで、ようやく安定した。

パイロット80語は全OK、でも系統的なバグが見つかった

作り直したWorkflowでcap-cept・atlas-standard-01の計80語を判定させると、全語OKだった。トークン消費は80語で約29万トークン。ここで安心しかけたが、最初に指摘したdiscloseを含むモジュール(atlas-plus-challenge-05、70語)でも同じ基準が機能するか、念のため確認させた。

そこで、意味の良し悪しとは別次元のバグが出てきた。persistentとwhatsoeverという意味的に無関係な2語が、同じ「空の食器棚」の画像を共有している。rotate・notify・minimalが同じ抽象図形を共有している。accumulation・inequality・intellectが同じシーソーの画像を共有している。ひとつのモジュール内だけで8件、こうした使い回しが見つかった。主観的な「微妙」判定ではなく、明確な生成ミスだった。

画像のハッシュを照合するだけならLLM不要でほぼ無料、数分で終わる。一方、意味的なレビュー(Vision LLMに画像と例文を見せて判定させる)は150語で約50万トークンかかっていたので、4000語に単純換算すると1300万トークン、所要60〜90分規模になる。高いコストをかけて意味レビューを全件走らせる前に、まず重複ハッシュの全数スキャンだけ先にやらせることにした。

4000枚の画像を一括ダウンロードさせると、37秒で終わった。ハッシュを計算させて重複クラスタを検出させると、40モジュール・169クラスタ・357語が重複画像の影響を受けていた。指で数えられる規模ではなかった。

コンテキストが尽きて、引き継ぎドキュメントを書く

ここまでで、ウィンドウがかなり埋まっていた。357語を対象にした意味的レビューをこれからやる必要があるが、同じセッションで続けるのは危うい。一旦区切って、新しいセッションに引き継ぐことにした。memo/2026-07-22/gogen-image-review-handoff.mdに、作業手順・判定基準・動作確認済みのWorkflowスクリプト全文・技術的な落とし穴までをまとめさせて、その日はここで一区切りにした。

新セッションで本番の意味レビューを走らせる

続きのセッションでは、まず引き継ぎドキュメントを読ませるところから始めた。前のセッションで自分が何を確認し、何を確認しないまま止めたのかを覚えていなかったので、その意味でも引き継ぎドキュメントは効いた。77モジュール・4000語という数字がモジュール一覧のJSONと一致していることを確認させ、確定済みのWorkflowスクリプトにスクラッチディレクトリのパスを差し替えさせて、そのまま本番に入らせた。

見積もり(1300万トークン規模・45〜75分)は前のセッションで合意済みだったので、再確認はせず開示だけして実行させた。並列14でバックグラウンド起動させ、完了通知を待つ間は他の作業を進めた。実測は1314万トークン、エラー0件で完走。重複クラスタ169件と意味レビューのNEEDS_REDO 198件を突き合わせさせ、確定198件・要目視確認47件という最終リストにまとめさせた。

スクリーニングから、作り直すフェーズへ

判定が出そろったところで、次に進むことにした。「スクリーナーズフェーズいきましょう」、それから「作り直すフェーズね」と、自分で工程に名前を付けた。判定して終わりの「スクリーナーズフェーズ」と、実際に画像を差し替える「作り直すフェーズ」を分けて考えることで、いま何をやっているのかが自分の中でもはっきりした。

まず欲しかったのは、作り直す前の画像と例文を、自分の目でひとつずつ確認できる場所だった。「一時的なビューアーでいい、HTMLで十分」と伝え、R2のCDNから直接画像を読み込む簡易ビューアーを作らせた。最初のバージョンはタブ切り替えと検索フィルタだけの一覧だった。実際に開かせて動きを確認させ、画像URLの到達性(404がないか)もまとめて検証させた。

そのうえで、Miller Column形式に作り直させた。左に一覧、中央にBefore(画像と例文を大きく表示)、右にAfter用の空欄を置く3ペイン構成にし、背景は白ベースにした。矢印キーでの単語送りや、要目視確認の兄弟語表示まで確認させてから、次のレビューに進んだ。

英語を大きく、のはずが日本語を大きくに変える

ビューアーができた直後、例文の文字を大きくしてほしいと伝えた。最初に口にしたのは「英語を大きく」だったが、言い終わる前に自分の判断ミスに気づいた。画像と見比べて正しいかどうかを判断するのは、英語の言い回しではなく日本語訳のほうだ。ここで見るべきは「この日本語の意味が、この画像に描かれているか」だけだった。日本語を26pxの太字に、英語は13pxの控えめなグレーに変えさせて、ようやく見比べやすいレイアウトになった。

具体的なズレを3件見て、判断が固まる

ビューアーで実際にレビューを始めると、すぐに具体的な事故が見つかった。

excuseの例文は「先生は雪のせいで遅刻した彼を許した」。先生と彼という、抽象概念ではなく実在の人物が主役の文なのに、画像には人物が写っていなかった。もう一語は、乳鉢のようなものに何かを入れてすりつぶしている絵が出てきて、単語の意味とは何の関係もなかった。

selfの例文は「長い旅を終えて彼には本当の自分を見つけるための静かな時間が必要だった」。この語は、まったく別の単語と同じ画像を共有していた。「重複が見つかった語は、そもそも画像自体が別物にすり替わっている」という見立てが、3件とも当たっていた。灯台や港町のような具体的な名詞が例文に出てくる語は、画像にもその名詞自体が写り込んでいないと違和感が強くなる、という基準もここで固まった。

3件だけ見比べたところで、残りを1件ずつ見て回る必要はないと判断した。傾向は明らかに悪い方に振れている。245件を1件ずつ目視するより、確定している198件をまとめて作り直すほうが早い。要目視確認の47件(同一画像を共有する2語のうち、どちらを残すか自分で決める必要があるペア)だけは後回しにして、確定分から再生成のキューに戻すことにした。

生成バッチを回しながら、進捗を見守る

再生成の対象198件は、まずドライランで整合性を確認させた。1:1対応が崩れていないこと、ステータスが全てdoneになっていることを確かめさせてから、pendingに戻して再生成のキューに乗せさせた。画像生成に使うcodex CLIやImageMagickが使える状態かも先に確かめさせ、認証が有効であることを確認したうえで、過去に実証済みの3並列×34枚チャンク運用(レート制限時は自動30分待機)で、バックグラウンドでバッチを開始させた。

進捗は定期的に確認させた。17件、38件、84件と数字が伸びていく途中、1つのランナーがレート制限に当たったが、残り2つは止まらずに進んだ。1枚あたり2分強×3並列というペースで、完走まで2〜2.5時間の見積もりだった。バックグラウンドで動くジョブだからといって放置せず、区切りごとに数字を見にいく方が、後で慌てずに済む。

途中、バッチとNuxtの開発サーバーが外部から止められたように見える瞬間があった。慌てて確認させると、実際のOSプロセスは開始時刻からずっと生き続けていて、止まっていたのはClaude Code側の追跡がプロセスへのハンドルを見失っていただけだった。実害はなく、念のため手動でpending化した2件も、その後何事もなく完了していた。

チャンク1が81件で完走し、自動的にチャンク2(残り117件、3並列)に移った。エラーは出ていない。84件まで進んだところで様子を見ると、前回の生成に比べてペースが落ちている感覚があった。今日はここまでにして、続きは明日に回すことにした。86/198件が完了、残り112件。ジョブはステータス駆動で安全に中断・再開できる設計にしてあるので、PCの電源を落としても、翌日そのまま続きから走らせられる。

学び

  • 1語のOKは全体のOKを保証しない。discloseは問題なかったが、隣のencloseで別の種類のズレが見つかった。指摘された箇所だけ直して終わらせていたら、もっと多くの事故を見落としていた
  • 安いチェックを先に全数、高いチェックはその後に絞る。重複ハッシュ照合(数分・ほぼ無料)を先に4000語全部にかけたことで、1300万トークン規模の意味レビューを無駄撃ちせずに済んだ
  • 数件見て傾向が明らかなら、全件個別チェックにこだわらない。245件の意味レビュー結果を1件ずつ目視するより、3件で判断を固めて198件を一括で作り直すほうが、実際の修正に早くたどり着けた
  • 止めどきは体感で決めていい。生成ペースが落ちて感じられた時点で、無理に完走まで見届けず翌日に回した。中断・再開が安全に設計してあれば、その日の判断を急ぐ理由はない