Skip to content
This repository was archived by the owner on Feb 1, 2026. It is now read-only.

Commit a469746

Browse files
Eveclaude
andcommitted
fix: TalkingHead Ctrl+drag not working on Linux
- Add pynput for global key state detection on Linux - Qt modifiers only work when app has focus, but overlay is click-through - pynput listener runs in background thread tracking Ctrl/Alt key state - Also fix Qt plugin path conflict with opencv-python Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ff34414 commit a469746

4 files changed

Lines changed: 114 additions & 7 deletions

File tree

apps/dashboard/TalkinHead/ivy_overlay.py

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,58 @@
1616
import sys
1717
import os
1818
import json
19-
import cv2
20-
import numpy as np
2119

20+
# IMPORTANT: Import PyQt5 BEFORE cv2 to avoid Qt plugin conflicts on Linux
21+
# cv2 bundles its own Qt which can override the system Qt plugin path
2222
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
2323
from PyQt5.QtCore import Qt, QTimer, QPoint, pyqtSignal
2424
from PyQt5.QtGui import QImage, QPixmap, QCursor, QFont
2525

26+
# Now safe to import cv2 (PyQt5 already claimed the Qt plugins)
27+
import cv2
28+
import numpy as np
29+
2630
# Pygame for reliable audio playback
2731
import pygame
2832
pygame.mixer.init()
2933

30-
# Win32 for global key state detection (Windows)
34+
# Platform-specific key state detection
3135
import ctypes
32-
user32 = ctypes.windll.user32
3336
VK_CONTROL = 0x11
3437
VK_MENU = 0x12 # Alt key
3538
VK_Q = 0x51 # Q key
3639

40+
# Windows uses user32.dll, Linux/macOS use pynput for global key detection
41+
if sys.platform == "win32":
42+
user32 = ctypes.windll.user32
43+
pynput_keyboard = None
44+
else:
45+
user32 = None
46+
# Linux/macOS: use pynput for global key state detection
47+
# (Qt modifiers only work when app has focus, but our window is click-through)
48+
try:
49+
from pynput import keyboard as pynput_keyboard
50+
51+
# Global key state tracking
52+
_keys_pressed = set()
53+
54+
def _on_press(key):
55+
_keys_pressed.add(key)
56+
57+
def _on_release(key):
58+
_keys_pressed.discard(key)
59+
60+
# Start global keyboard listener in background thread
61+
_keyboard_listener = pynput_keyboard.Listener(on_press=_on_press, on_release=_on_release)
62+
_keyboard_listener.daemon = True
63+
_keyboard_listener.start()
64+
print("pynput keyboard listener started for global key detection")
65+
except ImportError:
66+
print("WARNING: pynput not installed. Ctrl+drag may not work on Linux.")
67+
print("Install with: pip install pynput")
68+
pynput_keyboard = None
69+
_keys_pressed = set()
70+
3771
# Script directory for relative paths
3872
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
3973
CONFIG_FILE = os.path.join(SCRIPT_DIR, "config.json")
@@ -58,6 +92,7 @@ def __init__(self, parent=None):
5892

5993
# State tracking
6094
self.running = True
95+
self._q_pressed = False # For Linux/macOS Q key detection
6196
self.idle_frame_idx = 0
6297
self.phrase_frame_idx = 0
6398
self.is_playing_phrase = False
@@ -569,9 +604,34 @@ def _update_tooltip_position(self):
569604

570605
def _check_modifier_keys(self):
571606
"""Check for Ctrl (drag), Alt (resize), Ctrl+Q (quit), and mouse hover."""
572-
ctrl_held = (user32.GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0
573-
alt_held = (user32.GetAsyncKeyState(VK_MENU) & 0x8000) != 0
574-
q_held = (user32.GetAsyncKeyState(VK_Q) & 0x8000) != 0
607+
if user32:
608+
# Windows: use GetAsyncKeyState for global key state
609+
ctrl_held = (user32.GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0
610+
alt_held = (user32.GetAsyncKeyState(VK_MENU) & 0x8000) != 0
611+
q_held = (user32.GetAsyncKeyState(VK_Q) & 0x8000) != 0
612+
elif pynput_keyboard:
613+
# Linux/macOS: use pynput for global key state detection
614+
# Check if Ctrl, Alt, or Q keys are in the pressed set
615+
ctrl_held = any(
616+
key in _keys_pressed
617+
for key in [pynput_keyboard.Key.ctrl, pynput_keyboard.Key.ctrl_l, pynput_keyboard.Key.ctrl_r]
618+
)
619+
alt_held = any(
620+
key in _keys_pressed
621+
for key in [pynput_keyboard.Key.alt, pynput_keyboard.Key.alt_l, pynput_keyboard.Key.alt_r, pynput_keyboard.Key.alt_gr]
622+
)
623+
# Check for Q key (could be KeyCode or character)
624+
q_held = any(
625+
(hasattr(key, 'char') and key.char == 'q') or
626+
(hasattr(key, 'vk') and key.vk == 81) # 81 = Q
627+
for key in _keys_pressed
628+
)
629+
else:
630+
# Fallback: use Qt keyboard modifiers (only works when app has focus)
631+
modifiers = QApplication.keyboardModifiers()
632+
ctrl_held = bool(modifiers & Qt.ControlModifier)
633+
alt_held = bool(modifiers & Qt.AltModifier)
634+
q_held = getattr(self, '_q_pressed', False)
575635

576636
# Check mouse hover for tooltip
577637
mouse_pos = QCursor.pos()
@@ -691,6 +751,18 @@ def mouseReleaseEvent(self, event):
691751
self.setCursor(Qt.OpenHandCursor)
692752
self.save_config()
693753

754+
def keyPressEvent(self, event):
755+
"""Track key presses for Linux/macOS (Q key detection)."""
756+
if event.key() == Qt.Key_Q:
757+
self._q_pressed = True
758+
super().keyPressEvent(event)
759+
760+
def keyReleaseEvent(self, event):
761+
"""Track key releases for Linux/macOS."""
762+
if event.key() == Qt.Key_Q:
763+
self._q_pressed = False
764+
super().keyReleaseEvent(event)
765+
694766
def save_config(self):
695767
"""Save position and size to config.json."""
696768
try:

apps/dashboard/TalkinHead/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414

1515
import os
1616
import sys
17+
18+
# Fix Qt plugin path conflict with opencv-python on Linux
19+
# Only needed for non-headless opencv which bundles Qt
20+
# When using venv with opencv-python-headless, skip this (no Qt conflict)
21+
if sys.platform != "win32" and sys.prefix == sys.base_prefix:
22+
# Only override for system Python (not venv) - system cv2 may have Qt conflict
23+
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = "/usr/lib/x86_64-linux-gnu/qt5/plugins"
24+
os.environ.pop("QT_PLUGIN_PATH", None)
25+
1726
import signal
1827
import atexit
1928
from pathlib import Path
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# TalkinHead dependencies
2+
# Note: numpy<2 required due to OpenCV compatibility on older systems
3+
numpy<2
4+
opencv-python-headless
5+
pygame
6+
PyQt5
7+
pynput # For global key state detection on Linux
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
# TalkinHead Launcher for Linux
3+
# Uses venv to avoid numpy compatibility issues with system packages
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
VENV_DIR="$SCRIPT_DIR/.venv"
7+
8+
cd "$SCRIPT_DIR"
9+
10+
# Create venv if it doesn't exist
11+
if [ ! -d "$VENV_DIR" ]; then
12+
echo "Creating virtual environment..."
13+
python3 -m venv "$VENV_DIR"
14+
"$VENV_DIR/bin/pip" install --upgrade pip
15+
"$VENV_DIR/bin/pip" install "numpy<2" opencv-python-headless pygame PyQt5
16+
fi
17+
18+
# Run with venv python
19+
exec "$VENV_DIR/bin/python" main.py "$@"

0 commit comments

Comments
 (0)