# Modalities — Text to Speech (Text to Audio)

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** text to speech, TTS, voice synthesis, audio generation, speech synthesis
> **Read by Claude in:** implement (quando o projeto precisa gerar áudio a partir de texto)

**Verified against:** OpenAI 2.13.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não gera áudio. `AudioClient.GenerateSpeechAsync`, a sobrecarga de streaming e a ponte `AsITextToSpeechClient` foram **medidas por reflexão sobre as assemblies do pin** em 2026-09-08. Last-verified: 2026-09-08.

---

## Quando usar TTS

TTS converte texto em áudio narrado. É um caso de uso **raro** — só adicione quando há demanda real e comprovada:

- **Acessibilidade:** leitura em voz alta para usuários com deficiência visual ou dificuldade de leitura
- **Notificações em áudio:** resumos falados de alertas ou relatórios em contextos hands-free (painel industrial, carro)
- **Resumos falados:** o usuário quer ouvir um digest em vez de ler (ex: podcast personalizado gerado on-demand)

**Nota honesta:** TTS tem custo por caractere e latência perceptível. Se o caso de uso não é explicitamente solicitado pelo usuário final ou pelo cliente, não adicione — um botão "copiar texto" ou um leitor de tela nativo do SO resolve a maioria dos casos de acessibilidade sem custo de API.

TTS é uma **chamada de serviço**, não um agente conversacional. Use `AudioClient` diretamente via DI — não crie `AIAgent` para isso.

---

## A API

No OpenAI SDK (.NET 2.x), o caminho é `AudioClient.GenerateSpeech`:

```csharp
using OpenAI.Audio;

AudioClient client = new("tts-1", apiKey);
BinaryData speech = await client.GenerateSpeechAsync(text, GeneratedSpeechVoice.Alloy);
```

Retorna `BinaryData` (bytes de áudio) que você persiste ou serve via stream. Para controlar formato e velocidade, use `SpeechGenerationOptions`.

> **Conferência de 2026-09-08.** A assinatura na assembly `OpenAI` 2.13.0 é `GenerateSpeechAsync(string text, GeneratedSpeechVoice voice, SpeechGenerationOptions options, CancellationToken)` — a **voz é parâmetro obrigatório**, não uma opção dentro de `SpeechGenerationOptions`.
>
> **Novidade útil que não estava aqui:** existe `GenerateSpeechStreamingAsync(...)`, que devolve os pedaços de áudio conforme saem. Para texto longo servido a um usuário, é a diferença entre esperar o arquivo inteiro e começar a tocar — vale mais que qualquer ajuste de formato.
>
> **Caminho por abstração (opcional):** `Microsoft.Extensions.AI` 10.9.0 define `ITextToSpeechClient`, e `Microsoft.Extensions.AI.OpenAI` publica `audioClient.AsITextToSpeechClient()`.
>
> **Reescrita de conteúdo de mídia está FORA do escopo desta re-verificação** (é épico próprio). O que se fez aqui foi conferir nome por nome e re-datar.

---

## Exemplo C# completo

```csharp
using OpenAI.Audio;
using Microsoft.Extensions.Configuration;

public sealed class TextToSpeechService
{
    private readonly AudioClient _client;

    public TextToSpeechService(IConfiguration config)
    {
        // tts-1: mais rápido, menor qualidade. tts-1-hd: mais lento, maior qualidade.
        var model = config["TTS:Model"] ?? "tts-1";
        var apiKey = config["OpenAI:ApiKey"]!;
        _client = new AudioClient(model, apiKey);
    }

    /// <summary>
    /// Converte texto em áudio e retorna bytes MP3.
    /// Cachear o resultado é altamente recomendado para textos repetitivos — gerar o mesmo texto
    /// duas vezes é desperdício de custo e latência.
    /// </summary>
    public async Task<BinaryData> SynthesizeAsync(
        string text,
        GeneratedSpeechVoice? voice = null,
        GeneratedSpeechFormat? format = null,
        float speedRatio = 1.0f,
        CancellationToken ct = default)
    {
        var resolvedVoice  = voice  ?? GeneratedSpeechVoice.Alloy;
        var resolvedFormat = format ?? GeneratedSpeechFormat.Mp3;

        var options = new SpeechGenerationOptions
        {
            ResponseFormat = resolvedFormat,
            SpeedRatio     = speedRatio
        };

        BinaryData speech = await _client.GenerateSpeechAsync(text, resolvedVoice, options, ct);
        return speech;
    }
}
```

**Registro DI:**

```csharp
// Program.cs
builder.Services.AddSingleton<TextToSpeechService>();
```

**Servindo como download ou stream:**

```csharp
public class AudioController(TextToSpeechService tts) : ControllerBase
{
    [HttpPost("synthesize")]
    public async Task<IActionResult> Synthesize([FromBody] SynthesizeRequest req)
    {
        // Verificar cache antes de gerar (ver Anti-patterns)
        BinaryData audio = await tts.SynthesizeAsync(
            text:       req.Text,
            voice:      GeneratedSpeechVoice.Nova,
            format:     GeneratedSpeechFormat.Mp3,
            speedRatio: 1.1f);

        // the storage layer may assign the final name; use a timestamp-based name here
        var fileName = $"tts-{DateTime.UtcNow:yyyyMMddHHmmss}.mp3";
        return File(audio.ToStream(), "audio/mpeg", fileName);
    }
}
```

---

## Parâmetros

> Valores verificados contra OpenAI TTS API + OpenAI .NET SDK 2.x (2026-05-19).

**Vozes disponíveis (`GeneratedSpeechVoice`):**

| Voz | Característica |
|-----|---------------|
| `Alloy` | Neutra, versátil — boa escolha padrão |
| `Echo` | Masculina, suave |
| `Fable` | Britânica, expressiva |
| `Onyx` | Masculina, grave |
| `Nova` | Feminina, amigável |
| `Shimmer` | Feminina, suave |

**Formatos de saída (`GeneratedSpeechFormat`):**

| Formato | Caso de uso |
|---------|-------------|
| `Mp3` | Default — compatibilidade universal |
| `Opus` | Streaming de baixa latência |
| `Aac` | Dispositivos Apple / YouTube |
| `Flac` | Arquivo sem perda |
| `Wav` | Processamento de áudio downstream |
| `Pcm` | Raw bytes sem container |

**`SpeedRatio`:** `0.25` (muito lento) a `4.0` (muito rápido). Padrão `1.0`.

**Modelos:**
- `tts-1` — rápido, custo menor, qualidade suficiente para a maioria dos casos
- `tts-1-hd` — maior qualidade, mais lento e mais caro — use apenas quando a clareza de voz é crítica

---

## Anti-patterns

| Anti-pattern | Por quê é errado | Jeito certo |
|--------------|-----------------|-------------|
| Adicionar TTS sem caso de uso real | Custo por caractere + latência sem benefício ao usuário | Só adicione com demanda explícita; leitores de tela nativos cobrem acessibilidade básica |
| Gerar o mesmo texto repetidamente sem cache | Textos fixos (intro, disclaimers, respostas FAQ) custam o mesmo toda vez | Cachear bytes de áudio para textos invariantes — CDN ou Redis com key baseada em hash do texto + voz + formato |
| Criar `AIAgent` para TTS | `AudioClient.GenerateSpeech` não é conversacional; não há raciocínio LLM envolvido | Injete `TextToSpeechService` via DI — é um serviço de síntese, não um agente |
| Gerar texto longo de uma vez sem segmentar | Providers têm limite de caracteres por request (OpenAI: ~4096 tokens / ~4000 chars); textos muito longos falham | Divida em parágrafos, gere áudio por segmento, concatene os bytes |

---

## Checklist (verifiable by morph-eval)

- [ ] `AudioClient` injetado via DI — não instanciado inline
- [ ] Cache implementado para textos invariantes (não gera o mesmo áudio duas vezes)
- [ ] Textos longos segmentados antes de enviar ao provider
- [ ] `GeneratedSpeechVoice` e `GeneratedSpeechFormat` escolhidos adequados ao contexto (não só defaults)
- [ ] Model e API key vêm de config, não hardcoded
- [ ] Não usa `AIAgent`/`AsAIAgent` para TTS — é um `TextToSpeechService`

---

## References

- `ai-agents-setup` — packages, DI, e providers disponíveis
- `ai-agents-providers-model-registry` — padrão de alias; adapte para resolver `AudioClient` por provider
- OpenAI .NET SDK — `AudioClient`: https://github.com/openai/openai-dotnet

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/modalities-text-to-speech.md v1.1 (2026-09-08)*
