Speech
POST https://api-staging.impossiblecarrot.cc/api/v1/audio/speech
Text in, audio out. The response body is the recording itself — no JSON envelope — in the codec you asked for. Requires a bearer API key.
Routed only to models whose output modality is speech, and only to endpoints that declare the
voice, the codec and the speed you asked for. An endpoint speaks the voices it loaded, so one it did
not is a different endpoint's request. Routing and provider preferences otherwise work as on chat.
Use a voice sample
Send a sample instead of selecting a named voice. Choose an endpoint with supports_voice_cloning: true;
reference input is required capability, so an endpoint that cannot clone the sample is excluded.
The sample takes precedence over voice. It is sent for this request and does not create a saved voice.
curl https://api-staging.impossiblecarrot.cc/api/v1/audio/speech \
-H "Authorization: Bearer $HEIMDALL_API_KEY" \
-F model=openbmb/voxcpm2 \
-F 'input=Hello, this is a new sentence in the reference voice.' \
-F file=@voice.wav \
-F response_format=wav \
--output speech.wav
WAV, MP3, FLAC, OGG, M4A and WebM uploads are accepted, up to 5 MiB. The endpoint must accept the
sample format. A short, clear recording works best. Add a transcript form field if the sample's words
are known. Other form fields follow the JSON request; provider is a JSON-encoded object.
For JSON clients, send input_references: [{type: "input_audio", input_audio: {data: "BASE64_BYTES", format: "wav"}, transcript: "The sample's words"}].
A sample may carry role: "reference", which is the one role an audio item takes and the meaning a
sample naming no role already has.
The data member contains raw base64, without a data-URL prefix. Named voices continue to work when no
sample is supplied.
Voices
GET /speech/models lists every speech model with the voices, codecs and speed range
its endpoints declare, and that is the list voice is matched against. Providers name their own
voices; nothing obliges an endpoint to load the names the OpenAI SDKs enumerate — alloy, ash,
ballad, coral, echo, fable, nova, onyx, sage, shimmer, verse — and those SDKs make
voice a required argument, so one of the eleven is what an unchanged client sends.
Each of them lands in one of three places:
- An endpoint of the model declares it. It is routed to that endpoint and spoken as asked.
- None declares it, and the model's endpoints offer one voice between them. That voice is
spoken and named back in the
X-Heimdall-Voiceresponse header. An endpoint offers a voice when it declares one calleddefault, or when it declares exactly one voice — the two cases where standing in for a voice it does not have involves no choice. - None declares it, and the endpoints offer several voices or none. The request is a
400whose message lists the voices the model is served with, andmetadata.declared_voicescarries them as an array. Voices do not sound alike, so picking one of several for you would serve a voice you did not choose and bill you for it.
Leaving voice out asks for whichever voice the endpoint defaults to. No X-Heimdall-Voice is sent
then: the default is a voice only the endpoint knows the name of.
Request body
| Field | Type | Notes |
|---|---|---|
model | string | Required. |
input | string | Required. The text to synthesize, at most 15,000 characters. |
voice | string | A voice the endpoint declares. Absent uses the endpoint's own default. |
response_format | string | mp3 (default), wav, pcm, opus, flac, aac, mulaw, alaw. |
speed | number | 0.25 to 4.0, honoured where the endpoint declares a range covering it. |
seed | integer | 0 to 2,147,483,647, where the endpoint declares seeded sampling. |
max_new_tokens | integer | 1 to 4,096 on the public wire; the selected endpoint may declare a narrower range. |
instructions | string | Free text steering delivery, where the endpoint honours it. |
input_references | array | One input_audio voice sample to clone, where the endpoint declares it. |
user | string (≤ 256) | Opaque end-user identifier. |
provider | object | Provider preferences. |
provider.options carries a provider's own knobs, keyed by provider slug. Only the entry for the provider
that serves the request is read, and a name that provider published no descriptor for is refused with a
400 naming it rather than forwarded.
Example
curl https://api-staging.impossiblecarrot.cc/api/v1/audio/speech \
-H "Authorization: Bearer $HEIMDALL_API_KEY" \
-H "Content-Type: application/json" \
-o speech.wav \
-d '{
"model": "openbmb/voxcpm2",
"input": "The sky above the port was the color of television.",
"voice": "alloy",
"response_format": "wav",
"max_new_tokens": 2048
}'
Response
The body is the encoded audio, served with the codec's own content type:
response_format | Content-Type |
|---|---|
mp3 | audio/mpeg |
wav | audio/wav |
pcm | audio/pcm |
opus | audio/ogg; codecs=opus |
flac | audio/flac |
aac | audio/aac |
mulaw | audio/basic |
alaw | audio/x-alaw-basic |
X-Heimdall-Voice is sent when the voice that spoke is not the one you named, carrying the name of
the voice that did. X-Generation-Id and X-Provider-Name are sent as on chat completions, and what the call cost is
read back from GET /generation under that id. There is no
Content-Length: an endpoint that encodes as it decodes has no length to declare, and one that
answers whole would have to be buffered here to find out. Where a provider streams its audio the body
is relayed as it arrives, so a long passage starts playing before it finishes decoding.
A failed synthesis comes back as a provider error with the endpoint's own message, and nothing is
billed. Once the first byte of audio has gone out the status is fixed at 200, so a failure after
that arrives as a body that stops early rather than as an error document.
The reference VoxCPM2 endpoint declares max_new_tokens from 3 to 4,096. A cap below 3 produces no
audio on its serving engine. It declares no seed: its engine draws the sound's noise from a
generator no request seeds, so a seed would change nothing.
Billing
Billed per character of input, counted by the marketplace from your request. The count is known
before the request is sent, so a synthesis costs exactly what its text does and nothing reconciles
afterwards. An endpoint that publishes no character price records the characters as unpriced rather
than billing them at zero.
Seconds of audio produced are not billed. Where the codec states its own duration exactly — wav
does, every compressed container does not — the figure is measured and published as a deployment
metric. What you pay for is the text you submitted; how long an engine takes to say it is the
engine's own pacing, and two callers submitting the same sentence pay the same amount.
Voice cloning input is your audio: it is forwarded to the endpoint for that one request and retained nowhere.
Discovery
GET https://api-staging.impossiblecarrot.cc/api/v1/speech/models
Every served speech model, in the same shape GET /models answers in, plus a
speech object holding the union across the model's live endpoints:
{
"id": "openbmb/voxcpm2",
"name": "OpenBMB: VoxCPM 2",
"architecture": { "input_modalities": ["text"], "output_modalities": ["speech"] },
"speech": {
"voices": ["default", "narrator"],
"formats": ["mp3", "wav"],
"min_speed": 0.5,
"max_speed": 2
},
"links": { "details": "/api/v1/models/openbmb/voxcpm2/endpoints" }
}
A union is what the marketplace can be asked for somewhere, not what any one endpoint keeps: one
endpoint speaks each of these voices and no endpoint speaks all of them. What a single endpoint
speaks is speech_envelope on the endpoints listing links.details names, which is the reading to
use when you pin a provider with provider.only. min_speed and max_speed are present together
or not at all; absent, no endpoint declared a range and the model speaks at its own pace only.