All projects work/04 · runs on your machine

Gesture AI Desk

Controlling the computer with your hand through the webcam: pause music, change the volume, take a screenshot. Not one frame ever leaves your machine.

Year
2026
Role
Back end and interface
Stack
Python · MediaPipe · OpenCV · FastAPI · WebSocket
Link
code on GitHub ↗
Painel do Gesture AI Desk com a imagem da webcam e, sobre a mão, o esqueleto de 21 pontos ligados por linhas. Ao lado, o gesto detectado e o anel de progresso do hold.
the 21 landmarks MediaPipe returns, drawn over the hand in real time hover to see it in colour

The problem

It started as a practical question: can you pause a song with flour on your hands? Detecting the hand itself is a solved problem, and libraries have been doing it for years.

What is still hard in a gesture interface is intent: the computer has to tell the difference between you wanting something and you moving your hand past the camera to scratch your nose.

The decision

Everything offline: no API and no cloud vision service. Beyond the privacy question there is a practical reason: a live webcam sending images somewhere else is the kind of thing people uninstall on day two. The MediaPipe model runs locally, and the browser only talks to my own process over WebSocket.

Three locks between the hand and the action

At 30 frames per second a naive detector fires thirty times a second. You hold a thumbs-up for one second and the volume goes up thirty times. What separates a toy from a usable tool is the layers that came after the detection.

First the state of the fingers becomes a five-bit number, one bit per finger, and the gesture is a table lookup: cheap enough to fit inside each frame’s budget. Then a stabiliser only accepts a change when the same gesture shows up in five consecutive frames, which kills the detection jitter. On top of that, a timer requires half a second of held gesture before firing, with a progress ring on screen so you can see it charging and back out. And after it fires, one second of rest in which that gesture simply does not count.

On its own, each of those four layers is fairly stupid. Stacked, they manage to turn a noisy stream into an action the person meant to take.

hold_manager.py ver inteiro ↗
# Gestures that bypass the hold mechanism entirely
_CONTINUOUS_GESTURES = {Gesture.PINCH, Gesture.SWIPE_LEFT, Gesture.SWIPE_RIGHT}


def update(self, gesture: Gesture, timestamp: float) -> dict:
    # Continuous gestures skip hold entirely
    if gesture in _CONTINUOUS_GESTURES:
        return {"hold_progress": 0.0, "action_triggered": False, "in_cooldown": False}

    in_cooldown = (timestamp - self._last_trigger_time) < self.cooldown

    # Gesture changed — reset hold
    if gesture != self._current_gesture:
        self._current_gesture = gesture
        self._hold_start = timestamp
        self._triggered = False
        return {"hold_progress": 0.0, "action_triggered": False, "in_cooldown": in_cooldown}

    elapsed = timestamp - self._hold_start
    progress = min(elapsed / self.hold_duration, 1.0)

    # Already triggered for this hold — wait for gesture change
    if self._triggered:
        return {"hold_progress": 1.0, "action_triggered": False, "in_cooldown": in_cooldown}

    if progress >= 1.0 and not in_cooldown:
        self._triggered = True
        self._last_trigger_time = timestamp
        return {"hold_progress": 1.0, "action_triggered": True, "in_cooldown": False}

    return {"hold_progress": progress, "action_triggered": False, "in_cooldown": in_cooldown}
21landmarks per hand
30frames per second
5→0.5s→1sstabilise, hold, rest

The interface I had to rebuild

The repository had a frontend folder that was really a broken submodule pointer: the React panel had never been versioned there. I found that out trying to run the project from scratch, which is exactly what anyone cloning it would do.

I rewrote it as a single HTML file, no build and no dependency: camera, current gesture with the hold ring, command list, connection state and a log of fired actions. The hand skeleton is drawn on a canvas over the image, from the normalised coordinates the back end sends. It ended up simpler to maintain than what was there before, and now cloning the repository and running it works.

The hard part was not the one in the name

Gesture detection, the thing the project is named after, was one day of work, because MediaPipe carries all the weight. The rest of the time I spent deciding when to ignore what it detected, and that part never shows up in a demo.