- TypeScript 46.5%
- Python 45.2%
- Nix 7.3%
- Just 0.6%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Labeling a large project took the whole site down every twenty minutes or so: every page came back "fetch failed" until the io-worker was restarted by hand. The io-worker's endpoints are synchronous, so a slow one holds the GIL and delays every other request, /health included. /project/summary is the slow one — it opens every labeled-data folder's HDF5 file to count labeled rows, a second of pandas work on a project with 87 of them. Autosave then made that a loop: a save every 1.5s, a loader revalidation after each save, a full rescan per revalidation. With several in flight, /health missed its 3s readiness probe three times running and process-compose stopped the process for good. Three changes, because each layer was independently wrong: The labeling route no longer revalidates after its own POSTs. Nothing the loader returns changes because of a save — the labels just written are already the reducer's state — so the rescan was pure cost. Row counts are cached per (path, mtime, size). Any write, ours or an external one, produces a new key rather than a stale count, so the cache needs no explicit invalidation. Repeat summaries drop from ~1.05s to ~35ms. The readiness probe gets a wider window and, more importantly, a restart policy. Every other process depends on the io-worker, so leaving it stopped turns a slow request into a dead site; restarting is always the better guess. Measured on the project that triggered this: eight concurrent summaries used to hang /health for 9.6s and kill the process, and now keep it under 0.25s. Assisted-by: Claude Code:Opus 5 |
||
| deploy | ||
| docs | ||
| nix | ||
| python | ||
| web | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| justfile | ||
| LICENSE | ||
| README.md | ||
deeplabcut-web
A web-based GUI for DeepLabCut.
By separating a server that runs next to the GPU machine from a browser client, this project removes the pain points of the existing Qt/napari desktop GUI — "can't run headless" and "must operate directly on the GPU machine" — and aims to run the entire workflow (project creation → labeling → training → inference → review) from the browser alone.
Status: Phase 0 (foundation & PoC) and Phase 1 (project & labeling MVP) are done; Phase 2 (training & inference) is in progress — the job layer is implemented, the review/visualization surface is not. Multi-animal labeling landed early, ahead of its Phase 3 slot. See the implementation plan and the parity checklist for the per-feature state.
Documents
- Design — architecture, stack, licensing policy, implementation plan
- Labeling parity checklist — feature parity with napari-deeplabcut
- API mapping — DeepLabCut Python API ↔ server endpoints
Overview
- Supported versions: DeepLabCut 3.x (PyTorch engine) is the first-class target. 2.x projects open and work in 3.0 as well.
- License (planned): own code is Apache-2.0. DeepLabCut (LGPL-3.0) is used as a separately installed dependency across a process boundary.
- Stack: TypeScript + React Router (framework mode) + Mantine. The React Router server (Node) serves both the UI and the API layer (
loader/action); no separate public API server. Labeling is a WebGL canvas (PixiJS candidate) + pureuseReducer, touch-capable and responsive so it works on tablets such as iPad. DeepLabCut and HDF5 label I/O are confined to Python workers with no public HTTP (async jobs over a Redis queue, sync I/O over a localhost internal HTTP endpoint). See the design document for details.
Development
The whole dev environment is provided by Nix (flakes). DeepLabCut itself is not part of the dev environment — it is heavy and pulls the full torch/CUDA stack, so job workers import it lazily and dev/tests run without it (design §13); production supplies it via the official DLC container.
# enter the dev shell (Node 22, pnpm, Python 3.13 env, redis, just, the `dev` runner)
nix develop # or: direnv allow
just # list the dev commands
just install # install web dependencies (first time)
# run the whole stack: redis + io-worker + arq worker + web
just up
# → web on http://127.0.0.1:3000, io-worker on http://127.0.0.1:8081
# on a machine where those ports are taken, override them per run
DLCWEB_DEV_REDIS_PORT=6390 DLCWEB_DEV_WEB_PORT=3100 just up
# to reach it from another device (a tablet on the same tailnet, say), bind
# wider than loopback and name the hosts it may be addressed by
DLCWEB_DEV_WEB_HOST=0.0.0.0 \
DLCWEB_DEV_WEB_ALLOWED_HOSTS=kilimanjaro.tail4108.ts.net just up
# frame extraction, training and inference additionally need the DLC image
just dlc # joins the running stack as the `worker-dlc` process
The web server binds loopback unless DLCWEB_DEV_WEB_HOST says otherwise, because it has no authentication: whoever can open the page can read the project tree and queue GPU jobs. Widen it only onto a network you trust — a tailnet, or 0.0.0.0 on a host whose firewall bounds who reaches the port. Vite additionally answers only to a Host header it recognises (an IP, or a name in DLCWEB_DEV_WEB_ALLOWED_HOSTS), so reaching it by name without that second variable fails with Blocked request even once it is bound.
just up runs dev up, where dev is a process-compose wrapper generated from nix/dev.nix by process-compose-flake. It takes every process-compose subcommand (dev down, dev process restart web, …), needs no config file in the working tree, and also works without the dev shell as nix run .#dev -- up. Run it from the repository root: the processes inherit its working directory.
Every process in the stack reloads on save: web through Vite's HMR, io-worker and worker by watching python/src. The Python reload restarts the process, so a job running in worker when a file is saved dies with it — long S3 ingests are worth finishing before editing. worker-dlc is the exception: it runs prebuilt container code, so Python changes reach it only through a rebuilt image. To restart something by hand, use dev process restart <name>.
Everything in the stack is deeplabcut-independent — labeling, config edits and S3 ingest all work with just just up. The DLC tasks (frame extraction, training, inference) need the official DLC image, because deeplabcut cannot be packaged here: it pins numpy<2 and needs imgaug, which nixpkgs does not carry. That container is supervised as one more process in the same stack rather than as a second one, so just dlc adds it and just down takes everything with it. It wants DLCWEB_PROJECTS_ROOT exported where just up runs (it is mounted into the container at the same absolute path) and the image built once — see deploy/README.md.
Layout
| Path | What |
|---|---|
web/ |
React Router 8 (framework mode) + Mantine + PixiJS. The only public server: UI + loader/action API + SSE. |
python/src/dlcweb/labels/ |
CollectedData_*.h5 ⇔ JSON conversion, merge, atomic I/O (the highest-risk area; pinned by round-trip tests). |
python/src/dlcweb/config/ |
config.yaml read + single-writer diff-patch apply. |
python/src/dlcweb/io_worker/ |
FastAPI localhost service (config/labels I/O, job enqueue). deeplabcut-independent. |
python/src/dlcweb/jobs/ |
arq job worker; imports deeplabcut lazily. |
nix/, flake.nix |
Dev shell, the process-compose dev stack (nix/dev.nix) and packages (incl. a from-source arq build). |
justfile |
Shortcuts for the dev commands (just up, just test). |
Tests
just test # both suites
just test-python # pytest — extra args are passed through: just test-python -k labels
just test-web # TypeScript typecheck, then the vitest suite
Attribution
This project is not a derivative of DeepLabCut; it is an independent web GUI that uses DeepLabCut as a compute engine. "DeepLabCut" is a trademark of its original developers, and this project does not use that name or logo. Note that pretrained model weights such as SuperAnimal are CC-BY-NC (non-commercial).