
Chúng tôi xóa 5.000 dòng mã âm thanh — và bản ghi lời bắt đầu hiện lên hai lần
GeekBye v1.6 gỡ bỏ hai bộ phiên âm Swift chạy trên thiết bị và thay bằng một pipeline hợp nhất — xóa ròng hơn 5.000 dòng, thực hiện ngay khi người dùng đang giữa cuộc họp trên ứng dụng. Rồi bản ghi lời bắt đầu hiện lên hai lần, bởi lỗi đã di chuyển tới lớp duy nhất vẫn còn tưởng rằng có hai engine.
Có một nỗi kinh hoàng rất riêng khi tái cấu trúc phần mã đang chạy ngay lúc này, cho những người đang giữa cuộc họp. Bạn không thể tắt tính năng để bảo trì — có ai đó đang dựa vào nó để ghi lại một cuộc trò chuyện họ không thể lặp lại. GeekBye v1.6 đã làm đúng điều đó với đường đi quan trọng nhất của ứng dụng: pipeline âm thanh. Đây là câu chuyện về những gì chúng tôi đã xóa, những gì đã hỏng, và hai bài học về độ tin cậy rút ra từ đó.
Hai engine, một trong số đó viết bằng Swift
Trước v1.6, phần chuyển giọng nói thành văn bản của GeekBye chạy trên thiết bị bằng các binary Swift. Có hai binary: một bọc quanh framework Speech của Apple, và một cái lớn hơn (hơn 1,500 dòng) thu âm thanh kép — micro của bạn cộng với âm thanh hệ thống — qua ScreenCaptureKit và mở WebSocket của riêng nó thẳng tới backend của chúng tôi để truyền tới Deepgram. Hai bộ phiên âm, hai đường thu âm, hai cách nói chuyện với mạng, tất cả trong một ngôn ngữ biên dịch nghĩa là mỗi thay đổi đều đòi hỏi biên dịch lại một binary.
v1.6.0 gộp tất cả vào một pipeline. Commit hợp nhất duy nhất đã xóa bộ phiên âm Apple Speech (~1,145 dòng), bộ thu Deepgram Swift (~1,523 dòng), cả hai bridge TypeScript, orchestrator cũ, và các IPC handler âm thanh kép — xóa ròng hơn 5,200 dòng. Thay vào chỗ của chúng: renderer thu âm thanh qua các Web APIs tiêu chuẩn, gửi nó dưới dạng PCM qua IPC, và tiến trình chính Electron sở hữu một đường WebSocket duy nhất tới backend, với nhà cung cấp được chọn bởi cấu hình backend. Một đường thu âm, một chủ sở hữu socket, một nơi để suy luận.
Xóa năm nghìn dòng cảm giác thật tuyệt. Trong khoảng một ngày.
Lỗi đã di chuyển tới lớp vẫn còn tưởng có hai
Rồi bản ghi lời bắt đầu hiện lên hai lần. Một câu nói duy nhất sẽ được lưu thành hai mục giống hệt nhau — đôi khi còn tệ hơn.
Nguyên nhân gốc là điều đáng học nhất trong toàn bộ bản phát hành này, bởi nó là một quy luật chung của sự hợp nhất. Chúng tôi đã hợp nhất các nhà cung cấp trong tiến trình chính — một socket, một handler. Nhưng lớp React chưa nhận được thông báo: nó vẫn gắn hai event hook riêng cho từng nhà cung cấp cùng lúc, một cái dựng cho Deepgram và một cái cho ElevenLabs. Cả hai đều đang lắng nghe. Cả hai đều đang lưu. Mỗi bản ghi lời đều được lưu bởi bất kỳ hook nào còn sống — và cả hai đều còn sống.
Và còn có một sự nhân đôi thứ hai, ranh mãnh hơn, nằm bên dưới: ElevenLabs phát mỗi dòng đã chốt cuối hai lần — một lần dưới dạng sự kiện committed và lại một lần dưới dạng committed_with_timestamps. Vậy nên ngay cả với một hook duy nhất, một câu vẫn có thể đến dưới dạng hai sự kiện.
Bản sửa (trong nhánh v1.6) là một cặp gọn gàng ánh xạ chính xác lên hai nguyên nhân: chặn mỗi event hook sau một cờ enabled được điều khiển bởi cấu hình nhà-cung-cấp-đang-hoạt-động của backend, để chỉ một listener sống tại một thời điểm; và thêm khử trùng lặp văn bản theo từng nguồn (nhớ dòng đã lưu cuối cùng cho micro và cho âm thanh hệ thống) để nuốt cú phát-đôi của ElevenLabs.
Bài học lớn hơn lỗi này: khi bạn hợp nhất hai backend sau một giao diện, sự trùng lặp không biến mất — nó di cư tới lớp vẫn còn giả định rằng có hai. Chúng tôi gộp phần đường ống trong tiến trình chính và chỗ rò rỉ nổi lên hai lớp phía trên, trong cách nối dây sự kiện của giao diện. Hợp nhất một pipeline chưa xong khi producer đã được hợp nhất; nó xong khi mọi consumer đã thôi tin vào hình dạng cũ.
Lần kết nối lại đầu tiên: "bỏ cuộc sau năm lần thử"
v1.6.0 cũng ra mắt lần tự động kết nối lại WebSocket đầu tiên của GeekBye với backoff lũy thừa — và nó đáng để trưng ra chính vì nó là tổ tiên nguyên thủy của phiên bản chống đạn mà chúng tôi chạy ngày nay.
Bản gốc có giới hạn. Năm lần thử, độ trễ 1s, 2s, 4s, 8s, 16s — và rồi nó đầu hàng, phát ra một lỗi chí tử: "Mất kết nối — vui lòng khởi động lại phiên âm." Vào thời điểm đó điều này cảm thấy có trách nhiệm: đừng thử lại mãi mãi, hãy nói thật với người dùng. Trên thực tế nó là một lỗi UX trá hình. Một cú chớp mạng mười sáu giây — một lần chuyển giao Wi-Fi, một lần VPN kết nối lại, một đường hầm — là tạm thời, nhưng một retry có giới hạn đối xử với nó như chí tử. Người dùng chẳng làm gì sai mà bị bảo khởi động lại một cuộc họp đang diễn ra.
Đó chính là thất bại mà thiết kế hiện tại đã loại bỏ. Ngày nay GeekBye kết nối lại với backoff không giới hạn và thậm chí luân phiên các nhà cung cấp, và chỉ dừng vì một lý do thực sự chí tử — xác thực, hạn ngạch, thanh toán. Toàn bộ hành trình từ "bỏ cuộc sau năm lần" đến "không bao giờ bỏ cuộc trước một lỗi tạm thời" được kể trong tại sao trình ghi chú AI của bạn dừng khi Wi-Fi kém. v1.6 là nơi con đường ấy khởi đầu, với phiên bản ngây thơ buộc phải tồn tại trước.
Những kết nối cũ ám bạn trừ khi chúng có tên
Thêm một bài học về độ tin cậy nữa xuất hiện trong v1.6.3, sửa một loại lỗi tinh vi mà bất kỳ ai làm việc kết nối lại qua một backend có trạng thái rồi cũng sẽ gặp.
Khi bạn kết nối lại — hoặc chuyển ngôn ngữ phiên âm giữa phiên, việc mà ngầm bên dưới cũng kết nối lại — kết nối cũ không phải lúc nào cũng chết lặng lẽ. Những thông điệp hấp hối của nó (CONNECTION_LOST, disconnected) có thể đến sau khi kết nối mới đã khỏe mạnh, và phá hủy bản thay thế hoàn toàn tốt đẹp. Bản sửa trao cho mỗi lần thử kết nối một danh tính — một session ID riêng cho từng kết nối được đóng dấu lên URL của WebSocket — cộng với một khoảng ân hạn ngắn sau khi kết nối lại, trong đó những thông điệp ngắt kết nối cũ từ một lần thử trước bị bỏ qua. Nó cũng gửi một thông điệp stop tường minh trước khi đóng một socket để backend có thể dọn dẹp phiên cũ một cách gọn gàng.
Nguyên tắc: bất kỳ logic kết nối lại nào qua một backend có trạng thái đều cần gắn thẻ mỗi lần thử với một danh tính và coi các thông điệp đến muộn từ những lần thử trước là nhiễu — nếu không một lần kết nối lại thành công sẽ bị giết bởi hơi thở hấp hối của kẻ tiền nhiệm.
Ba điều v1.6 dạy chúng tôi
- Sự hợp nhất di dời lỗi; nó không xóa lỗi. Gộp hai engine thành một và soi lại mọi consumer vẫn còn tưởng có hai. Của chúng tôi là giao diện, gắn hai event hook sau khi đường ống đã hợp nhất.
- Lần kết nối lại đầu tiên của bạn sẽ có giới hạn, và có giới hạn là sai với các luồng trực tiếp. "Bỏ cuộc sau N lần thử" biến một cú chớp tạm thời thành một thất bại chí tử mà người dùng bị đổ lỗi. Phân biệt chí tử (xác thực, hạn ngạch) với tạm thời (một socket bị rớt) ngay từ phiên bản đầu tiên.
- Kết nối lại cần danh tính. Gắn thẻ mỗi lần thử; bỏ qua các thông điệp đến muộn từ những lần thử đã chết. Một kết nối không có tên có thể bị chính bóng ma của mình sát hại.
Đây là chương thứ hai của câu chuyện độ tin cậy trở thành GeekBye v2. Về chương đầu tiên, xem câu chuyện tự-động-cập-nhật ngốn sáu bản phát hành trong bốn ngày (v1.5.x); về nơi công việc kết nối lại cuối cùng cập bến, tại sao trình ghi chú AI của bạn dừng khi Wi-Fi kém; và về toàn bộ vòng cung, giải phẫu của việc ship phần mềm tới mức hoàn hảo.