Automatic spoken language detection with cURL & open source
Detect spoken language directly from audio with a multilingual Whisper model, served locally by whisper.cpp. cURL sends the recording to its HTTP endpoint, which returns a language candidate and a transcript to check against the recording. Once the software and model are downloaded, inference runs on your computer without an account or cloud API.
System requirements
Use Linux with Bash, a POSIX shell (sh), a C++17 compiler, Make, CMake, cURL, FFmpeg,
Python 3, and the usual tar, gzip, dirname, grep, mktemp, and rm commands. Install
missing tools through your distribution’s package manager first. Python reads the JSON response
using only its standard library.
This CPU walkthrough pins whisper.cpp v1.8.7 and targets a Debian 13 environment with
Bash 5.2, GCC 14.2, Make 4.4, CMake 3.31, cURL 8.14, FFmpeg 7.1, and Python 3.12.
Other platforms and older toolchains are outside this walkthrough. The multilingual tiny
model occupies about 75 MiB on disk. Allow additional disk
space for the source and build, and several hundred megabytes of free memory for inference.
Installation
From a fresh working directory, download the pinned release, build its server, and download the
multilingual model. Choose tiny, rather than the English-only tiny.en model. The parentheses
keep shell options and directory changes inside a subshell:
(
set -e
for tool in curl tar gzip cmake make c++ ffmpeg python3 sh dirname grep mktemp rm; do
command -v "$tool" >/dev/null || { printf 'Missing tool: %s\n' "$tool" >&2; exit 1; }
done
if [ -e whisper-v1.8.7.tar.gz ] || [ -e whisper.cpp-1.8.7 ]; then
printf 'Use a fresh directory; existing download or source found.\n' >&2
exit 1
fi
curl -q --fail --location --output whisper-v1.8.7.tar.gz \
https://github.com/ggml-org/whisper.cpp/archive/refs/tags/v1.8.7.tar.gz
tar -xzf whisper-v1.8.7.tar.gz
cd whisper.cpp-1.8.7
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DGGML_METAL=OFF -DWHISPER_BUILD_SERVER=ON
cmake --build build --config Release --target whisper-server -j 4
sh ./models/download-ggml-model.sh tiny
)
Setting up the transcription server
After installation succeeds, start the HTTP server from the same working directory:
(
cd whisper.cpp-1.8.7 &&
./build/bin/whisper-server --model models/ggml-tiny.bin \
--host 127.0.0.1 --port 8080 --language auto --no-gpu
)
Wait for the listening message, then leave this terminal open. The example server has no authentication, so keep it bound to loopback and use trusted local recordings. Stop it with Ctrl+C when you finish.
Transcribing audio with cURL
In a second terminal, work in a directory containing a spoken recording named input.mp3.
Use a trusted excerpt that starts with clear speech in one language. Convert it to mono,
16 kHz, 16-bit PCM WAV. The block rejects an existing audio.wav before conversion, and
-n also prevents FFmpeg from overwriting it. Choose a fresh directory for each recording;
FFmpeg 7.1 can report an overwrite refusal while returning status zero, so the explicit check matters.
(
if [ -e audio.wav ]; then
printf 'Existing audio.wav; use a fresh directory.\n' >&2
exit 1
fi
ffmpeg -nostdin -n -i input.mp3 -vn -ar 16000 -ac 1 -c:a pcm_s16le audio.wav
)
Listen to audio.wav and check that it contains the intended speech before uploading. FFmpeg
can recover parts of damaged recordings while still returning success; conversion alone does
not establish that the input was intact.
Language detection
Save this as read_language.py beside audio.wav. It reads the language inferred from audio
and prints the transcript for review. It rejects empty text and text made entirely of bracketed
non-speech markers, including repeated [BLANK_AUDIO] results:
import json
import re
import sys
def read_language(path):
with open(path, encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, dict):
raise ValueError("Expected a JSON object")
language = data.get("language")
text = data.get("text")
if not isinstance(language, str) or not language.strip():
raise ValueError("Response has no detected language")
if not isinstance(text, str) or not re.sub(r"\[[^\]]*\]", "", text).strip():
raise ValueError("No speech transcript; language result is inconclusive")
return language.strip(), " ".join(text.split())
def main():
if len(sys.argv) != 2:
raise ValueError("Usage: python3 read_language.py response.json")
language, text = read_language(sys.argv[1])
print(f"Language candidate: {language}")
print(f"Transcript: {text}")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError) as error:
print(f"Cannot read language result: {error}", file=sys.stderr)
sys.exit(1)
After the conversion succeeds and the reader is saved, paste this request block. It gives each request its own temporary response file and reads it only if cURL succeeds. The file is removed when the block finishes, including on failure, so a previous response cannot become this request’s result. The request allows 60 seconds total, including upload and inference; a busy CPU or longer recording can exceed that deadline. Increase it deliberately if needed.
(
set -e
response=$(mktemp ./language-response.XXXXXX)
trap 'rm -f "$response"' EXIT
curl -q --fail --silent --show-error --max-time 60 \
http://127.0.0.1:8080/inference \
--form 'file=@audio.wav;type=audio/wav' \
--form 'language=auto' \
--form 'response_format=verbose_json' \
--form 'no_language_probabilities=true' \
--output "$response" &&
python3 read_language.py "$response"
)
language=auto requests language detection. The server’s
verbose_json response
includes language and text; plain json omits the language. Translation is off, and
no_language_probabilities=true skips an optional second detection pass.
For English speech, expect Language candidate: english;
for Spanish, the server’s name is spanish. These are language names, not ISO codes.
Compare the printed transcript with what you hear. The marker check is only a sanity check:
Whisper can invent words,
and a nonempty transcript does not confirm speech or a correct language.
Performance optimization
Keep the server running across requests so it can reuse the loaded model. tiny keeps the
download small for local experiments, but its transcript can contain substantial errors.
Check it on known recordings before using its candidate to route other work.
In this version, automatic language detection starts at offset zero and uses the recording’s opening window, roughly 30 seconds, rather than reclassifying each later segment. Long leading silence can therefore produce a misleading language even when speech appears later. Use an excerpt that begins with speech; for language changes, assess separate excerpts. One file-level result is not a timeline of every language spoken.
Error handling
If installation fails, inspect its partial files, fix the missing prerequisite, and retry in a
new working directory. Existing downloads and extracted sources are deliberately left untouched.
If conversion fails, inspect any partial audio.wav before removing it or choosing another directory.
cURL exits nonzero for HTTP errors, connection failures, or timeouts. The request block then
skips the reader and cleans up its temporary response. Keep a successfully converted audio.wav
and rerun just the request block after resolving the failure. Invalid JSON, missing fields,
empty or marker-only transcripts, and unreadable files make the reader exit with status 1.
If startup fails, check the model path and whether port 8080 is already occupied. If an upload fails, inspect the local server terminal and confirm the WAV conversion succeeded. Do not treat a guessed language on silence, music, or very short speech as a reliable classification.
For hosted transcription in a file-processing workflow, see our Speech Robot.
