diff --git a/.gitignore b/.gitignore index 68cbfd2..e3cd8c6 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,6 @@ temp/ # Local Netlify folder .netlify .claude-workflow-rules.md +# Analysis documents (keep local) +CROSS_DISTRO_ANALYSIS.md +FIX_PLAN.md diff --git a/install.sh b/install.sh index 3046af8..2a421bc 100755 --- a/install.sh +++ b/install.sh @@ -245,6 +245,41 @@ detect_nvidia_gpu() { fi } +# Detect GI_TYPELIB_PATH for cross-distro compatibility +detect_typelib_path() { + # Try pkg-config first (most reliable) + if command -v pkg-config >/dev/null 2>&1; then + local path=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null) + if [ -n "$path" ] && [ -d "$path" ]; then + echo "$path" + return 0 + fi + fi + + # Fallback to common distribution-specific paths + # Order matters: more specific paths first + for path in \ + /usr/lib/x86_64-linux-gnu/girepository-1.0 \ + /usr/lib/aarch64-linux-gnu/girepository-1.0 \ + /usr/lib/arm-linux-gnueabihf/girepository-1.0 \ + /usr/lib/riscv64-linux-gnu/girepository-1.0 \ + /usr/lib/powerpc64le-linux-gnu/girepository-1.0 \ + /usr/lib/s390x-linux-gnu/girepository-1.0 \ + /usr/lib64/girepository-1.0 \ + /usr/lib/girepository-1.0 \ + /usr/local/lib/girepository-1.0 \ + /usr/local/lib64/girepository-1.0; do + if [ -d "$path" ]; then + echo "$path" + return 0 + fi + done + + # Ultimate fallback - will cause issues if wrong, but at least we try + echo "/usr/lib/girepository-1.0" + return 1 +} + # Print section header for interactive mode clear_screen() { if [ -t 1 ] && command -v clear >/dev/null 2>&1 && [ -n "${TERM:-}" ]; then @@ -1069,6 +1104,10 @@ VOSK_INTERACTIVE_CONFIG # Clean up log file if installation was successful rm -rf "$PIP_LOG_DIR" + # Detect GI_TYPELIB_PATH for cross-distro compatibility + GI_TYPELIB_DETECTED=$(detect_typelib_path) + print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED" + # Create wrapper scripts in ~/.local/bin for easy access mkdir -p "$HOME/.local/bin" @@ -1079,7 +1118,7 @@ VOSK_INTERACTIVE_CONFIG #!/bin/bash # Wrapper script for Vocalinux that sets required environment variables # and applies the 'input' group for keyboard shortcuts on Wayland -export GI_TYPELIB_PATH=/usr/lib/girepository-1.0 +export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED # Check if user is in input group but current session doesn't have it if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then @@ -1097,7 +1136,7 @@ WRAPPER_EOF #!/bin/bash # Wrapper script for Vocalinux GUI that sets required environment variables # and applies the 'input' group for keyboard shortcuts on Wayland -export GI_TYPELIB_PATH=/usr/lib/girepository-1.0 +export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED # Check if user is in input group but current session doesn't have it if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then diff --git a/src/vocalinux/speech_recognition/recognition_manager.py b/src/vocalinux/speech_recognition/recognition_manager.py index c62a54c..dfe29fb 100644 --- a/src/vocalinux/speech_recognition/recognition_manager.py +++ b/src/vocalinux/speech_recognition/recognition_manager.py @@ -23,24 +23,33 @@ from .command_processor import CommandProcessor def _setup_alsa_error_handler(): """Set up an error handler to suppress ALSA warnings.""" try: - asound = ctypes.CDLL("libasound.so.2") - # Define error handler type - ERROR_HANDLER_FUNC = ctypes.CFUNCTYPE( - None, - ctypes.c_char_p, - ctypes.c_int, - ctypes.c_char_p, - ctypes.c_int, - ctypes.c_char_p, - ) + # Try multiple library name variations for cross-distro compatibility + # Different distributions may use different soname or library naming + for lib_name in ["libasound.so.2", "libasound.so", "libasound.so.0", "asound"]: + try: + asound = ctypes.CDLL(lib_name) + # Define error handler type + ERROR_HANDLER_FUNC = ctypes.CFUNCTYPE( + None, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ) - # Create a no-op error handler - def _error_handler(filename, line, function, err, fmt): - pass + # Create a no-op error handler + def _error_handler(filename, line, function, err, fmt): + pass - _alsa_error_handler = ERROR_HANDLER_FUNC(_error_handler) - asound.snd_lib_error_set_handler(_alsa_error_handler) - return _alsa_error_handler # Keep reference to prevent GC + _alsa_error_handler = ERROR_HANDLER_FUNC(_error_handler) + asound.snd_lib_error_set_handler(_alsa_error_handler) + # Note: Can't use logger here as it's not defined yet + return _alsa_error_handler # Keep reference to prevent GC + except OSError: + continue + # If all library names fail, return None + return None except (OSError, AttributeError): # ALSA not available or different platform return None @@ -218,11 +227,64 @@ def _show_notification(title: str, message: str, icon: str = "dialog-warning"): # Define constants MODELS_DIR = os.path.expanduser("~/.local/share/vocalinux/models") -# Alternative locations for pre-installed models -SYSTEM_MODELS_DIRS = [ - "/usr/local/share/vocalinux/models", - "/usr/share/vocalinux/models", -] + + +def _get_system_model_paths() -> list: + """ + Get system-wide model paths based on distro standards. + + This function dynamically determines where system-wide models might be + installed based on XDG standards and distribution-specific conventions. + + Returns: + List of paths to check for pre-installed models + """ + paths = [] + + # XDG standard paths from XDG_DATA_DIRS + xdg_data = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share") + for base in xdg_data.split(":"): + if base: # Skip empty strings + paths.append(os.path.join(base, "vocalinux", "models")) + + # Distribution-specific paths + # Try to detect the distribution from /etc/os-release + try: + with open("/etc/os-release", "r") as f: + os_release = f.read().lower() + + # Fedora/RHEL/CentOS/Rocky/AlmaLinux use /usr/lib64 + if any( + id in os_release + for id in ["fedora", "rhel", "centos", "rocky", "almalinux", "red hat"] + ): + paths.append("/usr/lib64/vocalinux/models") + paths.append("/usr/lib/vocalinux/models") + + # Arch Linux doesn't use /usr/local + if "arch" in os_release: + paths.remove("/usr/local/share/vocalinux/models") + + except (IOError, OSError, FileNotFoundError): + pass # File doesn't exist on all systems + + # Add common fallback paths that might be used + additional_paths = [ + "/usr/local/lib/vocalinux/models", + "/usr/lib/vocalinux/models", + "/usr/lib64/vocalinux/models", + "/opt/vocalinux/models", # Some distros use /opt + ] + + for path in additional_paths: + if path not in paths: + paths.append(path) + + return paths + + +# Alternative locations for pre-installed models (now dynamic) +SYSTEM_MODELS_DIRS = _get_system_model_paths() class SpeechRecognitionManager: diff --git a/src/vocalinux/utils/resource_manager.py b/src/vocalinux/utils/resource_manager.py index cb559bd..36e3472 100644 --- a/src/vocalinux/utils/resource_manager.py +++ b/src/vocalinux/utils/resource_manager.py @@ -47,7 +47,7 @@ class ResourceManager: # Get the directory where this module is located module_dir = Path(__file__).parent.absolute() - # Try several methods to find the resources directory + # Build list of candidate paths in priority order candidates = [ # For direct repository execution (go up from src/vocalinux/utils to root) module_dir.parent.parent.parent / "resources", @@ -55,11 +55,31 @@ class ResourceManager: Path(sys.prefix) / "share" / "vocalinux" / "resources", # For development in virtual environment Path(sys.prefix).parent / "resources", - # Additional fallbacks + ] + + # Add XDG data directory paths + xdg_data = os.environ.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share") + for base in xdg_data.split(":"): + if base: # Skip empty strings + candidates.append(Path(base) / "vocalinux" / "resources") + + # Add distribution-specific standard paths + additional_paths = [ Path("/usr/local/share/vocalinux/resources"), Path("/usr/share/vocalinux/resources"), + # Some distros use lib64 or lib for package data + Path("/usr/local/lib/vocalinux/resources"), + Path("/usr/lib/vocalinux/resources"), + Path("/usr/local/lib64/vocalinux/resources"), + Path("/usr/lib64/vocalinux/resources"), + # Flatpak specific path + Path("/app/share/vocalinux/resources"), ] + for path in additional_paths: + if path not in candidates: + candidates.append(path) + # Log all candidates for debugging for candidate in candidates: logger.debug(