Construyendo voxlyn-ai: un asistente de voz con IA para Linux

Construyendo voxlyn-ai: un asistente de voz con IA para Linux
Photo by Growtika / Unsplash

Desde hace años quería tener un asistente de voz tipo "Jarvis" en mi escritorio Linux. Lo he visto en películas, en proyectos de código abierto, pero nunca encontré uno que realmente se sintiera natural, rápido y que se adaptara a mi flujo de trabajo. Así que decidí construir el mío propio: voxlyn-ai.

En este post quiero compartir mi experiencia creando voxlyn-ai, un asistente de voz manos libres para Linux que usa Whisper para reconocer voz, OpenCode como agente con modelo de lenguaje y Piper para sintetizar la respuesta. Todo corriendo como un servicio persistente, activado por atajo de teclado y mayormente local (excepto el modelo de lenguaje).

Pueden encontrar todo el código fuente en AudelDiaz/voxlyn-ai.

La inspiración

Todo empezó cuando vi el video de Nate Gentile sobre su configuración con CachyOS + Omarchy. Ver a alguien hablarle a su PC y que esta le respondiera de forma tan natural me hizo decir "esto es lo que necesito". Me encanta Omarchy, de hecho lo uso en mi ThinkPad T14, pero en mi PC de escritorio con CachyOS tengo Gnome de momento, otras diferencias masivas con el video de Nate son primero la bestia de maquina y el presupuesto de 100 euros para la licencia de Claude, que no es una opción para mí en este momento, y ahí fue donde OpenCode entró en juego: es gratuito, open source y hace exactamente lo que necesitaba como backend para el agente.

Tenía claros los requisitos desde el día uno:

  • Activación rápida: presionar una tecla, hablar y recibir respuesta.
  • Stack local en lo posible: Whisper para convertir voz a texto, Piper para convertir texto a voz.
  • Memoria conversacional: que recuerde el contexto de la charla.
  • Extensible: poder agregar habilidades fácilmente (búsqueda web, recordatorios, control del sistema, etc.).
  • Servicio persistente: que viva como un servicio systemd y no tenga que pensar en prenderlo.

La arquitectura

Voxlyn-ai tiene una arquitectura simple pero efectiva. Se compone de un disparador (cliente que envía la orden por un socket Unix) y un servicio (servicio systemd persistente que orquesta todo el proceso).

Disparador (atajo de teclado)
  │  "record" vía socket Unix
  ▼
Servicio (systemd user service)
  ├── faster-whisper (int8)    ← voz a texto
  ├── OpenCode agent            ← modelo de lenguaje con habilidades + memoria
  ├── Piper TTS                 ← texto a voz
  └── Mempalace (JSON)          ← historial de conversación

Cuando presionas el atajo (por ejemplo Alt+Z), el disparador se conecta al socket del servicio, este reproduce un tono, empieza a grabar, detecta silencio, transcribe con Whisper, envía el texto al agente OpenCode, recibe la respuesta y la sintetiza con Piper. Todo en cuestión de segundos.

ritupon-baishya-5L8xbydzMsQ-unsplash.jpg

El stack técnico a detalle

Conversión de voz a texto con faster-whisper

Usé faster-whisper con el modelo small en cuantización int8. Es impresionante lo rápido que corre en CPU, incluso en mi humilde Ryzen 7 5700X logra transcribir en menos de un segundo.

La grabación utiliza detección de actividad de voz (VAD) casera: después de 2 segundos de silencio, asume que terminaste de hablar, recorta el silencio inicial y final, y pasa el audio a Whisper.

def record_audio() -> np.ndarray:
    audio_chunks = []
    silent_chunks = 0
    max_silent = int(SILENCE_DURATION * SAMPLE_RATE / 1024)
    min_chunks = int(MIN_RECORD_SECONDS * SAMPLE_RATE / 1024)
    max_chunks = int(MAX_RECORD_SECONDS * SAMPLE_RATE / 1024)

    stream = sd.InputStream(
        samplerate=SAMPLE_RATE, channels=1, blocksize=1024, dtype="float32"
    )
    with stream:
        while len(audio_chunks) < max_chunks:
            chunk, _ = stream.read(1024)
            audio_chunks.append(chunk)
            if np.max(np.abs(chunk)) < SILENCE_THRESHOLD:
                silent_chunks += 1
            else:
                silent_chunks = 0
            if len(audio_chunks) > min_chunks and silent_chunks > max_silent:
                break
    ...

Algo que noté: Whisper alucina bastante con ruido de fondo. Implementé un filtro de alucinaciones que descarta frases como "Thanks for watching", "Subscribe to the channel" y otros textos que Whisper inventa del silencio.

_HALLUCINATED_PHRASES = frozenset(
    p.lower() for p in (
        "Thanks for watching",
        "Don't forget to subscribe",
        "Subscribe to the channel",
        "Like and subscribe",
    )
)

OpenCode como agente con modelo de lenguaje

Para el agente usé OpenCode, una herramienta que ya conocía y que uso en mi día a día para tareas en la terminal. La comunicación con OpenCode es vía su API REST, y el agente tiene acceso a 5 habilidades integradas: búsqueda web, control del sistema, recordatorios, notas rápidas e información del sistema.

SYSTEM_PROMPT = (
    "You are voxlyn, a helpful AI voice assistant. Respond concisely in 1-3 sentences "
    "in the same language as the user. Do not use markdown formatting.\n\n"
    "You have the following skills available:\n"
    "- web-search: Search the web for current events, facts, and information.\n"
    "- system-control: Adjust system volume, brightness, open applications, take screenshots.\n"
    "- reminders: Set and manage reminders and timers.\n"
    "- quick-notes: Save quick notes to a file.\n"
    "- system-info: Report system diagnostics (RAM, CPU, disk, uptime, updates).\n"
)

El cliente REST para OpenCode maneja sesiones, permite listar, crear, renombrar o eliminar sesiones por comando de voz.

Conversión de texto a voz con Piper

Para la síntesis de voz elegí Piper TTS. Es rápido, suena natural y corre completamente local. Descarga el modelo la primera vez que se ejecuta y lo almacena en caché en voices/. La voz por defecto es es_ES-davefx-medium que suena bastante bien para español.

def speak(text: str, voice: piper.PiperVoice) -> None:
    tts_text = clean_markdown(text)
    buf = io.BytesIO()
    with sf.SoundFile(buf, mode="w", samplerate=voice.config.sample_rate,
                      channels=1, format="WAV", subtype="PCM_16") as wav:
        for chunk in voice.synthesize(tts_text):
            wav.write(chunk.audio_int16_array)
    buf.seek(0)
    data, sr = sf.read(buf)
    sd.play(data, sr)
    sd.wait()

Si la respuesta del modelo contiene código, este se envía como notificación de escritorio en vez de leerlo en voz alta (nadie quiere oír def __init__(self): con sintetizador de voz).

La memoria: Mempalace

Uno de los mayores retos fue la memoria conversacional. Originalmente usaba chromadb con mempalace, pero era demasiado pesado: tiraba dependencias como kubernetes, grpcio, protobuf. Para un asistente de voz que debe arrancar en segundos era inaceptable.

Lo reemplacé por un backend en JSON con un búfer circular de 500 turnos. Cada turno se guarda con su marca de tiempo, el texto del usuario y la respuesta del asistente, con redacción automática de secretos (llaves de API, tokens, contraseñas) antes de guardarlo.

def save_turn(user_text: str, assistant_text: str) -> None:
    data = _load()
    data.append({
        "id": f"turn:{int(time.time())}",
        "user": _redact_secrets(user_text),
        "assistant": _redact_secrets(assistant_text),
        "timestamp": time.time(),
        "wing": WING,
        "agent": "voxlyn",
    })
    _dump(data)

Antes de enviar el mensaje al modelo de lenguaje, se buscan los últimos 5 turnos y se inyectan como contexto. Simple pero efectivo.

El servicio systemd

Todo el proceso vive en un servicio systemd de usuario. Esto significa que arranca con la sesión, se mantiene vivo y puede ser reiniciado automáticamente.

[Unit]
Description=voxlyn-ai voice assistant
After=network.target

[Service]
Type=simple
ExecStart=%h/voxlyn-ai/.venv/bin/python %h/voxlyn-ai/daemon.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Lecciones aprendidas

Lo que salió bien

  • La latencia es aceptable: Whisper small + Piper responden en 2-4 segundos.
  • Python 3.14 no rompió nada: usé uv como gestor de paquetes y la transición fue limpia.
  • Servicios systemd de usuario son perfectos para este tipo de aplicaciones.
  • Sockets Unix como mecanismo de comunicación entre procesos son simples y eficientes.

Lo que cambiaría

  • Whisper alucina mucho: el filtro de alucinaciones ayuda pero no es perfecto. Una palabra de activación tipo "Hey Voxlyn" eliminaría la necesidad de atajo de teclado y reduciría falsos positivos.
  • La memoria JSON es limitada: funciona para los últimos turnos, pero no hay búsqueda por significado. Me gustaría poder encontrar conversaciones viejas por su contenido, no solo por orden.
  • Sin modelo de lenguaje local: actualmente depende de OpenCode con un backend remoto. Tener una opción 100% offline con LM-Studio o Llama.cpp es el siguiente paso.

Lo que viene

Las posibilidades son infinitas. Algunas ideas que me rondan la cabeza:

  • Palabra de activación — "Hey Voxlyn" en vez de atajo de teclado, como un verdadero asistente.
  • Conversaciones continuas — poder hacer preguntas de seguimiento sin tener que reactivar cada vez.
  • Memoria por significado — que recuerde conversaciones viejas por su contenido, no solo las últimas.
  • Habilidades de terceros — que cualquiera pueda crear y compartir habilidades desde ~/.config/voxlyn/skills/.
  • Panel gráfico — historial de conversaciones, gestión de sesiones y voces desde una interfaz visual.
  • Modelo 100% local — empaquetarlo con LM-Studio para uso completamente offline, sin depender de internet ni de llaves de API.
  • Acompañante móvil — una app para activar el asistente desde el celular.

Cómo probarlo

Si quieren probar voxlyn-ai en su máquina:

git clone https://github.com/AudelDiaz/voxlyn-ai.git
cd voxlyn-ai
uv sync

Luego configuran el servicio systemd y vinculan el atajo de teclado. Todo está documentado en el README.

La instalación es bastante sencilla, y si usan Arch o CachyOS como yo, todo debería funcionar sin problemas con PulseAudio o PipeWire.

Conclusión

Crear voxlyn-ai fue un viaje interesante. Empezó como un experimento después de ver un video y terminó siendo un proyecto que uso a diario. Poder decir "Oye, busca en internet el clima" o "Guarda esta nota" sin tocar el teclado es una de esas cosas que no sabía que necesitaba hasta que lo tuve.

Si algo aprendí es que no necesitas PC más moderno, ni una tecnología complicada para tener un asistente de voz útil. Con Python bien estructurado, las herramientas de código abierto correctas y un diseño simple, puedes construir algo que realmente funciona.

El código está en AudelDiaz/voxlyn-ai. Las contribuciones, issues y sugerencias son bienvenidas.

Gracias por llegar hasta este punto, nos vemos en el próximo post.

Read more