Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2409a4a40 | ||
|
|
2c72ad0e01 | ||
|
|
3576c0fc5f | ||
|
|
1915d3fce3 | ||
|
|
a2bb7c8f57 | ||
|
|
cc9b33e8b8 | ||
|
|
862d102405 | ||
|
|
65c123ffb9 | ||
|
|
6d11883dcb | ||
|
|
2f4276e180 | ||
|
|
397d4c8b61 | ||
|
|
f6c0d29405 | ||
|
|
a80c7724a6 | ||
|
|
2e046c6b8b | ||
|
|
5d3c243a88 | ||
|
|
2b394504a0 | ||
|
|
c6d1dec918 | ||
|
|
2f6ec1d3b0 | ||
|
|
41f107548f | ||
|
|
f0297415ed | ||
|
|
69ceeef566 | ||
|
|
886019e04f | ||
|
|
61ae4ce68c | ||
|
|
00188fa5da | ||
|
|
8a5f3fb409 |
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
*
|
*
|
||||||
!requirements.txt
|
!pyproject.toml
|
||||||
!MANIFEST.in
|
|
||||||
!setup.py
|
|
||||||
!script/setup
|
!script/setup
|
||||||
!script/run
|
!script/run
|
||||||
!wyoming_satellite/*.py
|
!wyoming_satellite/*.py
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY sounds/ ./sounds/
|
COPY sounds/ ./sounds/
|
||||||
COPY script/setup ./script/
|
COPY script/setup ./script/
|
||||||
COPY setup.py requirements.txt MANIFEST.in ./
|
COPY pyproject.toml ./
|
||||||
COPY wyoming_satellite/ ./wyoming_satellite/
|
COPY wyoming_satellite/ ./wyoming_satellite/
|
||||||
|
|
||||||
RUN script/setup
|
RUN script/setup
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
include requirements.txt
|
|
||||||
include VERSION
|
|
||||||
+12
-4
@@ -6,7 +6,7 @@ This tutorial should work for almost any Raspberry Pi and USB microphone. Audio
|
|||||||
|
|
||||||
## Install OS
|
## Install OS
|
||||||
|
|
||||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (Legacy, **64-bit**) Lite".
|
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (**64-bit**) Lite".
|
||||||
|
|
||||||
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
||||||
|
|
||||||
@@ -55,9 +55,7 @@ python3 -m venv .venv
|
|||||||
.venv/bin/pip3 install --upgrade wheel setuptools
|
.venv/bin/pip3 install --upgrade wheel setuptools
|
||||||
.venv/bin/pip3 install \
|
.venv/bin/pip3 install \
|
||||||
-f 'https://synesthesiam.github.io/prebuilt-apps/' \
|
-f 'https://synesthesiam.github.io/prebuilt-apps/' \
|
||||||
-r requirements.txt \
|
-e '.[all]'
|
||||||
-r requirements_audio_enhancement.txt \
|
|
||||||
-r requirements_vad.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If the installation was successful, you should be able to run:
|
If the installation was successful, you should be able to run:
|
||||||
@@ -305,6 +303,13 @@ python3 -m venv --system-site-packages .venv
|
|||||||
.venv/bin/pip3 install 'wyoming==1.5.2'
|
.venv/bin/pip3 install 'wyoming==1.5.2'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In case you use a ReSpeaker USB 4mic array v2.0, additionally install pixel-ring:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.venv/bin/pip3 install 'pixel-ring'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
The `--system-site-packages` argument is used to access the pre-installed `gpiozero` and `spidev` Python packages. If these are **not already installed** in your system, run:
|
The `--system-site-packages` argument is used to access the pre-installed `gpiozero` and `spidev` Python packages. If these are **not already installed** in your system, run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -385,4 +390,7 @@ Try a voice command and see if the LEDs change. Use `journalctl` to check the lo
|
|||||||
journalctl -u 2mic_leds.service -f
|
journalctl -u 2mic_leds.service -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you encounter any issues, you can add the `--debug` argument to the command line to increase the log level.
|
||||||
|
To control the brightness of the LEDS, use the `--led-brightness ` argument, which accepts integer numbers from 1 to 31.
|
||||||
|
|
||||||
Make sure to run `sudo systemctl daemon-reload` every time you make changes to the service.
|
Make sure to run `sudo systemctl daemon-reload` every time you make changes to the service.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Create a voice satellite using a Raspberry Pi 3+ and USB microphone and speakers
|
|||||||
|
|
||||||
## Install OS
|
## Install OS
|
||||||
|
|
||||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (Legacy, **64-bit**) Lite".
|
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (**64-bit**) Lite".
|
||||||
|
|
||||||
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
||||||
|
|
||||||
|
|||||||
@@ -43,12 +43,18 @@ mod='seeed-voicecard'
|
|||||||
src='./'
|
src='./'
|
||||||
kernel="$(uname -r)"
|
kernel="$(uname -r)"
|
||||||
marker='0.0.0'
|
marker='0.0.0'
|
||||||
|
threads="$(getconf _NPROCESSORS_ONLN)"
|
||||||
|
memory="$(LANG=C free -m|awk '/^Mem:/{print $2}')"
|
||||||
|
|
||||||
|
if [ "${memory}" -le 512 ] && [ "${threads}" -gt 2 ]; then
|
||||||
|
threads=2
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "/usr/src/${mod}-${ver}"
|
mkdir -p "/usr/src/${mod}-${ver}"
|
||||||
cp -a "${src}"/* "/usr/src/${mod}-${ver}/"
|
cp -a "${src}"/* "/usr/src/${mod}-${ver}/"
|
||||||
|
|
||||||
dkms add -m "${mod}" -v "${ver}"
|
dkms add -m "${mod}" -v "${ver}"
|
||||||
dkms build -k "${kernel}" -m "${mod}" -v "${ver}" && {
|
dkms build -k "${kernel}" -m "${mod}" -v "${ver}" -j "${threads}" && {
|
||||||
dkms install --force -k "${kernel}" -m "${mod}" -v "${ver}"
|
dkms install --force -k "${kernel}" -m "${mod}" -v "${ver}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,14 @@ async def main() -> None:
|
|||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
||||||
#
|
|
||||||
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||||
|
parser.add_argument(
|
||||||
|
"--led-brightness",
|
||||||
|
type=int,
|
||||||
|
choices=range(1, 32),
|
||||||
|
default=31,
|
||||||
|
help="LED brightness (integer from 1 to 31)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||||
@@ -54,7 +60,7 @@ async def main() -> None:
|
|||||||
led_power = gpiozero.LED(LEDS_GPIO, active_high=False)
|
led_power = gpiozero.LED(LEDS_GPIO, active_high=False)
|
||||||
led_power.on()
|
led_power.on()
|
||||||
|
|
||||||
leds = APA102(num_led=NUM_LEDS)
|
leds = APA102(num_led=NUM_LEDS, global_brightness=args.led_brightness)
|
||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
server = AsyncServer.from_uri(args.uri)
|
server = AsyncServer.from_uri(args.uri)
|
||||||
@@ -148,7 +154,7 @@ class APA102:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
num_led,
|
num_led,
|
||||||
global_brightness=MAX_BRIGHTNESS,
|
global_brightness,
|
||||||
order="rgb",
|
order="rgb",
|
||||||
bus=0,
|
bus=0,
|
||||||
device=1,
|
device=1,
|
||||||
@@ -162,6 +168,7 @@ class APA102:
|
|||||||
self.global_brightness = self.MAX_BRIGHTNESS
|
self.global_brightness = self.MAX_BRIGHTNESS
|
||||||
else:
|
else:
|
||||||
self.global_brightness = global_brightness
|
self.global_brightness = global_brightness
|
||||||
|
_LOGGER.debug("LED brightness: %d", self.global_brightness)
|
||||||
|
|
||||||
self.leds = [self.LED_START, 0, 0, 0] * self.num_led # Pixel buffer
|
self.leds = [self.LED_START, 0, 0, 0] * self.num_led # Pixel buffer
|
||||||
self.spi = spidev.SpiDev() # Init the SPI device
|
self.spi = spidev.SpiDev() # Init the SPI device
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Controls the LEDs on the ReSpeaker Mic Array v2.0 (USB) ."""
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from pixel_ring import pixel_ring
|
||||||
|
from wyoming.event import Event
|
||||||
|
from wyoming.satellite import (
|
||||||
|
SatelliteConnected,
|
||||||
|
SatelliteDisconnected,
|
||||||
|
StreamingStarted,
|
||||||
|
StreamingStopped,
|
||||||
|
)
|
||||||
|
from wyoming.server import AsyncEventHandler, AsyncServer
|
||||||
|
from wyoming.snd import Played
|
||||||
|
from wyoming.vad import VoiceStarted, VoiceStopped
|
||||||
|
from wyoming.wake import Detection
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||||
|
_LOGGER.debug(args)
|
||||||
|
|
||||||
|
_LOGGER.info("Ready")
|
||||||
|
|
||||||
|
# Turn on power to LEDs
|
||||||
|
pixel_ring.set_color_palette(0x0080FF, 0x007A37)
|
||||||
|
pixel_ring.think()
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
pixel_ring.off()
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
server = AsyncServer.from_uri(args.uri)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await server.run(partial(LEDsEventHandler, args))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
pixel_ring.off()
|
||||||
|
|
||||||
|
|
||||||
|
class LEDsEventHandler(AsyncEventHandler):
|
||||||
|
"""Event handler for clients."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cli_args: argparse.Namespace,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.cli_args = cli_args
|
||||||
|
self.client_id = str(time.monotonic_ns())
|
||||||
|
|
||||||
|
_LOGGER.debug("Client connected: %s", self.client_id)
|
||||||
|
|
||||||
|
async def handle_event(self, event: Event) -> bool:
|
||||||
|
_LOGGER.debug(event)
|
||||||
|
|
||||||
|
if Detection.is_type(event.type):
|
||||||
|
_LOGGER.debug("Detection")
|
||||||
|
pixel_ring.wakeup()
|
||||||
|
elif VoiceStarted.is_type(event.type):
|
||||||
|
_LOGGER.debug("VoiceStarted")
|
||||||
|
pixel_ring.speak()
|
||||||
|
elif VoiceStopped.is_type(event.type):
|
||||||
|
_LOGGER.debug("VoiceStopped")
|
||||||
|
pixel_ring.spin()
|
||||||
|
elif StreamingStopped.is_type(event.type):
|
||||||
|
_LOGGER.debug("StreamingStopped")
|
||||||
|
pixel_ring.off()
|
||||||
|
elif SatelliteConnected.is_type(event.type):
|
||||||
|
_LOGGER.debug("SatelliteConnected")
|
||||||
|
pixel_ring.think()
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
pixel_ring.off()
|
||||||
|
elif Played.is_type(event.type):
|
||||||
|
_LOGGER.debug("Played")
|
||||||
|
pixel_ring.off()
|
||||||
|
elif SatelliteDisconnected.is_type(event.type):
|
||||||
|
_LOGGER.debug("SatelliteDisconnected")
|
||||||
|
pixel_ring.mono(0xFF0000)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
@@ -123,7 +123,7 @@ def apply_settings(settings: Settings) -> None:
|
|||||||
):
|
):
|
||||||
result = run_with_gauge(
|
result = run_with_gauge(
|
||||||
"Installing vad...",
|
"Installing vad...",
|
||||||
[pip_install("-r", str(PROGRAM_DIR / "requirements_vad.txt"))],
|
[pip_install("-e", f"{PROGRAM_DIR}[silerovad]")],
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
error("installing vad")
|
error("installing vad")
|
||||||
@@ -135,11 +135,7 @@ def apply_settings(settings: Settings) -> None:
|
|||||||
):
|
):
|
||||||
result = run_with_gauge(
|
result = run_with_gauge(
|
||||||
"Installing audio enhancements...",
|
"Installing audio enhancements...",
|
||||||
[
|
[pip_install("-e", f"{PROGRAM_DIR}[webrtc]")],
|
||||||
pip_install(
|
|
||||||
"-r", str(PROGRAM_DIR / "requirements_audio_enhancement.txt")
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
error("installing audio enhancements")
|
error("installing audio enhancements")
|
||||||
@@ -154,7 +150,7 @@ def apply_settings(settings: Settings) -> None:
|
|||||||
) and (not can_import("gpiozero", "spidev")):
|
) and (not can_import("gpiozero", "spidev")):
|
||||||
result = run_with_gauge(
|
result = run_with_gauge(
|
||||||
"Installing event requirements...",
|
"Installing event requirements...",
|
||||||
[pip_install("-r", str(PROGRAM_DIR / "requirements_respeaker.txt"))],
|
[pip_install("-e", f"{PROGRAM_DIR}[respeaker]")],
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
error("installing event requirements")
|
error("installing event requirements")
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=62.3"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "wyoming_satellite"
|
||||||
|
version = "1.3.1"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
description = "Wyoming server for remote voice satellite"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{name = "Michael Hansen", email = "mike@rhasspy.org"}
|
||||||
|
]
|
||||||
|
keywords = ["rhasspy", "wyoming", "satellite"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.9.0"
|
||||||
|
dependencies = [
|
||||||
|
"wyoming==1.5.4",
|
||||||
|
"zeroconf==0.88.0",
|
||||||
|
"pyring-buffer>=1,<2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
all = ["pysilero-vad==1.0.0", "webrtc-noise-gain==1.2.3"]
|
||||||
|
silerovad = ["pysilero-vad==1.0.0"]
|
||||||
|
webrtc = ["webrtc-noise-gain==1.2.3"]
|
||||||
|
respeaker = ["gpiozero", "spidev"]
|
||||||
|
dev = [
|
||||||
|
"black==22.12.0",
|
||||||
|
"flake8==6.0.0",
|
||||||
|
"isort==5.11.3",
|
||||||
|
"mypy==0.991",
|
||||||
|
"pylint==2.15.9",
|
||||||
|
"pytest==7.4.3",
|
||||||
|
"pytest-asyncio==0.23.3",
|
||||||
|
"build==1.2.2"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
wyoming-satellite = "wyoming_satellite:__main__.run"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Source Code" = "http://github.com/rhasspy/wyoming-satellite"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
platforms = ["any"]
|
||||||
|
zip-safe = true
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["wyoming_satellite"]
|
||||||
|
exclude = ["tests", "tests.*"]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
wyoming==1.5.4
|
|
||||||
zeroconf==0.88.0
|
|
||||||
pyring-buffer==1.0.0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
webrtc-noise-gain==1.2.3
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
black==22.12.0
|
|
||||||
flake8==6.0.0
|
|
||||||
isort==5.11.3
|
|
||||||
mypy==0.991
|
|
||||||
pylint==2.15.9
|
|
||||||
pytest==7.4.3
|
|
||||||
pytest-asyncio==0.23.3
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
gpiozero
|
|
||||||
spidev
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pysilero-vad==1.0.0
|
|
||||||
+1
-1
@@ -8,4 +8,4 @@ _PROGRAM_DIR = _DIR.parent
|
|||||||
_VENV_DIR = _PROGRAM_DIR / ".venv"
|
_VENV_DIR = _PROGRAM_DIR / ".venv"
|
||||||
|
|
||||||
context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
|
context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
|
||||||
subprocess.check_call([context.env_exe, _PROGRAM_DIR / "setup.py", "bdist_wheel"])
|
subprocess.check_call([context.env_exe, "-m", "build", "--wheel", "--sdist"])
|
||||||
|
|||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import venv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_DIR = Path(__file__).parent
|
||||||
|
_PROGRAM_DIR = _DIR.parent
|
||||||
|
_VENV_DIR = _PROGRAM_DIR / ".venv"
|
||||||
|
|
||||||
|
context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
|
||||||
|
subprocess.check_call(
|
||||||
|
[context.env_exe, str(_PROGRAM_DIR / "examples" / "usbmic_service.py")]
|
||||||
|
+ sys.argv[1:]
|
||||||
|
)
|
||||||
+3
-4
@@ -20,7 +20,6 @@ builder.create(_VENV_DIR)
|
|||||||
# Upgrade dependencies
|
# Upgrade dependencies
|
||||||
pip = [context.env_exe, "-m", "pip"]
|
pip = [context.env_exe, "-m", "pip"]
|
||||||
subprocess.check_call(pip + ["install", "--upgrade", "pip"])
|
subprocess.check_call(pip + ["install", "--upgrade", "pip"])
|
||||||
subprocess.check_call(pip + ["install", "--upgrade", "setuptools", "wheel"])
|
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
@@ -31,13 +30,13 @@ subprocess.check_call(
|
|||||||
"https://www.piwheels.org/simple",
|
"https://www.piwheels.org/simple",
|
||||||
"-f",
|
"-f",
|
||||||
"https://synesthesiam.github.io/prebuilt-apps/",
|
"https://synesthesiam.github.io/prebuilt-apps/",
|
||||||
"-r",
|
"-e",
|
||||||
str(_PROGRAM_DIR / "requirements.txt"),
|
str(_PROGRAM_DIR),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.dev:
|
if args.dev:
|
||||||
# Install dev requirements
|
# Install dev requirements
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
pip + ["install", "-r", str(_PROGRAM_DIR / "requirements_dev.txt")]
|
pip + ["install", "-e", f"{_PROGRAM_DIR}[dev]"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import setuptools
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
this_dir = Path(__file__).parent
|
|
||||||
module_dir = this_dir / "wyoming_satellite"
|
|
||||||
version_path = module_dir / "VERSION"
|
|
||||||
version = version_path.read_text(encoding="utf-8").strip()
|
|
||||||
|
|
||||||
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="wyoming_satellite",
|
|
||||||
version="1.1.0",
|
|
||||||
description="Wyoming server for remote voice satellite",
|
|
||||||
url="http://github.com/rhasspy/wyoming-satellite",
|
|
||||||
author="Michael Hansen",
|
|
||||||
author_email="mike@rhasspy.org",
|
|
||||||
packages=setuptools.find_packages(),
|
|
||||||
package_data={
|
|
||||||
"wyoming_satellite": [str(p.relative_to(module_dir)) for p in (version_path,)]
|
|
||||||
},
|
|
||||||
install_requires=install_requires,
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 3 - Alpha",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
],
|
|
||||||
keywords="rhasspy wyoming satellite",
|
|
||||||
extras_require=extras_require,
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": ["wyoming-satellite = wyoming_satellite:__main__.run"]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[tox]
|
[tox]
|
||||||
env_list =
|
env_list =
|
||||||
py{39,310,311}
|
py{39,310,311,312,313}
|
||||||
minversion = 4.12.1
|
minversion = 4.12.1
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
1.3.0
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Voice satellite using the Wyoming protocol."""
|
"""Voice satellite using the Wyoming protocol."""
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .satellite import (
|
from .satellite import (
|
||||||
@@ -17,7 +19,7 @@ from .settings import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_DIR = Path(__file__).parent
|
_DIR = Path(__file__).parent
|
||||||
__version__ = (_DIR / "VERSION").read_text(encoding="utf-8").strip()
|
__version__ = importlib.metadata.version("wyoming-satellite")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
|
|||||||
@@ -308,14 +308,14 @@ async def main() -> None:
|
|||||||
try:
|
try:
|
||||||
import webrtc_noise_gain # noqa: F401
|
import webrtc_noise_gain # noqa: F401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LOGGER.fatal("Install extras for webrtc")
|
_LOGGER.exception("Extras for webrtc are not installed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if needs_silero(args):
|
if needs_silero(args):
|
||||||
try:
|
try:
|
||||||
import pysilero_vad # noqa: F401
|
import pysilero_vad # noqa: F401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LOGGER.fatal("Install extras for silerovad")
|
_LOGGER.exception("Extras for silerovad are not installed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.awake_wav and (not Path(args.awake_wav).is_file()):
|
if args.awake_wav and (not Path(args.awake_wav).is_file()):
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Satellite code."""
|
"""Satellite code."""
|
||||||
|
|
||||||
import array
|
import array
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
import wave
|
import wave
|
||||||
|
from dataclasses import dataclass
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Dict, Final, List, Optional, Set, Union
|
from typing import Callable, Dict, Final, List, Optional, Set, Union
|
||||||
@@ -60,6 +62,12 @@ class State(Enum):
|
|||||||
STOPPED = auto()
|
STOPPED = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SoundEvent:
|
||||||
|
event: Event
|
||||||
|
is_tts: bool
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +84,7 @@ class SatelliteBase:
|
|||||||
self._mic_task: Optional[asyncio.Task] = None
|
self._mic_task: Optional[asyncio.Task] = None
|
||||||
self._mic_webrtc: Optional[Callable[[bytes], bytes]] = None
|
self._mic_webrtc: Optional[Callable[[bytes], bytes]] = None
|
||||||
self._snd_task: Optional[asyncio.Task] = None
|
self._snd_task: Optional[asyncio.Task] = None
|
||||||
self._snd_queue: "Optional[asyncio.Queue[Event]]" = None
|
self._snd_queue: "Optional[asyncio.Queue[SoundEvent]]" = None
|
||||||
self._wake_task: Optional[asyncio.Task] = None
|
self._wake_task: Optional[asyncio.Task] = None
|
||||||
self._wake_queue: "Optional[asyncio.Queue[Event]]" = None
|
self._wake_queue: "Optional[asyncio.Queue[Event]]" = None
|
||||||
self._event_task: Optional[asyncio.Task] = None
|
self._event_task: Optional[asyncio.Task] = None
|
||||||
@@ -507,9 +515,11 @@ class SatelliteBase:
|
|||||||
event = AudioChunk(
|
event = AudioChunk(
|
||||||
rate=chunk.rate,
|
rate=chunk.rate,
|
||||||
width=chunk.width,
|
width=chunk.width,
|
||||||
channels=chunk.channels
|
channels=(
|
||||||
|
chunk.channels
|
||||||
if (self.settings.mic.channel_index is None)
|
if (self.settings.mic.channel_index is None)
|
||||||
else 1,
|
else 1
|
||||||
|
),
|
||||||
audio=audio_bytes,
|
audio=audio_bytes,
|
||||||
).event()
|
).event()
|
||||||
else:
|
else:
|
||||||
@@ -543,10 +553,10 @@ class SatelliteBase:
|
|||||||
# Sound
|
# Sound
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
async def event_to_snd(self, event: Event) -> None:
|
async def event_to_snd(self, event: Event, is_tts: bool = True) -> None:
|
||||||
"""Send an event to the sound service."""
|
"""Send an event to the sound service."""
|
||||||
if self._snd_queue is not None:
|
if self._snd_queue is not None:
|
||||||
self._snd_queue.put_nowait(event)
|
self._snd_queue.put_nowait(SoundEvent(event, is_tts))
|
||||||
|
|
||||||
def _make_snd_client(self) -> Optional[AsyncClient]:
|
def _make_snd_client(self) -> Optional[AsyncClient]:
|
||||||
"""Create client for snd service."""
|
"""Create client for snd service."""
|
||||||
@@ -581,7 +591,8 @@ class SatelliteBase:
|
|||||||
if self._snd_queue is None:
|
if self._snd_queue is None:
|
||||||
self._snd_queue = asyncio.Queue()
|
self._snd_queue = asyncio.Queue()
|
||||||
|
|
||||||
event = await self._snd_queue.get()
|
snd_event = await self._snd_queue.get()
|
||||||
|
event = snd_event.event
|
||||||
|
|
||||||
if snd_client is None:
|
if snd_client is None:
|
||||||
snd_client = self._make_snd_client()
|
snd_client = self._make_snd_client()
|
||||||
@@ -608,6 +619,7 @@ class SatelliteBase:
|
|||||||
event.type
|
event.type
|
||||||
):
|
):
|
||||||
await _disconnect()
|
await _disconnect()
|
||||||
|
if snd_event.is_tts:
|
||||||
await self.trigger_played()
|
await self.trigger_played()
|
||||||
snd_client = None # reconnect on next event
|
snd_client = None # reconnect on next event
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@@ -652,9 +664,8 @@ class SatelliteBase:
|
|||||||
for event in wav_to_events(
|
for event in wav_to_events(
|
||||||
wav_path,
|
wav_path,
|
||||||
samples_per_chunk=self.settings.snd.samples_per_chunk,
|
samples_per_chunk=self.settings.snd.samples_per_chunk,
|
||||||
volume_multiplier=self.settings.snd.volume_multiplier,
|
|
||||||
):
|
):
|
||||||
await self.event_to_snd(event)
|
await self.event_to_snd(event, is_tts=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Unmute in case of an error
|
# Unmute in case of an error
|
||||||
self.microphone_muted = False
|
self.microphone_muted = False
|
||||||
@@ -797,6 +808,13 @@ class SatelliteBase:
|
|||||||
wake_names = [w.name for w in self.settings.wake.names]
|
wake_names = [w.name for w in self.settings.wake.names]
|
||||||
|
|
||||||
await self.event_to_wake(Detect(names=wake_names).event())
|
await self.event_to_wake(Detect(names=wake_names).event())
|
||||||
|
await self.event_to_wake(
|
||||||
|
AudioStart(
|
||||||
|
rate=self.settings.mic.rate,
|
||||||
|
width=self.settings.mic.width,
|
||||||
|
channels=self.settings.mic.channels,
|
||||||
|
).event()
|
||||||
|
)
|
||||||
await self.trigger_detect()
|
await self.trigger_detect()
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user