Vext 1.1 तीन हफ़्ते पहले ship हुआ। Vext 1.2.0 आज land करता है।
getvext.app · 1.1.0 → 1.2.0
User-facing 1.2.0 announcement app में नया क्या है यह cover करता है — एक Speakers tab, full multi-language UI, sharper meeting transcripts। यह post engineering counterpart है: वो problems जिन्हें ship करना genuinely मुश्किल था, और वो अभी code में कैसी दिखती हैं।
Streaming diarization काफ़ी नहीं है — तब भी जब वो "सही" हो
Real-time meeting transcription में एक degree of freedom है जिसे हम बाद में recover नहीं कर सकते: यह audio को एक बार, order में देखती है। Streaming diarizer हर VAD chunk को एक single speaker label assign करता है — chunk के लिए एक embedding। जब लोग बारी-बारी बोलते हैं तो यह एक अच्छा approximation है। Fast back-and-forth में यह गलत label पर collapse हो जाता है।
दो speakers का दस seconds तक एक-दूसरे पर बोलना "Speaker 1 for ten seconds" नहीं बनना चाहिए।
1.2.0 streaming pass को रखता है — meeting transcripts अभी भी live, chunk by chunk आती हैं — लेकिन meeting ख़त्म होने और provisional transcript save होने के बाद, Vext per-stream WAV archives पर एक second diarization pass चलाता है। Offline pipeline है:
- pyannote Community-1 segmentation के लिए
- WeSpeaker embeddings overlap-frame masking के साथ
- VBx Bayesian refinement clusters consolidate करने के लिए
Offline pass हर chunk को उसके globally correct cluster में re-attribute करता है। जब यह किसी known speaker को पहचानता है, उनका embedding database में update होता है — अगली meeting उन्हें तेज़ी से पहचानेगी। Refinement complete होने के बाद Vext temp WAV archives delete कर देता है।
Meeting के दौरान तुम streaming result देखते हो। बाद में refined result पढ़ते हो। यह same artifact नहीं हैं — और यह deliberate है।
Multi-speaker chunks: label के बजाय slice करो
Diarization story का दूसरा हिस्सा एक single chunk के अंदर होता है।
Sortformer एक per-frame speaker timeline emit करता है। अगर एक VAD chunk के अंदर दो या उससे ज़्यादा distinct speaker indices appear होते हैं, तो इसे एक single block के रूप में transcribe करने से model सब कुछ एक voice को attribute करने पर मजबूर होता है। एक brisk exchange वाला 30-second chunk एक speaker label के साथ एक transcribed block बन जाता है — दो voices एक में collapse।
1.2.0 audio को speaker change-points पर slice करता है और हर turn को independently transcribe करता है। एक chunk अंदर जाता है, N chunks बाहर आते हैं — हर एक अपने speaker label के साथ, हर एक एक discrete utterance के रूप में transcribed।
एक detail जिसमें ज़रूरत से ज़्यादा समय लगा: Sortformer noisy sub-300ms flickers fire करता है — एक single frame जो mid-utterance में किसी अलग speaker को attribute होता है। हर flicker पर split करने से transcript fragment होती है और phantom turns बनते हैं। 300ms से कम के regions अब slicer चलने से पहले longest adjacent run में absorb हो जाते हैं — इसलिए हम जो splits करते हैं, वो splits हैं जो exist करती हैं।
Microphone-poisoning bug
Apple का setVoiceProcessingEnabled AVAudioInputNode पर वो करता है जो वो कहता है: AGC, noise suppression, echo cancellation। यह कुछ और भी करता है जिसे docs emphasize नहीं करते — यह input device पर shared HAL state को mutate करता है।
Vext में इसे turn on करो, और same microphone पढ़ने वाला हर दूसरा app — Zoom, FaceTime, OBS, हर recorder — उनके feed पर भी AGC और noise suppression देखता है। User की voice उस call में जिसमें वो actually है, distant और gain-reduced सुनाई देती है। इसे turn off करो, और अगली बार जब कोई दूसरा app इसे turn on करे तो तुम्हें वही problem reverse में मिलती है।
Instinct है API से लड़ना — off push करो, फिर on, lock hold करो, state restore करो। सही जवाब है "इसे यहाँ बिल्कुल use न करो।" Vext meeting participants को microphone path के ज़रिए नहीं बल्कि एक separate system-audio process tap के ज़रिए capture करता है। Mic stream और system stream physically distinct हैं। उनके बीच echo cancellation कभी ज़रूरी नहीं थी; यह एक ऐसी problem solve कर रही थी जो इस architecture में exist ही नहीं करती।
1.2.0 वो call हटा देता है। Shared HAL state अब disturb नहीं होता।
एक event tap जो झूठ बोलता है
Global keyboard event tap — वो चीज़ जो hold-a-hotkey-and-speak को काम कराती है — में एक failure mode है जिसे describe करना ज़रूरी है क्योंकि इसे track down करने में काफ़ी समय लगा।
Display sleep, system sleep, या fast user switching के बाद, tap को back करने वाला mach port stale हो सकता है। CGEventTapIsEnabled true return करता रहता है। Events silently drop होती हैं। User hotkey hold करता है; कुछ नहीं होता। App restart करने से ठीक हो जाता है। Logs में कुछ explain नहीं होता।
1.2.0 self-heals:
- हम अब
NSWorkspace.didWakeNotification,screensDidWakeNotification, औरsessionDidBecomeActiveNotificationको subscribe करते हैं। हर एक full tap reinstall trigger करता है — re-enable नहीं, recreate। - जब system
tapDisabledByTimeoutfire करता है, हम verify करते हैं कि re-enable actually took hold। अगर नहीं हुआ, तो वही full reinstall चलता है। - Health-check timer
.commonrun-loop modes पर move हो गया — हमने इसे इसलिए move किया क्योंकि यह menu tracking और drag operations के दौरान block होता था, ठीक वो windows जब stale tap के user की next interaction होने की सबसे ज़्यादा संभावना है।
यह glamorous code नहीं है। यह वो code है जो decide करता है कि app तुम्हारी menu bar में रहने के लिए काफ़ी reliable है या नहीं।
एक trigger path
Vext keyboard hotkey या menu bar से dictation, note, या meeting start कर सकता है। 1.1 में ये दो code paths थे। वो drift हो गए थे।
Hotkey path coordinator के ज़रिए जाता था: cursor bubble shown, license checked, state callbacks fired, paste serialization enforced। Menu bar path उस सब को bypass करता था और recording layer को directly call करता था। Subtle bugs surface हुए — stale license state के साथ menu से start हुई एक session, एक missing cursor bubble जिसने users को सोचने पर मजबूर किया कि कुछ record हो रहा है या नहीं।
1.2.0 menu actions को hotkey जैसे same code path से route करता है, एक declared difference के साथ: menu से driven dictation hands-free treat होती है (start करने के लिए toggle, stop के लिए toggle), क्योंकि hold करने के लिए कोई physical key नहीं है। बाकी सब identical है क्योंकि यह same function call है।
Speaker labels जो drift नहीं करते
दो paths same audio के लिए names produce कर रहे थे। Per-meeting speaker snapshot — meeting detail में दिखने वाली voices और उनके assigned names की list — meeting ख़त्म होने के बाद DiarizerManager.getSpeakerList() से reconstruct होती थी, फिर chunk labels से अलग display names पर map होती थी।
1.2.0 recording के दौरान liveSnapshot के ज़रिए progressively snapshot build करता है, वही diarizeSpeaker() call use करके जो chunks को label करती है। Same source of truth, by construction। Global KnownSpeakerRepository में already present speakers को per-meeting snapshot से exclude किया जाता है, क्योंकि उनके embeddings globally live हैं और per meeting re-list करने की ज़रूरत नहीं।
पाँच भाषाएँ, एक table
Localization story short है क्योंकि implementation जानबूझकर boring है।
हर user-visible string — sidebar labels, menu items, empty states, onboarding prompts, permission descriptions, toolbar tooltips, About text, model picker labels — एक single centralized translation table से गुज़रती है। पाँच भाषाएँ: English, Spanish, Russian, Hindi, Thai। Missing keys silently English पर fall back होती हैं।
Language picker (Settings → General, और onboarding में) में एक AUTO option है जो macOS system locale follow करता है। कोई specific language choose करने से बिना restart के switch होता है — no app relaunch, no view reload, no flash। यह उसी कारण से संभव है जिस कारण से table को extend करना सस्ता है: हर visible string render time पर table से पढ़ती है, app launch पर नहीं।
अगर हम कोई छठी भाषा add करते हैं, तो काम translation में है, engineering में नहीं।
Upgrade कैसे करें
अगर 1.1.0 पर हो:
brew upgrade muvon/tap/vext
या getvext.app/download से DMG लो।
नए हो तो:
brew install muvon/tap/vext
Existing speakers, dictations, notes, और meetings preserve होते हैं। 1.2.0 के under record की गई पहली meeting वो पहली meeting है जिसमें two-pass diarization effect में है।
Vext 1.2.0 के पूरे release notes →
— डॉन



