SovrGPT Docs
API

Audio (speech · TTS & STT)

OpenAI-compatible speech synthesis (text-to-speech) and transcription (speech-to-text), EU-sovereign.

SovrGPT offers text-to-speech (/v1/audio/speech) and speech-to-text (/v1/audio/transcriptions) in the OpenAI-compatible format. Existing OpenAI audio clients work without a code change — just point baseURL at us.

Both endpoints require a valid SovrGPT API key with the matching capability scopespeech for TTS, transcribe for STT (see Authentication → Permissions).

SovrGPT offers three TTS engines, all EU-sovereign — selectable per request through the OpenAI-standard model field (supertonic-3 / cosyvoice-3 / voxtral-mini-tts) or the explicit provider field (derived from the voice if you set neither):

providerStrengthVoicesEmotion / tagsCloningFormat
supertonic (default)fast, no cold startM1M5 / F1F5<breath>/<sigh>wav/flac/ogg
cosyvoiceGerman + emotion + cloningde-thorsten or your own voicefull inline tags plus natural-language emotion✅ zero-shotwav (24 kHz)
mistralSaaS fallbackdefault-de/default-enmp3/wav/opus/flac
  • STT runs on Mistral Voxtral (Paris, GDPR).
  • Without provider you stay on the server default (Supertonic) — existing calls do not change.

AI disclosure: synthetically generated speech must be labelled as AI content (EU AI Act Art. 50). Supertonic weights are under OpenRAIL-M; CosyVoice carries no built-in watermark — the labelling happens at the application level.


POST /v1/audio/speech — text-to-speech

Produces spoken audio bytes from text. The response is the raw audio body (no JSON wrapper), just like OpenAI's.

curl https://sovrgpt.com/api/v1/audio/speech \
  -H "Authorization: Bearer $SOVR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "supertonic-3",
    "input": "Guten Tag <breath> willkommen bei SovrGPT.",
    "voice": "F3",
    "response_format": "wav"
  }' --output hello.wav

Request body

FieldTypeApplies toDescription
inputstringallRequired. The text to speak, 4,000 characters or fewer. Inline tags depend on the provider (see below).
voicestringallSupertonic: M1M5/F1F5. CosyVoice: de-thorsten. Mistral: default-de/default-en.
providerstringallsupertonic | cosyvoice | mistral. Optional — otherwise derived from model/voice, otherwise the server default (supertonic).
response_formatstringSupertonic/Mistralwav/flac/ogg (Supertonic), plus mp3/opus/aac (Mistral). CosyVoice always returns wav.
languagestringSupertonic/CosyVoiceISO 639-1 (de/en/…).
emotion / instructionstringCosyVoiceNatural-language style or emotion instruction, e.g. "Sprich sehr traurig und langsam."
reference_audio_b64stringCosyVoiceBase64 wav/flac (30 s or less) → clones that voice (zero-shot). Overrides voice.
prompt_textstringCosyVoiceTranscript of the reference clip (best quality). Automatic for built-in voices.
speednumberCosyVoice0.5–2.0 (default 1.0). Supertonic/Mistral: accepted, currently ignored.
modelstringallSelects the engine (the OpenAI-idiomatic way): supertonic-3, cosyvoice-3 or voxtral-mini-tts. provider takes precedence if both are set.

Response

200 with audio bytes. Headers:

Content-Type: audio/wav        (or audio/flac, audio/mpeg …)
X-Voice-Provider: supertonic   (or: cosyvoice / mistral)

Errors: 400 (missing or over-long input, invalid voice), 401/403 (auth/scope), 503 (TTS provider not configured or the CosyVoice worker is cold-starting → resend in about a minute), 502 (upstream error).


CosyVoice — German with emotion, tags and your own voice

For expressive German, emphasis, laughter/breathing and voice cloning, pick the cosyvoice provider. Full capabilities: Models → Voice.

Inline tags (directly inside input): [laughter], [breath], <laughter>…</laughter> (spoken while laughing), <strong>…</strong> (emphasis).

# German with tags and emphasis
curl https://sovrgpt.com/api/v1/audio/speech \
  -H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
  -d '{
    "provider": "cosyvoice",
    "voice": "de-thorsten",
    "input": "Und dann [breath] öffnete ich die Tür. [laughter] Das war <strong>unglaublich</strong>."
  }' --output tags.wav

Emotion — in natural language via emotion (or instruction):

curl https://sovrgpt.com/api/v1/audio/speech \
  -H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
  -d '{
    "provider": "cosyvoice",
    "voice": "de-thorsten",
    "input": "Diese Zeiten sind für immer vorbei.",
    "emotion": "Sprich sehr traurig, leise und langsam."
  }' --output sad.wav

Your own voice (zero-shot cloning) — pass a 10–20 second reference clip as base64, plus its transcript as prompt_text for the best quality:

REF=$(base64 -w0 my_voice.wav)
curl https://sovrgpt.com/api/v1/audio/speech \
  -H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
  -d "{
    \"provider\": \"cosyvoice\",
    \"input\": \"A new sentence, spoken in my cloned voice.\",
    \"reference_audio_b64\": \"$REF\",
    \"prompt_text\": \"<verbatim transcript of the reference clip>\"
  }" --output cloned.wav

Built-in voices: currently de-thorsten (German, male, calm; CC0). Your own voices at any time via reference_audio_b64 (zero-shot, no training). A persistent voice library with an upload UI is in preparation.

Cold start: the CosyVoice GPU worker scales to zero. Warm it delivers in about 6 seconds; a cold worker can take minutes — you then get 503 "warming up", so simply resend after about a minute. Supertonic (the default) has no cold start.


POST /v1/audio/transcriptions — speech-to-text

Transcribes an uploaded audio file (multipart/form-data).

curl https://sovrgpt.com/api/v1/audio/transcriptions \
  -H "Authorization: Bearer $SOVR_KEY" \
  -F file=@recording.webm \
  -F language=de \
  -F response_format=json

Form fields

FieldTypeDefaultDescription
filefileAudio (webm/ogg/mp3/wav/m4a …), 4.5 MiB or less — see the note below. Either file or upload_id.
upload_idstringIdentifier from POST /v1/audio/uploads. With it, the audio does not travel in the request body: up to 100 MiB. See "Large files" below.
languagestringISO 639-1, e.g. de. Mirrored back in the response; without it, language in the response stays null — this endpoint has no automatic language detection. Recognition itself works without it.
response_formatstringjsonjson ({ "text": … }), text (plain text), verbose_json ({ text, language, duration }, plus segments when requested).
timestamp_granularitiesstringsegment or word — adds a segments field with timestamps to the verbose_json response. ⚠️ Exactly one value. segment,word is rejected with 400.
context_biasstringRepeatable. Proper nouns and domain terms recognition should be pulled towards — product or customer names, say. A term must contain no space (hyphens are fine); terms with spaces are discarded. At most 100 entries.
format_textbooleanfalseAdditionally formats the transcript: paragraphs, a numbered list from spoken "first/second/third", a blank line after the salutation, a separated closing. See below.
modelstringAdvisory (voxtral-mini-transcribe).

The effective size limit is 4.5 MiB, not 25. The audio travels in the request body, and our platform truncates request bodies at 4.5 MiB — before this endpoint runs. A larger file therefore gets no answer from us at all, but a FUNCTION_PAYLOAD_TOO_LARGE error from the platform.

Until 2026-08-31 this said "25 MiB or less". That is the limit our code checks — it simply was never reached for oversized files. Thanks to the reporter who measured the difference (3.84 MB gets through, 4.80 MB does not).

In practice: use Opus or MP3 rather than WAV. 4.5 MiB of Opus at 32 kbit/s is roughly 20 minutes of speech; uncompressed WAV does not even last three.

For larger files there has been a second route since 2026-09-01upload_id instead of file. That gives you 100 MiB, because the bytes then never pass through our function at all. See the next section.

Large files: POST /v1/audio/uploads

The 4.5 MiB is a property of our platform, not a decision of ours — it cannot be fixed by putting a larger number in our code. The way around it is to place the audio past our function: you fetch a signed address, upload straight to it, and at transcription time you only name the identifier.

# 1. Fetch a ticket — same key, same `transcribe` scope
curl -s -X POST https://sovrgpt.com/api/v1/audio/uploads \
  -H "Authorization: Bearer $SOVR_KEY" \
  -H "content-type: application/json" \
  -d '{"mime":"audio/ogg","size":18234012}'
{
  "upload_id": "0f1c…",
  "upload_url": "https://…/storage/v1/object/upload/sign/api-audio/…",
  "token": "…",
  "content_type": "audio/ogg",
  "max_bytes": 104857600
}
# 2. Upload the bytes directly — NOT to us
curl -s -X PUT "$upload_url" \
  -H "content-type: audio/ogg" \
  --data-binary @recording.ogg

# 3. Transcribe, with no file in the body
curl -s -X POST https://sovrgpt.com/api/v1/audio/transcriptions \
  -H "Authorization: Bearer $SOVR_KEY" \
  -H "content-type: application/json" \
  -d '{"upload_id":"0f1c…","language":"de","response_format":"verbose_json","timestamp_granularities":"segment"}'
FieldTypeDescription
mimestringRequired. audio/mpeg, audio/mp4, audio/x-m4a, audio/aac, audio/wav, audio/webm, audio/ogg, audio/opus, audio/flac. Any other type is rejected with 415before the upload, not after.
sizenumberRequired. Size in bytes. Above 100 MiB → 413.

Three things worth knowing:

  • A ticket is single-use. After transcription the file is deleted — including when the transcription failed. A retry needs a new ticket. That is deliberate: no audio archive nobody asked for should build up here.
  • You are billed for the transcription, not for the ticket. A ticket you never redeem costs nothing.
  • The ticket route requires the same scope (transcribe) as transcription itself. It is not a side entrance with weaker permissions.

100 MiB is our limit, not our provider's. We have not measured what file size and runtime the recognition service accepts. For very long recordings it may therefore still make sense to cut them up — 100 MiB of Opus at 32 kbit/s would be roughly seven hours.

Use context_bias for proper nouns only. The parameter pulls the text towards the listed terms. On everyday vocabulary it therefore distorts words that were recognised correctly — a listed "Refactoring" turned a correctly transcribed "refactoren" into "refactoring".

Response

{ "text": "Hallo, dies ist ein Test." }

With response_format=verbose_json and timestamp_granularities:

{
  "text": "Die Bundeswehr braucht eine souveräne Plattform. Der letzte Satz ist genau der, den wir prüfen wollen.",
  "language": "de",
  "duration": 7,
  "segments": [
    { "text": "Die Bundeswehr braucht eine souveräne Plattform.", "start": 0.5, "end": 3.3, "speakerId": null },
    { "text": "Der letzte Satz ist genau der, den wir prüfen wollen.", "start": 3.6, "end": 6.9, "speakerId": null }
  ]
}

segments appears in the response only if timestamp_granularities was set — an empty field would be indistinguishable from "nothing recognised". The timestamps always refer to the unformatted text: format_text is a second model pass with its own wording, and there are no timestamps for it.

speakerId is always null today.

Errors: 400 (neither file nor upload_id, or two values in timestamp_granularities), 404 (upload_id unknown — or the bytes were never uploaded), 413 (over 4.5 MiB on the body route), 401/403 (auth/scope), 503 (MISTRAL_API_KEY not set), 502 (upstream error).

format_text — a transcript with structure

A raw transcript is a wall of text: no paragraphs, no lists, no blank line after the salutation. That is not a quirk of our provider — no speech-to-text service on the market produces that; "smart formatting" everywhere means punctuation and capitalisation only. Structure can only come from a model that understands the content.

With format_text=1 a second pass therefore runs over a model with audio understanding. Both passes start at the same time, so the response time is that of the slower one, not the sum.

curl https://sovrgpt.com/api/v1/audio/transcriptions \
  -H "Authorization: Bearer $SOVR_KEY" \
  -F file=@dictation.webm \
  -F format_text=1 \
  -F response_format=verbose_json
{
  "text": "Sehr geehrte Frau Dr. Schmitt,\n\nvielen Dank …\n\n1. Die Übertragung …\n2. Die Antwortzeiten …\n\nMit freundlichen Grüßen\nHans Elstner",
  "text_raw": "Sehr geehrte Frau Dr. Schmitt, vielen Dank … Erstens die Übertragung …",
  "language": null,
  "duration": 101,
  "formatting": { "applied": true, "verdict": "ok", "coverage": 0.996 }
}

The exact wording is protected. A model asked to format can also rephrase or shorten — and the result then looks flawless even though content is missing. Every formatted version is therefore checked against the raw transcript before it is returned:

formatting.verdictMeaningWhat you get
okOnly the structure changed.the formatted version
auffaelligIndividual words differ.the formatted version — read text_raw against it
— (applied: false)The wording was altered, or the pass failed.the raw transcript, plus formatting.reason

text_raw is always included when format_text=1, even if the formatting was applied. A client that depends on the literal text should always read text_raw and ignore text.

format_text is deliberately off by default. Existing integrations get exactly the same bytes as before. If you want structure, switch it on.

Limits of the formatted path. It uses a different model from the raw path and therefore does not know context_bias, speaker separation or word-level timestamps — those affect text_raw only. It also processes at most about 20 minutes of audio per request (the raw path handles considerably more). For dictation that is plenty; for recordings of long meetings it is not.


SDK example (OpenAI client)

from openai import OpenAI

client = OpenAI(base_url="https://sovrgpt.com/api/v1", api_key=SOVR_KEY)

# Text-to-speech (Supertonic, fast)
client.audio.speech.create(
    model="supertonic-3", voice="F3", response_format="wav",
    input="Hallo Welt <sigh> das war ein langer Tag.",
).stream_to_file("hello.wav")

# Text-to-speech (CosyVoice, German + emotion)
# model="cosyvoice-3" picks the engine; emotion (a SovrGPT extension) via extra_body
client.audio.speech.create(
    model="cosyvoice-3", voice="de-thorsten", response_format="wav",
    input="Was für ein wunderbarer Tag!",
    extra_body={"emotion": "Sprich fröhlich und aufgeregt."},
).stream_to_file("happy.wav")

# Speech-to-text
with open("recording.webm", "rb") as f:
    tr = client.audio.transcriptions.create(
        model="voxtral-mini-transcribe", file=f, language="de",
    )
print(tr.text)

In the chat

Without the API, speech works directly in the chat: the microphone icon in the composer dictates (STT → text in the input field), and the speaker button on any assistant answer reads it aloud (TTS). See Models → Voice.

Audio (speech · TTS & STT)