ai-virtual-mouse-controller 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/PKG-INFO +9 -9
  2. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/README.md +8 -6
  3. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/PKG-INFO +9 -9
  4. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/SOURCES.txt +12 -0
  5. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/requires.txt +0 -2
  6. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/config.py +71 -14
  7. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/__init__.py +1 -1
  8. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/camera.py +122 -25
  9. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/cursor_controller.py +32 -16
  10. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/gesture_detector.py +65 -40
  11. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_anchor.py +89 -5
  12. ai_virtual_mouse_controller-1.3.0/core/hand_gl_renderer.py +347 -0
  13. ai_virtual_mouse_controller-1.3.0/core/hand_mesh.py +501 -0
  14. ai_virtual_mouse_controller-1.3.0/core/hand_pose.py +217 -0
  15. ai_virtual_mouse_controller-1.3.0/core/hand_soft_renderer.py +262 -0
  16. ai_virtual_mouse_controller-1.3.0/core/hologram_gl_backend.py +334 -0
  17. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hologram_overlay.py +53 -4
  18. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/presentation_controller.py +3 -2
  19. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/runtime_settings.py +24 -16
  20. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/settings_panel.py +1 -1
  21. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/smoothing.py +37 -0
  22. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/ui_overlay.py +2 -2
  23. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/main.py +15 -1
  24. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/pyproject.toml +6 -8
  25. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/services/virtual_mouse_service.py +212 -127
  26. ai_virtual_mouse_controller-1.3.0/tests/test_camera.py +118 -0
  27. ai_virtual_mouse_controller-1.3.0/tests/test_cursor_feel.py +92 -0
  28. ai_virtual_mouse_controller-1.3.0/tests/test_gesture_detector_behavior.py +200 -0
  29. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_anchor_bottom.py +33 -0
  30. ai_virtual_mouse_controller-1.3.0/tests/test_hand_mesh.py +75 -0
  31. ai_virtual_mouse_controller-1.3.0/tests/test_hand_pose.py +111 -0
  32. ai_virtual_mouse_controller-1.3.0/tests/test_hand_renderers.py +98 -0
  33. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hologram_overlay.py +3 -3
  34. ai_virtual_mouse_controller-1.3.0/tests/test_motion_quality.py +87 -0
  35. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_overlay_smoke.py +4 -2
  36. ai_virtual_mouse_controller-1.3.0/tests/test_runtime_settings.py +50 -0
  37. ai_virtual_mouse_controller-1.3.0/tests/test_service_camera_loss.py +59 -0
  38. ai_virtual_mouse_controller-1.2.0/core/hand_mesh.py +0 -794
  39. ai_virtual_mouse_controller-1.2.0/core/hologram_gl_backend.py +0 -964
  40. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/LICENSE +0 -0
  41. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/dependency_links.txt +0 -0
  42. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/entry_points.txt +0 -0
  43. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/top_level.txt +0 -0
  44. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/avmc.py +0 -0
  45. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/click_burst.py +0 -0
  46. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/finger_posture.py +0 -0
  47. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_renderer.py +0 -0
  48. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_tracker.py +0 -0
  49. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/models/hand_landmarker.task +0 -0
  50. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/perf_telemetry.py +0 -0
  51. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/utils.py +0 -0
  52. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/windows_path.py +0 -0
  53. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/services/__init__.py +0 -0
  54. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/setup.cfg +0 -0
  55. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_click_burst.py +0 -0
  56. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_cursor_bottom_reach.py +0 -0
  57. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_cursor_click_at.py +0 -0
  58. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_gesture_detector.py +0 -0
  59. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_renderer.py +0 -0
  60. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_tracker.py +0 -0
  61. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_perf_telemetry.py +0 -0
  62. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_presentation_controller.py +0 -0
  63. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_settings_panel.py +0 -0
  64. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_smoothing.py +0 -0
  65. {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_windows_path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-virtual-mouse-controller
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Controle de cursor por gestos da mao via webcam, com holograma 3D opcional.
5
5
  Author: Guilherme Moraes Franco
6
6
  License: MIT
@@ -32,10 +32,8 @@ Requires-Dist: opencv-contrib-python<5.0.0,>=4.10.0
32
32
  Requires-Dist: mediapipe>=0.10.18
33
33
  Requires-Dist: pyautogui<1.0.0,>=0.9.54
34
34
  Requires-Dist: numpy>=1.26.0
35
- Requires-Dist: moderngl>=5.10.0
36
35
  Requires-Dist: PySide6>=6.6.0
37
36
  Provides-Extra: hologram
38
- Requires-Dist: moderngl>=5.10.0; extra == "hologram"
39
37
  Requires-Dist: PySide6>=6.6.0; extra == "hologram"
40
38
  Provides-Extra: dev
41
39
  Requires-Dist: pytest>=8.0.0; extra == "dev"
@@ -89,7 +87,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
89
87
 
90
88
  100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
91
89
 
92
- **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · ModernGL/PySide6 (opcional)
90
+ **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
93
91
 
94
92
  ---
95
93
 
@@ -202,12 +200,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
202
200
  | Seção | O que controla |
203
201
  |---|---|
204
202
  | `CAMERA` | índice da câmera, resolução, FPS alvo |
203
+ | `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
205
204
  | `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
206
- | `PINCH` | thresholds de detecção de clique (com escala adaptativa por tamanho da mão) |
205
+ | `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
207
206
  | `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
208
207
  | `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
209
208
  | `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
210
- | `HOLOGRAM` | backend (GL/QPainter), cor, opacidade, FPS |
209
+ | `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
211
210
 
212
211
  Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
213
212
 
@@ -240,7 +239,7 @@ webcam → MediaPipe Hands (21 landmarks)
240
239
  CursorController ← PyAutoGUI move o cursor real
241
240
  ```
242
241
 
243
- Componentes auxiliares: `HologramOverlay` (renderização 3D opcional via ModernGL), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
242
+ Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
244
243
 
245
244
  Estrutura de pastas:
246
245
 
@@ -249,7 +248,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
249
248
  services/ # orquestração do loop principal
250
249
  config.py # constantes de calibração documentadas
251
250
  main.py # entry point
252
- tests/ # cobertura dos módulos puros (sem cv2/mediapipe)
251
+ scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
252
+ tests/ # unitários + gate de qualidade de movimento
253
253
  docs/ # site estático + assets
254
254
  ```
255
255
 
@@ -269,7 +269,7 @@ make test-gpu # só testes que precisam de display/GPU real
269
269
  make test-cov # com coverage HTML
270
270
  ```
271
271
 
272
- Testes marcados com `gpu` (overlay PySide6/ModernGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
272
+ Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
273
273
 
274
274
  ---
275
275
 
@@ -42,7 +42,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
42
42
 
43
43
  100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
44
44
 
45
- **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · ModernGL/PySide6 (opcional)
45
+ **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
46
46
 
47
47
  ---
48
48
 
@@ -155,12 +155,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
155
155
  | Seção | O que controla |
156
156
  |---|---|
157
157
  | `CAMERA` | índice da câmera, resolução, FPS alvo |
158
+ | `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
158
159
  | `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
159
- | `PINCH` | thresholds de detecção de clique (com escala adaptativa por tamanho da mão) |
160
+ | `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
160
161
  | `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
161
162
  | `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
162
163
  | `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
163
- | `HOLOGRAM` | backend (GL/QPainter), cor, opacidade, FPS |
164
+ | `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
164
165
 
165
166
  Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
166
167
 
@@ -193,7 +194,7 @@ webcam → MediaPipe Hands (21 landmarks)
193
194
  CursorController ← PyAutoGUI move o cursor real
194
195
  ```
195
196
 
196
- Componentes auxiliares: `HologramOverlay` (renderização 3D opcional via ModernGL), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
197
+ Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
197
198
 
198
199
  Estrutura de pastas:
199
200
 
@@ -202,7 +203,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
202
203
  services/ # orquestração do loop principal
203
204
  config.py # constantes de calibração documentadas
204
205
  main.py # entry point
205
- tests/ # cobertura dos módulos puros (sem cv2/mediapipe)
206
+ scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
207
+ tests/ # unitários + gate de qualidade de movimento
206
208
  docs/ # site estático + assets
207
209
  ```
208
210
 
@@ -222,7 +224,7 @@ make test-gpu # só testes que precisam de display/GPU real
222
224
  make test-cov # com coverage HTML
223
225
  ```
224
226
 
225
- Testes marcados com `gpu` (overlay PySide6/ModernGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
227
+ Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
226
228
 
227
229
  ---
228
230
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-virtual-mouse-controller
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Controle de cursor por gestos da mao via webcam, com holograma 3D opcional.
5
5
  Author: Guilherme Moraes Franco
6
6
  License: MIT
@@ -32,10 +32,8 @@ Requires-Dist: opencv-contrib-python<5.0.0,>=4.10.0
32
32
  Requires-Dist: mediapipe>=0.10.18
33
33
  Requires-Dist: pyautogui<1.0.0,>=0.9.54
34
34
  Requires-Dist: numpy>=1.26.0
35
- Requires-Dist: moderngl>=5.10.0
36
35
  Requires-Dist: PySide6>=6.6.0
37
36
  Provides-Extra: hologram
38
- Requires-Dist: moderngl>=5.10.0; extra == "hologram"
39
37
  Requires-Dist: PySide6>=6.6.0; extra == "hologram"
40
38
  Provides-Extra: dev
41
39
  Requires-Dist: pytest>=8.0.0; extra == "dev"
@@ -89,7 +87,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
89
87
 
90
88
  100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
91
89
 
92
- **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · ModernGL/PySide6 (opcional)
90
+ **Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
93
91
 
94
92
  ---
95
93
 
@@ -202,12 +200,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
202
200
  | Seção | O que controla |
203
201
  |---|---|
204
202
  | `CAMERA` | índice da câmera, resolução, FPS alvo |
203
+ | `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
205
204
  | `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
206
- | `PINCH` | thresholds de detecção de clique (com escala adaptativa por tamanho da mão) |
205
+ | `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
207
206
  | `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
208
207
  | `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
209
208
  | `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
210
- | `HOLOGRAM` | backend (GL/QPainter), cor, opacidade, FPS |
209
+ | `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
211
210
 
212
211
  Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
213
212
 
@@ -240,7 +239,7 @@ webcam → MediaPipe Hands (21 landmarks)
240
239
  CursorController ← PyAutoGUI move o cursor real
241
240
  ```
242
241
 
243
- Componentes auxiliares: `HologramOverlay` (renderização 3D opcional via ModernGL), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
242
+ Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
244
243
 
245
244
  Estrutura de pastas:
246
245
 
@@ -249,7 +248,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
249
248
  services/ # orquestração do loop principal
250
249
  config.py # constantes de calibração documentadas
251
250
  main.py # entry point
252
- tests/ # cobertura dos módulos puros (sem cv2/mediapipe)
251
+ scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
252
+ tests/ # unitários + gate de qualidade de movimento
253
253
  docs/ # site estático + assets
254
254
  ```
255
255
 
@@ -269,7 +269,7 @@ make test-gpu # só testes que precisam de display/GPU real
269
269
  make test-cov # com coverage HTML
270
270
  ```
271
271
 
272
- Testes marcados com `gpu` (overlay PySide6/ModernGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
272
+ Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
273
273
 
274
274
  ---
275
275
 
@@ -17,8 +17,11 @@ core/cursor_controller.py
17
17
  core/finger_posture.py
18
18
  core/gesture_detector.py
19
19
  core/hand_anchor.py
20
+ core/hand_gl_renderer.py
20
21
  core/hand_mesh.py
22
+ core/hand_pose.py
21
23
  core/hand_renderer.py
24
+ core/hand_soft_renderer.py
22
25
  core/hand_tracker.py
23
26
  core/hologram_gl_backend.py
24
27
  core/hologram_overlay.py
@@ -33,17 +36,26 @@ core/windows_path.py
33
36
  core/models/hand_landmarker.task
34
37
  services/__init__.py
35
38
  services/virtual_mouse_service.py
39
+ tests/test_camera.py
36
40
  tests/test_click_burst.py
37
41
  tests/test_cursor_bottom_reach.py
38
42
  tests/test_cursor_click_at.py
43
+ tests/test_cursor_feel.py
39
44
  tests/test_gesture_detector.py
45
+ tests/test_gesture_detector_behavior.py
40
46
  tests/test_hand_anchor_bottom.py
47
+ tests/test_hand_mesh.py
48
+ tests/test_hand_pose.py
41
49
  tests/test_hand_renderer.py
50
+ tests/test_hand_renderers.py
42
51
  tests/test_hand_tracker.py
43
52
  tests/test_hologram_overlay.py
53
+ tests/test_motion_quality.py
44
54
  tests/test_overlay_smoke.py
45
55
  tests/test_perf_telemetry.py
46
56
  tests/test_presentation_controller.py
57
+ tests/test_runtime_settings.py
58
+ tests/test_service_camera_loss.py
47
59
  tests/test_settings_panel.py
48
60
  tests/test_smoothing.py
49
61
  tests/test_windows_path.py
@@ -2,7 +2,6 @@ opencv-contrib-python<5.0.0,>=4.10.0
2
2
  mediapipe>=0.10.18
3
3
  pyautogui<1.0.0,>=0.9.54
4
4
  numpy>=1.26.0
5
- moderngl>=5.10.0
6
5
  PySide6>=6.6.0
7
6
 
8
7
  [dev]
@@ -13,5 +12,4 @@ mypy>=1.10.0
13
12
  pre-commit>=3.7.0
14
13
 
15
14
  [hologram]
16
- moderngl>=5.10.0
17
15
  PySide6>=6.6.0
@@ -146,6 +146,17 @@ DPI_MULTIPLIER_MIN: float = 0.7
146
146
  DPI_MULTIPLIER_MAX: float = 1.4
147
147
  DPI_FIXED_MULTIPLIER: float = 1.00
148
148
 
149
+ DPI_SIZE_SMOOTHING_S: float = 0.6
150
+ """Constante de tempo (s) do tamanho da mao usado no DPI adaptativo.
151
+
152
+ O tamanho da mao no frame estima a DISTANCIA ate a camera — algo que muda
153
+ devagar. Frame a frame ele oscila quando o pulso inclina/gira e com o
154
+ ruido do tracker; como o DPI escala a posicao a partir do centro da tela,
155
+ isso fazia o cursor andar sozinho com a mao parada (ate ~50 px longe do
156
+ centro). 0.6 s filtra a inclinacao e ainda acompanha quem aproxima ou
157
+ afasta a mao da camera. 0 = comportamento antigo (cru).
158
+ """
159
+
149
160
  # ---------------------------------------------------------------------
150
161
  # SUAVIZACAO
151
162
  # ---------------------------------------------------------------------
@@ -155,9 +166,18 @@ SMOOTHING_STRATEGY: SmoothingStrategy = "one_euro"
155
166
 
156
167
  SMOOTHING_FACTOR: float = 0.6
157
168
 
169
+ PROFILE: str = "smooth"
170
+ """Perfil inicial do painel: 'smooth', 'precise', 'responsive' ou
171
+ 'stable'. Define suavizacao, sensibilidade, pinca, aim assist, sticky e a
172
+ trava no clique (ver core/runtime_settings.PROFILES)."""
173
+
158
174
  ONE_EURO_FREQ: float = 60.0
175
+ # min_cutoff e beta efetivos vem do PERFIL (painel, slider Suavizacao) —
176
+ # os dois abaixo ficam como referencia do perfil 'recommended'.
177
+ # beta esta na escala das coordenadas normalizadas (0-1): valores ~0.02
178
+ # deixavam o filtro sem adaptacao e o cursor arrastando atras da mao.
159
179
  ONE_EURO_MIN_CUTOFF: float = 1.2
160
- ONE_EURO_BETA: float = 0.020
180
+ ONE_EURO_BETA: float = 5.7
161
181
  ONE_EURO_D_CUTOFF: float = 1.0
162
182
  DEAD_ZONE_PIXELS: int = 1
163
183
 
@@ -192,9 +212,22 @@ vem do debounce de 2 frames + threshold.
192
212
 
193
213
  PINCH_VELOCITY_THRESHOLD: float = 0.008
194
214
 
215
+ PINCH_RELEASE_RATIO: float = 1.25
216
+ """Histerese da pinca (Schmitt trigger): pra SOLTAR a pinca, polegar e
217
+ indicador precisam abrir ate threshold * 1.25 (entrar continua em
218
+ threshold). Vale tambem pro clique direito.
219
+
220
+ Com limiar unico, o ruido do tracker com os dedos perto da borda fazia a
221
+ pinca "piscar": clique extra no encostar seguinte ou drag caindo sozinho.
222
+ Mesma tecnica dos detectores de pinca da Ultraleap (distancias separadas
223
+ de ativar/desativar). 1.0 = comportamento antigo (limiar unico).
224
+ Suba (1.35) se ainda notar clique duplo acidental; desca (1.15) se soltar
225
+ o drag ficar dificil.
226
+ """
227
+
195
228
  PINCH_MIN_HOLD_SECONDS: float = 0.015
196
- """Tempo minimo da pinca pra contar como click. 15ms permite cliques
197
- rapidos e fluidos sem disparar em encontros acidentais."""
229
+ """Sem efeito no modelo press-to-click (o clique sai no encostar).
230
+ Mantido so pra nao quebrar configs existentes."""
198
231
 
199
232
  # ---------------------------------------------------------------------
200
233
  # CLIQUE DIREITO — pinca polegar+medio
@@ -416,6 +449,13 @@ alvos pequenos errarem.
416
449
  0.12s cobre o intervalo entre a deteccao do raw shape PINCH e a
417
450
  estabilizacao da pose. Aumente pra 0.15-0.20s se ainda notar deriva.
418
451
  Diminua pra 0.08s se sentir cursor "preso" apos clicks consecutivos.
452
+
453
+ Em runtime o valor vem do PERFIL (slider "Trava no clique" do painel);
454
+ este e' o valor de referencia do perfil 'smooth'/'recommended'.
455
+
456
+ Ao destravar, o cursor NAO salta pra onde a mao foi durante a trava: a
457
+ diferenca e' absorvida aos poucos conforme a mao se move (ver
458
+ CursorController._reconcile_offset). Vale tambem pro fim do drag.
419
459
  """
420
460
 
421
461
  DRAG_PRECISION_FACTOR: float = 0.55
@@ -511,25 +551,42 @@ parece magica; em uso prolongado pode distrair.
511
551
  HOLOGRAM_BACKEND: str = "auto"
512
552
  """Backend de renderizacao do holograma.
513
553
 
514
- - "auto" : tenta GL primeiro; cai pra QPainter se driver falhar
515
- - "gl" : forca ModernGL (mesh 3D + GLSL shader)
516
- - "qpainter" : forca pipeline 2D vetorial (compatibilidade/debug)
554
+ - "auto" : OpenGL 3D se houver OpenGL 3.3 (testado na inicializacao);
555
+ senao cai pro renderer 2D QPainter no mesmo estilo
556
+ - "gl" : forca OpenGL (mesh 3D + shader)
557
+ - "qpainter" : forca o renderer 2D (compatibilidade/debug)
558
+
559
+ O OpenGL e' o do proprio Qt (PySide6) — nao depende de moderngl/
560
+ glcontext, cuja DLL o Smart App Control do Windows pode bloquear.
561
+ Em ambientes sem GPU/driver (RDP, VM sem aceleracao) o "auto" ja cai
562
+ pro QPainter sozinho.
563
+ """
564
+
565
+ HOLOGRAM_STYLE: str = "quest"
566
+ """Aparencia da mao.
567
+
568
+ - "quest" : mao 3D estilo headset VR — azul-acinzentado claro, luz
569
+ suave, borda luminosa, antebraco em fade, pontas brilham
570
+ na pinca (default)
571
+ - "graphite" : mesma mao, grafite translucido com contorno claro (fundos
572
+ claros)
573
+ - "neon" : visual antigo (silhueta neon + particulas, so 2D)
517
574
 
518
- ModernGL renderiza a mao como mesh 3D real (palm ellipsoide + finger
519
- capsules) com shader holografico (fresnel rim, depth fade, cyan
520
- volumetric). Requer driver OpenGL 3.3+. Em ambientes sem GPU/driver
521
- (RDP, VM sem aceleracao), usar "qpainter".
575
+ Nos estilos 3D a mao segue a pose real: aspecto correto da camera,
576
+ profundidade de cada articulacao e escala estavel pela palma (fechar a
577
+ mao nao a "infla"). Ver core/hand_pose.py.
522
578
  """
523
579
 
524
580
  HOLOGRAM_TOGGLE_KEY: str = "h"
525
581
  """Tecla pra alternar o holograma em runtime."""
526
582
 
527
- HOLOGRAM_OPACITY: float = 0.70
528
- """Opacidade base. Camadas (palm/finger outline, particles, tips) usam
529
- fracoes desse valor."""
583
+ HOLOGRAM_OPACITY: float = 0.90
584
+ """Opacidade da mao. No estilo "quest" o corpo fica ~72% opaco (borda
585
+ mais forte); no "neon" as camadas usam fracoes deste valor."""
530
586
 
531
587
  HOLOGRAM_HAND_SIZE_PX: int = 180
532
- """Tamanho do bounding box da mao em pixels."""
588
+ """Tamanho da mao na tela (px): comprimento da mao aberta, do pulso a
589
+ ponta do dedo medio. Nao muda com o gesto."""
533
590
 
534
591
  HOLOGRAM_FPS: int = 30
535
592
  """Taxa de redesenho. 30 Hz e' suave + economico em CPU."""
@@ -10,5 +10,5 @@ Versao do pacote vive em `__version__` e e' a fonte unica de verdade
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- __version__ = "1.2.0"
13
+ __version__ = "1.3.0"
14
14
  __all__ = ["__version__"]
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
  import logging
25
25
  import threading
26
26
  import time
27
- from typing import Optional
27
+ from typing import Optional, Tuple
28
28
 
29
29
  import cv2
30
30
  import numpy as np
@@ -71,41 +71,56 @@ class ThreadedCamera:
71
71
 
72
72
  self._cap: Optional[cv2.VideoCapture] = None
73
73
  self._frame: Optional[np.ndarray] = None
74
- self._lock = threading.Lock()
74
+ # Numero de sequencia do frame: deixa o consumidor processar cada
75
+ # frame da webcam UMA vez (sem reprocessar duplicatas).
76
+ self._seq: int = 0
77
+ self._cond = threading.Condition()
75
78
  self._stop_event = threading.Event()
76
79
  self._thread: Optional[threading.Thread] = None
77
80
  self._opened = False
81
+ # True quando a webcam parou de entregar frames (desconectada ou
82
+ # tomada por outro app). O thread tenta reabrir sozinho.
83
+ self._stalled = False
78
84
 
79
85
  # -----------------------------------------------------------------
80
86
  # Lifecycle
81
87
  # -----------------------------------------------------------------
82
88
 
83
- def open(self) -> "ThreadedCamera":
84
- """
85
- Abre a webcam e inicia o thread de captura.
86
-
87
- Tenta primeiro com backend nativo do SO; se falhar, usa default.
88
- """
89
+ def _open_capture(self) -> Optional[cv2.VideoCapture]:
90
+ """Abre o dispositivo (DirectShow primeiro, depois default)."""
89
91
  # Tenta backend especifico do SO (mais estavel)
90
92
  cap = cv2.VideoCapture(self.index, cv2.CAP_DSHOW)
91
93
  if not cap.isOpened():
92
94
  logger.debug("CAP_DSHOW falhou, tentando default")
95
+ cap.release()
93
96
  cap = cv2.VideoCapture(self.index)
94
-
95
97
  if not cap.isOpened():
96
- raise CameraOpenError(
97
- f"Nao foi possivel abrir a webcam (index={self.index}). "
98
- "Verifique se ela esta conectada e nao ocupada por outro app."
99
- )
98
+ cap.release()
99
+ return None
100
100
 
101
101
  cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
102
102
  cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
103
103
  cap.set(cv2.CAP_PROP_FPS, self.fps)
104
104
  # Buffer minimo = sempre frame mais recente
105
105
  cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
106
+ return cap
107
+
108
+ def open(self) -> "ThreadedCamera":
109
+ """
110
+ Abre a webcam e inicia o thread de captura.
111
+
112
+ Tenta primeiro com backend nativo do SO; se falhar, usa default.
113
+ """
114
+ cap = self._open_capture()
115
+ if cap is None:
116
+ raise CameraOpenError(
117
+ f"Nao foi possivel abrir a webcam (index={self.index}). "
118
+ "Verifique se ela esta conectada e nao ocupada por outro app."
119
+ )
106
120
 
107
121
  self._cap = cap
108
122
  self._opened = True
123
+ self._stalled = False
109
124
 
110
125
  # Aquece a webcam (descarta primeiros frames pretos)
111
126
  time.sleep(self.warmup_seconds)
@@ -129,6 +144,8 @@ class ThreadedCamera:
129
144
  return
130
145
 
131
146
  self._stop_event.set()
147
+ with self._cond:
148
+ self._cond.notify_all()
132
149
  if self._thread is not None:
133
150
  self._thread.join(timeout=2.0)
134
151
  self._thread = None
@@ -150,31 +167,70 @@ class ThreadedCamera:
150
167
  # Loop interno
151
168
  # -----------------------------------------------------------------
152
169
 
170
+ _MAX_FAILURES: int = 30
171
+ _REOPEN_INTERVAL_S: float = 1.0
172
+
153
173
  def _capture_loop(self) -> None:
154
- """Roda em thread separada lendo frames sem parar."""
174
+ """Roda em thread separada lendo frames sem parar.
175
+
176
+ Se a webcam parar de responder (cabo USB solto, outro app pegou o
177
+ dispositivo), marca `stalled`, descarta o ultimo frame (pra ninguem
178
+ processar imagem congelada como se fosse ao vivo) e tenta reabrir
179
+ a cada segundo ate voltar ou ate close().
180
+ """
155
181
  consecutive_failures = 0
156
- max_failures = 30
157
182
 
158
183
  while not self._stop_event.is_set():
159
- if self._cap is None:
160
- break
184
+ cap = self._cap or self._try_reopen()
185
+ if cap is None:
186
+ continue
161
187
 
162
- ok, frame = self._cap.read()
188
+ ok, frame = cap.read()
163
189
  if not ok or frame is None:
164
190
  consecutive_failures += 1
165
- if consecutive_failures >= max_failures:
191
+ if consecutive_failures >= self._MAX_FAILURES:
166
192
  logger.error(
167
- "Webcam falhou %d frames consecutivos, parando thread",
168
- max_failures,
193
+ "Webcam falhou %d frames consecutivos — tentando "
194
+ "reconectar", self._MAX_FAILURES,
169
195
  )
170
- break
196
+ self._mark_stalled()
197
+ consecutive_failures = 0
198
+ continue
171
199
  time.sleep(0.01)
172
200
  continue
173
201
 
174
202
  consecutive_failures = 0
175
-
176
- with self._lock:
203
+ with self._cond:
177
204
  self._frame = frame
205
+ self._seq += 1
206
+ self._cond.notify_all()
207
+
208
+ def _mark_stalled(self) -> None:
209
+ with self._cond:
210
+ self._stalled = True
211
+ self._frame = None
212
+ self._cond.notify_all()
213
+ if self._cap is not None:
214
+ self._cap.release()
215
+ self._cap = None
216
+
217
+ def _try_reopen(self) -> Optional[cv2.VideoCapture]:
218
+ """Uma tentativa de reabrir. None = esperou o intervalo e falhou."""
219
+ if self._stop_event.wait(self._REOPEN_INTERVAL_S):
220
+ return None
221
+ cap = self._open_capture()
222
+ if cap is None:
223
+ return None
224
+ if self._stop_event.is_set():
225
+ # close() chegou durante a abertura (DirectShow pode levar
226
+ # segundos): nao deixar o dispositivo aberto sem dono.
227
+ cap.release()
228
+ return None
229
+ self._cap = cap
230
+ with self._cond:
231
+ self._stalled = False
232
+ logger.info("Webcam %d reconectada", self.index)
233
+ return cap
178
234
 
179
235
  # -----------------------------------------------------------------
180
236
  # API publica
@@ -190,7 +246,7 @@ class ThreadedCamera:
190
246
  Returns:
191
247
  Frame BGR (numpy array) ou None se nada disponivel ainda.
192
248
  """
193
- with self._lock:
249
+ with self._cond:
194
250
  if self._frame is None:
195
251
  return None
196
252
  frame = self._frame.copy()
@@ -199,6 +255,47 @@ class ThreadedCamera:
199
255
  frame = cv2.flip(frame, 1)
200
256
  return frame
201
257
 
258
+ def read_new(
259
+ self,
260
+ last_seq: int,
261
+ timeout: float,
262
+ mirror: bool = True,
263
+ ) -> Tuple[Optional[np.ndarray], int]:
264
+ """
265
+ Espera ate `timeout` por um frame MAIS NOVO que `last_seq`.
266
+
267
+ O loop principal costuma rodar mais rapido que a webcam (30 fps
268
+ e' o comum). Com read() ele reprocessava o mesmo frame varias
269
+ vezes: inferencia desperdicada e, pior, frames repetidos com
270
+ velocidade zero distorciam histerese, sticky e follow-through.
271
+
272
+ Returns:
273
+ (frame, seq) — frame None se nada novo chegou no prazo; nesse
274
+ caso seq volta igual a `last_seq`.
275
+ """
276
+ with self._cond:
277
+ if self._seq == last_seq or self._frame is None:
278
+ self._cond.wait_for(
279
+ lambda: (
280
+ (self._seq != last_seq and self._frame is not None)
281
+ or self._stop_event.is_set()
282
+ ),
283
+ timeout=timeout,
284
+ )
285
+ if self._seq == last_seq or self._frame is None:
286
+ return None, last_seq
287
+ frame = self._frame.copy()
288
+ seq = self._seq
289
+
290
+ if mirror:
291
+ frame = cv2.flip(frame, 1)
292
+ return frame, seq
293
+
294
+ @property
295
+ def is_stalled(self) -> bool:
296
+ """Webcam parou de entregar frames e esta tentando reconectar."""
297
+ return self._stalled
298
+
202
299
  @property
203
300
  def is_opened(self) -> bool:
204
301
  """Camera aberta e thread ativa."""