Skip to main content

vaaksetu

A local speech-to-text server for Indian languages that speaks the OpenAI transcription API. Point an existing app's base_url at it and nothing else changes; the audio never leaves the machine.

from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="anything")
with open("clip.wav", "rb") as audio:
    print(client.audio.transcriptions.create(model="whisper-1", file=audio, language="hi").text)
# कुछ अणुओं में अस्थिर केंद्रक होता है जिसका मतलब यह कि उनमें थोड़े या बिना किसी झटके से टूटने की प्रगति होती है

That output is a real response from this server for a Hindi FLEURS test clip (reference: "...जिसका मतलब यह है कि... प्रवृत्ति होती है", two words differ).

The design, in three lines

Data path. Upload (multipart, size-capped) to a temp file, ffmpeg decodes it to 16 kHz mono PCM and the temp file is deleted, the PCM is cut into chunks of at most 30 s at the quietest nearby point, each chunk goes through log-mel features, the ONNX Conformer encoder and a CTC head restricted to the requested language's tokens, and the per-chunk texts are stitched back in order. Nothing is persisted; the server has no state beyond the loaded model.

Failure mode 1: confident garbage. This model decodes whatever it is given into whichever language you name. Hindi audio sent as Tamil comes back as fluent-looking Tamil script. So language is required and validated against the 22 codes the model has heads for (an unknown code returns the list); audio is always resampled by ffmpeg to the model's 16 kHz, never trusted to already be; the optional language=auto refuses to answer when its two best candidates are close.

Failure mode 2: long audio exhausting memory. Attention cost grows with the square of the input. Measured here: a single 120 s pass peaked at 1254 MB and ran at RTF 0.25; the same recording cut into 30 s chunks (all 318 s of it) peaked at 888 MB at RTF 0.14 (onnxruntime 1.27 run, output not saved). Chunk size, upload size and duration are all capped, and each refusal names the setting that caused it.

The opinion. Drop-in compatibility beats a nicer bespoke API. The route, the form fields, the json | text | verbose_json shapes and the error envelope match OpenAI's, so the switch is one base_url line.

Why this exists

  • ollama#1168 (open, 64 thumbs-up) asks for local speech-to-text; comments ask for an OpenWebUI/OpenAI-compatible transcription endpoint. ollama#11798 asks for audio input. Neither is shipped; neither targets Indian languages. (whisper.cpp#64 is about speaker diarization, not this.)
  • The model's own tracker has open requests that map onto this repo: a microphone example with the ONNX files (discussion #5), deployment (#4), streaming (#14), and language detection (#13). mic, serve and --lang auto answer three of them; #14 is only partly answered (see Limits).

Install and run

pip install vaaksetu            # needs ffmpeg on PATH
pip install "vaaksetu[mic]"     # adds the microphone command
vaaksetu fetch                  # downloads ~0.8 GB, verifies SHA-256, into ~/.cache/vaaksetu/model
vaaksetu serve                  # http://127.0.0.1:8000
vaaksetu transcribe clip.wav --lang hi
vaaksetu mic --lang hi          # speak; text appears after each pause

vaaksetu mic --replay clip.wav --lang hi feeds a file through the same path at real-time speed, for demos and latency checks without a microphone.

Not yet on PyPI; from a checkout use pip install -e ..

API

POST /v1/audio/transcriptions multipart: file, language (required: a code, a language name, or auto), model (accepted, ignored), response_format = json (default), text, verbose_json (segments with start/end, one per chunk). prompt and temperature are accepted and ignored. srt/vtt return 400 naming the valid formats.
GET /v1/models one entry, so clients that list models work
GET /health 200 when the model is loaded, 503 with the reason when it is not; includes limits and language codes

Errors use OpenAI's {"error": {"message", "type", "param", "code"}} envelope. Language codes: as bn brx doi gu hi kn kok ks mai ml mni mr ne or pa sa sat sd ta te ur.

Limits and settings (environment)

Variable Default Refusal
VAAKSETU_MAX_UPLOAD_MB 50 413 "upload is larger than the 50 MB limit"
VAAKSETU_MAX_DURATION_S 900 413 "audio is longer than the 900 s duration limit"
VAAKSETU_CHUNK_S 30 chunk ceiling, minimum 5
VAAKSETU_MODEL_DIR ~/.cache/vaaksetu/model /health 503 and a fetch hint
VAAKSETU_HOST / VAAKSETU_PORT 127.0.0.1 / 8000
VAAKSETU_API_KEY unset 401 unless Authorization: Bearer <key> matches
VAAKSETU_THREADS 0 (ORT default)

Auth posture: binds to loopback by default and, with no key, trusts any local caller (the OpenAI client always sends some key; it is ignored). serve refuses a non-loopback host unless VAAKSETU_API_KEY is set. There is no TLS; put a proxy in front for anything beyond one machine. Uploads are copied to a temp file that is deleted in a finally; ffmpeg runs with an argument list, never a shell. One inference runs at a time (a lock) so concurrent requests queue instead of multiplying memory. Note that the multipart parser spools a request without a Content-Length header before the size check runs.

Measured

Test set: the first 20-25 clips of the public FLEURS test split per language (CC-BY 4.0, ungated), 150 clips and 1918 s of audio, normalised references (lower-case, punctuation removed, NFC). Model: the int8 build described below. Machine: Ryzen 7 6800H (8 cores), CPU only, onnxruntime 1.31.1, Python 3.13, Windows 11. Output of the run behind these numbers: measure/results/evaluate.txt and measure/results/lid.txt; reproduce with measure/evaluate.py and measure/lid.py (clips and weights are not in the repo). An earlier run on onnxruntime 1.27 gave WER within 0.5 points of these.

lang clips audio s WER % CER % RTF
hi 25 318 12.2 4.4 0.152
bn 20 279 14.7 3.3 0.175
mr 20 265 18.1 4.4 0.133
kn 20 275 17.9 4.1 0.135
gu 20 228 19.4 6.5 0.158
te 25 276 20.8 6.6 0.151
ta 20 277 30.7 14.0 0.155

Overall RTF 0.151 (about 6.6x faster than real time). Peak resident memory 881 MB over the whole run, 762 MB of it the loaded model. A 318 s file through the HTTP server took 45 s (RTF 0.142) in the earlier onnxruntime 1.27 run, whose output was not saved. RTF excludes ffmpeg decode and was measured while another heavy job shared this machine, so treat it as an upper-ish bound. Tamil and Telugu WER is inflated by agglutination: CER is the fairer comparison there. 150 clips is a small sample; no confidence intervals were computed. The fp32 model was not measured (the disk could not hold it), so the cost of quantisation is not isolated by this repo.

Microphone latency (mic --replay, real-time feed, 0.7 s of silence ends an utterance): text appeared 1.6 s after speech stopped for a 4-5 s utterance and 2.8 s for a 10 s one (0.7 s hold + decode). Latency grows with utterance length because the model decodes whole utterances. Opening a real input stream at 16 kHz was verified on this machine; live speech through it was not.

Language detection (language=auto)

Opt-in, never the default; a request that omits language is refused. For each of the 22 heads the score is the mean over frames of the log of the probability mass the joint softmax puts on that head's tokens. On the 150 clips above, picking the top head was right 144 times (96.0%). A first scorer (per-head confidence) managed 92.0% and sent 11 of 25 Hindi clips to Urdu, which is why it was dropped. The six errors were Gujarati-as-Hindi (4) and Hindi-as-Urdu (2). With the server's rule "refuse if best minus runner-up is under 0.2 nats per frame", 128 of 150 clips are answered, all correctly, and 22 get a 422 naming the two close candidates. Caveats: only 7 of the 22 languages were tested; the 0.2 threshold was picked on the same clips it is reported on, so the 100% is optimistic; detection looks at the first 30 s only; closely related languages (Hindi/Urdu, Marathi/Hindi/Nepali, Konkani) are the weak spot. If it matters, pass language.

Model and provenance

IndicConformer 600M multilingual (AI4Bharat, MIT). The official repo is gated on Hugging Face (login required), so vaaksetu fetch pulls a public int8 repackaging (yashwork-byte/indic-conformer-600m-int8-onnx, MIT) pinned to one commit and verified against SHA-256 digests kept in src/vaaksetu/fetch.py. Its encoder is dynamically quantised from 2.4 GB fp32 to 0.65 GB. To use the official fp32 files, download them yourself and set VAAKSETU_MODEL_DIR; the layout (assets/encoder.onnx, ctc_decoder.onnx, language_masks.json, vocab.json, preprocessor.ts) is the same, and that path is not digest-checked. The card of the original says AI4Bharat now recommends Indic-Transcribe for quality; this repo targets IndicConformer because it ships ONNX and runs on a laptop CPU.

Decoding is greedy CTC. The model's TorchScript preprocessor is not executed (no torch dependency): frontend.py reads its window and mel filterbank out of the archive and reproduces the forward pass in numpy. The pickled graph is never loaded.

Limits

  • Not a streaming model. mic cuts the stream into utterances at pauses and decodes each once; it is not token-by-token.
  • CTC only; the RNNT decoder in the model is not wired. No translation, no diarization, no word timestamps (segments are per chunk), no srt/vtt.
  • Only 7 of the 22 languages were measured; the other 15 run through the same graph with unmeasured quality (Santali, Kashmiri, Manipuri and Bodo have far less training data than Hindi).
  • Needs about 1 GB of RAM free beyond the OS; a 30 s chunk adds roughly 100 MB on top of the model.
  • Audio quality in the field (phone calls, noise) is not covered by FLEURS read speech; expect higher error.

Tests

pytest runs 24 tests with a fake recognizer (real server on an ephemeral port, real openai client): response shapes, oversize and over-long refusals, unknown and missing language, non-audio upload and temp-file cleanup, chunk order and cut placement, auth, auto-language refusal, utterance segmentation. pytest -m model additionally needs VAAKSETU_MODEL_DIR (and VAAKSETU_FLEURS_DIR for the Hindi clip) and is skipped otherwise.

MIT licensed.

Metadata

Release files for vaaksetu 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vaaksetu 0.1.0
File Size Uploaded Provenance Actions
vaaksetu-0.1.0.tar.gz 26.2 kB Without provenance Details for vaaksetu-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for vaaksetu 0.1.0
File Interpreter ABI Platform Actions
vaaksetu-0.1.0-py3-none-any.whl Python 3 none any Details for vaaksetu-0.1.0-py3-none-any.whl

Total release size: 48.9 kB

Release files / vaaksetu-0.1.0.tar.gz

Download URL vaaksetu-0.1.0.tar.gz
Size 26.2 kB
Tags Source
SHA-256 checksum
How to use checksums
266b99f676a4ff7cb29d575062ecaceb3ce7a765113fac0d50ca467839c390a6
BLAKE2b-256 checksum
How to use checksums
69d7b8a642062c1a123a842c1b7c92a1907db756aae1020c7e5af11f3402940d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / vaaksetu-0.1.0-py3-none-any.whl

Download URL vaaksetu-0.1.0-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7e413bec2ff583ddfb0c615e27ffcc7ed9e5cc5163167e7eb11f14961d42bec5
BLAKE2b-256 checksum
How to use checksums
8beca0cba2e909d158ce35532b054421baaf6a897e28bfbaccc3d64ebf52ccd7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for vaaksetu 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page