- Python 97%
- Dockerfile 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Publish container image / publish (push) Successful in 2m19s
|
||
| .forgejo/workflows | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| app.py | ||
| Dockerfile | ||
| inference.py | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Home Whisper
Unauthenticated HTTP API for Brazilian Portuguese speech-to-text. Python/FastAPI, Whisper Small via faster-whisper, CPU-only INT8 inference. No database or external API. Multiple HTTP requests can wait concurrently; exactly one audio file is decoded/transcribed at a time, off the HTTP event loop.
Configuration
| Environment variable | Meaning |
|---|---|
MODEL_PATH |
Required. Absolute path to a mounted, complete CTranslate2 multilingual Small model directory. Not a model name, GGML file, OpenAI .pt checkpoint, or individual model.bin. |
MAX_CPU_THREADS |
Positive integer: inference CPU threads. Defaults to the process's CPU affinity count on Linux. Set explicitly to match the Kubernetes CPU allocation; the default does not account for CPU time quotas. |
MAX_CPU_THREADS controls CTranslate2's intra-operation threads, not a hard
process-wide CPU quota. Kubernetes remains responsible for enforcing CPU limits.
The decoder uses one thread; the Docker image also limits auxiliary OpenMP/BLAS
thread defaults. Inference explicitly uses the configured thread count.
The model directory must contain model.bin, config.json, and tokenizer.json.
preprocessor_config.json is optional: faster-whisper uses its built-in feature
extraction defaults when absent, as with Systran/faster-whisper-small.
Supply a CTranslate2 conversion of openai/whisper-small
(for example, the files from Systran/faster-whisper-small). A compatible Portuguese
fine-tune can be substituted after conversion and evaluation. Do not use
small.en: English-only models are rejected at startup. Portuguese is fixed to
pt; Whisper has no separate pt-BR language code. The task is transcription, not
translation. Model size/provenance is the responsibility of the provisioning job;
the service checks Portuguese support but does not enforce the Small architecture.
Neither the Docker build nor the application downloads models. Provision the
complete directory separately, e.g. with a PVC and an init container/job. Mount it
read-only and make it readable by UID/GID 10001. Publish all model files before
starting the service. Missing/incomplete models fail startup rather than triggering
a download. local_files_only=True is enforced, and the image additionally sets
HF_HUB_OFFLINE=1. Silero VAD is bundled with faster-whisper.
Docker
docker build -t home-whisper .
docker run --rm -p 8000:8000 \
--cpus=4 --memory=4g \
-e MAX_CPU_THREADS=4 \
-e MODEL_PATH=/models/small \
-v /absolute/path/to/ct2-small:/models/small:ro \
home-whisper
The image contains Python dependencies and the application only, not Whisper
weights. Dependencies are pinned in uv.lock. The runtime is non-root and does not
need CUDA, PyTorch, or a system FFmpeg executable; PyAV provides audio codecs.
Image publishing
.forgejo/workflows/docker-image.yml builds and publishes on pushes to master
and manual runs on master, using the existing ubuntu-latest Forgejo runner.
Configure the REGISTRY_USERNAME and REGISTRY_PASSWORD Actions secrets with
permission to push to the registry.
Images are published as
git.brennoflavio.com.br/brennoflavio/home-whisper:YYYYMMDDHHMMSS (UTC).
The workflow also maintains a build-cache registry tag. It does not create Git
tags, releases, or deployments, and does not run a separate test job.
API
curl --fail-with-body http://localhost:8000/transcribe \
-F 'file=@mensagem.ogg'
Returns 200 OK with text/plain; charset=utf-8:
Olá, gostaria de confirmar o horário da reunião.
- Send exactly one multipart file named
file; no other form fields are needed. - Common formats: WAV, MP3, FLAC, M4A, OGG/Opus, WebM (subject to PyAV codec support).
- Language/model selection and word timestamps are not exposed to callers.
- Silence is filtered with VAD. No detected speech produces an empty text response.
GET /healthz: process liveness.GET /readyz: model loaded and accepting work.- Interactive API documentation:
/docs.
Limits and errors
- At most 9 outstanding requests, including uploads: one running plus up to
eight waiting when inference is busy. Overflow returns
503withRetry-After: 5. - Jobs are FIFO in order of completed uploads, not initial HTTP arrival.
- Uploads must complete within 30 seconds or receive
408. Oversized declaredContent-Lengthis rejected before reading the body. - Maximum file size: 25 MiB. Total multipart body limit: 25 MiB + 64 KiB,
enforced while receiving, including requests without
Content-Length. - Maximum decoded duration: 10 minutes, checked during decoding rather than trusting file metadata.
400: malformed multipart/extra fields or files;413: size or duration limit;415: wrong request content type;422: missing/empty/undecodable audio;500: unexpected transcription failure. Error bodies are JSON withdetail.
Uploads may temporarily spool to the system temporary directory while parsing;
queued audio stays in bounded process memory. Upload files are closed after
parsing, and audio/transcripts are not intentionally persisted or logged. The
service has no durable queue: a pod restart loses outstanding requests. Disconnects
cancel queued work (logged as 499 when observed). Native inference already in
progress is not interrupted by request cancellation; its slot stays occupied until
it finishes. Cancelled queued entries are discarded when reached by the worker.
Kubernetes deployment notes
- Use one Uvicorn worker per pod (the image default); multiple processes would each load a model and have their own queue.
- Mount the separately provisioned PVC at
MODEL_PATH, read-only. - Set
MAX_CPU_THREADSalongside CPU requests/limits. For a 4-CPU allocation, useMAX_CPU_THREADS=4. More threads do not guarantee proportional speedups. - Start benchmarking with 4 CPUs and 4 GiB; actual needs depend on audio and CPU.
- Use a startup probe on
/readyz, readiness on/readyz, and liveness on/healthz. Allow enough startup time to load the mounted model. - Allow writable
/tmp(anemptyDirif using a read-only root filesystem). - Set client/ingress response timeouts to accommodate queue wait plus transcription.
On SIGTERM, Uvicorn stops accepting connections and allows 30 seconds for
requests to finish, then cancels their handlers and queued jobs. Active native
inference is still allowed to finish before process exit. Configure Kubernetes
terminationGracePeriodSeconds(e.g. 60 initially) as the final hard deadline; if exceeded, Kubernetes kills the pod and unfinished work is lost. Threads cannot safely interrupt hung native code. There is no hard in-process inference timeout or retry mechanism. - Keep the unauthenticated service internal (
ClusterIP/NetworkPolicy). Public exposure would allow anyone to consume the CPU and request slots.
Local development and tests
Python 3.12 and uv are required.
uv sync --frozen
uv run pytest
uv run ruff check .
uv run ruff format --check .
MODEL_PATH=/absolute/path/to/ct2-small MAX_CPU_THREADS=4 \
uv run uvicorn app:app --host 0.0.0.0 --port 8000 --workers 1 \
--timeout-graceful-shutdown 30
Tests use a fake inference model and real PyAV decoding; they do not download weights. They verify serial execution, queue overload, cancellation, responsive probes, request limits/cleanup, model configuration, and audio decoding. Real Portuguese accuracy, model compatibility, latency, and peak memory still need to be tested with the provisioned model and representative recordings.