Working on installer

This commit is contained in:
Michael Hansen
2024-01-11 17:12:25 -06:00
parent 693b8e0e19
commit d29d3d0613
6 changed files with 1176 additions and 11 deletions
+8
View File
@@ -22,6 +22,14 @@ Requires:
## Installation
Install the necessary system dependencies:
``` sh
sudo apt-get install python3-venv python3-pip
```
Then run the install script:
``` sh
script/setup
```
@@ -1,2 +1 @@
pysilero-vad==1.0.0
webrtc-noise-gain==1.2.3
+1
View File
@@ -0,0 +1 @@
pysilero-vad==1.0.0
+660
View File
@@ -0,0 +1,660 @@
#!/usr/bin/env python3
import array
import math
import logging
import json
import functools
import shlex
import subprocess
import shutil
import sys
import time
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
_DIR = Path(__file__).parent
_PROGRAM_DIR = _DIR.parent
_LOCAL_DIR = _PROGRAM_DIR / "local"
_SETTINGS_PATH = _LOCAL_DIR / "settings.json"
_LOGGER = logging.getLogger()
_RECORD_SECONDS = 8
_RECORD_RMS_MIN = 30
TITLE = "Wyoming Satellite"
WIDTH = "75"
HEIGHT = "20"
LIST_HEIGHT = "12"
ItemType = Union[str, Tuple[str, str]]
class SatelliteType(str, Enum):
ALWAYS_STREAMING = "always"
VAD = "vad"
WAKE = "wake"
class WakeWordSystem(str, Enum):
OPENWAKEWORD = "openWakeWord"
PORCUPINE1 = "porcupine1"
SNOWBOY = "snowboy"
@dataclass
class Settings:
microphone_device: Optional[str] = None
sound_device: Optional[str] = None
feedback_sounds: List[str] = field(default_factory=list)
wake_word_system: Optional[WakeWordSystem] = None
wake_word: Dict[WakeWordSystem, str] = field(default_factory=dict)
satellite_name: str = "Wyoming Satellite"
satellite_type: SatelliteType = SatelliteType.ALWAYS_STREAMING
@staticmethod
def load() -> "Settings":
kwargs: Dict[str, Any] = {}
if _SETTINGS_PATH.exists():
_LOGGER.debug("Loading settings from %s", _SETTINGS_PATH)
with open(_SETTINGS_PATH, "r", encoding="utf-8") as settings_file:
settings_dict = json.load(settings_file)
for settings_field in fields(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:
_LOGGER.debug("Saving settings to %s", _SETTINGS_PATH)
settings_dict = asdict(self)
_SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(_SETTINGS_PATH, "w", encoding="utf-8") as settings_file:
json.dump(settings_dict, settings_file, ensure_ascii=False, indent=2)
# -----------------------------------------------------------------------------
def main() -> None:
_LOCAL_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
filename=_LOCAL_DIR / "installer.log", filemode="w", level=logging.DEBUG
)
settings = Settings.load()
while True:
choice = main_menu(settings)
if choice == "mic":
configure_microphone(settings)
elif choice == "snd":
configure_sound(settings)
elif choice == "wake":
configure_wake_word(settings)
elif choice == "name":
set_satellite_name(settings)
elif choice == "type":
set_satellite_type(settings)
elif choice == "services":
pass
else:
break
# -----------------------------------------------------------------------------
def main_menu(settings: Settings) -> Optional[str]:
items = [("mic", "Configure Microphone"), ("snd", "Configure Sound")]
if settings.satellite_type == SatelliteType.WAKE:
items.append(("wake", "Configure Wake Word"))
items.extend(
[
("name", "Set Satellite Name"),
("type", "Set Satellite Type"),
("services", "Generate Services"),
]
)
return menu(
"Main Menu",
items,
"--ok-button",
"Select",
"--cancel-button",
"Exit",
)
# -----------------------------------------------------------------------------
# Microphone
# -----------------------------------------------------------------------------
def configure_microphone(settings: Settings) -> None:
while True:
choice = microphone_menu()
if choice == "detect":
best_device: Optional[str] = None
best_rms: Optional[float] = None
devices = get_microphone_devices()
with ThreadPoolExecutor() as executor:
futures: Dict[str, Future] = {}
for device in devices:
futures[device] = executor.submit(_record_proc, device)
gauge("Speak loudly into the microphone.", _RECORD_SECONDS)
for device, future in futures.items():
device_rms = future.result()
if device_rms < _RECORD_RMS_MIN:
continue
if (best_rms is None) or (device_rms > best_rms):
best_device = device
best_rms = device_rms
if best_device is not None:
msgbox(f"Successfully detected microphone: {best_device}")
settings.microphone_device = best_device
settings.save()
else:
msgbox("Audio was not detected from any microphone")
elif choice == "list":
microphone_device = radiolist(
"Select ALSA Device:",
get_microphone_devices(),
settings.microphone_device,
)
if microphone_device:
settings.microphone_device = microphone_device
settings.save()
elif choice == "manual":
microphone_device = inputbox(
"Enter ALSA Device:", settings.microphone_device
)
if microphone_device:
settings.microphone_device = microphone_device
settings.save()
elif choice == "respeaker":
if yesno(
"ReSpeaker drivers for the Raspberry Pi will now be compiled and installed. "
"This will take a while and require a reboot. "
"Continue?"
):
subprocess.check_call(
["sudo", str(_PROGRAM_DIR / "etc" / "install-respeaker-drivers.sh")]
)
msgbox(
"Driver installation complete. "
"Please reboot your Raspberry Pi and re-run the installer. "
'Once rebooted, select the "seeed" microphone.'
)
sys.exit(0)
else:
break
def microphone_menu() -> Optional[str]:
return menu(
"Configure Microphone",
[
("detect", "Autodetect"),
("list", "Select From List"),
("manual", "Enter Manually"),
("respeaker", "Install ReSpeaker Drivers"),
],
"--ok-button",
"Select",
"--cancel-button",
"Back",
)
def get_microphone_devices() -> List[str]:
devices = []
lines = subprocess.check_output(["arecord", "-L"]).decode("utf-8").splitlines()
for line in lines:
line = line.strip()
if line.startswith("plughw:"):
devices.append(line)
return devices
def _record_proc(device: str) -> float:
try:
audio = subprocess.check_output(
[
"arecord",
"-q",
"-D",
device,
"-r",
"16000",
"-c",
"1",
"-f",
"S16_LE",
"-t",
"raw",
"-d",
str(_RECORD_SECONDS),
],
stderr=subprocess.DEVNULL,
)
# 16-bit mono
audio_array = array.array("h", audio)
rms = math.sqrt(
(1 / len(audio_array) * sum(x * x for x in audio_array.tolist()))
)
return rms
except Exception:
_LOGGER.exception("Error recording from device: %s", device)
return 0
# -----------------------------------------------------------------------------
# Sound
# -----------------------------------------------------------------------------
def configure_sound(settings: Settings) -> None:
while True:
choice = sound_menu()
if choice == "detect":
pass
elif choice == "list":
sound_device = radiolist(
"Select ALSA Device:",
get_sound_devices(),
settings.sound_device,
)
if sound_device:
settings.sound_device = sound_device
settings.save()
elif choice == "manual":
sound_device = inputbox("Enter ALSA Device:", settings.sound_device)
if sound_device:
settings.sound_device = sound_device
settings.save()
elif choice == "disable":
settings.sound_device = None
settings.save()
msgbox("Sound disabled")
elif choice == "feedback":
feedback_sounds = checklist(
"Enabled Sounds:",
[("awake", "On wake-up"), ("done", "After voice command")],
settings.feedback_sounds,
)
if feedback_sounds is not None:
settings.feedback_sounds = feedback_sounds
settings.save()
else:
break
def sound_menu() -> Optional[str]:
return menu(
"Configure Sound",
[
("detect", "Autodetect"),
("list", "Select From List"),
("manual", "Enter Manually"),
("disable", "Disable Sound"),
("feedback", "Toggle Feedback Sounds"),
],
"--ok-button",
"Select",
"--cancel-button",
"Back",
)
def get_sound_devices() -> List[str]:
devices = []
lines = subprocess.check_output(["aplay", "-L"]).decode("utf-8").splitlines()
for line in lines:
line = line.strip()
if line.startswith("plughw:"):
devices.append(line)
return devices
# -----------------------------------------------------------------------------
# Wake Word
# -----------------------------------------------------------------------------
def configure_wake_word(settings: Settings) -> None:
while True:
choice = wake_word_menu(settings)
if choice == "system":
wake_word_system = radiolist(
"Wake Word System:",
[v.value for v in WakeWordSystem],
settings.wake_word_system,
)
if wake_word_system is not None:
settings.wake_word_system = wake_word_system
install_wake_word(settings)
elif choice == "wake_word":
pass
else:
break
def wake_word_menu(settings: Settings) -> Optional[str]:
items = [("system", "Select System")]
if settings.wake_word_system is not None:
items.append(("wake_word", "Select Wake Word"))
return menu(
"Configure Wake Word",
items,
"--ok-button",
"Select",
"--cancel-button",
"Back",
)
def install_wake_word(settings: Settings) -> None:
if settings.wake_word_system == WakeWordSystem.OPENWAKEWORD:
settings.wake_word.setdefault(WakeWordSystem.OPENWAKEWORD, "ok_nabu")
oww_dir = _LOCAL_DIR / "wyoming-openwakeword"
if not oww_dir.exists():
success = run_with_gauge(
"Installing openWakeWord",
[
[
"git",
"clone",
"https://github.com/rhasspy/wyoming-openwakeword.git",
str(oww_dir),
],
[str(oww_dir / "script" / "setup")],
],
)
if success:
msgbox("openWakeWord installed successfully")
else:
msgbox(
"An error occurred while installing openWakeWord. "
"See local/installer.log for details."
)
settings.wake_word_system = None
settings.save()
try:
shutil.rmtree(oww_dir)
except Exception:
pass
elif settings.wake_word_system == WakeWordSystem.PORCUPINE1:
settings.wake_word.setdefault(WakeWordSystem.PORCUPINE1, "porcupine")
elif settings.wake_word_system == WakeWordSystem.SNOWBOY:
settings.wake_word.setdefault(WakeWordSystem.SNOWBOY, "snowboy")
# -----------------------------------------------------------------------------
# Satellite
# -----------------------------------------------------------------------------
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 = satellite_type
settings.save()
# -----------------------------------------------------------------------------
def whiptail(*args) -> Optional[str]:
proc = subprocess.Popen(
["whiptail", "--title", TITLE] + list(args),
stderr=subprocess.PIPE,
)
_stdout, stderr = proc.communicate()
if proc.returncode != 0:
return None
return stderr.decode("utf-8")
def menu(text: str, items: List[ItemType], *args) -> Optional[str]:
assert items, "No items"
item_map: Dict[str, ItemType] = {}
item_args: List[str] = []
for i, item in enumerate(items):
item_id = str(i)
item_args.append(item_id)
if isinstance(item, str):
item_map[item_id] = item
item_args.append(item)
else:
item_map[item_id] = item[0]
item_args.append(item[1])
result = whiptail(
"--notags", *args, "--menu", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args
)
return item_map.get(result)
def inputbox(text: str, init: Optional[str] = None) -> Optional[str]:
return whiptail("--inputbox", text, HEIGHT, WIDTH, init or "")
def radiolist(
text: str,
items: List[ItemType],
selected_item: Any,
*args,
) -> Optional[str]:
assert items, "No items"
item_map: Dict[str, ItemType] = {}
item_args: List[str] = []
for i, item in enumerate(items):
item_id = str(i)
item_args.append(item_id)
if isinstance(item, str):
item_map[item_id] = item
item_args.append(item)
if item == selected_item:
item_args.append("1")
else:
item_args.append("0")
else:
item_map[item_id] = item[0]
item_args.append(item[1])
if item[0] == selected_item:
item_args.append("1")
else:
item_args.append("0")
result = whiptail(
"--notags", *args, "--radiolist", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args
)
if result is None:
return None
return item_map.get(result, result)
def checklist(
text: str,
items: List[ItemType],
selected_items: List[Any],
*args,
) -> List[str]:
assert items, "No items"
item_map: Dict[str, ItemType] = {}
item_args: List[str] = []
for i, item in enumerate(items):
item_id = str(i)
item_args.append(item_id)
if isinstance(item, str):
item_map[item_id] = item
item_args.append(item)
if item in selected_items:
item_args.append("1")
else:
item_args.append("0")
else:
item_map[item_id] = item[0]
item_args.append(item[1])
if item[0] in selected_items:
item_args.append("1")
else:
item_args.append("0")
result = whiptail(
"--notags", *args, "--checklist", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args
)
if result is None:
return None
return [
item_map.get(result_item, result_item) for result_item in shlex.split(result)
]
def yesno(text: str) -> bool:
return whiptail("--yesno", text, HEIGHT, WIDTH) is not None
def msgbox(text: str) -> None:
whiptail("--msgbox", text, HEIGHT, WIDTH)
def gauge(text: str, seconds: int, parts: int = 20) -> None:
proc = subprocess.Popen(
["whiptail", "--title", TITLE, "--gauge", text, HEIGHT, WIDTH, "0"],
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
assert proc.stdin is not None
percent = 0
while percent <= 100:
time.sleep(seconds / parts)
percent += int(100 / parts)
print(percent, file=proc.stdin, flush=True)
proc.communicate()
def run_with_gauge(text: str, commands: List[List[str]]) -> bool:
proc = subprocess.Popen(
["whiptail", "--title", TITLE, "--gauge", text, HEIGHT, WIDTH, "0"],
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
assert proc.stdin is not None
percent = 0
seconds = 5
parts = 20
with ThreadPoolExecutor() as executor:
for command in commands:
future = executor.submit(_run_command, command)
while not future.done():
time.sleep(seconds / parts)
percent += int(100 / parts)
if percent > 100:
percent = 0
print(percent, file=proc.stdin, flush=True)
if not future.result():
# Error occurred
return False
proc.communicate()
return True
def _run_command(command: List[str]) -> bool:
try:
proc = subprocess.Popen(
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
_stdout, stderr = proc.communicate()
if proc.returncode != 0:
_LOGGER.error("Error running command: %s", command)
_LOGGER.error(stderr)
return False
except Exception:
_LOGGER.exception("Error running command: %s", command)
return False
return True
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+482
View File
@@ -0,0 +1,482 @@
#!/usr/bin/env bash
set -eu
title='Wyoming Satellite'
# Directory of *this* script
this_dir="$( cd "$( dirname "$0" )" && pwd )"
program_dir="$(realpath "${this_dir}/..")"
# Directory to store stuff that won't interfere with git
local_dir="${program_dir}/local"
mkdir -p "${local_dir}"
debug_enabled=''
system_deps=''
satellite_name=''
satellite_type=''
wake_word_system=''
wake_word=''
custom_wake_word_dir="${local_dir}/custom-wake-words"
mkdir -p "${custom_wake_word_dir}"
noise_suppression=''
microphone_command='arecord -q -r 16000 -c 1 -f S16_LE -t raw'
speaker_command='aplay -q -r 22050 -c 1 -f S16_LE -t raw'
speaker_sounds=()
pushd () {
command pushd "$@" > /dev/null
}
popd () {
command popd "$@" > /dev/null
}
# Args
for arg in "$@"; do
if [ "${arg}" = '--debug' ]; then
debug_enabled='1'
fi
done
if [ -n "${debug_enabled}" ]; then
whiptail --title "${title}" --msgbox \
'Debug mode is enabled. This will cause all services to log verbosely, and audio to be recorded in local/debug-recording. Make sure to re-run the installer without debug mode before deploying the satellite.' \
10 75
fi
# -----------------------------------------------------------------------------
# System dependencies
# -----------------------------------------------------------------------------
commands=('whiptail' 'git' 'python3' 'pip3')
for command in "${commands[@]}";
do
if ! command -v "${command}" &> /dev/null; then
system_deps='1'
break
fi
done
if command -v python3 &> /dev/null; then
if ! python3 -c 'import venv'; then
system_deps='1'
fi
fi
if [ -n "${system_deps}" ]; then
echo 'Installing system dependencies'
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
whiptail python3-venv python3-pip git
fi
# -----------------------------------------------------------------------------
# Microphone
# -----------------------------------------------------------------------------
function get_microphone {
microphones=()
while read -r microphone; do
microphones+=("${microphone}" "${microphone}" 0)
done < <(arecord -L | grep 'plughw:')
whiptail --title "${title}" --notags \
--radiolist 'Select Microphone:' 20 70 \
12 "${microphones[@]}" \
'custom' 'Custom ALSA Device' 0 \
'respeaker' 'Install ReSpeaker Drivers' 0
}
function get_custom_microphone {
whiptail --title "${title}" --inputbox \
'ALSA Input Device Name:' \
8 75
}
microphone=''
while [ -z "${microphone}" ]; do
microphone="$(get_microphone 3>&1 1>&2 2>&3)"
done
if [ "${microphone}" = 'custom' ]; then
# Custom ALSA device name from "arecord -L"
microphone=''
while [ -z "${microphone}" ]; do
microphone="$(get_custom_microphone 3>&1 1>&2 2>&3)"
done
elif [ "${microphone}" = 'respeaker' ]; then
# ReSpeaker driver installation (requires reboot)
whiptail --title "${title}" --yesno \
'ReSpeaker drivers for the Raspberry Pi will now be compiled and installed. This will take a while and require a reboot. Continue?' \
8 75
echo 'Installing ReSpeaker drivers'
"${program_dir}/etc/install-respeaker-drivers.sh"
whiptail --title "${title}" --msgbox \
'Driver installation complete. The system will now reboot. Re-run the installer and select the "seeed" microphone.' \
8 75
sudo reboot
fi
# Add device
microphone_command="${microphone_command} -D ${microphone}"
# -----------------------------------------------------------------------------
# Speaker
# -----------------------------------------------------------------------------
function get_speaker {
speakers=()
while read -r speaker; do
speakers+=("${speaker}" "${speaker}" 0)
done < <(aplay -L | grep 'plughw:')
whiptail --title "${title}" --notags \
--radiolist 'Select Speaker:' 20 70 \
12 "${speakers[@]}" \
'custom' 'Custom ALSA Device' 0 \
'no_speaker' 'No Audio Output' 0
}
function get_custom_speaker {
whiptail --title "${title}" --inputbox \
'ALSA Output Device Name:' \
8 75
}
function get_speaker_sounds {
whiptail --title "${title}" --checklist \
'Play Sounds?' \
10 50 2 \
'awake' 'On wake-up' 0 \
'done' 'After voice command' 0
}
speaker=''
while [ -z "${speaker}" ]; do
speaker="$(get_speaker 3>&1 1>&2 2>&3)"
done
if [ "${speaker}" = 'custom' ]; then
# Custom ALSA device name from "aplay -L"
speaker=''
while [ -z "${speaker}" ]; do
speaker="$(get_custom_speaker 3>&1 1>&2 2>&3)"
done
fi
if [ "${speaker}" = 'no_speaker' ]; then
# No audio output
speaker_command=''
else
# Add device
speaker_command="${speaker_command} -D ${speaker}"
# awake/done sounds
speaker_sounds="$(get_speaker_sounds 3>&1 1>&2 2>&3)"
fi
# -----------------------------------------------------------------------------
# wyoming-satellite
# -----------------------------------------------------------------------------
if [ ! -d "${program_dir}/wyoming_satellite" ]; then
if [ ! -d "${this_dir}/wyoming-satellite" ]; then
# Download source here
git clone 'https://github.com/rhasspy/wyoming-satellite.git'
fi
program_dir="${this_dir}/wyoming-satellite"
fi
# Virtual environment
venv="${program_dir}/.venv"
pip="${venv}/bin/pip3"
if [ ! -d "${venv}" ]; then
echo 'Creating virtual environment'
"${program_dir}/script/setup"
fi
# Satellite name
function get_satellite_name {
whiptail --title "${title}" --inputbox \
'Satellite Name:' \
8 75 \
'Wyoming Satellite'
}
while [ -z "${satellite_name}" ]; do
satellite_name="$(get_satellite_name 3>&1 1>&2 2>&3)"
done
# -----------------------------------------------------------------------------
# Satellite type
# -----------------------------------------------------------------------------
function get_satellite_type {
whiptail --title "${title}" --notags \
--radiolist 'Select Satellite Type:' 10 50 \
3 \
always 'Always streaming' 1 \
vad 'Voice activity detection' 0 \
wake 'Local wake word detection' 0
}
function get_wake_word_system {
# TODO: Support porcupine1 and snowboy
echo 'openWakeWord'
# whiptail --title "${title}" --notags \
# --radiolist 'Select Wake Word System:' 10 50 \
# 3 \
# openWakeWord 'openWakeWord' 1 \
# porcupine1 'porcupine1' 0 \
# snowboy 'snowboy' 0
}
function install_openWakeWord {
if [ ! -d '/usr/share/doc/libopenblas-dev' ]; then
echo 'Installing system dependencies'
sudo apt-get install --yes --no-install-recommends \
libopenblas-dev
fi
oww_dir="${local_dir}/wyoming-openwakeword"
if [ ! -d "${oww_dir}" ]; then
echo 'Installing openWakeWord'
# Download
pushd "${local_dir}"
git clone 'https://github.com/rhasspy/wyoming-openwakeword.git'
popd
# Install
"${oww_dir}/script/setup"
fi
}
function list_openWakeWord {
whiptail --title "${title}" --notags \
--radiolist 'Select Wake Word:' 15 50 \
6 \
ok_nabu 'ok nabu' 1 \
hey_jarvis 'hey jarvis' 0 \
alexa 'alexa' 0 \
hey_rhasspy 'hey rhasspy' 0 \
hey_mycroft 'hey mycroft' 0 \
community 'Community Wake Words' 0
}
satellite_type="$(get_satellite_type 3>&1 1>&2 2>&3)"
if [ "${satellite_type}" = 'vad' ]; then
# Install vad requirements
"${pip}" install \
--extra-index-url 'https://www.piwheels.org/simple' \
-f 'https://synesthesiam.github.io/prebuilt-apps/' \
-r "${program_dir}/requirements_vad.txt"
elif [ "${satellite_type}" = 'wake' ]; then
# Install openWakeWord
wake_word_system="$(get_wake_word_system 3>&1 1>&2 2>&3)"
if [ "${wake_word_system}" = 'openWakeWord' ]; then
install_openWakeWord
mkdir -p "${custom_wake_word_dir}/openWakeWord"
# Choose wake word
wake_word="$(list_openWakeWord 3>&1 1>&2 2>&3)"
if [ "${wake_word}" = 'community' ]; then
# Download community wake words
community_wake_word_dir="${local_dir}/home-assistant-wakewords-collection"
if [ ! -d "${community_wake_word_dir}" ]; then
echo 'Downloading community wake words'
pushd "${local_dir}"
git clone 'https://github.com/fwartner/home-assistant-wakewords-collection.git'
popd
fi
# Select community wake word
community_wake_words=()
pushd "${community_wake_word_dir}"
while read -r wake_word_model; do
wake_word_name="$(basename "${wake_word_model}" .tflite)"
community_wake_words+=("${wake_word_model}" "${wake_word_name}" 0)
done < <(find . -name '*.tflite' -type f | sort)
popd
function list_community_wake_words {
whiptail --title "${title}" --notags \
--radiolist 'Select Wake Word:' 25 80 \
20 ${community_wake_words[@]}
}
wake_word="$(list_community_wake_words 3>&1 1>&2 2>&3)"
cp "${community_wake_word_dir}/${wake_word}" \
"${custom_wake_word_dir}/openWakeWord/"
wake_word="$(basename "${wake_word}" .tflite)"
fi # community wake word
fi # openWakeWord
fi # local wake word detection
# -----------------------------------------------------------------------------
# Audio enhancements
# -----------------------------------------------------------------------------
function get_noise_suppression {
whiptail --title "${title}" --notags --radiolist \
'Noise Suppression Level:' \
15 75 6 \
'' 'Off' 1 \
'1' 'Low' 0 \
'2' 'Medium' 0 \
'3' 'High' 0 \
'4' 'Max' 0
}
noise_suppression="$(get_noise_suppression 3>&1 1>&2 2>&3)"
if [ -n "${noise_suppression}" ]; then
# Install audio enhancement requirements
"${pip}" install \
--extra-index-url 'https://www.piwheels.org/simple' \
-f 'https://synesthesiam.github.io/prebuilt-apps/' \
-r "${program_dir}/requirements_audio_enhancement.txt"
fi
# -----------------------------------------------------------------------------
# Services
# -----------------------------------------------------------------------------
services_dir="${local_dir}/services"
mkdir -p "${services_dir}"
wake_service_requires=''
event_service_requires=''
# Disable any previous services
echo 'Stopping previous services'
for service in 'wyoming-satellite' 'wyoming-openwakeword'; do
sudo systemctl disable --now "${service}.service" 2>/dev/null || true
done
echo 'Creating systemd unit files'
new_services=('wyoming-satellite.service')
# openWakeWord
if [ "${wake_word_system}" = 'openWakeWord' ]; then
new_services+=('wyoming-openwakeword.service')
wake_service_requires='Requires=wyoming-openwakeword.service'
wake_word_command=("${local_dir}/wyoming-openwakeword/script/run")
wake_word_command+=('--uri' 'tcp://127.0.0.1:10400')
wake_word_command+=('--custom-model-dir' "'${custom_wake_word_dir}/openWakeWord'")
if [ -n "${debug_enabled}" ]; then
wake_word_command+=('--debug')
fi
cat > "${services_dir}/wyoming-openwakeword.service" <<EOF
[Unit]
Description=Wyoming openWakeWord
[Service]
Type=simple
User=$(id --name -u)
Group=$(id --name -g)
ExecStart=${wake_word_command[@]}
WorkingDirectory=${local_dir}/wyoming-openwakeword
Restart=always
RestartSec=1
[Install]
WantedBy=default.target
EOF
fi
# wyoming-satellite
satellite_command=("${program_dir}/script/run")
satellite_command+=('--name' "'${satellite_name}'")
satellite_command+=('--uri' 'tcp://0.0.0.0:10700')
satellite_command+=('--mic-command' "'${microphone_command}'")
if [ -n "${speaker_command}" ]; then
satellite_command+=('--snd-command' "'${speaker_command}'")
for sound in ${speaker_sounds[@]}; do
if [ "${sound}" = '"awake"' ]; then
awake_wav="${local_dir}/sounds/awake.wav"
if [ ! -f "${awake_wav}" ]; then
awake_wav="${program_dir}/sounds/awake.wav"
fi
satellite_command+=('--awake-wav' "'${awake_wav}'")
elif [ "${sound}" = '"done"' ]; then
done_wav="${local_dir}/sounds/done.wav"
if [ ! -f "${done_wav}" ]; then
done_wav="${program_dir}/sounds/done.wav"
fi
satellite_command+=('--done-wav' "'${done_wav}'")
fi
done
fi
if [ "${satellite_type}" = 'vad' ]; then
satellite_command+=('--vad')
elif [ "${satellite_type}" = 'wake' ]; then
satellite_command+=('--wake-uri' 'tcp://127.0.0.1:10400')
satellite_command+=('--wake-word-name' "'${wake_word}'")
fi
if [ -n "${noise_suppression}" ]; then
satellite_command+=('--mic-noise-suppression' "${noise_suppression}")
fi
if [ -n "${debug_enabled}" ]; then
satellite_command+=('--debug')
satellite_command+=('--debug-recording-dir' "${local_dir}/debug-recording")
fi
cat > "${services_dir}/wyoming-satellite.service" <<EOF
[Unit]
Description=Wyoming Satellite
Wants=network-online.target
After=network-online.target
${wake_service_requires}
${event_service_requires}
[Service]
Type=simple
User=$(id --name -u)
Group=$(id --name -g)
ExecStart=${satellite_command[@]}
WorkingDirectory=${program_dir}
Restart=always
RestartSec=1
[Install]
WantedBy=default.target
EOF
pushd "${services_dir}"
sudo cp ${new_services[@]} /etc/systemd/system/
popd
sudo systemctl daemon-reload
sudo systemctl enable --now ${new_services[@]}
# -----------------------------------------------------------------------------
whiptail --title "${title}" --msgbox \
'Installation Successful!' \
8 35
+25 -10
View File
@@ -1,16 +1,34 @@
#!/usr/bin/env python3
from pathlib import Path
from typing import List
import setuptools
from setuptools import setup
this_dir = Path(__file__).parent
requirements = []
requirements_path = this_dir / "requirements.txt"
if requirements_path.is_file():
with open(requirements_path, "r", encoding="utf-8") as requirements_file:
requirements = requirements_file.read().splitlines()
def get_requirements(req_path: Path) -> List[str]:
if not req_path.is_file():
return []
requirements: List[str] = []
with open(req_path, "r", encoding="utf-8") as req_file:
for line in req_file:
line = line.strip()
if not line:
continue
requirements.append(line)
return requirements
install_requires = get_requirements(this_dir / "requirements.txt")
extras_require = {
"silerovad": get_requirements(this_dir / "requirements_vad.txt"),
"webrtc": get_requirements(this_dir / "requirements_audio_enhancement.txt"),
}
# -----------------------------------------------------------------------------
@@ -23,7 +41,7 @@ setup(
author="Michael Hansen",
author_email="mike@rhasspy.org",
packages=setuptools.find_packages(),
install_requires=requirements,
install_requires=install_requires,
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
@@ -35,10 +53,7 @@ setup(
"Programming Language :: Python :: 3.11",
],
keywords="rhasspy wyoming satellite",
extras_require={
"silerovad": ["pysilero-vad==1.0.0"],
"webrtc": ["webrtc-noise-gain==1.2.3"],
},
extras_require=extras_require,
entry_points={
"console_scripts": ["wyoming-satellite = wyoming_satellite:__main__.run"]
},