
Από το OCR στα pixels χωρίς να χάσουμε το fallback
Το GeekBye σταμάτησε να διαβάζει τα στιγμιότυπα οθόνης σου με OCR και άρχισε να στέλνει την ίδια την εικόνα σε vision μοντέλα — που είναι το εύκολο κομμάτι. Το διδακτικό κομμάτι είναι η βελτιστοποίηση που παρέκαμψε το OCR εντελώς για τους vision χρήστες, έσπασε σιωπηλά το fallback προς τα μη-vision μοντέλα, και έπρεπε να αναιρεθεί σε μια έκδοση όπου το δίχτυ ασφαλείας τρέχει παράλληλα και δεν κοστίζει τίποτα.
Υπάρχει ένα γνήσιο σταυροδρόμι για κάθε εφαρμογή που σου επιτρέπει να τραβήξεις στιγμιότυπο οθόνης από κάτι και να ρωτήσεις ένα AI για αυτό. Μπορείς να τρέξεις OCR — εξάγεις το κείμενο στη συσκευή, στέλνεις ένα μικροσκοπικό string και πετάς την εικόνα. Ή μπορείς να στείλεις τα pixels — αποστέλλεις την ίδια την εικόνα και αφήνεις ένα μοντέλο ικανό για vision να τη διαβάσει. Το OCR είναι φθηνό και μικρό αλλά τυφλό σε ό,τι δεν είναι κείμενο: τη διάταξη, το διάγραμμα, την εσοχή του κώδικα, την κόκκινη κυματιστή γραμμή κάτω από το σφάλμα. Τα pixels διατηρούν όλα αυτά αλλά κοστίζουν περισσότερα bytes και απαιτούν ένα μοντέλο που μπορεί να βλέπει. Το GeekBye ξεκίνησε από την OCR πλευρά αυτού του σταυροδρομιού και, μέσα από το v1.8.6 και το v1.8.7, πέρασε στην πλευρά των pixels. Αυτή η μετάβαση είναι το εύκολο κομμάτι της ιστορίας. Το ενδιαφέρον κομμάτι είναι η βελτιστοποίηση που ήρθε μαζί της, έσπασε το fallback, και έπρεπε να αναιρεθεί.
Το πριν: διάβασε το κείμενο, πέτα την εικόνα
Μέχρι αυτή την πορεία, ένα στιγμιότυπο οθόνης γινόταν απάντηση κάπως έτσι: πατάς τη συντόμευση καταγραφής, ο client τρέχει OCR — το Vision framework της Apple μέσα από ένα Swift VisionOCR binary στο macOS, ένα PowerShell script στα Windows, και τα δύο πίσω από μία αφαίρεση IOcrEngine — και το εξαγόμενο κείμενο τυλίγεται σε ένα string και στέλνεται στο backend ως screenshotText. Η ίδια η εικόνα δεν έφευγε ποτέ από το μηχάνημα. Αυτό είναι αποδοτικό, και για έναν τοίχο από απλό κείμενο είναι μια χαρά. Αλλά ένα στιγμιότυπο οθόνης σπάνια είναι μόνο κείμενο. Είναι μια αγγελία εργασίας με διάταξη δύο στηλών, ένα πρόβλημα προγραμματισμού με ένα διάγραμμα, ένα σφάλμα με ένα stack trace του οποίου η εσοχή είναι το κλειδί. Το OCR ισοπεδώνει όλα αυτά σε μια γραμμή-προς-γραμμή μεταγραφή και το μοντέλο απαντά σε ένα πιο ισχνό ερώτημα από αυτό που ρώτησες.
Η μετάβαση: στείλε τα pixels (v1.8.6)
Το v1.8.6 πρόσθεσε το άλλο μονοπάτι. Ένα νέο imageOptimizer.ts, χτισμένο πάνω στο sharp και επομένως cross-platform, κάνει ακριβώς τρία πράγματα σε ένα ακατέργαστο στιγμιότυπο οθόνης:
sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })
Άλλαξε μέγεθος σε το πολύ 1920px πλάτος (ποτέ μη μεγεθύνεις μια μικρότερη καταγραφή), μετέτρεψε PNG σε JPEG, quality 80. Η έξοδος είναι ακατέργαστο base64 με τον τύπο media να μεταφέρεται σε ξεχωριστό πεδίο, και ταξιδεύει προς το backend σε δύο νέα πεδία αιτήματος μαζί με το παλιό: screenshotImage και screenshotImageMediaType, ανεξάρτητα από το screenshotText. Το backend μπορεί τώρα να λάβει εικόνα, κείμενο, ή και τα δύο.
Αυτό το βήμα βελτιστοποίησης δεν είναι καλλωπιστικό. Ένα ακατέργαστο PNG στιγμιότυπου είναι 3–8 MB· το JPEG μετά την αλλαγή μεγέθους είναι περίπου 200–500 KB. Αυτό είναι μια τάξη μεγέθους λιγότερο στο upload, και επίσης κόβει τον λογαριασμό του μοντέλου — τα vision μοντέλα χωρίζουν μια εικόνα σε πλακίδια ανάλογα με την ανάλυσή της και χρεώνουν ανά πλακίδιο, οπότε το να περιορίσεις το πλάτος στα 1920 και να ξανασυμπιέσεις μειώνει άμεσα τόσο τα bytes στο καλώδιο όσο και τα image tokens που πληρώνεις, χωρίς ουσιαστική απώλεια αναγνωσιμότητας για το κείμενο στην οθόνη.
Πώς ξέρει η εφαρμογή αν το μοντέλο μπορεί καν να βλέπει; Το backend το διαφημίζει. Η απόκριση /api/config απέκτησε ένα μπλοκ ai.vision κλειδωμένο ανά πλάνο, και ο client κάνει cache ένα μοναδικό boolean — aiSupportsVision — σε κάθε λήψη config. Όταν το vision υποστηρίζεται, βελτιστοποίησε την εικόνα· όταν δεν υποστηρίζεται, πέσε πίσω σε OCR. Έτσι η ικανότητα είναι ένα γεγονός του backend, cached στην πλευρά του client, και η απόφαση ανά στιγμιότυπο να τρέξει ή να παρακαμφθεί το OCR λαμβάνεται στον client από αυτό το flag. Το backend εξακολουθεί να κατέχει ποιο μοντέλο πραγματικά απαντά.
Η βελτιστοποίηση που ήταν υπερβολικά έξυπνη
Εδώ γίνεται διδακτικό. Η πρώτη εκδοχή του vision μονοπατιού έτρεχε το OCR και τη βελτιστοποίηση εικόνας μαζί, παράλληλα. Μετά ένα «perf» commit το έσφιξε σε ένα ή/ή: αν το πλάνο υποστηρίζει vision, βελτιστοποίησε την εικόνα και παράκαμψε το OCR εντελώς· αν δεν υποστηρίζει, τρέξε OCR και παράκαμψε τη δουλειά της εικόνας. Στα χαρτιά αυτό είναι η προφανής νίκη — γιατί να εξάγεις κείμενο που το vision μοντέλο δεν χρειάζεται; Το changelog μάλιστα το διαφημίζει: "OCR is skipped when the AI can read the image directly."
Το πρόβλημα βρίσκεται στην έννοια του fallback. Το backend δεν έχει ένα μοντέλο· έχει ένα κύριο και ένα fallback, και το fallback μπορεί να είναι ένα μη-vision μοντέλο. Φαντάσου την ακολουθία: το κύριο μοντέλο του πλάνου σου υποστηρίζει vision, οπότε ο client περήφανα παρακάμπτει το OCR και στέλνει μόνο την εικόνα. Ο κύριος πάροχος τραυλίζει. Το backend πέφτει πίσω σε ένα μοντέλο μόνο για κείμενο — στο οποίο τώρα δίνεται μια εικόνα που δεν μπορεί να διαβάσει και κανένα κείμενο, επειδή ο client βελτιστοποίησε το κείμενο μέχρι εξαφάνισης. Το γρήγορο μονοπάτι είχε σιωπηλά διαγράψει το δίχτυ ασφαλείας. Μπορείς να παρατήσεις με ασφάλεια ένα περιττό γρήγορο μονοπάτι· δεν μπορείς να παρατήσεις το πράγμα γύρω από το οποίο το γρήγορο μονοπάτι ήταν μια συντόμευση.
Η διόρθωση: εξαρτήσου από το fallback, όχι από το κύριο
Η διόρθωση είναι το κομμάτι που αξίζει να κλέψεις. Δεν ήταν να επιστρέψεις στο να τρέχεις πάντα OCR — αυτό θα πετούσε τη νίκη για τη συνηθισμένη περίπτωση. Ήταν να κάνεις την παράκαμψη εξαρτημένη από ένα δεύτερο capability flag: υποστηρίζει το fallback vision; Το config του backend απέκτησε ένα μπλοκ fallbackVision· ο client κάνει cache το aiFallbackSupportsVision δίπλα στο πρώτο flag. Τώρα η απόφαση έχει τέσσερις κλάδους:
- Και το κύριο και το fallback βλέπουν → βελτιστοποίησε την εικόνα, παράκαμψε το OCR. Το γρήγορο, συνηθισμένο μονοπάτι.
- Το κύριο βλέπει αλλά το fallback είναι τυφλό → βελτιστοποίησε την εικόνα και τρέξε OCR, ως ασφάλεια για το fallback.
- Η βελτιστοποίηση εικόνας πετάει σφάλμα → OCR ως σκληρό fallback.
- Το πλάνο δεν έχει καθόλου vision → μόνο OCR.
Και ο δεύτερος κλάδος — η περίπτωση ασφάλειας — είναι εκεί που έχει σημασία η τελευταία κίνηση. Το να τρέξεις OCR μετά τη βελτιστοποίηση εικόνας θα πρόσθετε την καθυστέρησή του ξανά από πάνω. Έτσι τα δύο τρέχουν μαζί κάτω από ένα Promise.all: η βελτιστοποίηση εικόνας και το OCR επικαλύπτονται, και το OCR που υπάρχει μόνο σε περίπτωση που το backend το χρειαστεί τελειώνει μέσα στον χρόνο που η εικόνα βελτιστοποιούνταν έτσι κι αλλιώς. Το δίχτυ ασφαλείας είναι δωρεάν από άποψη πραγματικού χρόνου όποτε δεν χρησιμοποιείται. Αυτό είναι όλο το κόλπο σε μία πρόταση: κράτα το φθηνό fallback, εξάρτησέ το από το αν ο πάροχος fallback σου πραγματικά το χρειάζεται, και παραλληλοποίησέ το ώστε να μην κοστίζει τίποτα μέχρι την ημέρα που σε σώζει.
Δύο μικρότερες παγίδες που ενεργοποίησε η παράκαμψη
Η επιθετική παράκαμψη αναβαλλόμενης δουλειάς έχει τον τρόπο να φέρνει στην επιφάνεια κάθε σημείο που σιωπηλά υπέθετε ότι η δουλειά θα γινόταν. Δύο βγήκαν στο PR review την ίδια εβδομάδα.
Ένα deadlock ουράς. Η ουρά στιγμιότυπων θεωρούσε ένα στοιχείο «ready» όταν είχε OCR κείμενο. Τα vision στιγμιότυπα τώρα δεν έπαιρναν ποτέ OCR κείμενο — εκ σχεδιασμού — οπότε ο έλεγχος πληρότητας hasIncompleteOCR περίμενε για πάντα και η ουρά κρεμούσε. Η διόρθωση επαναπροσδιορίζει το «ready» ως έχει μια βελτιστοποιημένη εικόνα ή έχει OCR κείμενο, που είναι αυτό που το pipeline πλέον πραγματικά εγγυάται.
Ένα ξεπερασμένο capability flag. Το aiSupportsVision είναι cached, πράγμα που σημαίνει ότι μπορεί να παλιώσει ακριβώς προς τη λάθος κατεύθυνση: ένας χρήστης αποσυνδέεται, ή μια λήψη config αποτυγχάνει, και ο client μένει να πιστεύει ότι μπορεί ακόμα να στέλνει εικόνες σε ένα μοντέλο που δεν τις υποστηρίζει πλέον. Το flag τώρα επαναφέρεται στην αποσύνδεση και στην αποτυχία λήψης config — μια cached ικανότητα πρέπει να έχει ένα σενάριο για το πότε το πράγμα που περιγράφει εξαφανίζεται.
Μια κουβέντα για τους αριθμούς
Επειδή η ειλικρίνεια είναι το νόημα αυτής της σειράς: τα μηνύματα commit κουβαλούν νούμερα — βελτιστοποίηση εικόνας "~30ms" έναντι OCR "~100–150ms," payloads "~200–500 KB vs ~3–8 MB PNG" — και είναι εύλογα, αλλά είναι οι δικές του inline εκτιμήσεις του συγγραφέα, όχι το αποτέλεσμα ενός benchmark. Δεν υπάρχει καμία μετρημένη end-to-end καθυστέρηση πριν/μετά πουθενά στο ιστορικό. Αντιμετώπισε το επιχείρημα της ταχύτητας ως υγιή σχεδιαστική πρόθεση, όχι ως αποδεδειγμένο αποτέλεσμα. Η μείωση του μεγέθους του payload είναι το ένα νούμερο που μπορείς να εμπιστευτείς, επειδή προκύπτει κατευθείαν από την αλλαγή μεγέθους και την ξανασυμπίεση.
Τρία πράγματα που μας δίδαξε αυτή η έκδοση
- Το στείλσιμο των pixels νικά το διάβασμα του κειμένου — όταν το μοντέλο μπορεί να βλέπει. Το OCR πετάει όλα όσα επικοινωνεί ένα στιγμιότυπο οθόνης πέρα από τους χαρακτήρες. Ένα vision μοντέλο διαβάζει τη διάταξη, το διάγραμμα και την εσοχή, και ένα βελτιστοποιημένο JPEG το κάνει προσιτό. Αλλά είναι ένα στοίχημα ικανότητας, γι' αυτό χρειάζεται ένα fallback.
- Μπορείς να βελτιστοποιήσεις μέχρι εξαφάνισης ένα γρήγορο μονοπάτι, ποτέ το δίχτυ ασφαλείας. Η παράκαμψη του OCR για τους vision χρήστες ήταν σωστή για το κύριο και λάθος για το fallback. Το bug δεν ήταν η παράκαμψη· ήταν η παράκαμψη με βάση την ικανότητα του κύριου όταν αυτή του fallback ήταν εκείνη που είχε σημασία.
- Ένα δωρεάν δίχτυ ασφαλείας είναι ένα παραλληλοποιημένο. Εξάρτησε την ασφαλιστική δουλειά από το αν πραγματικά χρειάζεται, μετά τρέξε την δίπλα στο γρήγορο μονοπάτι ώστε να επικαλύπτεται. Καμωμένη σωστά, η ομαλή υποβάθμιση δεν κοστίζει τίποτα μέχρι την ημέρα που είναι ο μοναδικός λόγος που η λειτουργία σου ακόμα δουλεύει.
Για το προηγούμενο κεφάλαιο της ιστορίας του v1, οι δύο τρόποι αστοχίας ενός click-through overlay (v1.8.5)· και για ολόκληρη την πορεία, η ανατομία της παράδοσης λογισμικού στην τελειότητα.