# Modalities — Speech to Text (Audio to Text)

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** speech to text, transcription, whisper, audio to text, voice transcription
> **Read by Claude in:** implement (quando o projeto precisa transcrever áudio para 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 transcreve áudio. `AudioClient.TranscribeAudioAsync` e a ponte `AsISpeechToTextClient` foram **medidas por reflexão sobre as assemblies do pin** em 2026-09-08. Last-verified: 2026-09-08.

---

## Quando usar STT

Use quando o input real do usuário é áudio e você precisa de texto para processamento downstream:

- **Transcrição de reuniões:** gravar áudio da reunião → transcrever → alimentar um Status Agent que extrai ações, decisões e responsáveis
- **Notas de voz:** usuário fala → transcreve → salva como nota textual ou input para outro agente
- **Suporte por voz:** áudio do cliente → transcreve → agente de roteamento classifica e responde

**Fluxo típico:** `áudio (bytes/arquivo)` → `TranscriptionService` → `texto` → `AIAgent` downstream

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

**Não se aplica** quando o input já é texto. Se o áudio foi transcrito por outra ferramenta, use o texto diretamente.

---

## A API

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

```csharp
using OpenAI.Audio;

AudioClient client = new("whisper-1", apiKey);
AudioTranscription transcription = await client.TranscribeAudioAsync(audioFilePath);
string text = transcription.Text;
```

O projeto injeta `AudioClient` via DI. Para transcrição com timestamps (ex: sincronização de legenda), use `AudioTranscriptionOptions` com `ResponseFormat = AudioTranscriptionFormat.Verbose`.

> **Conferência de 2026-09-08.** As sobrecargas existentes na assembly `OpenAI` 2.13.0 são `TranscribeAudioAsync(string audioFilePath, AudioTranscriptionOptions)` e `TranscribeAudioAsync(Stream audio, string audioFilename, AudioTranscriptionOptions, CancellationToken)` — **a de `Stream` exige o nome do arquivo**, porque é dele que o serviço deduz o formato. Passar só o stream não compila.
>
> **Caminho por abstração (opcional):** `Microsoft.Extensions.AI` 10.9.0 define `ISpeechToTextClient`, e `Microsoft.Extensions.AI.OpenAI` publica `audioClient.AsISpeechToTextClient()`. Vale quando o projeto quer trocar de provedor sem tocar no chamador; o `AudioClient` direto continua sendo o caminho mais curto.
>
> **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 TranscriptionService
{
    private readonly AudioClient _client;

    public TranscriptionService(IConfiguration config)
    {
        var model = config["Transcription:Model"] ?? "whisper-1";
        var apiKey = config["OpenAI:ApiKey"]!;
        _client = new AudioClient(model, apiKey);
    }

    /// <summary>
    /// Transcreve áudio a partir de um arquivo e retorna texto simples.
    /// O transcript retornado pode ser passado diretamente como input para um AIAgent downstream.
    /// </summary>
    public async Task<string> TranscribeAsync(string audioFilePath, CancellationToken ct = default)
    {
        AudioTranscription transcription = await _client.TranscribeAudioAsync(audioFilePath, cancellationToken: ct);
        return transcription.Text;
    }

    /// <summary>
    /// Transcreve áudio a partir de bytes (ex: upload do usuário) e retorna texto simples.
    /// </summary>
    public async Task<string> TranscribeBytesAsync(
        Stream audioStream,
        string fileName,
        string mediaType = "audio/mpeg",
        CancellationToken ct = default)
    {
        var options = new AudioTranscriptionOptions
        {
            ResponseFormat = AudioTranscriptionFormat.Simple
        };

        AudioTranscription transcription = await _client.TranscribeAudioAsync(
            audioStream, fileName, options, cancellationToken: ct);
        return transcription.Text;
    }

    /// <summary>
    /// Transcreve com timestamps por palavra e segmento (útil para legendas ou busca por trecho).
    /// </summary>
    public async Task<AudioTranscription> TranscribeVerboseAsync(string audioFilePath, CancellationToken ct = default)
    {
        var options = new AudioTranscriptionOptions
        {
            ResponseFormat         = AudioTranscriptionFormat.Verbose,
            TimestampGranularities = { AudioTimestampGranularity.Word, AudioTimestampGranularity.Segment }
        };

        return await _client.TranscribeAudioAsync(audioFilePath, options, cancellationToken: ct);
    }
}
```

**Alimentando um agente downstream com o transcript:**

```csharp
// O transcript é texto puro — basta passar como input para qualquer AIAgent
string transcript = await _transcription.TranscribeAsync("meeting-2024-05-19.mp3");

// Ex: Status Agent que extrai ações da reunião
AgentResponse<MeetingActions> actions = await _statusAgent.RunAsync<MeetingActions>(
    $"Extraia ações, decisões e responsáveis desta transcrição:\n\n{transcript}");
```

**Registro DI:**

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

---

## Formatos e limites

> Valores verificados contra OpenAI Whisper API (2026-05-19). Confirme limites atuais na documentação do provider ao fazer deploy.

| Aspecto | Detalhe |
|---------|---------|
| Formatos suportados | `mp3`, `mp4`, `mpeg`, `mpga`, `m4a`, `wav`, `webm` |
| Limite por request | **25 MB** por arquivo (OpenAI Whisper) |
| Duração aproximada | ~1h de áudio ≈ 25MB em MP3 128kbps |
| Áudio longo | Pré-processe client-side: divida em chunks antes de enviar (ffmpeg, NAudio, etc.) — não há API de chunking automático no SDK |
| Idiomas | Whisper suporta 99 idiomas; especifique `Language = "pt"` em `AudioTranscriptionOptions` para melhor acurácia em português |

**Chunking de áudio longo:** para arquivos acima de 25MB, divida o arquivo em segmentos de ~20MB antes de enviar. Concatene os textos retornados. Não há sobreposição automática — adicione alguns segundos de overlap entre chunks se a precisão nos cortes for crítica.

---

## Anti-patterns

| Anti-pattern | Por quê é errado | Jeito certo |
|--------------|-----------------|-------------|
| Transcrever quando o input já é texto | Custo e latência desnecessários | Use o texto diretamente como input do agente downstream |
| Enviar áudio longo (>25MB) sem chunking | O provider rejeita a request com erro 413 | Divida client-side em chunks ≤20MB antes de enviar |
| Criar `AIAgent` para transcrição | `AudioClient` não é `IChatClient`; o pipeline é simples (bytes → texto) | Injete `TranscriptionService` via DI — é um serviço, não um agente |
| Ignorar o idioma do áudio | Whisper infere o idioma, mas pode errar em áudios com muita sigla ou sotaque | Especifique `Language` em `AudioTranscriptionOptions` quando o idioma for conhecido |

---

## Checklist (verifiable by morph-eval)

- [ ] `AudioClient` injetado via DI — não instanciado inline
- [ ] Áudio longo (>25MB) tratado com chunking antes de enviar ao provider
- [ ] Transcript retornado como `string` e passado como input ao agente downstream — sem re-formatação manual
- [ ] `Language` especificado em `AudioTranscriptionOptions` quando o idioma do áudio é conhecido
- [ ] Model e API key vêm de config, não hardcoded

---

## 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-speech-to-text.md v1.1 (2026-09-08)*
