arrow_back Tum yazilar
6 dk okuma

Herhangi bir pencereye yazan bir sesli dikte uygulaması yaptık

Python ile gerçek zamanlı, çevrimdışı çalışabilen, aktif pencereye doğrudan tuş basışı enjekte eden bir masaüstü konuşmadan metne uygulaması yaptık.

R
Revolt Bots Team
Tacet main window showing live transcription preview while dictating

Çoğu dikte aracı metni kendi penceresine yazar. Önce yazıya dökersin, sonra başka bir yere kopyala yapıştır yaparsın. Bu akışını bozar. Tacet mikrofonunu dinler ve aktif olan uygulamaya doğrudan yazar. VS Code, Slack, tarayıcın, terminal. Pano yok, eklenti yok, pencere değiştirme yok.

Mevcut seçenekler ya seni tek bir ekosisteme kilitler (Apple Dictation, Google Voice Typing) ya da uygulamaya özel entegrasyon ister. Her yerde çalışan, gizlilik istersen çevrimdışı çalışabilen, internet bağlantısı veya API anahtarı gerektirmeyen bir şey istedik. Piyasada üçünü birden yapan bir şey yoktu.

Bunu gerçekten kim kullanıyor

Çok yazan ve bunun yerine konuşmak isteyen insanlar. Uzun içerik taslağı hazırlayan yazarlar. Editörlerinden çıkmadan yorum veya dokümantasyon dikte etmek isteyen geliştiriciler. RSI veya erişilebilirlik ihtiyacı olan, bütün gün klavye kullanamayan insanlar. Gizliliğe önem veren ve ses verisinin bir sunucuya gitmesini istemeyen insanlar.

Teknoloji yığını

Masaüstü arayüzü için CustomTkinter ile Python 3.9+. Yerel transkripsiyon için faster-whisper (CTranslate2 üzerinde çalışıyor). Gerçek zamanlı mikrofon yakalama için sounddevice. Global kısayollar ve tuş basışı enjeksiyonu için pynput. Ses sinyali işleme için numpy. Sistem tepsisi ikonu için pystray. Opsiyonel bulut sağlayıcılar olarak OpenAI API ve Deepgram API.

Süslü bir şey yok. Python’u seçtik çünkü yapay zeka/makine öğrenimi ekosistemi orada. faster-whisper, Python bağlayıcıları olan bir C++ Whisper implementasyonu. Yani transkripsiyon hızı bir Python sorunu değil. Arayüz “yavaş Python” olan tek kısım ve hızlı olmasına gerek yok.

En zor kısım: canlı önizleme

Konuşurken Tacet, ne dediğini düşündüğünün gerçek zamanlı önizlemesini gösteriyor. Whisper aynı ses penceresini her 900ms’de yeniden transkribe ediyor. Basit yaklaşım şu: eski önizlemeyi sil, yenisini yaz. Bu görünür titremeye neden oluyor çünkü saniyede birden fazla kez 40’tan fazla karakteri silip yeniden yazıyorsun.

Bunu stable-prefix diffing ile çözdük. Her güncellemede eski metin ile yeni metin arasındaki en uzun ortak ön eki buluyoruz. Sonra sadece değişen son eki silip yeni son eki yazıyoruz. Whisper “şunu demek” den “şunu demek istiyorum” a geçerse ilk 12 karaktere dokunmuyoruz. Sadece ” istiyorum” ekliyoruz.

# Tüm önizlemeyi silip yeniden yazmak yerine
# zaten doğru olanı bulup sadece sonunu düzeltiyoruz.

common_len = 0
limit = min(len(old_text), len(new_text))
while common_len < limit and old_text[common_len] == new_text[common_len]:
    common_len += 1

# Sadece değişeni sil
old_suffix_len = len(old_text) - common_len
backspace(self.kb, old_suffix_len)

# Sadece yeniyi yaz
new_suffix = new_text[common_len:]
if new_suffix:
    self._safe_type(new_suffix)

Bu, görünür tuş basışı karmaşasını yaklaşık %80 azalttı ve önizlemenin titrek yerine akıcı hissettirmesini sağladı.

Diğer baş ağrısı ses aktivite algılamasıydı. Sabit bir enerji eşiği işe yaramıyor çünkü her mikrofonun farklı arka plan gürültüsü var. Sonunda üstel hareketli ortalama kullanarak bir gürültü tabanı takip ettik, sonra konuşma/sessizlik eşiklerini o tabanın üzerinde çarpanlar olarak belirledik. Sessiz bir oda da gürültülü bir kafe de kullanıcı hiçbir ayara dokunmadan çalışıyor.

# Sabit eşikler gürültülü odalarda bozulur. Bunun yerine
# ortam gürültü seviyesini takip edip eşikleri buna göre ayarlıyoruz.

if noise_floor < 1e-6:
    noise_floor = rms
else:
    noise_floor = noise_alpha * rms + (1 - noise_alpha) * noise_floor

speech_threshold = max(self.energy_threshold, noise_floor * noise_speech_mult)
silence_threshold = max(self.energy_threshold, noise_floor * noise_silence_mult)

if rms >= speech_threshold:
    self._speaking = True

Veri nasıl akıyor

Üç thread. Ana thread arayüzü çalıştırıyor. Bir arka plan thread’i mikrofondan ses yakalıyor ve ses aktivite algılaması yapıyor, sesi sessizlik boşluklarına göre parçalıyor. Bir parça hazır olduğunda kuyruğa giriyor. İki transkripsiyon worker thread’i kuyruktan alıp sesi seçilen motor üzerinden geçiriyor (yerel Whisper, OpenAI API veya Deepgram API).

Transkribe edilmiş metin beş aşamalı bir işlemci hattından geçiyor: sesli komutlar (“nokta” ”.” oluyor), zaman damgaları, şablon genişletme, otomatik büyük harf ve kelime değiştirmeleri. Son metin pynput aracılığıyla aktif pencereye tuş basışı olarak enjekte ediliyor.

Motor ve arayüz callback’ler üzerinden ayrıştırılmış. Motor arayüzden hiçbir şey import etmiyor. Sadece on_status_change, on_preview_update, on_final_text çağırıyor. Arayüz bunları kendi güncelleme kuyruğuna bağlıyor. Motoru tek satır değiştirmeden başsız çalıştırabilirsin.

Gerçekte neler yapabiliyor

Herhangi bir uygulamayla çalışıyor

Tacet aktif olan pencereye doğrudan yazıyor. Tuş basışlarını işletim sistemi seviyesinde enjekte etmek için pynput kullanıyor. Klavye girişi kabul eden her uygulamayla çalışıyor. Tarayıcı eklentisi yok, API entegrasyonu yok, eklenti yok. E-postanı aç, dikte etmeye başla, kelimeler görünsün. Slack’e geç, konuşmaya devam et. Yeni satır ve tab gibi özel karakterleri kaçış dizileri yazmaya çalışmak yerine gerçek tuşlara basarak (Enter, Tab) ele alıyor.

Uyarlanabilir ses aktivite algılama

Uygulama konuşmaya başlayıp bitirdiğini otomatik olarak algılıyor. Üstel hareketli ortalama kullanarak ortam gürültü seviyelerini takip ediyor, sonra konuşma algılama eşiklerini gürültü tabanının üzerinde çarpanlar olarak ayarlıyor. Manuel kalibrasyon olmadan sessiz bir odada veya gürültülü bir ortamda çalışıyor. Ayrıca konuşma algılanmadan önce 300ms ses arabelleği tutuyor (preroll). Böylece bir cümlenin ilk hecesini asla kaçırmıyorsun.

Önce çevrimdışı, bulut yedek olarak

Varsayılan motor, CPU’nda yerel olarak çalışan faster-whisper. API anahtarı yok, internet yok, makinenden veri çıkmıyor. İlk çalıştırmada Tacet seçilen modeli hız testine tabi tutuyor. 1 saniyelik sesi transkribe etmek 5 saniyeden fazla sürerse otomatik olarak “tiny” modele düşürüyor ve bu tercihi kaydediyor. Daha yüksek doğruluk istiyorsan ve bulut seni rahatsız etmiyorsa, tek bir yapılandırma değişikliğiyle OpenAI veya Deepgram’a geçebilirsin.

Özelleştirilebilir sesli komutlar ve metin işleme

“Nokta” dersen ”.” yazıyor. “Yeni satır” dersen Enter’a basıyor. “Onu sil” dersen son parçayı geri siliyor. Bunların hepsi yapılandırılabilir. Kendi komutlarını ekleyebilir, yerleşik olanları devre dışı bırakabilir veya kelime değiştirmeleri ayarlayabilirsin (“btw” “by the way” olur). Metin beş aşamalı bir hattan geçiyor: sesli komutlar, zaman damgaları, şablonlar, otomatik büyük harf ve değiştirmeler. Her aşama bağımsız ve açılıp kapatılabiliyor.

Yaptığımız ödünler

Electron yerine CustomTkinter’ı seçtik. Electron daha az çabayla daha güzel bir arayüz verirdi ama bütün bir Chromium tarayıcısını birlikte getiriyor. Sistem tepsisinde sessizce oturan ve minimum kaynak kullanan bir uygulama için bu yanlış hissettirdi. CustomTkinter daha hafif, daha hızlı açılıyor ve kurulum boyutunu küçük tutuyor. Ödünü daha az arayüz bileşeni ve daha az görsel cilalama.

Kural tabanlı metin işleme tercih ettik, son işleme için LLM kullanmadık. Bir LLM dilbilgisini düzeltebilir, daha akıllı noktalama ekleyebilir, belki cümleleri yeniden yapılandırabilirdi. Ama gecikme eklerdi, ya bir bulut çağrısı ya da ikinci bir yerel model gerektirirdi ve çıktıyı öngörülemez yapardı. Biri sohbet penceresine dikte ederken kelimelerinin hemen yazılmasını ister, iki saniye sonra düzeltilmesini değil. Kurallar hızlı, öngörülebilir ve ücretsiz.

Yerel motor için gerçek zamanlı akış transkripsiyonunu atladık. Whisper tam ses parçaları üzerinde çalışıyor, akışlar üzerinde değil. Örtüşen pencerelerle bir akış hattı kurabilirdik ama karmaşıklığa değmezdi. Bunun yerine parçalamayı (sessizlik algılama, preroll tamponlama, minimum parça süresi) yeterince duyarlı hissettirmek için ayarladık. Çoğu ifade düzgün bir CPU’da bir saniyenin altında işleniyor.

Pano yerine tuş basışı enjeksiyonunu da tercih ettik. Pano enjeksiyonu daha basit ve platformlar arası daha güvenilir olurdu ama kullanıcının en son kopyaladığı şeyi üzerine yazar. Çoğu iş akışı için bu kabul edilemez.

Farklı ne yapardık

Yapılandırma sistemi düz bir JSON dosyası olarak başladı ve organik olarak büyüdü. Kelime değiştirmeleri için eski format geçiş kodu yazmak zorunda kaldık çünkü arayüz orijinal yapılandırmadan farklı bir yapıya ihtiyaç duyuyordu. Baştan başlasak, geçiş mantığını sonradan eklemek yerine ilk günden versiyonlu düzgün bir şema tanımlardık.

Threading modeli çalışıyor ama lock’lar, event’ler ve bayatlık tespiti için utterance ID’leriyle elle yazılmış. concurrent.futures gibi bir kütüphane veya asyncio ile asenkron bir yaklaşım daha temiz olurdu. Motoru ayrı bir process’e ayırmayı da düşünürdük. GIL’den tamamen kurtulmak için. Çünkü hem ses yakalama hem transkripsiyon CPU’ya bağlı.

Otomatik testlere de daha erken yatırım yapardık. Callback tabanlı mimari motoru teoride test edilebilir kılıyor ama tam hattı çalıştıran bir test süitimiz yok. Bu taşıdığımız teknik borç.

Etiketler

tacet desktop ai open-source python

Blogdan daha fazlasi