
Tyla buvo laikančioji
Du paskutiniai GeekBye v1 leidimai — apie tą pačią nemalonią tiesą: transkripcija realiu laiku per tikrą tinklą nėra be nuostolių, o sąžiningas žingsnis — liautis apsimetinėti, kad taip yra. v1.8.20 laikė kiekvieno garso fragmento kopiją diske, prieš atmesdamas jį pakartotinio prisijungimo metu, ir ėmė garsiai žymėti spragas transkripcijoje. v1.9.0 nustojo siųsti tylą, kad sutaupytų pralaidumą — ir atrado, kad tyla buvo būtent tas signalas, pagal kurį transkribuotojas suprato, jog sakinys baigėsi. Du leidimai apie daiktų išmetimo kainą.
Du paskutiniai GeekBye v1 leidimai — v1.8.20 ir v1.9.0, tie, kurie kaip tik prieš antrosios versijos etapą — turi bendrą temą, kuri tampa akivaizdi tik tada, kai perskaitai juos kartu. Abu apie dalykus, kuriuos programa tyliai išmesdavo, ir abu prieina prie tos pačios išvados: daiktas, kurį atmeti dėl efektyvumo, dirbo daugiau darbo, nei manei. Vienas leidimas išmesdavo garsą per tinklo pakartotinius prisijungimus. Kitas išmesdavo tylą, kad sutaupytų pralaidumą. Abiem atvejais išmestas daiktas pasirodė esąs laikantysis.
Transkripcija realiu laiku nėra be nuostolių, ir v1.8.20 tai pripažįsta
Štai faktas apie srautinį kalbos vertimą į tekstą per WebSocket, nuo kurio lengva nusukti žvilgsnį: kai ryšys nutrūksta ir prisijungia iš naujo — arba kai programa avariniu būdu persijungia nuo vieno transkripcijos teikėjo prie kito, nuo Deepgram prie ElevenLabs — klientas tyčia atmeta garso fragmentus, pagautus per tą tarpą. Kitaip negali. Ką tik prisijungęs teikėjas, kuris staiga gauna susikaupusią pasenusio garso krūvą, užsprings arba, dar blogiau, užkabins savo balso aktyvumo detektorių ant netinkamo dalyko. Tad klientas tuos fragmentus išmeta.
Problema ne išmetime. Problema tame, ką išmetimas anksčiau reiškė: tie žodžiai tiesiog niekada nepasirodydavo transkripcijoje, ir niekas nesakė tau, kad jų trūksta. Įrašas atrodė pilnas. Nebuvo.
v1.8.20 puola tai dviem frontais, ir nė vienas jų nėra „padaryk, kad pakartotiniai prisijungimai niekada neprarastų garso“ — nes negali, ne realiu laiku. Vietoj to jis padaro nuostolį atkuriamą ir sąžiningą.
Atkuriamoji pusė yra patvari PCM atšaka. Pagrindinio proceso pagavimo tvarkyklėje kiekvienas pagautas fragmentas dabar rašomas į failą diske prieš tai, kai pasiekia pakartotinio prisijungimo ir malonės laikotarpio atmetimo vartus:
every captured chunk is written to
<userData>/sessions/session-<ts>.wavbefore realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass
Mechanizmas maloniai nesudėtingas: teeChunkToDisk atidaro WriteStream, iš anksto rezervuoja 44 baitų WAV antraštę, prideda neapdorotus 16 bitų mono pavyzdžius jiems atkeliaujant ir sustojus pataiso antraštę galutiniais dydžiais (fs.openSync(..., 'r+'), įrašo 44 baitus poslinkyje 0). Rezultatas — atkuriamas WAV su pilnu garsu, įskaitant dalis, kurių realaus laiko transkripcija niekada nematė. Kai įrašymas baigiasi, jis paskleidžia session-audio-saved įvykį su failo keliu ir baitų skaičiumi.
Dvi sąžiningumo pastabos čia svarbios, nes tai lengva pervertinti. Pirma: atšaka šiuose leidimuose spragos neužpildo. Ji išsaugo neapdorotą garsą būsimam suderinimo praėjimui — paketinei pertranskripcijai, kuri suderina failą su gyva transkripcija — o tai kabliukas, kurį šie leidimai padeda, o ne funkcija, kurią jie pristato. Tęstinis commit netgi papildo session-audio-saved TODO komentaru, nukreipiančiu į suderinimo užduotis, kad niekas nepalaikytų to mirusiu kodu. Antra: tai kainuoja diską. Komanda įvertino maždaug 86 MB vienam 30 minučių įrašui, tad tas pats leidimas prideda StartupCleanupService, kuris ištrina išsaugotus seansus, senesnius nei 7 dienos. Apsauginis tinklas, augantis be ribų, yra nutekėjimas; būtent tas valymas padaro atšaką tinkamą pristatyti.
Sąžiningoji pusė dar paprastesnė. Per pirmą pakartotinio prisijungimo bandymą klientas įrašo tikrą, matomą įrašą į transkripciją:
[reconnecting — audio may be missing]
Ta viena eilutė yra visa leidimo filosofija miniatiūroje. Programa negali garantuoti, kad išgirdo viską, tad liaujasi užsiminusi, kad išgirdo. Spragą, kurią matai, gali apeiti; spraga, kurios nematai, yra klaidos pranešimas, laukiantis savo valandos.
v1.9.0: tyla, kurią optimizavai, buvo signalas
v1.9.0 yra paskutinis v1 leidimas, ir būtent jame backendas perdavė klientui rinkinį gyvos transkripcijos atsakomybių — atmesti tylą, priskirti kiekvieną transkripcijos eilutę mikrofonui ar sistemos garsui pagal vietinę balso aktyvumo laiko juostą, valdyti pakartotinio prisijungimo kilpą, prižiūrėti įrašymo limitus. Didžioji dalis to yra pamatas antrosios versijos architektūrai ir buvo papasakota kitur. Bet viena gija jame yra geriausia maža istorija iš abiejų leidimų, nes tai pataisymas, kuriam teko atitaisyti žalą, padarytą optimizacijos, kuri izoliuotai buvo visiškai teisinga.
Prasideda nuo akivaizdžiai gero pakeitimo: liaukis siųsti tylą. Klientas ėmė išfiltruoti fragmentus, kurių dominuojantis šaltinis buvo tyla, prieš siųsdamas juos per soketą — mažiau pralaidumo, mažiau bevertio garso backendui apdoroti, ir tai gerbė aiškų backendo kliento kontraktą, sakantį „nesiųsk tylos“. Kiekviena priežastis tai daryti buvo pagrįsta.
Ir tai visiškai sugadino pasakymų finalizavimą. Commit, kuris tai ištaiso, išdėsto priežastinę grandinę su neįprastu atvirumu:
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.
Tai platoniška pratekančios abstrakcijos, kuri atsikanda, forma. Transkribuotojo balso aktyvumo detektorius neturi atskiro įvesties „kalbėtojas baigė“ — jis numano sakinio pabaigą iš to, kad girdi maždaug sekundę tylos garse. Tyla buvo ne triukšmas laide; ji buvo žinutė. Išfiltravęs ją, kad sutaupytų pralaidumą, klientas netyčia nustojo siųsti tą vienintelį signalą, kuris transkribuotojui sakė, kad sakinys baigėsi. Tad kiekviena dalinė transkripcija tiesiog augo toliau, uodega nuslysdama į ..., kuris niekada neišsisprendė.
Galėtum tai ištaisyti grąžindamas tylą atgal — bet tada atšaukei optimizaciją. Tikrasis pataisymas geresnis: nusiųsk tylos prasmę, nesiųsdamas tylos. Klientas dabar seka vietoje, kaip ilgai garsas tyli, ir kai tai peržengia SILENCE_COMMIT_THRESHOLD_MS = 1000 — tyčia suderintą su paties backendo VAD tylos slenksčiu — jis paleidžia aiškų commit signalą už srauto ribų: sendMixedChunk pastebi nuolatinę tylą, siunčia realtime-audio:commit per IPC, pagrindinis procesas persiunčia { type: 'commit' } soketu, o backendas liepia ElevenLabs finalizuoti dalinį rezultatą. Du maži būsenos gabaliukai, silenceStartedAt ir silenceCommitFired, neleidžia jam suveikti pakartotinai. Pralaidumo laimėjimas išlieka; finalizavimas vėl veikia; galinis ... išsisprendžia.
Prie šio leidimo yra ir maža sąžininga išnaša: neribota pakartotinio prisijungimo logika, kurią įvedė v1.9.0, pirmiausia buvo pastatyta rendereryje kaip WsReconnectController, paskui to paties leidimo ribose pašalinta, kai tikroji realizacija nusileido pagrindinio proceso tvarkyklėje, kur jai ir vieta. Pati pakartotinio prisijungimo istorija — neribotas backoff, teikėjai, besikeičiantys kas penkis nesėkmes — papasakota atskirame skyriuje; ką v1.9.0 prideda — tai, kad būtent čia ta architektūra gimė ir kur klientas pirmą kartą perėmė ją sau.
Trys dalykai, kurių šie leidimai išmokė
- Kai negali užkirsti kelio nuostoliui, padaryk jį atkuriamą ir padaryk jį matomą. v1.8.20 nesustabdo pakartotinių prisijungimų nuo garso metimo — tai realiu laiku fiziškai neįmanoma. Jis pasilaiko neapdoroto garso kopiją vėlesniam laikui ir pažymi spragą transkripcijoje. Nė vienas nėra efektingas; kartu jie paverčia tylų duomenų praradimą žinoma, apibrėžta, atkuriama būsena.
- Apsauginiam tinklui reikia savų ribų. Patvari atšaka tyliai pripildytų diską, jei paliktum ją ramybėje — pataisymas, tampantis klaida sulėtintame kadre. Septynių dienų valymas, pristatytas tame pačiame leidime, nėra vėliau kilusi mintis; tai būtent tai, kas apskritai padaro atšaką saugią pristatyti. Jei pridedi mechanizmą, kuris kaupia, pridėk tuo pačiu atodūsiu mechanizmą, kuris jį apkarpo.
- Daiktas, kurį atmeti dėl efektyvumo, gali būti signalas. Aštriausia pamoka iš abiejų leidimų: išfiltruoti tylą buvo teisinga pagal kiekvieną ašį, kurią įprastai patikrini — pralaidumas, apkrova, kontrakto laikymasis — ir vis tiek tai sugadino produktą, nes žemesnė srauto sistema skaitė iš tylos prasmę. Kai šalini duomenis dėl efektyvumo, paklausk, kas iš tų duomenų kažką numanė. Kartais tenka siųsti prasmę tiesiogiai būtent todėl, kad nustojai siųsti daiktą, iš kurio ji anksčiau būdavo numanoma.
Ir tai pirmosios versijos istorijos pabaiga — paskutinis grūdinimas prieš perrašymą. Ankstesniam skyriui — trys veiksmažodžiai, kurie palaiko Web Audio gyvą (v1.8.16 + v2.0.2); tam, kur visa tai vedė — ko iš tikrųjų reikia antrai versijai: 206 commitai (v2.0.0); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.