Magpie-TTS Multilingual
NVIDIA Magpie-TTS Multilingual 357M en Swift sur Apple Silicon — un modèle TTS auto-régressif multi-codebook sur le Nano-Codec 22.05 kHz de NeMo. Neuf langues (anglais, espagnol, allemand, français, italien, vietnamien, hindi, mandarin, japonais) avec cinq identités de voix prédéfinies. Quantifié en INT4 (~247 Mo) ou INT8 (~411 Mo). Streaming, latence du premier paquet ~120 ms.
Magpie est le bon choix quand tu as besoin d'un seul petit bundle qui parle neuf langues avec une voix cohérente. Les cinq voix prédéfinies restent stables en identité d'une langue à l'autre — utile pour les assistants multilingues, les apps éducatives ou la narration de livres audio avec code-switching. Pour le clonage zero-shot utilise CosyVoice3, Qwen3-TTS Base ou VoxCPM2.
Architecture
Magpie est un pipeline MLX à 4 bundles : encodeur de texte → décodeur avec cross-attention → tête codebook LocalTransformer → codec audio Causal HiFi-GAN. Les bundles partagent les poids du décodeur entre les points d'entrée prefill et step pour rester compatibles avec le layout CoreML de FluidInference upstream.
| Étape | Module | Détails |
|---|---|---|
| 1. Tokenisation | MagpieTokenizer | G2P par langue (dict IPA / bytes byT5 / pinyin / katakana), vocab partagé 2360 tokens + offsets par tokenizer, EOS toujours ajouté |
| 2. Encodeur de texte | MagpieTextEncoder | 6 couches Transformer causales, d=768, FFN conv k=3 |
| 3. Decoder prefill | MagpieDecoder | 12 couches causales avec cross-attention. Amorce le cache KV avec le contexte 110 frames de la voix prédéfinie + BOS. |
| 4. LocalTransformer | MagpieLocalTransformer | Tête AR codebook 1 couche, d=256. Échantillonne les 8 codebooks par frame séquentiellement à partir du hidden décodeur. |
| 5. Decoder step | MagpieDecoder | Un pas AR par frame jusqu'à EOS ou la limite de 500 frames (~23 s). |
| 6. NanoCodec | MagpieNanoCodec | Inverse FSQ → HiFi-GAN causal → forme d'onde mono 22.05 kHz. |
Langues et G2P
Les neuf langues font l'aller-retour à travers Qwen3-ASR dans le testMultilingualRoundTrip du SDK. Chacune a un pipeline adapté :
| Langue | Code | Pipeline G2P |
|---|---|---|
| Anglais | en | Dictionnaire CMU IPA (125k entrées, embarqué) |
| Espagnol | es | Dictionnaire IPA espagnol (embarqué) |
| Allemand | de | Dictionnaire IPA allemand (embarqué) |
| Français | fr | Encodeur de bytes UTF-8 byT5 |
| Italien | it | Encodeur de bytes UTF-8 byT5 |
| Vietnamien | vi | Encodeur de bytes UTF-8 byT5 |
| Hindi | hi | Lookup de codepoint devanagari + sous-vocab last-wins |
| Mandarin | zh | Segmentation de mots NLTokenizer(.simplifiedChinese) + Apple .mandarinToLatin + dictionnaire pinyin → IPA embarqué + marqueurs de ton #N |
| Japonais | ja | Lecture kanji via CFStringTokenizer + dakuten préservé par NFC + marqueurs de pitch heiban + overrides de particules/salutations |
Le vocab partagé 2360 tokens concatène les sous-tokenizers de chaque langue avec un offset par langue (enregistré dans MagpieSubVocab). Le text-embedding ajoute deux lignes au-delà du vocab pour BOS/EOS ; eos_id = 2361 est ajouté à chaque séquence d'entrée.
Voix prédéfinies
Le checkpoint embarque cinq contextes de voix (110 frames × 768 dims chacun) utilisés en préfixe de chaque décodage AR. L'identité de la voix est cohérente sur les neuf langues.
| Index | Nom CLI | Identité |
|---|---|---|
| 0 | sofia | Sofia (par défaut) |
| 1 | aria | Aria |
| 2 | jason | Jason |
| 3 | leo | Leo |
| 4 | john | John Van Stan |
Variantes du modèle
| Variante | Disque | RAM (chargement + decode) | HuggingFace |
|---|---|---|---|
| INT4 (par défaut) | ~247 Mo | ~1.3 Go | aufklarer/Magpie-TTS-Multilingual-357M-MLX-4bit |
| INT8 | ~411 Mo | ~1.6 Go | aufklarer/Magpie-TTS-Multilingual-357M-MLX-8bit |
Les deux bundles utilisent la quantisation flat affine MLX (mlx_affine_flat, group size 64) et sont déquantifiés en FP32 au chargement — les activations runtime sont en pleine précision. INT4 et INT8 sont indistinguables à l'oreille pour ce modèle ; choisis INT4 sauf si tu as du stockage à revendre.
Utilisation CLI
# Anglais, décodage greedy
speech speak "Hello, world." --engine magpie --magpie-speaker aria \
--magpie-temperature 0 -o out.wav
# Espagnol (l'une des 9 langues — choisi avec --language)
speech speak "Hola, mundo." --engine magpie --language es \
--magpie-speaker aria -o out.wav
# Japonais — décodage stochastique requis (greedy se bloque sur la première phrase)
speech speak "こんにちは世界、これは音声合成システムです。" \
--engine magpie --language ja --magpie-temperature 0.6 \
--magpie-top-k 80 --seed 42 -o out.wav
# Synthèse en streaming avec lecture
speech speak "Streaming test" --engine magpie --stream --play
# Lister les 5 voix prédéfinies
speech speak --engine magpie --list-speakers
# IPA pré-phonémisé contourne le G2P par langue
speech speak "həˈloʊ" --engine magpie --magpie-prephonemized -o out.wav
Options
| Option | Défaut | Description |
|---|---|---|
--magpie-variant | int4 | Variante de quantisation : int4 ou int8 |
--magpie-speaker | sofia | Voix prédéfinie : sofia, aria, jason, leo, john |
--magpie-temperature | 0.6 | Température d'échantillonnage (0 = greedy) |
--magpie-top-k | 80 | Filtre top-k pour l'échantillonnage |
--magpie-max-frames | 500 | Plafond strict des frames codec (~23 s) |
--magpie-min-frames | 4 | Frames minimum avant que l'EOS soit autorisé |
--magpie-prephonemized | off | Traite l'entrée comme flux IPA/phonème ; contourne le G2P par langue |
--language | english | Sélectionne le tokenizer par langue |
--stream | off | Émet AsyncStream<AudioChunk> au lieu d'un seul WAV |
--seed | — | Échantillonnage Gumbel reproductible |
Les entrées japonaises plus longues qu'un mot nécessitent un décodage stochastique (--magpie-temperature 0.6 --magpie-top-k 80 --seed 42 reflète le test de référence NeMo). Greedy se bloque sur la première phrase parce que l'heuristique de pitch heiban dévie de la vérité mot à mot.
Clonage vocal — non pris en charge
Magpie n'a pas de conditionnement zero-shot du locuteur dans le modèle ; seules les 5 identités prédéfinies sont incluses. La CLI rejette les flags partagés --voice-sample, --speaker et --instruct avec une erreur actionnable qui pointe vers --magpie-speaker ou les engines qui supportent le clonage (Qwen3-TTS Base, CosyVoice3, VoxCPM2).
Performance (M4 Pro)
| Configuration | Audio | Temps réel | RTF |
|---|---|---|---|
| Batch, INT4, greedy, prompt court | 2.8 s | 0.88 s | 0.32 |
| Batch, INT4, greedy, phrase | 5.8 s | 1.35 s | 0.23 |
| Batch, INT4, sampled, sortie 23 s | 23 s | 5.6 s | 0.24 |
| Streaming, INT4, sampled | 23 s | 21.6 s | 0.93 |
La latence du premier paquet en mode streaming est ≈120 ms après chargement du modèle. Le RTF de streaming est plus élevé parce que le codec est re-invoqué sur le buffer de codes complet à chaque chunk émis (une révision future peut cacher l'état du codec).
API Swift
import MagpieTTS
let model = try await MagpieTTS.fromPretrained(variant: .int4)
// Synthèse batch (en/es/de/fr/it/vi/hi/zh — greedy fonctionne)
let audio = try model.synthesize(
text: "Hello, world.",
speaker: .aria,
language: .english,
params: MagpieTTSParams(temperature: 0, topK: 1, maxSteps: 500))
// Japonais — utiliser l'échantillonnage stochastique
let audioJA = try model.synthesize(
text: "こんにちは世界、これは音声合成システムです。",
speaker: .aria,
language: .japanese,
params: MagpieTTSParams(temperature: 0.6, topK: 80,
maxSteps: 300, seed: 42))
// Streaming (AsyncStream<AudioChunk>)
let stream = model.synthesizeStream(
text: "Streaming text",
speaker: .aria,
language: .english,
firstChunkFrames: 8,
framesPerChunk: 25)
for try await chunk in stream {
// chunk.samples est du Float32 mono 22.05 kHz
}
Backend CoreML (--engine magpie-coreml)
En plus du bundle MLX, Magpie livre un bundle CoreML (aufklarer/Magpie-TTS-Multilingual-357M-CoreML-8bit, ~342 Mo INT8). Quatre paquets .mlmodelc — text_encoder, decoder_prefill, decoder_step, nanocodec_decoder — tournent sur ANE / GPU ; un inverse FSQ côté Swift transforme les codes échantillonnés en latents 32 dimensions que le codec consomme.
# 8 langues (sans japonais), 5 voix prédéfinies
speech speak "Hello world." --engine magpie-coreml --magpie-speaker aria -o hi.wav
speech speak "Hola mundo." --engine magpie-coreml --language es --magpie-speaker leo -o es.wav
# --language ja redirige automatiquement vers le backend MLX (note stderr)
speech speak "こんにちは" --engine magpie-coreml --language ja -o ja.wav
Réserves par rapport à --engine magpie :
- Pipeline hybride aujourd'hui. Le LocalTransformer 1 couche (la vraie tête d'échantillonnage de codebooks entraînée par NeMo) et les 8 tables d'audio embeddings ne sont pas livrés dans le bundle CoreML. À la première synthèse, le moteur CoreML charge le bundle MLX INT4 pour piloter les deux. L'aller-retour ASR est identique bit pour bit au backend MLX ; la différence est que ce moteur télécharge aussi le bundle MLX. Un chemin CoreML pur pour un déploiement iOS uniquement ANE nécessite que le bundle livre
local_transformer/*.npy+audio_embedding_*.npyet un LT en Swift Accelerate (suivi prévu). - Pas de streaming.
nanocodec_decoder.mlmodelcest tracé à une fenêtre fixe de 64 frames. Les séquences plus longues sont fragmentées en interne, mais la latence du premier paquet serait ~3 s si nous émettions aux frontières de fragment.--streamest rejeté avec une erreur actionnable. - Pas de tokenizer japonais. Le bundle CoreML ne livre pas encore les JSONs du tokenizer JA.
--language jaavec ce moteur retombe automatiquement sur le backend MLX.
L'ordre des voix correspond au speaker_info.json du bundle CoreML (0=John, 1=Sofia, 2=Aria, 3=Jason, 4=Leo — différent de MLX) ; l'enum de voix mappe en interne pour que les noms CLI fonctionnent pour les deux moteurs.
Notes d'implémentation
Trois bugs à connaître lors du portage de TTS multilingue style NeMo :
- Division entière FSQ — Le
/de MLX-swift est une division réelle (mlx_divide) ; l'inverse FSQ de NeMo utilise//de Python. UtiliseMLX.floorDivide(...), sinon chaque slot FSQ décode avec des offsets fractionnaires et le codec floute l'audio. - Offsets de sous-vocab — L'
AggregatedTTSTokenizerde NeMo concatène les vocabulaires par langue avec des offsets. Une map globale naïve de first-occurrence atterrit toujours dans la région anglaise et produit de l'audio absurde pour les autres langues. - Hindi last-wins dedup —
HindiCharsTokenizerémet des entrées devanagari dupliquées dans son propre vocab (CHARSET chevauche PUNCT_LIST). La dict-comprehension Python{l: i for i, l in enumerate(tokens)}garde la dernière affectation ; reflète ça, pas first-occurrence.
Les trois corrections sont documentées inline dans le module Swift.
Source
- Poids upstream : nvidia/magpie_tts_multilingual_357m (NVIDIA Open Model License)
- Codec : nvidia/nemo-nano-codec-22khz-1.89kbps-21.5fps
- Paper : NanoCodec: Towards High-Quality Ultra Fast Speech LLM Inference (2025)
- Port CoreML de référence : FluidInference/mobius
- Modules Swift : MagpieTTS (MLX) + MagpieTTSCoreML (CoreML)
- Bundle CoreML : aufklarer/Magpie-TTS-Multilingual-357M-CoreML-8bit
Licence
- Poids du modèle : NVIDIA Open Model License (usage commercial autorisé ; voir la page HuggingFace)
- Port Swift + dictionnaires IPA/pinyin embarqués : identique à NeMo upstream (dictionnaires Apache 2.0, modèle NVIDIA OML)