
Η σιωπή ήταν φέρουσα
Οι δύο τελευταίες εκδόσεις του GeekBye v1 αφορούν την ίδια άβολη αλήθεια: η μεταγραφή σε πραγματικό χρόνο πάνω από ένα πραγματικό δίκτυο δεν είναι χωρίς απώλειες, και η έντιμη κίνηση είναι να σταματήσεις να προσποιείσαι ότι είναι. Η v1.8.20 κρατούσε ένα αντίγραφο κάθε κομματιού ήχου στον δίσκο πριν το πετάξει κατά τη διάρκεια μιας επανασύνδεσης, και άρχισε να σημειώνει φωναχτά τα κενά στη μεταγραφή. Η v1.9.0 σταμάτησε να στέλνει σιωπή για να εξοικονομήσει εύρος ζώνης — και ανακάλυψε ότι η σιωπή ήταν ακριβώς το σήμα που χρησιμοποιούσε το σύστημα μεταγραφής για να ξέρει ότι μια πρόταση είχε τελειώσει. Δύο εκδόσεις για το κόστος του να πετάς πράγματα.
Οι δύο τελευταίες εκδόσεις του GeekBye v1 — η v1.8.20 και η v1.9.0, εκείνες ακριβώς πριν το ορόσημο της έκδοσης 2 — μοιράζονται ένα θέμα που γίνεται προφανές μόνο όταν τις διαβάσεις μαζί. Και οι δύο αφορούν πράγματα που η εφαρμογή πετούσε σιωπηλά, και οι δύο καταλήγουν στο ίδιο συμπέρασμα: αυτό που πετάς για να είσαι αποδοτικός έκανε περισσότερη δουλειά από όση νόμιζες. Η μία έκδοση πετούσε ήχο κατά τις επανασυνδέσεις δικτύου. Η άλλη πετούσε σιωπή για να εξοικονομήσει εύρος ζώνης. Και στις δύο περιπτώσεις το πεταμένο πράγμα αποδείχτηκε φέρον.
Η μεταγραφή σε πραγματικό χρόνο δεν είναι χωρίς απώλειες, και η v1.8.20 το παραδέχεται
Να ένα γεγονός για το streaming speech-to-text πάνω από ένα WebSocket από το οποίο είναι εύκολο να γυρίσεις το βλέμμα αλλού: όταν η σύνδεση πέφτει και επανασυνδέεται — ή όταν η εφαρμογή κάνει failover από τον έναν πάροχο μεταγραφής στον άλλο, από το Deepgram στο ElevenLabs — ο client πετάει σκόπιμα τα κομμάτια ήχου που συλλαμβάνονται κατά τη διάρκεια εκείνου του κενού. Πρέπει. Ένας μόλις συνδεδεμένος πάροχος που ξαφνικά λαμβάνει μια συσσώρευση παλιού ήχου θα πνιγεί ή, χειρότερα, θα κλειδώσει τον ανιχνευτή φωνητικής δραστηριότητάς του στο λάθος πράγμα. Έτσι ο client πετάει εκείνα τα κομμάτια.
Το πρόβλημα δεν είναι το πέταγμα. Το πρόβλημα είναι τι σήμαινε το πέταγμα: εκείνες οι λέξεις απλώς δεν εμφανίζονταν ποτέ στη μεταγραφή, και τίποτα δεν σου έλεγε ότι έλειπαν. Η ηχογράφηση φαινόταν πλήρης. Δεν ήταν.
Η v1.8.20 επιτίθεται σε αυτό σε δύο μέτωπα, και κανένα από τα δύο δεν είναι „κάνε τις επανασυνδέσεις να μη χάνουν ποτέ ήχο" — επειδή δεν μπορείς, όχι σε πραγματικό χρόνο. Αντ αυτού κάνει την απώλεια ανακτήσιμη και έντιμη.
Το ανακτήσιμο μισό είναι ένα ανθεκτικό PCM tee. Στον handler σύλληψης της main-process, κάθε κομμάτι που συλλαμβάνεται γράφεται τώρα σε ένα αρχείο στον δίσκο πριν φτάσει στις πύλες απόρριψης της επανασύνδεσης και της περιόδου χάριτος:
every captured chunk is written to
<userData>/sessions/session-<ts>.wavbefore realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass
Ο μηχανισμός είναι ευχάριστα χαμηλής τεχνολογίας: το teeChunkToDisk ανοίγει ένα WriteStream, δεσμεύει εκ των προτέρων μια κεφαλίδα WAV 44 bytes, προσαρτά ακατέργαστα 16-bit mono samples καθώς φτάνουν, και διορθώνει την κεφαλίδα με τα τελικά μεγέθη στο σταμάτημα (fs.openSync(..., 'r+'), γράφει 44 bytes στο offset 0). Το αποτέλεσμα είναι ένα αναπαραγώγιμο WAV του πλήρους ήχου — συμπεριλαμβανομένων των μερών που η μεταγραφή σε πραγματικό χρόνο δεν είδε ποτέ. Όταν η ηχογράφηση τελειώνει, εκπέμπει ένα event session-audio-saved με τη διαδρομή του αρχείου και τον αριθμό των bytes.
Δύο σημειώσεις εντιμότητας έχουν σημασία εδώ, επειδή είναι εύκολο να το υπερβάλεις αυτό. Πρώτον: το tee δεν γεμίζει το κενό σε αυτές τις εκδόσεις. Διατηρεί τον ακατέργαστο ήχο για ένα μεταγενέστερο πέρασμα συμφιλίωσης — μια batch επαναμεταγραφή που συμφιλιώνει το αρχείο με τη ζωντανή μεταγραφή — που είναι ένα άγκιστρο που αυτές οι εκδόσεις τοποθετούν, όχι ένα χαρακτηριστικό που παραδίδουν. Το επακόλουθο commit μάλιστα σχολιάζει το session-audio-saved με ένα TODO που δείχνει στα ζητήματα συμφιλίωσης, ώστε κανείς να μην το μπερδέψει με νεκρό κώδικα. Δεύτερον: αυτό κοστίζει δίσκο. Η ομάδα υπολόγισε περίπου 86 MB ανά ηχογράφηση 30 λεπτών, οπότε η ίδια έκδοση προσθέτει ένα StartupCleanupService που κλαδεύει τις αποθηκευμένες sessions παλαιότερες των 7 ημερών. Ένα δίχτυ ασφαλείας που μεγαλώνει χωρίς όριο είναι μια διαρροή· το κλάδεμα είναι αυτό που καθιστά το tee εφικτό να κυκλοφορήσει.
Το έντιμο μισό είναι ακόμη πιο απλό. Στην πρώτη προσπάθεια επανασύνδεσης, ο client γράφει μια πραγματική, ορατή εγγραφή στη μεταγραφή:
[reconnecting — audio may be missing]
Εκείνη η μία γραμμή είναι όλη η φιλοσοφία της έκδοσης σε μινιατούρα. Η εφαρμογή δεν μπορεί να εγγυηθεί ότι άκουσε τα πάντα, οπότε σταματά να υπονοεί ότι το έκανε. Ένα κενό που μπορείς να δεις είναι ένα κενό που μπορείς να παρακάμψεις· ένα κενό που δεν μπορείς να δεις είναι μια αναφορά bug που περιμένει να συμβεί.
v1.9.0: η σιωπή που εξάλειψες με τη βελτιστοποίηση ήταν το σήμα
Η v1.9.0 είναι η τελευταία v1 έκδοση, και είναι εκεί όπου το backend παρέδωσε ένα σύνολο ευθυνών ζωντανής μεταγραφής στον client — πέταγμα σιωπής, απόδοση κάθε γραμμής μεταγραφής στο μικρόφωνο ή τον ήχο συστήματος από μια τοπική χρονογραμμή φωνητικής δραστηριότητας, κατοχή του βρόχου επανασύνδεσης, επιβολή ορίων ηχογράφησης. Το μεγαλύτερο μέρος αυτού είναι το θεμέλιο για την αρχιτεκτονική της έκδοσης 2 και έχει ειπωθεί αλλού. Αλλά ένα νήμα μέσα του είναι η καλύτερη μικρή ιστορία σε οποιαδήποτε από τις δύο εκδόσεις, επειδή είναι μια διόρθωση που έπρεπε να αναιρέσει τη ζημιά μιας βελτιστοποίησης που ήταν εντελώς σωστή μεμονωμένα.
Ξεκινά με μια προφανώς καλή αλλαγή: σταμάτα να στέλνεις σιωπή. Ο client άρχισε να φιλτράρει τα κομμάτια των οποίων η κυρίαρχη πηγή ήταν σιωπή πριν τα στείλει πάνω από το socket — λιγότερο εύρος ζώνης, λιγότερος άχρηστος ήχος για επεξεργασία από το backend, και τιμούσε ένα ρητό client-contract του backend που έλεγε „μη στέλνεις σιωπή". Κάθε λόγος για να το κάνεις ήταν βάσιμος.
Και έσπασε την οριστικοποίηση των εκφωνήσεων εντελώς. Το commit που το διορθώνει εκθέτει την αιτιακή αλυσίδα με ασυνήθιστη ειλικρίνεια:
After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.
Αυτή είναι η πλατωνική μορφή μιας leaky abstraction που δαγκώνει πίσω. Ο ανιχνευτής φωνητικής δραστηριότητας του συστήματος μεταγραφής δεν έχει μια ξεχωριστή είσοδο „ο ομιλητής τελείωσε" — συμπεραίνει το τέλος μιας πρότασης ακούγοντας περίπου ένα δευτερόλεπτο σιωπής στον ήχο. Η σιωπή δεν ήταν θόρυβος στο καλώδιο· ήταν ένα μήνυμα. Φιλτράροντάς την για να εξοικονομήσει εύρος ζώνης, ο client σταμάτησε κατά λάθος να στέλνει το ένα σήμα που έλεγε στο σύστημα μεταγραφής ότι μια πρόταση είχε τελειώσει. Έτσι κάθε μερική μεταγραφή απλώς συνέχιζε να μεγαλώνει, η ουρά της να σβήνει σε ένα ... που δεν επιλυόταν ποτέ.
Θα μπορούσες να το διορθώσεις αυτό βάζοντας τη σιωπή πίσω — αλλά τότε έχεις αναιρέσει τη βελτιστοποίηση. Η πραγματική διόρθωση είναι καλύτερη: στείλε το νόημα της σιωπής χωρίς να στείλεις τη σιωπή. Ο client τώρα παρακολουθεί πόση ώρα ο ήχος ήταν σιωπηλός τοπικά, και όταν αυτό ξεπεράσει το SILENCE_COMMIT_THRESHOLD_MS = 1000 — σκόπιμα ταιριασμένο με το ίδιο το κατώφλι σιωπής του VAD του backend — πυροδοτεί ένα ρητό σήμα commit εκτός ζώνης: το sendMixedChunk παρατηρεί τη συνεχή σιωπή, στέλνει ένα realtime-audio:commit μέσω IPC, η main process προωθεί ένα { type: 'commit' } στο socket, και το backend λέει στο ElevenLabs να οριστικοποιήσει το partial. Δύο μικρά κομμάτια κατάστασης, το silenceStartedAt και το silenceCommitFired, το εμποδίζουν να πυροδοτείται επανειλημμένα. Η νίκη του εύρους ζώνης επιβιώνει· η οριστικοποίηση δουλεύει ξανά· το ... στο τέλος επιλύεται.
Υπάρχει και μια μικρή, έντιμη υποσημείωση σε αυτή την έκδοση: η απεριόριστη λογική επανασύνδεσης που εισήγαγε η v1.9.0 χτίστηκε πρώτα στο renderer ως ένα WsReconnectController, μετά διαγράφηκε μέσα στην ίδια έκδοση μόλις η πραγματική υλοποίηση προσγειώθηκε στον handler της main-process όπου ανήκε. Η ίδια η ιστορία της επανασύνδεσης — απεριόριστο backoff, εναλλαγή παρόχων κάθε πέντε αποτυχίες — λέγεται σε δικό της κεφάλαιο· αυτό που προσθέτει η v1.9.0 είναι ότι εκεί γεννήθηκε εκείνη η αρχιτεκτονική, και όπου ο client πήρε για πρώτη φορά την κυριότητά της.
Τρία πράγματα που μας έμαθαν αυτές οι εκδόσεις
- Όταν δεν μπορείς να αποτρέψεις την απώλεια, κάνε την ανακτήσιμη και κάνε την ορατή. Η v1.8.20 δεν σταματά τις επανασυνδέσεις από το να πετούν ήχο — αυτό δεν είναι φυσικά δυνατό σε πραγματικό χρόνο. Κρατά ένα αντίγραφο του ακατέργαστου ήχου για αργότερα, και επισημαίνει το κενό στη μεταγραφή. Κανένα από τα δύο δεν είναι εντυπωσιακό· μαζί μετατρέπουν τη σιωπηλή απώλεια δεδομένων σε μια γνωστή, οριοθετημένη, ανακτήσιμη κατάσταση.
- Ένα δίχτυ ασφαλείας χρειάζεται τα δικά του όρια. Το ανθεκτικό tee θα γέμιζε σιωπηλά τον δίσκο αν το άφηνες μόνο του — μια διόρθωση που γίνεται ένα bug σε αργή κίνηση. Το κλάδεμα των 7 ημερών που κυκλοφόρησε στην ίδια έκδοση δεν είναι μια εκ των υστέρων σκέψη· είναι αυτό που κάνει το tee ασφαλές για κυκλοφορία εξαρχής. Αν προσθέσεις έναν μηχανισμό που συσσωρεύει, πρόσθεσε στην ίδια ανάσα τον μηχανισμό που τον κλαδεύει.
- Αυτό που πετάς για να είσαι αποδοτικός μπορεί να είναι ένα σήμα. Το πιο κοφτερό μεμονωμένο μάθημα σε οποιαδήποτε από τις δύο εκδόσεις: το φιλτράρισμα της σιωπής ήταν σωστό σε κάθε άξονα που θα έλεγχες κανονικά — εύρος ζώνης, φορτίο, συμμόρφωση με το συμβόλαιο — και παρ όλα αυτά έσπασε το προϊόν, επειδή ένα κατάντη σύστημα διάβαζε νόημα μέσα στη σιωπή. Όταν αφαιρείς δεδομένα για αποδοτικότητα, ρώτα τι συμπέραινε κάτι από εκείνα τα δεδομένα. Μερικές φορές πρέπει να στείλεις το νόημα ρητά ακριβώς επειδή σταμάτησες να στέλνεις το πράγμα από το οποίο συμπεραινόταν.
Και αυτό είναι το τέλος της ιστορίας της έκδοσης 1 — η τελευταία σκλήρυνση πριν την επανεγγραφή. Για το προηγούμενο κεφάλαιο, τα τρία ρήματα που κρατούν το Web Audio ζωντανό (v1.8.16 + v2.0.2)· για το πού κατευθυνόταν όλο αυτό, τι χρειάζεται πραγματικά μια έκδοση 2: 206 commits (v2.0.0)· και για ολόκληρη την πορεία, η ανατομία της παράδοσης λογισμικού στην τελειότητα.