Skip to content

STT API

VoiceLab Speech-to-Text endpoints: /api/v1/stt/*, /api/v2/stt/*, WebSocket, and gRPC.

Base URL: https://api.voicelab.uz
Auth: X-Api-Key on all REST requests.

Overview

  1. Short audio: POST /api/v1/stt/post/ (sync).
  2. Long audio: POST /api/v2/stt/post/ (async, returns task_id).
  3. Realtime: wss://api.voicelab.uz/api/v1/stt/realtime.
  4. Keep a realtime connection open and stream sequential chunks for longer sessions.
  5. gRPC streaming host: api.voicelab.uz:443 (separate RPC per Transcribe call).

API key

  • Header: X-Api-Key: <api_key>
  • WebSocket: query token
  • Realtime format query param: webm (default) or pcm

Short audio transcription

Request fields

  • audio (required, file) — input audio.
  • languageuz (default), en, ru.
  • has_diarization (boolean string) — label speakers.
  • has_offset (boolean string) — return segment offsets.
  • is_summary (boolean string) — generate a summary.
  • title (string) — optional label.

Audio formats: mp3, wav, ogg, m4a. Diarization requires audio ≥ 15 s.

Examples

bash
curl --request POST \
  --url https://api.voicelab.uz/api/v1/stt/post/ \
  --header 'X-Api-Key: your_api_key' \
  --header 'Accept-Language: en' \
  --form 'audio=@/path/to/voice-note.mp3' \
  --form 'language=uz' \
  --form 'has_diarization=false'
bash
export AISHA_API_KEY=your_api_key
npx aisha-ai stt ./audio.wav

Response

json
{
  "id": 531,
  "gender": "unknown",
  "title": null,
  "created_at": "2026-05-04T10:12:43.212Z",
  "duration": 18.7,
  "transcript": "Assalomu alaykum, this is a short audio transcription."
}

Status codes

  • 200 — Result returned.
  • 400 — Missing audio or bad format.
  • 402 — Insufficient balance.
  • 403 — Duration limit or access issue.
  • 503 — STT service temporarily unavailable.

v1 history list

bash
curl --request GET \
  --url 'https://api.voicelab.uz/api/v1/stt/get/?page=1&limit=10' \
  --header 'X-Api-Key: your_api_key'

GET /api/v1/stt/audios/ returns the same response shape.

json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 531,
      "title": null,
      "gender": "unknown",
      "status": "SUCCESS",
      "language": "uz",
      "created_at": "2026-05-04T10:12:43.212Z",
      "duration": 18.7,
      "transcript": "Assalomu alaykum, this is a short audio transcription.",
      "summary": "",
      "diarization": [],
      "audio_url": "/media/audio/f35f6c3a.wav",
      "speakers": []
    }
  ]
}

Status codes

  • 200 — History returned.
  • 403 — Missing or invalid API key.

Long audio async transcription

Request fields

  • audio (required, file) — audio file.
  • languageuz (default), en, ru.
  • has_diarization — speaker labeling.
  • has_offset — segment offsets.
  • is_summary — summary.
  • is_meeting — meeting profiling.
  • title — optional label.

Max file size: 500MB.

Example

bash
curl --request POST \
  --url https://api.voicelab.uz/api/v2/stt/post/ \
  --header 'X-Api-Key: your_api_key' \
  --form 'audio=@/path/to/meeting-record.mp3' \
  --form 'language=uz' \
  --form 'has_diarization=true' \
  --form 'is_summary=true'

Response

json
{
  "id": 901,
  "has_diarization": true,
  "is_meeting": false,
  "task_id": "66e92db4-95cf-4bb9-acbc-49462039d19f",
  "status": "PENDING",
  "title": "sales-call-13-april",
  "audio_url": "/media/audio/273bc5f8-1f91-4573-b3df-3c38c44294d0.mp3"
}

Status codes

  • 200 — Task created.
  • 400 — Missing audio or file too large.
  • 401 — Missing or invalid API key.
  • 403 — Balance or access error.
  • 500 — Internal error.

Realtime WebSocket transcription

Endpoint: wss://api.voicelab.uz/api/v1/stt/realtime?format=webm&token=<api_key>

Request fields

  • token (required).
  • formatwebm or pcm.
  • Stream audio bytes/chunks continuously.
  • End the session once with {"event":"end"}.

Examples

js
const token = 'your_api_key'
const ws = new WebSocket(
  `wss://api.voicelab.uz/api/v1/stt/realtime?format=webm&token=${encodeURIComponent(token)}`
)

ws.onmessage = event => {
  const message = JSON.parse(event.data)
  if (message.type === 'transcription') {
    console.log(message.text, message.partial, message.consumed_audio_seconds)
  }
}

const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' })

recorder.ondataavailable = async event => {
  if (event.data.size > 0 && ws.readyState === WebSocket.OPEN) {
    ws.send(await event.data.arrayBuffer())
  }
}
python
import asyncio
import websockets

TOKEN = "your_api_key"
URL = f"wss://api.voicelab.uz/api/v1/stt/realtime?format=pcm&token={TOKEN}"

async def stream_pcm():
    async with websockets.connect(URL, max_size=None) as ws:
        async def reader():
            async for message in ws:
                print(message)

        reader_task = asyncio.create_task(reader())
        with open("audio.s16le", "rb") as audio:
            while chunk := audio.read(3200):
                await ws.send(chunk)
                await asyncio.sleep(0.1)

        await ws.send('{"event":"end"}')
        await reader_task

asyncio.run(stream_pcm())

Responses

json
{
  "type": "session_started",
  "session_id": "7ab6d67a-9a29-4ad9-90b7-d2f5b2fc08fb",
  "user_id": "42",
  "allowed_audio_seconds": 1280,
  "format": "webm"
}
json
{
  "type": "transcription",
  "session_id": "7ab6d67a-9a29-4ad9-90b7-d2f5b2fc08fb",
  "text": "Assalomu alaykum, please check my order status.",
  "partial": false,
  "segment_event": "end",
  "consumed_audio_seconds": 4.32
}
json
{
  "type": "error",
  "code": "insufficient_balance",
  "message": "Balance limit reached",
  "session_id": "7ab6d67a-9a29-4ad9-90b7-d2f5b2fc08fb"
}

Close codes

  • 1000 — Stream closed normally.
  • 1008 — Token, balance, or format error.
  • 1011 — Server failed to process the stream.

gRPC streaming transcription

Endpoint: api.voicelab.uz:443

Request fields

  • audio_chunk (bytes, required).
  • language (uz / ru / en) — first chunk only.
  • first (bool) — first-chunk marker.

Example

python
import grpc
import stt_pb2
import stt_pb2_grpc

channel = grpc.secure_channel("api.voicelab.uz:443", grpc.ssl_channel_credentials())
client = stt_pb2_grpc.RealtimeSTTStub(channel)

def chunks(path):
    with open(path, "rb") as audio:
        first = True
        while data := audio.read(32000):
            yield stt_pb2.TranscribeChunk(
                audio_chunk=data,
                language="uz" if first else "",
                first=first,
            )
            first = False

def transcribe(path):
    response = client.Transcribe(chunks(path))
    print(response.text)
    for segment in response.segments:
        print(segment.start, segment.end, segment.text)

transcribe("audio.wav")
transcribe("another.wav")

Response

json
{
  "text": "Assalomu alaykum, please check my order status.",
  "language": "uz",
  "duration": 4.32,
  "audio_bytes": 138240,
  "segments": [
    {
      "start": 0.0,
      "end": 4.32,
      "text": "Assalomu alaykum, please check my order status."
    }
  ],
  "timings": {
    "transcribe_sec": 0.74,
    "total_sec": 0.82
  }
}

Close / error codes

  • 1000 — Stream closed normally.
  • 1008 — Token, balance, or format error.
  • 1011 — Server failed to process the stream.

v2 history and detail

v2 history

bash
curl --request GET \
  --url 'https://api.voicelab.uz/api/v2/stt/get/?page=1&limit=10' \
  --header 'X-Api-Key: your_api_key'
json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 901,
      "title": "sales-call-13-april",
      "status": "PENDING",
      "created_at": "2026-05-04T10:39:58.501Z",
      "duration": null,
      "audio_url": "/media/audio/273bc5f8-1f91-4573-b3df-3c38c44294d0.mp3"
    }
  ]
}
bash
curl --request GET \
  --url https://api.voicelab.uz/api/v2/stt/get/901/ \
  --header 'X-Api-Key: your_api_key'
json
{
  "id": 901,
  "title": "sales-call-13-april",
  "status": "SUCCESS",
  "created_at": "2026-05-04T10:39:58.501Z",
  "duration": 612.4,
  "transcript": "Long meeting transcript text...",
  "summary": "Short summary...",
  "diarization": [],
  "audio_url": "/media/audio/273bc5f8-1f91-4573-b3df-3c38c44294d0.mp3"
}

Status codes

  • 200 — History or detail returned.
  • 401 — Missing or invalid API key.
  • 404 — Transcript not found.

Task status polling

bash
curl --request GET \
  --url 'https://api.voicelab.uz/task-status/task-123/?instance_id=944' \
  --header 'Authorization: Bearer <user_jwt>'
json
{
  "task_id": "task-123",
  "status": "PENDING",
  "transcript_status": "PENDING",
  "message": "Task is still processing"
}
json
{
  "task_id": "task-123",
  "status": "SUCCESS",
  "transcript_status": "SUCCESS",
  "result": {
    "transcript": "Hello world"
  }
}

Prefer v2 detail with API keys

/task-status/ expects a user JWT. API-key integrations should poll GET /api/v2/stt/get/{id}/ instead.

Status codes

  • 200 — Task state returned.
  • 400 — Missing instance_id.
  • 403 — Access denied.
  • 500 — Task failed or unknown state.