Skip to main content
Version: 0.11

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 the file part; the server decodes and resamples it for you.
  • C FFI / Python — pass a base64-encoded WAV file in an audio_b64 field. The binding decodes the RIFF header to recover the sample rate automatically, so you don't pass a separate sample_rate.
  • Rust SDK — pass raw f32 PCM samples directly via audio_samples: Vec<f32> plus the sample_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.

FieldTypeDefaultDescription
modelstringrequiredModel id, e.g. in-memory::whisper
audio_samplesVec<f32>Raw PCM samples (Rust SDK; set programmatically)
sample_rateu32Sample rate of audio_samples
languagestring?auto-detectISO 639-1 code (en, ja, …)
response_formatstringjsonjson, text, or verbose_json
temperaturef640.0Decoder sampling temperature (0 = greedy)
timestamp_granularitiesstring[][]Any of segment, word

The response

The response deserializes from AudioTranscriptionResponse:

FieldTypeDescription
textstringThe transcribed text
languagestring?Detected or specified language
durationf32?Audio duration in seconds
segmentsarray?Segment-level timestamps (verbose_json)
wordsarray?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 valueVariantLanguages
whisper (default)base.enEnglish
whisper-tiny, whisper-tiny.entiny / tiny.enmulti / English
whisper-base, whisper-base.enbase / base.enmulti / English
whisper-small, whisper-small.ensmall / small.enmulti / English
whisper-medium, whisper-medium.enmedium / medium.enmulti / English
whisper-large-v3large-v3multi
whisper-large-v3-turbolarge-v3-turbomulti
distil-large-v3distil-large-v3multi

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