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

Διαγράψαμε 5.000 Γραμμές Κώδικα Ήχου — και οι Μεταγραφές Άρχισαν να Εμφανίζονται Δύο Φορές

Το GeekBye v1.6 ξερίζωσε δύο on-device Swift transcribers και τους αντικατέστησε με ένα ενοποιημένο pipeline — μια καθαρή διαγραφή πάνω από 5.000 γραμμές, καθώς κόσμος βρισκόταν εν μέσω συνάντησης στην εφαρμογή. Τότε οι μεταγραφές άρχισαν να εμφανίζονται δύο φορές, γιατί το bug μετακόμισε στο ένα επίπεδο που ακόμα νόμιζε ότι υπήρχαν δύο μηχανές.

Μηχανική
Μεταγραφή
Αρχιτεκτονική
Κυκλοφορίες GeekBye
Διαγράψαμε 5.000 Γραμμές Κώδικα Ήχου — και οι Μεταγραφές Άρχισαν να Εμφανίζονται Δύο Φορές

Υπάρχει ένα ιδιαίτερο είδος τρόμου στο να κάνεις 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

  1. Η ενοποίηση μετακομίζει τα bugs· δεν τα διαγράφει. Συμπύκνωσε δύο μηχανές σε μία και έλεγξε κάθε consumer που ακόμα νομίζει ότι υπάρχουν δύο. Ο δικός μας ήταν το UI, που προσάρταζε δύο event hooks αφότου οι σωλήνες είχαν ήδη συγχωνευτεί.
  2. Η πρώτη σου επανασύνδεση θα είναι περιορισμένη, και το περιορισμένο είναι λάθος για ζωντανά streams. Το «τα παρατάμε μετά από N προσπάθειες» μετατρέπει ένα παροδικό τρεμόπαιγμα σε τελεσίδικη αποτυχία για την οποία κατηγορείται ο χρήστης. Διάκρινε το μοιραίο (έλεγχος ταυτότητας, όριο) από το παροδικό (ένα πεσμένο socket) από την πρώτη κιόλας έκδοση.
  3. Οι επανασυνδέσεις χρειάζονται ταυτότητα. Επισήμανε κάθε προσπάθεια· αγνόησε τα καθυστερημένα μηνύματα από νεκρές προσπάθειες. Μια σύνδεση χωρίς όνομα μπορεί να δολοφονηθεί από το ίδιο της το φάντασμα.

Αυτό είναι το δεύτερο κεφάλαιο της ιστορίας αξιοπιστίας που γίνεται το GeekBye v2. Για το πρώτο, δες τη σάγκα του auto-update που πήρε έξι κυκλοφορίες σε τέσσερις μέρες (v1.5.x)· για το πού έφτασε τελικά η δουλειά της επανασύνδεσης, γιατί ο AI notetaker σου σταματά σε κακό Wi-Fi· και για ολόκληρη την πορεία, η ανατομία της παράδοσης λογισμικού στην τελειότητα.

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

Η ανατομία της τέλειας κυκλοφορίας λογισμικού: Πώς ο έλεγχος κώδικα έπιασε αυτό που τα τεστ δεν μπορούσαν
Steven
Steven7 λεπτά ανάγνωσης

Η ανατομία της τέλειας κυκλοφορίας λογισμικού: Πώς ο έλεγχος κώδικα έπιασε αυτό που τα τεστ δεν μπορούσαν

Σε όλη τη σειρά του GeekBye v2, το ίδιο πράγμα συμβαίνει ξανά και ξανά: μια διόρθωση περνάει κάθε τεστ στο μηχάνημα του προγραμματιστή, και μετά ο έλεγχος κώδικα αποδεικνύει ότι θα είχε αποτύχει για σχεδόν όλους. Αυτή είναι η ροή εργασίας πίσω από εννέα κυκλοφορίες — η πύλη ελέγχου, οι διορθώσεις που πιάστηκαν από νωρίς και η πειθαρχία του «τεστ πριν την αποστολή» που μετατρέπει το «δουλεύει σ' εμένα» σε «δουλεύει».

Μηχανική
Διαδικασία
Έλεγχος Κώδικα
Μία μεταβλητή CSS, πέντε γύροι ελέγχου κι ένα Swift toolchain που είπε ψέματα
Steven
Steven7 λεπτά ανάγνωσης

Μία μεταβλητή CSS, πέντε γύροι ελέγχου κι ένα Swift toolchain που είπε ψέματα

Το GeekBye v2.0.7 έκανε ολόκληρο το overlay ρυθμιζόμενα ημιδιαφανές — που ακούγεται σαν μια αλλαγή CSS μιας γραμμής και σε καμία περίπτωση δεν ήταν. Η πραγματική ιστορία είναι τι έπιασε ο έλεγχος κώδικα: δύο επιφάνειες που αρνούνταν να ξεθωριάσουν, μια μπάρα εγγραφής που φαινόταν λάθος στην ίδια αδιαφάνεια, κι ένα μεταγλωττισμένο εκτελέσιμο που ταλαντεύτηκε κατά 672 bytes επειδή η δική μας τεκμηρίωση είπε σε έναν ελεγκτή τη λάθος έκδοση Swift.

Μηχανική
Σχεδιασμός
Build
Ήρεμο Λογισμικό: Σκοτώνοντας έναν Διακόπτη που Τρεμόπαιζε και Μαθαίνοντας το Chat να Λέει σε Ποια Λειτουργία Βρίσκεται
Steven
Steven6 λεπτά ανάγνωσης

Ήρεμο Λογισμικό: Σκοτώνοντας έναν Διακόπτη που Τρεμόπαιζε και Μαθαίνοντας το Chat να Λέει σε Ποια Λειτουργία Βρίσκεται

Δύο μικρές κυκλοφορίες του GeekBye, χωρίς εντυπωσιακές λειτουργίες — απλώς ένας διακόπτης ρυθμίσεων που σταμάτησε να τρεμοπαίζει και ένας βοηθός chat που επιτέλους σου λέει αν απάντησε σε λειτουργία σύσκεψης ή σε λειτουργία προγραμματισμού. Να τι πραγματικά κοστίζει το «ήρεμο λογισμικό», μια λεπτομέρεια τη φορά.

Προϊόν
Σχεδιασμός
Μηχανική