
D'OCR als píxels sense perdre el fallback
GeekBye va deixar de llegir els teus screenshots amb OCR i va començar a enviar la imatge real als models de visió — que és la part fàcil. La part instructiva és l'optimització que es va saltar l'OCR completament per als usuaris de visió, va trencar silenciosament el fallback als models sense visió, i es va haver de desfer fins a una versió on la xarxa de seguretat corre en paral·lel i no costa res.
Hi ha una autèntica cruïlla per a qualsevol app que et deixa fer un screenshot d'alguna cosa i preguntar-li a una IA. Pots executar OCR — extreure el text al mateix dispositiu, enviar una cadena minúscula, i llençar la imatge. O pots enviar els píxels — enviar la imatge real i deixar que un model amb capacitat de visió la llegeixi. L'OCR és barat i petit però cec a tot el que no és text: la disposició, el diagrama, el sagnat del codi, el subratllat vermell sota l'error. Els píxels ho preserven tot això però costen més bytes i requereixen un model que hi vegi. GeekBye va començar al costat de l'OCR d'aquella cruïlla i, al llarg de la v1.8.6 i la v1.8.7, va travessar cap al costat dels píxels. Aquell canvi és la part fàcil d'aquesta història. La part interessant és l'optimització que va venir amb ell, va trencar el fallback, i es va haver de desfer.
L'abans: llegeix el text, llença la imatge
Fins a aquest episodi, un screenshot es convertia en una resposta així: prems la drecera de captura, el client executa OCR — el framework Vision d'Apple a través d'un binari Swift VisionOCR a macOS, un script de PowerShell a Windows, tots dos darrere d'una única abstracció IOcrEngine — i el text extret s'embolcalla en una cadena i s'envia al backend com a screenshotText. La imatge en si mai no sortia de la màquina. Això és eficient, i per a un mur de text pla està bé. Però un screenshot rarament és només text. És una oferta de feina amb una disposició a dues columnes, un problema de programació amb un diagrama, un error amb un stack trace el sagnat del qual és la pista. L'OCR aplana tot això en una transcripció línia a línia i el model respon una pregunta més prima que la que vas fer.
El canvi: envia els píxels (v1.8.6)
La v1.8.6 va afegir l'altre camí. Un nou imageOptimizer.ts, construït sobre sharp i per tant multiplataforma, fa exactament tres coses amb un screenshot en brut:
sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })
Redimensiona a com a màxim 1920px d'ample (no ampliïs mai una captura més petita), converteix PNG a JPEG, qualitat 80. La sortida és base64 en brut amb el tipus de mitjà transportat en un camp separat, i viatja al backend en dos camps de petició nous al costat de l'antic: screenshotImage i screenshotImageMediaType, independents de screenshotText. El backend ara pot rebre una imatge, text, o tots dos.
Aquell pas d'optimització no és cosmètic. Un PNG de screenshot en brut fa 3–8 MB; el JPEG redimensionat fa aproximadament 200–500 KB. Això és un ordre de magnitud menys a la pujada, i també retalla la factura del model — els models de visió divideixen una imatge en tessel·les segons la seva resolució i cobren per tessel·la, així que limitar l'ample a 1920 i recomprimir redueix directament tant els bytes al cable com els tokens d'imatge que pagues, sense cap pèrdua significativa de llegibilitat per al text en pantalla.
Com sap l'app si el model tan sols hi pot veure? El backend ho anuncia. La resposta de /api/config va guanyar un bloc ai.vision indexat per pla, i el client cacheja un únic booleà — aiSupportsVision — a cada obtenció de config. Quan la visió és suportada, optimitza la imatge; quan no ho és, recorre a l'OCR. Així que la capacitat és un fet del backend, cachejat al costat del client, i la decisió per screenshot d'executar o saltar l'OCR es pren al client a partir d'aquella flag. El backend encara mana quin model respon realment.
L'optimització que era massa llesta
Aquí és on es torna instructiu. La primera versió del camí de visió executava l'OCR i l'optimització d'imatge juntes, en paral·lel. Després un commit de "perf" ho va estrènyer a un o l'un o l'altre: si el pla suporta visió, optimitza la imatge i salta't l'OCR completament; si no, executa l'OCR i salta't la feina d'imatge. Sobre el paper això és la victòria òbvia — per què extreure text que el model de visió no necessita? El changelog fins i tot ho anuncia: "OCR is skipped when the AI can read the image directly."
El problema és aquella paraula: fallback. El backend no té un sol model; té un primari i un fallback, i el fallback podria ser un model sense visió. Imagina la seqüència: el model primari del teu pla suporta visió, així que el client s'estalvia orgullós l'OCR i envia només la imatge. El proveïdor primari té un ensopec. El backend recorre a un model només-de-text — que ara rep una imatge que no pot llegir i cap text, perquè el client va optimitzar el text fins a fer-lo desaparèixer. El camí ràpid havia esborrat silenciosament la xarxa de seguretat. Pots descartar amb seguretat un camí ràpid redundant; no pots descartar allò que el camí ràpid era una drecera per evitar.
La solució: condiciona-ho al fallback, no al primari
La solució és la part que val la pena robar. No va ser tornar a executar sempre l'OCR — això llençaria la victòria per al cas comú. Va ser fer que el salt fos condicional a una segona flag de capacitat: el fallback suporta visió? La config del backend va guanyar un bloc fallbackVision; el client cacheja aiFallbackSupportsVision al costat de la primera flag. Ara la decisió té quatre branques:
- Primari i fallback tots dos hi veuen → optimitza la imatge, salta l'OCR. El camí ràpid i comú.
- El primari hi veu però el fallback és cec → optimitza la imatge i executa l'OCR, com a assegurança per al fallback.
- L'optimització d'imatge llança una excepció → OCR com a fallback dur.
- El pla no té gens de visió → només OCR.
I la segona branca — el cas de l'assegurança — és on importa l'últim moviment. Executar l'OCR després de l'optimització d'imatge hi tornaria a sumar la seva latència a sobre. Així que tots dos s'executen junts sota un únic Promise.all: l'optimització d'imatge i l'OCR se solapen, i l'OCR que existeix només per si el backend el necessita acaba dins del temps en què la imatge ja s'estava optimitzant igualment. La xarxa de seguretat és gratuïta en termes de temps real sempre que no es fa servir. Aquest és tot el truc en una frase: conserva el fallback barat, condiciona'l a si el teu proveïdor de fallback realment el necessita, i paral·lelitza'l perquè no costi res fins al dia que et salva.
Dos paranys més petits que el salt va fer saltar
Saltar-se agressivament feina ajornada té la manera de fer aflorar cada lloc que assumia silenciosament que la feina passaria. Dos van sortir a la revisió de PR la mateixa setmana.
Un bloqueig de cua. La cua de screenshots considerava un ítem "llest" quan tenia text OCR. Els screenshots de visió ara mai no rebien text OCR — per disseny — així que la comprovació de completesa hasIncompleteOCR esperava per sempre i la cua es penjava. La solució redefineix "llest" com té una imatge optimitzada o té text OCR, que és el que la pipeline ara realment garanteix.
Una flag de capacitat obsoleta. aiSupportsVision està cachejat, cosa que vol dir que pot quedar obsolet exactament en la direcció equivocada: un usuari tanca la sessió, o una obtenció de config falla, i el client es queda creient que encara pot enviar imatges a un model que ja no les suporta. La flag ara es reinicia en tancar la sessió i en fallar l'obtenció de config — una capacitat cachejada ha de tenir una resposta per a quan allò que descriu desapareix.
Un mot sobre els números
Perquè l'honestedat és el sentit d'aquesta sèrie: els missatges de commit porten xifres — l'optimització d'imatge "~30ms" contra l'OCR "~100–150ms," payloads "~200–500 KB vs ~3–8 MB PNG" — i són plausibles, però són les estimacions inline del mateix autor, no la sortida d'un benchmark. No hi ha cap latència end-to-end mesurada d'abans/després enlloc de l'historial. Tracta l'argument de la velocitat com a intenció de disseny sòlida, no com a resultat provat. La reducció de la mida de la payload és l'únic número en què pots confiar, perquè surt directament de redimensionar i recomprimir.
Tres coses que aquesta release ens va ensenyar
- Enviar els píxels supera llegir el text — quan el model hi pot veure. L'OCR descarta tot el que un screenshot comunica a part dels caràcters. Un model de visió llegeix la disposició, el diagrama i el sagnat, i un JPEG optimitzat ho fa assequible. Però és una aposta de capacitat, per això necessita un fallback.
- Pots optimitzar fins a fer desaparèixer un camí ràpid, mai la xarxa de seguretat. Saltar l'OCR per als usuaris de visió era correcte per al primari i incorrecte per al fallback. El bug no era el salt; era saltar segons la capacitat del primari quan la del fallback era la que importava.
- Una xarxa de seguretat gratuïta és una de paral·lelitzada. Condiciona la feina d'assegurança a si realment cal, i després executa-la al costat del camí ràpid perquè se solapi. Ben feta, la degradació graciosa no costa res fins al dia que és l'única raó per la qual la teva funcionalitat encara funciona.
Per al capítol anterior de la història de la v1, els dos modes de fallada d'un overlay click-through (v1.8.5); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.