# `ALLM.SpeechResponse`
[🔗](https://github.com/cykod/ALLM/blob/v0.6.1/lib/allm/speech_response.ex#L1)

A text-to-speech response. Layer A serializable data.

    iex> resp = ALLM.SpeechResponse.new(audio: ALLM.Audio.from_binary(<<1, 2>>, "audio/mpeg"), format: :mp3)
    iex> ALLM.Audio.size(resp.audio)
    {:ok, 2}

## Fields

- `:audio`: the synthesized audio, as `%ALLM.Audio{source: {:binary, bytes}}`.
- `:format`: the format that *arrived*, derived from the response's
  content type through `mime_to_format/1`. It is not an echo of the
  request's `:format`, and the two differ whenever the request left
  `:format` as `nil`. It is `nil` when the content type is not in the
  table.
- `:sample_rate`: the sample rate in Hz of the audio that arrived, as
  the adapter reports it, or `nil` when the adapter cannot tell. For
  `:pcm` this is the rate to play the bytes at.
- `:id`, `:request_id`, `:model`, `:provider`: provider correlation.
- `:usage`: an `t:ALLM.Usage.t/0`, never `nil`. A provider that reports
  no counters for speech leaves every field `nil`.
- `:raw`: the provider's non-audio body, or `nil`. Audio bytes are never
  stored in `:raw`: they live once, in `:audio`, so a persisted response
  does not hold the audio twice. An adapter whose body carries encoded
  audio replaces it in `:raw` with a `"<N bytes>"` placeholder.
- `:metadata`: caller-owned.

## Format tables

`format_to_mime/1` and `mime_to_format/1` are the only MIME-to-format
tables in the library. They are separate functions so that a provider's
extra content type can extend the reverse table without changing the
forward one.

`:ulaw` maps to `audio/basic`, the registered type for 8 kHz µ-law
(RFC 2046), and `:alaw` to `audio/alaw`, which has no registered
equivalent. ElevenLabs answers `ulaw_8000` with `audio/ulaw` and
`alaw_8000` with `audio/alaw` (observed 2026-09-27), so the reverse table
also reads `audio/ulaw` as `:ulaw`.

# `t`

```elixir
@type t() :: %ALLM.SpeechResponse{
  audio: ALLM.Audio.t() | nil,
  format: ALLM.SpeechRequest.format() | nil,
  id: String.t() | nil,
  metadata: map(),
  model: String.t() | nil,
  provider: atom() | nil,
  raw: term(),
  request_id: String.t() | nil,
  sample_rate: pos_integer() | nil,
  usage: ALLM.Usage.t()
}
```

# `format_to_mime`

```elixir
@spec format_to_mime(ALLM.SpeechRequest.format()) :: String.t()
```

Map a format atom to its content type.

## Examples

    iex> ALLM.SpeechResponse.format_to_mime(:mp3)
    "audio/mpeg"

    iex> ALLM.SpeechResponse.format_to_mime(:ulaw)
    "audio/basic"

# `mime_to_format`

```elixir
@spec mime_to_format(String.t() | nil) :: ALLM.SpeechRequest.format() | nil
```

Map a response content type to a format atom.

Media-type parameters after `;` are stripped and the type is compared
case-insensitively. A content type not in the table, or `nil`, returns
`nil`.

## Examples

    iex> ALLM.SpeechResponse.mime_to_format("audio/mpeg")
    :mp3

    iex> ALLM.SpeechResponse.mime_to_format("audio/wav; codecs=1")
    :wav

    iex> ALLM.SpeechResponse.mime_to_format("audio/ulaw")
    :ulaw

    iex> ALLM.SpeechResponse.mime_to_format("video/mp4")
    nil

# `new`

```elixir
@spec new(keyword()) :: t()
```

Build a `%SpeechResponse{}` from keyword opts.

Unknown keys raise `KeyError` via `struct!/2`.

## Examples

    iex> resp = ALLM.SpeechResponse.new(model: "tts-1")
    iex> resp.usage
    %ALLM.Usage{}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
