Vibe coded whisper model api for brazilian portuguese transcription
  • Python 97%
  • Dockerfile 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
brennoflavio 7f8340e6a3
All checks were successful
Publish container image / publish (push) Successful in 2m19s
first commit
2026-09-20 01:34:52 -03:00
.forgejo/workflows first commit 2026-09-20 01:34:52 -03:00
tests first commit 2026-09-20 01:34:52 -03:00
.dockerignore first commit 2026-09-20 01:34:52 -03:00
.gitignore first commit 2026-09-20 01:34:52 -03:00
app.py first commit 2026-09-20 01:34:52 -03:00
Dockerfile first commit 2026-09-20 01:34:52 -03:00
inference.py first commit 2026-09-20 01:34:52 -03:00
pyproject.toml first commit 2026-09-20 01:34:52 -03:00
README.md first commit 2026-09-20 01:34:52 -03:00
uv.lock first commit 2026-09-20 01:34:52 -03:00

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 503 with Retry-After: 5.
  • Jobs are FIFO in order of completed uploads, not initial HTTP arrival.
  • Uploads must complete within 30 seconds or receive 408. Oversized declared Content-Length is 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 with detail.

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_THREADS alongside CPU requests/limits. For a 4-CPU allocation, use MAX_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 (an emptyDir if 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.