diff --git a/installer/__main__.py b/installer/__main__.py index 7baa579..d33fd85 100644 --- a/installer/__main__.py +++ b/installer/__main__.py @@ -5,6 +5,12 @@ from typing import List, Optional from .const import LOCAL_DIR, PROGRAM_DIR, SatelliteType, Settings from .drivers import install_drivers from .microphone import configure_microphone +from .packages import ( + can_import, + install_packages, + install_packages_nogui, + packages_installed, +) from .satellite import configure_satellite from .services import generate_services, install_services, stop_services from .speakers import configure_speakers @@ -22,6 +28,9 @@ def main() -> None: settings = Settings.load() + if not packages_installed("whiptail"): + install_packages_nogui("whiptail") + choice: Optional[str] = None while True: choice = main_menu(choice) @@ -85,6 +94,19 @@ def apply_settings(settings: Settings) -> None: msgbox("Please set wake word system") return + password: Optional[str] = None + if not packages_installed("python3-pip", "python3-venv"): + password = passwordbox("sudo password:") + if not password: + return + + success = install_packages( + "Installing Python packages...", password, "python3-pip", "python3-venv" + ) + if not success: + error("installing pip/venv for Python") + return + # Satellite venv venv_dir = PROGRAM_DIR / ".venv" if not venv_dir.exists(): @@ -96,7 +118,9 @@ def apply_settings(settings: Settings) -> None: return # silero (vad) - if settings.satellite.type == SatelliteType.VAD: + if (settings.satellite.type == SatelliteType.VAD) and ( + not can_import("pysilero_vad") + ): result = run_with_gauge( "Installing vad...", [pip_install("-r", str(PROGRAM_DIR / "requirements_vad.txt"))], @@ -106,7 +130,9 @@ def apply_settings(settings: Settings) -> None: return # webrtc (audio enhancements) - if (settings.mic.noise_suppression > 0) or (settings.mic.auto_gain > 0): + if ((settings.mic.noise_suppression > 0) or (settings.mic.auto_gain > 0)) and ( + not can_import("webrtc_noise_gain") + ): result = run_with_gauge( "Installing audio enhancements...", [ @@ -119,10 +145,13 @@ def apply_settings(settings: Settings) -> None: error("installing audio enhancements") return - if settings.satellite.event_service_command and ( - ("2mic" in settings.satellite.event_service_command) - or ("4mic" in settings.satellite.event_service_command) - ): + if ( + settings.satellite.event_service_command + and ( + ("2mic" in settings.satellite.event_service_command) + or ("4mic" in settings.satellite.event_service_command) + ) + ) and (not can_import("gpiozero", "spidev")): result = run_with_gauge( "Installing event requirements...", [pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))], @@ -133,9 +162,10 @@ def apply_settings(settings: Settings) -> None: generate_services(settings) - password = passwordbox("sudo password:") - if not password: - return + if password is None: + password = passwordbox("sudo password:") + if not password: + return stop_services(password) install_services(settings, password) diff --git a/installer/packages.py b/installer/packages.py new file mode 100644 index 0000000..02252ec --- /dev/null +++ b/installer/packages.py @@ -0,0 +1,74 @@ +"""Install system packages.""" +import subprocess + +from .const import PROGRAM_DIR +from .whiptail import run_with_gauge + + +def packages_installed(*packages) -> bool: + for package in packages: + try: + subprocess.check_call(["dpkg", "--status", str(package)]) + except Exception: + return False + + return True + + +def install_packages_nogui(*packages, update: bool = True) -> bool: + assert packages, "No packages" + + commands = [] + if update: + commands.append(["sudo", "apt-get", "update"]) + + commands.append( + ["sudo", "apt-get", "install", "--yes"] + [str(p) for p in packages] + ) + + for command in commands: + subprocess.check_call( + command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + + +def install_packages( + text: str, sudo_password: str, *packages, update: bool = True +) -> bool: + assert packages, "No packages" + + commands = [] + if update: + commands.append(["sudo", "-S", "apt-get", "update"]) + + commands.append( + ["sudo", "-S", "apt-get", "install", "--yes"] + [str(p) for p in packages] + ) + + return run_with_gauge(text, commands, sudo_password=sudo_password) + + +def can_import(*names) -> bool: + assert names, "No names" + + venv_dir = PROGRAM_DIR / ".venv" + if not venv_dir.exists(): + return False + + try: + import venv + except ImportError: + return False + + context = venv.EnvBuilder().ensure_directories(PROGRAM_DIR / ".venv") + + try: + subprocess.check_call( + [context.env_exe, "-c", "; ".join(f"import {name}" for name in names)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + return False + + return True diff --git a/installer/wake_word.py b/installer/wake_word.py index eb91164..72c2c03 100644 --- a/installer/wake_word.py +++ b/installer/wake_word.py @@ -6,7 +6,17 @@ from pathlib import Path from typing import Dict, Optional from .const import LOCAL_DIR, SatelliteType, Settings, WakeWordSystem -from .whiptail import error, inputbox, menu, msgbox, radiolist, run_with_gauge, yesno +from .packages import install_packages, packages_installed +from .whiptail import ( + error, + inputbox, + menu, + msgbox, + passwordbox, + radiolist, + run_with_gauge, + yesno, +) def configure_wake_word(settings: Settings) -> None: @@ -92,8 +102,7 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N error("installing openWakeWord") return - msgbox("openWakeWord installed successfully") - + msgbox("openWakeWord installed successfully") settings.wake.system = wake_word_system settings.save() return @@ -125,9 +134,9 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N pass error("installing porcupine1") - else: - msgbox("porcupine1 installed successfully") + return + msgbox("porcupine1 installed successfully") settings.wake.system = wake_word_system settings.save() return @@ -138,6 +147,19 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N if not yesno("Install snowboy?"): return + snowboy_packages = ["python3-dev", "swig", "libatlas-base-dev"] + if not packages_installed(*snowboy_packages): + password = passwordbox("sudo password:") + if not password: + return + + success = install_packages( + "Installing system packages...", password, *snowboy_packages + ) + if not success: + error("installing " + ", ".join(snowboy_packages)) + return + success = run_with_gauge( "Installing snowboy", [ @@ -159,9 +181,9 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N pass error("installing snowboy") - else: - msgbox("snowboy installed successfully") + return + msgbox("snowboy installed successfully") settings.wake.system = wake_word_system settings.save() return