
Sự Im Lặng Vốn Chịu Tải
Hai bản phát hành cuối cùng của GeekBye v1 đều xoay quanh cùng một sự thật khó chịu: phiên âm thời gian thực trên một mạng thật không phải là không mất mát, và nước đi trung thực là thôi giả vờ rằng nó như vậy. v1.8.20 giữ một bản sao của mỗi audio chunk trên đĩa trước khi bỏ nó trong một lần kết nối lại, và bắt đầu đánh dấu các khoảng trống trong transcript ra thành tiếng. v1.9.0 thôi gửi im lặng để tiết kiệm bandwidth — rồi phát hiện ra im lặng chính là tín hiệu chính xác mà bộ phiên âm dùng để biết một câu đã kết thúc. Hai bản phát hành về cái giá của việc vứt đồ đi.
Hai bản phát hành cuối cùng của GeekBye v1 — v1.8.20 và v1.9.0, những bản ngay trước cột mốc phiên bản 2 — chia sẻ một chủ đề chỉ trở nên rõ ràng khi bạn đọc chúng cùng nhau. Cả hai đều về những thứ mà app đang lặng lẽ vứt đi, và cả hai đều đi tới cùng một kết luận: thứ bạn bỏ đi để hiệu quả đang làm nhiều việc hơn bạn tưởng. Một bản phát hành đang bỏ audio trong lúc kết nối lại mạng. Bản kia đang bỏ im lặng để tiết kiệm bandwidth. Trong cả hai trường hợp, thứ bị bỏ đi hóa ra lại chịu tải.
Phiên âm thời gian thực không phải là không mất mát, và v1.8.20 thừa nhận điều đó
Có một sự thật về việc làm speech-to-text theo kiểu streaming qua một WebSocket mà người ta dễ ngoảnh mặt đi: khi kết nối rớt rồi nối lại — hoặc khi app failover từ một nhà cung cấp phiên âm này sang một nhà khác, từ Deepgram sang ElevenLabs — client cố ý bỏ các audio chunk bắt được trong cái khoảng trống đó. Nó buộc phải vậy. Một nhà cung cấp vừa mới kết nối mà đột nhiên nhận cả một đống audio cũ tồn đọng sẽ nghẹn, hoặc tệ hơn, khớp cái bộ dò voice-activity của nó vào nhầm thứ. Nên client vứt các chunk đó đi.
Vấn đề không phải là chuyện bỏ. Vấn đề là chuyện bỏ trước đây có nghĩa là gì: những lời đó đơn giản là chưa từng xuất hiện trong transcript, và chẳng có gì bảo bạn rằng chúng thiếu. Bản ghi trông có vẻ đầy đủ. Nó không đầy đủ.
v1.8.20 tấn công điều đó trên hai mặt trận, và chẳng mặt nào là «làm cho reconnect không bao giờ mất audio» — vì bạn không thể, không thể trong thời gian thực. Thay vào đó, nó làm cho sự mất mát trở nên phục hồi được và trung thực.
Nửa phục hồi được là một PCM tee bền bỉ. Trong capture handler ở main process, mỗi chunk bắt được giờ được ghi vào một file trên đĩa trước khi nó chạm tới các cổng bỏ của reconnect và của khoảng ân hạn:
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
Cơ chế thì mộc mạc một cách dễ chịu: teeChunkToDisk mở một WriteStream, đặt trước một header WAV 44 byte, nối thêm các mẫu mono 16-bit thô ngay khi chúng tới, và vá header với các kích thước cuối cùng lúc dừng (fs.openSync(..., 'r+'), ghi 44 byte ở offset 0). Kết quả là một file WAV chơi được của toàn bộ audio — bao gồm cả những phần mà transcript thời gian thực chưa từng thấy. Khi bản ghi kết thúc, nó phát ra một event session-audio-saved kèm đường dẫn file và số byte.
Hai ghi chú trung thực quan trọng ở đây, vì rất dễ tuyên bố quá lời về chuyện này. Thứ nhất: tee không lấp cái khoảng trống trong các bản phát hành này. Nó giữ lại audio thô cho một lượt đối soát về sau — một lần phiên âm lại theo kiểu batch để đối soát file với transcript trực tiếp — vốn là một cái móc mà các bản phát hành này đặt xuống, không phải một tính năng chúng xuất xưởng. Commit tiếp theo thậm chí chú thích session-audio-saved bằng một TODO trỏ tới các issue đối soát để không ai nhầm nó thành code chết. Thứ hai: chuyện này tốn đĩa. Nhóm ước tính cỡ 86 MB mỗi bản ghi 30 phút, nên cùng bản phát hành ấy thêm một StartupCleanupService để tỉa các session đã lưu cũ hơn 7 ngày. Một tấm lưới an toàn phình ra không giới hạn là một chỗ rò; chính cái dọn dẹp đó là thứ làm cho tee xuất xưởng được.
Nửa trung thực còn đơn giản hơn nữa. Ở lần thử reconnect đầu tiên, client ghi một entry thật, nhìn thấy được, vào transcript:
[reconnecting — audio may be missing]
Chỉ một dòng đó là toàn bộ triết lý của bản phát hành thu nhỏ lại. App không thể bảo đảm rằng nó đã nghe hết mọi thứ, nên nó thôi ám chỉ rằng nó đã nghe. Một khoảng trống bạn thấy được là một khoảng trống bạn có thể lách qua; một khoảng trống bạn không thấy được là một báo cáo bug đang chờ xảy ra.
v1.9.0: sự im lặng bạn tối ưu cho biến mất chính là tín hiệu
v1.9.0 là bản phát hành v1 cuối cùng, và nó là nơi backend trao xuống cho client một loạt trách nhiệm về live-transcript — bỏ im lặng, quy mỗi dòng transcript về mic hay system audio từ một dòng thời gian voice-activity cục bộ, làm chủ vòng lặp reconnect, thực thi các mức trần ghi. Phần lớn trong đó là nền móng cho kiến trúc phiên bản 2 và đã được kể ở chỗ khác. Nhưng một mạch trong nó là câu chuyện nhỏ hay nhất trong cả hai bản phát hành, vì nó là một bản sửa phải hoàn tác thiệt hại của một tối ưu hoàn toàn đúng khi đứng một mình.
Nó bắt đầu bằng một thay đổi hiển nhiên là tốt: thôi gửi im lặng. Client bắt đầu lọc bỏ các chunk có nguồn chủ đạo là im lặng trước khi đẩy chúng qua socket — ít bandwidth hơn, ít audio vô dụng hơn để backend xử lý, và nó tôn trọng một client-contract tường minh của backend nói rằng «đừng gửi im lặng». Mọi lý do để làm nó đều vững.
Và nó làm hỏng hoàn toàn việc kết thúc câu nói. Cái commit sửa nó bày ra chuỗi nhân quả với một sự thẳng thắn khác thường:
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.
Đây là dạng lý tưởng của một abstraction rò rỉ cắn ngược lại. Bộ dò voice-activity của bộ phiên âm không có một đầu vào riêng biệt kiểu «người nói đã xong» — nó suy ra điểm cuối của một câu từ việc nghe thấy khoảng một giây im lặng trong audio. Im lặng không phải là nhiễu trên dây; nó là một thông điệp. Bằng cách lọc nó ra để tiết kiệm bandwidth, client vô tình thôi gửi cái tín hiệu duy nhất báo cho bộ phiên âm rằng một câu đã kết thúc. Nên mọi partial transcript cứ thế lớn lên mãi, cái đuôi lê thê thành một ... không bao giờ giải quyết.
Bạn có thể sửa chuyện này bằng cách đặt im lặng trở lại — nhưng thế thì bạn đã hoàn tác mất tối ưu. Bản sửa thật sự thì hay hơn: gửi ý nghĩa của im lặng mà không gửi im lặng. Client giờ theo dõi audio đã im lặng bao lâu ở cục bộ, và khi điều đó vượt SILENCE_COMMIT_THRESHOLD_MS = 1000 — được khớp một cách có chủ ý với chính ngưỡng im lặng VAD của backend — nó bắn một tín hiệu commit tường minh ra ngoài băng tần: sendMixedChunk để ý thấy sự im lặng kéo dài, gửi một realtime-audio:commit qua IPC, main process chuyển tiếp một { type: 'commit' } trên socket, và backend bảo ElevenLabs kết thúc cái partial. Hai mẩu trạng thái nhỏ, silenceStartedAt và silenceCommitFired, giữ cho nó không bắn lặp đi lặp lại. Thắng lợi về bandwidth sống sót; việc kết thúc lại chạy được; cái ... ở đuôi được giải quyết.
Bản phát hành này cũng có một chú thích nhỏ, thành thật: cái logic reconnect vô hạn mà v1.9.0 giới thiệu ban đầu được dựng trong renderer dưới dạng một WsReconnectController, rồi bị xóa ngay trong cùng bản phát hành một khi phần hiện thực thật sự đáp xuống handler ở main process nơi nó thuộc về. Câu chuyện reconnect tự thân — backoff vô hạn, luân phiên nhà cung cấp mỗi năm lần thất bại — được kể trong chương riêng của nó; điều v1.9.0 thêm vào là đây là nơi kiến trúc đó ra đời, và là nơi client lần đầu nhận làm chủ nó.
Ba điều những bản phát hành này đã dạy
- Khi bạn không thể ngăn mất mát, hãy làm cho nó phục hồi được và làm cho nó nhìn thấy được. v1.8.20 không ngăn reconnect khỏi bỏ audio — điều đó bất khả về mặt vật lý trong thời gian thực. Nó giữ một bản sao của audio thô để dùng về sau, và nó dán nhãn cho khoảng trống trong transcript. Chẳng cái nào hào nhoáng; cùng nhau, chúng biến sự mất dữ liệu âm thầm thành một tình huống đã biết, có giới hạn, phục hồi được.
- Một tấm lưới an toàn cần có ranh giới của riêng nó. Cái tee bền bỉ sẽ lặng lẽ làm đầy đĩa nếu bị bỏ mặc — một bản sửa trở thành một con bug quay chậm. Cái dọn dẹp 7 ngày xuất xưởng trong cùng bản phát hành không phải một suy nghĩ thêm vào sau; nó là thứ khiến cho tee an toàn để xuất xưởng ngay từ đầu. Nếu bạn thêm một cơ chế tích lũy, hãy thêm cơ chế tỉa nó trong cùng một hơi thở.
- Thứ bạn bỏ đi để hiệu quả có thể là một tín hiệu. Bài học sắc bén nhất trong cả hai bản phát hành: lọc bỏ im lặng là đúng trên mọi trục bạn thường kiểm — bandwidth, tải, tuân thủ hợp đồng — vậy mà nó vẫn làm hỏng sản phẩm, vì một hệ thống hạ nguồn đang đọc ý nghĩa vào trong sự im lặng ấy. Khi bạn gỡ dữ liệu đi để hiệu quả, hãy hỏi xem cái gì đang suy ra điều gì đó từ dữ liệu ấy. Đôi khi bạn phải gửi ý nghĩa một cách tường minh chính bởi vì bạn đã thôi gửi cái thứ mà nó vốn được suy ra từ đó.
Và đó là kết thúc của câu chuyện phiên bản 1 — lần gia cố cuối cùng trước khi viết lại. Về chương trước, xem ba động từ giữ cho web audio sống (v1.8.16 + v2.0.2); về nơi tất cả những điều này hướng tới, xem một phiên bản 2 thực sự đòi hỏi những gì: 206 commit (v2.0.0); và về toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.