Steven
Steven5 min. skaitymo

Ištrynėme 5 000 eilučių garso kodo — ir transkripcijos ėmė pasirodyti dukart

GeekBye v1.6 išrovė du įrenginyje veikiančius Swift transkriptorius ir pakeitė juos vienu suvienytu kanalu — grynas daugiau nei 5 000 eilučių ištrynimas, atliktas žmonėms tebedirbant susitikimuose programoje. Paskui transkripcijos ėmė pasirodyti dukart, nes klaida persikėlė į vienintelį sluoksnį, kuris vis dar manė, kad variklių yra du.

Inžinerija
Transkripcija
Architektūra
GeekBye leidimai
Ištrynėme 5 000 eilučių garso kodo — ir transkripcijos ėmė pasirodyti dukart

Yra ypatinga siaubo rūšis, kai refaktorini kodą, kuris veikia šiuo metu, žmonėms, kurie yra susitikimo viduryje. Negali funkcijos išjungti priežiūrai — kažkas remiasi ja transkribuodamas pokalbį, kurio negalės pakartoti. GeekBye v1.6 padarė būtent tai su svarbiausiu programos keliu: garso kanalu. Tai pasakojimas apie tai, ką ištrynėme, kas sugedo, ir dvi patikimumo pamokas, kurios iš to kilo.

Du varikliai, vienas iš jų Swift kalba

Iki v1.6 GeekBye kalbos į tekstą vertimas įrenginyje veikė Swift binaruose. Jų buvo du: vienas apgaubdavo Apple Speech karkasą, o didesnysis (per 1 500 eilučių) fiksuodavo dvigubą garsą — jūsų mikrofoną plius sistemos garsą — per ScreenCaptureKit ir atverdavo savo WebSocket tiesiai į mūsų backendą srautui į Deepgram siųsti. Du transkriptoriai, du fiksavimo keliai, du būdai bendrauti su tinklu, ir visa tai kompiliuojama kalba, o tai reiškė, kad kiekvienas pakeitimas reikalavo iš naujo kompiliuoti binarą.

v1.6.0 sutraukė visa tai į vieną kanalą. Vienintelis suvienijantis commit ištrynė Apple Speech transkriptorių (~1 145 eilutės), Deepgram Swift fiksatorių (~1 523 eilutės), abu TypeScript tiltus, senąjį orkestratorių ir dvigubo garso IPC apdorotojus — grynas daugiau nei 5 200 eilučių ištrynimas. Jų vietoje: vaizduoklis fiksuoja garsą per standartinius Web APIs, siunčia jį kaip PCM per IPC, o Electron pagrindinis procesas valdo vieną WebSocket kelią į backendą, kur tiekėją parenka backendo konfigūracija. Vienas fiksavimo kelias, vienas soketo savininkas, viena vieta samprotauti.

Ištrinti penkis tūkstančius eilučių — puikus jausmas. Maždaug parai.

Klaida persikėlė į sluoksnį, kuris vis dar manė, kad jų du

Paskui transkripcijos ėmė pasirodyti dukart. Vienas ištartas sakinys būdavo išsaugomas kaip du identiški įrašai — kartais dar blogiau.

Pagrindinė priežastis yra pamokančiausias dalykas visame šiame leidime, nes tai bendras suvienijimo dėsnis. Mes buvome suvieniję tiekėjus pagrindiniame procese — vienas soketas, vienas apdorotojas. Bet React sluoksnis apie tai nesužinojo: jis vis dar prijungdavo du tiekėjams būdingus įvykių kabliukus vienu metu, vieną sukurtą Deepgram, kitą — ElevenLabs. Abu klausėsi. Abu saugojo. Kiekvieną transkripciją įrašydavo tie kabliukai, kurie buvo gyvi — o gyvi buvo abu.

Ir po tuo slypėjo antra, klastingesnė dubliavimosi priežastis: ElevenLabs kiekvieną užbaigtą eilutę siunčia dukart — vieną kartą kaip committed įvykį ir dar kartą kaip committed_with_timestamps. Tad net su vienu kabliuku vienas sakinys galėjo atkeliauti kaip du įvykiai.

Taisymas (v1.6 linijoje) yra tvarkinga dviejų dalių pataisa, tiksliai atitinkanti dvi priežastis: kiekvieną įvykio kabliuką atriboti enabled vėliava, valdoma backendo aktyvaus tiekėjo konfigūracijos, kad tik vienas klausytojas kada nors būtų gyvas; ir pridėti teksto dubliavimo šalinimą pagal šaltinį (įsiminti paskutinę išsaugotą eilutę mikrofonui ir sistemos garsui), kad būtų prarytas ElevenLabs dvigubas siuntimas.

Pamoka platesnė už šią klaidą: kai suvieniji du backendus po viena sąsaja, dubliavimasis neišnyksta — jis migruoja į sluoksnį, kuris vis dar mano, kad jų du. Mes sutraukėme vamzdyną pagrindiniame procese, o nutekėjimas išlindo dviem sluoksniais aukščiau, vartotojo sąsajos įvykių sujungime. Kanalo suvienijimas nebaigtas, kai suvienytas gamintojas; jis baigtas, kai kiekvienas vartotojas liovėsi tikėjęs sena forma.

Pirmasis persijungimas: „pasiduok po penkių bandymų“

v1.6.0 taip pat pristatė patį pirmąjį GeekBye WebSocket automatinį persijungimą su eksponentiniu atidėjimu — ir jį verta parodyti būtent todėl, kad jis yra primityvus protėvis tos neperšaunamos versijos, kurią naudojame šiandien.

Originali versija buvo apribota. Penki bandymai, atidėjimai 1s, 2s, 4s, 8s, 16s — ir tada ji pasiduodavo, siųsdama lemtingą klaidą: „Ryšys prarastas — prašome iš naujo paleisti transkripciją.“ Tuo metu tai atrodė atsakinga: nekartok amžinai, sąžiningai pasakyk vartotojui. Praktikoje tai buvo užmaskuota UX klaida. Šešiolikos sekundžių tinklo trikdis — Wi-Fi perjungimas, VPN prisijungimas iš naujo, tunelis — yra laikinas, bet apribotas kartojimas laiko jį galutiniu. Vartotojas nieko blogo nepadarė, o jam buvo liepta iš naujo paleisti gyvą susitikimą.

Būtent tą nesėkmę pašalino dabartinė konstrukcija. Šiandien GeekBye persijungia su neribotu atidėjimu ir netgi cikliškai keičia tiekėjus, o sustoja tik dėl tikrai lemtingos priežasties — autentifikavimo, kvotos, atsiskaitymo. Visą kelionę nuo „pasiduok po penkių“ iki „niekada nepasiduok dėl laikino trikčio“ pasakoja kodėl jūsų DI užrašininkas sustoja esant blogam Wi-Fi. v1.6 yra ta vieta, kur tas kelias prasidėjo, su naiviąja versija, kuri turėjo egzistuoti pirmiausia.

Pasenę ryšiai persekioja jus, nebent jie turi vardus

Dar viena patikimumo pamoka atkeliavo su v1.6.3, ištaisiusiu subtilią klaidų klasę, su kuria anksčiau ar vėliau susidurs kiekvienas, atliekantis persijungimą per būseną saugantį backendą.

Kai persijungi — arba pakeiti transkripcijos kalbą sesijos viduryje, o tai po dangčiu irgi persijungia — senasis ryšys ne visada miršta tyliai. Jo mirties gargaliavimo žinutės (CONNECTION_LOST, disconnected) gali atkeliauti po to, kai naujasis ryšys jau sveikas, ir sugriauti puikiai veikiantį pakaitalą. Taisymas suteikia kiekvienam ryšio bandymui identitetą — sesijos ID kiekvienam ryšiui, įspaustą į WebSocket URL — plius trumpą tolerancijos langą po persijungimo, per kurį pasenusios ankstesnio bandymo atsijungimo žinutės ignoruojamos. Jis taip pat siunčia aiškų stop pranešimą prieš uždarant soketą, kad backendas galėtų švariai sugriauti seną sesiją.

Principas: bet kokia persijungimo logika per būseną saugantį backendą turi pažymėti kiekvieną bandymą identitetu ir traktuoti pavėluotas ankstesnių bandymų žinutes kaip triukšmą — kitaip sėkmingą persijungimą nužudo jo pirmtako paskutinis atodūsis.

Trys dalykai, kurių išmokė v1.6

  1. Suvienijimas perkelia klaidas; jis jų neištrina. Sutrauk du variklius į vieną ir patikrink kiekvieną vartotoją, kuris vis dar mano, kad jų du. Mūsiškis buvo vartotojo sąsaja, prijungianti du įvykių kabliukus po to, kai vamzdynas jau buvo sujungtas.
  2. Tavo pirmasis persijungimas bus apribotas, o apribotas yra klaidingas gyviems srautams. „Pasiduok po N bandymų“ paverčia laikiną trikdį galutine nesėkme, dėl kurios kaltinamas vartotojas. Nuo pirmosios versijos skirk lemtingą (autentifikavimas, kvota) nuo laikino (nutrūkęs soketas).
  3. Persijungimams reikia identiteto. Pažymėk kiekvieną bandymą; ignoruok pavėluotas mirusių bandymų žinutes. Ryšys be vardo gali būti nužudytas savo paties šmėklos.

Tai antras skyrius patikimumo istorijos, kuri virsta GeekBye v2. Pirmąjį rasi automatinio atnaujinimo saga, užtrukusi šešis leidimus per keturias dienas (v1.5.x); kur galiausiai atkeliavo persijungimo darbas — kodėl jūsų DI užrašininkas sustoja esant blogam Wi-Fi; o dėl viso lanko — programinės įrangos tobulinimo anatomija.