
เราลบโค้ดเสียงทิ้ง 5,000 บรรทัด — แล้วบันทึกการถอดเสียงก็เริ่มโผล่ขึ้นมาสองครั้ง
GeekBye v1.6 ถอนตัวถอดเสียงบนเครื่องที่เขียนด้วย Swift สองตัวออก แล้วแทนที่ด้วยไปป์ไลน์เดียวที่รวมเป็นหนึ่ง — ลบสุทธิกว่า 5,000 บรรทัด ทำไปในขณะที่ผู้คนกำลังใช้แอปอยู่กลางการประชุม จากนั้นบันทึกการถอดเสียงก็เริ่มปรากฏขึ้นสองครั้ง เพราะบั๊กย้ายไปยังชั้นเดียวที่ยังคิดว่ามีเอนจินอยู่สองตัว
มีความหวาดกลัวชนิดหนึ่งโดยเฉพาะในการรีแฟกเตอร์โค้ดที่กำลังทำงานอยู่ ตอนนี้ เพื่อคนที่กำลังอยู่ กลางการประชุม คุณไม่สามารถปิดฟีเจอร์เพื่อบำรุงรักษาได้ — มีคนกำลังพึ่งพามันเพื่อถอดเสียงบทสนทนาที่พวกเขาย้อนกลับมาทำใหม่ไม่ได้ GeekBye v1.6 ทำแบบนั้นเป๊ะ ๆ กับเส้นทางที่สำคัญที่สุดในแอป: ไปป์ไลน์เสียง นี่คือเรื่องราวของสิ่งที่เราลบ สิ่งที่พัง และบทเรียนด้านความน่าเชื่อถือสองข้อที่ออกมาจากมัน
สองเอนจิน หนึ่งในนั้นเขียนด้วย Swift
ก่อน v1.6 การแปลงเสียงเป็นข้อความของ GeekBye รันบนเครื่องใน Swift binaries มีอยู่สองตัว: ตัวหนึ่งห่อหุ้มเฟรมเวิร์ก Speech ของ Apple และอีกตัวที่ใหญ่กว่า (กว่า 1,500 บรรทัด) ที่จับเสียงคู่ — ไมค์ของคุณบวกกับเสียงระบบ — ผ่าน ScreenCaptureKit และเปิด WebSocket ของตัวเอง ตรงไปยัง backend ของเราเพื่อสตรีมไปยัง Deepgram ตัวถอดเสียงสองตัว เส้นทางจับเสียงสองเส้นทาง สองวิธีในการคุยกับเครือข่าย ทั้งหมดอยู่ในภาษาที่คอมไพล์แล้ว ซึ่งหมายความว่าทุกการเปลี่ยนแปลงต้องคอมไพล์ไบนารีใหม่
v1.6.0 ยุบทั้งหมดนั้นให้เหลือไปป์ไลน์เดียว คอมมิตเดียวที่รวมทุกอย่างเข้าด้วยกันได้ลบตัวถอดเสียง Apple Speech (~1,145 บรรทัด), ตัวจับเสียง Deepgram ที่เขียนด้วย Swift (~1,523 บรรทัด), บริดจ์ TypeScript ทั้งสอง, ตัวจัดการเดิม และ dual-audio IPC handlers — ลบสุทธิกว่า 5,200 บรรทัด แทนที่ด้วย: renderer จับเสียงผ่าน Web APIs มาตรฐาน ส่งเป็น PCM ผ่าน IPC และ Electron main process เป็นเจ้าของเส้นทาง WebSocket เดียวไปยัง backend โดยเลือกผู้ให้บริการจากคอนฟิกฝั่ง backend เส้นทางจับเสียงเดียว เจ้าของซ็อกเก็ตเดียว จุดเดียวที่ต้องคิดถึงมัน
การลบโค้ดห้าพันบรรทัดรู้สึกดี อยู่ประมาณหนึ่งวัน
บั๊กย้ายไปยังชั้นที่ยังคิดว่ามีอยู่สองตัว
จากนั้นบันทึกการถอดเสียงก็เริ่มโผล่ขึ้นมา สองครั้ง ประโยคที่พูดออกมาประโยคเดียวจะถูกบันทึกเป็นสองรายการที่เหมือนกันเป๊ะ — บางครั้งก็แย่กว่านั้น
ต้นตอของปัญหาคือสิ่งที่ให้บทเรียนมากที่สุดในการปล่อยเวอร์ชันครั้งนี้ทั้งหมด เพราะมันคือกฎทั่วไปของการรวมเป็นหนึ่ง เราได้รวมผู้ให้บริการเข้าด้วยกัน ใน main process — ซ็อกเก็ตเดียว ตัวจัดการเดียว แต่ชั้น React ยังไม่ได้รับข่าว: มันยังคงติดตั้ง event hook เฉพาะผู้ให้บริการสองตัวพร้อมกัน ตัวหนึ่งสร้างมาเพื่อ Deepgram และอีกตัวสำหรับ ElevenLabs ทั้งคู่กำลังฟังอยู่ ทั้งคู่กำลังบันทึกอยู่ ทุกบันทึกการถอดเสียงถูกจัดเก็บโดย hook ตัวใดก็ตามที่ยังมีชีวิต — และทั้งคู่ก็ยังมีชีวิต
และยังมีการซ้ำที่แนบเนียนกว่าซ่อนอยู่ข้างใต้: ElevenLabs ปล่อยทุกบรรทัดที่สรุปเสร็จออกมาสองครั้ง — ครั้งหนึ่งเป็นเหตุการณ์ committed และอีกครั้งเป็น committed_with_timestamps ดังนั้นแม้จะมี hook เดียว ประโยคเดียวก็อาจมาถึงเป็นสองเหตุการณ์ได้
การแก้ไข (ในสาย v1.6) เป็นสองส่วนที่เรียบร้อยและจับคู่พอดีกับสาเหตุทั้งสอง: กั้นแต่ละ event hook ไว้หลังธง enabled ที่ขับด้วยคอนฟิกผู้ให้บริการที่กำลังทำงานของ backend เพื่อให้มี listener เพียงตัวเดียวที่มีชีวิตในแต่ละครั้ง และเพิ่มการตัดข้อความซ้ำแยกตามแหล่ง (จำบรรทัดสุดท้ายที่บันทึกไว้สำหรับไมค์และสำหรับเสียงระบบ) เพื่อกลืนการปล่อยซ้ำของ ElevenLabs
บทเรียนนั้นใหญ่กว่าบั๊กนี้: เมื่อคุณรวมสอง backend ไว้หลังหนึ่งอินเทอร์เฟซ การซ้ำไม่ได้หายไป — มันย้ายไปยังชั้นที่ยังสมมติว่ามีอยู่สองตัว เรายุบท่อร้อยสายใน main process แล้วรอยรั่วก็โผล่ขึ้นมาสองชั้นเหนือขึ้นไป ในการเดินสายเหตุการณ์ของ UI การรวมไปป์ไลน์ยังไม่เสร็จเมื่อผู้ผลิตถูกรวมเป็นหนึ่ง มันเสร็จเมื่อ ผู้บริโภค ทุกตัวหยุดเชื่อในรูปทรงเดิมแล้ว
การเชื่อมต่อใหม่ครั้งแรก: "ยอมแพ้หลังลองห้าครั้ง"
v1.6.0 ยังส่งมอบการเชื่อมต่อ WebSocket อัตโนมัติครั้งแรกสุดของ GeekBye ด้วย exponential backoff — และมันคุ้มค่าที่จะนำมาแสดงเพราะมันคือ บรรพบุรุษดั้งเดิม ของเวอร์ชันกันกระสุนที่เรารันอยู่ทุกวันนี้
ตัวดั้งเดิมมีขีดจำกัด ห้าครั้ง ดีเลย์ 1s, 2s, 4s, 8s, 16s — แล้วมันก็ ยอมแพ้ ปล่อยข้อผิดพลาดร้ายแรงออกมา: "Connection lost — please restart transcription." ในตอนนั้นมันรู้สึกว่ามีความรับผิดชอบ: อย่าลองซ้ำตลอดกาล บอกผู้ใช้อย่างตรงไปตรงมา ในทางปฏิบัติมันคือบั๊ก UX ที่ปลอมตัวมา ช่วงเน็ตสะดุดสิบหกวินาที — การสลับ Wi-Fi, การเชื่อมต่อ VPN ใหม่, อุโมงค์ — เป็นเรื่อง ชั่วคราว แต่การลองซ้ำแบบมีขีดจำกัดมองมันเป็นเรื่อง ถาวร ผู้ใช้ไม่ได้ทำอะไรผิดเลยแต่กลับถูกบอกให้เริ่มการประชุมที่กำลังดำเนินอยู่ใหม่
นั่นคือความล้มเหลวเป๊ะ ๆ ที่การออกแบบปัจจุบันกำจัดทิ้งไป ทุกวันนี้ GeekBye เชื่อมต่อใหม่ด้วย backoff แบบ ไม่มีขีดจำกัด และยังหมุนสลับผู้ให้บริการ และจะหยุดก็ต่อเมื่อมีเหตุผลร้ายแรงจริง ๆ เท่านั้น — auth, quota, billing เส้นทางทั้งหมดจาก "ยอมแพ้หลังห้าครั้ง" ไปสู่ "ไม่มีวันยอมแพ้กับข้อผิดพลาดชั่วคราว" ถูกเล่าไว้ในทำไมแอปจดบันทึก AI ของคุณถึงหยุดเมื่อ Wi-Fi แย่ v1.6 คือจุดที่ถนนสายนั้นเริ่มต้น ด้วยเวอร์ชันแบบเรียบง่ายที่ต้องมีอยู่ก่อน
การเชื่อมต่อที่เก่าค้างจะหลอกหลอนคุณ เว้นแต่มันจะมีชื่อ
บทเรียนด้านความน่าเชื่อถืออีกหนึ่งข้อลงจอดใน v1.6.3 แก้บั๊กประเภทที่แนบเนียนที่ใครก็ตามที่ทำการเชื่อมต่อใหม่บน backend ที่มีสถานะจะได้พบสักวันหนึ่ง
เมื่อคุณเชื่อมต่อใหม่ — หรือสลับภาษาการถอดเสียงกลางเซสชัน ซึ่งเชื่อมต่อใหม่อยู่เบื้องหลัง — การเชื่อมต่อ เก่า ไม่ได้ตายอย่างเงียบ ๆ เสมอไป ข้อความลมหายใจเฮือกสุดท้ายของมัน (CONNECTION_LOST, disconnected) อาจมาถึง หลังจาก ที่การเชื่อมต่อใหม่แข็งแรงแล้ว และรื้อตัวแทนที่ดีสมบูรณ์ทิ้ง การแก้ไขให้ทุกความพยายามเชื่อมต่อมี อัตลักษณ์ — session ID ต่อการเชื่อมต่อที่ประทับไว้บน WebSocket URL — บวกกับ ช่วงเวลาผ่อนผัน สั้น ๆ หลังเชื่อมต่อใหม่ ที่ในระหว่างนั้นข้อความตัดการเชื่อมต่อที่เก่าค้างจากความพยายามก่อนหน้าจะถูกเพิกเฉย มันยังส่งข้อความ stop อย่างชัดแจ้งก่อนปิดซ็อกเก็ต เพื่อให้ backend รื้อเซสชันเก่าลงได้อย่างสะอาด
หลักการ: ตรรกะการเชื่อมต่อใหม่ใด ๆ บน backend ที่มีสถานะต้องติดป้ายอัตลักษณ์ให้แต่ละความพยายาม และมองข้อความที่มาช้าจากความพยายามก่อนหน้าเป็นสัญญาณรบกวน — มิฉะนั้นการเชื่อมต่อใหม่ที่สำเร็จจะถูกฆ่าด้วยลมหายใจเฮือกสุดท้ายของรุ่นก่อน
สามสิ่งที่ v1.6 สอน
- การรวมเป็นหนึ่งย้ายที่บั๊ก มันไม่ได้ลบบั๊กทิ้ง ยุบสองเอนจินให้เป็นหนึ่ง แล้วตรวจสอบผู้บริโภคทุกตัวที่ยังคิดว่ามีอยู่สองตัว ของเราคือ UI ที่ติดตั้ง event hook สองตัวหลังจากที่ท่อร้อยสายได้รวมเข้าด้วยกันแล้ว
- การเชื่อมต่อใหม่ครั้งแรกของคุณจะมีขีดจำกัด และการมีขีดจำกัดนั้นผิดสำหรับสตรีมสด "ยอมแพ้หลังลอง N ครั้ง" เปลี่ยนช่วงเน็ตสะดุดชั่วคราวให้กลายเป็นความล้มเหลวถาวรที่ผู้ใช้ถูกกล่าวโทษ จงแยกความล้มเหลวร้ายแรง (auth, quota) ออกจากชั่วคราว (ซ็อกเก็ตหลุด) ตั้งแต่เวอร์ชันแรกสุด
- การเชื่อมต่อใหม่ต้องการอัตลักษณ์ ติดป้ายให้แต่ละความพยายาม เพิกเฉยต่อข้อความที่มาช้าจากความพยายามที่ตายไปแล้ว การเชื่อมต่อที่ไม่มีชื่ออาจถูกฆาตกรรมด้วยผีของตัวเอง
นี่คือบทที่สองของเรื่องราวด้านความน่าเชื่อถือที่กลายมาเป็น GeekBye v2 สำหรับบทแรก ดูมหากาพย์การอัปเดตอัตโนมัติที่กินหกการปล่อยเวอร์ชันในสี่วัน (v1.5.x); สำหรับจุดที่งานเชื่อมต่อใหม่ไปถึงในที่สุด ทำไมแอปจดบันทึก AI ของคุณถึงหยุดเมื่อ Wi-Fi แย่; และสำหรับเส้นเรื่องทั้งหมด กายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ