Vibe-coded pose/body detection for kinematic Godot video games targeting Steam Deck + webcam hardware.
  • Rust 80.4%
  • Nix 8.1%
  • Python 6.6%
  • Objective-C 2.5%
  • OpenSCAD 1.7%
  • Other 0.7%
Find a file
Daniel Flanagan 464280309d
All checks were successful
/ check (push) Successful in 8s
feat(deck-pose): publish the region a player can be detected in
The model crop decides where a person can be seen at all, and until now
nothing outside inference could know it. Coordinates are folded back out
to full-frame, which made the crop look like a private implementation
detail — and it is, for interpreting a position. It is not private for
deciding where a consumer may place something a player has to reach.

deck-slice sized its play area as a fraction of the FRAME while this
daemon detected in half of it, so the band between the two was reachable
in the game and invisible here. The settings screen's own buttons sat in
that band and could not be pressed. Every message type matched, every
test passed, and neither repository could have caught it alone: each
number was defensible where it was written.

So `detect` goes on every pose message, and doc/protocol.md §3 states
the invariant it exists for: a consumer's play region must be DERIVED
from it, never sized independently. The value published is the letterbox's
SANITISED crop rather than `cfg.model.crop` — a bound taken from a config
value that was silently corrected would not hold, which is the one case
where it matters. Per-frame rather than sent once, because an adaptive
crop would vary it and there is no other channel a consumer is
guaranteed to keep receiving.

Tested: `the_detect_fraction_matches_the_reach_of_the_model_view` ties
the reported number to the geometry through `undo` rather than to the
config it came from, so a field that merely echoed the setting fails it;
both new decode tests were confirmed to fail against a deliberately
wrong value. `every_pose_line_carries_the_detected_region` covers the
empty-players case, since a frame with nobody in it still bounds where
somebody could be.

Additive, so no consumer breaks: an old one sees a field it ignores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtnYWFX9eMBH7oK3gmcMMs
2026-08-10 12:46:40 -05:00
.forgejo/workflows chore: stand up deck-pose as its own repository 2026-08-04 15:51:54 -05:00
doc feat(deck-pose): publish the region a player can be detected in 2026-08-10 12:46:40 -05:00
issues/open docs(deck-pose): one document for the macOS port, pickable up cold 2026-08-05 18:36:46 -05:00
LICENSES feat(deck-pose): ship the attribution the bundled weights require 2026-08-05 14:28:33 -05:00
macos fix(deck-pose): stop the macOS launcher from orphaning the camera 2026-08-07 11:48:22 -05:00
nix feat(deck-pose): publish the region a player can be detected in 2026-08-10 12:46:40 -05:00
optional-hardware feat(optional-hardware): printable case for the ELP camera board 2026-08-04 16:54:10 -05:00
src feat(deck-pose): publish the region a player can be detected in 2026-08-10 12:46:40 -05:00
tools docs(deck-pose): framing beats resolution, and wrists are the constraint 2026-08-05 10:03:39 -05:00
.gitignore feat(deck-pose): benchmark inference without taking the camera 2026-08-04 20:30:57 -05:00
build.rs feat(deck-pose): capture on macOS, so the daemon builds and runs there 2026-08-05 20:22:13 -05:00
Cargo.lock feat(deck-pose): encode video for cameras that cannot produce JPEG 2026-08-05 20:42:24 -05:00
Cargo.toml feat(deck-pose): encode video for cameras that cannot produce JPEG 2026-08-05 20:42:24 -05:00
flake.lock chore: stand up deck-pose as its own repository 2026-08-04 15:51:54 -05:00
flake.nix fix(deck-pose): stop the macOS launcher from orphaning the camera 2026-08-07 11:48:22 -05:00
LICENSE chore: license under GPL-3.0-or-later 2026-08-05 14:19:52 -05:00
NOTICE fix(deck-pose): the JPEG encoder's IJG obligation travels with builds too 2026-08-05 20:59:34 -05:00
README.md docs(deck-pose): one document for the macOS port, pickable up cold 2026-08-05 18:36:46 -05:00

deck-pose

Markerless full-body pose input: a camera-only body-tracking daemon that publishes a pose stream and discrete gesture events over Unix sockets, so a game can use a body as a controller. Modelled on the Nex Playground.

v4l2 capture (newest-frame-only) → MoveNet MultiPose Lightning via ONNX Runtime → IoU + Hungarian player tracking → One Euro smoothing → gesture recognisers → NDJSON on a socket, plus an optional second socket forwarding the camera's raw MJPEG for a video background.

Licence

GPL-3.0-or-later — see LICENSE.

Dependencies keep their own terms: ONNX Runtime is MIT, the ort bindings and most of the Rust tree are Apache-2.0/MIT, and the MoveNet weights are Apache-2.0. GPLv3 rather than GPLv2 is a requirement rather than a preference — Apache-2.0 is compatible with the former and not the latter, and the weights and ort are both Apache-2.0.

The consumer's THIRD-PARTY.md carries the full audit, including the provenance of the model re-export.

Read this first

  • doc/protocol.md — the wire protocol. This is the API this repo owns and the contract consumers depend on. Consumers live in other repositories now, so a protocol change is not visible in one diff.
  • doc/deck-pose.md — running it, the latency budget, the model rationale, and measurements from real cameras.
  • doc/cross-platform.mdthis is Linux only, and what it would take not to be. Read before promising a Windows or macOS build: capture and the publish socket are both platform-bound, and the cost is weeks rather than a weekend.
  • doc/macos-port.md — the macOS port, measured on real hardware and deliberately deferred. Self-contained: build blockers, the capture contract a backend must implement, and the gotchas (CoreML is slower; camera permission fails silently).
  • issues/open/ — known open defects. Notably, the daemon still emits phantom swipe/punch/jump events on tracking discontinuities.

Known consumer: deck-slice, a Fruit-Ninja-style slice game.

Using it

nix run git+https://git.lyte.dev/lytedev/deck-pose -- --help

As a flake input, with the NixOS module:

{
  inputs.deck-pose.url = "git+https://git.lyte.dev/lytedev/deck-pose";

  # in your host's modules:
  imports = [ inputs.deck-pose.nixosModules.default ];

  lyte.deck-pose = {
    enable = true;
    users = [ "you" ];              # camera access; granting it is a decision
    socket = "%t/deck-pose.sock";   # %t, never a literal /run/user/1000
    video = {
      enable = true;
      socket = "%t/deck-pose-video.sock";
    };
    service.enable = true;          # socket-activated; exits when idle
  };
}

The module pins the package itself, so importing it works without also applying overlays.default. Apply the overlay instead (or as well) if you want pkgs.deck-pose available generally.

Outputs

output what
packages.<system>.deck-pose the daemon (also .default)
overlays.default adds pkgs.deck-pose
nixosModules.default the lyte.deck-pose module (also .deck-pose)
checks.<system> package (build + cargo test), clippy, fmt
devShells.<system>.default rust toolchain, ORT_DYLIB_PATH preset, v4l-utils

Building and testing

nix build
nix flake check

The unit tests are pure arithmetic over the filter, tracker, config and publisher layers — no camera, no model file, no network, no timezone database — so they run inside the nix sandbox. Keep them that way: cargo test runs inside the build derivation, so a test that needs any of those does not fail loudly, it makes the package unbuildable.