Vext 1.1 ออกไปสามอาทิตย์ก่อน Vext 1.2.0 ลงสนามวันนี้

getvext.app · 1.1.0 → 1.2.0

ประกาศ 1.2.0 สำหรับผู้ใช้ ครอบคลุมสิ่งใหม่ในแอป — แท็บ Speakers, UI หลายภาษาเต็มรูปแบบ, transcript ประชุมที่ชัดขึ้น โพสต์นี้คือส่วนวิศวกรรม: ปัญหาที่ยากจริง ๆ ในการ ship และตอนนี้มันหน้าตาอย่างไรในโค้ด


Streaming diarization ไม่พอ — แม้แต่ตอนที่มัน "ถูกต้อง"

การถอดเสียงประชุมแบบ real-time มีอิสระหนึ่งองศาที่เราไม่สามารถกู้คืนได้ภายหลัง: มันได้ยินเสียงครั้งเดียว ตามลำดับ Streaming diarizer กำหนดป้าย speaker เดียวให้แต่ละ VAD chunk โดยใช้ embedding หนึ่งต่อหนึ่ง chunk นั่นเป็นการประมาณที่ดีเมื่อคนพูดสลับกัน แต่มันพังเมื่อไม่ได้พูดสลับกัน

สอง speaker พูดทับกันสิบวินาทีไม่ควรกลายเป็น "Speaker 1 สิบวินาที"

1.2.0 เก็บ streaming pass ไว้ — transcript ประชุมยังคงปรากฏแบบ live chunk ต่อ chunk — แต่หลังจากประชุมเสร็จและบันทึก transcript ชั่วคราวแล้ว Vext รัน diarization รอบสองบนไฟล์ WAV archive ต่อ stream offline pipeline คือ:

  • pyannote Community-1 สำหรับ segmentation
  • WeSpeaker embeddings พร้อม overlap-frame masking
  • VBx Bayesian refinement เพื่อรวม cluster

Offline pass re-attribute ทุก chunk ไปยัง cluster ที่ถูกต้องในระดับ global เมื่อจำ speaker ที่รู้จักได้ embedding ของพวกเขาอัปเดตในฐานข้อมูล — ประชุมครั้งหน้าจะจำได้เร็วขึ้น Vext ลบ WAV archive ชั่วคราวหลังจาก refinement เสร็จ

คุณเห็นผลลัพธ์จาก streaming ระหว่างประชุม คุณอ่านผลลัพธ์ที่ refined หลังจากนั้น สองอย่างนี้ไม่ใช่ artifact เดียวกัน — และนั่นตั้งใจแล้ว


Chunk หลาย speaker: ตัดแทนการติดป้าย

อีกครึ่งของเรื่อง diarization เกิดขึ้นภายใน chunk เดียว

Sortformer ส่ง speaker timeline ต่อ frame ถ้ามี speaker index ต่างกันสองตัวขึ้นไปใน VAD chunk เดียว การถอดเสียงทั้งก้อนจะบังคับให้โมเดลโยน attribute ทุกอย่างให้เสียงเดียว chunk 30 วินาทีที่มีการโต้ตอบเร็ว ๆ กลายเป็นบล็อก transcript บล็อกเดียวกับป้าย speaker เดียว — สองเสียงรวมเป็นหนึ่ง

1.2.0 ตัดเสียงที่จุดเปลี่ยน speaker และถอดเสียงแต่ละรอบอย่างอิสระ หนึ่ง chunk เข้า N chunk ออก — แต่ละตัวมีป้าย speaker ของตัวเอง แต่ละตัวถอดเสียงเป็น utterance แยก

รายละเอียดที่ใช้เวลานานกว่าที่ควร: Sortformer ยิง flicker รบกวนต่ำกว่า 300ms — frame เดียวที่ attribute ให้ speaker อื่นกลางคำพูด การตัดทุก flicker ทำให้ transcript แตกกระจายและสร้าง turn ผีขึ้นมา ตอนนี้บริเวณต่ำกว่า 300ms ถูกดูดซับเข้าใน run ที่ยาวที่สุดที่อยู่ติดกันก่อนที่ slicer จะทำงาน ดังนั้นการตัดที่เราทำคือการตัดที่มีอยู่จริง


Bug ทำให้ไมโครโฟนเป็นพิษ

setVoiceProcessingEnabled ของ Apple บน AVAudioInputNode ทำสิ่งที่บอกไว้: AGC, noise suppression, echo cancellation มันยังทำบางอย่างที่ docs ไม่เน้น — มัน mutate shared HAL state บน input device

เปิดใน Vext และทุกแอปที่อ่าน microphone เดียวกัน — Zoom, FaceTime, OBS, ทุก recorder — ก็เห็น AGC และ noise suppression ถูก apply กับ feed ของพวกเขาด้วย เสียงของผู้ใช้ฟังดูไกลและ gain ลดลงในการโทรที่พวกเขา in จริง ๆ ปิดมันและคุณได้ปัญหาเดียวกันในทางกลับกันเมื่อแอปอื่นเปิดมันครั้งถัดไป

สัญชาตญาณคือสู้กับ API — push off แล้ว on กลับ hold lock restore state คำตอบที่ถูกคือ "ไม่ใช้มันที่นี่เลย" Vext capture ผู้เข้าร่วมประชุมผ่าน system-audio process tap แยกต่างหาก ไม่ใช่ผ่าน microphone path mic stream และ system stream แยกจากกันทางกายภาพ Echo cancellation ระหว่างพวกมันไม่เคยจำเป็น มันแก้ปัญหาที่ไม่มีอยู่ใน architecture นี้

1.2.0 เอา call ออก Shared HAL state ไม่ถูกรบกวนอีกต่อไป


Event tap ที่โกหก

Global keyboard event tap — สิ่งที่ทำให้กดค้าง hotkey แล้วพูดทำงานได้ — มี failure mode ที่ควรอธิบาย เพราะมันใช้เวลานานในการตามหา

หลังจาก display sleep, system sleep หรือ fast user switching mach port ที่รอง tap ไว้อาจกลายเป็น stale CGEventTapIsEnabled ยังคง return true Events ถูก drop อย่างเงียบ ผู้ใช้กด hotkey ค้างไว้ ไม่มีอะไรเกิดขึ้น รีสตาร์ทแอปแก้ได้ ไม่มีอะไรใน log อธิบาย

1.2.0 ซ่อมตัวเอง:

  • เราตอนนี้ subscribe NSWorkspace.didWakeNotification, screensDidWakeNotification และ sessionDidBecomeActiveNotification แต่ละตัว trigger การติดตั้ง tap ใหม่ทั้งหมด — ไม่ใช่ re-enable แต่เป็น recreate
  • เมื่อระบบ fire tapDisabledByTimeout เราตรวจสอบว่า re-enable ได้ผลจริง ถ้าไม่ได้ก็รัน full reinstall เดิม
  • Health-check timer ย้ายไปยัง .common run-loop modes — เราย้ายมันเพราะก่อนหน้านี้มันถูก block ระหว่าง menu tracking และ drag operations ซึ่งเป็นช่วงที่ stale tap มีโอกาสสูงสุดที่จะเป็น interaction ถัดไปของผู้ใช้

มันไม่ใช่โค้ดหรูหรา แต่เป็นโค้ดที่ตัดสินว่าแอปน่าเชื่อถือพอที่จะอยู่ใน menu bar ของคุณหรือเปล่า


Path เดียวสำหรับ trigger

Vext สามารถเริ่ม dictation, note หรือ meeting จาก keyboard hotkey หรือ menu bar ใน 1.1 มันเป็นสอง code path พวกมัน drift กัน

Hotkey path ผ่าน coordinator: cursor bubble แสดง, license ตรวจสอบ, state callback fire, paste serialization บังคับใช้ Menu bar path ข้ามส่วนใหญ่ไปและ call recording layer โดยตรง Bug เล็ก ๆ โผล่ขึ้นมา — session ที่เริ่มจากเมนูด้วย license state เก่า, cursor bubble หายไปทำให้ผู้ใช้ไม่แน่ใจว่ากำลัง record อยู่หรือเปล่า

1.2.0 route action ของเมนูผ่าน code path เดียวกับ hotkey โดยมีความแตกต่างหนึ่งที่ประกาศไว้: dictation ที่ขับเคลื่อนจากเมนูถือว่าเป็น hands-free (toggle เพื่อเริ่ม, toggle เพื่อหยุด) เพราะไม่มีคีย์จริงให้กดค้าง ที่เหลือเหมือนกันทุกอย่างเพราะมันเป็น function call เดียวกัน


ป้าย Speaker ที่ไม่ drift

สอง path กำลังผลิตชื่อสำหรับ audio เดียวกัน per-meeting speaker snapshot — รายชื่อเสียงและชื่อที่กำหนดที่คุณเห็นใน meeting detail — ก่อนหน้านี้ถูก reconstruct จาก DiarizerManager.getSpeakerList() หลังประชุมเสร็จ แล้ว map ไปยัง display name แยกจาก chunk label

1.2.0 build snapshot แบบ progressive ระหว่างการบันทึกผ่าน liveSnapshot โดยใช้ diarizeSpeaker() call เดียวกับที่ label chunk แหล่งข้อมูลเดียว ตามการออกแบบ Speaker ที่อยู่ใน global KnownSpeakerRepository แล้วถูกยกเว้นจาก per-meeting snapshot เพราะ embedding ของพวกเขาอยู่ใน global และไม่จำเป็นต้อง list ใหม่ต่อ meeting


ห้าภาษา หนึ่งตาราง

เรื่อง localization สั้น เพราะ implementation น่าเบื่อโดยตั้งใจ

ทุก string ที่ผู้ใช้มองเห็น — label sidebar, รายการเมนู, empty state, prompt onboarding, คำอธิบาย permission, tooltip toolbar, ข้อความ About, label model picker — ผ่านตารางแปลรวมศูนย์ตารางเดียว ห้าภาษา: อังกฤษ สเปน รัสเซีย ฮินดี ไทย key ที่หายไป fall back ไปอังกฤษอย่างเงียบ

Language picker (Settings → General และใน onboarding) มีตัวเลือก AUTO ที่ตาม locale ระบบของ macOS เลือกภาษาที่ต้องการแล้วสลับโดยไม่ต้องรีสตาร์ท — ไม่ relaunch แอป ไม่โหลดวิวใหม่ ไม่กะพริบ เป็นไปได้ด้วยเหตุผลเดียวกับที่ตารางขยายได้ราคาถูก: ทุก string ที่มองเห็นอ่านจากตารางตอน render ไม่ใช่ตอนแอปเปิด

ถ้าเราเพิ่มภาษาที่หก งานอยู่ที่การแปล ไม่ใช่วิศวกรรม


อัปเกรดอย่างไร

ถ้าอยู่ที่ 1.1.0:

brew upgrade muvon/tap/vext

หรือเอา DMG จาก getvext.app/download

ถ้าเป็นผู้ใช้ใหม่:

brew install muvon/tap/vext

Speaker, dictation, note และ meeting ที่มีอยู่ถูกเก็บรักษาไว้ การประชุมครั้งแรกที่บันทึกภายใต้ 1.2.0 คือครั้งแรกที่มี two-pass diarization ใช้งานได้

Vext 1.2.0 release notes เต็ม →

— Don