Evom Labs

Developer platform

Loli 2.0 Speech-to-Text API

Public API contract for file transcription, NDJSON progress and real-time speech recognition over a WebSocket.

On this page

Endpoints

POST/api/v1/stt/transcriptions

Transcribes one audio file and returns the result as JSON.

POST/api/v1/stt/transcriptions?stream=true

Transcribes an audio file and returns progress and results as NDJSON lines.

WS/api/public/v1/stt/ws?token=<stt_key>

Takes binary PCM16 and returns partial, segment and final results in real time.

GET/api/public/v1/auth/check

Checks the status of an STT API key.

Needs verification: public gateway configuration

The two paths /api/public/v1/auth/check and /api/public/v1/stt/ws belong to the STT gateway. Whether they are exposed on the public origin depends on the edge configuration of each deployment. If the gateway is not exposed directly, use the origin your operator provides instead of the examples below.

File transcription

POST/api/v1/stt/transcriptions

Send multipart/form-data and receive the full transcription when it finishes.

Form fields

FieldTypeRequiredDefaultDescription
audiofileYes-Audio file, up to 100 MiB. file is accepted as an alias.
languagestringNoautoauto, one of the 30 single-language modes, or one of the three bilingual modes listed below.
streambooleanNofalseQuery parameter. Set true to receive NDJSON progressively.

Containers recognised at present: WAV 16-bit PCM, MP3, WebM, OGG and MP4 or M4A. Decoded duration must be between about 0.2 and 1,800 seconds. Which encoded formats decode depends on the deployment, so do not rely on the filename extension alone.

Examples

curl
curl -X POST "https://studio.evomlabs.com/api/v1/stt/transcriptions" \
  -H "Authorization: Bearer stt_sk_live_YOUR_KEY" \
  -F "audio=@recording.wav" \
  -F "language=auto"
Python
import requests

with open("recording.wav", "rb") as audio:
    response = requests.post(
        "https://studio.evomlabs.com/api/v1/stt/transcriptions",
        headers={"Authorization": "Bearer stt_sk_live_YOUR_KEY"},
        files={"audio": audio},
        data={"language": "auto"},
        timeout=1800,
    )

response.raise_for_status()
print(response.json()["text"])

Response

JSON
{
  "text": "Nแป™i dung ฤ‘รฃ phiรชn รขm",
  "language": "vi",
  "duration_ms": 1234,
  "provider": "loli-asr",
  "model": "Loli 2.0"
}

Important

A successful response does not use the ok/data envelope; it returns the transcription object directly. duration_ms is processing latency, not the length of the audio.

Language modes

ModeLanguages accepted
autoEvery supported language; the model detects it for you
vi๐Ÿ‡ป๐Ÿ‡ณ Vietnamese
en๐Ÿ‡ฌ๐Ÿ‡ง English
zh๐Ÿ‡จ๐Ÿ‡ณ Chinese
yue๐Ÿ‡ญ๐Ÿ‡ฐ Cantonese
ja๐Ÿ‡ฏ๐Ÿ‡ต Japanese
ko๐Ÿ‡ฐ๐Ÿ‡ท Korean
th๐Ÿ‡น๐Ÿ‡ญ Thai
id๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesian
ms๐Ÿ‡ฒ๐Ÿ‡พ Malay
tl๐Ÿ‡ต๐Ÿ‡ญ Filipino
hi๐Ÿ‡ฎ๐Ÿ‡ณ Hindi
ar๐Ÿ‡ธ๐Ÿ‡ฆ Arabic
fa๐Ÿ‡ฎ๐Ÿ‡ท Persian
ru๐Ÿ‡ท๐Ÿ‡บ Russian
mk๐Ÿ‡ฒ๐Ÿ‡ฐ Macedonian
el๐Ÿ‡ฌ๐Ÿ‡ท Greek
tr๐Ÿ‡น๐Ÿ‡ท Turkish
de๐Ÿ‡ฉ๐Ÿ‡ช German
fr๐Ÿ‡ซ๐Ÿ‡ท French
es๐Ÿ‡ช๐Ÿ‡ธ Spanish
pt๐Ÿ‡ต๐Ÿ‡น Portuguese
it๐Ÿ‡ฎ๐Ÿ‡น Italian
nl๐Ÿ‡ณ๐Ÿ‡ฑ Dutch
pl๐Ÿ‡ต๐Ÿ‡ฑ Polish
cs๐Ÿ‡จ๐Ÿ‡ฟ Czech
ro๐Ÿ‡ท๐Ÿ‡ด Romanian
hu๐Ÿ‡ญ๐Ÿ‡บ Hungarian
sv๐Ÿ‡ธ๐Ÿ‡ช Swedish
da๐Ÿ‡ฉ๐Ÿ‡ฐ Danish
fi๐Ÿ‡ซ๐Ÿ‡ฎ Finnish
en-vi๐Ÿ‡ฌ๐Ÿ‡ง / ๐Ÿ‡ป๐Ÿ‡ณ English and Vietnamese
ko-en๐Ÿ‡ฐ๐Ÿ‡ท / ๐Ÿ‡ฌ๐Ÿ‡ง Korean and English
ja-en๐Ÿ‡ฏ๐Ÿ‡ต / ๐Ÿ‡ฌ๐Ÿ‡ง Japanese and English

Note

The language field in a response is a label inferred from the transcript or from a single-language mode. It can differ from the mode you sent, and an empty transcript in a multilingual mode can return unknown.

Full coverage on the Languages page.

NDJSON progress

POST/api/v1/stt/transcriptions?stream=true

Emits one complete JSON object per line as each chunk finishes, instead of waiting for the whole file. The response carries Content-Type application/x-ndjson.

curl
curl -N -X POST "https://studio.evomlabs.com/api/v1/stt/transcriptions?stream=true" \
  -H "Authorization: Bearer stt_sk_live_YOUR_KEY" \
  -F "audio=@meeting.mp3" \
  -F "language=vi"

Response

NDJSON
{"type":"started","duration_seconds":42.5,"chunks_total":2,"provider":"loli-asr","model":"Loli 2.0"}
{"type":"chunk","index":1,"chunks_total":2,"text":"...","combined_text":"...","language":"vi"}
{"type":"chunk","index":2,"chunks_total":2,"text":"...","combined_text":"...","language":"vi"}
{"type":"final","text":"...","language":"vi","is_final":true,"provider":"loli-asr","model":"Loli 2.0","latency_ms":5230}

Long audio is split into chunks of at most about 25 seconds, cut at a silent point near the boundary where possible. Read the type and chunks_total fields rather than deriving the number of chunks from the duration.

NDJSON
{
  "type": "error",
  "error": {
    "code": "stt_upstream_error",
    "message": "Error description"
  }
}

A stream can end with an error

If the pipeline fails after the response has started, the last line is an error object instead of a final one. Read line by line and handle both final and error.

Real-time WebSocket

WS/api/public/v1/stt/ws?token=<stt_key>&language=auto

Choose automatic VAD or manual commits for a continuous PCM16 microphone stream.

URL
wss://studio.evomlabs.com/api/public/v1/stt/ws?token=stt_sk_live_YOUR_KEY&language=auto

Start message

JSON
{
  "type": "start",
  "mode": "auto",
  "language": "auto",
  "sample_rate": 48000,
  "format": "pcm_s16le"
}
FieldTypeRequiredDefaultDescription
typestringYes-Use start.
mode"auto" | "manual"Noautoauto uses server VAD; manual waits for a client commit.
languagestringNoautoOne of the language modes listed above.
sample_rateintegerNo16000Integer binary-input rate from 8,000 to 192,000 Hz.
formatstringNopcm_s16leExactly pcm_s16le: mono signed 16-bit little-endian PCM.
session_idstringNoServer generatedOptional client-provided identifier.

In auto mode, send binary PCM16 mono continuously. The server resamples to the rate reported in started, detects speech and silence, sends replaceable partial events, and commits segment events at a silence boundary.

Send stop before closing

Send a stop message to end the session. The server then emits final, followed by closed. Closing the socket without sending stop cancels the session and does not guarantee a final transcript.

Manual mode for Push-to-Talk or client VAD

Start with mode manual. The server does not emit partials or detect silence. It buffers PCM after the previous commit and returns exactly one final segment when the client sends commit. A commit needs at least 0.2 seconds of audio; an empty or short commit produces a recoverable bad_request error, and a short buffer remains for more PCM. stop commits a sufficiently long trailing buffer, discards a shorter fragment, then sends final and closed.

JavaScript
// Start with: { "type": "start", "mode": "manual", ... }
// Send PCM16 frames, then commit exactly that buffered speech:
ws.send(JSON.stringify({ type: "commit" }));

// The server returns one final segment. Send commit again for the next turn,
// or stop to commit a sufficiently long trailing buffer and close the session.
ws.send(JSON.stringify({ type: "stop" }));

Server events

JSON
// Server to client

{ "type": "started", "session_id": "session_id", "mode": "auto", "language": "auto",
  "sample_rate": 16000, "provider": "loli-asr", "model": "Loli 2.0" }

{ "type": "partial", "index": 0, "text": "live window text",
  "committed_text": "", "live_text": "live utterance text",
  "combined_text": "live utterance text", "is_final": false,
  "language": "vi", "latency_ms": 210 }

{ "type": "segment", "index": 0, "text": "committed utterance",
  "committed_text": "committed utterance", "live_text": "",
  "combined_text": "committed utterance", "is_final": true,
  "language": "vi", "latency_ms": 350 }

{ "type": "final", "text": "Toร n bแป™ transcript", "language": "vi",
  "is_final": true, "provider": "loli-asr", "model": "Loli 2.0",
  "latency_ms": 4100 }

{ "type": "closed", "session_id": "session_id", "reason": "stopped" }

{ "type": "error", "session_id": "session_id-or-null",
  "message": "Error description", "code": "bad_request" }

Note

A partial can be replaced by a newer partial for the same utterance; a segment commits one utterance at a silence boundary. Use combined_text to display the transcript so far.

Examples

JavaScript
const ws = new WebSocket(
  "wss://studio.evomlabs.com/api/public/v1/stt/ws" +
  "?token=stt_sk_live_YOUR_KEY&language=auto"
);

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    type: "start",
    mode: "auto",
    language: "auto",
    sample_rate: 48000,
    format: "pcm_s16le",
  }));
});

ws.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);
  if (message.type === "partial" || message.type === "segment") {
    renderTranscript(message.combined_text);
  }
  if (message.type === "final") {
    renderTranscript(message.text);
  }
  if (message.type === "error") {
    console.error(message.code, message.message);
  }
});

// Send PCM16 frames with ws.send(arrayBuffer).
function stopTranscription() {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "stop" }));
  }
}

Security

The key travels in the query string, so it is exposed if the connection is opened from client-side code. Open the socket from your own server, over TLS. A missing or wrong key emits the error code unauthorized and the socket closes with code 1008.

A real-time session uses the same audio ceiling of 1,800 seconds.