⚠ Traducción desactualizada

Consulta la versión en inglés para conocer la información más reciente sobre las llamadas a herramientas y el rendimiento de VoiceChat.

Modelos de voz a voz

Soniqo admite dos familias de modelos MLX nativos para conversación de voz a voz: PersonaPlex 7B y VoiceChat 11B. Ambos consumen voz y generan directamente el audio del modelo, pero usan arquitecturas dúplex y contratos de runtime distintos.

Elige un modelo

ModeloModelo de conversaciónIdeal para
PersonaPlex 7BFull-duplex simultáneo con Moshi, Depformer y MimiEscucha y habla solapados, con 18 voces seleccionables
VoiceChat 11BDúplex continuo y sincronizado por tramas con FastConformer, Nemotron-H, EAR-TTS y un códec neuronalTramas de streaming de 80 ms, eventos de texto/función y voz nativa del checkpoint

Estos son modelos conversacionales de voz a voz. Hibiki también transforma voz directamente en voz, pero su tarea es la traducción de voz en streaming y se documenta por separado.

PersonaPlex 7B

Modelo de diálogo voz a voz full-duplex basado en la arquitectura Moshi (Kyutai). PersonaPlex 7B genera respuestas habladas directamente a partir de la entrada hablada — sin pipeline intermedio de texto. El modelo incluye 18 presets de voz y está disponible con cuantización de 8 bits (recomendada) y 4 bits. 8 bits es el valor por defecto — es un 30 % más rápido y produce respuestas coherentes, mientras que 4 bits degrada la calidad de la salida.

VoiceChat 11B

Referencia de speech-to-speech dúplex: SALM-Duplex: Efficient and Direct Duplex Modeling for Speech-to-Speech Language Model (Interspeech 2025), citada por la ficha upstream de VoiceChat de NVIDIA.

M5 Pro, 48 GB, Release, cadencia live de 80 ms, INT5 con cabezales protegidos: tres ejecuciones controladas midieron un RTF de la ruta completa de 0.94, 0.92 y 0.92, con unos 8.70 GB de RSS máximo. El primer texto hablado llegó en 44.3–46.7 ms y el primer audio reproducible en 74.0–76.4 ms. Son cifras de cálculo en caliente, no latencia de toma de turno aprendida; el rendimiento INT8 optimizado aún no se ha vuelto a medir.

VoiceChat es el otro runtime nativo MLX de voz a voz de speech-swift. Combina percepción FastConformer causal, canales dúplex de texto/función Nemotron-H, EAR-TTS independiente condicionado por texto y un códec neuronal de 22,05 kHz. La sesión acepta audio mono de 16 kHz de forma continua y devuelve eventos de texto y una trama de salida de 1.764 muestras cada 80 ms.

Carga con VoiceChatModel.load(from:), inicia una VoiceChatSession y envía audio mediante pushAudio. Un bundle completo debe contener encoder/, llm/ y tts/; se rechazan los bundles antiguos de solo comprensión.

Rendimiento actual

INT5 mantiene un rendimiento agregado inferior a RTF 1 en el M5 Pro probado. Las tres ejecuciones también mantuvieron el p95 total por trama por debajo de 80 ms, entre 76.2 y 78.6 ms; por tanto, el margen de plazo sigue siendo estrecho. Mide por separado el inicio del turno del modelo y la latencia de cálculo del hardware.

CLI de VoiceChat en directo

Habla con Soniqo mediante el bundle INT5 completo con protected head, basado en NVIDIA Nemotron VoiceChat 11B. El comando mantiene captura y reproducción en un solo AVAudioEngine para Apple AEC, transmite los subtítulos RNN-T del usuario incluidos en el bundle y reproduce audio del modelo a 22,05 kHz mientras sigue escuchando.

El prompt predeterminado de Soniqo deja claros sus límites: este CLI puede conversar y responder preguntas, pero no accede a calendarios, recordatorios, aplicaciones, cuentas, dispositivos ni servicios externos. Ante esas solicitudes, Soniqo indica claramente que no puede realizar la acción, no pide una confirmación que no puede cumplir y sugiere brevemente qué puede hacer el usuario.

speech voice-chat

speech voice-chat --input question.wav --output response.wav

speech voice-chat \\
  --mcp-config Examples/VoiceChatMCP/apple-reminders.json

Herramientas MCP configurables

El adaptador incluido apple-reminders-eventkit ahora solo expone al modelo list_reminders, create_reminder y update_reminder. El descubrimiento de listas queda privado dentro del adaptador. Para actualizar, el usuario dice el nombre del recordatorio y el runtime lo resuelve a un ID de EventKit privado y fiable; ese ID nunca aparece en el resultado visible para el modelo. La creación y su fecha se confirman en un solo guardado de EventKit. El primer arranque dispone de al menos 60 segundos y las llamadas normales conservan 15 segundos por defecto.

MCP solo se activa al proporcionar --mcp-config. El JSON independiente del proveedor puede iniciar cualquier servidor MCP stdio y selecciona explícitamente hasta cinco herramientas mediante enabledTools. Toda herramienta que no figure en readOnlyTools se trata como escritura. La política predeterminada es confirm: la primera llamada queda pendiente y una confirmación determinista de Soniqo repite los argumentos interpretados y aclara que nada ha cambiado. Solo una nueva respuesta afirmativa del usuario puede ejecutarla una vez. Una llamada idéntica no puede ejecutarse dos veces sin nueva voz del usuario, incluso con allow. También están disponibles deny y la autorización explícita allow. El resultado real vuelve por el canal de funciones del modelo y el éxito exige una respuesta ok.

Los argumentos de escritura se comparan con la transcripción del usuario antes de confirmar. Las fechas relativas usan la fecha local actual; nunca se ejecutan nombres de listas inventados, fechas obsoletas, prioridades ni otros valores sin verificar. Una nueva voz RNN-T seguida del fin de la intervención resuelve la confirmación incluso tras reiniciar la transcripción. Soniqo solo anuncia éxito cuando el servidor MCP devuelve realmente ok: true; de lo contrario indica que no pudo confirmar la ejecución.

«¿Qué recordatorios tengo?» realiza una sola llamada plana a list_reminders que abarca todas las listas de EventKit. Se guardan en caché la lista, el vencimiento, el estado y el ID estable reservado al runtime, de modo que las preguntas de detalle y las actualizaciones seguras no releen cada lista. El panel muestra aparte el tiempo, los pasos de token y los token/s de la decodificación de función; el RTF de audio sigue midiendo únicamente las tramas de 80 ms en primer plano.

La confirmación hablada pregunta si el usuario desea «continuar» y evita deliberadamente todas las frases afirmativas permitidas, de modo que el eco de la reproducción no pueda autorizar la acción pendiente.

Los servidores MCP secundarios solo heredan PATH, HOME, el directorio temporal y las variables de configuración regional. Las credenciales deben pasarse explícitamente en el mapa env del servidor; no se reenvían otros secretos del proceso principal.

La proyección de funciones de 8 bits ocupa unos 518 MB. Durante el habla normal, VoiceChat evalúa únicamente una sonda en caché de 36 KB para PAD e inicio de herramienta. La cabeza completa de 131.072 filas solo se ejecuta cuando el inicio de herramienta supera primero a PAD, verifica después el argmax global y permanece activa mientras se genera una llamada JSON abierta. Cuando el resultado MCP ya se conoce, VoiceChat lo reproduce en bloques de prefill causal limitados a 16 tokens, preservando el feedback de función entrenado y el estado de las cachés de lenguaje y voz sin ejecutar un decode de 11B por cada token del resultado. Así la conversación MCP sigue en tiempo real sin debilitar las decisiones de inicio de herramienta.

La reproducción almacena por defecto tres tramas de 80 ms (240 ms). Tras un corte espera un colchón de recuperación de ocho tramas antes de reiniciarse. Tras la primera llamada de 88 ms o tres tramas en cola, el refinamiento baja de ocho a dos pasos; con 120 ms, seis tramas o una resincronización, baja directamente a un paso. Si aun así la cola alcanza ocho tramas (640 ms), solo se descarta el mínimo audio antiguo necesario para admitir la entrada nueva. Una sobrecarga sostenida se trata como un único episodio de recuperación y un turno ya confirmado por RNN-T permanece activo en vez de borrarse con reinicios repetidos. Las palabras omitidas siguen marcadas y quizá deban repetirse.

Las métricas describen la experiencia: behind 0.3 s es el audio de micrófono en espera, RTF 0.93× es tiempo de cálculo dividido por tiempo de audio (menos de 1 sigue el ritmo; más de 1 se retrasa) y last 74 ms for 80 ms audio compara el último cálculo con los 80 ms de audio que representa cada trama. La media móvil usa las últimas 120 llamadas de micrófono; los eventos de repetición no cuentan como tiempo de audio adicional.

El modo micrófono usa por defecto el control de turnos RNN-T de NVIDIA. Las decisiones BOS/EOS aprendidas por el modelo siguen siendo la ruta normal de baja latencia. La primera predicción de cada trama de 80 ms se interpreta como blank o non-blank; tras confirmar voz del usuario, 40 tramas blank consecutivas (3,2 s) actúan solo como respaldo de seguridad para iniciar la respuesta, y 40 nuevas tramas non-blank ofrecen el respaldo equivalente para interrumpir. El contador de voz se reinicia cuando Soniqo comienza, por lo que la actividad del turno ya completado no puede cortar inmediatamente la respuesta. Todas las tramas de audio siguen llegando al modelo dúplex; --no-rnnt-turn-taking desactiva estos respaldos y deja BOS/EOS únicamente al cabezal de lenguaje aprendido. El panel informa por separado de barge-ins RNN-T y huecos de reproducción.

En modo normal se suprime el BOS nativo del modelo hasta confirmar voz del usuario, por lo que Soniqo no habla primero. --greet permite explícitamente el saludo inicial. Tras el contenido audible, los PAD blank permanecen dentro del turno abierto durante al menos 16 tramas o tres tramas por token de contenido, lo que sea mayor. El primer PAD blank que excede ese presupuesto cierra el turno lógico. Esta cola proporcional al contenido conserva palabras tardías que un corte fijo de 1,28 segundos puede silenciar.

El modo interactivo usa una pantalla alternativa con un panel fijo, por lo que las actualizaciones se redibujan en el sitio sin llenar el historial. Usa --plain para una salida acumulativa.

Para el diagnóstico local, --debug-timeline marca cada frase del usuario y de Soniqo e inserta en la misma cronología los argumentos de la llamada nativa analizada, el inicio/final de MCP y la sincronización de la caché de resultados. También marca pronunciation ended en la última ventana PCM generada audible; es tiempo de salida del modelo, no el final de reproducción del dispositivo. La demo omite los bloques históricos de tiempos; las métricas detalladas de decodificación nativa, proveedor, caché y presión del micrófono quedan en el JSON de voicechat-bench. Puede exponer argumentos de herramientas, así que no lo uses en registros compartidos. Los temporizadores de fase se reinician entre la decodificación nativa y la espera del proveedor.

Las sesiones en directo conservan el prompt de voz inmutable de 37 tramas más 20 segundos de historial EAR-TTS reciente; Nemotron-H mantiene por separado el contexto semántico de la conversación. Los PAD dentro de la cola acústica proporcional al contenido se generan normalmente; solo los PAD silenciosos posteriores avanzan causalmente en lotes de ocho. Así no se corta la respuesta hablada y tampoco crecen sin límite la atención TTS ni el trabajo en reposo. --live-speech-context-seconds 0 restaura todo el historial TTS y el modo archivo mantiene por defecto el historial completo exacto.

En un perfil de 63,6 segundos en un M5 Pro con la cola segura, la ruta en directo midió RTF global 0,87, RTF 0,71 en la ventana final, síntesis final de 4,1 ms/trama, RSS máximo de 8,72 GB y huella física máxima de 23,67 GB. Las ventanas activas con ocho pasos llegaron a RTF 1,05 y 1,12, por lo que el CLI interactivo mantiene su reducción reversible a dos pasos y un modo de emergencia de un paso cuando se acumula entrada. La similitud coseno mínima entre los estados de caché en reposo secuencial y por lotes fue 0,9999999.

El detector de chasquidos iniciales de voicechat-bench analiza la entrada generada y los primeros 50 ms de voz sostenida. Solo marca saltos que superan una amplitud de 0,05 y seis veces el p99 de voz estable; un escenario puede fijar maximum_suspect_onset_transients en cero para bloquear regresiones.

Medido con subtítulos en directo

Tres ejecuciones Release en un M5 Pro de 48 GB midieron RTF 0,92, p95 por trama total de 74,8–75,8 ms, primer audio reproducible en 74,2–74,9 ms y unos 8,74 GB de RSS máximo. La transcripción y la respuesta fueron idénticas en las tres ejecuciones.

Arquitectura y uso de PersonaPlex

PersonaPlex es un modelo autorregresivo multi-stream con tres componentes principales:

ComponenteDetalles
Temporal Transformer32 capas, dim=4096, 32 cabezas, SwiGLU (hidden_scale=4.125), RoPE, cuantizado a 8 bits (por defecto)
Depformer6 capas, dim=1024, 16 cabezas, MultiLinear (weights_per_step=true), dep_q=16
Codec Mimi16 codebooks, frame rate de 12.5 Hz, salida de audio a 24 kHz

El modelo procesa 17 streams simultáneamente: 1 stream de texto + 8 streams de audio del usuario + 8 streams de audio del agent. Esta arquitectura permite la conversación full-duplex en la que el modelo puede escuchar y hablar al mismo tiempo.

Presets de voz

PersonaPlex incluye 18 presets de voz integrados con estilos naturales y variados:

CategoríaPresets
Femenino naturalNATF0, NATF1, NATF2, NATF3
Masculino naturalNATM0, NATM1, NATM2, NATM3
Femenino variadoVARF0, VARF1, VARF2, VARF3, VARF4
Masculino variadoVARM0, VARM1, VARM2, VARM3, VARM4

Monólogo interno

PersonaPlex genera dos streams paralelos en cada paso: 8 tokens de codebook de audio para el codec Mimi y un token de texto para el monólogo interno del modelo. El stream de texto es lo que el modelo está “pensando” mientras habla — puede divergir ligeramente del audio final, pero en la práctica refleja la respuesta hablada con suficiente fidelidad como para usarlo como transcripción en vivo.

Los tokens de texto se devuelven como IDs de piezas SentencePiece en crudo. Decodifícalos con el SentencePieceDecoder que incluye PersonaPlex:

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let decoder = model.tokenizer  // SentencePieceDecoder?

let result = model.respond(userAudio: userSamples, voice: .NATM0)
let transcript = decoder?.decode(result.textTokens) ?? ""
print(transcript)        // "Sure, I can help with that..."
playAudio(result.audio)  // 24 kHz mono Float32

En modo streaming, respondStream emite chunks de textTokens según se van produciendo — decodifícalos de forma incremental para alimentar una vista de subtítulos en vivo mientras el audio sigue generándose. El flag --transcript de la CLI hace exactamente esto por detrás.

Por qué importa: SentencePieceDecoder está construido sobre el lector de protobuf compartido AudioCommon.SentencePieceModel, así que PersonaPlex, OmnilingualASR y cualquier futuro modelo basado en SentencePiece decodifican a través de la misma implementación del tokenizador. Consulta la referencia de SentencePieceModel.

Prompts del sistema

Pasa cualquier prompt del sistema personalizado como una simple cadena de texto — no hace falta tokenización externa:

let response = model.respond(
    userAudio: audio,
    voice: .NATM0,
    systemPrompt: "You enjoy having a good conversation."
)

O usa un preset integrado:

Uso de la CLI

Genera una respuesta hablada a partir de una entrada de audio:

# Basic speech-to-speech
.build/release/speech respond --input question.wav

# Choose a voice preset
.build/release/speech respond --input question.wav --voice NATM0

# Stream audio output during generation
.build/release/speech respond --input question.wav --stream

# Custom system prompt text
.build/release/speech respond --input question.wav --system-prompt-text "You enjoy having a good conversation."

# Use a preset system prompt
.build/release/speech respond --input question.wav --system-prompt customer-service

# Get transcript alongside audio
.build/release/speech respond --input question.wav --transcript

# JSON output with metadata
.build/release/speech respond --input question.wav --json

Opciones

OpciónDescripción
--inputArchivo de audio de entrada (WAV, obligatorio)
--voiceNombre del preset de voz (p. ej., NATM0, VARF2)
--system-promptPreset de prompt del sistema: assistant, focused, customer-service, teacher
--system-prompt-textTexto personalizado del prompt del sistema (sobrescribe --system-prompt)
--max-stepsPasos máximos de generación
--streamEmite chunks de audio durante la generación
--compileUsa inferencia compilada de MLX para una generación más rápida
--transcriptEmite la transcripción de texto junto al audio
--jsonSalida JSON con metadatos

Los parámetros de sampling también se pueden sobrescribir:

OpciónPor defectoDescripción
--audio-temp0.8Temperatura de sampling de tokens de audio
--audio-top-k250Top-k sampling para tokens de audio
--text-temp0.7Temperatura de sampling de tokens de texto
--text-top-k25Top-k sampling para tokens de texto

Streaming

El flag --stream activa la salida de audio en tiempo real. Los chunks de audio se emiten conforme se generan, por lo que la reproducción puede comenzar antes de que la respuesta completa esté lista. Esto resulta especialmente útil para aplicaciones interactivas donde la baja latencia importa.

Rendimiento

MétricaValor
Factor de tiempo real (RTF)~1.4 (8 bits, casi en tiempo real)
Latencia por paso~112 ms/paso en M2 Max (8 bits)
Tamaño del modelo (8 bits)~9.1 GB
RAM máxima (8 bits)~11 GB
Tamaño del modelo (4 bits)~4.9 GB
RAM máxima (4 bits)~7 GB
Importante

PersonaPlex 7B (8 bits) requiere al menos 24 GB de RAM. La variante de 4 bits cabe en dispositivos con 16 GB pero produce una salida degradada. En dispositivos con 8 GB no cabe ninguna de las variantes. Usa --compile para obtener el mejor rendimiento en el hardware compatible.

Variantes del modelo

ModeloTamañoHuggingFace
PersonaPlex-7B (8 bits) recomendado9.1 GBaufklarer/PersonaPlex-7B-MLX-8bit
PersonaPlex-7B (4 bits)4.9 GBaufklarer/PersonaPlex-7B-MLX-4bit

API de Swift

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let response = model.respond(
    userAudio: userSamples,
    voice: .NATM0,
    systemPrompt: "You are a helpful assistant."
)
try WAVWriter.write(samples: response.audio, sampleRate: 24000, to: URL(filePath: "answer.wav"))