Steven
Steven7 min læsning

Fra OCR til pixels uden at miste reserven

GeekBye holdt op med at læse dine skærmbilleder med OCR og begyndte at sende selve billedet til visionsmodeller — hvilket er den nemme del. Den lærerige del er optimeringen, der sprang OCR helt over for visionsbrugere, stille brød reserven til ikke-visionsmodeller, og måtte trækkes tilbage til en version, hvor sikkerhedsnettet kører parallelt og koster ingenting.

Udvikling
AI
Desktop
GeekBye-releaser
Fra OCR til pixels uden at miste reserven

Der er en ægte skillevej for enhver app, der lader dig tage et skærmbillede af noget og spørge en AI om det. Du kan køre OCR — udtrække teksten på enheden, sende en bittelille streng og smide billedet væk. Eller du kan sende pixels — levere selve billedet og lade en seende model læse det. OCR er billigt og lille, men blindt for alt, der ikke er tekst: layoutet, diagrammet, indrykningen af koden, den røde bølgestreg under fejlen. Pixels bevarer alt det, men koster flere bytes og kræver en model, der kan se. GeekBye startede på OCR-siden af den skillevej og gik, hen over v1.8.6 og v1.8.7, over til pixel-siden. Det skift er den nemme del af denne historie. Den interessante del er optimeringen, der fulgte med, brød reserven og måtte fortrydes.

Før: læs teksten, smid billedet

Indtil denne bue blev et skærmbillede til et svar sådan her: du trykker på optagelsesgenvejen, klienten kører OCR — Apples Vision-framework gennem en Swift-VisionOCR-binær på macOS, et PowerShell-script på Windows, begge bag én IOcrEngine-abstraktion — og den udtrukne tekst pakkes ind i en streng og sendes til backend som screenshotText. Selve billedet forlod aldrig maskinen. Det er effektivt, og for en mur af ren tekst er det fint. Men et skærmbillede er sjældent kun tekst. Det er et jobopslag med et to-kolonne-layout, et kodningsproblem med et diagram, en fejl med et stack trace, hvis indrykning er ledetråden. OCR fladgør det hele til en linje-for-linje-transskription, og modellen svarer på et tyndere spørgsmål end det, du stillede.

Skiftet: send pixels (v1.8.6)

v1.8.6 tilføjede den anden vej. En ny imageOptimizer.ts, bygget på sharp og derfor platformsuafhængig, gør præcis tre ting ved et råt skærmbillede:

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

Ændr størrelsen til højst 1920px bredt (opskaler aldrig en mindre optagelse), konverter PNG til JPEG, kvalitet 80. Outputtet er råt base64 med medietypen båret i et separat felt, og det rider til backend i to nye anmodningsfelter ved siden af det gamle: screenshotImage og screenshotImageMediaType, uafhængigt af screenshotText. Backend kan nu modtage et billede, tekst eller begge.

Det optimeringstrin er ikke kosmetisk. En rå skærmbillede-PNG er 3–8 MB; den størrelsesændrede JPEG er cirka 200–500 KB. Det er en størrelsesorden af uploadet, og det skærer også modellens regning — visionsmodeller inddeler et billede i fliser efter dets opløsning og opkræver per flise, så at begrænse bredden ved 1920 og komprimere om reducerer direkte både bytes på ledningen og de billed-tokens, du betaler for, uden noget meningsfuldt tab af læsbarhed for tekst på skærmen.

Hvordan ved appen overhovedet, om modellen kan se? Backend annoncerer det. /api/config-svaret fik en ai.vision-blok nøglet på plan, og klienten cacher en enkelt boolean — aiSupportsVision — ved hver konfigurationshentning. Når vision understøttes, optimer billedet; når den ikke gør, fald tilbage på OCR. Så kapabiliteten er en backend-kendsgerning, cachet på klientsiden, og beslutningen per skærmbillede om at køre eller springe OCR over træffes på klienten ud fra det flag. Backend ejer stadig, hvilken model der faktisk svarer.

Optimeringen, der var for smart

Her bliver det lærerigt. Den første udgave af visionsvejen kørte OCR og billedoptimering sammen, parallelt. Så strammede en "perf"-commit det til et enten/eller: hvis planen understøtter vision, optimer billedet og spring OCR helt over; hvis den ikke gør, kør OCR og spring billedarbejdet over. På papiret er dette den oplagte gevinst — hvorfor udtrække tekst, som visionsmodellen ikke har brug for? Ændringsloggen reklamerer endda med det: "OCR is skipped when the AI can read the image directly."

Problemet er ordet reserve. Backend har ikke én model; den har en primær og en reserve, og reserven kan være en ikke-visionsmodel. Forestil dig sekvensen: din plans primære model understøtter vision, så klienten springer stolt OCR over og sender kun billedet. Den primære udbyder hikker. Backend falder tilbage på en model kun til tekst — som nu rækkes et billede, den ikke kan læse, og ingen tekst, fordi klienten optimerede teksten væk. Den hurtige vej havde stille slettet sikkerhedsnettet. Du kan droppe en overflødig hurtig vej trygt; du kan ikke droppe det, som den hurtige vej blot var en genvej uden om.

Rettelsen: betinge på reserven, ikke den primære

Rettelsen er den del, der er værd at stjæle. Den var ikke at gå tilbage til altid at køre OCR — det ville smide gevinsten væk for det almindelige tilfælde. Den var at gøre overspringningen betinget af et andet kapabilitetsflag: understøtter reserven vision? Backends konfiguration fik en fallbackVision-blok; klienten cacher aiFallbackSupportsVision ved siden af det første flag. Nu har beslutningen fire grene:

  • Både primær og reserve ser → optimer billedet, spring OCR over. Den hurtige, almindelige vej.
  • Primær ser, men reserven er blind → optimer billedet og kør OCR, som forsikring for reserven.
  • Billedoptimeringen kaster → OCR som en hård reserve.
  • Planen har slet ingen vision → kun OCR.

Og den anden gren — forsikringstilfældet — er der, hvor det sidste træk betyder noget. At køre OCR efter billedoptimeringen ville lægge dens latens oveni igen. Så de to kører sammen under ét Promise.all: billedoptimering og OCR overlapper, og den OCR, der kun findes i tilfælde af, at backend har brug for den, bliver færdig inden for den tid, billedet alligevel blev optimeret. Sikkerhedsnettet er gratis i vægur-forstand, når som helst det ikke bruges. Det er hele tricket i én sætning: behold den billige reserve, betinge den af, om din reserveudbyder faktisk har brug for den, og parallelliser den, så den koster ingenting, indtil den redder dig.

To mindre fælder, som overspringningen udløste

At springe udskudt arbejde aggressivt over har en tendens til at bringe hvert sted, der stille antog, at arbejdet ville ske, op til overfladen. To kom frem i PR-gennemgangen samme uge.

Et kølås. Skærmbilledkøen anså et element for "klar", når det havde OCR-tekst. Visionsskærmbilleder fik nu aldrig OCR-tekst — med vilje — så fuldstændighedstjekket hasIncompleteOCR ventede for evigt, og køen hang. Rettelsen omdefinerer "klar" som har et optimeret billede eller har OCR-tekst, hvilket er, hvad pipelinen nu faktisk garanterer.

Et forældet kapabilitetsflag. aiSupportsVision er cachet, hvilket betyder, at det kan blive forældet i præcis den forkerte retning: en bruger logger ud, eller en konfigurationshentning fejler, og klienten efterlades i troen på, at den stadig må sende billeder til en model, der ikke længere understøtter dem. Flaget nulstilles nu ved logout og ved mislykket konfigurationshentning — en cachet kapabilitet skal have en plan for, når det, den beskriver, forsvinder.

Et ord om tallene

Fordi ærlighed er pointen med denne serie: commit-beskederne bærer tal — billedoptimering "~30ms" mod OCR "~100–150ms," nyttelaster "~200–500 KB vs ~3–8 MB PNG" — og de er plausible, men de er forfatterens egne inline-skøn, ikke outputtet af et benchmark. Der er ingen målt ende-til-ende før/efter-latens nogetsteds i historikken. Behandl hastighedsargumentet som sund designhensigt, ikke et bevist resultat. Reduktionen af nyttelaststørrelse er det ene tal, du kan stole på, fordi det falder direkte ud af størrelsesændring og omkomprimering.

Tre ting denne release lærte os

  1. At sende pixels slår at læse teksten — når modellen kan se. OCR kaster alt, hvad et skærmbillede kommunikerer ud over tegn, bort. En visionsmodel læser layoutet, diagrammet og indrykningen, og en optimeret JPEG gør det overkommeligt. Men det er et kapabilitetsvæddemål, hvilket er grunden til, at det har brug for en reserve.
  2. Du kan optimere en hurtig vej væk, aldrig sikkerhedsnettet. At springe OCR over for visionsbrugere var rigtigt for den primære og forkert for reserven. Fejlen var ikke overspringningen; det var at springe over baseret på den primæres kapabilitet, når det var reservens, der betød noget.
  3. Et gratis sikkerhedsnet er et parallelliseret et. Betinge forsikringsarbejdet af, om det faktisk er nødvendigt, kør det så ved siden af den hurtige vej, så det overlapper. Gjort rigtigt koster yndefuld degradering ingenting indtil den dag, den er den eneste grund til, at din funktion stadig virker.

For det forrige kapitel i v1-historien, de to fejltilstande hos et klik-igennem-overlay (v1.8.5); og for hele buen, anatomien i at shippe software til perfektion.