
Διαγράψαμε 5.000 Γραμμές Κώδικα Ήχου — και οι Μεταγραφές Άρχισαν να Εμφανίζονται Δύο Φορές
Το GeekBye v1.6 ξερίζωσε δύο on-device Swift transcribers και τους αντικατέστησε με ένα ενοποιημένο pipeline — μια καθαρή διαγραφή πάνω από 5.000 γραμμές, καθώς κόσμος βρισκόταν εν μέσω συνάντησης στην εφαρμογή. Τότε οι μεταγραφές άρχισαν να εμφανίζονται δύο φορές, γιατί το bug μετακόμισε στο ένα επίπεδο που ακόμα νόμιζε ότι υπήρχαν δύο μηχανές.
Υπάρχει ένα ιδιαίτερο είδος τρόμου στο να κάνεις refactor τον κώδικα που τρέχει αυτή τη στιγμή, για ανθρώπους που βρίσκονται εν μέσω συνάντησης. Δεν μπορείς να ρίξεις τη λειτουργία για συντήρηση — κάποιος βασίζεται πάνω της για να μεταγράψει μια συνομιλία που δεν μπορεί να επαναλάβει. Το GeekBye v1.6 έκανε ακριβώς αυτό στο πιο κρίσιμο μονοπάτι της εφαρμογής: το audio pipeline. Αυτή είναι η ιστορία του τι διαγράψαμε, τι χάλασε, και τα δύο μαθήματα αξιοπιστίας που προέκυψαν από αυτό.
Δύο μηχανές, η μία σε Swift
Πριν από το v1.6, το speech-to-text του GeekBye έτρεχε on-device σε Swift binaries. Υπήρχαν δύο από αυτά: ένα που τύλιγε το Speech framework της Apple, και ένα μεγαλύτερο (πάνω από 1.500 γραμμές) που συνέλαβε διπλό ήχο — το μικρόφωνό σου συν τον ήχο συστήματος — μέσω ScreenCaptureKit και άνοιγε το δικό του WebSocket απευθείας στο backend μας για να κάνει streaming προς το Deepgram. Δύο transcribers, δύο μονοπάτια σύλληψης, δύο τρόποι να μιλήσεις στο δίκτυο, όλα σε μια compiled γλώσσα που σήμαινε ότι κάθε αλλαγή απαιτούσε recompile ενός binary.
Το v1.6.0 τα συμπύκνωσε όλα σε ένα pipeline. Το ενιαίο commit ενοποίησης διέγραψε τον Apple Speech transcriber (~1,145 γραμμές), τον Deepgram Swift capturer (~1,523 γραμμές), και τις δύο TypeScript γέφυρες, τον παλιό orchestrator, και τους dual-audio IPC handlers — μια καθαρή διαγραφή πάνω από 5,200 γραμμές. Στη θέση τους: ο renderer συλλαμβάνει τον ήχο μέσω τυπικών Web APIs, τον στέλνει ως PCM πάνω από IPC, και η Electron main process κατέχει ένα μοναδικό WebSocket μονοπάτι προς το backend, με τον πάροχο να επιλέγεται από τη διαμόρφωση του backend. Ένα μονοπάτι σύλληψης, ένας κάτοχος socket, ένα μέρος για να σκέφτεσαι.
Το να διαγράφεις πέντε χιλιάδες γραμμές είναι υπέροχο. Για περίπου μία μέρα.
Το bug μετακόμισε στο επίπεδο που ακόμα νόμιζε ότι υπήρχαν δύο
Τότε οι μεταγραφές άρχισαν να εμφανίζονται δύο φορές. Μία μόνο προφορική πρόταση αποθηκευόταν ως δύο πανομοιότυπες εγγραφές — μερικές φορές χειρότερα.
Η βαθύτερη αιτία είναι το πιο διδακτικό πράγμα σε όλη αυτή την κυκλοφορία, γιατί είναι ένας γενικός νόμος της ενοποίησης. Είχαμε ενοποιήσει τους παρόχους στη main process — ένα socket, ένας handler. Αλλά το επίπεδο React δεν το είχε πάρει χαμπάρι: εξακολουθούσε να προσαρτά δύο event hooks ειδικά ανά πάροχο ταυτόχρονα, ένα φτιαγμένο για το Deepgram και ένα για το ElevenLabs. Και τα δύο άκουγαν. Και τα δύο αποθήκευαν. Κάθε μεταγραφή γινόταν persist από όποια hooks ήταν ζωντανά — και ήταν και τα δύο.
Και υπήρχε ένας δεύτερος, πιο ύπουλος διπλασιασμός από κάτω του: το ElevenLabs εκπέμπει κάθε οριστικοποιημένη γραμμή δύο φορές — μία ως συμβάν committed και ξανά ως committed_with_timestamps. Έτσι, ακόμα και με ένα μόνο hook, μία πρόταση μπορούσε να φτάσει ως δύο συμβάντα.
Η διόρθωση (στη γραμμή v1.6) είναι ένα καθαρό δίπτυχο που αντιστοιχεί ακριβώς στις δύο αιτίες: βάλε κάθε event hook πίσω από μια σημαία enabled καθοδηγούμενη από τη διαμόρφωση ενεργού παρόχου του backend, ώστε μόνο ένας listener να είναι ποτέ ζωντανός· και πρόσθεσε αφαίρεση διπλότυπων κειμένου ανά πηγή (θυμήσου την τελευταία αποθηκευμένη γραμμή για το μικρόφωνο και για τον ήχο συστήματος) για να καταπιείς τη διπλή εκπομπή του ElevenLabs.
Το μάθημα είναι μεγαλύτερο από αυτό το bug: όταν ενοποιείς δύο backends πίσω από ένα interface, η διπλασιασμός δεν εξαφανίζεται — μεταναστεύει στο επίπεδο που ακόμα υποθέτει ότι υπάρχουν δύο. Συμπυκνώσαμε τους σωλήνες στη main process και η διαρροή αναδύθηκε δύο επίπεδα πιο πάνω, στην καλωδίωση συμβάντων του UI. Η ενοποίηση ενός pipeline δεν έχει ολοκληρωθεί όταν ενοποιηθεί ο producer· έχει ολοκληρωθεί όταν κάθε consumer έχει πάψει να πιστεύει στο παλιό σχήμα.
Η πρώτη επανασύνδεση: «τα παρατάμε μετά από πέντε προσπάθειες»
Το v1.6.0 παρέδωσε επίσης την πρώτη-πρώτη WebSocket αυτόματη επανασύνδεση του GeekBye με exponential backoff — και αξίζει να τη δείξουμε ακριβώς επειδή είναι ο πρωτόγονος πρόγονος της αλεξίσφαιρης έκδοσης που τρέχουμε σήμερα.
Η αρχική ήταν περιορισμένη. Πέντε προσπάθειες, καθυστερήσεις 1s, 2s, 4s, 8s, 16s — και μετά παραδινόταν, εκπέμποντας ένα μοιραίο σφάλμα: «Χάθηκε η σύνδεση — παρακαλώ κάνε επανεκκίνηση της μεταγραφής.» Εκείνη την εποχή αυτό έμοιαζε υπεύθυνο: μην κάνεις retry για πάντα, πες στον χρήστη ειλικρινά. Στην πράξη ήταν ένα UX bug μεταμφιεσμένο. Ένα δεκαεξάδευτο τρεμόπαιγμα δικτύου — ένα Wi-Fi handoff, μια επανασύνδεση VPN, ένα tunnel — είναι παροδικό, αλλά μια περιορισμένη επανάληψη το αντιμετωπίζει ως τελεσίδικο. Ο χρήστης δεν έκανε τίποτα λάθος και του είπαν να κάνει επανεκκίνηση μιας ζωντανής συνάντησης.
Αυτή είναι ακριβώς η αποτυχία που εξάλειψε ο σημερινός σχεδιασμός. Σήμερα το GeekBye επανασυνδέεται με απεριόριστο backoff και μάλιστα εναλλάσσει παρόχους, και σταματά μόνο για έναν πραγματικά μοιραίο λόγο — έλεγχος ταυτότητας, όριο, χρέωση. Ολόκληρο το ταξίδι από το «τα παρατάμε μετά από πέντε» στο «μην τα παρατάς ποτέ σε ένα παροδικό σφάλμα» αφηγείται στο γιατί ο AI notetaker σου σταματά σε κακό Wi-Fi. Το v1.6 είναι εκεί που ξεκίνησε αυτός ο δρόμος, με την αφελή έκδοση που έπρεπε να υπάρξει πρώτη.
Οι ξεπερασμένες συνδέσεις σε στοιχειώνουν εκτός αν έχουν ονόματα
Ένα ακόμα μάθημα αξιοπιστίας προσγειώθηκε στο v1.6.3, διορθώνοντας μια λεπτή κατηγορία bug που όποιος κάνει επανασύνδεση πάνω από ένα stateful backend θα συναντήσει τελικά.
Όταν επανασυνδέεσαι — ή αλλάζεις γλώσσα μεταγραφής εν μέσω συνεδρίας, που κάνει επανασύνδεση από κάτω — η παλιά σύνδεση δεν πεθαίνει πάντα ήσυχα. Τα μηνύματα του θανατοκουδουνίσματός της (CONNECTION_LOST, disconnected) μπορούν να φτάσουν αφότου η νέα σύνδεση είναι ήδη υγιής, και να γκρεμίσουν την τέλεια καλή αντικαταστάτρια. Η διόρθωση δίνει σε κάθε προσπάθεια σύνδεσης μια ταυτότητα — ένα session ID ανά σύνδεση σφραγισμένο πάνω στο WebSocket URL — συν ένα σύντομο παράθυρο χάριτος μετά την επανασύνδεση, κατά τη διάρκεια του οποίου τα ξεπερασμένα μηνύματα αποσύνδεσης από μια προηγούμενη προσπάθεια αγνοούνται. Στέλνει επίσης ένα ρητό μήνυμα stop πριν κλείσει ένα socket, ώστε το backend να μπορεί να γκρεμίσει την παλιά συνεδρία καθαρά.
Η αρχή: οποιαδήποτε λογική επανασύνδεσης πάνω από ένα stateful backend χρειάζεται να επισημαίνει κάθε προσπάθεια με μια ταυτότητα και να αντιμετωπίζει τα καθυστερημένα μηνύματα από προηγούμενες προσπάθειες ως θόρυβο — αλλιώς μια επιτυχημένη επανασύνδεση σκοτώνεται από την τελευταία πνοή του προκατόχου της.
Τρία πράγματα που δίδαξε το v1.6
- Η ενοποίηση μετακομίζει τα bugs· δεν τα διαγράφει. Συμπύκνωσε δύο μηχανές σε μία και έλεγξε κάθε consumer που ακόμα νομίζει ότι υπάρχουν δύο. Ο δικός μας ήταν το UI, που προσάρταζε δύο event hooks αφότου οι σωλήνες είχαν ήδη συγχωνευτεί.
- Η πρώτη σου επανασύνδεση θα είναι περιορισμένη, και το περιορισμένο είναι λάθος για ζωντανά streams. Το «τα παρατάμε μετά από N προσπάθειες» μετατρέπει ένα παροδικό τρεμόπαιγμα σε τελεσίδικη αποτυχία για την οποία κατηγορείται ο χρήστης. Διάκρινε το μοιραίο (έλεγχος ταυτότητας, όριο) από το παροδικό (ένα πεσμένο socket) από την πρώτη κιόλας έκδοση.
- Οι επανασυνδέσεις χρειάζονται ταυτότητα. Επισήμανε κάθε προσπάθεια· αγνόησε τα καθυστερημένα μηνύματα από νεκρές προσπάθειες. Μια σύνδεση χωρίς όνομα μπορεί να δολοφονηθεί από το ίδιο της το φάντασμα.
Αυτό είναι το δεύτερο κεφάλαιο της ιστορίας αξιοπιστίας που γίνεται το GeekBye v2. Για το πρώτο, δες τη σάγκα του auto-update που πήρε έξι κυκλοφορίες σε τέσσερις μέρες (v1.5.x)· για το πού έφτασε τελικά η δουλειά της επανασύνδεσης, γιατί ο AI notetaker σου σταματά σε κακό Wi-Fi· και για ολόκληρη την πορεία, η ανατομία της παράδοσης λογισμικού στην τελειότητα.