J’ai ajouté une superposition en temps réel à Parakeet STT, mon outil de transcription push-to-talk local pour Linux.
Parakeet STT est entièrement local. Vous maintenez la touche « Ctrl droite » enfoncée, vous parlez, puis vous la relâchez. Le texte transcrit est injecté à l’endroit où se trouve votre curseur via « uinput ». Il n’y a ni cloud ni abonnement. Le backend est un démon Python exécutant le modèle NeMo Parakeet TDT 0.6B v3 de NVIDIA, tandis que le frontend est un binaire Rust (« parakeet-ptt ») gérant le raccourci clavier et la connexion WebSocket.
La boucle de rétroaction manquante
La version précédente manquait de rétroaction visuelle. Travailler dans un terminal impliquait de deviner si le microphone était actif ou si le modèle était encore en cours de traitement.
J’ai créé parakeet-overlay pour résoudre ce problème. Il s’agit d’une pilule en verre dépoli ancrée au bas de l’écran qui passe par quatre états : Caché -> À l’écoute -> Provisoire -> Finalisation -> Caché.
Elle affiche une forme d’onde en direct, un texte provisoire défilant avec un fondu de 16 ms par caractère et une animation « respirante » (cycle de 3 s, amplitude de 5 %) pour indiquer que le système est actif.
Isolation des processus pour plus de fiabilité
J’ai décidé de ne pas intégrer la superposition dans le binaire principal. Au lieu de cela, « parakeet-overlay » est un processus distinct lancé par « parakeet-ptt ». La communication s’effectue via des trames NDJSON sur le stdout de l’enfant.
Cela crée un contrat de fiabilité strict. La transmission du texte est essentielle ; le retour visuel est facultatif. Si la superposition plante, se bloque ou si le compositeur crée un bug graphique, cela ne peut pas bloquer la transcription ou le pipeline d’injection de texte. La logique d’injection (clipboard -> uinput -> ydotool) est complètement découplée de la boucle de rendu.
Choix du protocole : layer shell vs xdg
La superposition n’est pas une fenêtre normale. Elle utilise le protocole zwlr_layer_shell_v1, qui permet aux surfaces de s’ancrer aux bords de l’écran sans décorations de fenêtre, à l’instar des notifications toast ou des barres d’état.
Au démarrage, le binaire sonde le registre Wayland. Si le compositeur prend en charge zwlr_layer_shell_v1, il l’utilise. Si ce n’est pas le cas (ce qui est courant avec certains compositeurs non wlroots), il revient à xdg_toplevel comme fenêtre standard.
J’ai également exposé PARAKEET_OVERLAY_MODE pour forcer le comportement : auto, layer-shell, fallback-window ou disabled.
Rendu logiciel pur
J’ai complètement évité OpenGL et wgpu. Le pipeline de rendu est une manipulation pure des pixels côté CPU, écrite directement dans un tampon de mémoire partagée Wayland.
- Graphismes : composition alpha prémultipliée et calculs de couverture rectangulaire arrondie pour l’anti-aliasing.
- Texte :
fontdbpour la découverte des polices système etfontduepour la rastérisation des glyphes en Rust pur. - Pourquoi : l’initialisation d’un contexte GPU prend 50 à 200 ms ; le mappage de la RAM prend quelques microsecondes. Cela garantit que la superposition apparaît instantanément, tout en éliminant complètement les pilotes graphiques du domaine des défaillances.
Le chemin de la forme d’onde
Le démon diffuse un message audio_level (valeur en dB) via WebSocket. Le binaire PTT le transmet à la superposition sous forme de trame NDJSON :
{"AudioLevel": {"session_id": "...", "level_db": -24.3}}
La superposition implémente le comportement classique d’un vumètre. J’utilise un coefficient d’attaque de 0,85 et un coefficient de relâchement de 0,05 pour lisser le mouvement de l’aiguille. La plage visuelle couvre -60dB à -5dB.
Pile
- ASR : NeMo Parakeet TDT 0.6B v3
- Daemon : Python, FastAPI, sounddevice
- Superposition : Rust,
wayland-client,wayland-protocols-wlr - IPC : NDJSON sur stdout
- Injection :
uinput(au niveau du noyau) avecydotoolen secours
Si vous développez des outils STT locaux sur Wayland, la principale leçon à retenir ici est de découpler le chemin critique (injection de texte) de l’interface utilisateur.