Production-grade Docker images that package speech AI models behind a uniform HTTP API. Designed to be deployed on Koyeb GPU pods (L4 / L40S) and called by a Rust API gateway over the internal Koyeb network.
DockerPodApi/
├── README.md
├── shared/ # cross-model docs and conventions
│ ├── API_CONTRACT.md # exact request/response schema
│ ├── SECURITY.md # SSRF, auth, secrets
│ └── DEPLOY.md # Koyeb deployment notes
└── models/
└── echo-l4/ # first model: Echo (DiariZen V4 + SE-DiCoW)
├── README.md
├── requirements.txt
├── app/
│ ├── server.py # FastAPI: /infer, /infer/async, /healthz, /readyz, /metrics
│ ├── config.py
│ ├── download.py # SSRF-safe https-only WAV downloader
│ ├── pipeline.py # model-specific inference wrapper
│ ├── trt_chain.py # backend selector (PyTorch today)
│ └── jobs.py # async job queue + signed webhooks
└── bazel/ # rules_oci build (build image without docker daemon)
Every model image exposes the same contract:
| endpoint | method | purpose |
|---|---|---|
/infer | POST | sync inference (≤ 5 min audio). Returns JSON or SSE per Accept |
/infer/async | POST | async inference (≤ 60 min audio). Returns 202 + job_id, webhook on completion |
/jobs/{id} | GET | polling fallback for async |
/healthz | GET | liveness probe |
/readyz | GET | readiness probe (model loaded) |
/metrics | GET | Prometheus counters |
Auth: X-Internal-Secret header on every endpoint except health/metrics.
Full request/response schema in shared/API_CONTRACT.md.
A model image is fully described by one directory under models/. To
add a new model, copy models/echo-l4/ and adapt three files:
models/<your-model>/app/pipeline.pyImplement an EchoPyTorchBackend-shaped class with two methods:
class MyBackend:
def load(self) -> None:
# Load weights, tokenizer, etc. Called once at boot.
...
def infer(self, audio_path: Path) -> dict[str, Any]:
# Take a local WAV path, return the canonical schema:
return {
"transcript": "...",
"speakers": [
{"id": "S0", "segments": [{"start": 0.0, "end": 5.2, "text": "..."}]},
...
],
"rttm": "SPEAKER ... 1 0.000 5.200 <NA> <NA> S0 <NA> <NA>\n...",
"meta": {
"n_speakers": <int>,
"inference_seconds": <float>,
"backend": "pytorch",
},
}
def infer_streaming(self, audio_path: Path):
# Yield events in time order:
# {"event": "segment", "speaker_id": ..., "start": ..., "end": ..., "text": ...}
# End with: {"event": "done", "meta": {...}}
...
The streaming variant can be a simple wrapper around infer() that yields
pre-sorted segments (this is what echo-l4 does today since DiCoW does not
expose a true streaming runtime).
models/<your-model>/app/trt_chain.pyWire your new backend class into PyTorchBackend.try_load(). If you want
to add ONNX or TensorRT variants, declare them as siblings of
PyTorchBackend and add them to the Pipeline._backend selection. Today
only PyTorch is implemented.
models/<your-model>/requirements.txtList the pip dependencies needed at runtime. Pin versions. The build
process pre-installs these into a portable site-packages baked into the
image so the container does not run pip install at boot.
models/<your-model>/bazel/BUILD.bazelAdjust the OCI image target if your model needs extra layers (e.g., a vendored upstream repo like DiCoW). The default template covers "FastAPI + Python deps + model weights + app".
From inside models/<your-model>/:
ECHO_INTERNAL_SECRET=dev-test-secret-at-least-24-chars-long \
ECHO_INSECURE_LOCAL_DOWNLOAD=1 \
PYTHONPATH=. \
python3 -m uvicorn app.server:app --host 127.0.0.1 --port 8000
Curl smoke tests in shared/SMOKE_TESTS.md.
On a Koyeb GPU pod (where Bazel rules_oci works without a docker daemon), as a non-root user (rules_python refuses root):
cd models/<your-model>/bazel
bazel build //:image
Output: an OCI tarball under bazel-bin/image/.
bazel run //:push
Token / credentials configured in shared/DEPLOY.md.
We considered classic Dockerfiles. On Koyeb pods the host has neither
dockerd nor user namespaces, so docker build, buildah, kaniko,
buildctl all fail. Bazel rules_oci is the only path that produces a
valid OCI image without those primitives, because it manipulates tar
files and manifests rather than chrooting.
HF_HUB_OFFLINE=1.Each model directory inherits the license of its upstream weights (typically CC BY-NC 4.0 for the BUT-FIT family). The wrapper code here is MIT unless stated otherwise.
6 commits
Python
86.5%
Shell
8.1%
Starlark
5.5%
Production-grade Docker images that package speech AI models behind a uniform HTTP API. Designed to be deployed on Koyeb GPU pods (L4 / L40S) and called by a Rust API gateway over the internal Koyeb network.
DockerPodApi/
├── README.md
├── shared/ # cross-model docs and conventions
│ ├── API_CONTRACT.md # exact request/response schema
│ ├── SECURITY.md # SSRF, auth, secrets
│ └── DEPLOY.md # Koyeb deployment notes
└── models/
└── echo-l4/ # first model: Echo (DiariZen V4 + SE-DiCoW)
├── README.md
├── requirements.txt
├── app/
│ ├── server.py # FastAPI: /infer, /infer/async, /healthz, /readyz, /metrics
│ ├── config.py
│ ├── download.py # SSRF-safe https-only WAV downloader
│ ├── pipeline.py # model-specific inference wrapper
│ ├── trt_chain.py # backend selector (PyTorch today)
│ └── jobs.py # async job queue + signed webhooks
└── bazel/ # rules_oci build (build image without docker daemon)
Every model image exposes the same contract:
| endpoint | method | purpose |
|---|---|---|
/infer | POST | sync inference (≤ 5 min audio). Returns JSON or SSE per Accept |
/infer/async | POST | async inference (≤ 60 min audio). Returns 202 + job_id, webhook on completion |
/jobs/{id} | GET | polling fallback for async |
/healthz | GET | liveness probe |
/readyz | GET | readiness probe (model loaded) |
/metrics | GET | Prometheus counters |
Auth: X-Internal-Secret header on every endpoint except health/metrics.
Full request/response schema in shared/API_CONTRACT.md.
A model image is fully described by one directory under models/. To
add a new model, copy models/echo-l4/ and adapt three files:
models/<your-model>/app/pipeline.pyImplement an EchoPyTorchBackend-shaped class with two methods:
class MyBackend:
def load(self) -> None:
# Load weights, tokenizer, etc. Called once at boot.
...
def infer(self, audio_path: Path) -> dict[str, Any]:
# Take a local WAV path, return the canonical schema:
return {
"transcript": "...",
"speakers": [
{"id": "S0", "segments": [{"start": 0.0, "end": 5.2, "text": "..."}]},
...
],
"rttm": "SPEAKER ... 1 0.000 5.200 <NA> <NA> S0 <NA> <NA>\n...",
"meta": {
"n_speakers": <int>,
"inference_seconds": <float>,
"backend": "pytorch",
},
}
def infer_streaming(self, audio_path: Path):
# Yield events in time order:
# {"event": "segment", "speaker_id": ..., "start": ..., "end": ..., "text": ...}
# End with: {"event": "done", "meta": {...}}
...
The streaming variant can be a simple wrapper around infer() that yields
pre-sorted segments (this is what echo-l4 does today since DiCoW does not
expose a true streaming runtime).
models/<your-model>/app/trt_chain.pyWire your new backend class into PyTorchBackend.try_load(). If you want
to add ONNX or TensorRT variants, declare them as siblings of
PyTorchBackend and add them to the Pipeline._backend selection. Today
only PyTorch is implemented.
models/<your-model>/requirements.txtList the pip dependencies needed at runtime. Pin versions. The build
process pre-installs these into a portable site-packages baked into the
image so the container does not run pip install at boot.
models/<your-model>/bazel/BUILD.bazelAdjust the OCI image target if your model needs extra layers (e.g., a vendored upstream repo like DiCoW). The default template covers "FastAPI + Python deps + model weights + app".
From inside models/<your-model>/:
ECHO_INTERNAL_SECRET=dev-test-secret-at-least-24-chars-long \
ECHO_INSECURE_LOCAL_DOWNLOAD=1 \
PYTHONPATH=. \
python3 -m uvicorn app.server:app --host 127.0.0.1 --port 8000
Curl smoke tests in shared/SMOKE_TESTS.md.
On a Koyeb GPU pod (where Bazel rules_oci works without a docker daemon), as a non-root user (rules_python refuses root):
cd models/<your-model>/bazel
bazel build //:image
Output: an OCI tarball under bazel-bin/image/.
bazel run //:push
Token / credentials configured in shared/DEPLOY.md.
We considered classic Dockerfiles. On Koyeb pods the host has neither
dockerd nor user namespaces, so docker build, buildah, kaniko,
buildctl all fail. Bazel rules_oci is the only path that produces a
valid OCI image without those primitives, because it manipulates tar
files and manifests rather than chrooting.
HF_HUB_OFFLINE=1.Each model directory inherits the license of its upstream weights (typically CC BY-NC 4.0 for the BUT-FIT family). The wrapper code here is MIT unless stated otherwise.
6 commits
Python
86.5%
Shell
8.1%
Starlark
5.5%