# /tts — voz do Google Cloud Text-to-Speech no servidor local

Lido e medido em 2026-09-25. O que a documentação diz vem com o link; o que eu medi vem marcado como **medido**.

## O contrato

`POST /tts`, JSON, mesma origem da página:

```
{ "texto": "café" }                               ou
{ "ssml": "<speak>…</speak>", "voz": "pt-BR-Neural2-B", "velocidade": 0.9 }
```

- Mande **ou** `texto` **ou** `ssml`, nunca os dois. `voz` é opcional (padrão abaixo). `velocidade` é opcional,
  de 0,25 a 2,0 (é o `speakingRate`, [referência do AudioConfig](https://docs.cloud.google.com/text-to-speech/docs/reference/rest/v1/AudioConfig)).
- Volta `audio/mpeg` (MP3, 24 kHz, mono). O cabeçalho `X-TTS-Cache: hit|miss` diz se veio do disco.
- Erro volta JSON `{ok:false, erro}`: 400 para entrada inválida, 502 quando o Google recusa (voz que não existe,
  SSML malformado). A página cai no modo sem som.
- Limite de 5.000 bytes por pedido, que é o do Google ([cotas](https://docs.cloud.google.com/text-to-speech/quotas)).

Na página:

```js
const r = await fetch('/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ssml }) });
if (r.ok) new Audio(URL.createObjectURL(await r.blob())).play();
```

**Cache.** O arquivo é `cache-tts/<sha256 de [tipo, texto ou ssml, voz, velocidade]>.mp3`. Uma fala igual nunca é
gerada duas vezes, nem quando dois pedidos iguais chegam juntos (há uma trava por hash).

**A chave.** É lida a cada chamada de `~/.config/xcool/google-tts.key` e só vai no cabeçalho `X-Goog-Api-Key`.
Não aparece na página, nem no log, nem nas mensagens de erro.
**Medido:** `grep` da chave em `log-tts.jsonl`, `log.jsonl` e `servidor.log` dá 0.

**Log.** Fica em `log-tts.jsonl`, uma linha por pedido: entrada, voz, velocidade, hash, cache hit/miss, bytes e
latência. **Medido:** 0,4 a 1 s sem cache; menos de 1 ms com cache.

## As vozes pt-BR

A lista vem da própria API (`GET /v1/voices?languageCode=pt-BR`, **medido**). As famílias são descritas em
[tipos de voz](https://docs.cloud.google.com/text-to-speech/docs/list-voices-and-types).

| Família | Vozes pt-BR | SSML, pela documentação | Grátis por mês | Depois |
|---|---|---|---|---|
| **Chirp 3: HD** | 30 (Achernar, Charon, Kore, Puck…) | a [tabela de tipos](https://docs.cloud.google.com/text-to-speech/docs/list-voices-and-types) diz "sem SSML"; a [página do Chirp 3](https://docs.cloud.google.com/text-to-speech/docs/chirp3-hd) lista `speak, say-as, p, s, phoneme, sub, break, audio, prosody, voice`, **sem `<emphasis>`**, e só em pedido síncrono | 1 milhão de caracteres | US$ 30 por milhão |
| **Neural2** | A (fem.), B (masc.), C (fem.) | SSML completo | 1 milhão | US$ 16 por milhão |
| **WaveNet** | A, B, C, D, E | SSML completo | **4 milhões, somados com Standard** (é o mesmo SKU) | US$ 4 por milhão |
| **Standard** | A, B, C, D, E | SSML completo | (os mesmos 4 milhões) | US$ 4 por milhão |
| Studio, Polyglot | nenhuma voz pt-BR na lista | — | — | — |

Os preços estão na [página de preços](https://cloud.google.com/text-to-speech/pricing). Pela mesma página, **toda
tag SSML conta como caractere**, menos `<mark>`: um `<prosody>` em volta de uma palavra custa mais que a palavra.

### SSML completo

Pela [referência de SSML](https://docs.cloud.google.com/text-to-speech/docs/ssml):

- `<phoneme>` aceita IPA ou X-SAMPA, com acento primário `ˈ` no começo da sílaba forte e `.` separando sílabas;
- `<emphasis>`, com `level` strong, moderate, none ou reduced;
- `<prosody>`, com rate, pitch e volume;
- `<say-as>`, com characters, cardinal, ordinal, date e outros;
- `<break>`, com time ou strength.

A [página de fonemas](https://docs.cloud.google.com/text-to-speech/docs/phonemes) **não traz tabela para pt-BR**.
Por isso a documentação não garante `<phoneme>` em português, e eu medi. As medições (**medido**, 8 vozes, todas as
tags):

- Neural2, WaveNet e Standard aceitam `phoneme`, `emphasis`, `prosody`, `say-as` e `break` sem erro.
- `prosody pitch="-3st"` baixa o tom médio na proporção esperada: Neural2-B de 149 para 130 Hz, e −3 semitons
  seriam 125.
- `phoneme` em IPA **funciona em pt-BR**: `ˈka.fi` põe a força na primeira sílaba (veja a prova abaixo).
- O Chirp 3 HD também aceitou as cinco tags, e `phoneme` e `prosody` fizeram efeito. Mas a documentação não promete
  `<emphasis>` para ele, então ele não conta como "SSML completo".

Duas surpresas **medidas**:

- `pt-BR-Neural2-A` e `pt-BR-Standard-A` devolvem o **mesmo áudio, byte a byte**.
- `pt-BR-Neural2-B` e `pt-BR-Wavenet-B` dão a mesma duração e o mesmo tom médio em três frases, embora o MP3 não seja
  idêntico.

Ou seja: em pt-BR, o nome "Neural2" não garante voz diferente da mais barata. Fica registrado, sem conclusão.

### A voz padrão: `pt-BR-Neural2-B`

- **Por quê.** Pela documentação, é a família de maior qualidade com SSML completo, inclusive `<emphasis>`. O Chirp
  3 HD é mais natural, mas não entra por dois motivos: a tabela de tipos diz que ele não tem SSML, e a página dele não
  lista `<emphasis>`. B é masculina.
- **Como trocar.** Pelo ambiente, `TTS_VOICE=...`, ou pedido a pedido, com `"voz"`.
- **Custo.** Se o custo apertar, `pt-BR-Wavenet-B` soa igual pelas medidas acima e tem 4 vezes a cota grátis.
- **Chirp 3 HD.** Fica para a fala natural do pet ou do NPC, que não precisa de `<emphasis>`: por exemplo
  `"voz": "pt-BR-Chirp3-HD-Kore"`.
  - **Medido:** o áudio começa com ~0,4 s de chiado baixo.

## A prova: "café" certo e "CA-fe" com a força na primeira

Os áudios estão em `prova-tts/`. O medidor é `prova-tts/analisar.py`, com librosa: ele parte a voz nos vales de
energia e mede cada sílaba.

**O critério.** O português reduz a sílaba fraca que vem depois da forte. Em "café", o "fé" fica tão alto quanto
o "ca". Em "CA-fe", o "fe" vira um sussurro ~17 dB mais baixo e mais curto.

| arquivo | pedido | sílaba 1 | sílaba 2 | força |
|---|---|---|---|---|
| cafe-texto | `{"texto":"café"}` | 260 ms, −14,3 dB | 240 ms, −13,5 dB | **2ª** ca-FÉ |
| cafe-ipa | phoneme `kaˈfɛ` | igual ao de cima, byte a byte | | **2ª** |
| CAfe-ipa | phoneme `ˈka.fi` | 330 ms, −11,8 dB | 130 ms, **−29,6 dB** | **1ª** CA-fe |
| CAfe-reescrita | `{"texto":"cáfe"}` | 330 ms, −11,6 dB | 120 ms, **−30,9 dB** | **1ª** CA-fe |
| robo-cafe | prosody 85 %, −4 st + "café" | 320 ms, −13,0 dB | 260 ms, −14,9 dB | **2ª** |
| robo-CAfe | prosody 85 %, −4 st + phoneme `ˈka.fi` | 390 ms, −12,0 dB | 120 ms, **−29,9 dB** | **1ª** |

O gráfico de energia e tom está em `prova-tts/prova-cafe.png`, e a saída do medidor em `prova-tts/prova-cafe.txt`.

**Controle** (**medido**, a mesma régua):

- casa → 1ª; casá → 2ª;
- sofá → 2ª; sófa → 1ª;
- lápis → 1ª; lapís → 2ª.

**Com três sílabas a régua é mais fraca.** Em lâmpada / lampáda a diferença aparece na 2ª sílaba: 180 ms e 110 Hz
contra 280 ms e 132 Hz. Mas a regra dos 8 dB não separa as duas.

**Os dois caminhos funcionam para a força:** IPA com `<phoneme>` e a reescrita com acento no lugar errado ("cáfe").
**Mas só o `<phoneme>` controla a vogal.** O humano ouviu "vocé" sair com som de você: o sintetizador normaliza a grafia. O
robô-acento agora fala tudo por IPA; medido por palavra em `robo-acento/prova-vogais.md`. **Atenção:** um símbolo fora do
inventário da voz (h, ʁ) faz o Google ignorar a tag inteira em silêncio; para o r inicial use `x`.

- **A reescrita** acerta a força, mas deixa a vogal por conta do dicionário da voz.
- **O IPA** controla força e vogal (`ˈka.fi` fecha o "e" final em "i", como se fala no Brasil). É o que o robô-acento usa.

## Quem ganha /tts

- **Já tem:** o servidor da demo, na 8785, foi reiniciado com /tts, e o robô-acento, na 8787, nasceu com ele.
- **Só depois de reiniciar:** os mocks na 8783 (canteiro), 8784 (conserto) e 8786 (mapa-foto) rodam o código antigo
  até alguém reiniciar o servidor deles.
