Skip to main content

freehands-architecture

Arquitectura completa del proyecto FreeHands — control del PC sin manos: mirada + gestos + voz. Módulos, flujos de datos y patrón para añadir nuevas funcionalidades.

Jump to install

Source facts

Repository
Ntizar/NtizarBrainMasterMind
Last source activity
June 26, 2026 at 12:05
Detected SKILL.md language
Spanish
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
19 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
freehands-architecture
description
Arquitectura completa del proyecto FreeHands — control del PC sin manos: mirada + gestos + voz. Módulos, flujos de datos y patrón para añadir nuevas funcionalidades.
version
4.0.0
author
Ntizar
tags
["mastermind","freehands","gaze","gestures","voice"]
# FreeHands — Arquitectura del Proyecto Proyecto de control del PC sin manos: **gaze tracking + gestos de mano + comandos de voz**. PyQt6 + MediaPipe FaceMesh + pyautogui. ## 📁 Estructura del proyecto ``` src/freehands/ ├── __main__.py # Entry point: freehands CLI ├── main.py # run_system() — orquestador principal ├── config.py # Constantes globales (FPS, dwell, thresholds) ├── gaze/ │ ├── __init__.py # Exporta GazeTracker, GazeRegressor, DeadZoneClamper │ ├── tracker.py # GazeTracker — MediaPipe FaceMesh → GazeFeatures (6-d) │ ├── calibration.py # GazeRegressor — ridge regression → coordenadas pantalla │ ├── dead_zones.py # DeadZoneClamper — recorte de bordes de pantalla │ └── head_pose.py # HeadPose — estimación 6DoF desde FaceMesh landmarks ├── gestures/ │ ├── hand_tracker.py # MediaPipe Hands → gesture classification │ ├── face_tracker.py # Expresiones faciales (AUs) │ └── stabilizer.py # Debounce + stability frames para gestos ├── fusion/ │ ├── fusion.py # MultimodalFusion — estado, dwell, bindings │ ├── state_machine.py # StateMachine — IDLE → ACTIVE → CONFIRMING → COOLDOWN │ └── channel_priority.py # Priorización dinámica: gesto vs voz cuando compiten ├── actions/ │ └── dispatcher.py # ActionDispatcher — pyautogui (click, scroll, zoom...) ├── voice/ │ ├── whisper_listener.py # Faster-Whisper ASR → acciones de voz │ └── continuous_dictation.py # Transcripción continua con feedback visual ├── plugins/ │ ├── base.py # FreeHandsPlugin base class — 7 pipeline hooks │ ├── loader.py # PluginLoader — discovery, priority, lifecycle │ └── example_cursor_trail.py # Ejemplo: trail de cursor ├── profiles/ │ ├── store.py # Profile (Pydantic) — JSON persistence, migration │ └── __init__.py # Gestos, bindings, thresholds por defecto ├── capture/ │ ├── camera.py # OpenCV webcam capture │ └── __init__.py └── ui/ ├── overlay.py # GazeOverlay — widget PyQt6 transparente ├── theme.py # Estilos CSS (azul #1E5BFF + naranja #FF7A1A) ├── calibration_game.py # Juego de calibración 9 puntos └── camera_selector.py # Selector de cámara tests/ ├── test_dead_zones.py # Tests dead zones ├── test_fusion.py # Tests multimodal fusion ├── test_state_machine.py # Tests state machine ├── test_profiles.py # Tests profiles ├── test_hand_tracker.py # Tests hand tracker ├── test_stabilizer.py # Tests gesture stabilizer ├── test_voice_commands.py # Tests voice commands └── test_head_pose.py # Tests head pose estimation ``` ## 🔄 Flujo de datos principal ``` Camera ──frame──▶ GazeTracker ──GazeFeatures──▶ GazeRegressor ──cursor (x,y) │ DeadZoneClamper ──cursor_clamped │ FineAimPointer ──cursor_smooth │ HandTracker ──gesture──▶ GestureStabilizer ──confirmed_gesture │ ┌─────────────────────────┘ ▼ MultimodalFusion (state machine + dwell + bindings) │ ▼ FusionResult {cursor_xy, state, dwell, fired_action} │ ▼ PluginPipeline (hooks: on_gaze, on_action, ...) │ ▼ ActionDispatcher ──pyautogui events ``` ## 🔑 Conceptos clave ### GazeFeatures (6-dimensional) Vector compacto extraído por frame: - `l_rel[0:2]` — iris izquierdo normalizado por ancho/alto ojo izq - `r_rel[0:2]` — iris derecho normalizado por ancho/alto ojo der - `head[0:2]` — proxy de pose de cabeza (nariz vs centro ojos) ### State Machine Estados: `IDLE → ACTIVE → CONFIRMING → COOLDOWN → ACTIVE` - **IDLE**: sistema pausado (palma abierta → resume) - **ACTIVE**: esperando mirada estable - **CONFIRMING**: mirada estable durante dwell_time_ms - **COOLDOWN**: tras acción, cooldown de COOLDOWN_MS_AFTER_ACTION ### Palm-scroll Gestos de scroll por palma (palm_scroll_up/down) son **instantáneos** — bypass del state machine, disparan directamente scroll_up/scroll_down. ### Dead Zones Recorte de coordenadas del cursor para que no alcance bordes extremos de pantalla. Por defecto 5% de margen en cada lado, mínimo 40px. ### Channel Priority (priorización dinámica) Cuando gesto y voz proponen acciones diferentes en el mismo frame, `decide_channel_priority()` resuelve el conflicto: 1. **Comandos de sistema** (volume, screenshot, show_desktop) → voz siempre gana 2. **Mismas acciones** en ambos → gesto gana (modalidad primaria) 3. **Acciones diferentes no-sistema** → gesto gana (tiene contexto espacial) - `voice_should_bypass_fusion()` identifica acciones que no pasan por el state machine. - Ver `references/channel-priority-pattern.md` para detalles completos. ### Head Pose (6DoF coarse displacement) El módulo `gaze/head_pose.py` estima rotación de cabeza (yaw, pitch, roll) desde landmarks de FaceMesh: - **Yaw**: vector nariz-ojo en plano XZ con `arctan2` - **Pitch**: eje frente-mentón relativo a línea horizontal ojo - **Roll**: inclinación lateral - Se aplica como desplazamiento "coarse" en la capa de fusión, complementando el pointer "fine" de gaze - Dead zone por defecto: ±5° para evitar jitter en pose neutral - Ver `references/head-pose-estimation-pattern.md` para patrón completo. ### Control de volumen por posición Y de mano Módulo `gestures/volume_control.py` — detector de volumen basado en la posición vertical del centroid de la mano en el frame. Es un **gesto de posición de zona**, distinto de palm-scroll (motion-based) y expresiones faciales (state-based): - **Zona superior** (Y < 0.35) → `volume_up` - **Zona inferior** (Y > 0.65) → `volume_down` - **Zona neutra** (0.35 ≤ Y ≤ 0.65) → sin acción - Usa centroid (promedio 21 landmarks) en lugar de muñeca sola → más estable - Cooldown de 15 frames (~0.5s) para evitar disparos continuos - Se ejecuta directamente vía `dispatcher.execute()`, sin pasar por gesture bindings - Ver `references/volume-control-pattern.md` para patrón completo. ### Fusión bimanual (HandFusion) Módulo `gestures/hand_fusion.py` — asigna roles complementarios cuando ambas manos están visibles: - **Mano derecha** → offset de cursor fino (±10px basado en posición relativa al centro) - **Mano izquierda** → scroll vertical (barrido) + zoom (pinch open/close) - Con una sola mano visible → no-op, comportamiento normal - Se integra en `tick()` después de `volume_control`, antes de `fusion.step_and_voice()` - Ver `references/bimanual-fusion-pattern.md` para patrón completo de integración. ### Integración de widgets UI overlay (patrón radial-menu) Para añadir un nuevo widget PyQt6 overlay al pipeline principal: 1. **Crear el módulo** en `ui/` con clase principal heredando de `QtWidgets.QWidget` 2. **Configurar flags Qt**: `FramelessWindowHint | WindowStaysOnTopHint | Tool` + `WA_TranslucentBackground` + `WA_TransparentForMouseEvents` 3. **Exportar en `ui/__init__.py`** para acceso desde `main.py` 4. **Integrar en `main.py`**: - Importar la clase + constantes necesarias (ej: `MENU_OPEN_DURATION_MS`) - Instanciar en `run_system()` con variables de estado (hold frames, gesture tracking) - Conectar señales con `pyqtSignal` → función de callback en scope de `run_system` - En el loop `tick()`: - `nonlocal` variables de estado del menú - Detectar trigger (ej: palm-hold sostenido) - Llamar `widget.update_dwell(cursor_xy)` si visible - Llamar `widget.close()` en transición a IDLE 5. **Verificar sintaxis** con `ast.parse()` antes de commit 6. **Commit + push** a ambos repos (FreeHands + Mastermind plan) Ver `references/radial-menu-integration.md` para ejemplo completo. ### Patrón para widgets que siguen al cursor (magnifier overlay) Algunos widgets no se abren/cierran con gestos sino que siguen al cursor de mirada de forma continua. Patrón diferente al de radial-menu: 1. **Crear el módulo** en `ui/` con clase principal que capture la pantalla (`QScreen.grabWindow`) y la escale. 2. **Método `update_cursor(cursor)`**: si cursor es None → hide; si no → show + update. 3. **Timer de refresh** interno (~80ms) para recapturar la pantalla sin depender del tick principal. 4. **Integrar en `main.py`**: - Instanciar con `zoom_factor` y `radius` configurables. - Variable de estado `magnifier_visible` (bool) declarada en `nonlocal` en `tick()`. - En `handle_voice_action()`: interceptar comandos como `"zoom_in"`/`"zoom_out"` ANTES de llegar al dispatcher, para repurposarlos como toggle del widget en lugar de `Ctrl++`/`Ctrl--`. - En el loop `tick()`: si `magnifier_visible`, llamar `magnifier.update_cursor(result.cursor_xy)`; si no y widget visible, llamar `update_cursor(None)`. - En `State.IDLE`: cerrar el widget y resetear `magnifier_visible = False`. 5. **Importante**: usar `overlay.flash_action()` para feedback visual, NO `magnifier.flash_action()` (el magnifier no tiene ese método). 6. **Verificar balance de braces/parens/brackets** con Python tras los cambios en main.py. Ver `references/magnifier-overlay-pattern.md` para patrón completo. ### Teclado virtual dual layout por ojos (Keyboard-Typing-with-Eyes) Teclado dividido en mitades izquierda/derecha; se muestra solo la mitad donde está el cursor de mirada. Reduce el espacio de búsqueda a la mitad. - Clase `LayoutSide` (enum: BOTH, LEFT, RIGHT) - Cada tecla marcada con `is_left_key: bool` - `get_visible_keys()` filtra dinámicamente - `update_layout_side(cursor_x)` detecta lado del cursor - `process_blink(blink)` soporta blink-to-select - Signal `layout_changed` para audio feedback - Ver `references/dual-layout-keyboard-pattern.md` para patrón completo. ### Detección de intención multimodal (gaze dwell + voz) Patrón para activar funcionalidades de dictado por voz mediante **detección multimodal**: el usuario debe mirar una región de texto (dwell) Y decir un comando de voz. Esto evita activaciones accidentales y da control preciso sobre dónde se inserta el texto dictado. - Clase `DictationIntentDetector` en `voice/dictation_intent.py` - Estado: `idle → gazing → ready → activate → reset` - Dwell configurable (default 500ms), debounce configurable (default 300ms) - Feedback visual: anillo pulsante naranja + badge "🎙 DICTANDO" - Integrado en `main.py` tick loop, independiente de `gaze_text_sel` - Ver `references/multimodal-dictation-intent-pattern.md` para patrón completo. ### Perfiles (Pydantic) - `gesture_bindings`: dict → mapea gesture_name → action_name - `gesture_thresholds`: dict → stability_frames + confidence_min por gesture - Migraciones automáticas en `load_profile()` para compatibilidad con versiones anteriores ### Sistema de Plugins (mejora #16) Arquitectura extensible para inyectar lógica custom en cada etapa del pipeline: ``` Camera → [on_frame] → Gaze → [on_gaze] → Filter → [on_filter] → Gesture → [on_gesture] → Fusion → [on_fusion] → Action → [on_action] → Overlay → [on_overlay] ``` **API principal:** - `FreeHandsPlugin` — clase base con 7 hooks (todos opcionales, passthrough por defecto) - `PluginContext` — dataclass con frame, cursor, gesture, action, blink, metadata - `PluginLoader` — descubrimiento automático desde directorio, registro manual, orden por priority - `plugins_dir` en Profile para configurar ruta de plugins **Patrón para crear un plugin:** ```python from freehands.plugins import FreeHandsPlugin, PluginContext class MiPlugin(FreeHandsPlugin): name = "mi_plugin" version = "1.0.0" description = "Descripción" priority = 50 # menor = ejecuta antes def on_gaze(self, cursor, ctx): # Modificar cursor antes de llegar a fusion return modified_cursor def on_action(self, action, ctx): # Interceptar acción antes de dispatch return suppressed_or_modified_action ``` **Reglas:** - Hooks no implementados se saltan silenciosamente (no override) - Excepciones en hooks se capturan y loguean, no rompen el pipeline - `ctx.metadata` es un dict compartido entre todos los plugins del frame - `loader.run_all(ctx)` modifica el contexto en-place, se aplica al main loop Ver `references/plugin-system-pattern.md` para API completa y ejemplos. ### Calibración con Gaussian Process (auto-calibración continua) Módulo adicional `gaze/calibration.py` que añade un modelo GP alongside el Ridge regression: - `GPGazeModel` — modelo serializable (kernel RBF/Matern, lengthscale, noise_level, training data sliding window) - `GPGazeRegressor` — predictor con Kalman smoothing, API idéntica a `GazeRegressor` - `fit_gp_model()` — entrena GP desde samples de calibración - `update_gp_model()` — añade muestras y reentrena con ventana deslizante (max 200 samples) - Auto-calibración en `main.py`: durante uso normal en estado ACTIVE, cuando cursor es estable (<15px movimiento) y confianza >0.7, se recopilan muestras implícitas - Reentrenamiento cada 30 frames, guardado al perfil cada 5 min - Ver `references/gp-auto-calibration-pattern.md` para patrón completo. ## 🛠️ Patrón para añadir un nuevo módulo Cuando se añade una funcionalidad nueva a FreeHands: 1. **Crear el módulo** en el subdirectorio correspondiente (`gaze/`, `gestures/`, `fusion/`, `voice/`, `ui/`, `actions/`) 2. **Exportar en `__init__.py`** del subdirectorio 3. **Integrar en `main.py`**: - Importar la nueva clase/módulo - Instanciar en `run_system()` con los parámetros adecuados - Aplicar en el loop `tick()` en el punto correcto del pipeline 4. **Añadir tests** en `tests/test_{modulo}.py` 5. **Actualizar perfil** si hay nuevas configuraciones (`profiles/store.py`) 6. **Verificar sintaxis** antes de commit: `python3 -c "import py_compile; py_compile.compile('path/to/file.py', doraise=True)"` 7. **Commit + push**: `git add -A && git commit -m "9009: mejora #N: descripción" && git push` ### Patrón para detector de intención multimodal (gaze + voz) Cuando la mejora requiere activar una funcionalidad por **mirada + voz simultáneamente**: 1. **Crear detector** en `voice/` con estado: `idle → gazing → ready → activate → reset` 2. **Hit-test** contra regiones de texto (coordenadas relativas al centro del overlay) 3. **Dwell configurable** + debounce para evitar falsos positivos por glitches de MediaPipe 4. **Integrar en main.py tick()**: actualizar detector con regiones, check voice commands si `ready_to_activate` 5. **Feedback visual** en overlay: anillo pulsante cuando `ready`, badge cuando activo 6. **Consume el estado** con `consume_ready()` para evitar activaciones repetidas ## 📐 Convenciones de código - `from __future__ import annotations` en todos los archivos - Dataclasses con `frozen=True` para configuraciones inmutables - Type hints en todos los parámetros y retornos - Docstrings en inglés, comentarios en castellano - Líneas máx: 100 caracteres (ruff config) - Tests: nombre `test_{funcionalidad}.py`, funciones `test_{descripción}()` - No usar `pytest` como dependencia — los tests pueden correr sin él (import directo) ## 📚 Referencias | Archivo | Descripción | |---------|-------------| | `references/channel-priority-pattern.md` | Priorización dinámica de canales: reglas de prioridad gesto vs voz | | `references/audio-feedback-pattern.md` | Patrón para añadir feedback auditivo (beeps) en gestos y voz | | `references/dead-zones-pattern.md` | Patrón para dead zones en bordes de pantalla | | `references/blink-detection-ear.md` | Detección de parpadeo via Eye Aspect Ratio (EAR) — mejora #5 | | `references/external-gesture-profiles.md` | Perfiles de gestos externos JSON: carga, merge, pitfall de `_repair_essential_bindings` | | `references/asr-backend-pattern.md` | Patrón para añadir nuevos backends ASR (Vosk, whisper.cpp, etc.) a VoiceListener | | `references/radial-menu-integration.md` | Patrón para integrar widgets overlay PyQt6 (radial menu, OSD) en el pipeline principal | | `references/dual-layout-keyboard-pattern.md` | Teclado virtual dual-layout: split izq/der basado en posición del cursor, blink-to-select | | `references/overlay-enhancement-pattern.md` | Patrón para mejorar GazeOverlay: halo radial, pens cosméticos, soporte multi-monitor | | `references/plugin-system-pattern.md` | API completa del sistema de plugins: FreeHandsPlugin, PluginContext, PluginLoader, hooks, ejemplos | | `references/head-pose-estimation-pattern.md` | Estimación 6DoF head pose desde FaceMesh: algoritmo yaw/pitch/roll, dead zones, coarse displacement | | `references/volume-control-pattern.md` | Control de volumen por posición Y de mano: gesto de posición de zona, cooldown, centroid vs muñeca | | `references/multimodal-dictation-intent-pattern.md` | Detección de intención multimodal: gaze dwell en región de texto + comando de voz para activar dictado sin falsos positivos | | `references/air-scroll-pattern.md` | Air-scroll / swipe: detección de barrido vertical con cualquier pose de mano, umbral de desplazamiento |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub