
Fra OCR til piksler uten å miste reserven
GeekBye sluttet å lese skjermbildene dine med OCR og begynte å sende selve bildet til visjonsmodeller — som er den enkle delen. Den lærerike delen er optimaliseringen som hoppet over OCR helt for visjonsbrukere, stille brøt reserven til ikke-visjonsmodeller, og måtte trekkes tilbake til en versjon der sikkerhetsnettet kjører parallelt og koster ingenting.
Det finnes et ekte veiskille for enhver app som lar deg ta et skjermbilde av noe og spørre en AI om det. Du kan kjøre OCR — trekke ut teksten på enheten, sende en pyttliten streng og kaste bildet. Eller du kan sende pikslene — levere selve bildet og la en seende modell lese det. OCR er billig og lite, men blind for alt som ikke er tekst: layouten, diagrammet, innrykket i koden, den røde bølgestreken under feilen. Piksler bevarer alt det, men koster flere byte og krever en modell som kan se. GeekBye startet på OCR-siden av det veiskillet og gikk, over v1.8.6 og v1.8.7, over til piksel-siden. Det byttet er den enkle delen av denne historien. Den interessante delen er optimaliseringen som fulgte med, brøt reserven og måtte reverseres.
Før: les teksten, kast bildet
Fram til denne buen ble et skjermbilde til et svar slik: du trykker på fangst-snarveien, klienten kjører OCR — Apples Vision-rammeverk gjennom en Swift-VisionOCR-binær på macOS, et PowerShell-skript på Windows, begge bak én IOcrEngine-abstraksjon — og den uttrukne teksten pakkes inn i en streng og sendes til backend som screenshotText. Selve bildet forlot aldri maskinen. Det er effektivt, og for en vegg av ren tekst er det greit. Men et skjermbilde er sjelden bare tekst. Det er en stillingsannonse med et to-kolonners layout, et kodeproblem med et diagram, en feil med et stack trace hvis innrykk er ledetråden. OCR flater alt det ut til en linje-for-linje-transkripsjon, og modellen svarer på et tynnere spørsmål enn det du stilte.
Byttet: send pikslene (v1.8.6)
v1.8.6 la til den andre veien. En ny imageOptimizer.ts, bygd på sharp og derfor plattformsuavhengig, gjør nøyaktig tre ting med et rått skjermbilde:
sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })
Endre størrelsen til høyst 1920px bredt (skaler aldri opp en mindre fangst), konverter PNG til JPEG, kvalitet 80. Utdataen er rå base64 med medietypen båret i et separat felt, og den rir til backend i to nye forespørselsfelt ved siden av det gamle: screenshotImage og screenshotImageMediaType, uavhengig av screenshotText. Backend kan nå motta et bilde, tekst eller begge.
Det optimaliseringssteget er ikke kosmetisk. En rå skjermbilde-PNG er 3–8 MB; den størrelsesendrede JPEG-en er omtrent 200–500 KB. Det er en størrelsesorden av opplastingen, og det kutter også modellens regning — visjonsmodeller deler et bilde inn i fliser etter oppløsningen og tar betalt per flis, så å begrense bredden ved 1920 og komprimere om reduserer direkte både bytene på ledningen og bilde-tokenene du betaler for, uten noe meningsfullt tap av lesbarhet for tekst på skjermen.
Hvordan vet appen i det hele tatt om modellen kan se? Backend annonserer det. /api/config-svaret fikk en ai.vision-blokk nøklet på plan, og klienten cacher en enkelt boolean — aiSupportsVision — ved hver konfigurasjonshenting. Når visjon støttes, optimaliser bildet; når den ikke gjør det, fall tilbake på OCR. Så kapabiliteten er et backend-faktum, cachet på klientsiden, og beslutningen per skjermbilde om å kjøre eller hoppe over OCR tas på klienten ut fra det flagget. Backend eier fortsatt hvilken modell som faktisk svarer.
Optimaliseringen som var for smart
Her blir det lærerikt. Den første versjonen av visjonsveien kjørte OCR og bildeoptimalisering sammen, parallelt. Så strammet en "perf"-commit det til et enten/eller: hvis planen støtter visjon, optimaliser bildet og hopp over OCR helt; hvis den ikke gjør det, kjør OCR og hopp over bildearbeidet. På papiret er dette den opplagte gevinsten — hvorfor trekke ut tekst som visjonsmodellen ikke trenger? Endringsloggen reklamerer til og med for det: "OCR is skipped when the AI can read the image directly."
Problemet er ordet reserve. Backend har ikke én modell; den har en primær og en reserve, og reserven kan være en ikke-visjonsmodell. Se for deg sekvensen: planens primære modell støtter visjon, så klienten hopper stolt over OCR og sender kun bildet. Den primære leverandøren hikker. Backend faller tilbake på en modell kun for tekst — som nå rekkes et bilde den ikke kan lese og ingen tekst, fordi klienten optimaliserte teksten bort. Den raske veien hadde stille slettet sikkerhetsnettet. Du kan droppe en overflødig rask vei trygt; du kan ikke droppe det som den raske veien bare var en snarvei rundt.
Fiksen: betinge på reserven, ikke den primære
Fiksen er den delen det er verdt å stjele. Den var ikke å gå tilbake til alltid å kjøre OCR — det ville kaste bort gevinsten for det vanlige tilfellet. Den var å gjøre overhoppingen betinget av et andre kapabilitetsflagg: støtter reserven visjon? Backends konfigurasjon fikk en fallbackVision-blokk; klienten cacher aiFallbackSupportsVision ved siden av det første flagget. Nå har beslutningen fire grener:
- Både primær og reserve ser → optimaliser bildet, hopp over OCR. Den raske, vanlige veien.
- Primær ser, men reserven er blind → optimaliser bildet og kjør OCR, som forsikring for reserven.
- Bildeoptimaliseringen kaster → OCR som en hard reserve.
- Planen har ingen visjon i det hele tatt → kun OCR.
Og den andre grenen — forsikringstilfellet — er der det siste trekket betyr noe. Å kjøre OCR etter bildeoptimaliseringen ville legge latensen dens oppå igjen. Så de to kjører sammen under én Promise.all: bildeoptimalisering og OCR overlapper, og den OCR-en som bare finnes i tilfelle backend trenger den, blir ferdig innenfor tiden bildet uansett ble optimalisert. Sikkerhetsnettet er gratis i veggklokke-forstand hver gang det ikke brukes. Det er hele trikset i én setning: behold den billige reserven, betinge den av om reserveleverandøren din faktisk trenger den, og parallelliser den slik at den koster ingenting før den redder deg.
To mindre feller overhoppingen utløste
Å hoppe aggressivt over utsatt arbeid har en tendens til å bringe fram hvert sted som stille antok at arbeidet ville skje. To kom fram i PR-gjennomgangen samme uke.
En kølås. Skjermbildekøen anså et element som "klart" når det hadde OCR-tekst. Visjonsskjermbilder fikk nå aldri OCR-tekst — med hensikt — så fullstendighetssjekken hasIncompleteOCR ventet for alltid og køen hang. Fiksen omdefinerer "klart" som har et optimalisert bilde eller har OCR-tekst, som er det pipelinen nå faktisk garanterer.
Et utdatert kapabilitetsflagg. aiSupportsVision er cachet, som betyr at det kan bli utdatert i akkurat feil retning: en bruker logger ut, eller en konfigurasjonshenting feiler, og klienten sitter igjen i troen på at den fortsatt kan sende bilder til en modell som ikke lenger støtter dem. Flagget nullstilles nå ved utlogging og ved mislykket konfigurasjonshenting — en cachet kapabilitet må ha en plan for når det den beskriver forsvinner.
Et ord om tallene
Fordi ærlighet er poenget med denne serien: commit-meldingene bærer tall — bildeoptimalisering "~30ms" mot OCR "~100–150ms," nyttelaster "~200–500 KB vs ~3–8 MB PNG" — og de er plausible, men de er forfatterens egne inline-anslag, ikke utdataen fra en benchmark. Det finnes ingen målt ende-til-ende før/etter-latens noe sted i historikken. Behandle hastighetsargumentet som fornuftig designhensikt, ikke et bevist resultat. Reduksjonen i nyttelaststørrelse er det ene tallet du kan stole på, fordi det faller rett ut av størrelsesendring og omkomprimering.
Tre ting denne releasen lærte oss
- Å sende pikslene slår å lese teksten — når modellen kan se. OCR kaster bort alt et skjermbilde formidler utover tegn. En visjonsmodell leser layouten, diagrammet og innrykket, og en optimalisert JPEG gjør det overkommelig. Men det er et kapabilitetsveddemål, som er grunnen til at det trenger en reserve.
- Du kan optimalisere bort en rask vei, aldri sikkerhetsnettet. Å hoppe over OCR for visjonsbrukere var riktig for den primære og feil for reserven. Bugen var ikke overhoppingen; det var å hoppe over basert på den primæres kapabilitet når det var reservens som betydde noe.
- Et gratis sikkerhetsnett er et parallellisert et. Betinge forsikringsarbeidet av om det faktisk trengs, kjør det så ved siden av den raske veien slik at det overlapper. Gjort riktig koster grasiøs degradering ingenting før den dagen den er den eneste grunnen til at funksjonen din fortsatt virker.
For det forrige kapittelet i v1-historien, de to feilmodusene til et klikk-gjennom-overlegg (v1.8.5); og for hele buen, anatomien i å shippe programvare til perfeksjon.