
Från OCR till pixlar utan att förlora reserven
GeekBye slutade läsa dina skärmdumpar med OCR och började skicka själva bilden till visionsmodeller — vilket är den lätta delen. Den lärorika delen är optimeringen som hoppade över OCR helt för visionsanvändare, tyst bröt reserven till icke-visionsmodeller, och fick backas till en version där skyddsnätet kör parallellt och kostar ingenting.
Det finns en genuin skiljeväg för vilken app som helst som låter dig ta en skärmdump av något och fråga en AI om det. Du kan köra OCR — extrahera texten på enheten, skicka en pytteliten sträng och kasta bilden. Eller så kan du skicka pixlarna — leverera själva bilden och låta en seende modell läsa den. OCR är billigt och litet men blint för allt som inte är text: layouten, diagrammet, indenteringen av koden, det röda krumeluret under felet. Pixlar bevarar allt det men kostar fler byte och kräver en modell som kan se. GeekBye började på OCR-sidan av den skiljevägen och gick, över v1.8.6 och v1.8.7, över till pixelsidan. Det bytet är den lätta delen av den här historien. Den intressanta delen är optimeringen som kom med det, bröt reserven och fick backas.
Innan: läs texten, släng bilden
Fram till den här bågen blev en skärmdump ett svar så här: du trycker på fångstgenvägen, klienten kör OCR — Apples Vision-ramverk genom en Swift-VisionOCR-binär på macOS, ett PowerShell-skript på Windows, båda bakom en IOcrEngine-abstraktion — och den extraherade texten viras in i en sträng och skickas till backend som screenshotText. Själva bilden lämnade aldrig maskinen. Det är effektivt, och för en vägg av ren text är det helt okej. Men en skärmdump är sällan bara text. Det är en jobbannons med en tvåkolumnslayout, ett kodningsproblem med ett diagram, ett fel med en stackspårning vars indentering är ledtråden. OCR plattar ut allt det till en rad-för-rad-transkription och modellen svarar på en tunnare fråga än den du ställde.
Bytet: skicka pixlarna (v1.8.6)
v1.8.6 lade till den andra vägen. En ny imageOptimizer.ts, byggd på sharp och därmed plattformsoberoende, gör exakt tre saker med en rå skärmdump:
sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })
Ändra storlek till högst 1920px bred (skala aldrig upp en mindre fångst), konvertera PNG till JPEG, kvalitet 80. Utdatan är rå base64 med mediatypen buren i ett separat fält, och den åker till backend i två nya begäransfält vid sidan av det gamla: screenshotImage och screenshotImageMediaType, oberoende av screenshotText. Backend kan nu ta emot en bild, text eller båda.
Det optimeringssteget är inte kosmetiskt. En rå skärmdumps-PNG är 3–8 MB; den omstorleksändrade JPEG:en är ungefär 200–500 KB. Det är en storleksordning bort från uppladdningen, och det skär också modellens räkning — visionsmodeller delar in en bild i rutor efter dess upplösning och tar betalt per ruta, så att kapa bredden vid 1920 och komprimera om minskar direkt både byten på tråden och de bild-tokens du betalar för, utan någon meningsfull förlust av läsbarhet för text på skärmen.
Hur vet appen ens om modellen kan se? Backend annonserar det. /api/config-svaret fick ett ai.vision-block nycklat på plan, och klienten cachar en enda boolean — aiSupportsVision — vid varje konfigurationshämtning. När vision stöds, optimera bilden; när den inte gör det, fall tillbaka på OCR. Så kapabiliteten är ett backend-faktum, cachat på klientsidan, och beslutet per skärmdump att köra eller hoppa över OCR fattas på klienten utifrån den flaggan. Backend äger fortfarande vilken modell som faktiskt svarar.
Optimeringen som var för smart
Här blir det lärorikt. Den första versionen av visionsvägen körde OCR och bildoptimering tillsammans, parallellt. Sedan stramade en "perf"-commit åt det till ett antingen/eller: om planen stöder vision, optimera bilden och hoppa över OCR helt; om den inte gör det, kör OCR och hoppa över bildarbetet. På papper är detta den uppenbara vinsten — varför extrahera text som visionsmodellen inte behöver? Ändringsloggen gör till och med reklam för det: "OCR is skipped when the AI can read the image directly."
Problemet är ordet reserv. Backend har inte en modell; den har en primär och en reserv, och reserven kan vara en icke-visionsmodell. Föreställ dig sekvensen: din plans primära modell stöder vision, så klienten hoppar stolt över OCR och skickar bara bilden. Den primära leverantören hickar till. Backend faller tillbaka på en modell för enbart text — som nu räcks en bild den inte kan läsa och ingen text, eftersom klienten optimerade bort texten. Den snabba vägen hade tyst raderat skyddsnätet. Du kan släppa en överflödig snabb väg tryggt; du kan inte släppa det som den snabba vägen bara var en genväg förbi.
Fixen: villkora på reserven, inte den primära
Fixen är den del som är värd att sno. Den var inte att gå tillbaka till att alltid köra OCR — det skulle slänga bort vinsten för det vanliga fallet. Den var att göra överhoppningen villkorad av en andra kapabilitetsflagga: stöder reserven vision? Backends konfiguration fick ett fallbackVision-block; klienten cachar aiFallbackSupportsVision bredvid den första flaggan. Nu har beslutet fyra grenar:
- Både primär och reserv ser → optimera bilden, hoppa över OCR. Den snabba, vanliga vägen.
- Primär ser men reserven är blind → optimera bilden och kör OCR, som försäkring för reserven.
- Bildoptimeringen kastar → OCR som en hård reserv.
- Planen har ingen vision alls → enbart OCR.
Och den andra grenen — försäkringsfallet — är där det sista draget spelar roll. Att köra OCR efter bildoptimeringen skulle lägga tillbaka dess latens ovanpå. Så de två kör tillsammans under ett Promise.all: bildoptimering och OCR överlappar, och den OCR som bara finns ifall backend skulle behöva den blir klar inom den tid bilden ändå optimerades. Skyddsnätet är gratis i väggklockstermer närhelst det inte används. Det är hela knepet i en mening: behåll den billiga reserven, villkora den på huruvida din reservleverantör faktiskt behöver den, och parallellisera den så att den kostar ingenting förrän den räddar dig.
Två mindre fällor som överhoppningen utlöste
Att aggressivt hoppa över uppskjutet arbete har en förmåga att få varje ställe som tyst antog att arbetet skulle ske att komma upp till ytan. Två kom fram i PR-granskningen samma vecka.
Ett kölås. Skärmdumpskön ansåg ett objekt "redo" när det hade OCR-text. Visionsskärmdumpar fick nu aldrig OCR-text — avsiktligt — så fullständighetskontrollen hasIncompleteOCR väntade för evigt och kön hängde sig. Fixen omdefinierar "redo" som har en optimerad bild eller har OCR-text, vilket är vad pipelinen nu faktiskt garanterar.
En inaktuell kapabilitetsflagga. aiSupportsVision är cachad, vilket betyder att den kan bli inaktuell i precis fel riktning: en användare loggar ut, eller en konfigurationshämtning misslyckas, och klienten lämnas i tron att den fortfarande får skicka bilder till en modell som inte längre stöder dem. Flaggan nollställs nu vid utloggning och vid misslyckad konfigurationshämtning — en cachad kapabilitet måste ha en plan för när det den beskriver försvinner.
Ett ord om siffrorna
Eftersom ärlighet är poängen med den här serien: commit-meddelandena bär siffror — bildoptimering "~30ms" mot OCR "~100–150ms," nyttolaster "~200–500 KB vs ~3–8 MB PNG" — och de är rimliga, men de är författarens egna inline-uppskattningar, inte utdatan från ett benchmark. Det finns ingen uppmätt änd-till-änd före/efter-latens någonstans i historiken. Behandla hastighetsargumentet som sund designavsikt, inte ett bevisat resultat. Minskningen av nyttolaststorlek är den enda siffran du kan lita på, eftersom den faller rakt ut ur omstorleksändring och omkomprimering.
Tre saker den här releasen lärde oss
- Att skicka pixlarna slår att läsa texten — när modellen kan se. OCR slänger allt en skärmdump förmedlar utöver tecken. En visionsmodell läser layouten, diagrammet och indenteringen, och en optimerad JPEG gör det överkomligt. Men det är en kapabilitetssatsning, vilket är varför den behöver en reserv.
- Du kan optimera bort en snabb väg, aldrig skyddsnätet. Att hoppa över OCR för visionsanvändare var rätt för den primära och fel för reserven. Buggen var inte överhoppningen; det var att hoppa över baserat på den primäras kapabilitet när det var reservens som spelade roll.
- Ett gratis skyddsnät är ett parallelliserat. Villkora försäkringsarbetet på huruvida det faktiskt behövs, kör det sedan vid sidan av den snabba vägen så att det överlappar. Rätt gjort kostar graciös degradering ingenting förrän den dag den är enda skälet till att din funktion fortfarande fungerar.
För det föregående kapitlet i v1-historien, de två fellägena hos ett klick-genom-överlägg (v1.8.5); och för hela bågen, anatomin i att skeppa mjukvara till perfektion.