Speech-to-Text
Atelico transcribes audio on-device with OpenAI Whisper — from the tiny
English-only build all the way up to large-v3 and large-v3-turbo, including
quantized GGUF variants for memory-constrained deployments. As with the rest of
the engine, there are no API keys and no network calls: the audio never leaves
the machine.
STT is wired into the in-memory:: backend and exposed over the
OpenAI-compatible route POST /v1/audio/transcriptions, plus every native SDK.
Quick start
Over HTTP, upload an audio file as multipart form data:
curl http://localhost:11434/v1/audio/transcriptions \
-H "Content-Type: multipart/form-data" \
-F model=in-memory::whisper \
-F file=@speech.wav
Response:
{
"text": "Hello from Atelico.",
"language": "en",
"duration": 1.4
}
in-memory::whisper resolves to the default base.en build. Other ids select
specific Whisper sizes — see Choosing a model.
Audio input
How you pass audio depends on the surface:
- HTTP — upload a file (
WAV,MP3,FLAC, …) as thefilepart; the server decodes and resamples it for you. - C FFI / Python — pass a base64-encoded WAV file in an
audio_b64field. The binding decodes the RIFF header to recover the sample rate automatically, so you don't pass a separatesample_rate. - Rust SDK — pass raw f32 PCM samples directly via
audio_samples: Vec<f32>plus thesample_rate. This skips WAV encode/decode entirely, which is the cheapest path for live-mic capture where you already have float frames in hand.
Whisper operates internally at 16 kHz mono; audio at other rates is resampled before inference.
The request
The fields below come from AudioTranscriptionRequest in
atelico-audio/src/audio_types.rs. Note that audio_samples and sample_rate
are marked #[serde(skip)] — they are populated programmatically (or decoded
from the uploaded file / audio_b64), not parsed from the JSON body.
| Field | Type | Default | Description |
|---|---|---|---|
model | string | required | Model id, e.g. in-memory::whisper |
audio_samples | Vec<f32> | — | Raw PCM samples (Rust SDK; set programmatically) |
sample_rate | u32 | — | Sample rate of audio_samples |
language | string? | auto-detect | ISO 639-1 code (en, ja, …) |
response_format | string | json | json, text, or verbose_json |
temperature | f64 | 0.0 | Decoder sampling temperature (0 = greedy) |
timestamp_granularities | string[] | [] | Any of segment, word |
The response
The response deserializes from AudioTranscriptionResponse:
| Field | Type | Description |
|---|---|---|
text | string | The transcribed text |
language | string? | Detected or specified language |
duration | f32? | Audio duration in seconds |
segments | array? | Segment-level timestamps (verbose_json) |
words | array? | Word-level timestamps (when word granularity is requested) |
Each segments entry is { start, end, text }; each words entry is
{ start, end, word } (all times in seconds).
Languages and auto-detection
English-only builds (*.en) always decode English. Multilingual builds
(whisper-large-v3, large-v3-turbo, distil-large-v3, and the non-.en
small/base/tiny variants) insert a language token into the decoder prompt: if you
set language to an ISO 639-1 code that token is forced, otherwise the model
auto-detects and reports the result in the language response field.
Timestamps
Pass response_format=verbose_json to receive segments with segment-level
timing. To additionally get word-level timing, request
timestamp_granularities[]=word. Word timestamps are useful for caption
karaoke, subtitle alignment, or syncing on-screen events to spoken words.
Choosing a model
After the in-memory:: prefix, the id selects the Whisper build:
model value | Variant | Languages |
|---|---|---|
whisper (default) | base.en | English |
whisper-tiny, whisper-tiny.en | tiny / tiny.en | multi / English |
whisper-base, whisper-base.en | base / base.en | multi / English |
whisper-small, whisper-small.en | small / small.en | multi / English |
whisper-medium, whisper-medium.en | medium / medium.en | multi / English |
whisper-large-v3 | large-v3 | multi |
whisper-large-v3-turbo | large-v3-turbo | multi |
distil-large-v3 | distil-large-v3 | multi |
Smaller builds are faster and lighter; larger builds are more accurate,
especially on accented or noisy audio. large-v3-turbo is the best
accuracy-per-millisecond pick for multilingual work, and quantized GGUF builds
(Q5_0) cut the memory footprint substantially — the HTTP route auto-selects a
quantized variant when the requested id resolves to one in the asset store.
SDK usage (in-process)
Rust SDK
The Rust SDK takes f32 PCM samples directly — no WAV round-trip:
use atelico_sdk::{Engine, AudioTranscriptionRequest};
let engine = Engine::new()?;
// Decode a WAV off disk to (samples, sample_rate), or use live-mic frames.
let (samples, sample_rate) =
atelico_audio::processing::wav_io::read_wav_file(std::path::Path::new("speech.wav"))?;
let request = AudioTranscriptionRequest {
model: "in-memory::whisper".into(),
audio_samples: samples,
sample_rate,
..Default::default()
};
let result = engine.audio().transcribe_sync(request)?;
println!("{}", result.text);
if let Some(lang) = result.language {
println!("language: {lang}");
}
transcribe_sync is the blocking call; transcribe is its async sibling, and
transcribe_async hands back a one-shot StreamHandle<AudioTranscriptionResponse>
for polling from a game loop.
Python
The Python binding takes a base64-encoded WAV file in audio_b64:
import base64, json
from atelico import Engine
engine = Engine()
wav_b64 = base64.b64encode(open("speech.wav", "rb").read()).decode()
result = json.loads(engine.audio_transcribe(json.dumps({
"model": "in-memory::whisper",
"audio_b64": wav_b64,
"language": "en", # optional; omit to auto-detect
})))
print(result["text"])
C / FFI
// Blocking STT — audio_b64 is a base64-encoded WAV file.
const char* request =
"{\"model\":\"in-memory::whisper\",\"audio_b64\":\"UklGRn...\",\"language\":\"en\"}";
const char* response_json = NULL;
atelico_audio_transcribe(engine, request, &response_json);
// response_json: {"text":"Hello from Atelico.","language":"en","duration":1.4}
The output pointer is owned by the FFI layer's thread-local return buffer — copy
it before the next API call on the same thread. The language field is optional;
omit it to let multilingual models auto-detect.
Streaming / live-mic transcription
For real-time use the engine ships a StreamingTranscriber that accumulates
audio, detects speech boundaries with energy-based voice-activity detection
(VAD), and emits TranscriptionChunk { text, start_time, end_time, is_final } as
phrases complete. It is generic over the STT backend (default WhisperStt) so
future models slot in unchanged. This is the recommended path for dictation and
live captioning where you want partial results before the speaker stops talking.
See also
- Text-to-Speech — Kokoro synthesis, the inverse path
- Models — full supported-model catalogue
- Server Configuration — STT-related env vars