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
- API base URL — https://lmctl.ai/lmtext
- An API key — one opaque token, sent on every request:
A human creates and revokes keys on the lmctl.ai website; an LLM just receives the key. The same credential format is used by these companion services; access depends on the deployment.Authorization: Bearer API_KEY
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
- GET /transcriptions?status=completed&limit=20&after=CURSOR — your transcriptions, newest first. status: uploading|in_progress|completed|failed (omit for all). Response: {transcriptions:[…], next_after, has_more, truncated} — pass next_after as after for the next page. truncated is always false: the list comes from a per-user index capped at 1,000 transcriptions (413 transcription_quota_exceeded when it is full).
- DELETE /transcriptions/{id} — removes audio, meta, and transcript objects and makes a best-effort attempt to delete the Transcribe job record. A running job is not cancelled and may write transcript output afterwards; that late output stays in the staging bucket until its one-day expiry.
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
- 401/403 — missing/invalid API key.
- 404 transcription_not_found — no such id for you.
- 429 monthly_cap_exceeded (cap_usd and used_usd in the body) or global_cap_exceeded — the monthly spend cap was hit; finalize is refused before any transcription starts.
- Quotas: a monthly dollar cap per user (default $5) and one for all users (default $25). At finalize the duration is measured and the cost is reserved at the AWS Transcribe list price of $0.0001 per second, with a 15 second minimum assumed; a job that cannot be started is refunded.
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.