lmsound — text → speech for LLM agents

What this is. lmsound turns text into an audio file using AWS Polly. You POST some text (plain or SSML), the server synthesizes speech, stores the audio, and hands you back a short-lived signed download URL for the mp3 (or ogg/pcm). Plain HTTP, one opaque API key — exactly like calling an LLM API. No SDK, no websocket, no schema dance.

Use it to give any agent a voice: narrate a summary, read out an alert, produce a spoken reply.


What you need

export API="https://lmctl.ai/tools/lmsound"
export KEY="API_KEY"

Synthesize speech — POST /speak

curl -s -X POST "$API/speak" -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"text":"Hello from lmsound. Your build is green."}'

Response:

{
  "id": "3f9a…-uuid",
  "voice_id": "Joanna",
  "engine": "neural",
  "format": "mp3",
  "text_type": "text",
  "char_count": 42,
  "size": 20361,
  "duration_ms": 2640,
  "sample_rate": 24000,
  "bitrate": 48000,
  "download_url": "https://lmctl.ai/tools/lmsound/_blob/download?…&expires=…&signature=…"
}

duration_ms, sample_rate (Hz) and bitrate (bps) are measured from the synthesized audio — no need to re-probe with ffprobe. They're handy for aligning narration to video/animation beats. (For pcm, which has no container header, these media stats may be omitted.) Fetch the audio (the download_url is a signed GET, valid for 300 seconds by default (configurable with ttl)):

# Copy download_url from the JSON response into AUDIO_URL.
curl --fail --location "$AUDIO_URL" -o hello.mp3

Body fields (all but text optional)

field default notes
text — 1–3000 characters. Empty → 400 invalid_text; over 3000 → 400 text_too_long.
voice_id Joanna Any Polly voice — see GET /voices. Unknown/incompatible → 400 invalid_voice.
engine neural standard | neural | long-form | generative. neural is great quality broadly; generative/long-form are the highest quality but only some voices support them (the error lists which).
format mp3 mp3 | ogg_vorbis | pcm.
text_type auto text or ssml. Omitted → auto-detected (ssml if the text starts with <speak).
sample_rate Polly default Hz as a string. mp3/ogg_vorbis: 8000|16000|22050|24000; pcm: 8000|16000. Invalid for the format → 400 invalid_sample_rate.
ttl 300 Lifetime (seconds) of the returned download_url: a number, not a string, from 60 to 3600. Out of range → 400 invalid_ttl. Bump it for batch pipelines that generate many clips before assembling.

SSML — fine-grained control

curl -s -X POST "$API/speak" -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{
    "voice_id":"Matthew","engine":"neural",
    "text":"<speak>Deploy <break time=\"300ms\"/> <emphasis>succeeded</emphasis>.</speak>"
  }'

Set text_type:"ssml" explicitly, or just start the text with <speak> and it's detected. Malformed SSML → 400 {"error":"invalid_ssml"}.


Pick a voice — GET /voices

curl -s "$API/voices" -H "Authorization: Bearer $KEY"
# -> {"voices":[{"id":"Joanna","name":"Joanna","language_code":"en-US",
#      "gender":"Female","supported_engines":["standard","neural","generative"]}, …]}

Filter to the voices you can actually use with an engine/language:

curl -s "$API/voices?engine=generative&language=en-US" -H "Authorization: Bearer $KEY"

Use supported_engines to choose a valid voice_id × engine pair before calling /speak.


Re-download later — GET /speak/{id}

The download_url from /speak expires after the requested TTL (5 minutes by default). To get a fresh link for an audio you already made, hit its id — the server 302-redirects to a new signed URL:

curl -sL "$API/speak/3f9a…-uuid" -H "Authorization: Bearer $KEY" -o hello.mp3
# fresh link with a longer lifetime: add ?ttl=SECONDS (default 300s)
curl -sL "$API/speak/3f9a…-uuid?ttl=1800" -H "Authorization: Bearer $KEY" -o hello.mp3

Ids are scoped to your key — someone else's id returns 404 not_found.

List your recent audio — GET /speak

curl -s "$API/speak" -H "Authorization: Bearer $KEY"
# -> {"items":[{"id":"3f9a…","format":"mp3","size":20361,"created_at":"…"}, …]}  # newest first

Check your monthly spend — GET /usage

There's a per-user monthly Polly spend cap. Check where you stand:

curl -s "$API/usage" -H "Authorization: Bearer $KEY"
# -> {"month":"2026-07","used_usd":0.0123,"cap_usd":10,"remaining_usd":9.9877}

When a POST /speak would push you over the cap it's rejected with 429 {"error":"monthly_cap_exceeded","cap_usd":10,"used_usd":…} before any audio is synthesized The estimate is not a billing guarantee, especially under concurrent requests. Cost is estimated from characters × the engine's Polly rate; the counter resets at the start of each UTC month.


Notes

That's the whole skill: POST /speak to synthesize (text or SSML), GET /voices to choose a narrator, GET /speak/{id} for a fresh link, GET /speak to list — all with one Authorization: Bearer API_KEY.

Verification scope

The API and CLI syntax above was checked against the tool source on 2026-10-19. The raw HTTP steps were also run end to end against https://lmctl.ai on 2026-10-20. Account permissions, limits and credentials differ per deployment; confirm them with your operator. Do not assume that access to one deployment grants access to another.