GPT-SoVITS adaptado para português do Brasil — modelo base
Estudo de caso sobre como adicionar um idioma novo ao GPT-SoVITS. O upstream suporta chinês, cantonês, inglês, japonês e coreano — não português. Este repo guarda os checkpoints de uma adaptação para pt-BR, versão v2Pro.
⚠️ Trabalho em andamento e experimental. Os checkpoints aqui são de um treino em curso, publicados conforme saem. Não espere qualidade de produção. A intenção é que isto vire uma base pt-BR, sobre a qual se faz fine-tune de uma voz específica depois.
O problema central: a tabela de símbolos é indexada por posição
No GPT-SoVITS o ID de cada fonema é a posição dele na lista de símbolos
({s: i for i, s in enumerate(symbols)}). A lista da v2 tem 732 entradas e é
construída com sorted(). Então inserir um símbolo novo no meio desloca todos os
IDs seguintes e invalida o embedding de texto pré-treinado — sem lançar erro
nenhum: o modelo simplesmente passa a falar embolado.
O upstream já tinha resolvido isso ao adicionar coreano e cantonês: os símbolos
novos são anexados depois do sorted(), nunca intercalados. Esta adaptação
segue a mesma regra.
O que foi feito
| Item | Escolha |
|---|---|
| G2P | espeak-ng, voz pt-br, chamado por subprocesso |
| Símbolos pt-BR | 88, anexados nos IDs 732–819 |
| Prefixo | P em todo símbolo pt |
| Embedding de texto | estendido de 732 → 820 linhas |
| Corpus | TTS-Portuguese Corpus (CC BY 4.0) |
| Versão | v2Pro (GPT s1v3 + SoVITS s2Gv2Pro) |
Por que espeak-ng e não gruut
O espeak tem voz pt-br de verdade (a do gruut é lusitana) e resolve palavra fora
de dicionário por regra — o que funciona bem porque a ortografia do português é
regular.
Por que os ditongos são separados
O espeak emite ditongos fundidos (ˈɐ̃ʊ̃). Aqui eles são quebrados em núcleo +
glide (ˈɐ̃ + ʊ̃). Isso quase divide o inventário pela metade e faz cada símbolo
receber muito mais ocorrências no treino — o que decide o resultado quando os dados
são poucos.
Por que todo símbolo leva prefixo P
Sem prefixo, 22 dos fonemas pt (a, b, k, t, …) colidiriam com as iniciais de
pinyin e o romaji japonês que já estão na tabela. Como o ID vem de enumerate(), a
chave duplicada passaria a apontar para a última posição e roubaria
silenciosamente o ID de um símbolo já treinado. O cantonês do upstream prefixou tudo
com Y exatamente por isso.
Por que fonema estrangeiro é mapeado, não adicionado
Em palavra estrangeira o espeak troca de idioma e emite fonemas de fora do
português: θ (Python), h (Hitler), ɑ (Darwin), c (Isaac). Esses não
entram no inventário — virariam linhas de embedding que nenhuma palavra portuguesa
treina, e sairiam como ruído na inferência. Cada um é mapeado para o vizinho
português, que por acaso costuma ser como um brasileiro lê mesmo (Hitler → x, o
"r" forte).
Dados
TTS-Portuguese Corpus (Casanova et al.), CC BY 4.0 — um falante, 10,48 h brutas medidas.
Preparo: corte de 2–20 s, reamostragem para 32 kHz mono. Sobram 8,38 h em 3192 clipes, com 0 UNK e 0 falhas de G2P.
Três áudios foram descartados por aparecerem duas vezes nos metadados do corpus com transcrições diferentes — o mesmo áudio com dois textos ensina um mapeamento contraditório.
Plano de treino
Fase 1 (esta) — só o TTS-Portuguese. GPT treinado pesado, SoVITS de leve.
O GPT (s1) mapeia fonemas → tokens semânticos: é onde mora pronúncia, ritmo e
entonação, e é o que de fato precisa aprender o português. O SoVITS (s2) converte
tokens semânticos + referência → forma de onda: é onde mora o timbre. Treinar o
SoVITS pesado num corpus de um falante só assaria aquele timbre e o ruído do
corpus no modelo acústico — justo o que precisa ficar neutro para o fine-tune
posterior funcionar com outra voz.
Fase 2 (planejada) — somar um corpus multi-falante (CML-TTS português) para a base ficar neutra em timbre e gênero.
Resultado da fase 1
Treinado em 2026-08-13 numa RTX 4070 (12 GB). SoVITS: 6 épocas, ~25 min. GPT: 20 épocas, ~21 min (532 passos/época, batch 6).
Arquivos aqui: gpt/ptbr_base-eN.ckpt (épocas 2 a 20, de 2 em 2) e
sovits/ptbr_base_eN_sM.pth (épocas 4 e 6).
As linhas pt-BR aprenderam?
Deslocamento do embedding em relação à base estendida, em % do desvio do próprio tensor (as escalas dos dois modelos são muito diferentes, então o valor absoluto não é comparável):
| Modelo | época | 732 antigas | 88 pt | razão |
|---|---|---|---|---|
| SoVITS | 6 | 0,2% | 4,1% | 17,1x |
| GPT | 2 | 2,0% | 8,3% | 4,2x |
| GPT | 20 | 6,7% | 18,2% | 2,7x |
O SoVITS saiu como planejado: as linhas pt aprenderam e as antigas ficaram
praticamente congeladas. Efeito do treino leve com text_low_lr_rate = 1.0, que
mira o LR só nos parâmetros de texto.
O GPT reescreve o pré-treino. As 732 linhas antigas andaram 6,7%, porque o s1
não tem equivalente do text_low_lr_rate — o modelo inteiro treina com LR cheio.
A razão estabiliza em 2,7x a partir da época 12, ou seja, é deriva que assenta e
não esquecimento descontrolado. Ainda assim: este GPT fica pior em
zh/en/ja/ko/yue. Para uma base pt-BR o custo é aceitável; para uso multilíngue,
não.
Experimento: init normal contra warm
As 88 linhas novas do embedding podem começar de duas formas: sorteadas de uma
normal com a média e o desvio do próprio tensor (normal), ou herdando o vetor do
fonema ARPAbet mais próximo já treinado (warm).
Comparação controlada — mesmos dados, mesma seed, mesmo batch, 20 épocas, e as 732 linhas antigas bit a bit idênticas nos dois pontos de partida:
| época | top_3_acc normal | warm | ganho |
|---|---|---|---|
| 0 | 0,16843 | 0,21763 | +29,2% |
| 1 | 0,25449 | 0,27412 | +7,7% |
| 2 | 0,28576 | 0,29454 | +3,1% |
| 19 | 0,55436 | 0,56039 | +1,1% |
Em tempo: o warm adianta +0,42 época, número ao qual a acurácia e a loss chegam de forma independente.
Conclusão: o warm dá uma partida bem melhor, mas a vantagem é absorvida rápido e acaba modesta. Vale como padrão porque é de graça; importaria de verdade em treino curto. Ressalva: uma seed por braço, sem intervalo de confiança.
Os pesos dos dois braços estão aqui: gpt/ptbr_base-* (normal) e gpt/ptbr_warm-*
(warm).
O que ainda não foi medido
Não há avaliação perceptual (MOS, WER de ASR reversa) nem comparação entre
--init normal e --init warm no embedding. As amostras geradas terminam por EOS
dentro do limite de tokens e têm duração compatível com o texto, mas isso é sanidade
de pipeline, não qualidade de voz.
Como usar
Os pesos são para o GPT-SoVITS v2Pro,
e exigem o frontend pt-BR (o symbols2.py com os 88 símbolos e o
text/portuguese.py). Com o symbols2.py original de 732 símbolos eles não
funcionam: os IDs ≥732 estouram o embedding.
Selecione o idioma Português na interface de inferência.
Licenças e créditos
- Código GPT-SoVITS: MIT, © 2024 RVC-Boss.
- TTS-Portuguese Corpus: CC BY 4.0, Edresson Casanova et al.
- Estes checkpoints: CC BY 4.0, herdando a exigência de atribuição do corpus.
Se você usar isto, cite o corpus:
@article{casanova2022tts,
title = {TTS-Portuguese Corpus: a corpus for speech synthesis in Brazilian Portuguese},
author = {Casanova, Edresson and Junior, Arnaldo Candido and Shulby, Christopher
and de Oliveira, Frederico Santos and Teixeira, João Paulo and
Ponti, Moacir Antonelli and Aluisio, Sandra},
journal = {Language Resources and Evaluation},
year = {2022}
}