# `PhxDemo.TTS`
[🔗](https://github.com/thenatespack/phoenix_demo/blob/main/lib/phx_demo/tts.ex#L1)

Text-to-speech for `PhxDemo.Steps.say/3`.

Narration is on by default, using `PhxDemo.TTS.Piper`: a free, local,
open-source voice. Run `mix phx_demo.piper.setup` once to install it.
Choose another adapter, or turn narration off, in config/test.exs:

    config :phx_demo, :tts, {PhxDemo.TTS.Piper, voice: "en_US-amy-medium"}
    config :phx_demo, :tts, {PhxDemo.TTS.OpenAI, voice: "nova"}         # OPENAI_API_KEY
    config :phx_demo, :tts, {PhxDemo.TTS.ElevenLabs, voice_id: "..."}  # ELEVENLABS_API_KEY
    config :phx_demo, :tts, PhxDemo.TTS.MacSay                         # macOS only
    config :phx_demo, :tts, false                                      # captions only

With an adapter, each `say` line is synthesized before it plays (cached by
adapter, options and text), the demo pauses for the clip's real length, and
`mix demo.record` mixes the clips into the MP4 at the right offsets.

## Writing an adapter

Implement `c:synthesize/3`: write audio for `text` to `path` (no extension;
add the one you produce) and return the final path. Any format ffmpeg
reads works. Optionally implement `c:check/1` to fail fast, before any
demo runs, when the adapter isn't usable (missing binary, API key, …).

# `check`
*optional* 

```elixir
@callback check(opts :: keyword()) :: :ok | {:error, String.t()}
```

# `synthesize`

```elixir
@callback synthesize(text :: String.t(), path :: Path.t(), opts :: keyword()) ::
  {:ok, Path.t()} | {:error, term()}
```

# `cache_dir`

User-level cache directory for downloaded voices and tools.

# `check`

Runs the configured adapter's `check/1`, if any.

# `config`

The configured `{adapter, opts}`, or nil when narration is off. Defaults to Piper.

# `speak`

Synthesizes `text` with the configured adapter, using a cache under
`<output_dir>/.tts_cache`. Returns `{:ok, %{path: path, duration_ms: ms}}`,
or `:disabled` when no adapter is configured.

---

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