Steven
Steven1 分で読める

OCRからピクセルへ、フォールバックを失わずに

GeekByeはスクリーンショットをOCRで読むのをやめ、実際の画像をvision対応モデルへ送り始めました — そこは簡単な部分です。学ぶべきなのは、vision利用者に対してOCRを丸ごと省いた最適化のほうです。それはvision非対応モデルへのフォールバックを静かに壊し、セーフティネットが並行して走り何のコストもかからないバージョンへと差し戻さねばなりませんでした。

エンジニアリング
AI
デスクトップ
GeekByeリリース
OCRからピクセルへ、フォールバックを失わずに

スクリーンショットを撮ってそれについてAIに尋ねられるアプリには、どれにも本物の分かれ道があります。OCRを走らせる — テキストを端末側で抽出し、ごく小さな文字列を送り、画像は捨てる。あるいはピクセルを送る — 実際の画像を送り出し、vision対応のモデルにそれを読ませる。OCRは安上がりで小さいけれど、テキストでないものすべてには盲目です: レイアウト、図、コードのインデント、エラーの下の赤い波線。ピクセルはそのすべてを保ちますが、より多くのバイトを要し、見ることのできるモデルを必要とします。GeekByeはその分かれ道のOCR側から始まり、v1.8.6とv1.8.7をまたいで、ピクセル側へと歩み渡りました。その切り替えは、この物語の簡単な部分です。面白い部分は、それに伴ってやってきて、フォールバックを壊し、取り消さねばならなかった最適化のほうです。

以前: テキストを読み、画像を捨てる

この一連の流れまでは、スクリーンショットはこうして答えになっていました: あなたがキャプチャのショートカットを押すと、クライアントがOCRを走らせます — macOSではSwiftのVisionOCRバイナリを通したAppleのVisionフレームワーク、WindowsではPowerShellスクリプト、どちらも一つのIOcrEngine抽象の背後にあります — そして抽出されたテキストが文字列に包まれ、screenshotTextとしてバックエンドへ送られます。画像そのものはマシンから出ていきませんでした。それは効率的で、べた書きのテキストの壁ならそれで十分です。しかしスクリーンショットが単なるテキストであることはめったにありません。二段組レイアウトの求人票、図を伴うコーディング問題、インデントこそが手がかりであるスタックトレース付きのエラー。OCRはそのすべてを一行ずつの書き起こしへと平板化し、モデルはあなたが尋ねたよりも痩せた問いに答えるのです。

切り替え: ピクセルを送る (v1.8.6)

v1.8.6はもう一方の経路を加えました。sharpの上に築かれ、それゆえクロスプラットフォームな新しいimageOptimizer.tsは、生のスクリーンショットに対してちょうど三つのことをします:

sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })

最大でも1920px幅にリサイズし(小さなキャプチャを決して拡大しない)、PNGをJPEGへ変換し、品質80。出力はメディアタイプを別のフィールドに持たせた生のbase64で、それは古いものと並ぶ二つの新しいリクエストフィールドに乗ってバックエンドへ向かいます: screenshotTextとは独立した、screenshotImagescreenshotImageMediaTypeです。バックエンドは今や画像を、テキストを、あるいは両方を受け取れます。

その最適化ステップは見た目だけのものではありません。生のスクリーンショットPNGは3–8 MB; リサイズされたJPEGはおよそ200–500 KBです。それはアップロードを一桁削り、モデルの請求も減らします — visionモデルは画像をその解像度でタイル分割し、タイルごとに課金するので、幅を1920で頭打ちにして再圧縮することは、回線上のバイトと、あなたが支払う画像トークンの両方を直接減らすのです。画面上のテキストの読みやすさには意味のある損失なしに。

そもそもモデルが見ることができるかどうかを、アプリはどうやって知るのでしょう? バックエンドがそれを告知します。/api/configのレスポンスにはプランをキーとするai.visionブロックが加わり、クライアントは設定を取得するたびに一つの真偽値 — aiSupportsVision — をキャッシュします。visionがサポートされていれば画像を最適化し; そうでなければOCRへフォールバックします。つまり_能力_はバックエンドの事実であってクライアント側にキャッシュされ、OCRを走らせるか省くかという_スクリーンショットごとの判断_は、そのフラグからクライアント上で下されます。どのモデルが実際に答えるかは、依然としてバックエンドが握っています。

小賢しすぎた最適化

ここが教訓に富む部分です。visionパスの最初の一手は、OCRと画像最適化を一緒に、並行して走らせていました。それから「perf」のコミットがそれを二者択一へと締め上げました: プランがvisionをサポートするなら画像を最適化してOCRを丸ごと省く; そうでなければOCRを走らせて画像の作業を省く。紙の上ではこれは明白な勝ちです — visionモデルが必要としないテキストを、なぜ抽出するのか? changelogでさえそれを宣伝しています: "OCR is skipped when the AI can read the image directly."

問題はフォールバックという言葉です。バックエンドは一つのモデルを持つのではありません; プライマリとフォールバックを持ち、そのフォールバックはvision非対応のモデルかもしれません。この流れを思い描いてください: あなたのプランのプライマリモデルはvisionをサポートするので、クライアントは誇らしげにOCRを省き、画像だけを送ります。プライマリのプロバイダーがしゃっくりをします。バックエンドはテキスト専用のモデルへフォールバックします — そのモデルは今、読めない画像と_テキストの無い_状態を手渡されます。クライアントがテキストを最適化して消してしまったからです。速い経路が、静かにセーフティネットを削除していたのです。冗長な速い経路は安全に落とせます; しかし、その速い経路が_迂回するための_近道であった当のものを、落とすことはできません。

修正: プライマリではなく、フォールバックで判断する

この修正こそ真似る価値のある部分です。常にOCRを走らせるやり方へ戻ることではありませんでした — それはよくあるケースでの勝ちを捨ててしまいます。省略を二つ目の能力フラグに条件づけることでした: _フォールバック_はvisionをサポートするか? バックエンドの設定にはfallbackVisionブロックが加わり; クライアントは最初のフラグの隣にaiFallbackSupportsVisionをキャッシュします。いまや判断は四つの分岐を持ちます:

  • プライマリもフォールバックも見える → 画像を最適化し、OCRを省く。速く、よくある経路。
  • プライマリは見えるがフォールバックは見えない → 画像を最適化し、かつOCRを走らせる。フォールバックのための保険として。
  • 画像最適化が例外を投げる → 最終手段のフォールバックとしてのOCR。
  • プランにvisionがまったく無い → OCRのみ。

そして二つ目の分岐 — 保険のケース — こそ、最後の一手が効いてくる場所です。画像最適化の_あとに_OCRを走らせれば、そのレイテンシがそのまま上乗せされてしまいます。だから二つは一つのPromise.allのもとで一緒に走ります: 画像最適化とOCRが重なり合い、バックエンドが必要とした場合にのみ存在するそのOCRは、どのみち画像が最適化されているその時間の内側で終わります。セーフティネットは、使われないかぎり実時間としては無料です。仕掛けの全体を一文で言えばこうです: 安上がりなフォールバックを保ち、あなたのフォールバック提供先がそれを実際に必要とするかどうかで判断をかけ、そして並行化して、それが救ってくれるその日まで何のコストもかからないようにせよ。

省略が仕掛けた二つの小さな罠

先送りされた作業を積極的に省くと、その作業が起きるものと静かに前提していたあらゆる場所を、浮かび上がらせる癖があります。同じ週のPRレビューで二つが出てきました。

キューのデッドロック。 スクリーンショットのキューは、項目がOCRテキストを持ったときにそれを「準備完了」とみなしていました。visionのスクリーンショットは今や — 設計上 — OCRテキストを決して得ないので、完全性チェックのhasIncompleteOCRは永遠に待ち、キューは固まりました。修正は「準備完了」を_最適化された画像を持っている、OCRテキストを持っている_と再定義します。それがパイプラインが今や実際に保証するものです。

古びた能力フラグ。 aiSupportsVisionはキャッシュされており、それはまさに間違った方向へ古びうるということです: ユーザーがログアウトする、あるいは設定の取得が失敗する、すると、もはや画像をサポートしないモデルへまだ画像を送ってよいと信じたまま、クライアントが取り残されます。フラグは今やログアウト時と設定取得失敗時にリセットされます — キャッシュされた能力は、それが記述している当のものが消えたときのための筋書きを持っていなければなりません。

数字について一言

正直さがこのシリーズの眼目なので言えば: コミットメッセージは数値を運んでいます — 画像最適化「~30ms」対OCR「~100–150ms」、ペイロード「~200–500 KB vs ~3–8 MB PNG」 — そしてそれらはもっともらしいものの、ベンチマークの出力ではなく、作者自身のインラインな見積もりです。計測された端から端までの前後レイテンシは、履歴のどこにもありません。速度の主張は、証明された結果としてではなく、健全な設計意図として受け取ってください。ペイロードサイズの削減は、信頼できる唯一の数字です。それはリサイズと再圧縮からまっすぐ導かれるからです。

このリリースが教えてくれた三つのこと

  1. ピクセルを送ることは、テキストを読むことに勝る — モデルが見えるなら。 OCRは、文字以外にスクリーンショットが伝えるすべてを捨てます。visionモデルはレイアウトを、図を、インデントを読み、最適化されたJPEGがそれを手頃にします。しかしそれは能力への賭けであり、だからこそフォールバックを必要とするのです。
  2. 速い経路は最適化して消せても、セーフティネットは決して消せない。 visionユーザーに対してOCRを省くのは、プライマリにとっては正しく、フォールバックにとっては間違いでした。バグは省略そのものではありませんでした; それは、本当に効いてくるのがフォールバックの能力であるときに、プライマリの能力に基づいて省いたことでした。
  3. 無料のセーフティネットとは、並行化されたセーフティネットである。 保険の作業を、それが実際に必要かどうかで判断にかけ、それから速い経路と並べて走らせて重ね合わせよ。正しくやれば、グレースフルな劣化は、それがあなたの機能をなお動かしている唯一の理由となるその日まで、何のコストもかかりません。

v1の物語の前の章については、クリックスルー・オーバーレイの二つの故障モード(v1.8.5)を; そして弧の全体については、ソフトウェアを完璧に出荷することの解剖学をご覧ください。

関連記事

クリックスルー・オーバーレイの二つの故障モード
Steven
Steven1 分で読める

クリックスルー・オーバーレイの二つの故障モード

GeekByeのウィンドウはすべての上に浮かび、ボタンのある場所を除いて、あなたのクリックをそのまま通り抜けさせます。それは二面的な契約であり、v1.8.5とv1.8.14は、その各面が壊れたときにどう見えるかを示します: 一方のリリースではオーバーレイがシステムダイアログを飲み込み、もう一方ではあなたのキー入力を奪いました。二つ目の勝利をもたらした修正は、コードを削除することでした。

エンジニアリング
Electron
デスクトップ
リリースをCIに載せる、二度
Steven
Steven1 分で読める

リリースをCIに載せる、二度

GeekBye v1.8.4はリリースビルドをmacOSとWindowsの両方でCIへ移しました。変更履歴が語らないのは、これを一度前にも試し、ランナーコストのせいで一か月後に削除し、二度目でようやく定着させた — という事実です。なぜなら、その頃にはリリーススクリプトがすでに手作業で機能していたからです。

エンジニアリング
CI/CD
デスクトップ
セーフティネットなしで30言語を出荷する
Steven
Steven1 分で読める

セーフティネットなしで30言語を出荷する

GeekBye v1.8.3は`react-i18next`を組み込み、アプリ全体を30言語へ翻訳しました。ライブラリの部分はお決まりの作業です。面白いのは欠けていたもののほうです: キーの整合性チェックも文字列抽出ツールもなかった — だからハードコードされた英語が漏れ出し、デンマーク語のタイポが出荷され、変更履歴はアプリが28か29か30のどの言語を話すのかで意見が一致しませんでした。

エンジニアリング
i18n
デスクトップ