Steven
Steven3 นาทีที่อ่าน

ความเงียบคือสิ่งที่รับน้ำหนัก

สอง release สุดท้ายของ GeekBye v1 ว่าด้วยความจริงอันน่าอึดอัดข้อเดียวกัน: การถอดเสียงแบบเรียลไทม์บนเครือข่ายจริงไม่ได้ไร้การสูญเสีย และการเดินหมากที่ซื่อสัตย์คือเลิกแกล้งทำเป็นว่ามันไร้การสูญเสีย v1.8.20 เก็บสำเนาของ audio chunk ทุกก้อนไว้บนดิสก์ก่อนจะทิ้งมันระหว่างการเชื่อมต่อใหม่ และเริ่มทำเครื่องหมายช่องว่างใน transcript ออกมาดัง ๆ v1.9.0 เลิกส่งความเงียบเพื่อประหยัด bandwidth — แล้วก็ค้นพบว่าความเงียบนั่นแหละคือสัญญาณเป๊ะ ๆ ที่เครื่องถอดเสียงใช้รู้ว่าประโยคจบแล้ว สอง release ว่าด้วยราคาของการโยนของทิ้ง

วิศวกรรม
Audio
ความน่าเชื่อถือ
GeekBye รีลีส
ความเงียบคือสิ่งที่รับน้ำหนัก

สอง release สุดท้ายของ GeekBye v1 — v1.8.20 และ v1.9.0, สองอันที่อยู่ก่อนหมุดหมายเวอร์ชัน 2 พอดี — มีธีมร่วมกันที่จะชัดเจนก็ต่อเมื่อคุณอ่านมันคู่กัน ทั้งคู่ว่าด้วยสิ่งที่แอปกำลังโยนทิ้งอย่างเงียบ ๆ, และทั้งคู่มาถึงข้อสรุปเดียวกัน: สิ่งที่คุณทิ้งเพื่อความมีประสิทธิภาพนั้นกำลังทำงานมากกว่าที่คุณคิด release หนึ่งกำลังทิ้ง audio ระหว่างการ reconnect ของเครือข่าย อีกอันกำลังทิ้งความเงียบเพื่อประหยัด bandwidth ทั้งสองกรณี, สิ่งที่ถูกทิ้งกลับกลายเป็นว่ามันรับน้ำหนัก

การถอดเสียงแบบเรียลไทม์ไม่ได้ไร้การสูญเสีย และ v1.8.20 ยอมรับมัน

มีข้อเท็จจริงหนึ่งเกี่ยวกับการทำ speech-to-text แบบสตรีมผ่าน WebSocket ที่มองข้ามไปง่าย ๆ: เมื่อการเชื่อมต่อหลุดแล้วต่อใหม่ — หรือเมื่อแอป failover จากผู้ให้บริการถอดเสียงรายหนึ่งไปอีกราย, จาก Deepgram ไป ElevenLabs — client จงใจทิ้ง audio chunk ที่จับได้ระหว่างช่องว่างนั้น มันจำเป็นต้องทำ ผู้ให้บริการที่เพิ่งต่อใหม่ซึ่งจู่ ๆ ได้รับเสียงเก่าที่ค้างคาเป็นกอง จะสำลัก, หรือแย่กว่านั้น, ล็อกตัวตรวจจับ voice-activity ของมันไปเกาะสิ่งที่ผิด ดังนั้น client จึงโยน chunk เหล่านั้นทิ้ง

ปัญหาไม่ใช่การทิ้ง ปัญหาคือการทิ้งแต่ก่อน_หมายความ_ว่าอะไร: คำพูดเหล่านั้นไม่เคยโผล่ใน transcript เลย, และไม่มีอะไรบอกคุณว่ามันขาดหายไป การอัดดูครบถ้วน มันไม่ครบ

v1.8.20 เข้าตีเรื่องนี้จากสองแนวรบ, และไม่มีอันไหนเลยที่เป็น «ทำให้การ reconnect ไม่มีวันสูญ audio» — เพราะคุณทำไม่ได้, ไม่ได้ในเรียลไทม์ แทนที่จะเป็นอย่างนั้น, มันทำให้การสูญเสีย กู้คืนได้ และ ซื่อสัตย์

ครึ่งที่กู้คืนได้คือ PCM tee ที่คงทน ใน capture handler ของ main process, ทุก chunk ที่จับได้ตอนนี้ถูกเขียนลงไฟล์บนดิสก์ ก่อน ที่มันจะไปถึงประตูทิ้งของการ reconnect และช่วงผ่อนผัน:

every captured chunk is written to <userData>/sessions/session-<ts>.wav before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass

กลไกนั้น low-tech อย่างน่าพอใจ: teeChunkToDisk เปิด WriteStream, จอง header WAV 44 ไบต์ไว้ล่วงหน้า, ต่อท้ายตัวอย่าง mono 16-bit ดิบ ๆ ไปเรื่อย ๆ ตามที่มันมาถึง, และ patch header ด้วยขนาดสุดท้ายตอนหยุด (fs.openSync(..., 'r+'), เขียน 44 ไบต์ที่ offset 0) ผลลัพธ์คือ WAV ที่เล่นได้ของ audio ทั้งครบ — รวมถึงส่วนที่ transcript เรียลไทม์ไม่เคยเห็น เมื่อการอัดจบ มันปล่อย event session-audio-saved พร้อม path ของไฟล์และจำนวนไบต์

มีโน้ตความซื่อสัตย์สองอันที่สำคัญตรงนี้, เพราะมันง่ายที่จะเคลมเกินจริง อย่างแรก: tee ไม่ได้ เติมช่องว่างใน release เหล่านี้ มันรักษาเสียงดิบไว้ สำหรับ รอบกระทบยอดในภายหลัง — การถอดเสียงซ้ำแบบ batch ที่กระทบยอดไฟล์กับ transcript สด — ซึ่งเป็นตะขอที่ release เหล่านี้วางไว้, ไม่ใช่ฟีเจอร์ที่มันส่งออก commit ตามมาถึงกับใส่ TODO ที่ชี้ไปยัง issue เรื่องการกระทบยอดกำกับ session-audio-saved เพื่อไม่ให้ใครเข้าใจผิดว่ามันเป็นโค้ดตาย อย่างที่สอง: มันกินดิสก์ ทีมประเมินคร่าว ๆ ว่า ราว 86 MB ต่อการอัด 30 นาที, ดังนั้น release เดียวกันจึงเพิ่ม StartupCleanupService ที่ตัด session ที่บันทึกไว้เก่ากว่า 7 วัน ทิ้ง ตาข่ายนิรภัยที่โตไม่มีขอบเขตคือรอยรั่ว; การเก็บกวาดนั่นแหละคือสิ่งที่ทำให้ tee ส่งออกได้

ครึ่งที่ซื่อสัตย์นั้นเรียบง่ายยิ่งกว่า ในความพยายาม reconnect ครั้งแรก, client เขียน entry ที่เป็นจริงและมองเห็นได้ลงใน transcript:

[reconnecting — audio may be missing]

บรรทัดเดียวนั้นคือปรัชญาทั้งหมดของ release ในรูปย่อส่วน แอปรับประกันไม่ได้ว่ามันได้ยินทุกอย่าง, ดังนั้นมันจึงเลิกทำเป็นบอกเป็นนัยว่ามันได้ยิน ช่องว่างที่คุณเห็นได้คือช่องว่างที่คุณเลี่ยงมันได้; ช่องว่างที่คุณเห็นไม่ได้คือรายงานบั๊กที่รอจะเกิด

v1.9.0: ความเงียบที่คุณ optimize ทิ้งไปนั่นแหละคือสัญญาณ

v1.9.0 คือ v1 release สุดท้าย, และมันเป็นจุดที่ backend ส่งมอบชุดความรับผิดชอบเรื่อง live-transcript ลงมาให้ client — ทิ้งความเงียบ, ระบุว่าแต่ละบรรทัดของ transcript มาจากไมค์หรือ system audio จาก timeline ของ voice-activity ในเครื่อง, เป็นเจ้าของ reconnect loop, บังคับใช้เพดานการอัด ส่วนใหญ่ของนั่นคืองานปูฐานสำหรับสถาปัตยกรรมเวอร์ชัน 2 และถูกเล่าไว้ที่อื่นแล้ว แต่มีเส้นด้ายหนึ่งในนั้นที่เป็นเรื่องเล็ก ๆ ที่ดีที่สุดในทั้งสอง release, เพราะมันเป็นวิธีแก้ที่ต้องล้างความเสียหายของ optimization ที่ถูกต้องอย่างสมบูรณ์เมื่อดูโดด ๆ

มันเริ่มด้วยการเปลี่ยนแปลงที่ดีอย่างเห็นได้ชัด: เลิกส่งความเงียบ client เริ่มกรอง chunk ที่มีแหล่งเด่นเป็นความเงียบออกก่อนจะส่งมันไปบน socket — bandwidth น้อยลง, audio ไร้ประโยชน์ให้ 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 ที่ย้อนกลับมากัด ตัวตรวจจับ voice-activity ของเครื่องถอดเสียงไม่มี input แยกต่างหากที่บอกว่า «ผู้พูดจบแล้ว» — มัน_อนุมาน_การจบของประโยคจากการได้ยินความเงียบราวหนึ่งวินาทีใน audio ความเงียบไม่ใช่ noise บนสาย; มันคือข้อความ ด้วยการกรองมันออกเพื่อประหยัด bandwidth, client บังเอิญเลิกส่งสัญญาณเดียวที่บอกเครื่องถอดเสียงว่าประโยคจบแล้ว ดังนั้น partial transcript ทุกอันจึงเอาแต่โตขึ้นเรื่อย ๆ, หางลากยาวไปเป็น ... ที่ไม่เคยคลี่คลาย

คุณอาจแก้เรื่องนี้ด้วยการเอาความเงียบใส่กลับเข้าไป — แต่ถ้าอย่างนั้นคุณก็ยกเลิก optimization ไปแล้ว วิธีแก้จริงดีกว่านั้น: ส่งความหมายของความเงียบโดยไม่ส่งความเงียบ ตอนนี้ client ติดตามว่า audio เงียบมานานแค่ไหนในเครื่อง, และเมื่อมันข้าม SILENCE_COMMIT_THRESHOLD_MS = 1000 — จับคู่ไว้อย่างจงใจกับ threshold ความเงียบของ VAD ฝั่ง backend เอง — มันก็ยิงสัญญาณ commit ที่ชัดแจ้งออกไปนอกแบนด์: sendMixedChunk สังเกตความเงียบที่ต่อเนื่อง, ส่ง realtime-audio:commit ผ่าน IPC, main process ส่งต่อ { type: 'commit' } บน socket, และ backend ก็บอก ElevenLabs ให้สรุป partial ที่ค้างอยู่ สถานะเล็ก ๆ สองชิ้น, silenceStartedAt และ silenceCommitFired, กันมันไม่ให้ยิงซ้ำ ๆ ชัยชนะด้าน bandwidth รอด; การสรุปทำงานอีกครั้ง; ... ต่อท้ายคลี่คลาย

release นี้ก็มีเชิงอรรถเล็ก ๆ ที่ซื่อสัตย์เช่นกัน: logic reconnect แบบไร้ขอบเขตที่ v1.9.0 นำเข้ามาถูกสร้างขึ้นครั้งแรกใน renderer ในฐานะ WsReconnectController, แล้วก็ถูกลบภายใน release เดียวกันเมื่อการอิมพลีเมนต์จริงมาลงจอดใน handler ฝั่ง main process ที่มันควรอยู่ เรื่องราวของ reconnect เอง — backoff ไร้ขอบเขต, สลับผู้ให้บริการทุกห้าครั้งที่ล้มเหลว — ถูกเล่าไว้ในบทของมันเอง; สิ่งที่ v1.9.0 เพิ่มเข้ามาคือมันเป็นจุดที่สถาปัตยกรรมนั้นถือกำเนิด, และเป็นที่ที่ client เข้ารับเป็นเจ้าของมันเป็นครั้งแรก

สามสิ่งที่ release เหล่านี้สอน

  1. เมื่อคุณกันการสูญเสียไม่ได้, ทำให้มันกู้คืนได้และทำให้มันมองเห็นได้ v1.8.20 ไม่ได้หยุดการ reconnect จากการทิ้ง audio — นั่นเป็นไปไม่ได้ทางกายภาพในเรียลไทม์ มันเก็บสำเนาของเสียงดิบไว้เผื่อภายหลัง, และมันติดป้ายช่องว่างใน transcript ไม่มีอันไหนหรูหรา; รวมกันแล้วมันเปลี่ยนการสูญข้อมูลอย่างเงียบ ๆ ให้เป็นสภาวะที่รู้จัก, มีขอบเขต, และกู้คืนได้
  2. ตาข่ายนิรภัยต้องมีขอบเขตของมันเอง tee ที่คงทนจะเติมดิสก์จนเต็มอย่างเงียบ ๆ ถ้าปล่อยไว้ตามลำพัง — วิธีแก้ที่กลายเป็นบั๊กแบบสโลว์โมชัน การเก็บกวาด 7 วันที่ส่งออกใน release เดียวกันไม่ใช่ความคิดที่นึกได้ทีหลัง; มันคือสิ่งที่ทำให้ tee ปลอดภัยพอจะส่งออกได้เลยแต่แรก ถ้าคุณเพิ่มกลไกที่สะสม, ก็เพิ่มกลไกที่ตัดมันทิ้งในลมหายใจเดียวกัน
  3. สิ่งที่คุณทิ้งเพื่อความมีประสิทธิภาพอาจเป็นสัญญาณ บทเรียนที่คมที่สุดข้อเดียวในทั้งสอง release: การกรองความเงียบออกนั้นถูกต้องบนทุกแกนที่คุณจะตรวจตามปกติ — bandwidth, โหลด, การปฏิบัติตามสัญญา — และมันก็ยังทำให้ผลิตภัณฑ์พัง, เพราะระบบปลายน้ำกำลังอ่านความหมายเข้าไปในความเงียบนั้น เมื่อคุณเอาข้อมูลออกเพื่อความมีประสิทธิภาพ, จงถามว่ามีอะไรกำลัง_อนุมาน_บางอย่างจากข้อมูลนั้น บางที คุณต้องส่งความหมายออกไปอย่างชัดแจ้ง ก็เพราะคุณเลิกส่งสิ่งที่แต่ก่อนมันถูกใช้อนุมานนั่นเอง

และนั่นคือจุดจบของเรื่องราวเวอร์ชัน 1 — การเสริมความแกร่งครั้งสุดท้ายก่อนการเขียนใหม่ สำหรับบทก่อนหน้า ดูสามกริยาที่ทำให้ web audio ยังมีชีวิต (v1.8.16 + v2.0.2); สำหรับปลายทางที่ทั้งหมดนี้มุ่งไป ดูเวอร์ชัน 2 จริง ๆ แล้วต้องแลกด้วยอะไร: 206 คอมมิต (v2.0.0); และสำหรับทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต
Steven
Steven5 นาทีที่อ่าน

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต

GeekBye สอง point release ห่างกันสองเดือนและอยู่คนละไฟล์ สอนโค้ดเสียงของเราด้วยบทเรียนเดียวกันจากปลายสองด้านตรงข้าม: เลิกมอง AudioContext ของ browser ว่าเป็นของใช้แล้วทิ้ง release หนึ่งเรียนรู้ที่จะ resume() context ที่ macOS แอบ suspend ไปกลางการอัด; อีกอันเรียนรู้ที่จะ suspend() แทน close() เพื่อให้ session ที่ต่อกันเป็นพรืดเลิกพุ่งชนเพดานราวหก context ของ Chromium resume, suspend, close — นั่นคือทั้งเรื่อง

วิศวกรรม
Audio
Desktop
การหลุดของการเชื่อมต่อครั้งเดียวไม่ควรทำให้ทั้งแอปพัง — แต่ของเราพัง
Steven
Steven2 นาทีที่อ่าน

การหลุดของการเชื่อมต่อครั้งเดียวไม่ควรทำให้ทั้งแอปพัง — แต่ของเราพัง

เมื่อแบ็กเอนด์ของเราหลุดออฟไลน์กลางการประชุม มันไม่ได้แค่หยุดการถอดเสียง — มันทำให้ทั้งแอปพัง สาเหตุคือเหตุการณ์ที่ไม่ถูกจัดการเพียงตัวเดียว และการแก้ไขมีแค่สิบบรรทัด นี่คือชุดรีลีสที่ทำให้ GeekBye ยังคงล็อกอินอยู่และยังคงเชื่อมต่ออยู่ ผ่านสิ่งที่เคยฆ่ามันมาก่อน

วิศวกรรม
ความน่าเชื่อถือ
Electron
แยกการโทรออกจากแอปที่แค่เปิดค้างไว้
Steven
Steven4 นาทีที่อ่าน

แยกการโทรออกจากแอปที่แค่เปิดค้างไว้

GeekBye สังเกตได้ว่าคุณเข้าร่วมการประชุมวิดีโอแล้ว และเสนอบันทึกมันให้ด้วยคลิกเดียว การตรวจจับกลายเป็นครึ่งที่ง่าย — Swift binary ที่อ่านชื่อหน้าต่างทุกสิบวินาที ครึ่งที่ยากคือความแม่นยำ: ไม่ยิงตอน Zoom แค่เปิดค้างอยู่, ไม่เตือนสำหรับการประชุมที่คุณกำลังบันทึกอยู่แล้ว, และไม่ mute ไมค์ในการโทรที่คุณอยู่จริง ๆ สามรีลีส และแต่ละอันคือ guard ที่ต้องเรียนรู้ที่จะไม่เอาชนะตัวเอง

วิศวกรรม
macOS
Desktop