Añadí una superposición en tiempo real a Parakeet STT, mi herramienta de transcripción push-to-talk local para Linux.

Parakeet STT es totalmente local. Mantienes pulsada la tecla Ctrl derecha, hablas y la sueltas. El texto transcrito se inserta dondequiera que esté el cursor mediante «uinput». No hay nube ni suscripción. El backend es un demonio Python que ejecuta el modelo NeMo Parakeet TDT 0.6B v3 de NVIDIA, mientras que el frontend es un binario Rust («parakeet-ptt») que gestiona la tecla de acceso rápido y la conexión WebSocket.

El bucle de retroalimentación que faltaba

La versión anterior carecía de retroalimentación visual. Trabajar en un terminal significaba adivinar si el micrófono estaba activo o si el modelo aún estaba procesando.

Creé parakeet-overlay para resolver esto. Es una pastilla de vidrio esmerilado anclada en la parte inferior de la pantalla que pasa por cuatro estados: Oculto -> Escuchando -> Intermedio -> Finalizando -> Oculto.

Renderiza una forma de onda en directo, texto provisional con un fundido escalonado de 16 ms por carácter y una animación de «respiración» (ciclo de 3 s, amplitud del 5 %) para indicar que el sistema está activo.

Aislamiento de procesos para mayor fiabilidad

Decidí no integrar la superposición en el binario principal. En su lugar, «parakeet-overlay» es un proceso independiente generado por «parakeet-ptt». La comunicación se realiza a través de tramas NDJSON sobre el stdout del hijo.

Esto crea un contrato de fiabilidad estricto. La entrega de texto es crítica; la retroalimentación visual es opcional. Si la superposición se bloquea, se cuelga o el compositor crea un fallo gráfico, no puede bloquear la transcripción o el canal de inyección de texto. La lógica de inyección (clipboard -> uinput -> ydotool) está completamente desacoplada del bucle de renderización.

Elección del protocolo: capa shell vs xdg

La superposición no es una ventana normal. Utiliza el protocolo zwlr_layer_shell_v1, que permite que las superficies se anclen a los bordes de la pantalla sin decoraciones de ventana, de forma similar a las notificaciones emergentes o las barras de estado.

Al iniciarse, el binario sondea el registro Wayland. Si el compositor es compatible con zwlr_layer_shell_v1, lo utiliza. Si no es así (algo habitual en algunos compositores que no son wlroots), recurre a xdg_toplevel como ventana estándar.

También expuse PARAKEET_OVERLAY_MODE para forzar el comportamiento: auto, layer-shell, fallback-window o disabled.

Renderizado puramente por software

Evité por completo OpenGL y wgpu. El proceso de renderizado es una manipulación de píxeles puramente por parte de la CPU, escrita directamente en un búfer de memoria compartida de Wayland.

  • Gráficos: Composición alfa premultiplicada y cálculos de cobertura de rectángulos redondeados para el suavizado.
  • Texto: fontdb para la detección de fuentes del sistema y fontdue para la rasterización de glifos en Rust puro.
  • Por qué: La inicialización de un contexto de GPU tarda entre 50 y 200 ms; el mapeo de la RAM tarda microsegundos. Esto garantiza que la superposición aparezca al instante, al tiempo que elimina por completo los controladores gráficos del dominio de fallos.

La ruta de la forma de onda

El demonio transmite un mensaje audio_level (valor en dB) a través de WebSocket. El binario PTT lo reenvía a la superposición como un fotograma NDJSON:

{"AudioLevel": {"session_id": "...", "level_db": -24.3}}

La superposición implementa el comportamiento clásico del medidor VU. Utilizo un coeficiente de ataque de 0,85 y una liberación de 0,05 para suavizar el movimiento de la aguja. El rango visual abarca de -60dB a -5dB.

Pila

  • ASR: NeMo Parakeet TDT 0.6B v3
  • Daemon: Python, FastAPI, sounddevice
  • Superposición: Rust, wayland-client, wayland-protocols-wlr
  • IPC: NDJSON sobre stdout
  • Inyección: uinput (a nivel del kernel) con ydotool como alternativa

Si estás creando herramientas STT locales en Wayland, la lección más importante aquí es desacoplar la ruta crítica (inyección de texto) de la interfaz de usuario.

Ver el código en GitHub