Magpie-TTS Multilingual
NVIDIA Magpie-TTS Multilingual 357M em Swift no Apple Silicon — um modelo TTS autorregressivo multi-codebook sobre o Nano-Codec de 22.05 kHz da NeMo. Nove idiomas (inglês, espanhol, alemão, francês, italiano, vietnamita, hindi, mandarim, japonês) com cinco identidades de orador predefinidas. Quantizado em INT4 (~247 MB) ou INT8 (~411 MB). Pronto para streaming, latência do primeiro pacote ~120 ms.
Magpie é a escolha certa quando precisas de um único bundle pequeno que fale nove idiomas com uma voz consistente. Os cinco oradores predefinidos mantêm a identidade estável entre idiomas — útil para assistentes multilíngues, apps educacionais ou narração de audiolivros com alternância de código. Para clonagem zero-shot usa CosyVoice3, Qwen3-TTS Base ou VoxCPM2.
Arquitetura
Magpie é uma pipeline MLX de 4 bundles: codificador de texto → decodificador com cross-attention → cabeça codebook LocalTransformer → codec de áudio Causal HiFi-GAN. Os bundles partilham pesos do decodificador entre os pontos de entrada prefill e step para manter compatibilidade com o layout CoreML do FluidInference upstream.
| Etapa | Módulo | Detalhes |
|---|---|---|
| 1. Tokenização | MagpieTokenizer | G2P por idioma (dicionário IPA / bytes byT5 / pinyin / katakana), vocabulário partilhado de 2360 tokens + offsets por tokenizador, EOS sempre anexado |
| 2. Codificador de texto | MagpieTextEncoder | 6 camadas Transformer causais, d=768, FFN conv k=3 |
| 3. Decoder prefill | MagpieDecoder | 12 camadas causais com cross-attention. Semeia a cache KV com o contexto de 110 frames do orador predefinido + BOS. |
| 4. LocalTransformer | MagpieLocalTransformer | Cabeça AR de codebook de 1 camada, d=256. Amostra os 8 codebooks por frame sequencialmente a partir do hidden do decodificador. |
| 5. Decoder step | MagpieDecoder | Um passo AR por frame até EOS ou o limite de 500 frames (~23 s). |
| 6. NanoCodec | MagpieNanoCodec | Inversa FSQ → HiFi-GAN causal → forma de onda mono a 22.05 kHz. |
Idiomas e G2P
Os nove idiomas fazem ida-e-volta através do Qwen3-ASR no testMultilingualRoundTrip do SDK. Cada um tem uma pipeline adaptada:
| Idioma | Código | Pipeline G2P |
|---|---|---|
| Inglês | en | Dicionário CMU IPA (125k entradas, embutido) |
| Espanhol | es | Dicionário IPA espanhol (embutido) |
| Alemão | de | Dicionário IPA alemão (embutido) |
| Francês | fr | Codificador de bytes UTF-8 byT5 |
| Italiano | it | Codificador de bytes UTF-8 byT5 |
| Vietnamita | vi | Codificador de bytes UTF-8 byT5 |
| Hindi | hi | Lookup de codepoint devanagari + sub-vocab last-wins |
| Mandarim | zh | Segmentação de palavras NLTokenizer(.simplifiedChinese) + Apple .mandarinToLatin + dicionário pinyin → IPA embutido + marcadores de tom #N |
| Japonês | ja | Leitura kanji via CFStringTokenizer + dakuten preservado por NFC + marcadores de pitch heiban + overrides de partículas/saudações |
O vocab partilhado de 2360 tokens concatena os sub-tokenizadores de cada idioma com um offset por idioma (registado em MagpieSubVocab). O text-embedding adiciona duas linhas extra após o vocab para BOS/EOS; eos_id = 2361 é anexado a cada sequência de entrada.
Oradores predefinidos
O checkpoint embute cinco contextos de orador (110 frames × 768 dims cada) usados como prefixo de cada decodificação AR. A identidade do orador é consistente nos nove idiomas.
| Índice | Nome CLI | Identidade |
|---|---|---|
| 0 | sofia | Sofia (padrão) |
| 1 | aria | Aria |
| 2 | jason | Jason |
| 3 | leo | Leo |
| 4 | john | John Van Stan |
Variantes do modelo
| Variante | Disco | RAM (carregamento + decode) | HuggingFace |
|---|---|---|---|
| INT4 (padrão) | ~247 MB | ~1.3 GB | aufklarer/Magpie-TTS-Multilingual-357M-MLX-4bit |
| INT8 | ~411 MB | ~1.6 GB | aufklarer/Magpie-TTS-Multilingual-357M-MLX-8bit |
Ambos os bundles usam quantização flat affine MLX (mlx_affine_flat, group size 64) e são dequantizados para FP32 no carregamento — as activações em runtime são de precisão total. INT4 e INT8 são indistinguíveis ao ouvido para este modelo; escolhe INT4 a menos que tenhas armazenamento de sobra.
Uso CLI
# Inglês, decodificação greedy
speech speak "Hello, world." --engine magpie --magpie-speaker aria \
--magpie-temperature 0 -o out.wav
# Espanhol (qualquer um dos 9 idiomas — escolhe com --language)
speech speak "Hola, mundo." --engine magpie --language es \
--magpie-speaker aria -o out.wav
# Japonês — precisa de decodificação estocástica (greedy fica preso na primeira frase)
speech speak "こんにちは世界、これは音声合成システムです。" \
--engine magpie --language ja --magpie-temperature 0.6 \
--magpie-top-k 80 --seed 42 -o out.wav
# Síntese streaming com reprodução
speech speak "Streaming test" --engine magpie --stream --play
# Listar os 5 oradores predefinidos
speech speak --engine magpie --list-speakers
# IPA pré-fonemizado ignora o G2P por idioma
speech speak "həˈloʊ" --engine magpie --magpie-prephonemized -o out.wav
Opções
| Opção | Padrão | Descrição |
|---|---|---|
--magpie-variant | int4 | Variante de quantização: int4 ou int8 |
--magpie-speaker | sofia | Orador predefinido: sofia, aria, jason, leo, john |
--magpie-temperature | 0.6 | Temperatura de amostragem (0 = greedy) |
--magpie-top-k | 80 | Filtro top-k para amostragem |
--magpie-max-frames | 500 | Limite rígido de frames do codec (~23 s) |
--magpie-min-frames | 4 | Frames mínimos antes de permitir EOS |
--magpie-prephonemized | off | Trata a entrada como fluxo IPA/fonema; ignora o G2P por idioma |
--language | english | Seleciona o tokenizador por idioma |
--stream | off | Emite AsyncStream<AudioChunk> em vez de um único WAV |
--seed | — | Amostragem Gumbel reproduzível |
Entradas japonesas com mais de uma palavra precisam de decodificação estocástica (--magpie-temperature 0.6 --magpie-top-k 80 --seed 42 espelha o teste de referência da NeMo). Greedy fica preso na primeira frase porque a heurística de pitch heiban desvia-se da verdade por palavra.
Clonagem de voz — não suportada
Magpie não tem condicionamento zero-shot de orador no modelo; apenas as 5 identidades predefinidas estão incluídas no bundle. A CLI rejeita os flags partilhados --voice-sample, --speaker e --instruct com um erro acionável que aponta para --magpie-speaker ou para os engines que suportam clonagem (Qwen3-TTS Base, CosyVoice3, VoxCPM2).
Desempenho (M4 Pro)
| Configuração | Áudio | Tempo real | RTF |
|---|---|---|---|
| Batch, INT4, greedy, prompt curto | 2.8 s | 0.88 s | 0.32 |
| Batch, INT4, greedy, frase | 5.8 s | 1.35 s | 0.23 |
| Batch, INT4, sampled, saída 23 s | 23 s | 5.6 s | 0.24 |
| Streaming, INT4, sampled | 23 s | 21.6 s | 0.93 |
A latência do primeiro pacote em streaming é ≈120 ms após carregar o modelo. O RTF de streaming é maior porque o codec é re-invocado sobre o buffer completo de códigos a cada chunk emitido (uma revisão futura pode cachear o estado do codec).
API Swift
import MagpieTTS
let model = try await MagpieTTS.fromPretrained(variant: .int4)
// Síntese batch (en/es/de/fr/it/vi/hi/zh — greedy funciona)
let audio = try model.synthesize(
text: "Hello, world.",
speaker: .aria,
language: .english,
params: MagpieTTSParams(temperature: 0, topK: 1, maxSteps: 500))
// Japonês — usar amostragem estocástica
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 é Float32 mono a 22.05 kHz
}
Backend CoreML (--engine magpie-coreml)
Junto com o bundle MLX, o Magpie envia um bundle CoreML (aufklarer/Magpie-TTS-Multilingual-357M-CoreML-8bit, ~342 MB INT8). Quatro pacotes .mlmodelc — text_encoder, decoder_prefill, decoder_step, nanocodec_decoder — rodam em ANE / GPU; uma FSQ inversa em Swift transforma os códigos amostrados nos latentes de 32 dimensões que o codec consome.
# 8 idiomas (sem japonês), 5 oradores predefinidos
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 roteia automaticamente para o backend MLX (aviso em stderr)
speech speak "こんにちは" --engine magpie-coreml --language ja -o ja.wav
Ressalvas vs. --engine magpie:
- Pipeline híbrida hoje. O LocalTransformer de 1 camada (a cabeça real de amostragem de codebooks que o NeMo treina) e as 8 tabelas de audio embeddings não estão incluídas no bundle CoreML. Na primeira síntese, o engine CoreML carrega o bundle MLX INT4 para conduzir ambos. O round-trip ASR é idêntico bit a bit ao backend MLX; a diferença é que este engine também baixa o bundle MLX. Um caminho CoreML puro para deploy iOS apenas em ANE precisa que o bundle envie
local_transformer/*.npy+audio_embedding_*.npye um LT em Swift Accelerate (acompanhamento pendente). - Sem streaming.
nanocodec_decoder.mlmodelcestá traçado com janela fixa de 64 frames. Sequências mais longas são fragmentadas internamente, mas a latência do primeiro pacote seria ~3 s se emitíssemos nos limites de fragmento.--streamé rejeitado com erro acionável. - Sem tokenizer japonês. O bundle CoreML ainda não envia JSONs do tokenizer JA.
--language jacom este engine cai automaticamente para o backend MLX.
A ordem dos oradores corresponde ao speaker_info.json do bundle CoreML (0=John, 1=Sofia, 2=Aria, 3=Jason, 4=Leo — diferente do MLX); o enum de oradores mapeia internamente para que os nomes CLI funcionem nos dois engines.
Notas de implementação
Três bugs a conhecer ao portar TTS multilíngue estilo NeMo:
- Divisão inteira FSQ — O
/do MLX-swift é divisão real (mlx_divide); a inversa FSQ da NeMo usa//do Python. UsaMLX.floorDivide(...)ou cada slot FSQ decodifica para offsets fracionários e o codec borra o áudio. - Offsets de sub-vocab — O
AggregatedTTSTokenizerda NeMo concatena os vocabulários por idioma com offsets. Um mapa global ingénuo de first-occurrence cai sempre na região inglesa e produz áudio sem sentido para outros idiomas. - Hindi last-wins dedup —
HindiCharsTokenizeremite entradas devanagari duplicadas no seu próprio vocab (CHARSET sobrepõe-se a PUNCT_LIST). A dict-comprehension Python{l: i for i, l in enumerate(tokens)}mantém a última atribuição; espelha isso, não uses first-occurrence.
As três correções estão documentadas inline no módulo Swift.
Fonte
- Pesos 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 referência: FluidInference/mobius
- Módulos Swift: MagpieTTS (MLX) + MagpieTTSCoreML (CoreML)
- Bundle CoreML: aufklarer/Magpie-TTS-Multilingual-357M-CoreML-8bit
Licença
- Pesos do modelo: NVIDIA Open Model License (uso comercial permitido; ver detalhes na página HuggingFace)
- Port Swift + dicionários IPA/pinyin embutidos: mesmo da NeMo upstream (dicionários Apache 2.0, modelo NVIDIA OML)