Todos os projetos projetos/04 · roda na sua máquina

Gesture AI Desk

Controlar o computador com a mão pela webcam: pausar música, mudar volume, tirar print. Nenhum quadro sai da sua máquina em nenhum momento.

Ano
2026
Papel
Backend e interface
Stack
Python · MediaPipe · OpenCV · FastAPI · WebSocket
Link
código no 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.
os 21 pontos que o MediaPipe devolve, desenhados por cima da mão em tempo real passe o cursor para ver em cor

O problema

Começou como uma pergunta prática: dá pra pausar uma música com a mão suja de farinha? Detectar a mão em si é problema resolvido, e bibliotecas fazem isso há anos.

O que continua difícil em interface por gesto é saber a intenção: o computador precisa distinguir entre você querendo alguma coisa e você passando a mão na frente da câmera pra coçar o nariz.

A decisão

Tudo offline: nenhuma API e nenhum serviço de visão na nuvem. Além da questão de privacidade, tem um motivo prático: uma webcam ligada mandando imagem pra fora é o tipo de coisa que a pessoa desinstala no segundo dia. O modelo do MediaPipe roda local, e o navegador só conversa com o meu próprio processo por WebSocket.

Três travas entre a mão e a ação

A 30 quadros por segundo, um detector ingênuo dispara trinta vezes por segundo. Você faz sinal de positivo por um segundo e o volume sobe trinta vezes. O que separa um brinquedo de uma ferramenta usável são as camadas que vieram depois da detecção.

Primeiro o estado dos dedos vira um número de cinco bits, um por dedo, e o gesto é uma consulta numa tabela: barato o bastante pra caber no orçamento de cada quadro. Depois um estabilizador só aceita a mudança quando o mesmo gesto aparece em cinco quadros seguidos, o que mata o tremor de detecção. Em cima disso, um temporizador exige meio segundo de gesto mantido antes de disparar, com um anel de progresso na tela pra você ver que está carregando e poder desistir. E depois de disparar, um segundo de descanso em que aquele gesto simplesmente não conta.

Sozinha, cada uma dessas quatro camadas é bem burra. Empilhadas, elas dão conta de transformar um fluxo barulhento em uma ação que a pessoa quis fazer.

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}
21pontos por mão
30quadros por segundo
5→0,5s→1sestabilizar, segurar, descansar

A interface que eu tive que reconstruir

O repositório tinha uma pasta de frontend que na verdade era um ponteiro quebrado de submodule: o painel em React nunca esteve versionado ali. Descobri isso tentando rodar o projeto do zero, que é exatamente o que qualquer pessoa que clona faria.

Reescrevi como um arquivo HTML único, sem build e sem dependência: câmera, gesto atual com o anel de hold, lista de comandos, estado da conexão e log de ações disparadas. O esqueleto da mão é desenhado num canvas por cima da imagem, com as coordenadas normalizadas que o backend manda. Ficou mais simples de manter do que o que existia antes, e agora clonar o repositório e rodar funciona.

A parte difícil não era a do nome

A detecção de gestos, que é o que dá nome ao projeto, foi um dia de trabalho, porque o MediaPipe carrega o peso todo. O resto do tempo eu passei decidindo quando ignorar o que ele detectou, e essa parte não aparece em nenhuma demonstração.