⚠ Traduction obsolète

Consultez la version anglaise pour les informations les plus récentes sur les appels d’outils et les performances de VoiceChat.

Modèles parole-à-parole

Soniqo prend en charge deux familles de modèles MLX natifs pour la conversation parole-à-parole : PersonaPlex 7B et VoiceChat 11B. Tous deux reçoivent de la parole et génèrent directement l'audio du modèle, mais leurs architectures duplex et leurs contrats d'exécution diffèrent.

Choisir un modèle

ModèleModèle de conversationIdéal pour
PersonaPlex 7BFull-duplex simultané avec Moshi, Depformer et MimiÉcoute et parole simultanées, avec 18 voix sélectionnables
VoiceChat 11BDuplex continu et synchronisé par trame avec FastConformer, Nemotron-H, EAR-TTS et un codec neuronalTrames de streaming de 80 ms, événements texte/fonction et voix native du checkpoint

Il s'agit de modèles conversationnels parole-à-parole. Hibiki transforme aussi directement la parole en parole, mais sa tâche est la traduction vocale en streaming et sa documentation est séparée.

PersonaPlex 7B

Modèle de dialogue parole-à-parole full-duplex basé sur l'architecture Moshi (Kyutai). PersonaPlex 7B génère des réponses parlées directement à partir d'une entrée parlée — sans pipeline texte intermédiaire. Le modèle est livré avec 18 préréglages de voix et est disponible en quantification 8 bits (recommandée) et 4 bits. Le 8 bits est la valeur par défaut — il est 30 % plus rapide et produit des réponses cohérentes, tandis que le 4 bits dégrade la qualité de sortie.

VoiceChat 11B

Référence speech-to-speech duplex : SALM-Duplex: Efficient and Direct Duplex Modeling for Speech-to-Speech Language Model (Interspeech 2025), citée par la fiche VoiceChat amont de NVIDIA.

M5 Pro, 48 Go, Release, cadence live de 80 ms, INT5 avec têtes protégées : trois exécutions contrôlées ont mesuré un RTF du pipeline complet de 0.94, 0.92 et 0.92, avec environ 8.70 Go de RSS maximal. Le premier texte parlé est arrivé en 44.3–46.7 ms et le premier audio lisible en 74.0–76.4 ms. Ce sont des mesures de calcul à chaud, pas une latence de prise de tour apprise ; les performances INT8 optimisées n'ont pas encore été remesurées.

VoiceChat est l'autre runtime parole-à-parole MLX natif de speech-swift. Il combine perception FastConformer causale, canaux duplex texte/fonction Nemotron-H, EAR-TTS autonome conditionné par le texte et codec neuronal à 22,05 kHz. Une session reçoit en continu de l'audio mono 16 kHz et renvoie toutes les 80 ms des événements texte ainsi qu'une trame de sortie de 1 764 échantillons.

Chargez avec VoiceChatModel.load(from:), démarrez une VoiceChatSession et envoyez l'audio via pushAudio. Un bundle complet doit contenir encoder/, llm/ et tts/ ; les anciens bundles limités à la compréhension sont refusés.

Performances actuelles

INT5 maintient un débit global sous RTF 1 sur le M5 Pro testé. Les trois exécutions ont aussi gardé le p95 total par trame sous 80 ms, entre 76.2 et 78.6 ms ; la marge d'échéance reste donc faible. Mesurez séparément le début de tour du modèle et la latence de calcul matériel.

CLI VoiceChat en direct

Discutez avec Soniqo via le bundle INT5 complet à protected head, basé sur NVIDIA Nemotron VoiceChat 11B. La commande regroupe capture et lecture dans un seul AVAudioEngine pour l'AEC Apple, diffuse les sous-titres utilisateur RNN-T du bundle et lit l'audio modèle à 22,05 kHz tout en continuant d'écouter.

Le prompt Soniqo par défaut précise ses limites : cette CLI peut converser et répondre aux questions, mais n’accède ni aux calendriers, ni aux rappels, ni aux applications, comptes, appareils ou services externes. Pour ces demandes, Soniqo indique clairement qu’elle ne peut pas effectuer l’action, ne sollicite pas une confirmation impossible à honorer et suggère brièvement ce que l’utilisateur peut faire lui-même.

speech voice-chat

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

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

Outils MCP configurables

L’adaptateur fourni apple-reminders-eventkit n’expose désormais au modèle que list_reminders, create_reminder et update_reminder. La découverte des listes reste privée dans l’adaptateur. Pour une mise à jour, l’utilisateur prononce le nom du rappel et le runtime le résout vers un identifiant EventKit privé et fiable, jamais renvoyé au modèle. La création et son échéance sont toujours validées par une seule sauvegarde EventKit. Le premier démarrage dispose d’au moins 60 secondes et les appels ordinaires gardent 15 secondes par défaut.

MCP n’est activé qu’avec --mcp-config. Le JSON indépendant du fournisseur peut lancer tout serveur MCP stdio et sélectionne explicitement jusqu’à cinq outils via enabledTools. Tout outil absent de readOnlyTools est considéré comme une écriture. La politique par défaut est confirm : le premier appel reste en attente, puis une confirmation déterministe de Soniqo répète les arguments compris et précise que rien n’a changé. Seule une nouvelle réponse affirmative de l’utilisateur peut l’exécuter une fois. Un appel identique ne peut pas être exécuté deux fois sans nouvelle parole de l’utilisateur, même sous allow. deny et l’autorisation explicite allow sont aussi disponibles. Le résultat réel revient par le canal de fonctions du modèle et tout succès exige une réponse ok.

Les arguments d’écriture sont comparés à la transcription de l’utilisateur avant confirmation. Les dates relatives utilisent la date locale actuelle ; les noms de listes inventés, les dates obsolètes, les priorités et toute autre valeur non vérifiée ne sont jamais exécutés. Une nouvelle parole RNN-T suivie de la fin d’énoncé résout l’accord même après une réinitialisation de la transcription. Soniqo n’annonce la réussite qu’après un retour réel ok: true du serveur MCP ; sinon il précise que l’exécution n’a pas pu être confirmée.

« Quels rappels ai-je ? » effectue un seul appel aplati à list_reminders couvrant toutes les listes EventKit. La liste, l’échéance, l’état d’achèvement et l’identifiant stable réservé au runtime sont mis en cache ; les questions de détail et les mises à jour sûres ne relisent donc pas chaque liste. Le tableau de bord affiche séparément la durée, les étapes de token et les token/s du décodage de fonction ; le RTF audio continue de mesurer uniquement les trames audio de 80 ms au premier plan.

La confirmation vocale demande si l’utilisateur souhaite « continuer » et évite volontairement toute formule affirmative autorisée, afin qu’un écho de lecture ne puisse pas autoriser l’action en attente.

Les serveurs MCP enfants n’héritent que de PATH, HOME, du répertoire temporaire et des variables de langue. Les identifiants doivent être fournis explicitement dans la table env du serveur ; les autres secrets du processus parent ne sont pas transmis.

La projection de fonctions 8 bits occupe environ 518 MB. Pendant la parole ordinaire, VoiceChat n’évalue qu’une sonde PAD/début d’outil de 36 KB mise en cache. La tête complète de 131 072 lignes ne s’exécute que lorsque le début d’outil dépasse d’abord PAD, vérifie ensuite l’argmax global, puis reste active durant la génération d’un appel JSON ouvert. Une fois le résultat MCP connu, VoiceChat le rejoue par blocs de prefill causal limités à 16 tokens, en préservant le feedback de fonction appris et l’état des caches de langue et de parole sans exécuter un décodage 11B pour chaque token du résultat. La conversation MCP reste ainsi en temps réel sans affaiblir les décisions de début d’outil.

La lecture précharge par défaut trois trames de 80 ms (240 ms). Après une coupure, elle attend un tampon de récupération de huit trames avant de redémarrer. Dès le premier callback de 88 ms ou trois trames en attente, le raffinement passe de huit à deux étapes ; à 120 ms, six trames ou après une resynchronisation, il passe directement à une étape. Si la file atteint tout de même huit trames (640 ms), seul le minimum d’ancien audio nécessaire pour accepter la nouvelle entrée est abandonné. Une surcharge continue est traitée comme un seul épisode de récupération et un tour déjà confirmé par RNN-T reste armé au lieu d’être effacé par des réinitialisations répétées. Les mots sautés restent signalés et peuvent devoir être répétés.

Les métriques décrivent l’expérience : behind 0.3 s est l’audio micro en attente, RTF 0.93× est le temps de calcul divisé par le temps audio (sous 1 le système suit, au-dessus de 1 il prend du retard) et last 74 ms for 80 ms audio compare le dernier temps de calcul aux 80 ms d’audio représentées par une trame. La moyenne glissante couvre les 120 derniers callbacks micro ; les événements de rejeu ne comptent pas comme temps audio supplémentaire.

Le mode microphone utilise par défaut la gestion des tours RNN-T de NVIDIA. Les décisions BOS/EOS apprises par le modèle restent la voie normale à faible latence. La première prédiction de chaque trame de 80 ms est traitée comme blank ou non-blank ; après confirmation de la parole utilisateur, 40 trames blank consécutives (3,2 s) servent uniquement de repli de sécurité pour lancer la réponse, et 40 nouvelles trames non-blank fournissent le repli correspondant pour l’interruption. Le compteur vocal est remis à zéro lorsque Soniqo commence, afin que l’activité du tour utilisateur terminé ne puisse pas couper immédiatement la réponse. Toutes les trames audio continuent d’alimenter le modèle duplex ; --no-rnnt-turn-taking désactive ces replis et laisse BOS/EOS au seul head de langage appris. Le tableau de bord sépare les barge-ins RNN-T des interruptions de lecture.

En mode normal, le BOS natif du modèle est supprimé tant que la parole utilisateur n’est pas confirmée, afin que Soniqo ne parle pas en premier. --greet autorise explicitement le salut initial. Après le contenu audible, les PAD blank restent dans le tour ouvert pendant au moins 16 trames ou trois trames par token de contenu, selon la valeur la plus longue. Le premier PAD blank au-delà de ce budget ferme le tour logique. Cette traîne proportionnelle au contenu préserve les mots tardifs qu’une coupure fixe à 1,28 seconde peut rendre muets.

Le mode interactif utilise un tableau de bord fixe dans l’écran alternatif du terminal : les mises à jour se redessinent sur place sans remplir l’historique. Utilisez --plain pour une sortie cumulative.

Pour le diagnostic local, --debug-timeline horodate chaque phrase de l’utilisateur et de Soniqo, puis insère dans la même chronologie les arguments de l’appel natif analysé, le début/la fin MCP et la synchronisation du cache de résultat. Il marque aussi pronunciation ended à la dernière fenêtre PCM générée audible ; il s’agit du temps de sortie du modèle, pas de la fin de lecture du haut-parleur. La démo masque les anciens blocs de temps ; les mesures détaillées du décodage natif, du fournisseur, du cache et de la pression micro restent dans le JSON de voicechat-bench. Des arguments d’outil peuvent être exposés : ne l’utilisez pas dans des journaux partagés. Les compteurs de phase sont réinitialisés entre le décodage natif et l’attente du fournisseur.

Les sessions en direct conservent le prompt vocal immuable de 37 trames et 20 secondes d’historique EAR-TTS récent ; Nemotron-H conserve séparément le contexte sémantique. Les PAD de la traîne acoustique proportionnelle au contenu sont générés normalement ; seuls les PAD silencieux suivants avancent causalement par lots de huit. La réponse parlée n’est donc pas coupée et l’attention TTS ainsi que le travail au repos restent bornés. --live-speech-context-seconds 0 rétablit tout l’historique TTS et le mode fichier reste exact et complet par défaut.

Sur un profil de 63,6 secondes avec un M5 Pro et la traîne sûre, le chemin direct a mesuré un RTF global de 0,87, un RTF de 0,71 dans la fenêtre finale, 4,1 ms/trame de synthèse finale, 8,72 Go de RSS maximal et 23,67 Go d’empreinte physique maximale. Les fenêtres vocales actives à huit étapes ont atteint des RTF de 1,05 et 1,12 ; le CLI interactif conserve donc sa réduction réversible à deux étapes avec un mode d’urgence à une étape lorsque l’entrée s’accumule. La similarité cosinus minimale entre les états de cache au repos séquentiel et par lots était de 0,9999999.

Le détecteur de clic initial de voicechat-bench analyse l'amorce générée et les 50 premières ms de parole soutenue. Il ne signale que les sauts supérieurs à 0,05 d'amplitude et à six fois la base p99 de la parole stable ; un scénario peut fixer maximum_suspect_onset_transients à zéro.

Mesuré avec les sous-titres en direct

Trois exécutions Release sur M5 Pro 48 Go ont mesuré un RTF de 0,92, un p95 par trame totale de 74,8–75,8 ms, un premier audio lisible en 74,2–74,9 ms et environ 8,74 Go de RSS maximal. La transcription et la réponse étaient identiques.

Architecture et utilisation de PersonaPlex

PersonaPlex est un modèle autorégressif multi-flux avec trois composants principaux :

ComposantDétails
Temporal Transformer32 couches, dim=4096, 32 têtes, SwiGLU (hidden_scale=4.125), RoPE, quantifié 8 bits (par défaut)
Depformer6 couches, dim=1024, 16 têtes, MultiLinear (weights_per_step=true), dep_q=16
Mimi Codec16 codebooks, taux de trame 12,5 Hz, sortie audio 24 kHz

Le modèle traite 17 flux simultanément : 1 flux texte + 8 flux audio utilisateur + 8 flux audio agent. Cette architecture permet une conversation full-duplex où le modèle peut écouter et parler en même temps.

Préréglages de voix

PersonaPlex inclut 18 préréglages de voix intégrés dans des styles naturels et variés :

CatégoriePréréglages
Femme naturelleNATF0, NATF1, NATF2, NATF3
Homme naturelNATM0, NATM1, NATM2, NATM3
Femme variéeVARF0, VARF1, VARF2, VARF3, VARF4
Homme variéVARM0, VARM1, VARM2, VARM3, VARM4

Monologue intérieur

PersonaPlex génère deux flux parallèles à chaque étape : 8 tokens de codebook audio pour le codec Mimi et un token de texte pour le monologue intérieur du modèle. Le flux de texte est ce que le modèle « pense » pendant qu'il parle — il peut diverger légèrement de l'audio final, mais en pratique il reflète suffisamment la réponse parlée pour être utilisé comme transcription en direct.

Les tokens de texte reviennent sous forme d'IDs de pièces SentencePiece bruts. Décodez-les avec le SentencePieceDecoder fourni par 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 mode streaming, respondStream émet des chunks textTokens à mesure qu'ils sont produits — décodez-les de façon incrémentale pour piloter une vue de sous-titres en direct pendant que l'audio est encore en génération. L'option CLI --transcript fait exactement cela en coulisses.

Pourquoi c'est important : SentencePieceDecoder est construit sur le lecteur protobuf partagé AudioCommon.SentencePieceModel, donc PersonaPlex, OmnilingualASR et tout futur modèle basé sur SentencePiece décodent via la même implémentation de tokenizer. Voir la référence SentencePieceModel.

Prompts système

Passez n'importe quel prompt système personnalisé sous forme de chaîne simple — aucune tokenisation externe n'est nécessaire :

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

Ou utilisez un préréglage intégré :

Utilisation en CLI

Générer une réponse parlée à partir d'une entrée 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

Options

OptionDescription
--inputFichier audio d'entrée (WAV, requis)
--voiceNom de préréglage de voix (ex. NATM0, VARF2)
--system-promptPréréglage de prompt système : assistant, focused, customer-service, teacher
--system-prompt-textTexte de prompt système personnalisé (prime sur --system-prompt)
--max-stepsÉtapes de génération maximum
--streamÉmettre des chunks audio pendant la génération
--compileUtiliser l'inférence compilée MLX pour une génération plus rapide
--transcriptAfficher la transcription textuelle avec l'audio
--jsonSortie JSON avec métadonnées

Les paramètres d'échantillonnage peuvent également être surchargés :

OptionPar défautDescription
--audio-temp0,8Température d'échantillonnage des tokens audio
--audio-top-k250Échantillonnage top-k des tokens audio
--text-temp0,7Température d'échantillonnage des tokens texte
--text-top-k25Échantillonnage top-k des tokens texte

Streaming

L'option --stream active la sortie audio en temps réel. Les chunks audio sont émis à mesure qu'ils sont générés, donc la lecture peut commencer avant que la réponse complète ne soit terminée. C'est particulièrement utile pour les applications interactives où une faible latence compte.

Performance

MétriqueValeur
Facteur temps réel (RTF)~1,4 (8 bits, proche du temps réel)
Latence par étape~112 ms/étape sur M2 Max (8 bits)
Taille du modèle (8 bits)~9,1 Go
RAM en pic (8 bits)~11 Go
Taille du modèle (4 bits)~4,9 Go
RAM en pic (4 bits)~7 Go
Important

PersonaPlex 7B (8 bits) requiert au moins 24 Go de RAM. La variante 4 bits tient sur des appareils 16 Go mais produit une sortie dégradée. Sur des appareils 8 Go, aucune variante ne tiendra. Utilisez --compile pour de meilleures performances sur le matériel pris en charge.

Variantes du modèle

ModèleTailleHuggingFace
PersonaPlex-7B (8 bits) recommandé9,1 Goaufklarer/PersonaPlex-7B-MLX-8bit
PersonaPlex-7B (4 bits)4,9 Goaufklarer/PersonaPlex-7B-MLX-4bit

API 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"))