First working version

This commit is contained in:
Michael Hansen
2024-01-14 22:19:01 -06:00
parent 316427948e
commit 8063f5b773
12 changed files with 740 additions and 439 deletions
+17 -212
View File
@@ -1,34 +1,15 @@
"""Command-line installer.""" """Command-line installer."""
import array
import math
import logging import logging
import json from typing import List, Optional
import shlex
import subprocess
import shutil
import sys
import time
from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor, Future
from enum import Enum
from dataclasses import dataclass, fields, asdict, field
from pathlib import Path
from typing import Any, Dict, List, Tuple, Optional, Union
from .const import ( from .const import LOCAL_DIR, PROGRAM_DIR, SatelliteType, Settings
Settings,
PROGRAM_DIR,
LOCAL_DIR,
SatelliteType,
SERVICES_DIR,
WakeWordSystem,
)
from .whiptail import run_with_gauge, menu, ItemType, msgbox, error, passwordbox
from .satellite import configure_satellite
from .microphone import configure_microphone
from .speakers import configure_speakers
from .drivers import install_drivers from .drivers import install_drivers
from .microphone import configure_microphone
from .satellite import configure_satellite
from .services import generate_services, install_services, stop_services
from .speakers import configure_speakers
from .wake_word import configure_wake_word from .wake_word import configure_wake_word
from .whiptail import ItemType, error, menu, msgbox, passwordbox, run_with_gauge
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@@ -41,12 +22,6 @@ def main() -> None:
settings = Settings.load() settings = Settings.load()
venv_dir = PROGRAM_DIR / ".venv"
if not venv_dir.exists():
run_with_gauge(
"Installing satellite base...", [[str(PROGRAM_DIR / "script" / "setup")]]
)
choice: Optional[str] = None choice: Optional[str] = None
while True: while True:
choice = main_menu(choice) choice = main_menu(choice)
@@ -99,151 +74,17 @@ def pip_install(*args) -> List[str]:
] + list(args) ] + list(args)
def stop_services(password: str) -> None:
stop_commands = []
for service in ("satellite", "openwakeword", "porcupine1", "snowboy"):
service_filename = f"wyoming-{service}.service"
service_path = Path("/etc/systemd/system") / service_filename
if not service_path.exists():
continue
stop_commands.append(["sudo", "-S", "systemctl", "stop", service_filename])
stop_commands.append(["sudo", "-S", "systemctl", "disable", service_filename])
run_with_gauge("Stopping Services...", stop_commands, sudo_password=password)
def generate_services(settings: Settings) -> None:
SERVICES_DIR.mkdir(parents=True, exist_ok=True)
user = subprocess.check_output(["id", "--name", "-u"], text=True).strip()
satellite_command: List[str] = [
str(PROGRAM_DIR / "script" / "run"),
"--name",
settings.satellite_name,
"--uri",
"tcp://0.0.0.0:10700",
"--mic-command",
f"arecord -D {settings.microphone_device} -q -r 16000 -c 1 -f S16_LE -t raw",
]
satellite_requires: List[str] = []
if settings.sound_device is not None:
# Audio output
satellite_command.extend(
[
"--snd-command",
f"aplay -D {settings.sound_device} -q -r 22050 -c 1 -f S16_LE -t raw",
]
)
for sound_name in settings.feedback_sounds:
# Try local/sounds first
sound_path = LOCAL_DIR / "sounds" / f"{sound_name}.wav"
if not sound_path.exists():
sound_path = PROGRAM_DIR / "sounds" / f"{sound_name}.wav"
satellite_command.extend([f"--{sound_name}-wav", str(sound_path)])
if settings.satellite_type == SatelliteType.VAD:
# Voice activity detect
satellite_command.append("--vad")
elif settings.satellite_type == SatelliteType.WAKE:
# Local wake word detection
assert settings.wake_word_system is not None, "Wake word system not set"
wake_word = settings.wake_word.get(settings.wake_word_system)
assert wake_word, "No wake word set"
wake_word_service = "wyoming-" + str(settings.wake_word_system).lower()
wake_word_dir = LOCAL_DIR / wake_word_service
wake_word_command = [
str(wake_word_dir / "script" / "run"),
"--uri",
"tcp://127.0.0.1:10400",
]
if settings.wake_word_system in (
WakeWordSystem.OPENWAKEWORD,
WakeWordSystem.SNOWBOY,
):
wake_word_command.extend(
[
"--custom-model-dir",
str(
LOCAL_DIR
/ "custom-wake-words"
/ WakeWordSystem(settings.wake_word_system).value
),
]
)
if settings.debug_enabled:
wake_word_command.append("--debug")
wake_word_command_str = shlex.join(wake_word_command)
with open(
SERVICES_DIR / f"{wake_word_service}.service", "w", encoding="utf-8"
) as service_file:
print("[Unit]", file=service_file)
print(f"Description={settings.wake_word_system}", file=service_file)
print("", file=service_file)
print("[Service]", file=service_file)
print("Type=simple", file=service_file)
print(f"User={user}", file=service_file)
print(f"ExecStart={wake_word_command_str}", file=service_file)
print(f"WorkingDirectory={wake_word_dir}", file=service_file)
print("Restart=always", file=service_file)
print("RestartSec=1", file=service_file)
print("", file=service_file)
print("[Install]", file=service_file)
print("WantedBy=default.target", file=service_file)
satellite_command.extend(
[
"--wake-uri",
"tcp://127.0.0.1:10400",
"--wake-word-name",
wake_word,
]
)
satellite_requires.append(f"{wake_word_service}.service")
if settings.debug_enabled:
satellite_command.extend(
["--debug", "--debug-recording-dir", str(LOCAL_DIR / "debug-recording")]
)
satellite_command_str = shlex.join(satellite_command)
with open(
SERVICES_DIR / "wyoming-satellite.service", "w", encoding="utf-8"
) as service_file:
print("[Unit]", file=service_file)
print("Description=Wyoming Satellite", file=service_file)
print("Wants=network-online.target", file=service_file)
print("After=network-online.target", file=service_file)
for requires in satellite_requires:
print(f"Requires={requires}", file=service_file)
print("", file=service_file)
print("[Service]", file=service_file)
print("Type=simple", file=service_file)
print(f"User={user}", file=service_file)
print(f"ExecStart={satellite_command_str}", file=service_file)
print(f"WorkingDirectory={PROGRAM_DIR}", file=service_file)
print("Restart=always", file=service_file)
print("RestartSec=1", file=service_file)
print("", file=service_file)
print("[Install]", file=service_file)
print("WantedBy=default.target", file=service_file)
def apply_settings(settings: Settings) -> None: def apply_settings(settings: Settings) -> None:
if settings.microphone_device is None: if settings.mic.device is None:
msgbox("Please configure microphone") msgbox("Please configure microphone")
return return
if (settings.satellite.type == SatelliteType.WAKE) and (
settings.wake.system is None
):
msgbox("Please set wake word system")
return
# Satellite venv # Satellite venv
venv_dir = PROGRAM_DIR / ".venv" venv_dir = PROGRAM_DIR / ".venv"
if not venv_dir.exists(): if not venv_dir.exists():
@@ -255,7 +96,7 @@ def apply_settings(settings: Settings) -> None:
return return
# silero (vad) # silero (vad)
if settings.satellite_type == SatelliteType.VAD: if settings.satellite.type == SatelliteType.VAD:
result = run_with_gauge( result = run_with_gauge(
"Installing vad...", "Installing vad...",
[pip_install("-r", str(PROGRAM_DIR / "requirements_vad.txt"))], [pip_install("-r", str(PROGRAM_DIR / "requirements_vad.txt"))],
@@ -265,7 +106,7 @@ def apply_settings(settings: Settings) -> None:
return return
# webrtc (audio enhancements) # webrtc (audio enhancements)
if (settings.noise_suppression_level > 0) or (settings.auto_gain > 0): if (settings.mic.noise_suppression > 0) or (settings.mic.auto_gain > 0):
result = run_with_gauge( result = run_with_gauge(
"Installing audio enhancements...", "Installing audio enhancements...",
[ [
@@ -285,43 +126,7 @@ def apply_settings(settings: Settings) -> None:
return return
stop_services(password) stop_services(password)
install_services(settings, password)
# Install and run
installed_services = ["satellite"]
if settings.satellite_type == SatelliteType.WAKE:
assert settings.wake_word_system is not None, "No wake word system"
installed_services.append(
WakeWordSystem(settings.wake_word_system).value.lower()
)
install_commands = []
for service in installed_services:
service_filename = f"wyoming-{service}.service"
install_commands.append(
[
"sudo",
"-S",
"cp",
str(SERVICES_DIR / service_filename),
"/etc/systemd/system/",
]
)
install_commands.append(["sudo", "-S", "systemctl", "daemon-reload"])
# Copy first, then enable and start
for service in installed_services:
install_commands.append(["sudo", "-S", "systemctl", "enable", service_filename])
install_commands.append(["sudo", "-S", "systemctl", "start", "wyoming-satellite.service"])
success = run_with_gauge(
"Installing Services...", install_commands, sudo_password=password
)
if success:
msgbox("Successfully installed services")
else:
error("installing services")
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+60 -32
View File
@@ -1,10 +1,12 @@
"""Constants and dataclasses.""" """Constants and dataclasses."""
import logging
import json import json
import logging
from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from dataclasses import dataclass, fields, asdict, field
from typing import Any, Dict, Optional, List
from pathlib import Path from pathlib import Path
from typing import List, Optional
from .dataclasses_json import DataClassJsonMixin
_DIR = Path(__file__).parent _DIR = Path(__file__).parent
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@@ -29,6 +31,47 @@ class SatelliteType(str, Enum):
WAKE = "wake" WAKE = "wake"
@dataclass
class SatelliteSettings(DataClassJsonMixin):
name: str = "Wyoming Satellite"
type: SatelliteType = SatelliteType.ALWAYS_STREAMING
debug: bool = False
@dataclass
class MicrophoneSettings(DataClassJsonMixin):
device: Optional[str] = None
noise_suppression: int = 0
auto_gain: int = 0
volume_multiplier: float = 1.0
@dataclass
class SpeakerSettings(DataClassJsonMixin):
device: Optional[str] = None
volume_multiplier: float = 1.0
feedback_sounds: List[str] = field(default_factory=list)
@dataclass
class OpenWakeWordSettings(DataClassJsonMixin):
wake_word: str = "ok_nabu"
threshold: float = 0.5
trigger_level: int = 1
@dataclass
class Porcupine1Settings(DataClassJsonMixin):
wake_word: str = "porcupine"
sensitivity: float = 0.5
@dataclass
class SnowboySettings(DataClassJsonMixin):
wake_word: str = "snowboy"
sensitivity: float = 0.5
class WakeWordSystem(str, Enum): class WakeWordSystem(str, Enum):
OPENWAKEWORD = "openWakeWord" OPENWAKEWORD = "openWakeWord"
PORCUPINE1 = "porcupine1" PORCUPINE1 = "porcupine1"
@@ -36,49 +79,34 @@ class WakeWordSystem(str, Enum):
@dataclass @dataclass
class Settings: class WakeWordSettings(DataClassJsonMixin):
microphone_device: Optional[str] = None system: Optional[WakeWordSystem] = None
noise_suppression_level: int = 0 openwakeword: OpenWakeWordSettings = field(default_factory=OpenWakeWordSettings)
auto_gain: int = 0 porcupine1: Porcupine1Settings = field(default_factory=Porcupine1Settings)
mic_volume_multiplier: float = 1.0 snowboy: SnowboySettings = field(default_factory=SnowboySettings)
sound_device: Optional[str] = None
feedback_sounds: List[str] = field(default_factory=list)
wake_word_system: Optional[WakeWordSystem] = None @dataclass
wake_word: Dict[WakeWordSystem, str] = field( class Settings(DataClassJsonMixin):
default_factory=lambda: { satellite: SatelliteSettings = field(default_factory=SatelliteSettings)
WakeWordSystem.OPENWAKEWORD: "ok_nabu", mic: MicrophoneSettings = field(default_factory=MicrophoneSettings)
WakeWordSystem.PORCUPINE1: "porcupine", snd: SpeakerSettings = field(default_factory=SpeakerSettings)
WakeWordSystem.SNOWBOY: "snowboy", wake: WakeWordSettings = field(default_factory=WakeWordSettings)
}
)
satellite_name: str = "Wyoming Satellite"
satellite_type: SatelliteType = SatelliteType.ALWAYS_STREAMING
debug_enabled: bool = False
@staticmethod @staticmethod
def load() -> "Settings": def load() -> "Settings":
kwargs: Dict[str, Any] = {}
if SETTINGS_PATH.exists(): if SETTINGS_PATH.exists():
_LOGGER.debug("Loading settings from %s", SETTINGS_PATH) _LOGGER.debug("Loading settings from %s", SETTINGS_PATH)
with open(SETTINGS_PATH, "r", encoding="utf-8") as settings_file: with open(SETTINGS_PATH, "r", encoding="utf-8") as settings_file:
settings_dict = json.load(settings_file) settings_dict = json.load(settings_file)
return Settings.from_dict(settings_dict)
for settings_field in fields(Settings): return Settings()
value = settings_dict.get(settings_field.name)
if value is not None:
kwargs[settings_field.name] = value
return Settings(**kwargs)
def save(self) -> None: def save(self) -> None:
_LOGGER.debug("Saving settings to %s", SETTINGS_PATH) _LOGGER.debug("Saving settings to %s", SETTINGS_PATH)
settings_dict = asdict(self) settings_dict = self.to_dict()
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(SETTINGS_PATH, "w", encoding="utf-8") as settings_file: with open(SETTINGS_PATH, "w", encoding="utf-8") as settings_file:
+70
View File
@@ -0,0 +1,70 @@
"""Implement a tiny subset of dataclasses_json for config."""
from collections.abc import Mapping, Sequence
from dataclasses import asdict, fields, is_dataclass
from typing import Any, Dict, Type
class DataClassJsonMixin:
"""Adds from_dict to dataclass."""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Any:
"""Parse dataclasses recursively."""
kwargs: Dict[str, Any] = {}
cls_fields = {field.name: field for field in fields(cls)}
for key, value in data.items():
if key not in cls_fields:
# Skip unknown fields
continue
field = cls_fields[key]
if is_dataclass(field.type):
assert issubclass(field.type, DataClassJsonMixin), field.type
kwargs[key] = field.type.from_dict(value)
else:
kwargs[key] = _decode(value, field.type)
# Fill in optional fields with None
for field in cls_fields.values():
if (field.name not in kwargs) and _is_optional(field.type):
kwargs[field.name] = None
return cls(**kwargs)
def to_dict(self) -> Dict[str, Any]:
"""Alias for asdict."""
return asdict(self)
def _decode(value: Any, target_type: Type) -> Any:
"""Decode value using (possibly generic) type."""
if is_dataclass(target_type):
assert issubclass(target_type, DataClassJsonMixin), target_type
return target_type.from_dict(value) if value is not None else None
if hasattr(target_type, "__args__"):
# Optional[T]
if type(None) in target_type.__args__:
optional_type = target_type.__args__[0]
return _decode(value, optional_type)
# List[T]
if isinstance(value, Sequence):
list_type = target_type.__args__[0]
return [_decode(item, list_type) for item in value]
# Dict[str, T]
if isinstance(value, Mapping):
value_type = target_type.__args__[1]
return {
map_key: _decode(map_value, value_type)
for map_key, map_value in value.items()
}
return value
def _is_optional(target_type: Type):
"""True if type is Optional"""
return hasattr(target_type, "__args__") and (type(None) in target_type.__args__)
+2 -2
View File
@@ -2,8 +2,8 @@
import sys import sys
from typing import Optional from typing import Optional
from .const import Settings, PROGRAM_DIR from .const import PROGRAM_DIR, Settings
from .whiptail import msgbox, run_with_gauge, menu, yesno, passwordbox, error from .whiptail import error, menu, msgbox, passwordbox, run_with_gauge, yesno
def install_drivers(settings: Settings) -> None: def install_drivers(settings: Settings) -> None:
+39 -37
View File
@@ -1,13 +1,13 @@
"""Microphone settings.""" """Microphone settings."""
import array import array
import logging
import math import math
import subprocess import subprocess
import logging from concurrent.futures import Future, ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, Future from typing import Dict, List, Optional
from typing import Dict, Optional, List
from .const import Settings, RECORD_SECONDS, RECORD_RMS_MIN from .const import RECORD_RMS_MIN, RECORD_SECONDS, Settings
from .whiptail import gauge, msgbox, radiolist, menu, inputbox from .whiptail import gauge, inputbox, menu, msgbox, radiolist
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@@ -40,29 +40,25 @@ def configure_microphone(settings: Settings) -> None:
if best_device is not None: if best_device is not None:
msgbox(f"Successfully detected microphone: {best_device}") msgbox(f"Successfully detected microphone: {best_device}")
settings.microphone_device = best_device settings.mic.device = best_device
settings.save() settings.save()
else: else:
msgbox("Audio was not detected from any microphone") msgbox("Audio was not detected from any microphone")
elif choice == "list": elif choice == "list":
# arecord -L # arecord -L
microphone_device = radiolist( microphone_device = radiolist(
"Select ALSA Device:", "Select ALSA Device:", get_microphone_devices(), settings.mic.device
get_microphone_devices(),
settings.microphone_device,
) )
if microphone_device: if microphone_device:
settings.microphone_device = microphone_device settings.mic.device = microphone_device
settings.save() settings.save()
elif choice == "manual": elif choice == "manual":
microphone_device = inputbox( microphone_device = inputbox("Enter ALSA Device:", settings.mic.device)
"Enter ALSA Device:", settings.microphone_device
)
if microphone_device: if microphone_device:
settings.microphone_device = microphone_device settings.mic.device = microphone_device
settings.save() settings.save()
elif choice == "enhancements": elif choice == "settings":
configure_audio_enhancements(settings) configure_audio_settings(settings)
else: else:
break break
@@ -74,7 +70,7 @@ def microphone_menu(last_choice: Optional[str]) -> Optional[str]:
("detect", "Autodetect"), ("detect", "Autodetect"),
("list", "Select From List"), ("list", "Select From List"),
("manual", "Enter Manually"), ("manual", "Enter Manually"),
("enhancements", "Audio Enhancements"), ("settings", "Audio Settings"),
], ],
selected_item=last_choice, selected_item=last_choice,
menu_args=["--ok-button", "Select", "--cancel-button", "Back"], menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
@@ -86,7 +82,9 @@ def get_microphone_devices() -> List[str]:
lines = subprocess.check_output(["arecord", "-L"]).decode("utf-8").splitlines() lines = subprocess.check_output(["arecord", "-L"]).decode("utf-8").splitlines()
for line in lines: for line in lines:
line = line.strip() line = line.strip()
if line.startswith("plughw:"):
# default = PulseAudio
if (line == "default") or line.startswith("plughw:"):
devices.append(line) devices.append(line)
return devices return devices
@@ -126,50 +124,54 @@ def _record_proc(device: str) -> float:
return 0 return 0
def configure_audio_enhancements(settings: Settings) -> None: def configure_audio_settings(settings: Settings) -> None:
choice: Optional[str] = None choice: Optional[str] = None
while True: while True:
choice = audio_enhancements_menu(choice) choice = audio_settings_menu(choice)
if choice == "noise": if choice == "noise":
result = radiolist( noise_suppression = radiolist(
"Noise Suppression Level", "Noise Suppression Level",
[(0, "Off"), (1, "Low"), (2, "Medium"), (3, "High"), (4, "Maximum")], [(0, "Off"), (1, "Low"), (2, "Medium"), (3, "High"), (4, "Maximum")],
settings.noise_suppression_level, settings.mic.noise_suppression,
) )
if result is not None: if noise_suppression is not None:
settings.noise_suppression_level = result settings.mic.noise_suppression = noise_suppression
settings.save() settings.save()
elif choice == "gain": elif choice == "gain":
while True: while True:
result = inputbox("Auto Gain (0-31 dbFS)", settings.auto_gain) auto_gain = inputbox("Auto Gain (0-31 dbFS)", settings.mic.auto_gain)
if result is not None: if auto_gain is None:
break
try: try:
auto_gain = int(result) auto_gain_int = int(auto_gain)
except ValueError: except ValueError:
msgbox("Invalid value") msgbox("Invalid value")
continue continue
if 0 <= auto_gain <= 31: if 0 <= auto_gain_int <= 31:
settings.auto_gain = auto_gain settings.mic.auto_gain = auto_gain_int
settings.save() settings.save()
break break
msgbox("Must be 0-31") msgbox("Must be 0-31")
elif choice == "multiplier": elif choice == "multiplier":
while True: while True:
result = inputbox( volume_multiplier = inputbox(
"Volume Multipler (1 = default)", settings.mic_volume_multiplier "Volume Multipler (1 = default)", settings.mic.volume_multiplier
) )
if result is not None: if volume_multiplier is None:
break
try: try:
volume_multiplier = float(result) volume_multiplier_float = float(volume_multiplier)
except ValueError: except ValueError:
msgbox("Invalid value") msgbox("Invalid value")
continue continue
if volume_multiplier > 0: if volume_multiplier_float > 0:
settings.mic_volume_multiplier = volume_multiplier settings.mic.volume_multiplier = volume_multiplier_float
settings.save() settings.save()
break break
@@ -178,9 +180,9 @@ def configure_audio_enhancements(settings: Settings) -> None:
break break
def audio_enhancements_menu(last_choice: Optional[str]) -> Optional[str]: def audio_settings_menu(last_choice: Optional[str]) -> Optional[str]:
return menu( return menu(
"Main > Microphone > Audio Enhancements", "Main > Microphone > Audio Settings",
[ [
("noise", "Noise Suppression"), ("noise", "Noise Suppression"),
("gain", "Auto Gain"), ("gain", "Auto Gain"),
+49 -27
View File
@@ -1,8 +1,8 @@
"""Satellite settings.""" """Satellite settings."""
from typing import Optional from typing import Optional
from .const import Settings, SatelliteType from .const import SatelliteType, Settings
from .whiptail import menu, inputbox, radiolist, run_with_gauge from .whiptail import error, inputbox, menu, passwordbox, radiolist, run_with_gauge
def configure_satellite(settings: Settings) -> None: def configure_satellite(settings: Settings) -> None:
@@ -11,9 +11,51 @@ def configure_satellite(settings: Settings) -> None:
choice = satellite_menu(choice) choice = satellite_menu(choice)
if choice == "name": if choice == "name":
set_satellite_name(settings) name = inputbox("Satellite Name:", settings.satellite.name)
if name:
settings.satellite.name = name
settings.save()
elif choice == "type": elif choice == "type":
set_satellite_type(settings) satellite_type = radiolist(
"Satellite Type:",
[
(SatelliteType.ALWAYS_STREAMING, "Always streaming"),
(SatelliteType.VAD, "Voice activity detection"),
(SatelliteType.WAKE, "Local wake word detection"),
],
settings.satellite.type,
)
if satellite_type is not None:
settings.satellite.type = SatelliteType(satellite_type)
settings.save()
elif choice in ("stop", "start"):
password = passwordbox("sudo password:")
if not password:
continue
command = ["sudo", "-S", "systemctl", choice, "wyoming-satellite.service"]
text = (
"Stopping satellite..." if choice == "stop" else "Starting satellite..."
)
success = run_with_gauge(text, [command], sudo_password=password)
if not success:
error(
"stopping satellite" if choice == "stop" else "starting satellite"
)
elif choice == "debug":
debug = radiolist(
"Debug Mode:",
[
("disabled", "Disabled"),
("enabled", "Enabled"),
],
"enabled" if settings.satellite.debug else "disabled",
)
if debug is not None:
settings.satellite.debug = debug == "enabled"
settings.save()
else: else:
break break
@@ -24,30 +66,10 @@ def satellite_menu(last_choice: Optional[str]) -> Optional[str]:
[ [
("name", "Satellite Name"), ("name", "Satellite Name"),
("type", "Satellite Type"), ("type", "Satellite Type"),
("stop", "Stop Service"),
("start", "Start Service"),
("debug", "Set Debug Mode"),
], ],
selected_item=last_choice, selected_item=last_choice,
menu_args=["--ok-button", "Select", "--cancel-button", "Back"], menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
) )
def set_satellite_name(settings: Settings) -> None:
name = inputbox("Satellite Name:", settings.satellite_name)
if name:
settings.satellite_name = name
settings.save()
def set_satellite_type(settings: Settings) -> None:
satellite_type = radiolist(
"Satellite Type:",
[
(SatelliteType.ALWAYS_STREAMING, "Always streaming"),
(SatelliteType.VAD, "Voice activity detection"),
(SatelliteType.WAKE, "Local wake word detection"),
],
settings.satellite_type,
)
if satellite_type is not None:
settings.satellite_type = SatelliteType(satellite_type)
settings.save()
+231
View File
@@ -0,0 +1,231 @@
"""Systemd service management."""
import shlex
import subprocess
from pathlib import Path
from typing import List
from .const import (
LOCAL_DIR,
PROGRAM_DIR,
SERVICES_DIR,
SatelliteType,
Settings,
WakeWordSystem,
)
from .whiptail import error, msgbox, run_with_gauge
def stop_services(password: str) -> None:
stop_commands = []
for service in ("satellite", "openwakeword", "porcupine1", "snowboy"):
service_filename = f"wyoming-{service}.service"
service_path = Path("/etc/systemd/system") / service_filename
if not service_path.exists():
continue
stop_commands.append(["sudo", "-S", "systemctl", "stop", service_filename])
stop_commands.append(["sudo", "-S", "systemctl", "disable", service_filename])
run_with_gauge("Stopping Services...", stop_commands, sudo_password=password)
def generate_services(settings: Settings) -> None:
SERVICES_DIR.mkdir(parents=True, exist_ok=True)
user_id = subprocess.check_output(["id", "-u"], text=True).strip()
user_name = subprocess.check_output(["id", "--name", "-u"], text=True).strip()
satellite_command: List[str] = [
str(PROGRAM_DIR / "script" / "run"),
"--name",
settings.satellite.name,
"--uri",
"tcp://0.0.0.0:10700",
"--mic-command",
f"arecord -D {settings.mic.device} -q -r 16000 -c 1 -f S16_LE -t raw",
]
satellite_requires: List[str] = []
if settings.snd.device is not None:
# Audio output
satellite_command.extend(
[
"--snd-command",
f"aplay -D {settings.snd.device} -q -r 22050 -c 1 -f S16_LE -t raw",
]
)
for sound_name in settings.snd.feedback_sounds:
# Try local/sounds first
sound_path = LOCAL_DIR / "sounds" / f"{sound_name}.wav"
if not sound_path.exists():
sound_path = PROGRAM_DIR / "sounds" / f"{sound_name}.wav"
satellite_command.extend([f"--{sound_name}-wav", str(sound_path)])
if settings.mic.noise_suppression > 0:
satellite_command.extend(
["--mic-noise-suppression", str(settings.mic.noise_suppression)]
)
if settings.mic.auto_gain > 0:
satellite_command.extend(["--mic-auto-gain", str(settings.mic.auto_gain)])
if settings.mic.volume_multiplier != 1.0:
satellite_command.extend(
["--mic-volume-multiplier", str(settings.mic.volume_multiplier)]
)
if settings.satellite.type == SatelliteType.VAD:
# Voice activity detect
satellite_command.append("--vad")
elif settings.satellite.type == SatelliteType.WAKE:
# Local wake word detection
assert settings.wake.system is not None, "Wake word system not set"
wake_word_service = (
"wyoming-" + WakeWordSystem(settings.wake.system).value.lower()
)
wake_word_dir = LOCAL_DIR / wake_word_service
wake_word_command = [
str(wake_word_dir / "script" / "run"),
"--uri",
"tcp://127.0.0.1:10400",
]
if settings.wake.system == WakeWordSystem.OPENWAKEWORD:
wake_word = settings.wake.openwakeword.wake_word
wake_word_command.extend(
[
"--threshold",
str(settings.wake.openwakeword.threshold),
"--trigger-level",
str(settings.wake.openwakeword.trigger_level),
"--custom-model-dir",
str(LOCAL_DIR / "custom-wake-words" / "openWakeWord"),
]
)
elif settings.wake.system == WakeWordSystem.PORCUPINE1:
wake_word = settings.wake.porcupine1.wake_word
wake_word_command.extend(
["--sensitivity", str(settings.wake.porcupine1.sensitivity)]
)
elif settings.wake.system == WakeWordSystem.SNOWBOY:
wake_word = settings.wake.snowboy.wake_word
wake_word_command.extend(
[
"--sensitivity",
str(settings.wake.snowboy.sensitivity),
"--custom-model-dir",
str(LOCAL_DIR / "custom-wake-words" / "snowboy"),
]
)
else:
raise ValueError(settings.wake.system)
if settings.satellite.debug:
wake_word_command.append("--debug")
wake_word_command_str = shlex.join(wake_word_command)
with open(
SERVICES_DIR / f"{wake_word_service}.service", "w", encoding="utf-8"
) as service_file:
print("[Unit]", file=service_file)
print(
f"Description={WakeWordSystem(settings.wake.system).value}",
file=service_file,
)
print("", file=service_file)
print("[Service]", file=service_file)
print("Type=simple", file=service_file)
print(f"User={user_name}", file=service_file)
print(f"ExecStart={wake_word_command_str}", file=service_file)
print(f"WorkingDirectory={wake_word_dir}", file=service_file)
print("Restart=always", file=service_file)
print("RestartSec=1", file=service_file)
print("", file=service_file)
print("[Install]", file=service_file)
print("WantedBy=default.target", file=service_file)
satellite_command.extend(
[
"--wake-uri",
"tcp://127.0.0.1:10400",
"--wake-word-name",
wake_word,
]
)
satellite_requires.append(f"{wake_word_service}.service")
if settings.satellite.debug:
satellite_command.extend(
["--debug", "--debug-recording-dir", str(LOCAL_DIR / "debug-recording")]
)
satellite_command_str = shlex.join(satellite_command)
with open(
SERVICES_DIR / "wyoming-satellite.service", "w", encoding="utf-8"
) as service_file:
print("[Unit]", file=service_file)
print("Description=Wyoming Satellite", file=service_file)
print("Wants=network-online.target", file=service_file)
print("After=network-online.target", file=service_file)
for requires in satellite_requires:
print(f"Requires={requires}", file=service_file)
print("", file=service_file)
print("[Service]", file=service_file)
print("Type=simple", file=service_file)
print(f"User={user_name}", file=service_file)
# For PulseAudio
print(f"Environment=XDG_RUNTIME_DIR=/run/user/{user_id}", file=service_file)
print(f"ExecStart={satellite_command_str}", file=service_file)
print(f"WorkingDirectory={PROGRAM_DIR}", file=service_file)
print("Restart=always", file=service_file)
print("RestartSec=1", file=service_file)
print("", file=service_file)
print("[Install]", file=service_file)
print("WantedBy=default.target", file=service_file)
def install_services(settings: Settings, password: str):
# Install and run
installed_services = ["satellite"]
if settings.satellite.type == SatelliteType.WAKE:
assert settings.wake.system is not None, "No wake word system"
installed_services.append(WakeWordSystem(settings.wake.system).value.lower())
install_commands = []
for service in installed_services:
service_filename = f"wyoming-{service}.service"
install_commands.append(
[
"sudo",
"-S",
"cp",
str(SERVICES_DIR / service_filename),
"/etc/systemd/system/",
]
)
install_commands.append(["sudo", "-S", "systemctl", "daemon-reload"])
# Copy first, then enable and start
for service in installed_services:
install_commands.append(["sudo", "-S", "systemctl", "enable", service_filename])
install_commands.append(
["sudo", "-S", "systemctl", "start", "wyoming-satellite.service"]
)
success = run_with_gauge(
"Installing Services...", install_commands, sudo_password=password
)
if success:
msgbox("Successfully installed services")
else:
error("installing services")
+24 -15
View File
@@ -1,10 +1,10 @@
"""Speaker settings.""" """Speaker settings."""
import subprocess
import logging import logging
from typing import Optional, List import subprocess
from typing import List, Optional
from .const import Settings, PROGRAM_DIR from .const import PROGRAM_DIR, Settings
from .whiptail import radiolist, inputbox, msgbox, checklist, menu from .whiptail import checklist, inputbox, menu, msgbox, radiolist
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@@ -14,38 +14,44 @@ def configure_speakers(settings: Settings) -> None:
while True: while True:
choice = speakers_menu(choice) choice = speakers_menu(choice)
if choice == "test": if choice == "play":
if settings.snd.device is None:
msgbox("No speaker device set")
continue
test_sound_device(settings.snd.device)
elif choice == "test":
sound_device = test_speakers() sound_device = test_speakers()
if sound_device is not None: if sound_device is not None:
settings.sound_device = sound_device settings.snd.device = sound_device
settings.save() settings.save()
elif choice == "list": elif choice == "list":
sound_device = radiolist( sound_device = radiolist(
"Select ALSA Device:", "Select ALSA Device:",
get_sound_devices(), get_sound_devices(),
settings.sound_device, settings.snd.device,
) )
if sound_device: if sound_device:
settings.sound_device = sound_device settings.snd.device = sound_device
settings.save() settings.save()
elif choice == "manual": elif choice == "manual":
sound_device = inputbox("Enter ALSA Device:", settings.sound_device) sound_device = inputbox("Enter ALSA Device:", settings.snd.device)
if sound_device: if sound_device:
settings.sound_device = sound_device settings.snd.device = sound_device
settings.save() settings.save()
elif choice == "disable": elif choice == "disable":
settings.sound_device = None settings.snd.device = None
settings.save() settings.save()
msgbox("Sound disabled") msgbox("Sound disabled")
elif choice == "feedback": elif choice == "feedback":
feedback_sounds = checklist( feedback_sounds = checklist(
"Enabled Sounds:", "Enabled Sounds:",
[("awake", "On wake-up"), ("done", "After voice command")], [("awake", "On wake-up"), ("done", "After voice command")],
settings.feedback_sounds, settings.snd.feedback_sounds,
) )
if feedback_sounds is not None: if feedback_sounds is not None:
settings.feedback_sounds = feedback_sounds settings.snd.feedback_sounds = feedback_sounds
settings.save() settings.save()
else: else:
break break
@@ -55,7 +61,8 @@ def speakers_menu(last_choice: Optional[str]) -> Optional[str]:
return menu( return menu(
"Main > Speakers", "Main > Speakers",
[ [
("test", "Test Speakers"), ("play", "Play Sound"),
("test", "Test All Speakers"),
("list", "Select From List"), ("list", "Select From List"),
("manual", "Enter Manually"), ("manual", "Enter Manually"),
("disable", "Disable Sound"), ("disable", "Disable Sound"),
@@ -71,7 +78,9 @@ def get_sound_devices() -> List[str]:
lines = subprocess.check_output(["aplay", "-L"]).decode("utf-8").splitlines() lines = subprocess.check_output(["aplay", "-L"]).decode("utf-8").splitlines()
for line in lines: for line in lines:
line = line.strip() line = line.strip()
if line.startswith("plughw:"):
# default = PulseAudio
if (line == "default") or line.startswith("plughw:"):
devices.append(line) devices.append(line)
return devices return devices
+217 -79
View File
@@ -3,25 +3,38 @@ import itertools
import re import re
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Optional, Dict from typing import Dict, Optional
from .const import Settings, SatelliteType, WakeWordSystem, LOCAL_DIR from .const import LOCAL_DIR, SatelliteType, Settings, WakeWordSystem
from .whiptail import msgbox, menu, radiolist, run_with_gauge, yesno, error from .whiptail import error, inputbox, menu, msgbox, radiolist, run_with_gauge, yesno
def configure_wake_word(settings: Settings) -> None: def configure_wake_word(settings: Settings) -> None:
if settings.satellite_type != SatelliteType.WAKE: if settings.satellite.type != SatelliteType.WAKE:
if not yesno("Set satellite type to local wake word?"): if not yesno("Set satellite type to local wake word?"):
return return
settings.satellite_type = SatelliteType.WAKE settings.satellite.type = SatelliteType.WAKE
settings.save() settings.save()
def star(ww_system: WakeWordSystem) -> str:
return (
f"{ww_system.value} [*]"
if settings.wake.system == ww_system
else ww_system.value
)
choice: Optional[str] = None choice: Optional[str] = None
while True: while True:
choice = menu( choice = menu(
"Main > Wake Word", "Main > Wake Word",
[("system", "Wake Word System"), ("wake_word", "Wake Word")], [
("system", "Wake Word System"),
("wake_word", "Choose Wake Word"),
("openWakeWord", star(WakeWordSystem.OPENWAKEWORD)),
("porcupine1", star(WakeWordSystem.PORCUPINE1)),
("snowboy", star(WakeWordSystem.SNOWBOY)),
],
selected_item=choice, selected_item=choice,
menu_args=["--ok-button", "Select", "--cancel-button", "Back"], menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
) )
@@ -30,7 +43,7 @@ def configure_wake_word(settings: Settings) -> None:
wake_word_system = radiolist( wake_word_system = radiolist(
"Wake Word System:", "Wake Word System:",
[v.value for v in WakeWordSystem], [v.value for v in WakeWordSystem],
settings.wake_word_system, settings.wake.system,
) )
if wake_word_system is None: if wake_word_system is None:
continue continue
@@ -39,6 +52,12 @@ def configure_wake_word(settings: Settings) -> None:
install_wake_word(settings, wake_word_system) install_wake_word(settings, wake_word_system)
elif choice == "wake_word": elif choice == "wake_word":
select_wake_word(settings) select_wake_word(settings)
elif choice == "openWakeWord":
configure_openWakeWord(settings)
elif choice == "porcupine1":
configure_porcupine1(settings)
elif choice == "snowboy":
configure_snowboy(settings)
else: else:
break break
@@ -75,8 +94,7 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N
msgbox("openWakeWord installed successfully") msgbox("openWakeWord installed successfully")
settings.wake_word_system = wake_word_system settings.wake.system = wake_word_system
settings.wake_word.setdefault(WakeWordSystem.OPENWAKEWORD, "ok_nabu")
settings.save() settings.save()
return return
@@ -110,7 +128,7 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N
else: else:
msgbox("porcupine1 installed successfully") msgbox("porcupine1 installed successfully")
settings.wake_word_system = wake_word_system settings.wake.system = wake_word_system
settings.save() settings.save()
return return
@@ -144,21 +162,19 @@ def install_wake_word(settings: Settings, wake_word_system: WakeWordSystem) -> N
else: else:
msgbox("snowboy installed successfully") msgbox("snowboy installed successfully")
settings.wake_word_system = wake_word_system settings.wake.system = wake_word_system
settings.save() settings.save()
return return
def select_wake_word(settings: Settings) -> None: def select_wake_word(settings: Settings) -> None:
if settings.wake_word_system == WakeWordSystem.OPENWAKEWORD: if settings.wake.system == WakeWordSystem.OPENWAKEWORD:
oww_dir = LOCAL_DIR / "wyoming-openwakeword" oww_dir = LOCAL_DIR / "wyoming-openwakeword"
if not oww_dir.exists(): if not oww_dir.exists():
msgbox("openWakeWord is not installed") msgbox("openWakeWord is not installed")
return return
custom_wake_word_dir = ( custom_wake_word_dir = LOCAL_DIR / "custom-wake-words" / "openWakeWord"
LOCAL_DIR / "custom-wake-words" / WakeWordSystem.OPENWAKEWORD.value
)
custom_wake_word_dir.mkdir(parents=True, exist_ok=True) custom_wake_word_dir.mkdir(parents=True, exist_ok=True)
community_wake_word_dir = LOCAL_DIR / "home-assistant-wakewords-collection" community_wake_word_dir = LOCAL_DIR / "home-assistant-wakewords-collection"
@@ -187,18 +203,97 @@ def select_wake_word(settings: Settings) -> None:
ww_paths[ww_name] = ww_path ww_paths[ww_name] = ww_path
items = [("__community__", "Download Community Wake Words")] + list( items = sorted(list(ww_paths.keys()))
sorted(ww_paths.keys())
)
wake_word = radiolist( wake_word = radiolist(
"Wake Word:", items, settings.wake_word.get(WakeWordSystem.OPENWAKEWORD) "Wake Word:", items, settings.wake.openwakeword.wake_word
) )
if wake_word is None: if wake_word is None:
break break
if wake_word == "__community__": wake_word_path = ww_paths[wake_word]
if wake_word_path.is_relative_to(community_wake_word_dir):
# Copy to custom directory
shutil.copy(wake_word_path, custom_wake_word_dir)
settings.wake.openwakeword.wake_word = wake_word
settings.save()
break
return
if settings.wake.system == WakeWordSystem.PORCUPINE1:
porcupine1_dir = LOCAL_DIR / "wyoming-porcupine1"
if not porcupine1_dir.exists():
msgbox("porcupine1 is not installed")
return
ww_names = sorted(
list(
set(
p.stem.rsplit("_", maxsplit=1)[0]
for p in (
porcupine1_dir / "wyoming_porcupine1" / "data" / "resources"
).rglob("*.ppn")
)
)
)
wake_word = radiolist(
"Wake Word:", ww_names, settings.wake.porcupine1.wake_word
)
if wake_word is not None:
settings.wake.porcupine1.wake_word = wake_word
settings.save()
return
if settings.wake.system == WakeWordSystem.SNOWBOY:
snowboy_dir = LOCAL_DIR / "wyoming-snowboy"
if not snowboy_dir.exists():
msgbox("snowboy is not installed")
return
custom_wake_word_dir = LOCAL_DIR / "custom-wake-words" / "snowboy"
custom_wake_word_dir.mkdir(parents=True, exist_ok=True)
builtin_wake_words = (snowboy_dir / "wyoming_snowboy" / "data").glob("*.umdl")
custom_wake_words = itertools.chain(
custom_wake_word_dir.glob("*.pmdl"), custom_wake_word_dir.glob("*.umdl")
)
ww_names = sorted(
list(
set(
p.stem
for p in itertools.chain(builtin_wake_words, custom_wake_words)
)
)
)
wake_word = radiolist("Wake Word:", ww_names, settings.wake.snowboy.wake_word)
if wake_word is not None:
settings.wake.snowboy.wake_word = wake_word
settings.save()
return
def configure_openWakeWord(settings: Settings) -> None:
choice: Optional[str] = None
while True:
choice = menu(
"Main > Wake Word > openWakeWord",
[
("community", "Download Community Wake Words"),
("threshold", "Set Threshold"),
("trigger_level", "Set Trigger Level"),
],
selected_item=choice,
menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
)
if choice == "community":
community_wake_word_dir = LOCAL_DIR / "home-assistant-wakewords-collection"
if not community_wake_word_dir.exists(): if not community_wake_word_dir.exists():
success = run_with_gauge( success = run_with_gauge(
"Downloading community wake words...", "Downloading community wake words...",
@@ -231,78 +326,121 @@ def select_wake_word(settings: Settings) -> None:
if not success: if not success:
error("updating community wake words") error("updating community wake words")
elif choice == "threshold":
while True:
threshold = inputbox(
"Threshold (0-1, 0.5 = default):",
settings.wake.openwakeword.threshold,
)
if threshold is None:
break
try:
threshold_float = float(threshold)
except ValueError:
msgbox("Invalid value")
continue continue
wake_word_path = ww_paths[wake_word] if 0 < threshold_float < 1:
if wake_word_path.is_relative_to(community_wake_word_dir): settings.wake.openwakeword.threshold = threshold_float
# Copy to custom directory
shutil.copy(wake_word_path, custom_wake_word_dir)
settings.wake_word[WakeWordSystem.OPENWAKEWORD] = wake_word
settings.save() settings.save()
break break
return msgbox("Threshold must be in (0, 1)")
elif choice == "trigger_level":
if settings.wake_word_system == WakeWordSystem.PORCUPINE1: while True:
porcupine1_dir = LOCAL_DIR / "wyoming-porcupine1" trigger_level = inputbox(
if not porcupine1_dir.exists(): "Trigger Level (> 0, 1 = default):",
msgbox("porcupine1 is not installed") settings.wake.openwakeword.trigger_level,
return
ww_names = sorted(
list(
set(
p.stem.rsplit("_", maxsplit=1)[0]
for p in (
porcupine1_dir / "wyoming_porcupine1" / "data" / "resources"
).rglob("*.ppn")
)
)
) )
if trigger_level is None:
break
wake_word = radiolist( try:
"Wake Word:", trigger_level_int = int(trigger_level)
ww_names, except ValueError:
settings.wake_word.get(WakeWordSystem.PORCUPINE1), msgbox("Invalid value")
) continue
if wake_word is not None: if trigger_level_int > 0:
settings.wake_word[WakeWordSystem.PORCUPINE1] = wake_word settings.wake.openwakeword.trigger_level = trigger_level_int
settings.save() settings.save()
break
return msgbox("Trigger level must be > 0")
else:
break
if settings.wake_word_system == WakeWordSystem.SNOWBOY:
snowboy_dir = LOCAL_DIR / "wyoming-snowboy"
if not snowboy_dir.exists():
msgbox("snowboy is not installed")
return
custom_wake_word_dir = ( def configure_porcupine1(settings: Settings) -> None:
LOCAL_DIR / "custom-wake-words" / WakeWordSystem.SNOWBOY.value choice: Optional[str] = None
) while True:
custom_wake_word_dir.mkdir(parents=True, exist_ok=True) choice = menu(
"Main > Wake Word > porcupine1",
builtin_wake_words = (snowboy_dir / "wyoming_snowboy" / "data").glob("*.umdl") [
custom_wake_words = itertools.chain( ("sensitivity", "Set Sensitivity"),
custom_wake_word_dir.glob("*.pmdl"), custom_wake_word_dir.glob("*.umdl") ],
) selected_item=choice,
ww_names = sorted( menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
list(
set(
p.stem
for p in itertools.chain(builtin_wake_words, custom_wake_words)
)
)
) )
wake_word = radiolist( if choice == "sensitivity":
"Wake Word:", ww_names, settings.wake_word.get(WakeWordSystem.SNOWBOY) while True:
sensitivity = inputbox(
"Sensitivity (0-1, 0.5 = default):",
settings.wake.porcupine1.sensitivity,
) )
if sensitivity is None:
break
if wake_word is not None: try:
settings.wake_word[WakeWordSystem.SNOWBOY] = wake_word sensitivity_float = float(sensitivity)
except ValueError:
msgbox("Invalid value")
continue
if 0 < sensitivity_float < 1:
settings.wake.porcupine1.sensitivity = sensitivity_float
settings.save() settings.save()
break
return msgbox("Sensitivity must be in (0, 1)")
else:
break
def configure_snowboy(settings: Settings) -> None:
choice: Optional[str] = None
while True:
choice = menu(
"Main > Wake Word > snowboy",
[
("sensitivity", "Set Sensitivity"),
],
selected_item=choice,
menu_args=["--ok-button", "Select", "--cancel-button", "Back"],
)
if choice == "sensitivity":
while True:
sensitivity = inputbox(
"Sensitivity (0-1, 0.5 = default):",
settings.wake.snowboy.sensitivity,
)
if sensitivity is None:
break
try:
sensitivity_float = float(sensitivity)
except ValueError:
msgbox("Invalid value")
continue
if 0 < sensitivity_float < 1:
settings.wake.snowboy.sensitivity = sensitivity_float
settings.save()
break
msgbox("Sensitivity must be in (0, 1)")
else:
break
+11 -7
View File
@@ -1,13 +1,13 @@
"""Python interface to whiptail command.""" """Python interface to whiptail command."""
import subprocess
import time
import logging import logging
import shlex import shlex
from concurrent.futures import ThreadPoolExecutor import subprocess
import time
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Dict, List, Tuple, Optional, Union from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional, Tuple, Union
from .const import TITLE, WIDTH, HEIGHT, LIST_HEIGHT from .const import HEIGHT, LIST_HEIGHT, TITLE, WIDTH
ItemType = Union[str, Tuple[Any, str]] ItemType = Union[str, Tuple[Any, str]]
@@ -66,7 +66,9 @@ def menu(
def inputbox(text: str, init: Optional[Any] = None) -> Optional[str]: def inputbox(text: str, init: Optional[Any] = None) -> Optional[str]:
return whiptail("--inputbox", text, HEIGHT, WIDTH, str(init) or "") return whiptail(
"--inputbox", text, HEIGHT, WIDTH, str(init) if init is not None else ""
)
def passwordbox(text: str) -> Optional[str]: def passwordbox(text: str) -> Optional[str]:
@@ -184,7 +186,9 @@ def gauge(text: str, seconds: int, parts: int = 20) -> None:
def error(reason: str) -> None: def error(reason: str) -> None:
msgbox(f"An error occurred while {reason}.\n" "See local/installer.log for details.") msgbox(
f"An error occurred while {reason}.\n" "See local/installer.log for details."
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
+5 -6
View File
@@ -8,11 +8,10 @@ _PROGRAM_DIR = _DIR.parent
_VENV_DIR = _PROGRAM_DIR / ".venv" _VENV_DIR = _PROGRAM_DIR / ".venv"
_MODULE_DIR = _PROGRAM_DIR / "wyoming_satellite" _MODULE_DIR = _PROGRAM_DIR / "wyoming_satellite"
_TESTS_DIR = _PROGRAM_DIR / "tests" _TESTS_DIR = _PROGRAM_DIR / "tests"
_INSTALLER_DIR = _PROGRAM_DIR / "installer"
_FORMAT_DIRS = [_MODULE_DIR, _TESTS_DIR, _INSTALLER_DIR]
context = venv.EnvBuilder().ensure_directories(_VENV_DIR) context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
subprocess.check_call( subprocess.check_call([context.env_exe, "-m", "black"] + _FORMAT_DIRS)
[context.env_exe, "-m", "black", str(_MODULE_DIR), str(_TESTS_DIR)] subprocess.check_call([context.env_exe, "-m", "isort"] + _FORMAT_DIRS)
)
subprocess.check_call(
[context.env_exe, "-m", "isort", str(_MODULE_DIR), str(_TESTS_DIR)]
)
+8 -15
View File
@@ -8,20 +8,13 @@ _PROGRAM_DIR = _DIR.parent
_VENV_DIR = _PROGRAM_DIR / ".venv" _VENV_DIR = _PROGRAM_DIR / ".venv"
_MODULE_DIR = _PROGRAM_DIR / "wyoming_satellite" _MODULE_DIR = _PROGRAM_DIR / "wyoming_satellite"
_TESTS_DIR = _PROGRAM_DIR / "tests" _TESTS_DIR = _PROGRAM_DIR / "tests"
_INSTALLER_DIR = _PROGRAM_DIR / "installer"
_LINT_DIRS = [_MODULE_DIR, _TESTS_DIR, _INSTALLER_DIR]
context = venv.EnvBuilder().ensure_directories(_VENV_DIR) context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
subprocess.check_call( subprocess.check_call([context.env_exe, "-m", "black"] + _LINT_DIRS + ["--check"])
[context.env_exe, "-m", "black", str(_MODULE_DIR), str(_TESTS_DIR), "--check"] subprocess.check_call([context.env_exe, "-m", "isort"] + _LINT_DIRS + ["--check"])
) subprocess.check_call([context.env_exe, "-m", "flake8"] + _LINT_DIRS)
subprocess.check_call( subprocess.check_call([context.env_exe, "-m", "pylint"] + _LINT_DIRS)
[context.env_exe, "-m", "isort", str(_MODULE_DIR), str(_TESTS_DIR), "--check"] subprocess.check_call([context.env_exe, "-m", "mypy"] + _LINT_DIRS)
)
subprocess.check_call(
[context.env_exe, "-m", "flake8", str(_MODULE_DIR), str(_TESTS_DIR)]
)
subprocess.check_call(
[context.env_exe, "-m", "pylint", str(_MODULE_DIR), str(_TESTS_DIR)]
)
subprocess.check_call(
[context.env_exe, "-m", "mypy", str(_MODULE_DIR), str(_TESTS_DIR)]
)