Steven
Steven7 min czytania

Od OCR do pikseli bez utraty ścieżki awaryjnej

GeekBye przestał czytać twoje zrzuty ekranu OCR-em i zaczął wysyłać właściwy obraz do modeli wizyjnych — co jest łatwą częścią. Pouczająca część to optymalizacja, która pomijała OCR całkowicie dla użytkowników z widzeniem, po cichu psuła ścieżkę awaryjną do modeli bez widzenia i musiała zostać wycofana do wersji, w której siatka bezpieczeństwa biegnie równolegle i nic nie kosztuje.

Inżynieria
AI
Desktop
Wydania GeekBye
Od OCR do pikseli bez utraty ścieżki awaryjnej

Każda aplikacja, która pozwala zrobić zrzut ekranu i zapytać o niego AI, staje przed prawdziwym rozwidleniem drogi. Możesz uruchomić OCR — wyodrębnić tekst lokalnie, wysłać maleńki ciąg znaków i wyrzucić obraz. Albo możesz wysłać piksele — przesłać właściwy obraz i pozwolić modelowi zdolnemu do widzenia go odczytać. OCR jest tani i mały, ale ślepy na wszystko, co nie jest tekstem: układ, diagram, wcięcia kodu, czerwony zygzak pod błędem. Piksele zachowują to wszystko, ale kosztują więcej bajtów i wymagają modelu, który potrafi widzieć. GeekBye zaczął po stronie OCR tego rozwidlenia i, na przestrzeni v1.8.6 i v1.8.7, przeszedł na stronę pikseli. To przełączenie jest łatwą częścią tej historii. Ciekawa część to optymalizacja, która przyszła razem z nim, złamała ścieżkę awaryjną i musiała zostać cofnięta.

Przedtem: przeczytaj tekst, porzuć obraz

Do tego rozdziału zrzut ekranu stawał się odpowiedzią tak: naciskasz skrót przechwytywania, klient uruchamia OCR — framework Vision od Apple przez swiftowy plik binarny VisionOCR na macOS, skrypt PowerShell na Windowsie, oba za jedną abstrakcją IOcrEngine — a wyodrębniony tekst zostaje opakowany w ciąg znaków i wysłany do backendu jako screenshotText. Sam obraz nigdy nie opuszczał maszyny. To wydajne i dla ściany zwykłego tekstu w zupełności wystarcza. Ale zrzut ekranu rzadko jest samym tekstem. To ogłoszenie o pracę z dwukolumnowym układem, zadanie programistyczne z diagramem, błąd ze śladem stosu, którego wcięcia są wskazówką. OCR spłaszcza to wszystko do transkrypcji linijka po linijce, a model odpowiada na uboższe pytanie niż to, które zadałeś.

Przełączenie: wyślij piksele (v1.8.6)

v1.8.6 dodało drugą ścieżkę. Nowy imageOptimizer.ts, zbudowany na sharp i przez to wieloplatformowy, robi surowemu zrzutowi ekranu dokładnie trzy rzeczy:

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

Przeskaluj do najwyżej 1920px szerokości (nigdy nie powiększaj mniejszego przechwycenia), zamień PNG na JPEG, jakość 80. Wynik to surowy base64 z typem mediów niesionym w osobnym polu, a jedzie do backendu w dwóch nowych polach żądania obok starego: screenshotImage i screenshotImageMediaType, niezależnie od screenshotText. Backend może teraz otrzymać obraz, tekst albo oba.

Ten krok optymalizacji nie jest kosmetyczny. Surowy PNG zrzutu ekranu ma 3–8 MB; przeskalowany JPEG to mniej więcej 200–500 KB. To rząd wielkości mniej przy wysyłce, a przy tym obcina też rachunek za model — modele wizyjne dzielą obraz na kafle według jego rozdzielczości i liczą za kafel, więc ograniczenie szerokości do 1920 i ponowna kompresja wprost redukują zarówno bajty w kablu, jak i tokeny obrazu, za które płacisz, bez znaczącej utraty czytelności tekstu na ekranie.

Skąd aplikacja w ogóle wie, czy model potrafi widzieć? Ogłasza to backend. Odpowiedź /api/config zyskała blok ai.vision z kluczem według planu, a klient przy każdym pobraniu konfiguracji buforuje pojedynczą wartość logiczną — aiSupportsVision. Gdy widzenie jest wspierane, optymalizuj obraz; gdy nie jest, sięgnij po OCR. Więc zdolność to fakt po stronie backendu, buforowany po stronie klienta, a decyzja dla każdego zrzutu, czy uruchomić, czy pominąć OCR, zapada na kliencie na podstawie tej flagi. Backend nadal decyduje, który model faktycznie odpowiada.

Optymalizacja, która była zbyt sprytna

Tu robi się pouczająco. Pierwsza wersja ścieżki wizyjnej uruchamiała OCR i optymalizację obrazu razem, równolegle. Potem commit „perf" ścisnął to w alternatywę rozłączną: jeśli plan wspiera widzenie, optymalizuj obraz i pomiń OCR całkowicie; jeśli nie, uruchom OCR i pomiń pracę nad obrazem. Na papierze to oczywista wygrana — po co wyodrębniać tekst, którego model wizyjny nie potrzebuje? Changelog wręcz to reklamuje: „OCR is skipped when the AI can read the image directly."

Problem tkwi w słowie fallback. Backend nie ma jednego modelu; ma model główny i awaryjny, a ten awaryjny może nie umieć widzieć. Wyobraź sobie ten ciąg zdarzeń: główny model twojego planu wspiera widzenie, więc klient dumnie pomija OCR i wysyła sam obraz. Główny dostawca się zacina. Backend przełącza się na model tekstowy — któremu wręcza się teraz obraz, jakiego nie potrafi odczytać, i żadnego tekstu, bo klient zoptymalizował tekst tak, że zniknął. Szybka ścieżka po cichu skasowała siatkę bezpieczeństwa. Zbędną szybką ścieżkę możesz bezpiecznie porzucić; nie możesz porzucić tego, co ta szybka ścieżka omijała na skróty.

Lekarstwo: warunkuj decyzję modelem awaryjnym, nie głównym

Lekarstwo to część warta podkradzenia. Nie polegało na powrocie do zawsze uruchamianego OCR — to wyrzuciłoby wygraną w typowym przypadku. Polegało na uzależnieniu pominięcia od drugiej flagi zdolności: czy model awaryjny wspiera widzenie? Konfiguracja backendu zyskała blok fallbackVision; klient buforuje aiFallbackSupportsVision obok pierwszej flagi. Teraz decyzja ma cztery gałęzie:

  • Główny i awaryjny oba widzą → optymalizuj obraz, pomiń OCR. Szybka, typowa ścieżka.
  • Główny widzi, ale awaryjny jest ślepy → optymalizuj obraz i uruchom OCR, jako polisę dla modelu awaryjnego.
  • Optymalizacja obrazu rzuca wyjątek → OCR jako twarde zabezpieczenie.
  • Plan nie ma widzenia w ogóle → tylko OCR.

A druga gałąź — przypadek z polisą — to miejsce, gdzie liczy się ostatni ruch. Uruchomienie OCR po optymalizacji obrazu dołożyłoby jego opóźnienie na wierzch. Więc oba biegną razem pod jednym Promise.all: optymalizacja obrazu i OCR nakładają się, a OCR, który istnieje tylko na wypadek, gdyby backend go potrzebował, kończy się w czasie, w którym obraz i tak był optymalizowany. Siatka bezpieczeństwa jest darmowa w kategoriach czasu zegarowego, ilekroć nie jest używana. Cały trik w jednym zdaniu: zachowaj tani mechanizm awaryjny, uzależnij go od tego, czy twój dostawca awaryjny naprawdę go potrzebuje, i zrównolegl go tak, by nic nie kosztował, dopóki cię nie uratuje.

Dwie mniejsze pułapki, które zastawiło pominięcie

Agresywne pomijanie odroczonej pracy ma to do siebie, że wywleka na wierzch każde miejsce, które po cichu zakładało, że ta praca się wydarzy. Dwa wyszły na jaw w przeglądzie PR w tym samym tygodniu.

Zakleszczenie kolejki. Kolejka zrzutów uznawała element za „gotowy", gdy miał tekst OCR. Zrzuty wizyjne z założenia nigdy już nie dostawały tekstu OCR, więc sprawdzenie kompletności hasIncompleteOCR czekało w nieskończoność i kolejka zawisała. Lekarstwo na nowo definiuje „gotowy" jako ma zoptymalizowany obraz albo ma tekst OCR, czyli to, co pipeline teraz faktycznie gwarantuje.

Nieaktualna flaga zdolności. aiSupportsVision jest buforowana, co znaczy, że może się zdezaktualizować dokładnie w złym kierunku: użytkownik się wylogowuje albo pobranie konfiguracji zawodzi, a klient zostaje w przekonaniu, że wciąż może wysyłać obrazy do modelu, który już ich nie wspiera. Flaga resetuje się teraz przy wylogowaniu i przy nieudanym pobraniu konfiguracji — buforowana zdolność musi mieć plan na moment, gdy to, co opisuje, znika.

Słowo o liczbach

Ponieważ uczciwość jest sensem tej serii: komunikaty commitów niosą liczby — optymalizacja obrazu „~30ms" kontra OCR „~100–150ms", ładunki „~200–500 KB vs ~3–8 MB PNG" — i są wiarygodne, ale to własne szacunki autora wpisane w kod, nie wynik benchmarku. Nigdzie w historii nie ma zmierzonego opóźnienia end-to-end przed i po. Traktuj argument o szybkości jako rozsądną intencję projektową, nie udowodniony rezultat. Redukcja rozmiaru ładunku to jedyna liczba, której możesz zaufać, bo wypada wprost z przeskalowania i ponownej kompresji.

Trzy rzeczy, których nauczyło nas to wydanie

  1. Wysyłanie pikseli bije czytanie tekstu — gdy model potrafi widzieć. OCR odrzuca wszystko, co zrzut ekranu komunikuje poza znakami. Model wizyjny czyta układ, diagram i wcięcia, a zoptymalizowany JPEG czyni to przystępnym cenowo. Ale to zakład o zdolność, dlatego potrzebuje ścieżki awaryjnej.
  2. Możesz zoptymalizować szybką ścieżkę tak, że zniknie, ale nigdy siatki bezpieczeństwa. Pomijanie OCR dla użytkowników z widzeniem było słuszne dla modelu głównego, a błędne dla awaryjnego. Błędem nie było samo pominięcie; było nim pominięcie na podstawie zdolności modelu głównego, gdy liczyła się zdolność awaryjnego.
  3. Darmowa siatka bezpieczeństwa to zrównoleglona siatka. Uzależnij pracę-polisę od tego, czy naprawdę jest potrzebna, a potem uruchom ją obok szybkiej ścieżki, żeby się nakładały. Zrobiona dobrze, łagodna degradacja nic nie kosztuje aż do dnia, w którym jest jedynym powodem, dla którego twoja funkcja wciąż działa.

Po poprzedni rozdział historii v1 — dwa tryby awarii nakładki przepuszczającej kliknięcia (v1.8.5); a po cały łuk — anatomia dostarczania oprogramowania do perfekcji.