No description
  • TypeScript 46.5%
  • Python 45.2%
  • Nix 7.3%
  • Just 0.6%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
natsukium b6658f6d02
Stop labeling from starving the io-worker to death
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
2026-08-23 12:00:30 +09:00
deploy Drop the compose file; the dev stack is the way to run this 2026-07-26 00:55:35 +09:00
docs Drop the add/select/pan interaction modes 2026-08-23 11:14:59 +09:00
nix Stop labeling from starving the io-worker to death 2026-08-23 12:00:30 +09:00
python Stop labeling from starving the io-worker to death 2026-08-23 12:00:30 +09:00
web Stop labeling from starving the io-worker to death 2026-08-23 12:00:30 +09:00
.gitignore Drop the add/select/pan interaction modes 2026-08-23 11:14:59 +09:00
flake.lock Generate the dev stack's process-compose config from Nix 2026-07-25 23:42:25 +09:00
flake.nix Put a just entry point in front of the dev commands 2026-07-25 23:42:25 +09:00
justfile Run the vitest suite from just test 2026-08-23 11:34:12 +09:00
LICENSE Implement Phase 0 foundation: Nix env, io-worker, job queue, labeling PoC 2026-07-18 20:42:42 +09:00
README.md Run the vitest suite from just test 2026-08-23 11:34:12 +09:00

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

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) + pure useReducer, 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).