Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mycroft-precise support #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ See `servers` section of `configuration.yaml` file.
* wake
* [porcupine1](https://github.com/Picovoice/porcupine)
* [precise-lite](https://github.com/mycroftAI/mycroft-precise)
* [mycroft-precise](https://github.com/mycroftAI/mycroft-precise)
* [snowboy](https://github.com/Kitt-AI/snowboy)
* vad
* [silero](https://github.com/snakers4/silero-vad)
Expand Down
146 changes: 146 additions & 0 deletions programs/wake/mycroft_precise/bin/precise_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import socket
from pathlib import Path

from precise_runner import PreciseEngine, PreciseRunner, ReadWriteStream

from rhasspy3.audio import AudioChunk, AudioStart, AudioStop
from rhasspy3.event import read_event, write_event
from rhasspy3.wake import Detection, NotDetected

_FILE = Path(__file__)
_DIR = _FILE.parent
_LOGGER = logging.getLogger(_FILE.stem)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("model", help="Location to .pb model file to use (with .pb.params)")
parser.add_argument(
"--socketfile", required=True, help="Path to Unix domain socket file"
)
parser.add_argument("--engine", default='.venv/bin/precise-engine', help="Path to precise-engine executable")
parser.add_argument("--chunk_size", type=int, default=2048, help="Number of *bytes* per prediction. Higher numbers decrease CPU usage but increase latency")
parser.add_argument("--trigger_level", type=int, default=3, help="Number of chunk activations needed to trigger detection event. Higher values add latency but reduce false positives")
parser.add_argument("--sensitivity", type=float, default=0.5, help="From 0.0 to 1.0, how sensitive the network should be")
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)

# Need to unlink socket if it exists
try:
os.unlink(args.socketfile)
except OSError:
pass

try:
# Create socket server
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(args.socketfile)
sock.listen()

# Load converted faster-whisper model
engine = PreciseEngine(args.engine, args.model, args.chunk_size)
stream = ReadWriteStream()

conn_file_s = None
is_detected = False
timestamp = None
name = os.path.basename(args.model).removesuffix(".pb")
sensitivity = args.sensitivity

def activation():
nonlocal is_detected, timestamp, conn_file_s, name

_LOGGER.info("Detected")

if is_detected:
return
if conn_file_s is None:
return

try:
write_event(
Detection(
name=name, timestamp=timestamp
).event(),
conn_file_s,
) # type: ignore

is_detected = True
except Exception:
pass

def prediction(prob):
nonlocal is_detected, conn_file_s

if not is_detected:
return
if conn_file_s is None:
return
if prob >= sensitivity:
return

try:
write_event(NotDetected().event(), conn_file_s) # type: ignore
except Exception:
pass

is_detected = False

runner = PreciseRunner(engine, trigger_level=args.trigger_level, sensitivity=args.sensitivity, stream=stream, on_activation=activation, on_prediction=prediction)
runner.start()
_LOGGER.info("Ready")

# Listen for connections
while True:
try:
connection, client_address = sock.accept()
_LOGGER.debug("Connection from %s", client_address)

with connection, connection.makefile(mode="rwb") as conn_file:
conn_file_s = conn_file
while True:
event = read_event(conn_file) # type: ignore
if event is None:
break

if AudioStart.is_type(event.type):
_LOGGER.debug("Receiving audio")
continue

if AudioStop.is_type(event.type):
_LOGGER.debug("Audio stopped")
break

if not AudioChunk.is_type(event.type):
continue

chunk = AudioChunk.from_event(event)

timestamp = chunk.timestamp
stream.write(chunk.audio)

if is_detected:
write_event(NotDetected().event(), conn_file) # type: ignore

except KeyboardInterrupt:
break
except Exception:
_LOGGER.exception("Error communicating with socket client")
finally:
conn_file_s = None
is_detected = False
finally:
os.unlink(args.socketfile)
runner.stop()


# -----------------------------------------------------------------------------

if __name__ == "__main__":
main()
83 changes: 83 additions & 0 deletions programs/wake/mycroft_precise/bin/precise_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import logging
import os
from pathlib import Path

from precise_runner import PreciseEngine, PreciseRunner, ReadWriteStream

from rhasspy3.audio import AudioChunk, AudioStop
from rhasspy3.event import read_event, write_event
from rhasspy3.wake import Detection, NotDetected

_FILE = Path(__file__)
_DIR = _FILE.parent
_LOGGER = logging.getLogger(_FILE.stem)

# -----------------------------------------------------------------------------


def main() -> None:
"""Main method."""
parser = argparse.ArgumentParser()
parser.add_argument("model", help="Location to .pb model file to use (with .pb.params)")
parser.add_argument("--engine", default='.venv/bin/precise-engine', help="Path to precise-engine executable")
parser.add_argument("--chunk_size", type=int, default=2048, help="Number of *bytes* per prediction. Higher numbers decrease CPU usage but increase latency")
parser.add_argument("--trigger_level", type=int, default=3, help="Number of chunk activations needed to trigger detection event. Higher values add latency but reduce false positives")
parser.add_argument("--sensitivity", type=float, default=0.5, help="From 0.0 to 1.0, how sensitive the network should be")
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)

engine = PreciseEngine(args.engine, args.model, args.chunk_size)
stream = ReadWriteStream()

is_detected = False
timestamp = None
name = os.path.basename(args.model).removesuffix(".pb")

def activation():
nonlocal is_detected, timestamp, name
try:
write_event(
Detection(
name=name, timestamp=timestamp
).event()
)
is_detected = True
except Exception:
pass


runner = PreciseRunner(engine, trigger_level=args.trigger_level, sensitivity=args.sensitivity, stream=stream, on_activation=activation)
runner.start()

try:
while True:
event = read_event()
if event is None:
break

if AudioStop.is_type(event.type):
break

if not AudioChunk.is_type(event.type):
continue

chunk = AudioChunk.from_event(event)

timestamp = chunk.timestamp
stream.write(chunk.audio)

if is_detected:
write_event(NotDetected().event())
except KeyboardInterrupt:
pass

runner.stop()

# -----------------------------------------------------------------------------

if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions programs/wake/mycroft_precise/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
git+https://github.com/MycroftAI/mycroft-precise.git@refs/pull/242/merge
61 changes: 61 additions & 0 deletions programs/wake/mycroft_precise/script/download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
import argparse
import logging
import re
import subprocess
import tarfile
from pathlib import Path
from urllib.request import urlopen

_DIR = Path(__file__).parent
_LOGGER = logging.getLogger("download")


def get_models():
_LOGGER.info("Downloading list of models...")
url = "https://github.com/MycroftAI/Precise-Community-Data.git/branches/master"
url2 = "https://github.com/MycroftAI/Precise-Community-Data/raw/master/"
filelist = subprocess.run(["svn", "ls", "-R", url], capture_output=True)
result = {}
for file in [x.decode() for x in filelist.stdout.split(b'\n')]:
if not file.endswith('.tar.gz') or not "/models/" in file:
continue
result.update({re.split('[0-9]', file)[0].split('/')[-1].removesuffix('-'): url2 + file})
return result

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--destination", help="Path to destination directory (default: data)"
)
parser.add_argument(
"--name",
help="Model name",
)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)

if args.destination:
args.destination = Path(args.destination)
else:
# Assume we're in programs/wake/mycroft_precise/script
data_dir = _DIR.parent.parent.parent.parent / "data"
args.destination = data_dir / "wake" / "mycroft_precise"

args.destination.parent.mkdir(parents=True, exist_ok=True)

models = get_models()
if args.name is None or args.name not in models:
_LOGGER.info("Available models: %s", list(models.keys()))
return

_LOGGER.info("Downloading %s", args.name)
_LOGGER.info("URL: %s", models[args.name])
with urlopen(models[args.name]) as response:
with tarfile.open(mode="r|*", fileobj=response) as tar_gz:
_LOGGER.info("Extracting to %s", args.destination)
tar_gz.extractall(args.destination)


if __name__ == "__main__":
main()
20 changes: 20 additions & 0 deletions programs/wake/mycroft_precise/script/server
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -eo pipefail

# Directory of *this* script
this_dir="$( cd "$( dirname "$0" )" && pwd )"

# Base directory of repo
base_dir="$(realpath "${this_dir}/..")"

# Path to virtual environment
: "${venv:=${base_dir}/.venv}"

if [ -d "${venv}" ]; then
source "${venv}/bin/activate"
fi

socket_dir="${base_dir}/var/run"
mkdir -p "${socket_dir}"

python3 "${base_dir}/bin/precise_server.py" --socketfile "${socket_dir}/precise.socket" "$@"
47 changes: 47 additions & 0 deletions programs/wake/mycroft_precise/script/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -eo pipefail

# Directory of *this* script
this_dir="$( cd "$( dirname "$0" )" && pwd )"

# Base directory of repo
base_dir="$(realpath "${this_dir}/..")"

# Path to virtual environment
: "${venv:=${base_dir}/.venv}"

# Python binary to use
: "${PYTHON=python3}"

python_version="$(${PYTHON} --version)"

if [ ! -d "${venv}" ]; then
# Create virtual environment
echo "Creating virtual environment at ${venv} (${python_version})"
rm -rf "${venv}"
"${PYTHON}" -m venv "${venv}"
source "${venv}/bin/activate"

pip3 install --upgrade pip
pip3 install --upgrade wheel setuptools
else
source "${venv}/bin/activate"
fi

# Install Python dependencies
echo 'Installing Python dependencies'
pip3 install -r "${base_dir}/requirements.txt"

# Download official models
URL="https://github.com/MycroftAI/precise-data.git/branches/models"
DEST="$this_dir/../../../../data/wake/mycroft_precise"
echo "Downloading models"
mkdir -p "$DEST"
for MODEL in $(svn ls "$URL" | grep '\.pb'); do
echo "$MODEL"
svn cat "$URL/$MODEL" > "$DEST/$MODEL"
done

# -----------------------------------------------------------------------------

echo "OK"
Loading