lmtext — speech → text for LLM agents

What this is. lmtext turns an audio file into a transcript using AWS Transcribe — the reverse of lmsound. You upload an audio file (mp3/wav/flac/ogg/webm/amr/m4a/mp4) with a signed upload form, kick off transcription, then poll for the transcript. Plain HTTP, one opaque API key — exactly like calling an LLM API. No SDK, no websocket.

Use it to give any agent ears: transcribe a voice memo, a meeting snippet, a podcast clip.


What you need

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

The flow (4 steps)

1. Allocate an upload — POST /transcriptions

resp="$(curl -s -X POST "$API/transcriptions" -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{"file_name":"memo.mp3"}')"
printf '%s\n' "$resp" | jq .

id="$(jq -r '.transcription_id' <<<"$resp")"
upload_url="$(jq -r '.upload_url' <<<"$resp")"

Response:

{
  "transcription_id": "3f9a…-uuid",
  "upload_url": "https://lmctl.ai/lmtext/_blob/upload?…",
  "upload_fields": {"key":"…","Content-Type":"audio/mpeg","expires":"…","signature":"…"},
  "expires_in": 300
}

Body fields:

field default notes
file_name — Extension selects the media format: .mp3 .wav .flac .ogg .webm .amr .m4a/.mp4. Unsupported → 400.
content_type from extension Must match the upload's Content-Type (signed into the upload form).
language_code en-US e.g. es-US, fr-FR, de-DE. Invalid shape → 400; unsupported by Transcribe → fails at finalize.

2. Upload the audio — signed form upload

mapfile -t form_fields < <(
  jq -r '.upload_fields | to_entries[] | "-F\n\(.key)=\(.value)"' <<<"$resp"
)
curl -s -X POST "$upload_url" \
  "${form_fields[@]}" \
  -F "file=@memo.mp3"

(Every upload_fields entry must be a form field, then the file last.) Max size 25 MB, link valid 300 s. Over 25 MB → 413 audio_too_large. The upload needs no Authorization header; the signed URL is the credential.

3. Start transcription — POST /transcriptions/{id}/finalize

curl -s -X POST "$API/transcriptions/$id/finalize" -H "Authorization: Bearer $KEY"

Verifies the upload exists, probes duration, starts the AWS Transcribe job. Errors: 409 not_uploaded (step 2 not done), 400 audio_invalid (unparseable audio), 413 audio_too_large. Idempotent — calling it again returns current status.

4. Poll for the result — GET /transcriptions/{id}

curl -s "$API/transcriptions/$id" -H "Authorization: Bearer $KEY"
{
  "transcription_id": "3f9a…-uuid",
  "status": "completed",
  "file_name": "memo.mp3",
  "language_code": "en-US",
  "media_format": "mp3",
  "duration_ms": 2640,
  "size": 20361,
  "created_at": "…", "started_at": "…", "completed_at": "…",
  "transcript": "hello lmtext, your build is green."
}

status: uploading → in_progress → completed | failed (with failure_reason). transcript is present once completed. Poll every few seconds; typical short clips finish asynchronously; completion time depends on the service and input.


List and delete

You only ever see your own transcriptions; other users' ids return 404 transcription_not_found.


CLI

cd cloud
npm install
npm run build
npm link   # exposes the package bin named lmtext
export LMTEXT_API_URL="$API"

lmtext transcribe memo.mp3 --wait        # upload → finalize → poll → print transcript
lmtext list --status completed
lmtext get 3f9a…-uuid
lmtext delete 3f9a…-uuid

Credentials resolve like every lmctl tool: LMCTL_APPKEY, LMCTL_APPKEY_FILE, or the shared appkey location (@lmctl-ai/appkey).


Errors and limits

Verification scope

The API and CLI syntax above was checked against the tool source on 2026-10-19. The upload, finalize, poll, list and delete steps were also run end to end against https://lmctl.ai on 2026-10-21 with a spoken memo. 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.