Skip to main content

Speech to Text

Transcribe and translate audio files using speech models through the platform API.

Overview

The platform provides OpenAI-compatible audio endpoints for turning recordings into text. If you already use the OpenAI SDK for transcription, switching over requires only changing the base_url -- no other code changes needed.

Key features:

  • OpenAI SDK compatible -- use the official Python or Node.js SDK, or plain multipart HTTP
  • Transcription and translation -- keep the spoken language, or translate speech into English
  • Multiple output formats -- json, plain text, verbose_json with segments, and ready-to-use srt / vtt subtitles
  • Streaming -- receive the transcript as it is produced, over Server-Sent Events
  • Long recordings -- files of up to 25 MB and 1891 seconds (31 minutes 31 seconds) are handled in a single request
Model availability

Speech models appear in your model list only where they are deployed. If you do not see one, contact your platform administrator.

Quick Start

Python

from openai import OpenAI

client = OpenAI(
base_url="https://api.bulutistan.ai/v1",
api_key="sk-proj-your-api-key"
)

with open("meeting.mp3", "rb") as audio:
transcript = client.audio.transcriptions.create(
model="your-speech-model",
file=audio,
language="en"
)

print(transcript.text)

Node.js

import OpenAI from 'openai';
import fs from 'fs';

const client = new OpenAI({
apiKey: 'sk-proj-your-api-key',
baseURL: 'https://api.bulutistan.ai/v1'
});

const transcript = await client.audio.transcriptions.create({
model: 'your-speech-model',
file: fs.createReadStream('meeting.mp3'),
language: 'en'
});

console.log(transcript.text);

cURL

curl -X POST https://api.bulutistan.ai/v1/audio/transcriptions \
-H "Authorization: Bearer sk-proj-your-api-key" \
-F "file=@meeting.mp3" \
-F "model=your-speech-model" \
-F "language=en"

API Reference

Endpoints

POST /v1/audio/transcriptions
POST /api/v1/audio/transcriptions

POST /v1/audio/translations
POST /api/v1/audio/translations

Both prefixes are supported for each endpoint. Use /v1/... when setting the SDK base_url to https://api.bulutistan.ai/v1. Use the full /api/v1/... path for direct HTTP requests.

  • Transcriptions return the speech in the language it was spoken in. Every speech-to-text model serves this endpoint.
  • Translations return English text regardless of the spoken language, and therefore take no language parameter. Not every speech model translates. The endpoint is served only by models whose operator has declared translation support; any other speech model is refused with 400 and the message "Model does not support translation. Use a model with translation capability." Transcription on the same model is unaffected.

Both endpoints accept multipart/form-data only -- send the audio as a file part, and every other parameter as a form field. Do not send a JSON body.

API keys need the inference:audio scope. Keys with full access or a general inference scope already cover it; keys created with a hand-picked scope list need inference:audio selected. See Authentication.

Request Parameters

ParameterTypeRequiredDescription
filebinaryYesThe audio file to transcribe. Up to 25 MB and 1891 seconds (31 minutes 31 seconds) -- whichever limit is reached first.
modelstringYesThe model identifier of a speech-to-text model.
languagestringNoInput language as an ISO-639-1 code (for example en, tr, de). Transcriptions only.
promptstringNoOptional text to guide the model's style or help it with names and terminology.
response_formatstringNoOutput format: json (default), text, verbose_json, srt, vtt. Translations support json, text and verbose_json.
temperaturefloatNoSampling temperature between 0 and 1. Lower values are more deterministic.
streambooleanNotrue streams the transcript over Server-Sent Events. Combines with json and text only.
timestamp_granularities[]stringNoOnly segment is supported, and only together with response_format=verbose_json. Transcriptions only.
seedintegerNoSampling seed, for a repeatable decode. Accepted on both endpoints.
top_pfloatNoNucleus sampling. Transcriptions only.
top_kintegerNoTop-k sampling. Transcriptions only.
min_pfloatNoMinimum-probability sampling. Transcriptions only.

The four sampling fields are forwarded to the model only when you send them; leaving one out is not the same as sending a default, and the platform picks no value on your behalf. They matter far less here than on text generation -- transcription is a task where the confident answer is almost always the right one. Reach for temperature=0 and a fixed seed when you need the same audio to produce the same transcript twice.

/v1/audio/translations takes only seed, because its upstream schema is narrower than the transcription one. top_p, top_k and min_p sent to it are refused rather than ignored.

Accepted upload formats: mp3, mp4, mpeg, mpga, m4a, wav, webm, flac, ogg. An individual model may accept a narrower list; the error message names the formats that model takes.

The field list is closed

Both endpoints accept the fields in the table above and nothing else. Any other multipart part is refused with a 400 naming the field in error.param, rather than being quietly dropped -- a parameter you believed was applied never was.

CodeWhat it means
unknown_parameterThe endpoint does not recognise the field name at all.
unsupported_parameterA real engine knob that is outside this platform's contract (for example hotwords, chunking_strategy, repetition_penalty, use_beam_search), or a transcription-only field sent to /v1/audio/translations (language, timestamp_granularities[], top_p, top_k, min_p).

chunking_strategy is in the second list on purpose: the platform does its own splitting of long recordings, and honouring a caller's chunking plan would contradict the spans the timeline and the billing are built on. See Long Recordings.

Newer OpenAI SDK fields are refused, not ignored

Recent OpenAI SDK releases added transcription fields this platform does not implement -- include[], known_speaker_names[] and known_speaker_references[]. Sending one returns 400 unknown_parameter. If an SDK upgrade starts sending a field on your behalf, that is what the error is telling you; pin the SDK version or stop passing the option.

Always pass language when you know it

If language is omitted, the model's configured default language is used rather than automatic detection. Passing the language explicitly is the reliable way to transcribe a recording whose language differs from that default.

Background jobs are not available for audio

Audio requests are synchronous. Sending background=true (or the async alias) returns 400 -- submit the request normally and read the response.

Which languages a model handles

A speech model can publish the languages it is configured for. They appear in the model's audio block on GET /v1/models and GET /v1/models/{id}, as two-letter ISO-639-1 codes:

{
"id": "your-speech-model",
"object": "model",
"audio": {
"default_language": "tr",
"supported_languages": ["tr", "en"]
}
}
  • default_language is what a request with no language decodes as.
  • supported_languages is the operator's declaration of what this deployment is set up for. Read it before hardcoding a language list of your own -- it is per model, and it changes when the operator changes it.

The key is absent on a model that declares no list, which is the ordinary case; treat that as "no list published" rather than as an empty one. The Client Portal Playground turns the language field into a picker of the declared codes where a model publishes them, and leaves it as free text where it does not.

Response Format

json (default)

{
"text": "Thanks everyone for joining the call today.",
"usage": {
"type": "duration",
"seconds": 128
}
}

The usage object reports the measured input duration -- the same measure the request is billed on.

text

Returns the transcript as text/plain, with no JSON envelope:

Thanks everyone for joining the call today.

verbose_json

{
"task": "transcribe",
"language": "en",
"duration": 128.4,
"text": "Thanks everyone for joining the call today.",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 24.2,
"text": "Thanks everyone for joining the call today."
}
]
}
FieldTypeDescription
taskstringtranscribe or translate.
languagestringThe language used for the transcription.
durationfloatMeasured input duration in seconds.
textstringThe full transcript.
segmentsarrayTimed segments covering the recording.

Word-level timestamps are not offered: timestamp_granularities[]=word returns 400.

srt and vtt

Subtitle files rendered from the timed segments, returned as text/plain (srt) and text/vtt (vtt):

curl -X POST https://api.bulutistan.ai/v1/audio/transcriptions \
-H "Authorization: Bearer sk-proj-your-api-key" \
-F "file=@interview.mp3" \
-F "model=your-speech-model" \
-F "language=en" \
-F "response_format=srt" \
-o interview.srt
Subtitle cue length

Cues cover blocks of up to 28 seconds rather than single spoken lines, because the model returns one segment per processed block. Timings line up with the audio, so the files play correctly, but if you need short caption lines you will want to split the cues in your own player or post-processing step.

Streaming

Set stream=true to receive the transcript incrementally over Server-Sent Events. The event shapes are OpenAI-compatible: transcript.text.delta events carry the incremental text, a final transcript.text.done event carries the complete transcript, and the stream closes with data: [DONE].

curl -N -X POST https://api.bulutistan.ai/v1/audio/transcriptions \
-H "Authorization: Bearer sk-proj-your-api-key" \
-H "Accept-Encoding: identity" \
-F "file=@meeting.mp3" \
-F "model=your-speech-model" \
-F "language=en" \
-F "stream=true"
data: {"type":"transcript.text.delta","delta":"Thanks everyone "}

data: {"type":"transcript.text.delta","delta":"for joining the call today."}

data: {"type":"transcript.text.done","text":"Thanks everyone for joining the call today.","usage":{"type":"duration","seconds":128}}

data: [DONE]
done replaces, it does not extend

The two event types carry the transcript in two different ways, and mixing them up duplicates the entire output.

  • transcript.text.delta carries an increment. Append every delta to what you already have.
  • transcript.text.done carries the complete transcript in its text field. It is a replacement, not the final increment.

If you accumulate the deltas and then append done.text to them, you end up with the transcript twice over. Either accumulate the deltas and use done purely as the end-of-stream signal, or ignore the deltas and take done.text as the answer. Do not do both.

Python

from openai import OpenAI

client = OpenAI(
base_url="https://api.bulutistan.ai/v1",
api_key="sk-proj-your-api-key"
)

with open("meeting.mp3", "rb") as audio:
stream = client.audio.transcriptions.create(
model="your-speech-model",
file=audio,
language="en",
response_format="text",
stream=True
)

for event in stream:
if event.type == "transcript.text.delta":
print(event.delta, end="", flush=True)
elif event.type == "transcript.text.done":
print()

Streaming combines with response_format=json and response_format=text only. Asking for verbose_json, srt or vtt together with stream=true returns 400 -- those formats need the full timeline before they can be rendered.

If an error occurs after the stream has started, the connection is not dropped silently: an error event is written and the stream still closes with data: [DONE], so your read loop terminates normally. See the Streaming guide for general SSE handling.

Long Recordings

Recordings longer than about half a minute are split into consecutive blocks on the server, transcribed in order, and merged back into one transcript before the response is returned. The blocks are all the same length -- the recording is divided evenly, not cut into full-size blocks with a short remainder at the end. Consecutive blocks overlap slightly so a word landing on a cut survives, and the duplicate wording is removed during the merge.

For verbose_json, srt and vtt, segment timings are placed on the timeline of the original file: each block's segments are shifted by that block's start offset and then clipped to the block's own non-overlapping display window, so the overlap never produces two cues competing for the same instant. Subtitles stay in sync with the audio you uploaded, from the first cue to the last.

This is transparent to the caller -- one request, one response, one charge for the input duration -- but it is worth knowing about for two reasons:

  • Latency grows with duration. Blocks are processed sequentially, so a 20-minute recording takes noticeably longer than a 2-minute one. Use stream=true if you want text to appear while the rest is still being processed.
  • The limit is honest and immediate. The ceiling of 1891 seconds is derived from how many of those blocks one request will process; it is a property of the request shape, not a timeout and not a size limit. A longer file is refused up front with 400 audio_too_long -- before any processing, before any charge -- rather than failing minutes later. Split the recording and send the parts.

Non-Speech Audio

Speech models describe what they hear, and silence or non-speech passages can therefore appear in the transcript as a short bracketed note (for example (Silence)) rather than as an empty string. These annotations are passed through as the model produced them. If your application needs a strictly verbatim transcript, filter bracketed segments on your side, or trim leading and trailing silence before uploading.

Error Handling

StatusError codeCauseResolution
400Bad RequestMissing file or model, an empty file, an unsupported container format, word timestamp granularity, stream combined with a subtitle format, background=true, or /v1/audio/translations called with a model that does not translate.Check the parameters against the reference above.
400unknown_parameterThe request carries a multipart field the endpoint does not recognise. error.param names it.Remove it. Only the documented fields are accepted.
400unsupported_parameterA real engine knob outside the platform contract, or a transcriptions-only field sent to /v1/audio/translations. error.param names it.Remove it. See The field list is closed.
400audio_too_longThe recording is longer than 1891 seconds (31 minutes 31 seconds), the most one request transcribes.Split the recording into shorter parts.
400audio_duration_unreadableThe file's duration could not be read -- it may be truncated, empty, or not the format its extension claims.Re-export the file and try again.
401UnauthorizedMissing or invalid API key.Verify your API key is correct and active.
402model_not_pricedThe model has no audio pricing configured.Contact your platform administrator.
403ForbiddenThe API key is not permitted to call this model, or the model belongs to another organization.Use a key whose scope covers audio inference and the requested model.
404Not FoundThe model does not exist.Check available models in the Client Portal or via the models endpoint.
413Payload Too LargeThe upload exceeds the 25 MB ceiling. It is enforced while the body streams in, so an oversized file is cut off rather than buffered.Compress the audio or split the recording.
429too_many_requestsYour key's request rate or concurrency limit was reached.Back off and retry. See the Rate Limiting guide.
429model_at_capacityThe model deployment is already transcribing as many requests as it can at once. limit_type is model_concurrent_requests, Retry-After is 10.Retry after the Retry-After interval.
429model_long_audio_at_capacityThe model deployment is at its separate ceiling for long recordings. limit_type is model_concurrent_long_requests, Retry-After is 30.Retry after the Retry-After interval, or send a shorter file.
502transcription_corruptedThe model returned an unusable transcript and the result was discarded. The request is not charged.Retry. If it persists, the recording may be unusually noisy or near-silent.
503billing_unavailableThe metering path is temporarily unreachable, so inference is held back for safety. Retry-After is 10.Retry. Nothing about your account changed.
503audio_processing_unavailableThe audio processing path is temporarily unavailable. Retry-After is 5.Retry after the Retry-After interval.
503model_unavailableThe model is starting, restarting, or not currently serving. Retry-After is 30.Retry with backoff, or pick another model.
504audio_timeoutThe request exceeded the processing deadline. error.type is timeout_error.Send a shorter file.

The two 429 codes that name a model are about that deployment, not about you: someone else's traffic can trigger them while your own limits are untouched. too_many_requests is the per-tenant one. All three are worth retrying; only the per-tenant one is worth adjusting your own pacing for.

Errors use the standard platform error envelope:

{
"error": {
"message": "The audio file is 2340 seconds long, which exceeds what this endpoint transcribes in a single request. Split the recording and try again.",
"type": "invalid_request_error",
"code": "audio_too_long"
}
}

Python Error Handling

from openai import (
OpenAI,
APIError,
RateLimitError,
AuthenticationError,
BadRequestError,
NotFoundError
)
import time

client = OpenAI(
api_key="sk-proj-your-api-key",
base_url="https://api.bulutistan.ai/v1"
)

def transcribe_with_retry(path, max_retries=3):
for attempt in range(max_retries):
try:
with open(path, "rb") as audio:
return client.audio.transcriptions.create(
model="your-speech-model",
file=audio,
language="en"
)

except AuthenticationError:
print("Invalid API key. Check your credentials.")
raise

except NotFoundError:
print("Model not found. Verify the model identifier.")
raise

except BadRequestError as e:
# Includes audio_too_long; retrying will not help.
print(f"Invalid request: {e.message}")
raise

except RateLimitError:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt
print(f"Rate limited. Retrying in {wait}s...")
time.sleep(wait)

except APIError as e:
if attempt == max_retries - 1:
raise
if e.status_code >= 500:
wait = 2 ** attempt
print(f"Server error. Retrying in {wait}s...")
time.sleep(wait)
else:
raise

transcript = transcribe_with_retry("meeting.mp3")
print(transcript.text)

Playground

Speech models are available in the Client Portal Playground: upload a file or record from your microphone, pick the output format, and read the transcript without writing any code. prompt, temperature and segment timestamps have their own controls there, and a Task selector switches between transcription and translation on models that translate. See the Playground guide.

Rate Limits & Pricing

Audio requests count against the standard per-key request rate limits. See the Rate Limiting guide for the limits and response headers.

How transcription is billed:

  • Measured in seconds of input audio. The duration of the file you upload is measured and rounded up to the whole second, once per request. Output length has no effect on the price.
  • Communicated per minute. Per-model rates are shown in USD per minute in the Client Portal; the underlying measurement stays per second, so a 90-second recording is billed as one and a half minutes, not two.
  • Only successful responses are billed. A request that fails with a server-side error (5xx), including a discarded transcript, is recorded for your visibility but carries no charge.
  • Pay as you go. Audio usage is not drawn from plan included-token allowances -- it is billed at your organization's audio rate on top of the plan. See Plans and Usage.

Long recordings are split internally, but you are charged once for the input duration -- the internal split does not multiply the charge.

tip

Trim silence and non-speech padding before uploading. Billing follows the length of the file you send, so a tighter file is both cheaper and faster.