Steven
Steven8 λεπτά ανάγνωσης

Από το OCR στα pixels χωρίς να χάσουμε το fallback

Το GeekBye σταμάτησε να διαβάζει τα στιγμιότυπα οθόνης σου με OCR και άρχισε να στέλνει την ίδια την εικόνα σε vision μοντέλα — που είναι το εύκολο κομμάτι. Το διδακτικό κομμάτι είναι η βελτιστοποίηση που παρέκαμψε το OCR εντελώς για τους vision χρήστες, έσπασε σιωπηλά το fallback προς τα μη-vision μοντέλα, και έπρεπε να αναιρεθεί σε μια έκδοση όπου το δίχτυ ασφαλείας τρέχει παράλληλα και δεν κοστίζει τίποτα.

Μηχανική
AI
Desktop
Εκδόσεις GeekBye
Από το OCR στα pixels χωρίς να χάσουμε το fallback

Υπάρχει ένα γνήσιο σταυροδρόμι για κάθε εφαρμογή που σου επιτρέπει να τραβήξεις στιγμιότυπο οθόνης από κάτι και να ρωτήσεις ένα 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 είναι το ένα νούμερο που μπορείς να εμπιστευτείς, επειδή προκύπτει κατευθείαν από την αλλαγή μεγέθους και την ξανασυμπίεση.

Τρία πράγματα που μας δίδαξε αυτή η έκδοση

  1. Το στείλσιμο των pixels νικά το διάβασμα του κειμένου — όταν το μοντέλο μπορεί να βλέπει. Το OCR πετάει όλα όσα επικοινωνεί ένα στιγμιότυπο οθόνης πέρα από τους χαρακτήρες. Ένα vision μοντέλο διαβάζει τη διάταξη, το διάγραμμα και την εσοχή, και ένα βελτιστοποιημένο JPEG το κάνει προσιτό. Αλλά είναι ένα στοίχημα ικανότητας, γι' αυτό χρειάζεται ένα fallback.
  2. Μπορείς να βελτιστοποιήσεις μέχρι εξαφάνισης ένα γρήγορο μονοπάτι, ποτέ το δίχτυ ασφαλείας. Η παράκαμψη του OCR για τους vision χρήστες ήταν σωστή για το κύριο και λάθος για το fallback. Το bug δεν ήταν η παράκαμψη· ήταν η παράκαμψη με βάση την ικανότητα του κύριου όταν αυτή του fallback ήταν εκείνη που είχε σημασία.
  3. Ένα δωρεάν δίχτυ ασφαλείας είναι ένα παραλληλοποιημένο. Εξάρτησε την ασφαλιστική δουλειά από το αν πραγματικά χρειάζεται, μετά τρέξε την δίπλα στο γρήγορο μονοπάτι ώστε να επικαλύπτεται. Καμωμένη σωστά, η ομαλή υποβάθμιση δεν κοστίζει τίποτα μέχρι την ημέρα που είναι ο μοναδικός λόγος που η λειτουργία σου ακόμα δουλεύει.

Για το προηγούμενο κεφάλαιο της ιστορίας του v1, οι δύο τρόποι αστοχίας ενός click-through overlay (v1.8.5)· και για ολόκληρη την πορεία, η ανατομία της παράδοσης λογισμικού στην τελειότητα.

Σχετικά Άρθρα

Οι δύο τρόποι αστοχίας ενός click-through overlay
Steven
Steven9 λεπτά ανάγνωσης

Οι δύο τρόποι αστοχίας ενός click-through overlay

Το παράθυρο του GeekBye επιπλέει πάνω από τα πάντα και αφήνει τα κλικ σου να περνούν μέσα από αυτό — εκτός από εκεί όπου έχει κουμπιά. Αυτό είναι ένα αμφίπλευρο συμβόλαιο, και το v1.8.5 και το v1.8.14 δείχνουν πώς μοιάζει όταν σπάει η κάθε πλευρά: μια έκδοση όπου το overlay κατάπιε έναν διάλογο συστήματος, μία όπου έκλεψε τα πλήκτρα σου. Η νικήτρια διόρθωση για τη δεύτερη ήταν η διαγραφή κώδικα.

Μηχανική
Electron
Desktop
Βάζοντας την έκδοση στο CI, δύο φορές
Steven
Steven8 λεπτά ανάγνωσης

Βάζοντας την έκδοση στο CI, δύο φορές

Το GeekBye v1.8.4 μετέφερε τα release builds στο CI για macOS και Windows. Αυτό που δεν λέει το changelog είναι ότι το δοκιμάσαμε ξανά μια φορά, το διαγράψαμε έναν μήνα αργότερα λόγω του κόστους των runners, και το κάναμε να κρατήσει μόνο τη δεύτερη φορά — επειδή ως τότε το release script δούλευε ήδη με το χέρι.

Μηχανική
CI/CD
Desktop
Κυκλοφορώντας τριάντα γλώσσες χωρίς δίχτυ ασφαλείας
Steven
Steven8 λεπτά ανάγνωσης

Κυκλοφορώντας τριάντα γλώσσες χωρίς δίχτυ ασφαλείας

Το GeekBye v1.8.3 συνέδεσε το react-i18next και μετέφρασε ολόκληρη την εφαρμογή σε 30 γλώσσες. Το κομμάτι της library ήταν ρουτίνα. Το ενδιαφέρον κομμάτι είναι αυτό που έλειπε: κανένας έλεγχος parity κλειδιών και κανένας extractor strings — έτσι hardcoded αγγλικά ξέφυγαν, ένα δανέζικο τυπογραφικό λάθος κυκλοφόρησε, και το changelog δεν μπορούσε να συμφωνήσει αν η εφαρμογή μιλούσε 28, 29 ή 30 γλώσσες.

Μηχανική
i18n
Desktop