Simas Razinskas

Pirmiausia vietinė diktavimo sistema „macOS“

Pirmiausia vietinis DI diktavimas „macOS“: garsas lieka įrenginyje, tekstas papildomas ekrano kontekstu, o leidimų ir įrangos pokyčiai nesugriauna srauto.

Paskelbta 2026-06-30Peržiūrėta 2026-07-02

Trumpai

Problema
Debesinis diktavimas netiko jautriam darbui, o konteksto neturinti vietinė transkripcija kūrė tekstą, atsietą nuo dabartinio ekrano.
Apribojimas
Programa turėjo suderinti garso fiksavimą, kalbos atpažinimą, ekrano kontekstą, iškarpinę, pažymėjimą, leidimus ir įklijavimą „macOS“ aplinkoje.
Sukurta sistema
Swift konvejeris aplink whisper.cpp, Accessibility API, CoreGraphics fiksavimą, konteksto surinkimą, pasirenkamą LLM sutvarkymą ir atsparų įrenginių valdymą.
Rezultatas
Kalba lieka vietoje, delsa maža, o įklijuojamas rezultatas gali atspindėti ekrano kontekstą nesugriūdamas pasikeitus vienai OS posistemei.
Kur tai pritaikoma
Tinka DI darbastalio įrankiams, kuriuose privatumas, vietiniai modeliai, OS leidimai ir grakštus degradavimas svarbūs tiek pat kiek modelio kokybė.
Technologijos
Swift / SwiftUI · whisper.cpp · Accessibility & CoreGraphics APIs

Kodėl tai svarbu

Jei jūsų komanda planuoja sistemą su tais pačiais gedimo scenarijais — būtent nuo tokio mąstymo prasideda pirmas pokalbis.

Apžvalga

Pirmiau aprašytas apribojimas — visa užduotis: fiksuoti garsą, jį atpažinti, suprasti, į ką naudotojas šiuo metu žiūri, ir įklijuoti švarų rezultatą į tą lauką, kuriame jis jau rašė — viskas viename „Mac“ kompiuteryje, nieko nesiunčiant iš įrenginio nė viename žingsnyje. Nėra debesijos modelio, į kurį galima persijungti, kai vietinis lėtai veikia, ir nėra trumpojo kelio, kuriuo kelios sekundės garso būtų siunčiamos serveriui geresnei transkripcijai gauti. Viskas toliau — natyvių „macOS“ posistemių grandinė: garso fiksavimas, įrenginyje veikiantis kalbos atpažinimas, ekrano ir iškarpinės kontekstas, Accessibility API — sujungta konvejeriu, kuris turi toliau veikti, kai bet kuri viena grandis pakeičia būseną sesijos metu: atimamas leidimas, prisijungia ausinės, fokusas perkeliamas į kitą langą. Inžinerinis iššūkis — ne pati transkripcijos tikslumo problema; jis tame, kaip penkias nepriklausomas, savaime trapias OS posistemes priversti veikti kaip vieną patikimą įrankį.

Įrenginyje vykdoma transkripcija su whisper.cpp

Pagrindas — kalbos atpažinimo modelis, veikiantis visiškai programos procese, be jokio tinklo kvietimo transkripcijos kelyje. whisper.cpp — tai C/C++ „OpenAI Whisper“ modelio realizacija, sukurta būtent tam, kad efektyviai veiktų standartinėje aparatinėje įrangoje, įskaitant „Apple Silicon“ procesorių ir Neural Engine, be „Python“ vykdymo aplinkos ar GPU klasterio. Tai daro jį tinkamą darbastalio įrankiui: jis pristatomas kaip susiejama biblioteka, vieną kartą paleidimo metu įkelia kvantuotą modelio failą ir vykdo atpažinimą su trumpais garso buferiais pakankamai greitai, kad tai jaustųsi kaip gyvas diktavimas, o ne paketinis apdorojimas.

Garso fiksavimas veikia žemiau modelio, tiekdamas jam nuolatinį pavyzdžių srautą, o ne vieną ilgą įrašą, apdorojamą po fakto. Žemo lygio jungtis prie įvesties įrenginio renka neapdorotus PCM kadrus į besisukantį buferį naudotojui kalbant; konvejeris pjausto šį buferį natūraliose pauzėse ir kiekvieną gabalą perduoda whisper.cpp, todėl transkripcija ima rodytis gerokai anksčiau, nei naudotojas baigia kalbėti, o ne tik atleidus klavišą.

audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
    ringBuffer.append(buffer)
    if ringBuffer.hasReachedSilenceBoundary() {
        let chunk = ringBuffer.drain()
        transcriptionQueue.async {
            let segment = whisper.transcribe(chunk)
            onPartialResult(segment)
        }
    }
}

Gabalais fiksuojamas garsas, tiekiantis pamažu vykstančią transkripciją, o ne vieną blokuojantį kvietimą pabaigoje

Modelio kvietimo vykdymas ne garso gijoje svarbus tiek pat, kiek pati gabalų strategija: modelio iškvietimas, blokuojantis jungties atgalinį kvietimą, sustabdytų patį fiksavimą, prarasdamas garsą, o ne tiesiog atidėdamas transkripciją.

Kodėl ne debesijos diktavimo API

Debesijos kalbos API, modelis prieš modelį, dažniausiai transkribuoja tiksliau nei kvantuotas vietinis modelis — daugiau parametrų, daugiau skaičiavimo galios, jokio šiluminio ar baterijos biudžeto, į kurį reikėtų atsižvelgti. Tačiau ne tai čia yra lemiamas veiksnys. Tą akimirką, kai garsas palieka įrenginį, įrankis naudotojo vardu duoda pažadą, kur gali keliauti jo balsas — ir viskas, ką jis tuo metu diktuoja — o jautriam darbui to pažado įrankis duoti neturi teisės. Debesijos kelias taip pat prideda apsisukimą į kritinį funkcijos, kuri turėtų jaustis akimirksniška, kelią: tinklo delsa, eilė serverio pusėje ir pakartojimo strategija ryšiui nutrūkus — visa tai atsiranda tarp kalbėjimo ir teksto pasirodymo, funkcijoje, kur puse sekundės ilgesnė delsa yra skirtumas tarp „nepastebima“ ir „erzina“. Pirmiausia vietinis sprendimas pašalina abi problemas iš karto, o ne iškeičia vieną į kitą: nei vienas duomuo nepalieka mašinos, ir nėra tinklo šuolio, galinčio pridėti delsą ar sugesti.

Konteksto rinkimas: ekranas, iškarpinė ir pažymėjimas

Vien transkripcija pasako, kas buvo pasakyta, bet ne tai, į ką naudotojas žiūrėjo tai sakydamas, o didelė dalis realaus diktavimo — trumpinys, turintis prasmę tik su tuo kontekstu: „atsakyk į šitą“, „padaryk trumpesnį“, „ištaisyk klaidą aukščiau“. Prieš užbaigiant transkripciją, konvejeris surenka tris ekrano būsenos šaltinius: ekrano nuotrauką, perleistą per OCR matomam tekstui atkurti, tai, kas šiuo metu yra sistemos iškarpinėje, ir tekstą, pažymėtą pirmame plane esančioje programoje, nuskaitomą per Accessibility API, o ne per kopijavimo-įklijavimo apsisukimą, kuris pastebimai sutrikdytų iškarpinę.

Šie trys signalai sudedami į kompaktišką konteksto bloką ir kartu su neapdorota transkripcija perduodami pasirenkamam LLM sutvarkymo žingsniui. Šis žingsnis atlieka du dalykus, kurių vien transkripcijos modelis negali: išsprendžia nuorodas, kurias naudotojas padarė į ekrane matomą tekstą, ir pataiso smulkias kalbos nesklandumų vietas — netikras pradžias, prisipildymo žodžius, vidursakinę pataisą — kurios būdingos gyvai kalbai, bet ne rašytiniam tekstui. Šis žingsnis pasirenkamas sąmoningai, ne tik konfigūracijos lygmenyje: naudotojas, dirbantis be nieko jautraus ekrane, arba norintis kuo neapdorotesnės transkripcijos, gali jį visiškai praleisti ir vis tiek gauti naudingą, tik mažiau kontekstą suvokiantį rezultatą tiesiai iš whisper.cpp.

Čia pirmiausia vietinis principas atsiperka antrą kartą: tas pats įrenginyje veikiantis modelis, kuris garsą laiko atokiai nuo tinklo, gali būti pakartotinai panaudotas ir ekrano kontekstui tinkle nepalikti — taigi konteksto suvokimas tyliai vėl neatveria privatumo klausimo, kurį garso kelias jau uždarė.

Įklijavimas per Accessibility API

Transkripcija naudinga tik atsidūrusi tame lauke, kuriame naudotojas iš tikrųjų rašė, ir tai — savo paties nedidelė integracijos problema. Konvejeris iš anksto nežino, kuri programa turi fokusą, koks tai teksto laukas, ir ar jis apskritai palaiko sudėtingesnius Accessibility API reikšmės nustatymo kvietimus — moderni natyvi teksto sritis dažniausiai palaiko, tačiau nemažai internetu grįstų redaktorių ir kai kurios kelioms platformoms skirtos programos atveria tik dalinį arba nestandartinį prieinamumo medį.

Konvejeris pirmiausia klausia dabar fokusuoto elemento, per AXUIElementCopyAttributeValue, ar jis priima tiesioginį reikšmės įrašymą. Kai tai pavyksta, transkripcija nustatoma kaip lauko reikšmė vienu atominiu kvietimu — jokių matomų klavišų paspaudimų, jokios rizikos, kad simbolis pradings sistemai apkrautai. Kai taikinys tokio kelio nepalaiko, konvejeris pereina prie klaviatūros įvykių sintezės per CoreGraphics įvykių jungčių API, iš esmės „surašydamas“ transkripciją simbolis po simbolio taip, kaip tai darytų labai greitas ir labai tikslus rašytojas.

if let focusedElement = AXUIElementCopyFocusedElement(),
   AXUIElementIsAttributeSettable(focusedElement, kAXValueAttribute) {
    AXUIElementSetAttributeValue(focusedElement, kAXValueAttribute, cleanedTranscript as CFString)
} else {
    synthesizeKeystrokes(for: cleanedTranscript)
}

Pirmenybė tiesioginiam prieinamumo įrašymui, atsarginis variantas — sintetiniai klavišų paspaudimai

Atsarginis kelias lėtesnis ir iš principo pertraukiamas, jei naudotojas ima rašyti tuo pačiu metu, kai vyksta įklijavimas — tai reali, ne teorinė riba, nes visa įrankio prasmė — veikti šalia įprasto rašymo, o ne jį pakeisti. Pirmenybės teikimas tiesioginiam įrašymui, kai tik jis pasiekiamas, ir palaiko patikimą įklijavimą tarp labai skirtingų teksto laukų realizacijų, su kuriomis bendros paskirties diktavimo įrankis realiai susiduria.

Penkių „macOS“ leidimų koordinavimas

Nė vienas iš aukščiau aprašytų dalykų neveikia be naudotojo atskiro sutikimo, duodamo Sistemos nustatymuose, o „macOS“ kiekvieną galimybę saugo už savo atskiro leidimo lango: Microphone garso fiksavimui, Screen Recording OCR konteksto žingsniui, Accessibility esamam pažymėjimui nuskaityti ir įklijavimui, Input Monitoring visuotiniam klaviatūros trumpiniui, paleidžiančiam ir stabdančiam diktavimą, ir Automation užklausti, kuri programa šiuo metu yra pirmame plane. Kiekvienas iš jų — atskiras įrašas sistemos privatumo duomenų bazėje, prašomas ir atšaukiamas nepriklausomai nuo kitų keturių, o „macOS“ nesiūlo vieno jungiklio „pasitikėti šia programa“, apimančio visus penkis iš karto.

Tai sukelia dvi pasekmes, kurioms konvejeris turi būti pritaikytas iš anksto. Pirma, programa turi mokėti paleistis būsenoje, kurioje suteikti tik kai kurie leidimai — švariai įdiegtoje programoje nesuteiktas nė vienas — ir siūlyti nusilpusį, bet veikiantį režimą, o ne atsisakyti veikti: diktavimas be ekrano konteksto puikiai veikia turint tik Microphone ir Accessibility leidimus, pavyzdžiui. Antra, ir ne taip akivaizdu — bet kuris iš penkių gali būti atšauktas naudotojo bet kuriuo metu jau po to, kai buvo suteiktas, o ne tik atmestas iš anksto, todėl vienkartinis leidimo patikrinimas paleidimo metu nėra pakankamas; konvejeris iš naujo patikrina atitinkamą leidimą tiesiai prieš vykdydamas galimybę, kuriai jo reikia, o ne pasikliauja prieš kelias minutes ar valandas gauta atsakymo talpykla.

Grakštus degradavimas keičiantis įrenginiams ir būsenoms

Aukščiau aprašyta grandinė pakankamai ilga, kad kažkas joje pasikeistų sesijos metu ne kaip retas įvykis — o kaip įprastas antradienis. Aiškiausias atvejis — garso aparatinė įranga: „Bluetooth“ ausinių prijungimas ar USB mikrofono atjungimas įrašymo metu ankstesnėse konvejerio versijose buvo traktuojami kaip lemtingi, sugriaunantys visą fiksavimo sesiją ir prarandantys viską, kas buvo buferyje. Sprendimas — užsiprenumeruoti „Core Audio“ pranešimus apie įrenginio pasikeitimą ir įrangos pakeitimą laikyti įprastu įvykiu: konvejeris pirmiausia praleidžia per modelį jau buferyje esantį garsą, tada išardo ir iš naujo sukuria garso jungtį prie naujo numatytojo įrenginio ir tęsia fiksavimą — naudotojui girdimai tai pasireiškia trumpu tarpu, o ne lūžiu ar tyliai nutrūkusia transkripcija.

Ta pati laikysena taikoma ir kitoms posistemėms. Sesijos metu atšauktas leidimas — naudotojas atveria Sistemos nustatymus ir išjungia Screen Recording vykstant diktavimui — degraduoja tik tą vieną konteksto šaltinį, o ne nutraukia visą konvejerį; transkripcija vis tiek užbaigiama, tik be ekrano OCR, kuris būtų papildęs tos sesijos sutvarkymo žingsnį. Fokuso pasikeitimas į kitą programą tarp „įrašymas pradėtas“ ir „transkripcija paruošta įklijuoti“ tvarkomas iš naujo nustatant fokusuotą elementą pačiu įklijavimo momentu, o ne prisimenant jį iš įrašymo pradžios, todėl transkripcija atsiduria ten, kur fokusas realiai yra tuo metu, kai ji paruošta, o ne ten, kur jis buvo prieš kelias sekundes.

NotificationCenter.default.addObserver(forName: .AVAudioEngineConfigurationChange, object: audioEngine, queue: nil) { _ in
    let pending = ringBuffer.drain()
    if !pending.isEmpty { transcriptionQueue.async { whisper.transcribe(pending) } }
    audioEngine.stop()
    configureTap(on: audioEngine.inputNode)
    try? audioEngine.start()
}

Įrenginio pasikeitimas traktuojamas kaip tęsimo taškas, o ne kaip griūtis

Projektavimo principas, glūdintis po visu tuo, tas pats, kuris matomas ir leidimų valdyme: kiekviena posistemė, nuo kurios konvejeris priklauso, laikoma galinčia sugesti ar pakeisti būseną nepriklausomai nuo kitų, o konvejerio užduotis — išlaikyti veikiančią kuo didesnę likusios grandinės dalį, o ne sugriauti visą sesiją dėl vienos grandies.

Kompromisai ir apribojimai

Nė vienas iš šių dalykų nėra nemokamas, ir verta aiškiai įvardyti, kas lieka neišspręsta:

  • Įrenginyje vykdomas atpažinimas turi realią tikslumo ir greičio ribą, palyginti su didžiausiais debesijos kalbos modeliais — mainai už garso laikymą vietoje reiškia priimti šią ribą, o ne vytis pačią pažangiausią transkripcijos kokybę.
  • Ekrano OCR kontekstas padeda tik tada, kai reikiamas tekstas iš tikrųjų matomas ir įskaitomas ekrane; nuoroda į kažką, kas nuslinkę už ekrano ribų, esanti paveikslėlyje ar programoje, tekstą piešiančioje kaip bitmapą, iš to konteksto naudos negauna.
  • Penki nepriklausomai atšaukiami leidimai reiškia penkias atskiras vietas, kur sesiją gali nutraukti naudotojo pakeistas nustatymas, o ne vieną — daugiau judančių dalių, kurioms reikia numatyti grakštų kelią, o ne mažiau.
  • Pasirenkamas LLM sutvarkymo žingsnis pats savaime yra vieta, kur gali atsirasti klaidų — nesklandumus taisantis modelis gali ir per daug pataisyti, praleisdamas ar perfrazuodamas tai, ką naudotojas iš tikrųjų norėjo pasakyti.
  • Grakštus degradavimas sumažina lūžių skaičių, bet ne sumaištį; kai konteksto šaltinis tyliai išnyksta dėl sesijos metu atšaukto leidimo, tai saugiau nei lūžis, bet naudotojas vis tiek turi pastebėti, kad rezultatas tapo mažiau kontekstą suvokiantis nei įprastai.
  • Kalbos modelio vykdymas vietoje turi realią resursų kainą — procesoriaus ir Neural Engine laiką, kurio debesijos kvietimas įrenginyje nesunaudotų — o tai svarbiau senesniuose ar šiluminiu požiūriu ribotuose „Mac“ kompiuteriuose nei toje aparatinėje įrangoje, kuriai sistema buvo kurta.

Šaltiniai ir tolesnis skaitymas

Susijusios atvejų analizės

Sistemos, kurias su šia sieja bendras apribojimas — pakartojimai, izoliacija, kaina, atkūrimas.

Technologijos: Swift / SwiftUI · whisper.cpp · Accessibility & CoreGraphics APIs← Visos atvejų analizės

Turite sistemą, kuri privalo veikti?

Rezervuokite pokalbį — susidėliosime, ką ji turi daryti, kur ji lūš ir kokią mažiausią versiją verta kurti pirmiausia.