An OpenAI-compatible Whisper speech-to-text REST API, running locally on an AMD GPU (ROCm) with Django + Django REST Framework.
Implements the official OpenAI audio endpoints, so any OpenAI client/SDK works
by just changing base_url:
| Endpoint | Description |
|---|---|
POST /v1/audio/transcriptions |
Transcribe audio (multipart upload) |
POST /v1/audio/translations |
Translate audio to English |
GET /v1/models |
List available models |
Supported parameters: file, model (whisper-1 or any whisper model name:
tiny, base, small, medium, large, turbo, ...), language,
prompt, response_format (json, text, srt, verbose_json, vtt),
temperature, timestamp_granularities[] (segment, word).
- uv
ffmpeg(system package, used by whisper to decode audio)- AMD GPU with ROCm-supported kernel driver (developed on RX 7900 GRE / gfx1100). Falls back to CPU automatically if no GPU is available.
uv sync # installs torch 2.13+rocm7.2, whisper, Django
uv run python manage.py migrate # one-time, for admin/session tables
uv run python manage.py runserver 0.0.0.0:8000The default model (turbo) is downloaded and loaded onto the GPU at startup.
All configuration lives in settings.toml, layered by
environment and overridable via environment variables:
[default]
whisper_model = "turbo" # model served for "whisper-1"
whisper_preload = true # load model at startup
device = "cuda" # cuda (GPU) | cpu | auto
max_upload_mb = 100
log_level = "INFO"
[development]
debug = true
log_level = "DEBUG"
[production]
debug = falseResolution order (highest wins):
- Environment variables:
WHISPER_API_<KEY>— e.g.WHISPER_API_WHISPER_MODEL=small,WHISPER_API_LOG_LEVEL=DEBUG .secrets.toml(git-ignored, for local overrides likesecret_key)- Active environment section — switch with
WHISPER_API_ENV=production [default]section
Example:
WHISPER_API_ENV=production WHISPER_API_WHISPER_MODEL=large-v3 \
uv run python manage.py runserverColorized, detailed logs via colorlog (configured in config/settings.py):
02:19:27.717 INFO api.whisper_engine:get_model:55 Loading whisper model 'base' on cuda (AMD Radeon RX 7900 GRE) ...
02:19:28.430 INFO api.whisper_engine:get_model:65 Model 'base' ready in 0.71s (params: 71M, VRAM allocated: 0.27 GiB)
02:19:28.430 INFO api.views:post:86 Transcribing 'jfk.flac' (1.10 MiB) | task=transcribe model=base language=auto format=json word_timestamps=False
02:19:29.480 INFO api.views:post:98 Done in 1.05s | audio 10.6s (10.1x realtime) | detected language=en | 2 segment(s), 107 chars
02:19:29.530 WARNING api.views:post:44 Bad request: missing 'file' field
- Level colors: DEBUG cyan, INFO green, WARNING yellow, ERROR red
DEBUGlevel additionally logs full transcripts and model cache hits- Level is controlled by
log_levelin settings.toml /WHISPER_API_LOG_LEVEL
The image is GPU-ready: PyTorch ROCm wheels bundle all user-space ROCm
libraries, so the slim Debian base is enough — only the host kernel driver
is used, passed through via /dev/kfd + /dev/dri (no special toolkit
needed, unlike NVIDIA).
Images are built and pushed to GHCR by
.github/workflows/docker.yml on every
push to main / version tag — no local build needed:
docker pull ghcr.io/openprojectx/whisper-api:main
docker compose up -d # uses the GHCR imageThe Dockerfile is multi-stage with build-time caching designed for CI:
| Stage | Contents | Rebuilds when |
|---|---|---|
deps |
uv venv (torch ROCm etc.) — the huge layer | uv.lock changes |
models |
whisper weights downloaded at build time | WHISPER_MODELS build arg changes |
runtime |
slim base + ffmpeg, copies of the two layers above | app code changes |
All layers are cached in the GitHub Actions cache
(cache-to: type=gha,mode=max), so unchanged stages are nearly free.
Two optional build args (set them in the workflow for forks):
ROCM_ARCH=gfx1100— prune the bundled ROCm kernel libraries (rocblas, hipblaslt, MIOpen DBs, aotriton) to a single GPU architecture. Cuts ~5-6 GB off the image; the container then only runs on that arch.WHISPER_MODELS="small turbo"— space-separated list of models to bake in (default:small, matchingwhisper_modelin settings.toml).
docker run -d -p 8000:8000 \
--device=/dev/kfd --device=/dev/dri \
--group-add 992 --group-add 44 \ # render/video GIDs: getent group render video
-v whisper-cache:/cache \ # persist runtime model downloads
ghcr.io/openprojectx/whisper-api:mainThe default model is baked into the image (symlinked into /cache/whisper),
so first startup needs no download. Models requested per-request that aren't
baked in are downloaded once into the /cache volume.
Available models (GET /v1/models lists them at runtime):
| Model | Params | Disk | ~VRAM | English-only variant | Notes |
|---|---|---|---|---|---|
tiny |
39M | 75M | ~1 GB | tiny.en |
fastest, lowest accuracy |
base |
74M | 139M | ~1 GB | base.en |
good for quick tests |
small |
244M | 461M | ~2 GB | small.en |
decent quality/speed balance |
medium |
769M | 1.5G | ~5 GB | medium.en |
high accuracy |
large-v1 / large-v2 / large-v3 |
1550M | 2.9G | ~10 GB | — | best accuracy, multilingual |
turbo (= large-v3-turbo) |
809M | 1.6G | ~4 GB | — | near-large accuracy, ~8x faster than large |
The .en variants only understand English but tend to perform slightly
better on English audio. turbo is the recommended default for the
RX 7900 GRE (16 GB VRAM).
1. Server-side default — used when clients send model=whisper-1
(or omit the field). In settings.toml:
[default]
whisper_model = "turbo"or via environment variable:
WHISPER_API_WHISPER_MODEL=small uv run python manage.py runserver2. Per-request — any loaded model name can be requested directly, overriding the server default:
curl http://localhost:8000/v1/audio/transcriptions \
-F file=@speech.mp3 -F model=large-v3client.audio.transcriptions.create(model="medium", file=f)Requested models are downloaded on first use and cached in memory, so subsequent requests with the same model skip loading. Pre-download with:
uv run python manage.py download_models turbo mediumModel weights are cached in $XDG_CACHE_HOME/whisper (default
~/.cache/whisper/). To download them in advance instead of at server
startup or first request:
uv run python manage.py download_models turbo # one model
uv run python manage.py download_models tiny small # several models
uv run python manage.py download_models all # every modelTo use a different cache location, set XDG_CACHE_HOME (both when
downloading and when running the server):
XDG_CACHE_HOME=/data/cache uv run python manage.py download_models turbo
XDG_CACHE_HOME=/data/cache uv run python manage.py runserverApproximate sizes: tiny 75M, base 139M, small 461M, medium 1.5G, large/large-v3 2.9G, turbo 1.6G.
curl http://localhost:8000/v1/audio/transcriptions \
-F file=@speech.mp3 \
-F model=whisper-1 \
-F response_format=verbose_json \
-F "timestamp_granularities[]=word"from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
with open("speech.mp3", "rb") as f:
transcript = client.audio.transcriptions.create(model="whisper-1", file=f)
print(transcript.text)-
GPU monitoring & verification: see docs/GPU_MONITORING.md.
-
PyTorch ROCm wheels bundle their own ROCm runtime — the system ROCm installation does not need to match the wheel version.
-
openai-whispernormally depends on CUDA-onlytriton; the dependency metadata override inpyproject.tomlstrips it (torch's ROCm wheel shipstriton-rocminstead). -
The Django dev server is single-threaded; for production run under gunicorn (
uv add gunicorn) — whisper GPU inference serializes on the model lock anyway.