Ir al contenido

Voz

APX tiene una capa de voz unificada: una fachada TTS (texto a voz) para hablar texto y un sidecar STT (voz a texto) basado en Whisper para transcribir el audio del micrófono. La CLI apx voice expone ambos. La ventana de Desktop y el Deck usan los mismos motores subyacentes.

Ventana de terminal
# Synthesize and play
apx voice say "Hello, world!"
apx voice say "Hello" --provider piper
apx voice say "Hello" --provider gemini --voice Aoede
apx voice say "Hello" --no-play # generate file, skip playback
# Listen (mic → STT → super-agent → TTS reply)
apx voice listen # stop on silence (sox required)
apx voice listen --seconds 5 # fixed-duration capture
apx voice listen --seconds 5 --no-play # transcribe + agent, no audio playback
# List configured providers
apx voice providers

--provider sobrescribe el default configurado solo para esa llamada. --voice sobrescribe el ID de voz dentro de un proveedor (soportado por OpenAI, ElevenLabs y Gemini). --no-play escribe el archivo de audio en ~/.apx/tmp/tts/ e imprime la ruta sin reproducirlo.

apx
$ apx voice providers
  TTS providers                          default: piper

✓ piper        local    es_AR-daniela-high.onnx
✓ elevenlabs   cloud    eleven_multilingual_v2   key ••••a91f
✓ openai       cloud    tts-1                     key ••••7c2d
⚠ gemini       cloud    no key — run apx config set voice.tts.gemini.api_key
• mock         local    silent WAV (tests only)

STT
✓ whisper      local    model base   lang es
apx voice providers — estado en vivo de cada motor
ID¿Local?¿Clave necesaria?CalidadNotas
pipernoBuenaRecomendación por defecto. Requiere el binario piper + modelo .onnx.
elevenlabsnoExcelenteTier gratuito: 10 k caracteres/mes. Modelo eleven_multilingual_v2.
openainoBuenaReutiliza engines.openai.api_key. Modelo tts-1.
gemininoBuenaDevuelve L16 PCM crudo — APX lo envuelve en WAV automáticamente. Soporta tags de emoción.
custom:<slug>dependeopcionalDependeCualquier servidor de voz compatible con OpenAI (ej. una instancia local de QVox/Qwen3-TTS). Soporta tags de emoción.
mocknoSilenciosoPlaceholder de WAV silencioso. Útil solo para tests.

El proveedor auto prueba en orden: piper → elevenlabs → openai → gemini → mock. Los proveedores custom solo se usan si los agregás al orden de la cadena. En una instalación nueva sin ningún proveedor configurado, auto cae hasta mock (silencio). Configurá al menos un proveedor real antes de esperar audio.

La configuración de los proveedores vive en ~/.apx/config.json bajo voice.tts:

{
"voice": {
"tts": {
"provider": "piper",
"piper": {
"bin": "piper",
"model": "/home/you/.apx/voices/es_AR-daniela-high.onnx"
},
"elevenlabs": {
"api_key": "...",
"model": "eleven_multilingual_v2",
"voice_id": "..."
},
"openai": {
"api_key": "...",
"model": "tts-1",
"voice": "alloy",
"format": "mp3"
},
"gemini": {
"api_key": "...",
"model": "gemini-2.5-flash-preview-tts",
"voice": "Aoede"
}
}
}
}

Cambiá el proveedor activo con:

Ventana de terminal
apx config set voice.tts.provider piper

También podés gestionar los proveedores desde el panel Web bajo Voces (/m/voice).

Piper corre totalmente offline. Necesitás el binario piper y un modelo de voz .onnx.

  1. Instalá el binario de Piper:

    Ventana de terminal
    curl -L https://github.com/rhasspy/piper/releases/latest/download/piper_macos_aarch64.tar.gz \
    -o /tmp/piper.tar.gz
    sudo tar xzf /tmp/piper.tar.gz -C /usr/local/bin --strip-components=1
  2. Descargá un modelo de voz. Para español argentino (recomendado):

    Ventana de terminal
    mkdir -p ~/.apx/voices
    cd ~/.apx/voices
    curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_AR/daniela/high/es_AR-daniela-high.onnx
    curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_AR/daniela/high/es_AR-daniela-high.onnx.json
  3. Apuntá APX hacia él:

    Ventana de terminal
    apx config set voice.tts.provider piper
    apx config set voice.tts.piper.model "$HOME/.apx/voices/es_AR-daniela-high.onnx"
  4. Probá:

    Ventana de terminal
    apx voice say "hola mundo" --provider piper

Gemini cloud (lo más rápido con una clave existente)

Sección titulada «Gemini cloud (lo más rápido con una clave existente)»
Ventana de terminal
apx config set voice.tts.provider gemini
apx config set voice.tts.gemini.api_key '<YOUR_GEMINI_KEY>'
apx voice say "hola mundo" --provider gemini
Ventana de terminal
apx config set voice.tts.provider elevenlabs
apx config set voice.tts.elevenlabs.api_key '<YOUR_11L_KEY>'
apx config set voice.tts.elevenlabs.voice_id '<VOICE_ID>'
apx voice say "hola mundo" --provider elevenlabs

Proveedores custom compatibles con OpenAI (QVox / Qwen3-TTS)

Sección titulada «Proveedores custom compatibles con OpenAI (QVox / Qwen3-TTS)»

Además de los proveedores incorporados, podés apuntar APX a cualquier servidor de voz compatible con OpenAI — por ejemplo una instancia local de QVox / Qwen3-TTS. Los proveedores custom viven bajo voice.tts.custom.<slug> y se sirven todos a través del mismo adaptador openai; cada uno aparece en la cadena con el id de motor custom:<slug>.

{
"voice": {
"tts": {
"custom": {
"qvox": {
"base_url": "http://127.0.0.1:5111/v1",
"api_key": "",
"model": "qwen3-tts",
"voice": "default",
"label": "QVox (local)"
}
}
}
}
}

base_url es solo configuración — nunca está hardcodeado, así que custom:<slug> funciona con cualquier servidor compatible con OpenAI, no solo QVox. El manejo de claves y de los requests difiere del OpenAI estándar cuando hay base_url configurado:

  • El motor usa únicamente su propia api_key (a menudo vacía para un servidor local/abierto) — nunca cae hacia engines.openai.api_key ni OPENAI_API_KEY, así que tu clave de OpenAI nunca se filtra a un endpoint de terceros.
  • Campos extra que la API estándar de OpenAI no acepta — instruct (la voz/ estilo base), language y temperature — se envían solo a los endpoints custom.
  • El formato de respuesta por defecto para endpoints custom es wav (OpenAI estándar usa mp3 por defecto).

Agregá y gestioná proveedores custom desde el panel Web bajo Voces (/m/voice) — Agregar/Quitar un proveedor custom, editar base_url y los campos avanzados de modelo/voz ahí en vez de editar el JSON a mano.

Algunos backends de TTS — hoy los proveedores custom:<slug> y gemini — pueden interpretar marcadores [tag] inline en el texto y cambiar la emoción hablada por segmento (ej. [excited] ¡Listo! [calm] Lo dejé anotado.). Esto es una capacidad por motor, opt-in, no una feature hardcodeada de QVox: activala con voice.tts.<id>.emotions.enabled (o voice.tts.custom.<slug>.emotions.enabled para proveedores custom).

{
"voice": {
"tts": {
"custom": {
"qvox": {
"base_url": "http://127.0.0.1:5111/v1",
"emotions": {
"enabled": true,
"tags": ["happy", "sad", "excited", "angry", "calm", "whisper", "shout", "laugh", "cry", "narrator", "neutral"]
}
}
}
}
}
}

tags es opcional — si se omite, cae al conjunto canónico de arriba (refleja el set de tags por defecto de QVox).

Cuando las emociones están habilitadas para el motor que efectivamente va a hablar la respuesta, APX inyecta una guía breve en el system prompt del modo voz para que el agente conozca la sintaxis [tag] y la use con moderación. La guía siempre coincide con el primer motor habilitado en la cadena configurada — no cualquier motor con capacidad de tags — así el agente nunca emite tags que termine hablando un motor distinto (sin soporte de tags).

Los tags son una señal exclusiva de TTS:

  • Se mantienen en el texto que recibe el motor de TTS, para que un motor con soporte de tags pueda actuar sobre ellos.
  • Se quitan de todo lo que el usuario lee — el bubble del chat, el historial de mensajes y el índice de RAG — vía stripEmotionTags(). En un motor que no soporta tags, esta misma función limpia cualquier marcador suelto antes de la síntesis para que nunca se lean en voz alta literalmente.

Desde el módulo Voces del panel Web, cada fila de proveedor con capacidad de tags muestra un toggle compacto de Emotions para activar/desactivar la capacidad por motor sin abrir el diálogo completo de configuración.

POST /voice/turn es un único round-trip bidireccional:

  1. STT — transcribe el audio entrante (o acepta text directamente, salteando el STT).
  2. Agente — corre el super-agente sobre el texto transcrito.
  3. TTS — sintetiza la respuesta y devuelve una ruta a un archivo de audio.
Ventana de terminal
# Drive from curl with pre-transcribed text
curl -X POST http://127.0.0.1:7430/voice/turn \
-H "Authorization: Bearer $(cat ~/.apx/daemon.token)" \
-H "Content-Type: application/json" \
-d '{"text": "What tasks are open?", "channel": "voice"}'

Respuesta:

{
"user_text": "What tasks are open?",
"reply_text": "You have 3 open tasks…",
"reply_audio_path": "/home/you/.apx/tmp/tts/reply-abc123.wav",
"reply_duration_s": 4.1,
"reply_mime": "audio/wav",
"provider": "piper"
}

La ventana de Desktop y el Deck usan internamente este endpoint. El campo channel (voice, deck, desktop, telegram) controla el formato de la respuesta: los canales voice/deck reciben respuestas cortas y aptas para hablar; telegram recibe texto formateado en Markdown.

El motor STT local está basado en Whisper y se adapta a tu hardware en vez de obligarte a elegir entre CTranslate2/MLX/whisper.cpp vos mismo:

HardwareBackend recomendadoModelo
Apple Silicon (Metal)mlx (mlx-whisper, GPU/Neural Engine)mlx-community/whisper-large-v3-turbo
NVIDIA (CUDA)faster (faster-whisper, CUDA)large-v3
AMD / Radeonfaster (CPU — CTranslate2 no soporta ROCm)small
Solo CPUfaster (CPU)small

transcription.local.backend puede ser auto (default, usa la tabla de arriba), mlx o faster. Desde el módulo Voces del panel Web, la card de STT muestra un bloque de Hardware detectado (badge de aceleración — Metal/CUDA/Vulkan-ROCm/CPU — más nombre de GPU y memoria total) y un selector de Aceleración/Motor para elegir el backend explícitamente; la lista de modelos se adapta al backend elegido y muestra el estado de descarga en disco por modelo.

Venv dedicado (aísla mlx de tu Python de sistema)

Sección titulada «Venv dedicado (aísla mlx de tu Python de sistema)»

mlx-whisper trae consigo torch/scipy/numba, lo que puede contaminar o generar conflictos con tus otros proyectos de Python si se instala en el intérprete de sistema o user-site. APX en cambio es dueño de un virtualenv dedicado en ~/.apx/runtime/whisper-venv: lo crea, instala faster-whisper/mlx-whisper ahí adentro, y el proceso del servidor whisper arranca usando el intérprete de ese venv. Si el venv todavía no existe (ej. una instalación vieja), APX cae al python3 del sistema — el camino legacy que instala los paquetes de Whisper en el user-site.

El idioma se fuerza desde la config, no se autodetecta

Sección titulada «El idioma se fuerza desde la config, no se autodetecta»

STT resuelve el idioma efectivo de transcripción con esta prioridad: un transcription.local.language explícito, si no config.user.language, si no "auto" (Whisper autodetecta). El camino de captura de Desktop antes siempre mandaba un header de idioma auto a /transcribe/chunk, lo que sobreescribía tu idioma configurado y perjudicaba la precisión en clips cortos y nombres propios. /transcribe/chunk ahora solo sobreescribe el idioma resuelto cuando quien llama fija uno real (no "auto") — así que tu idioma configurado siempre se mantiene salvo que pidas autodetección explícitamente.

apx voice say usa el primer reproductor del sistema disponible: afplay (macOS) → paplayaplayplay (sox) → ffplay.

Si no se encuentra ninguno, el archivo se escribe y se imprime la ruta — sin error, pero sin sonido. Instalá cualquiera de los anteriores para tener reproducción.

apx voice providers muestra solo mock

No hay ningún proveedor real configurado. Seguí una de las rutas de configuración de arriba.

El archivo se genera pero no hay sonido

No hay ningún reproductor de audio del sistema en tu PATH. En macOS afplay está siempre presente. En Linux instalá sox (apt install sox) o pulseaudio-utils.

La salida de Gemini suena distorsionada o no se reproduce

Las versiones más viejas de APX no envolvían el L16 PCM crudo en un header WAV. Actualizá APX o convertí con ffmpeg -f s16le -ar 24000 -ac 1 -i raw.pcm out.wav.

apx voice listen devuelve texto vacío

sox es requerido para la grabación con detección de silencio. Instalalo o usá --seconds N para especificar una duración fija en su lugar.

apx
$ apx voice say "hello" --provider piper
✓ provider  piper (local)
✓ voice     en_US-amy-medium
• sintetizando 1 oración … 0.4s
♪ reproduciendo 1.2s de audio
✓ listo
apx voice say — sintetizando y reproduciendo una frase de prueba