Steven
Steven8 min citire

De la OCR la pixeli fără a pierde fallback-ul

GeekBye a încetat să-ți citească capturile de ecran cu OCR și a început să trimită imaginea propriu-zisă către modelele cu viziune — iar asta e partea ușoară. Partea instructivă e optimizarea care a sărit complet peste OCR pentru utilizatorii cu viziune, a rupt pe tăcute fallback-ul către modelele fără viziune și a trebuit readusă la o versiune în care plasa de siguranță rulează în paralel și nu costă nimic.

Inginerie
AI
Desktop
Lansări GeekBye
De la OCR la pixeli fără a pierde fallback-ul

Există o adevărată răscruce de drumuri pentru orice aplicație care te lasă să faci o captură de ecran și să întrebi un AI despre ea. Poți rula OCR — extragi textul pe dispozitiv, trimiți un șir minuscul și arunci poza. Sau poți trimite pixelii — expediezi imaginea propriu-zisă și lași un model capabil de viziune s-o citească. OCR-ul e ieftin și mic, dar orb la tot ce nu e text: layout-ul, diagrama, indentarea codului, linia roșie ondulată de sub eroare. Pixelii păstrează tot asta, dar costă mai mulți octeți și cer un model care poate vedea. GeekBye a pornit din partea OCR a acelei răscruci și, de-a lungul v1.8.6 și v1.8.7, a trecut de partea pixelilor. Comutarea aceea e partea ușoară a poveștii. Partea interesantă e optimizarea care a venit odată cu ea, a rupt fallback-ul și a trebuit desfăcută.

Înainte: citește textul, aruncă poza

Până la acest capitol, o captură de ecran devenea răspuns așa: apeși scurtătura de captură, clientul rulează OCR — framework-ul Vision de la Apple printr-un binar Swift VisionOCR pe macOS, un script PowerShell pe Windows, ambele în spatele unei singure abstracții IOcrEngine — iar textul extras e împachetat într-un șir și trimis către backend ca screenshotText. Imaginea în sine nu părăsea niciodată mașina. E eficient, și pentru un perete de text simplu e în regulă. Dar o captură de ecran e rareori doar text. E un anunț de job cu un layout pe două coloane, o problemă de programare cu o diagramă, o eroare cu un stack trace a cărui indentare e indiciul. OCR-ul aplatizează tot asta într-o transcriere linie cu linie, iar modelul răspunde la o întrebare mai subțire decât cea pe care ai pus-o.

Comutarea: trimite pixelii (v1.8.6)

v1.8.6 a adăugat celălalt traseu. Un nou imageOptimizer.ts, construit pe sharp și prin urmare cross-platform, face exact trei lucruri cu o captură de ecran brută:

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

Redimensionează la cel mult 1920px lățime (nu mări niciodată o captură mai mică), convertește PNG în JPEG, quality 80. Rezultatul e base64 brut cu tipul media purtat într-un câmp separat, și călătorește către backend în două câmpuri de cerere noi alături de cel vechi: screenshotImage și screenshotImageMediaType, independent de screenshotText. Backend-ul poate primi acum o imagine, text, sau ambele.

Pasul acela de optimizare nu e cosmetic. Un PNG brut de captură are 3–8 MB; JPEG-ul redimensionat are aproximativ 200–500 KB. Asta e un ordin de mărime scăzut din upload, și taie totodată nota de plată a modelului — modelele cu viziune împart o imagine în dale după rezoluția ei și taxează per dală, așa că plafonarea lățimii la 1920 și recomprimarea reduc direct atât octeții de pe fir cât și image tokens pe care îi plătești, fără nicio pierdere semnificativă de lizibilitate pentru textul de pe ecran.

De unde știe aplicația dacă modelul poate măcar vedea? Backend-ul o anunță. Răspunsul /api/config a căpătat un bloc ai.vision cheiat după plan, iar clientul memorează un singur boolean — aiSupportsVision — la fiecare preluare de config. Când viziunea e suportată, optimizează imaginea; când nu e, recade pe OCR. Așadar capabilitatea e un fapt de backend, memorat pe partea de client, iar decizia per-captură de a rula sau sări peste OCR se ia pe client din acel flag. Backend-ul deține în continuare care model răspunde de fapt.

Optimizarea care a fost prea isteață

Aici devine instructiv. Prima variantă a traseului cu viziune rula OCR și optimizarea imaginii împreună, în paralel. Apoi un commit „perf" l-a strâns într-un ori/ori: dacă planul suportă viziune, optimizează imaginea și sări complet peste OCR; dacă nu, rulează OCR și sări peste munca cu imaginea. Pe hârtie asta e câștigul evident — de ce să extragi text de care modelul cu viziune n-are nevoie? Changelog-ul chiar o promovează: "OCR is skipped when the AI can read the image directly."

Problema stă în noțiunea de fallback. Backend-ul nu are un singur model; are unul primar și unul de rezervă, iar cel de rezervă ar putea fi un model fără viziune. Imaginează-ți secvența: modelul primar al planului tău suportă viziune, așa că clientul sare mândru peste OCR și trimite doar imaginea. Furnizorul primar are un sughiț. Backend-ul recade pe un model doar-text — căruia i se înmânează acum o imagine pe care n-o poate citi și niciun text, pentru că clientul a optimizat textul până la dispariție. Traseul rapid ștersese pe tăcute plasa de siguranță. Poți renunța în siguranță la un traseu rapid redundant; nu poți renunța la lucrul pe lângă care traseul rapid era o scurtătură.

Fix-ul: condiționează după fallback, nu după cel primar

Fix-ul e partea pe care merită s-o furi. N-a fost întoarcerea la a rula mereu OCR — asta ar arunca la gunoi câștigul pentru cazul comun. A fost să facă săritul condiționat de un al doilea flag de capabilitate: fallback-ul suportă viziune? Configul backend-ului a căpătat un bloc fallbackVision; clientul memorează aiFallbackSupportsVision lângă primul flag. Acum decizia are patru ramuri:

  • Primarul și fallback-ul văd amândouă → optimizează imaginea, sări peste OCR. Traseul rapid, comun.
  • Primarul vede dar fallback-ul e orb → optimizează imaginea și rulează OCR, ca asigurare pentru fallback.
  • Optimizarea imaginii aruncă eroare → OCR ca fallback dur.
  • Planul n-are deloc viziune → doar OCR.

Iar a doua ramură — cazul de asigurare — e locul unde contează ultima mișcare. A rula OCR după optimizarea imaginii i-ar adăuga latența înapoi pe deasupra. Așa că cele două rulează împreună sub un singur Promise.all: optimizarea imaginii și OCR se suprapun, iar OCR-ul care există doar în caz că backend-ul are nevoie de el se termină în timpul în care oricum se optimiza imaginea. Plasa de siguranță e gratuită în termeni de timp real ori de câte ori nu e folosită. Ăsta e tot trucul într-o singură propoziție: păstrează fallback-ul ieftin, condiționează-l după dacă furnizorul tău de rezervă chiar are nevoie de el, și paralelizează-l ca să nu coste nimic până în ziua în care te salvează.

Două capcane mai mici pe care le-a declanșat săritul

Săritul agresiv peste munca amânată are darul de a scoate la suprafață fiecare loc care presupunea pe tăcute că munca se va întâmpla. Două au ieșit la iveală în review-ul de PR în aceeași săptămână.

Un deadlock de coadă. Coada de capturi considera un element „ready" când avea text OCR. Capturile cu viziune acum nu primeau niciodată text OCR — din construcție — așa că verificarea de completitudine hasIncompleteOCR aștepta la nesfârșit și coada se bloca. Fix-ul redefinește „ready" ca are o imagine optimizată sau are text OCR, ceea ce garantează de fapt acum pipeline-ul.

Un flag de capabilitate învechit. aiSupportsVision e memorat, ceea ce înseamnă că se poate învechi exact în direcția greșită: un utilizator se deloghează, sau o preluare de config eșuează, iar clientul rămâne crezând că mai poate trimite imagini către un model care nu le mai suportă. Flag-ul se resetează acum la delogare și la eșecul de preluare a configului — o capabilitate memorată trebuie să aibă un plan pentru momentul când lucrul pe care-l descrie dispare.

Un cuvânt despre cifre

Pentru că onestitatea e scopul acestei serii: mesajele de commit poartă cifre — optimizarea imaginii "~30ms" față de OCR "~100–150ms," payload-uri "~200–500 KB vs ~3–8 MB PNG" — și sunt plauzibile, dar sunt estimările inline ale autorului însuși, nu rezultatul unui benchmark. Nu există nicăieri în istoric vreo latență end-to-end măsurată înainte/după. Tratează argumentul vitezei ca intenție de design sănătoasă, nu ca rezultat dovedit. Reducerea dimensiunii de payload e singura cifră în care poți avea încredere, pentru că iese direct din redimensionare și recomprimare.

Trei lucruri pe care ni le-a predat această lansare

  1. Trimiterea pixelilor bate citirea textului — când modelul poate vedea. OCR-ul aruncă tot ce comunică o captură de ecran în afară de caractere. Un model cu viziune citește layout-ul, diagrama și indentarea, iar un JPEG optimizat face asta accesibil. Dar e un pariu pe capabilitate, motiv pentru care are nevoie de un fallback.
  2. Poți optimiza dispariția unui traseu rapid, niciodată a plasei de siguranță. Săritul peste OCR pentru utilizatorii cu viziune era corect pentru primar și greșit pentru fallback. Bug-ul nu era săritul; era săritul bazat pe capabilitatea primarului când cea a fallback-ului era cea care conta.
  3. O plasă de siguranță gratuită e una paralelizată. Condiționează munca de asigurare după dacă e chiar necesară, apoi rulează-o alături de traseul rapid ca să se suprapună. Făcută cum trebuie, degradarea grațioasă nu costă nimic până în ziua în care e singurul motiv pentru care funcționalitatea ta încă merge.

Pentru capitolul anterior al poveștii v1, cele două moduri de eșec ale unui overlay click-through (v1.8.5); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.