Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e26ecf82f1 | |||
| e4db52f845 | |||
| 846313e236 | |||
| b68a3fac4a | |||
| 7459f0d9d9 | |||
| 617b789d89 | |||
| bb20cd280b | |||
| ce780feb74 | |||
| 2225262a53 | |||
| 5b5529339b | |||
| 90f5c5aef7 | |||
| 78f263582d | |||
| 6c8608f1a1 | |||
| 3e5437856b | |||
| 15aaea2810 | |||
| 61d8930e38 | |||
| 5748b2dc3a | |||
| 8f7158f7cc | |||
| 97226286e3 | |||
| 896b3ddfba | |||
| 1772f6e740 | |||
| b5dfd6518b | |||
| 2730c131d0 | |||
| 05ded030c8 | |||
| 3b90383145 | |||
| 1bb5462150 | |||
| 95a354e2a3 | |||
| d203a3ed75 | |||
| 59d473b931 | |||
| 17737f7fed | |||
| 76cf173849 | |||
| 55d1cfacdd | |||
| 4f6d02169c | |||
| 74761b942f | |||
| b88acb3a34 | |||
| 7b323a08bb | |||
| ec55dbfa5b | |||
| 15af0ae3c1 | |||
| f8542f7ac1 | |||
| de67b3318c | |||
| b47dca03aa | |||
| 89a1921c3e | |||
| e5fe2a31b3 | |||
| afdd241c57 | |||
| bea38cc64f | |||
| c2562aa674 | |||
| 7dec472ec4 | |||
| 007ea4266e | |||
| a627f8746c | |||
| 13f183afd4 | |||
| 130cbeb7a8 | |||
| 358e7b087e | |||
| 8e2d2f2352 | |||
| ac3c92e24a | |||
| a501c52954 | |||
| f8f0b48140 | |||
| 2a8972fb99 | |||
| cbbfc23395 | |||
| 0c2a1931f6 | |||
| 414457f150 | |||
| 640be7b0ac | |||
| 421f59518a |
@@ -81,9 +81,7 @@ g2p: $(G2P_MODELS)
|
||||
# Testing
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
mypy:
|
||||
mypy app.py rhasspy
|
||||
|
||||
check:
|
||||
flake8 --exclude=lexconvert.py app.py test.py rhasspy/*.py
|
||||
pylint --ignore=lexconvert.py app.py test.py rhasspy/*.py
|
||||
mypy app.py test.py rhasspy/*.py
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
defaults:
|
||||
-
|
||||
scope:
|
||||
path: ""
|
||||
values:
|
||||
render_with_liquid: false
|
||||
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import atexit
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -12,6 +13,7 @@ from typing import Any, Dict, List, Tuple, Union
|
||||
from uuid import uuid4
|
||||
|
||||
import attr
|
||||
import json5
|
||||
from quart import (
|
||||
Quart,
|
||||
Response,
|
||||
@@ -27,8 +29,7 @@ from swagger_ui import quart_api_doc
|
||||
|
||||
from rhasspy.actor import ActorSystem, ConfigureEvent, RhasspyActor
|
||||
from rhasspy.core import RhasspyCore
|
||||
from rhasspy.dialogue import ProfileTrainingFailed
|
||||
from rhasspy.intent import IntentRecognized
|
||||
from rhasspy.events import IntentRecognized, ProfileTrainingFailed
|
||||
from rhasspy.utils import (
|
||||
FunctionLoggingHandler,
|
||||
buffer_to_wav,
|
||||
@@ -185,7 +186,7 @@ async def api_profiles() -> Response:
|
||||
return jsonify(
|
||||
{
|
||||
"default_profile": core.profile.name,
|
||||
"profiles": sorted(list(profile_names)),
|
||||
"profiles": sorted(profile_names),
|
||||
"downloaded": downloaded,
|
||||
"missing_files": missing_files,
|
||||
}
|
||||
@@ -295,14 +296,14 @@ async def api_profile() -> Union[str, Response]:
|
||||
|
||||
if request.method == "POST":
|
||||
# Ensure that JSON is valid
|
||||
profile_json = await request.json
|
||||
profile_json = json5.loads(await request.data)
|
||||
recursive_remove(core.profile.system_json, profile_json)
|
||||
|
||||
profile_path = Path(core.profile.write_path("profile.json"))
|
||||
with open(profile_path, "w") as profile_file:
|
||||
json.dump(profile_json, profile_file, indent=4)
|
||||
|
||||
msg = "Wrote profile to %s" % profile_path
|
||||
msg = f"Wrote profile to {profile_path}"
|
||||
logger.debug(msg)
|
||||
return msg
|
||||
|
||||
@@ -441,7 +442,7 @@ async def api_sentences():
|
||||
num_chars = 0
|
||||
paths_written = []
|
||||
|
||||
sentences_dict = await request.json
|
||||
sentences_dict = json5.loads(await request.data)
|
||||
for sentences_path, sentences_text in sentences_dict.items():
|
||||
# Path is relative to profile directory
|
||||
sentences_path = Path(core.profile.write_path(sentences_path))
|
||||
@@ -460,9 +461,7 @@ async def api_sentences():
|
||||
logger.debug("Removing %s", sentences_path)
|
||||
sentences_path.unlink()
|
||||
|
||||
return "Wrote {} char(s) to {}".format(
|
||||
num_chars, [str(p) for p in paths_written]
|
||||
)
|
||||
return f"Wrote {num_chars} char(s) to {[str(p) for p in paths_written]}"
|
||||
|
||||
# Update sentences.ini only
|
||||
sentences_path = Path(
|
||||
@@ -472,7 +471,7 @@ async def api_sentences():
|
||||
data = await request.data
|
||||
with open(sentences_path, "wb") as sentences_file:
|
||||
sentences_file.write(data)
|
||||
return "Wrote {} byte(s) to {}".format(len(data), sentences_path)
|
||||
return f"Wrote {len(data)} byte(s) to {sentences_path}"
|
||||
|
||||
# GET
|
||||
sentences_path_rel = core.profile.read_path(
|
||||
@@ -547,7 +546,7 @@ async def api_custom_words():
|
||||
print(line, file=custom_words_file)
|
||||
lines_written += 1
|
||||
|
||||
return "Wrote %s line(s) to %s" % (lines_written, custom_words_path)
|
||||
return f"Wrote {lines_written} line(s) to {custom_words_path}"
|
||||
|
||||
custom_words_path = Path(
|
||||
core.profile.read_path(
|
||||
@@ -794,12 +793,13 @@ async def api_text_to_speech() -> Union[bytes, str]:
|
||||
play = request.args.get("play", "true").strip().lower() == "true"
|
||||
language = request.args.get("language")
|
||||
voice = request.args.get("voice")
|
||||
siteId = request.args.get("siteId")
|
||||
data = await request.data
|
||||
sentence = last_sentence if repeat else data.decode().strip()
|
||||
|
||||
assert core is not None
|
||||
result = await core.speak_sentence(
|
||||
sentence, play=play, language=language, voice=voice
|
||||
sentence, play=play, language=language, voice=voice, siteId=siteId
|
||||
)
|
||||
|
||||
last_sentence = sentence
|
||||
@@ -821,7 +821,17 @@ async def api_slots() -> Union[str, Response]:
|
||||
|
||||
if request.method == "POST":
|
||||
overwrite_all = request.args.get("overwrite_all", "false").lower() == "true"
|
||||
new_slot_values = await request.json
|
||||
new_slot_values = json5.loads(await request.data)
|
||||
|
||||
word_casing = core.profile.get(
|
||||
"speech_to_text.dictionary_casing", "ignore"
|
||||
).lower()
|
||||
word_transform = lambda s: s
|
||||
|
||||
if word_casing == "lower":
|
||||
word_transform = str.lower
|
||||
elif word_casing == "upper":
|
||||
word_transform = str.upper
|
||||
|
||||
slots_dir = Path(
|
||||
core.profile.write_path(
|
||||
@@ -831,7 +841,7 @@ async def api_slots() -> Union[str, Response]:
|
||||
|
||||
if overwrite_all:
|
||||
# Remote existing values first
|
||||
for name in new_slot_values.keys():
|
||||
for name in new_slot_values:
|
||||
slots_path = safe_join(slots_dir, f"{name}")
|
||||
if slots_path.is_file():
|
||||
try:
|
||||
@@ -849,15 +859,17 @@ async def api_slots() -> Union[str, Response]:
|
||||
slots_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Merge with existing values
|
||||
values = set(values)
|
||||
values = {word_transform(v.strip()) for v in values}
|
||||
if slots_path.is_file():
|
||||
values.update(line for line in slots_path.read_text().splitlines())
|
||||
values.update(
|
||||
word_transform(line.strip())
|
||||
for line in slots_path.read_text().splitlines()
|
||||
)
|
||||
|
||||
# Write merged values
|
||||
if values:
|
||||
with open(slots_path, "w") as slots_file:
|
||||
for value in values:
|
||||
value = value.strip()
|
||||
if value:
|
||||
print(value, file=slots_file)
|
||||
|
||||
@@ -1138,6 +1150,8 @@ async def api_events_log() -> None:
|
||||
while True:
|
||||
text = await q.get()
|
||||
await websocket.send(text)
|
||||
except concurrent.futures.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("api_events_log")
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ def main():
|
||||
|
||||
# Load dictionary
|
||||
word_dict = {}
|
||||
logging.info("Loading dictionary from %s" % args.dictionary)
|
||||
logging.info("Loading dictionary from %s", args.dictionary)
|
||||
with open(args.dictionary, "r") as dict_file:
|
||||
read_dict(dict_file, word_dict)
|
||||
|
||||
@@ -53,7 +53,7 @@ def main():
|
||||
all_words.append(word)
|
||||
|
||||
assert len(phonemes) == len(phoneme_words), "Not enough words to cover phonemes"
|
||||
logging.debug("Phonemes: %s" % ", ".join(phoneme_words.keys()))
|
||||
logging.debug("Phonemes: %s", ", ".join(phoneme_words))
|
||||
|
||||
phoneme_hyps = defaultdict(lambda: defaultdict(float))
|
||||
|
||||
@@ -66,7 +66,7 @@ def main():
|
||||
phoneme_hyps[phoneme][hyp] = count
|
||||
|
||||
# Sample words from the dictionary
|
||||
logging.info("Starting %s sample(s)" % args.samples)
|
||||
logging.info("Starting %s sample(s)", args.samples)
|
||||
phoneme_futures = {}
|
||||
with ProcessPoolExecutor() as executor:
|
||||
# Schedule eSpeak word samples
|
||||
@@ -80,7 +80,7 @@ def main():
|
||||
for i, future in enumerate(as_completed(phoneme_futures)):
|
||||
if i % len(phonemes) == 0:
|
||||
logging.info(
|
||||
"Sample %s of %s" % ((i // len(phonemes) + 1), args.samples)
|
||||
"Sample %s of %s", (i // len(phonemes) + 1), args.samples
|
||||
)
|
||||
|
||||
phoneme = phoneme_futures[future]
|
||||
@@ -113,14 +113,14 @@ def main():
|
||||
best = {}
|
||||
todo = set(phonemes)
|
||||
used = set()
|
||||
while len(todo) > 0:
|
||||
while todo:
|
||||
for phoneme in list(todo):
|
||||
best_to_worst = sorted(
|
||||
phoneme_hyps[phoneme].items(), key=lambda kv: kv[1], reverse=True
|
||||
)
|
||||
|
||||
for hyp, count in best_to_worst:
|
||||
if not hyp in used:
|
||||
if hyp not in used:
|
||||
best[phoneme] = hyp
|
||||
used.add(hyp)
|
||||
todo.remove(phoneme)
|
||||
@@ -165,7 +165,7 @@ def read_dict(dict_file, word_dict):
|
||||
"""
|
||||
for line in dict_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
word, pronounce = re.split("[ ]+", line, maxsplit=1)
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import calendar
|
||||
import json
|
||||
import locale
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("generate-slots")
|
||||
parser.add_argument("profiles_dir")
|
||||
args = parser.parse_args()
|
||||
|
||||
for profile_dir in Path(args.profiles_dir).glob("*"):
|
||||
if not profile_dir.is_dir():
|
||||
continue
|
||||
|
||||
with open(profile_dir / "profile.json", "r") as profile_file:
|
||||
profile = json.load(profile_file)
|
||||
locale_name = profile["locale"] + ".UTF-8"
|
||||
locale.setlocale(locale.LC_ALL, locale_name)
|
||||
|
||||
slots_dir = profile_dir / "slots" / "rhasspy"
|
||||
slots_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Day names
|
||||
with open(slots_dir / "days", "w") as days_file:
|
||||
for day_num in range(7):
|
||||
print(calendar.day_name[day_num], file=days_file)
|
||||
|
||||
# Month names
|
||||
with open(slots_dir / "months", "w") as month_file:
|
||||
for month_num in range(1, 13):
|
||||
print(calendar.month_name[month_num], file=month_file)
|
||||
|
||||
print(locale_name)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -38,9 +38,7 @@ def main():
|
||||
|
||||
if not os.path.exists(html_path):
|
||||
# Download
|
||||
url = "https://www.ezglot.com/most-frequently-used-words.php?l={0}&submit=Select".format(
|
||||
language
|
||||
)
|
||||
url = f"https://www.ezglot.com/most-frequently-used-words.php?l={language}&submit=Select"
|
||||
print(f"Downloading from {url}")
|
||||
|
||||
with open(html_path, "w") as html_file:
|
||||
|
||||
@@ -26,7 +26,7 @@ def main():
|
||||
with open(args.dictionary, "r") as dict_file:
|
||||
for line in dict_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = re.split(r"[\t ]+", line)
|
||||
@@ -44,11 +44,11 @@ def main():
|
||||
|
||||
# Pick unique example words for every phoneme
|
||||
used_words = set()
|
||||
for phoneme in sorted(examples.keys()):
|
||||
for phoneme in sorted(examples):
|
||||
# Choose the shortest, unused example word for this phoneme.
|
||||
# Exclude words with 3 or fewer letters.
|
||||
for word, pron in sorted(examples[phoneme], key=lambda kv: len(kv[0])):
|
||||
if len(word) > 3 and (not word in used_words):
|
||||
if len(word) > 3 and (word not in used_words):
|
||||
# Output format is:
|
||||
# phoneme word pronunciation
|
||||
print(phoneme, word, " ".join(pron))
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ def main():
|
||||
with open(args.dictionary, "r") as dict_file:
|
||||
for line in dict_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = re.split(r"[\t ]+", line)
|
||||
@@ -70,7 +70,7 @@ def main():
|
||||
with open(args.frequent_phones, "r") as freq_phones_file:
|
||||
for line in freq_phones_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = re.split(r"[ ]+", line, maxsplit=1)
|
||||
@@ -82,7 +82,7 @@ def main():
|
||||
mappings = []
|
||||
bad_espeak = (":", ";", "-", "#")
|
||||
for word, espeak in freq_espeak.items():
|
||||
if not word in freq_phonemes:
|
||||
if word not in freq_phonemes:
|
||||
# No pronunciation
|
||||
continue
|
||||
|
||||
@@ -134,7 +134,7 @@ def main():
|
||||
m = 4
|
||||
for p in all_phonemes:
|
||||
candidate_counts = [
|
||||
(e, phoneme_counts[(cp, e)]) for (cp, e) in phoneme_counts.keys() if cp == p
|
||||
(e, phoneme_counts[(cp, e)]) for (cp, e) in phoneme_counts if cp == p
|
||||
]
|
||||
candidate_counts = [ec for ec in candidate_counts if ec[1] > n]
|
||||
candidate_counts = sorted(candidate_counts, key=lambda x: x[1], reverse=True)
|
||||
@@ -213,7 +213,7 @@ assign(P, E) :- maybe_assign(P, E).
|
||||
predicates = []
|
||||
for line in proc.stdout.splitlines():
|
||||
line = line.decode().strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
elif line.startswith("OPTIMUM FOUND"):
|
||||
break
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ def main():
|
||||
with open(dict_path, "r") as dict_file:
|
||||
for line in dict_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = re.split(r"[ ]+", line)
|
||||
|
||||
@@ -12,7 +12,7 @@ def main():
|
||||
with open(sys.argv[1], "r") as dict_file:
|
||||
for line in dict_file:
|
||||
line = line.strip()
|
||||
if len(line) == 0:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = re.split(r"[ ]+", line)
|
||||
|
||||
@@ -128,7 +128,7 @@ if [[ -z "${no_system}" ]]; then
|
||||
echo "Installing system dependencies"
|
||||
|
||||
run_sudo apt-get update
|
||||
run_sudo apt-get install --no-install-recommends --yes \
|
||||
run_sudo apt-get install --no-install-recommends \
|
||||
python3 python3-pip python3-venv python3-dev \
|
||||
python \
|
||||
build-essential autoconf autoconf-archive libtool automake bison \
|
||||
@@ -183,7 +183,7 @@ if [[ ! -d "${phonetisaurus_dir}/build" ]]; then
|
||||
phonetisaurus_file="${download_dir}/phonetisaurus-2019.tar.gz"
|
||||
|
||||
if [[ ! -s "${phonetisaurus_file}" ]]; then
|
||||
phonetisaurus_url='https://github.com/synesthesiam/phonetisaurus-2019/releases/download/v1.0/phonetisaurus-2019.tar.gz'
|
||||
phonetisaurus_url='https://github.com/synesthesiam/docker-phonetisaurus/raw/master/download/phonetisaurus-2019.tar.gz'
|
||||
echo "Downloading phonetisaurus (${phonetisaurus_url})"
|
||||
maybe_download "${phonetisaurus_url}" "${phonetisaurus_file}"
|
||||
fi
|
||||
@@ -191,7 +191,7 @@ fi
|
||||
|
||||
# Kaldi
|
||||
kaldi_dir="${this_dir}/opt/kaldi"
|
||||
if [[ ! -d "${kaldi_dir}" ]]; then
|
||||
if [[ -z "${no_kaldi}" && ! -d "${kaldi_dir}" ]]; then
|
||||
install libatlas-base-dev libatlas3-base gfortran
|
||||
run_sudo ldconfig
|
||||
kaldi_file="${download_dir}/kaldi-2019.tar.gz"
|
||||
@@ -398,5 +398,5 @@ esac
|
||||
|
||||
if [[ -z "${no_web}" ]]; then
|
||||
echo "Building web interface"
|
||||
cd "${this_dir}" && yarn build
|
||||
cd "${this_dir}" && yarn install && yarn build
|
||||
fi
|
||||
|
||||
+2
-2
@@ -95,7 +95,7 @@ function maybe_download {
|
||||
if [[ -z "${no_system}" ]]; then
|
||||
echo "Installing system dependencies"
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends --yes \
|
||||
sudo apt-get install --no-install-recommends \
|
||||
python3 python3-pip python3-venv python3-dev \
|
||||
python \
|
||||
build-essential autoconf autoconf-archive libtool automake bison \
|
||||
@@ -122,7 +122,7 @@ if [[ -z "${FLAGS_python}" ]]; then
|
||||
PYTHON='python3.6'
|
||||
else
|
||||
echo "Installing Python 3.6 from source. This is going to take a LONG time."
|
||||
sudo apt-get install --no-install-recommends --yes \
|
||||
sudo apt-get install --no-install-recommends \
|
||||
tk-dev libncurses5-dev libncursesw5-dev \
|
||||
libreadline6-dev libdb5.3-dev libgdbm-dev \
|
||||
libsqlite3-dev libssl-dev libbz2-dev \
|
||||
|
||||
@@ -36,7 +36,7 @@ Add to your [profile](profiles.md):
|
||||
This system listens for up to `timeout_sec` for a voice command. The first few frames of audio data are discarded (`throwaway_buffers`) to avoid clicks from the microphone being engaged. When speech is detected for some number of successive frames (`speech_buffers`), the voice command is considered to have *started*. After `min_sec`, Rhasspy will start listening for silence. If at least `silence_sec` goes by without any speech detected, the command is considered *finished*, and the recorded WAV data is sent to the [speech recognition system](speech-to-text.md).
|
||||
|
||||
You may want to adjust `min_sec`, `silence_sec`, and `vad_mode` for your environment.
|
||||
These control how short a voice command can be (`min_sec`), how much silence is required before Rhasspy stops listening (`silence_sec`), and how sensitive the voice activity detector is (`vad_mode`, higher is more sensitive).
|
||||
These control how short a voice command can be (`min_sec`), how much silence is required before Rhasspy stops listening (`silence_sec`), and how aggressive the voice activity filter `vad_mode` is: this is an integer between 0 and 3. 0 is the least aggressive about filtering out non-speech, 3 is the most aggressive.
|
||||
|
||||
**NOTE**: you must set `chunk_size` such that (relative to sample rate) it produces 10, 20, or 30 millisecond buffers. This is required by `webrtcvad`.
|
||||
|
||||
|
||||
+43
-6
@@ -46,10 +46,19 @@ If you're using [docker compose](https://docs.docker.com/compose/), add the foll
|
||||
devices:
|
||||
- "/dev/snd:/dev/snd"
|
||||
command: --user-profiles /profiles --profile en
|
||||
|
||||
### Updating Docker Image
|
||||
|
||||
To update your Rhasspy Docker image, just run:
|
||||
|
||||
```bash
|
||||
docker pull synesthesiam/rhasspy-server:latest
|
||||
```
|
||||
on your Rhasspy server and restart the Docker container.
|
||||
|
||||
## Hass.io
|
||||
|
||||
The second easiest was to install Rhasspy is as a [Hass.io add-on](https://www.home-assistant.io/addons/). Following the [installation instructions for Hass.io](https://www.home-assistant.io/hassio/installation/) before proceeding.
|
||||
The second easiest way to install Rhasspy is as a [Hass.io add-on](https://www.home-assistant.io/addons/). Follow the [installation instructions for Hass.io](https://www.home-assistant.io/hassio/installation/) before proceeding.
|
||||
|
||||
To install the add-on, add my [Hass.IO Add-On Repository](https://github.com/synesthesiam/hassio-addons) in the Add-On Store, refresh, then install the "Rhasspy Assistant" under “Synesthesiam Hass.IO Add-Ons” (all the way at the bottom of the Add-On Store screen).
|
||||
|
||||
@@ -63,24 +72,48 @@ Before starting the add-on, make sure to give it access to your microphone and s
|
||||
|
||||

|
||||
|
||||
### Updating Hass.IO Add-On
|
||||
|
||||
You should receive notifications when a new version of Rhasspy is available for Hass.IO. Follow the instructions from Hass.IO on how to update the add-on.
|
||||
|
||||
## Virtual Environment
|
||||
|
||||
Rhasspy can be installed into a Python virtual environment, though there are a number of requirements. This may be desirable, however, if you have trouble getting Rhasspy to access your microphone from within a Docker container. To start, clone the repo somewhere:
|
||||
|
||||
git clone https://github.com/synesthesiam/rhasspy.git
|
||||
```bash
|
||||
git clone https://github.com/synesthesiam/rhasspy.git
|
||||
```
|
||||
|
||||
Then run the `download-dependencies.sh` and `create-venv.sh` scripts (assumes a Debian distribution):
|
||||
|
||||
cd rhasspy/
|
||||
./download-dependencies.sh
|
||||
./create-venv.sh
|
||||
```bash
|
||||
cd rhasspy/
|
||||
./download-dependencies.sh
|
||||
./create-venv.sh
|
||||
```
|
||||
|
||||
Once the installation finishes (5-10 minutes on a Raspberry Pi 3), you can use the `run-venv.sh` script to start Rhasspy:
|
||||
|
||||
./run-venv.sh --profile en
|
||||
```bash
|
||||
./run-venv.sh --profile en
|
||||
```
|
||||
|
||||
If all is well, the web interface will be available at [http://localhost:12101](http://localhost:12101)
|
||||
|
||||
### Updating Virtual Environment
|
||||
|
||||
To update your Rhasspy virtual environment to the latest version, run:
|
||||
|
||||
```bash
|
||||
git pull origin master
|
||||
```
|
||||
|
||||
in your `rhasspy` directory. You should also re-build the web interface:
|
||||
|
||||
1. Install [yarn](https://yarnpkg.com) on your system
|
||||
2. Run `yarn build` in the `rhasspy` directory
|
||||
3. Restart any running instances of Rhasspy
|
||||
|
||||
### Running as a Service
|
||||
|
||||
Once installed, Rhasspy can be run as a [systemd service](https://systemd.io/). An [example unit file](https://github.com/synesthesiam/rhasspy/blob/master/etc/rhasspy.service) is available (thanks [UnderpantsGnome](https://github.com/UnderpantsGnome)):
|
||||
@@ -151,4 +184,8 @@ On low memory devices like the Raspberry Pi, building the tools above can quickl
|
||||
|
||||
You can skip building Kaldi if you plan to just [use Pocketsphinx](speech-to-text.md#pocketsphinx) for speech recognition.
|
||||
|
||||
### Updating Source Install
|
||||
|
||||
Follow the same instructions as [updating a virtual environment](#updating-virtual-environment).
|
||||
|
||||
|
||||
|
||||
+8
-1
@@ -84,6 +84,10 @@ Application authors may want to use the [rhasspy-client](https://pypi.org/projec
|
||||
* POST a WAV file and have Rhasspy process it as a voice command
|
||||
* Returns intent JSON when command is finished
|
||||
* `?nohass=true` - stop Rhasspy from handling the intent
|
||||
* `/api/speech-to-text`
|
||||
* POST a WAV file and have Rhasspy return the text transcription
|
||||
* Set `Accept: application/json` to receive JSON with more details
|
||||
* `?noheader=true` - send raw 16-bit 16Khz mono audio without a WAV header
|
||||
* `/api/start-recording`
|
||||
* POST to have Rhasspy start recording a voice command
|
||||
* `/api/stop-recording`
|
||||
@@ -425,12 +429,14 @@ All available profile sections and settings are listed below:
|
||||
* `g2p_model` - finite-state transducer for phonetisaurus to guess word pronunciations
|
||||
* `g2p_casing` - casing to force for g2p model (`upper`, `lower`, or blank)
|
||||
* `dictionary_casing` - casing to force for dictionary words (`upper`, `lower`, or blank)
|
||||
* `grammars_dir` - directory to write generated JSGF grammars from sentences ini file
|
||||
* `slots_dir` - directory to look for [slots lists](training.md#slots-lists) (default: `slots`)
|
||||
* `slot_programs` - directory to look for [slot programs](training.md#slot-programs) (default `slot_programs`)
|
||||
* `fsts_dir` - directory to write generated finite state transducers from JSGF grammars
|
||||
* `intent` - transforming text commands to intents
|
||||
* `system` - intent recognition system (`fsticuffs`, `fuzzywuzzy`, `rasa`, `remote`, `adapt`, `command`, or `dummy`)
|
||||
* `fsticuffs` - configuration for [OpenFST-based](https://www.openfst.org) intent recognizer
|
||||
* `intent_fst` - path to generated finite state transducer with all intents combined
|
||||
* `converters_dir` - directory to look for [converter](training.md#converters) programs (default: `converters`)
|
||||
* `ignore_unknown_words` - true if words not in the FST symbol table should be ignored
|
||||
* `fuzzy` - true if text is matching in a fuzzy manner, skipping words in `stop_words.txt`
|
||||
* `fuzzywuzzy` - configuration for simplistic [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) based intent recognizer
|
||||
@@ -447,6 +453,7 @@ All available profile sections and settings are listed below:
|
||||
* `command` - configuration for external speech-to-text program
|
||||
* `program` - path to executable
|
||||
* `arguments` - list of arguments to pass to program
|
||||
* `replace_numbers` if true, automatically replace number ranges (`N..M`) or numbers (`N`) with words
|
||||
* `text_to_speech` - pronouncing words
|
||||
* `system` - text to speech system (`espeak`, `flite`, `picotts`, `marytts`, `command`, or `dummy`)
|
||||
* `espeak` - configuration for [eSpeak](http://espeak.sourceforge.net)
|
||||
|
||||
+89
-1
@@ -5,7 +5,11 @@ Rhasspy is designed to recognize voice commands [in a template language](#senten
|
||||
* Intent Recognition
|
||||
* [Basic Syntax](#basic-syntax)
|
||||
* [Named Entities](#tags)
|
||||
* [Number Ranges](#number-ranges)
|
||||
* [Slots](#slots-lists)
|
||||
* [Slot Synonyms](#slot-synonyms)
|
||||
* [Slot Programs](#slot-programs)
|
||||
* [Converters](#converters)
|
||||
* Speech Recognition
|
||||
* [Custom Words](#custom-words)
|
||||
* [Language Model Mixing](#language-model-mixing)
|
||||
@@ -156,6 +160,24 @@ You can **share rules** across intents by referencing them as `<IntentName.rule_
|
||||
|
||||
The second intent (`GetLightColor`) references the `colors` rule from `SetLightColor`. Rule references without a dot must exist in the current intent.
|
||||
|
||||
### Number Ranges
|
||||
|
||||
Rhasspy supports using number literals (`75`) and number ranges (`1..10`) directly in your sentence templates. During training, the [num2words](https://pypi.org/project/num2words) package is used to generate words that the speech recognizer can handle ("seventy five"). For example:
|
||||
|
||||
```
|
||||
[SetBrightness]
|
||||
set brightness to (0..100){brightness}
|
||||
```
|
||||
|
||||
The `brightness` property of the recognized `SetBrightness` intent will automatically be [converted](#converters) to an integer for you. You can optionally add a step to the integer range:
|
||||
|
||||
```
|
||||
evens = 0..100,2
|
||||
odds = 1..100,2
|
||||
```
|
||||
|
||||
Under the hood, number ranges are actually references to the `rhasspy/number` [slot program](#slot-programs). You can override this behavior by creating your `slot_programs/rhasspy/number` program or disable it entirely by setting `intent.replace_numbers` to `false` in [your profile](profiles.md).
|
||||
|
||||
### Slots Lists
|
||||
|
||||
Large [alternatives](#alternatives) can become unwieldy quickly. For example, say you have a list of movie names:
|
||||
@@ -185,7 +207,11 @@ play ($movies){movie_name}
|
||||
|
||||
When matched, the `PlayMovie` intent JSON will contain `movie_name` property with either "Primer", "Moon", etc.
|
||||
|
||||
Make sure to **re-train** Rhasspy whenever you update your slot values.
|
||||
Make sure to **re-train** Rhasspy whenever you update your slot values!
|
||||
|
||||
#### Slot Directories
|
||||
|
||||
Slot files can be put in **sub-directories** under `slots`. A list in `slots/foo/bar` should be referenced in `sentences.ini` as `$foo/bar`.
|
||||
|
||||
#### Slot Synonyms
|
||||
|
||||
@@ -206,6 +232,68 @@ which is referenced by `$rooms` and will match:
|
||||
|
||||
This will always output just "den" because `[the:]` optionally matches "the" and then drops the word.
|
||||
|
||||
#### Slot Programs
|
||||
|
||||
Slot lists are great if your slot values always stay the same and are easily written out by hand. If you have slot values that you need to be generated *each time Rhasspy is trained*, you can use slot programs.
|
||||
|
||||
Create a directory named `slot_programs` in your profile (e.g., `$HOME/.config/rhasspy/profiles/en/slot_programs`):
|
||||
|
||||
```bash
|
||||
slot_programs="${HOME}/.config/rhasspy/profiles/en/slot_programs"
|
||||
mkdir -p "${slot_programs}"
|
||||
```
|
||||
|
||||
Add a file in `slot_programs` with the name of your slot, e.g. `colors`. Write a program in this file, such as a bash script. Make sure to include the [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) and mark the file as executable:
|
||||
|
||||
```bash
|
||||
cat <<EOF > "${slot_programs}/colors"
|
||||
#/usr/bin/env bash
|
||||
echo 'red'
|
||||
echo 'green'
|
||||
echo 'blue'
|
||||
EOF
|
||||
|
||||
chmod +x "${slot_programs}/colors"
|
||||
```
|
||||
|
||||
Now, when you reference `$colors` in your `sentences.ini`, Rhasspy will run the program you wrote and collect the slot values from each line. Note that you can output all the same things as regular [slots lists](#slots-lists), including optional words, alternatives, etc.
|
||||
|
||||
You can pass **arguments** to your program using the syntax `$name,arg1,arg2,...` in `sentences.ini` (no spaces). Arguments will be pass on the command-line, so `arg1` and `arg2` will be `$1` and `$2` in a bash script.
|
||||
|
||||
Like regular slots lists, slot programs can also be put in sub-directories under `slot_programs`. A program in `slot_programs/foo/bar` should be referenced in `sentences.ini` as `$foo/bar`.
|
||||
|
||||
### Converters
|
||||
|
||||
By default, all named entity values in a recognized intent's JSON are strings. If you need a different data type, such as an integer or float, or want to do some kind of complex *conversion*, use a converter:
|
||||
|
||||
```
|
||||
[SetBrightness]
|
||||
set brightness to (low:0 | medium:0.5 | high:1){brightness!float}
|
||||
```
|
||||
|
||||
The `!name` syntax calls a converter by name. Rhasspy includes several built-in converters:
|
||||
|
||||
* int - convert to integer
|
||||
* float - convert to real
|
||||
* bool - convert to boolean
|
||||
* lower - lower-case
|
||||
* upper - upper-case
|
||||
|
||||
You can define your own converters by placing a file in the `converters` directory of your profile. Like [slot programs](#slot-programs), this file should contain a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) and be marked as executable (`chmod +x`). A file named `converters/foo/bar` should be referenced as `!foo/bar` in `sentences.ini`.
|
||||
|
||||
Your custom converter will receive the value to convert on standard in (`stdin`) encoded as JSON. You should print a converted JSON value to standard out `stdout`. The example below demonstrates converting a string value into an integer:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import json
|
||||
|
||||
value = json.load(sys.stdin)
|
||||
print(int(value))
|
||||
```
|
||||
|
||||
Converters can be *chained*, so `!foo!bar` will call the `foo` converter and then pass the result to `bar`.
|
||||
|
||||
### Special Cases
|
||||
|
||||
If one of your sentences happens to start with an optional word (e.g., `[the]`), this can lead to a problem:
|
||||
|
||||
+90
-5
@@ -63,7 +63,90 @@ You can now fill in the rest of the Home Assistant automation:
|
||||
rgb_color: [255, 0, 0]
|
||||
entity_id: light.bedroom
|
||||
|
||||
This will handle the specific case of setting the bedroom light to red, but not any other color. You can either add additional automations to handle these, or make use of [automation templating](https://www.home-assistant.io/docs/automation/templating/) to do it all at once.
|
||||
This will handle the specific case of setting the bedroom light to red, but not any other color. You can either add additional automations to handle these, or make use of [automation templating](https://www.home-assistant.io/docs/automation/templating/) to do it all at once. [Home Assistant Template Example](Home-Assistant-Template-Example)
|
||||
|
||||
### Home Assistant Template Example
|
||||
|
||||
Using the following additions, you can get Home Assistant to respond to turning on / off *ANY* light in your setup.
|
||||
|
||||
#### Slots
|
||||
|
||||
Add the following JSON to the Slots tab in your Rhasspy web interface:
|
||||
|
||||
```json
|
||||
{
|
||||
"lights": [
|
||||
"(living room wall):light.bulb_3",
|
||||
"(living room desk):switch.m4",
|
||||
"(living room floor):switch.sonoff",
|
||||
"(bar lights):switch.maxcio1",
|
||||
"(entry wall):light.bulb_4",
|
||||
"(guest wall):light.bulb_6",
|
||||
"(guest floor):switch.m5",
|
||||
"(bedroom wall):light.bulb_5",
|
||||
"(bedroom desk):light.bulb_1",
|
||||
"(bedroom floor):light.bulb_2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Sentences
|
||||
|
||||
A simple sentence to turn any of the lights in the slots file on or off.
|
||||
Note the use of the `<state>` rule and the slot `$lights`
|
||||
|
||||
```
|
||||
[ChangeLightState]
|
||||
state = (on | off) {light_state}
|
||||
turn [the] ($lights) {light_name} <state>
|
||||
```
|
||||
|
||||
#### Home Assistant
|
||||
|
||||
In your Home Assistant `automations.yaml` file, use a `data_template` to get the Rhasspy event data with `trigger.event.data.<your property name>` and then pass those along to a script:
|
||||
|
||||
```yaml
|
||||
- id: '1577164768008'
|
||||
alias: Rhasspy Light States
|
||||
description: Voice Control on/off states for all lights
|
||||
trigger:
|
||||
- event_data: {}
|
||||
event_type: rhasspy_ChangeLightState
|
||||
platform: event
|
||||
condition: []
|
||||
action:
|
||||
- alias: ''
|
||||
data_template:
|
||||
light_name: "{{ trigger.event.data.light_name }}"
|
||||
light_state: "{{ trigger.event.data.light_state }}"
|
||||
service: script.rhasspy_light_state
|
||||
|
||||
```
|
||||
|
||||
In `scripts.yaml`, the `service_template` casts the `light_state` into a string and checks to see if you said 'on' or 'off'. The homeassistant-service can toggle both lights and switches, which is helpful if you have a combination of "light" types:
|
||||
|
||||
```yaml
|
||||
rhasspy_light_state:
|
||||
alias: change_light_state
|
||||
fields:
|
||||
light_name:
|
||||
description: "Light Entity"
|
||||
example: light.bulb_1
|
||||
light_state:
|
||||
description: "State to change the light to"
|
||||
example: on
|
||||
sequence:
|
||||
- service_template: >
|
||||
{% set this_state = light_state | string %}
|
||||
{% if this_state == 'on' %}
|
||||
homeassistant.turn_on
|
||||
{%else %}
|
||||
homeassistant.turn_off
|
||||
{% endif %}
|
||||
|
||||
data_template:
|
||||
entity_id: "{{ light_name }}"
|
||||
```
|
||||
|
||||
## Client/Server Setup
|
||||
|
||||
@@ -96,9 +179,10 @@ Contributed by [jaburges](https://community.home-assistant.io/u/jaburges)
|
||||
[Rhasspy]
|
||||
Listen for wake word on Startup = UNchecked
|
||||
|
||||
[Home Assistant]
|
||||
Do not use Home Assistant (note you obviously can instead of Node-Red)
|
||||
|
||||
[Intent Handling]
|
||||
Do not handle intent on this device
|
||||
#There is no harm in having the Server handle Intents, but the Client must handle Intents
|
||||
|
||||
[Wake Word]
|
||||
No Wake word on this device
|
||||
|
||||
@@ -181,7 +265,8 @@ Contributed by [jaburges](https://community.home-assistant.io/u/jaburges)
|
||||
Listen for wake word on Startup = checked
|
||||
|
||||
[Home Assistant]
|
||||
Do not use Home Assistant (note you obviously can instead of Node-Red)
|
||||
Enable Intent Handling on this device
|
||||
#Do not use Home Assistant if using Node-Red
|
||||
|
||||
[Wake Word]
|
||||
Use snowboy (this should trigger a download of more files)
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ Add to your [profile](profiles.md):
|
||||
|
||||
There are a lot of [keyword files](https://github.com/Picovoice/Porcupine/tree/master/resources/keyword_files) available for download. Use the `linux` platform if you're on desktop/laptop (`amd64`) and the `raspberrypi` platform if you're using a Raspberry Pi (`armhf`/`aarch64`). The `.ppn` files should go in the `porcupine` directory inside your profile (referenced by `keyword_path`).
|
||||
|
||||
If you want to create a custom wake word, you will need to run the [Porcupine Optimizer](https://github.com/Picovoice/Porcupine/tree/master/tools/optimizer). **NOTE**: the generated keyword file is only valid for 30 days, though you can always just re-run the optimizer.
|
||||
If you want to create a custom wake word, you will need to use the [Picovoice Console](https://github.com/Picovoice/porcupine#picovoice-console). **NOTE**: the generated keyword file is only valid for 30 days, though you can always just re-run the optimizer.
|
||||
|
||||
See `rhasspy.wake.PorcupineWakeListener` for details.
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ ignore_missing_imports = True
|
||||
[mypy-json5.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-quart.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-quart_cors.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
+4
-4
@@ -64,16 +64,16 @@ class Porcupine(object):
|
||||
"""
|
||||
|
||||
if not os.path.exists(library_path):
|
||||
raise IOError("Could not find Porcupine's library at '%s'" % library_path)
|
||||
raise IOError(f"Could not find Porcupine's library at '{library_path}'")
|
||||
|
||||
library = cdll.LoadLibrary(library_path)
|
||||
|
||||
if not os.path.exists(model_file_path):
|
||||
raise IOError("Could not find model file at '%s'" % model_file_path)
|
||||
raise IOError(f"Could not find model file at '{model_file_path}'")
|
||||
|
||||
if sensitivity is not None and keyword_file_path is not None:
|
||||
if not os.path.exists(keyword_file_path):
|
||||
raise IOError("Could not find keyword file at '%s'" % keyword_file_path)
|
||||
raise IOError(f"Could not find keyword file at '{keyword_file_path}'")
|
||||
keyword_file_paths = [keyword_file_path]
|
||||
|
||||
if not (0 <= sensitivity <= 1):
|
||||
@@ -85,7 +85,7 @@ class Porcupine(object):
|
||||
|
||||
for x in keyword_file_paths:
|
||||
if not os.path.exists(os.path.expanduser(x)):
|
||||
raise IOError("Could not find keyword file at '%s'" % x)
|
||||
raise IOError(f"Could not find keyword file at '{x}'")
|
||||
|
||||
for x in sensitivities:
|
||||
if not (0 <= x <= 1):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "ca",
|
||||
"name": "ca",
|
||||
"locale": "ca_ES",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
dilluns
|
||||
dimarts
|
||||
dimecres
|
||||
dijous
|
||||
divendres
|
||||
dissabte
|
||||
diumenge
|
||||
@@ -0,0 +1,12 @@
|
||||
de gener
|
||||
de febrer
|
||||
de març
|
||||
d’abril
|
||||
de maig
|
||||
de juny
|
||||
de juliol
|
||||
d’agost
|
||||
de setembre
|
||||
d’octubre
|
||||
de novembre
|
||||
de desembre
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "de",
|
||||
"name": "de",
|
||||
"locale": "de_DE",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower",
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Montag
|
||||
Dienstag
|
||||
Mittwoch
|
||||
Donnerstag
|
||||
Freitag
|
||||
Samstag
|
||||
Sonntag
|
||||
@@ -0,0 +1,12 @@
|
||||
Januar
|
||||
Februar
|
||||
März
|
||||
April
|
||||
Mai
|
||||
Juni
|
||||
Juli
|
||||
August
|
||||
September
|
||||
Oktober
|
||||
November
|
||||
Dezember
|
||||
+11
-4
@@ -28,10 +28,10 @@
|
||||
"program": ""
|
||||
},
|
||||
"forward_to_hass": false,
|
||||
"system": "dummy",
|
||||
"system": "dummy",
|
||||
"remote": {
|
||||
"url": "http://my-server:port/endpoint"
|
||||
},
|
||||
}
|
||||
},
|
||||
"home_assistant": {
|
||||
"access_token": "",
|
||||
@@ -52,14 +52,17 @@
|
||||
"conversation": {
|
||||
"handle_speech": true
|
||||
},
|
||||
"error_sound": true,
|
||||
"fuzzywuzzy": {
|
||||
"examples_json": "intent_examples.json",
|
||||
"min_confidence": 0
|
||||
},
|
||||
"fsticuffs": {
|
||||
"intent_fst": "intent.fst",
|
||||
"intent_graph": "intent.json",
|
||||
"ignore_unknown_words": true,
|
||||
"fuzzy": true
|
||||
"fuzzy": true,
|
||||
"converters_dir": "converters"
|
||||
},
|
||||
"flair": {
|
||||
"cache_dir": "flair/cache",
|
||||
@@ -125,7 +128,8 @@
|
||||
"sounds": {
|
||||
"recorded": "${RHASSPY_BASE_DIR}/etc/wav/beep_lo.wav",
|
||||
"system": "aplay",
|
||||
"wake": "${RHASSPY_BASE_DIR}/etc/wav/beep_hi.wav"
|
||||
"wake": "${RHASSPY_BASE_DIR}/etc/wav/beep_hi.wav",
|
||||
"error": "${RHASSPY_BASE_DIR}/etc/wav/beep_error.wav"
|
||||
},
|
||||
"speech_to_text": {
|
||||
"command": {
|
||||
@@ -186,6 +190,7 @@
|
||||
"sentences_ini": "sentences.ini",
|
||||
"sentences_dir": "intents",
|
||||
"slots_dir": "slots",
|
||||
"slot_programs_dir": "slot_programs",
|
||||
"system": "dummy"
|
||||
},
|
||||
"text_to_speech": {
|
||||
@@ -193,6 +198,7 @@
|
||||
"arguments": [],
|
||||
"program": ""
|
||||
},
|
||||
"disable_wake": false,
|
||||
"espeak": {},
|
||||
"flite": {
|
||||
"voice": "kal16"
|
||||
@@ -284,6 +290,7 @@
|
||||
},
|
||||
"system": "dummy"
|
||||
},
|
||||
"webhooks": {},
|
||||
"download": {
|
||||
"cache_dir": "download",
|
||||
"conditions": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "el",
|
||||
"name": "el",
|
||||
"locale": "el_GR",
|
||||
"speech_to_text": {
|
||||
"g2p_casing": "lower",
|
||||
"system": "pocketsphinx",
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Δευτέρα
|
||||
Τρίτη
|
||||
Τετάρτη
|
||||
Πέμπτη
|
||||
Παρασκευή
|
||||
Σάββατο
|
||||
Κυριακή
|
||||
@@ -0,0 +1,12 @@
|
||||
Ιανουαρίου
|
||||
Φεβρουαρίου
|
||||
Μαρτίου
|
||||
Απριλίου
|
||||
Μαΐου
|
||||
Ιουνίου
|
||||
Ιουλίου
|
||||
Αυγούστου
|
||||
Σεπτεμβρίου
|
||||
Οκτωβρίου
|
||||
Νοεμβρίου
|
||||
Δεκεμβρίου
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "en",
|
||||
"name": "en",
|
||||
"locale": "en_US",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower",
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Monday
|
||||
Tuesday
|
||||
Wednesday
|
||||
Thursday
|
||||
Friday
|
||||
Saturday
|
||||
Sunday
|
||||
@@ -0,0 +1,12 @@
|
||||
January
|
||||
February
|
||||
March
|
||||
April
|
||||
May
|
||||
June
|
||||
July
|
||||
August
|
||||
September
|
||||
October
|
||||
November
|
||||
December
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "es",
|
||||
"name": "es",
|
||||
"locale": "es_ES",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
lunes
|
||||
martes
|
||||
miércoles
|
||||
jueves
|
||||
viernes
|
||||
sábado
|
||||
domingo
|
||||
@@ -0,0 +1,12 @@
|
||||
enero
|
||||
febrero
|
||||
marzo
|
||||
abril
|
||||
mayo
|
||||
junio
|
||||
julio
|
||||
agosto
|
||||
septiembre
|
||||
octubre
|
||||
noviembre
|
||||
diciembre
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "fr",
|
||||
"name": "fr",
|
||||
"locale": "fr_FR",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower",
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
lundi
|
||||
mardi
|
||||
mercredi
|
||||
jeudi
|
||||
vendredi
|
||||
samedi
|
||||
dimanche
|
||||
@@ -0,0 +1,12 @@
|
||||
janvier
|
||||
février
|
||||
mars
|
||||
avril
|
||||
mai
|
||||
juin
|
||||
juillet
|
||||
août
|
||||
septembre
|
||||
octobre
|
||||
novembre
|
||||
décembre
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "hi",
|
||||
"name": "hi",
|
||||
"locale": "hi_IN",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
सोमवार
|
||||
मंगलवार
|
||||
बुधवार
|
||||
गुरुवार
|
||||
शुक्रवार
|
||||
शनिवार
|
||||
रविवार
|
||||
@@ -0,0 +1,12 @@
|
||||
जनवरी
|
||||
फ़रवरी
|
||||
मार्च
|
||||
अप्रैल
|
||||
मई
|
||||
जून
|
||||
जुलाई
|
||||
अगस्त
|
||||
सितंबर
|
||||
अक्तूबर
|
||||
नवंबर
|
||||
दिसंबर
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "it",
|
||||
"language": "it",
|
||||
"locale": "it_IT",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
lunedì
|
||||
martedì
|
||||
mercoledì
|
||||
giovedì
|
||||
venerdì
|
||||
sabato
|
||||
domenica
|
||||
@@ -0,0 +1,12 @@
|
||||
gennaio
|
||||
febbraio
|
||||
marzo
|
||||
aprile
|
||||
maggio
|
||||
giugno
|
||||
luglio
|
||||
agosto
|
||||
settembre
|
||||
ottobre
|
||||
novembre
|
||||
dicembre
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "nl",
|
||||
"name": "nl",
|
||||
"locale": "nl_NL",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower",
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
maandag
|
||||
dinsdag
|
||||
woensdag
|
||||
donderdag
|
||||
vrijdag
|
||||
zaterdag
|
||||
zondag
|
||||
@@ -0,0 +1,12 @@
|
||||
januari
|
||||
februari
|
||||
maart
|
||||
april
|
||||
mei
|
||||
juni
|
||||
juli
|
||||
augustus
|
||||
september
|
||||
oktober
|
||||
november
|
||||
december
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "pt",
|
||||
"name": "pt",
|
||||
"locale": "pt_BR",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
segunda
|
||||
terça
|
||||
quarta
|
||||
quinta
|
||||
sexta
|
||||
sábado
|
||||
domingo
|
||||
@@ -0,0 +1,12 @@
|
||||
janeiro
|
||||
fevereiro
|
||||
março
|
||||
abril
|
||||
maio
|
||||
junho
|
||||
julho
|
||||
agosto
|
||||
setembro
|
||||
outubro
|
||||
novembro
|
||||
dezembro
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"language": "ru",
|
||||
"name": "ru",
|
||||
|
||||
"locale": "ru_RU",
|
||||
"speech_to_text": {
|
||||
"system": "pocketsphinx",
|
||||
"dictionary_casing": "lower"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Понедельник
|
||||
Вторник
|
||||
Среда
|
||||
Четверг
|
||||
Пятница
|
||||
Суббота
|
||||
Воскресенье
|
||||
@@ -0,0 +1,12 @@
|
||||
января
|
||||
февраля
|
||||
марта
|
||||
апреля
|
||||
мая
|
||||
июня
|
||||
июля
|
||||
августа
|
||||
сентября
|
||||
октября
|
||||
ноября
|
||||
декабря
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "sv",
|
||||
"name": "sv",
|
||||
"locale": "sv_SE",
|
||||
"speech_to_text": {
|
||||
"kaldi": {
|
||||
"compatible": true
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
måndag
|
||||
tisdag
|
||||
onsdag
|
||||
torsdag
|
||||
fredag
|
||||
lördag
|
||||
söndag
|
||||
@@ -0,0 +1,12 @@
|
||||
januari
|
||||
februari
|
||||
mars
|
||||
april
|
||||
maj
|
||||
juni
|
||||
juli
|
||||
augusti
|
||||
september
|
||||
oktober
|
||||
november
|
||||
december
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"language": "vi",
|
||||
"name": "vi",
|
||||
"locale": "vi_VN",
|
||||
"speech_to_text": {
|
||||
"kaldi": {
|
||||
"compatible": true
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
Thứ hai
|
||||
Thứ ba
|
||||
Thứ tư
|
||||
Thứ năm
|
||||
Thứ sáu
|
||||
Thứ bảy
|
||||
Chủ nhật
|
||||
@@ -0,0 +1,12 @@
|
||||
Tháng 1
|
||||
Tháng 2
|
||||
Tháng 3
|
||||
Tháng 4
|
||||
Tháng 5
|
||||
Tháng 6
|
||||
Tháng 7
|
||||
Tháng 8
|
||||
Tháng 9
|
||||
Tháng 10
|
||||
Tháng 11
|
||||
Tháng 12
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "zh",
|
||||
"language": "zh",
|
||||
"locale": "zh_CN",
|
||||
"speech_to_text": {
|
||||
"g2p_casing": "n/a",
|
||||
"dictionary_casing": "n/a"
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser("number")
|
||||
parser.add_argument("lower", type=int, help="Lower bound")
|
||||
parser.add_argument("upper", type=int, help="Upper bound (inclusive)")
|
||||
args, rest_args = parser.parse_known_args()
|
||||
|
||||
lower = args.lower
|
||||
upper = args.upper
|
||||
step = 1
|
||||
|
||||
if rest_args:
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
星期一
|
||||
星期二
|
||||
星期三
|
||||
星期四
|
||||
星期五
|
||||
星期六
|
||||
星期日
|
||||
@@ -0,0 +1,12 @@
|
||||
一月
|
||||
二月
|
||||
三月
|
||||
四月
|
||||
五月
|
||||
六月
|
||||
七月
|
||||
八月
|
||||
九月
|
||||
十月
|
||||
十一月
|
||||
十二月
|
||||
@@ -383,6 +383,11 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
default: true
|
||||
- in: query
|
||||
name: siteId
|
||||
description: 'Hermes siteId to use in playBytes'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
|
||||
+1
-1
@@ -15,6 +15,6 @@ pydash==4.7.6
|
||||
quart==0.6.15
|
||||
quart-cors==0.1.3
|
||||
requests==2.22.0
|
||||
rhasspy-nlu==0.1.3
|
||||
rhasspy-nlu==0.1.4.1
|
||||
swagger-ui-py==0.1.7
|
||||
webrtcvad==2.0.10
|
||||
|
||||
@@ -15,11 +15,12 @@ pydash==4.7.6
|
||||
quart==0.6.15
|
||||
quart-cors==0.1.3
|
||||
requests==2.22.0
|
||||
rhasspy-nlu==0.1.3
|
||||
rhasspy-nlu==0.1.4.1
|
||||
swagger-ui-py==0.1.7
|
||||
webrtcvad==2.0.10
|
||||
|
||||
flake8==3.7.9
|
||||
pylint==2.4.4
|
||||
pyinstaller==3.5
|
||||
mypy==0.700
|
||||
mkdocs==1.0.4
|
||||
|
||||
@@ -12,6 +12,6 @@ pydash==4.7.6
|
||||
quart==0.6.15
|
||||
quart-cors==0.1.3
|
||||
requests==2.22.0
|
||||
rhasspy-nlu==0.1.1
|
||||
rhasspy-nlu==0.1.4
|
||||
swagger-ui-py==0.1.7
|
||||
webrtcvad==2.0.10
|
||||
|
||||
+12
-11
@@ -4,7 +4,6 @@ import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
import logging.config
|
||||
import os
|
||||
@@ -302,11 +301,11 @@ async def main() -> None:
|
||||
if not args.no_check and (args.command not in ["check", "download"]):
|
||||
# Verify that profile has necessary files
|
||||
missing_files = core.check_profile()
|
||||
if len(missing_files) > 0:
|
||||
if missing_files:
|
||||
logger.fatal(
|
||||
"Missing required files for %s: %s. Please run download command and try again.",
|
||||
profile.name,
|
||||
missing_files.keys(),
|
||||
list(missing_files),
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -335,7 +334,7 @@ async def main() -> None:
|
||||
|
||||
async def wav2text(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Transcribe WAV file(s)"""
|
||||
if len(args.wav_files) > 0:
|
||||
if args.wav_files:
|
||||
# Read WAV paths from argument list
|
||||
transcriptions = {}
|
||||
for wav_path in args.wav_files:
|
||||
@@ -361,7 +360,7 @@ async def wav2text(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
async def text2intent(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Parse sentences from command line or stdin"""
|
||||
intents = {}
|
||||
sentences = args.sentences if len(args.sentences) > 0 else sys.stdin
|
||||
sentences = args.sentences or sys.stdin
|
||||
for sentence in sentences:
|
||||
sentence = sentence.strip()
|
||||
intent = (await core.recognize_intent(sentence)).intent
|
||||
@@ -382,7 +381,7 @@ async def text2intent(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
|
||||
async def wav2intent(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Recognize intent from WAV file(s)"""
|
||||
if len(args.wav_files) > 0:
|
||||
if args.wav_files:
|
||||
# Read WAV paths from argument list
|
||||
transcriptions = {}
|
||||
for wav_path in args.wav_files:
|
||||
@@ -494,7 +493,9 @@ async def mic2intent(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
|
||||
async def word2phonemes(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Get pronunciation(s) for word(s)"""
|
||||
words = args.words if len(args.words) > 0 else sys.stdin
|
||||
words = args.words
|
||||
if not words:
|
||||
words = [w.strip() for w in sys.stdin if w.strip()]
|
||||
|
||||
# Get pronunciations for all words
|
||||
pronunciations = (
|
||||
@@ -558,9 +559,9 @@ def _send_frame(
|
||||
async def wav2mqtt(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Publish WAV to MQTT as audio frames"""
|
||||
# hermes/audioServer/<SITE_ID>/audioFrame
|
||||
topic = "hermes/audioServer/%s/audioFrame" % args.site_id
|
||||
topic = f"hermes/audioServer/{args.site_id}/audioFrame"
|
||||
|
||||
if len(args.wav_files) > 0:
|
||||
if args.wav_files:
|
||||
# Read WAV paths from argument list
|
||||
for wav_path in args.wav_files:
|
||||
with wave.open(wav_path, "rb") as wav_file:
|
||||
@@ -585,7 +586,7 @@ async def wav2mqtt(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
# Read actual audio data
|
||||
audio_data = wav_file.readframes(args.frames)
|
||||
|
||||
while len(audio_data) > 0:
|
||||
while audio_data:
|
||||
_send_frame(core, topic, audio_data, rate, width, channels)
|
||||
time.sleep(args.pause)
|
||||
|
||||
@@ -629,7 +630,7 @@ async def text2wav(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
async def text2speech(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Speak sentences"""
|
||||
sentences = args.sentences
|
||||
if len(sentences) == 0:
|
||||
if not sentences:
|
||||
sentences = sys.stdin
|
||||
|
||||
for sentence in sentences:
|
||||
|
||||
+15
-37
@@ -6,43 +6,14 @@ import uuid
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.mqtt import MqttPublish
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PlayWavFile:
|
||||
"""Play a WAV file."""
|
||||
|
||||
def __init__(self, wav_path: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.wav_path = wav_path
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class PlayWavData:
|
||||
"""Play a WAV buffer."""
|
||||
|
||||
def __init__(
|
||||
self, wav_data: bytes, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.wav_data = wav_data
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WavPlayed:
|
||||
"""Response to PlayWavFile or PlayWavData."""
|
||||
|
||||
pass
|
||||
|
||||
from rhasspy.events import MqttPublish, PlayWavData, PlayWavFile, WavPlayed
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_sound_class(system: str) -> Type[RhasspyActor]:
|
||||
"""Get class type for profile audio player."""
|
||||
assert system in ["aplay", "hermes", "dummy"], "Unknown sound system: %s" % system
|
||||
assert system in ["aplay", "hermes", "dummy"], f"Unknown sound system: {system}"
|
||||
|
||||
if system == "aplay":
|
||||
return APlayAudioPlayer
|
||||
@@ -180,29 +151,36 @@ class HermesAudioPlayer(RhasspyActor):
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in started state."""
|
||||
if isinstance(message, PlayWavFile):
|
||||
self.play_file(message.wav_path)
|
||||
self.play_file(message.wav_path, siteId=message.siteId)
|
||||
self.send(message.receiver or sender, WavPlayed())
|
||||
elif isinstance(message, PlayWavData):
|
||||
self.play_data(message.wav_data)
|
||||
self.play_data(message.wav_data, siteId=message.siteId)
|
||||
self.send(message.receiver or sender, WavPlayed())
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def play_file(self, path: str) -> None:
|
||||
def play_file(self, path: str, siteId: Optional[str] = None) -> None:
|
||||
"""Send WAV file over MQTT."""
|
||||
if not os.path.exists(path):
|
||||
self._logger.warning("Path does not exist: %s", path)
|
||||
return
|
||||
|
||||
with open(path, "rb") as wav_file:
|
||||
self.play_data(wav_file.read())
|
||||
self.play_data(wav_file.read(), siteId=siteId)
|
||||
|
||||
def play_data(self, wav_data: bytes) -> None:
|
||||
def play_data(self, wav_data: bytes, siteId: Optional[str] = None) -> None:
|
||||
"""Send WAV buffer over MQTT."""
|
||||
request_id = str(uuid.uuid4())
|
||||
|
||||
if siteId:
|
||||
# Send to a specific site id
|
||||
publish_sites = [siteId]
|
||||
else:
|
||||
# Send to all site ids
|
||||
publish_sites = self.site_ids
|
||||
|
||||
# Send to all site ids
|
||||
for site_id in self.site_ids:
|
||||
for site_id in publish_sites:
|
||||
topic = f"hermes/audioServer/{site_id}/playBytes/{request_id}"
|
||||
self.send(self.mqtt, MqttPublish(topic, wav_data))
|
||||
|
||||
|
||||
+33
-70
@@ -14,55 +14,12 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.intent import IntentRecognized
|
||||
from rhasspy.mqtt import MqttMessage, MqttSubscribe
|
||||
from rhasspy.stt import WavTranscription
|
||||
from rhasspy.events import (AudioData, IntentRecognized, MqttMessage,
|
||||
MqttSubscribe, StartRecordingToBuffer,
|
||||
StartStreaming, StopRecordingToBuffer,
|
||||
StopStreaming, WavTranscription)
|
||||
from rhasspy.utils import convert_wav
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AudioData:
|
||||
"""Raw 16-bit 16Khz audio data."""
|
||||
|
||||
def __init__(self, data: bytes, **kwargs: Any) -> None:
|
||||
self.data = data
|
||||
self.info = kwargs
|
||||
|
||||
|
||||
class StartStreaming:
|
||||
"""Tells microphone to begin recording. Emits AudioData chunks."""
|
||||
|
||||
def __init__(self, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class StopStreaming:
|
||||
"""Tells microphone to stop recording."""
|
||||
|
||||
def __init__(self, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class StartRecordingToBuffer:
|
||||
"""Tells microphone to record audio data to named buffer."""
|
||||
|
||||
def __init__(self, buffer_name: str) -> None:
|
||||
self.buffer_name = buffer_name
|
||||
|
||||
|
||||
class StopRecordingToBuffer:
|
||||
"""Tells microphone to stop recording to buffer and emit AudioData."""
|
||||
|
||||
def __init__(
|
||||
self, buffer_name: str, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.buffer_name = buffer_name
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -76,7 +33,7 @@ def get_microphone_class(system: str) -> Type[RhasspyActor]:
|
||||
"stdin",
|
||||
"http",
|
||||
"gstreamer",
|
||||
], ("Unknown microphone system: %s" % system)
|
||||
], f"Unknown microphone system: {system}"
|
||||
|
||||
if system == "arecord":
|
||||
# Use arecord locally
|
||||
@@ -191,7 +148,7 @@ class PyAudioRecorder(RhasspyActor):
|
||||
|
||||
# Start audio system
|
||||
def stream_callback(data, frame_count, time_info, status):
|
||||
if len(data) > 0:
|
||||
if data:
|
||||
# Send to this actor to avoid threading issues
|
||||
self.send(self.myAddress, AudioData(data))
|
||||
|
||||
@@ -221,6 +178,7 @@ class PyAudioRecorder(RhasspyActor):
|
||||
)
|
||||
except Exception:
|
||||
self._logger.exception("to_recording")
|
||||
self._stop_microphone()
|
||||
self.transition("started")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -257,8 +215,8 @@ class PyAudioRecorder(RhasspyActor):
|
||||
# Check to see if anyone is still listening
|
||||
if (
|
||||
(not self.keep_device_open)
|
||||
and (len(self.receivers) == 0)
|
||||
and (len(self.buffers) == 0)
|
||||
and not self.receivers
|
||||
and not self.buffers
|
||||
):
|
||||
# Terminate audio recording
|
||||
if self.mic is not None:
|
||||
@@ -274,6 +232,11 @@ class PyAudioRecorder(RhasspyActor):
|
||||
|
||||
def to_stopped(self, from_state: str) -> None:
|
||||
"""Transition to stopped state."""
|
||||
self._stop_microphone()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _stop_microphone(self) -> None:
|
||||
try:
|
||||
if self.mic is not None:
|
||||
self.mic.stop_stream()
|
||||
@@ -284,7 +247,7 @@ class PyAudioRecorder(RhasspyActor):
|
||||
self.audio.terminate()
|
||||
self.audio = None
|
||||
except Exception:
|
||||
self._logger.exception("to_stopped")
|
||||
self._logger.exception("_stop_microphone")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -342,7 +305,7 @@ class PyAudioRecorder(RhasspyActor):
|
||||
finally:
|
||||
pyaudio_stream.close()
|
||||
except Exception:
|
||||
result[device_index] = "%s (error)" % device_name
|
||||
result[device_index] = f"{device_name} (error)"
|
||||
continue
|
||||
|
||||
# compute RMS of debiased audio
|
||||
@@ -353,9 +316,9 @@ class PyAudioRecorder(RhasspyActor):
|
||||
)
|
||||
|
||||
if debiased_energy > 30: # probably actually audio
|
||||
result[device_index] = "%s (working!)" % device_name
|
||||
result[device_index] = f"{device_name} (working!)"
|
||||
else:
|
||||
result[device_index] = "%s (no sound)" % device_name
|
||||
result[device_index] = f"{device_name} (no sound)"
|
||||
finally:
|
||||
audio.terminate()
|
||||
|
||||
@@ -390,7 +353,7 @@ class ARecordAudioRecorder(RhasspyActor):
|
||||
|
||||
if self.device_name is not None:
|
||||
self.device_name = str(self.device_name)
|
||||
if len(self.device_name) == 0:
|
||||
if not self.device_name:
|
||||
self.device_name = None
|
||||
|
||||
self.chunk_size = int(
|
||||
@@ -438,7 +401,7 @@ class ARecordAudioRecorder(RhasspyActor):
|
||||
while self.is_recording:
|
||||
# Pull from process STDOUT
|
||||
data = self.record_proc.stdout.read(self.chunk_size)
|
||||
if len(data) > 0:
|
||||
if data:
|
||||
# Send to this actor to avoid threading issues
|
||||
self.send(self.myAddress, AudioData(data))
|
||||
else:
|
||||
@@ -495,8 +458,8 @@ class ARecordAudioRecorder(RhasspyActor):
|
||||
# Check to see if anyone is still listening
|
||||
if (
|
||||
(not self.keep_device_open)
|
||||
and (len(self.receivers) == 0)
|
||||
and (len(self.buffers) == 0)
|
||||
and not self.receivers
|
||||
and not self.buffers
|
||||
):
|
||||
# Terminate audio recording
|
||||
self.is_recording = False
|
||||
@@ -573,7 +536,7 @@ class ARecordAudioRecorder(RhasspyActor):
|
||||
buffer = proc.stdout.read(chunk_size * 2)
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
result[device_id] = "%s (error)" % device_name
|
||||
result[device_id] = f"{device_name} (error)"
|
||||
continue
|
||||
|
||||
# compute RMS of debiased audio
|
||||
@@ -584,9 +547,9 @@ class ARecordAudioRecorder(RhasspyActor):
|
||||
)
|
||||
|
||||
if debiased_energy > 30: # probably actually audio
|
||||
result[device_id] = "%s (working!)" % device_name
|
||||
result[device_id] = f"{device_name} (working!)"
|
||||
else:
|
||||
result[device_id] = "%s (no sound)" % device_name
|
||||
result[device_id] = f"{device_name} (no sound)"
|
||||
|
||||
return result
|
||||
|
||||
@@ -613,11 +576,11 @@ class HermesAudioRecorder(RhasspyActor):
|
||||
"""Transition to started state."""
|
||||
self.mqtt = self.config["mqtt"]
|
||||
self.site_ids = self.profile.get("mqtt.site_id", "default").split(",")
|
||||
if len(self.site_ids) > 0:
|
||||
if self.site_ids:
|
||||
self.site_id = self.site_ids[0]
|
||||
else:
|
||||
self.site_id = "default"
|
||||
self.topic_audio_frame = "hermes/audioServer/%s/audioFrame" % self.site_id
|
||||
self.topic_audio_frame = f"hermes/audioServer/{self.site_id}/audioFrame"
|
||||
self.send(self.mqtt, MqttSubscribe(self.topic_audio_frame))
|
||||
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
@@ -760,7 +723,7 @@ class StdinAudioRecorder(RhasspyActor):
|
||||
self.send(message.receiver or sender, AudioData(buffer))
|
||||
|
||||
# Check to see if anyone is still listening
|
||||
if (len(self.receivers) == 0) and (len(self.buffers) == 0):
|
||||
if not self.receivers and not self.buffers:
|
||||
# Terminate audio recording
|
||||
self.is_recording = False
|
||||
self.transition("started")
|
||||
@@ -778,7 +741,7 @@ class StdinAudioRecorder(RhasspyActor):
|
||||
"""Forward single audio chunk."""
|
||||
while True:
|
||||
data = sys.stdin.buffer.read(self.chunk_size)
|
||||
if self.is_recording and (len(data) > 0):
|
||||
if self.is_recording and data:
|
||||
# Actor will forward
|
||||
self.send(self.myAddress, AudioData(data))
|
||||
|
||||
@@ -827,7 +790,7 @@ class HTTPStreamServer(BaseHTTPRequestHandler):
|
||||
while True:
|
||||
# Assume chunked transfer encoding
|
||||
chunk_size_str = self.rfile.readline().decode().strip()
|
||||
if len(chunk_size_str) == 0:
|
||||
if not chunk_size_str:
|
||||
break
|
||||
|
||||
chunk_size = int(chunk_size_str, 16)
|
||||
@@ -952,7 +915,7 @@ class HTTPAudioRecorder(RhasspyActor):
|
||||
self.send(message.receiver or sender, AudioData(buffer))
|
||||
|
||||
# Check to see if anyone is still listening
|
||||
if (len(self.receivers) == 0) and (len(self.buffers) == 0):
|
||||
if not self.receivers and not self.buffers:
|
||||
self.transition("started")
|
||||
|
||||
def to_stopped(self, from_state: str) -> None:
|
||||
@@ -1049,7 +1012,7 @@ class GStreamerAudioRecorder(RhasspyActor):
|
||||
|
||||
while True:
|
||||
chunk = self.gstreamer_proc.stdout.read(self.chunk_size)
|
||||
if len(chunk) > 0:
|
||||
if chunk:
|
||||
if first_audio:
|
||||
self._logger.debug("Receiving audio")
|
||||
first_audio = False
|
||||
@@ -1111,7 +1074,7 @@ class GStreamerAudioRecorder(RhasspyActor):
|
||||
self.send(message.receiver or sender, AudioData(buffer))
|
||||
|
||||
# Check to see if anyone is still listening
|
||||
if (len(self.receivers) == 0) and (len(self.buffers) == 0):
|
||||
if not self.receivers and not self.buffers:
|
||||
self.transition("started")
|
||||
|
||||
def to_stopped(self, from_state: str) -> None:
|
||||
|
||||
@@ -11,45 +11,18 @@ from typing import Any, Dict, List, Optional, Type
|
||||
import webrtcvad
|
||||
|
||||
from rhasspy.actor import RhasspyActor, WakeupMessage
|
||||
from rhasspy.audio_recorder import AudioData, StartStreaming, StopStreaming
|
||||
from rhasspy.mqtt import MqttMessage, MqttSubscribe
|
||||
from rhasspy.events import (AudioData, ListenForCommand, MqttMessage,
|
||||
MqttSubscribe, StartStreaming, StopStreaming,
|
||||
VoiceCommand)
|
||||
from rhasspy.utils import convert_wav
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ListenForCommand:
|
||||
"""Tell Rhasspy to listen for a voice command."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
timeout: Optional[float] = None,
|
||||
entities: List[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
self.timeout = timeout
|
||||
self.entities = entities or []
|
||||
|
||||
|
||||
class VoiceCommand:
|
||||
"""Response to ListenForCommand."""
|
||||
|
||||
def __init__(self, data: bytes, timeout: bool = False, handle: bool = True) -> None:
|
||||
self.data = data
|
||||
self.timeout = timeout
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_command_class(system: str) -> Type[RhasspyActor]:
|
||||
"""Return class type for profile command listener."""
|
||||
assert system in ["dummy", "webrtcvad", "command", "oneshot", "hermes"], (
|
||||
"Unknown voice command system: %s" % system
|
||||
f"Unknown voice command system: {system}"
|
||||
)
|
||||
|
||||
if system == "webrtcvad":
|
||||
|
||||
+15
-12
@@ -14,13 +14,9 @@ import aiohttp
|
||||
|
||||
# Internal imports
|
||||
from rhasspy.actor import ActorSystem, ConfigureEvent, RhasspyActor
|
||||
from rhasspy.audio_recorder import (
|
||||
from rhasspy.dialogue import DialogueManager
|
||||
from rhasspy.events import (
|
||||
AudioData,
|
||||
StartRecordingToBuffer,
|
||||
StopRecordingToBuffer,
|
||||
)
|
||||
from rhasspy.dialogue import (
|
||||
DialogueManager,
|
||||
GetActorStates,
|
||||
GetMicrophones,
|
||||
GetProblems,
|
||||
@@ -29,6 +25,8 @@ from rhasspy.dialogue import (
|
||||
GetWordPhonemes,
|
||||
GetWordPronunciations,
|
||||
HandleIntent,
|
||||
IntentHandled,
|
||||
IntentRecognized,
|
||||
ListenForCommand,
|
||||
ListenForWakeWord,
|
||||
MqttPublish,
|
||||
@@ -38,21 +36,23 @@ from rhasspy.dialogue import (
|
||||
ProfileTrainingComplete,
|
||||
ProfileTrainingFailed,
|
||||
RecognizeIntent,
|
||||
SentenceSpoken,
|
||||
SpeakSentence,
|
||||
SpeakWord,
|
||||
StartRecordingToBuffer,
|
||||
StopRecordingToBuffer,
|
||||
TestMicrophones,
|
||||
TrainProfile,
|
||||
TranscribeWav,
|
||||
VoiceCommand,
|
||||
WakeWordDetected,
|
||||
WakeWordNotDetected,
|
||||
WavTranscription,
|
||||
WordPhonemes,
|
||||
WordPronunciations,
|
||||
WordSpoken,
|
||||
)
|
||||
from rhasspy.intent import IntentRecognized
|
||||
from rhasspy.intent_handler import IntentHandled
|
||||
from rhasspy.profiles import Profile
|
||||
from rhasspy.pronounce import WordPhonemes, WordPronunciations, WordSpoken
|
||||
from rhasspy.stt import WavTranscription
|
||||
from rhasspy.tts import SentenceSpoken
|
||||
from rhasspy.utils import numbers_to_words
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -322,13 +322,16 @@ class RhasspyCore:
|
||||
play: bool = True,
|
||||
language: Optional[str] = None,
|
||||
voice: Optional[str] = None,
|
||||
siteId: Optional[str] = None,
|
||||
) -> SentenceSpoken:
|
||||
"""Speak an entire sentence using text to speech system."""
|
||||
assert self.actor_system is not None
|
||||
with self.actor_system.private() as sys:
|
||||
result = await sys.async_ask(
|
||||
self.dialogue_manager,
|
||||
SpeakSentence(sentence, play=play, language=language, voice=voice),
|
||||
SpeakSentence(
|
||||
sentence, play=play, language=language, voice=voice, siteId=siteId
|
||||
),
|
||||
)
|
||||
assert isinstance(result, SentenceSpoken), result
|
||||
return result
|
||||
|
||||
+96
-122
@@ -6,125 +6,70 @@ from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
import pydash
|
||||
import pywrapfst as fst
|
||||
import requests
|
||||
|
||||
from rhasspy.actor import (ActorExitRequest, ChildActorExited, Configured,
|
||||
ConfigureEvent, RhasspyActor, StateTransition,
|
||||
WakeupMessage)
|
||||
from rhasspy.audio_player import (PlayWavData, PlayWavFile, WavPlayed,
|
||||
get_sound_class)
|
||||
from rhasspy.audio_recorder import (AudioData, HTTPAudioRecorder,
|
||||
StartRecordingToBuffer,
|
||||
StopRecordingToBuffer,
|
||||
get_microphone_class)
|
||||
from rhasspy.command_listener import (ListenForCommand, VoiceCommand,
|
||||
get_command_class)
|
||||
from rhasspy.intent import (IntentRecognized, RecognizeIntent,
|
||||
get_recognizer_class)
|
||||
from rhasspy.intent_handler import (HandleIntent, IntentHandled,
|
||||
get_intent_handler_class)
|
||||
from rhasspy.intent_train import (IntentTrainingComplete, IntentTrainingFailed,
|
||||
TrainIntent, get_intent_trainer_class)
|
||||
from rhasspy.mqtt import MqttPublish
|
||||
from rhasspy.pronounce import GetWordPhonemes, GetWordPronunciations, SpeakWord
|
||||
from rhasspy.stt import TranscribeWav, WavTranscription, get_decoder_class
|
||||
from rhasspy.actor import (
|
||||
ActorExitRequest,
|
||||
ChildActorExited,
|
||||
Configured,
|
||||
ConfigureEvent,
|
||||
RhasspyActor,
|
||||
StateTransition,
|
||||
WakeupMessage,
|
||||
)
|
||||
from rhasspy.audio_player import get_sound_class
|
||||
from rhasspy.audio_recorder import HTTPAudioRecorder, get_microphone_class
|
||||
from rhasspy.command_listener import get_command_class
|
||||
from rhasspy.events import (
|
||||
AudioData,
|
||||
GetActorStates,
|
||||
GetMicrophones,
|
||||
GetProblems,
|
||||
GetSpeakers,
|
||||
GetVoiceCommand,
|
||||
GetWordPhonemes,
|
||||
GetWordPronunciations,
|
||||
HandleIntent,
|
||||
IntentHandled,
|
||||
IntentRecognized,
|
||||
IntentTrainingComplete,
|
||||
IntentTrainingFailed,
|
||||
ListenForCommand,
|
||||
ListenForWakeWord,
|
||||
MqttPublish,
|
||||
PlayWavData,
|
||||
PlayWavFile,
|
||||
Problems,
|
||||
ProfileTrainingComplete,
|
||||
ProfileTrainingFailed,
|
||||
Ready,
|
||||
RecognizeIntent,
|
||||
SpeakSentence,
|
||||
SpeakWord,
|
||||
StartRecordingToBuffer,
|
||||
StopListeningForWakeWord,
|
||||
StopRecordingToBuffer,
|
||||
TestMicrophones,
|
||||
TrainIntent,
|
||||
TrainProfile,
|
||||
TranscribeWav,
|
||||
VoiceCommand,
|
||||
WakeWordDetected,
|
||||
WakeWordNotDetected,
|
||||
WavPlayed,
|
||||
WavTranscription,
|
||||
)
|
||||
from rhasspy.intent import get_recognizer_class
|
||||
from rhasspy.intent_handler import get_intent_handler_class
|
||||
from rhasspy.intent_train import get_intent_trainer_class
|
||||
from rhasspy.stt import get_decoder_class
|
||||
from rhasspy.stt_train import get_speech_trainer_class
|
||||
from rhasspy.train import train_profile
|
||||
from rhasspy.tts import SpeakSentence, get_speech_class
|
||||
from rhasspy.tts import get_speech_class
|
||||
from rhasspy.utils import buffer_to_wav
|
||||
from rhasspy.wake import (ListenForWakeWord, StopListeningForWakeWord,
|
||||
WakeWordDetected, WakeWordNotDetected,
|
||||
get_wake_class)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GetMicrophones:
|
||||
"""Request list of micrphones."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class TestMicrophones:
|
||||
"""Request live microphones."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class GetSpeakers:
|
||||
"""Request list of audio players."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class TrainProfile:
|
||||
"""Request training for profile."""
|
||||
|
||||
def __init__(
|
||||
self, receiver: Optional[RhasspyActor] = None, reload_actors: bool = True
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.reload_actors = reload_actors
|
||||
|
||||
|
||||
class ProfileTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str):
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"FAILED: {self.reason}"
|
||||
|
||||
|
||||
class ProfileTrainingComplete:
|
||||
"""Response when training succeeds."""
|
||||
|
||||
def __repr__(self):
|
||||
return "OK"
|
||||
|
||||
|
||||
class Ready:
|
||||
"""Emitted when all actors have been loaded."""
|
||||
|
||||
def __init__(
|
||||
self, timeout: bool = False, problems: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
self.timeout = timeout
|
||||
self.problems = problems or {}
|
||||
|
||||
|
||||
class GetVoiceCommand:
|
||||
"""Request to record a voice command."""
|
||||
|
||||
def __init__(
|
||||
self, receiver: Optional[RhasspyActor] = None, timeout: Optional[float] = None
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class GetActorStates:
|
||||
"""Request for actors' current states."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GetProblems:
|
||||
"""Request any problems during startup."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Problems:
|
||||
"""Response to GetProblems."""
|
||||
|
||||
def __init__(self, problems: Optional[Dict[str, Any]] = None):
|
||||
self.problems = problems or {}
|
||||
|
||||
from rhasspy.wake import get_wake_class
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -225,6 +170,9 @@ class DialogueManager(RhasspyActor):
|
||||
self.word_pronouncer_class: Optional[Type] = None
|
||||
self._word_pronouncer: Optional[RhasspyActor] = None
|
||||
|
||||
# Webhooks
|
||||
self.webhooks: Dict[str, List[str]] = {}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
@@ -303,6 +251,14 @@ class DialogueManager(RhasspyActor):
|
||||
self.send_ready = self.config.get("ready", False)
|
||||
self.observer = self.config.get("observer", None)
|
||||
|
||||
# Load web hooks
|
||||
self.webhooks = self.profile.get("webhooks", {})
|
||||
for hook_event in self.webhooks:
|
||||
# Convert all URLs to lists
|
||||
hook_url = self.webhooks[hook_event]
|
||||
if isinstance(hook_url, str):
|
||||
self.webhooks[hook_event] = [hook_url]
|
||||
|
||||
if self.profile.get("mqtt.enabled", False):
|
||||
self.transition("loading_mqtt")
|
||||
else:
|
||||
@@ -360,7 +316,7 @@ class DialogueManager(RhasspyActor):
|
||||
del self.wait_actors[sender_name]
|
||||
self._logger.debug("%s started", sender_name)
|
||||
|
||||
if len(self.wait_actors) == 0:
|
||||
if not self.wait_actors:
|
||||
self._logger.debug("Actors loaded")
|
||||
self.transition("ready")
|
||||
|
||||
@@ -372,7 +328,7 @@ class DialogueManager(RhasspyActor):
|
||||
if self.send_ready:
|
||||
self.send(self._parent, Ready())
|
||||
elif isinstance(message, WakeupMessage):
|
||||
wait_names = list(self.wait_actors.keys())
|
||||
wait_names = list(self.wait_actors)
|
||||
self._logger.warning(
|
||||
"Actor timeout! Still waiting on %s Loading anyway...", wait_names
|
||||
)
|
||||
@@ -423,6 +379,13 @@ class DialogueManager(RhasspyActor):
|
||||
self.transition("awake")
|
||||
if self.wake_receiver is not None:
|
||||
self.send(self.wake_receiver, message)
|
||||
|
||||
awake_hooks = self.webhooks.get("awake", [])
|
||||
if awake_hooks:
|
||||
hook_json = {"wakewordId": message.name, "siteId": self.site_id}
|
||||
for hook_url in awake_hooks:
|
||||
self._logger.debug("POST-ing to %s", hook_url)
|
||||
requests.post(hook_url, json=hook_json)
|
||||
elif isinstance(message, WakeWordNotDetected):
|
||||
self._logger.debug("Wake word NOT detected. Staying asleep.")
|
||||
self.transition("ready")
|
||||
@@ -484,7 +447,7 @@ class DialogueManager(RhasspyActor):
|
||||
"text": message.text,
|
||||
"likelihood": 1,
|
||||
"seconds": 0,
|
||||
"wakeId": self.wake_detected_name or ""
|
||||
"wakeId": self.wake_detected_name or "",
|
||||
}
|
||||
).encode()
|
||||
|
||||
@@ -511,6 +474,16 @@ class DialogueManager(RhasspyActor):
|
||||
def in_recognizing(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in recognizing state."""
|
||||
if isinstance(message, IntentRecognized):
|
||||
|
||||
if not pydash.get(message.intent, "intent.name", ""):
|
||||
if self.profile.get("intent.error_sound", True):
|
||||
# Play error sound when not recognized
|
||||
wav_path = os.path.expandvars(
|
||||
self.profile.get("sounds.error", None)
|
||||
)
|
||||
if wav_path is not None:
|
||||
self.send(self.player, PlayWavFile(wav_path))
|
||||
|
||||
if self.recorder_class == HTTPAudioRecorder:
|
||||
# Forward to audio recorder
|
||||
self.send(self.recorder, message)
|
||||
@@ -654,7 +627,7 @@ class DialogueManager(RhasspyActor):
|
||||
if actor != sender
|
||||
}
|
||||
|
||||
if len(self.wait_actors) == 0:
|
||||
if not self.wait_actors:
|
||||
self._logger.info("Actors reloaded")
|
||||
self.transition("ready")
|
||||
self.send(self.training_receiver, ProfileTrainingComplete())
|
||||
@@ -726,6 +699,7 @@ class DialogueManager(RhasspyActor):
|
||||
play=message.play,
|
||||
voice=message.voice,
|
||||
language=message.language,
|
||||
siteId=message.siteId,
|
||||
),
|
||||
)
|
||||
elif isinstance(message, TrainProfile):
|
||||
@@ -762,7 +736,7 @@ class DialogueManager(RhasspyActor):
|
||||
def handle_transition(self, message: StateTransition, sender: RhasspyActor) -> None:
|
||||
"""Report state transition of actor."""
|
||||
self.actor_states[message.name] = message.to_state
|
||||
topic = "rhasspy/%s/transition/%s" % (self.profile.name, message.name)
|
||||
topic = f"rhasspy/{self.profile.name}/transition/{message.name}"
|
||||
payload = message.to_state.encode()
|
||||
|
||||
if self.mqtt is not None:
|
||||
@@ -791,11 +765,11 @@ class DialogueManager(RhasspyActor):
|
||||
recorder_system = message.system
|
||||
|
||||
recorder_class = get_microphone_class(recorder_system)
|
||||
test_path = "microphone.%s.test_chunk_size" % recorder_system
|
||||
test_path = f"microphone.{recorder_system}.test_chunk_size"
|
||||
chunk_size = int(self.profile.get(test_path, 1024))
|
||||
|
||||
assert recorder_class is not None
|
||||
test_mics = recorder_class.test_microphones(chunk_size)
|
||||
test_mics = recorder_class.test_microphones(chunk_size) # type: ignore
|
||||
self.send(sender, test_mics)
|
||||
elif isinstance(message, GetSpeakers):
|
||||
# Get all speakers
|
||||
@@ -922,5 +896,5 @@ class DialogueManager(RhasspyActor):
|
||||
)
|
||||
self.wait_actors[name] = actor
|
||||
|
||||
actor_names = list(self.wait_actors.keys())
|
||||
actor_names = list(self.wait_actors)
|
||||
self._logger.debug("Actors created. Waiting for %s to start.", actor_names)
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
"""Actor events for Rhasspy"""
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pywrapfst as fst
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Wake
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ListenForWakeWord:
|
||||
"""Request to start listening for a wake word."""
|
||||
|
||||
def __init__(self, receiver: Optional[RhasspyActor] = None, record=True) -> None:
|
||||
self.receiver = receiver
|
||||
self.record = record
|
||||
|
||||
|
||||
class StopListeningForWakeWord:
|
||||
"""Request to stop listening for a wake word."""
|
||||
|
||||
def __init__(
|
||||
self, receiver: Optional[RhasspyActor] = None, record=True, clear_all=False
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.record = record
|
||||
self.clear_all = clear_all
|
||||
|
||||
|
||||
class WakeWordDetected:
|
||||
"""Response when wake word is detected."""
|
||||
|
||||
def __init__(self, name: str, audio_data_info: Dict[Any, Any] = None) -> None:
|
||||
self.name = name
|
||||
self.audio_data_info = audio_data_info or {}
|
||||
|
||||
|
||||
class WakeWordNotDetected:
|
||||
"""Response when wake word is not detected."""
|
||||
|
||||
def __init__(self, name: str, audio_data_info: Dict[Any, Any] = None) -> None:
|
||||
self.name = name
|
||||
self.audio_data_info = audio_data_info or {}
|
||||
|
||||
|
||||
class PauseListeningForWakeWord:
|
||||
"""Pause wake word detection."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResumeListeningForWakeWord:
|
||||
"""Resume wake word detection."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# audio Player
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PlayWavFile:
|
||||
"""Play a WAV file."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wav_path: str,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
siteId: Optional[str] = None,
|
||||
) -> None:
|
||||
self.wav_path = wav_path
|
||||
self.receiver = receiver
|
||||
self.siteId = siteId
|
||||
|
||||
|
||||
class PlayWavData:
|
||||
"""Play a WAV buffer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wav_data: bytes,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
siteId: Optional[str] = None,
|
||||
) -> None:
|
||||
self.wav_data = wav_data
|
||||
self.receiver = receiver
|
||||
self.siteId = siteId
|
||||
|
||||
|
||||
class WavPlayed:
|
||||
"""Response to PlayWavFile or PlayWavData."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Audio Recording
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AudioData:
|
||||
"""Raw 16-bit 16Khz audio data."""
|
||||
|
||||
def __init__(self, data: bytes, **kwargs: Any) -> None:
|
||||
self.data = data
|
||||
self.info = kwargs
|
||||
|
||||
|
||||
class StartStreaming:
|
||||
"""Tells microphone to begin recording. Emits AudioData chunks."""
|
||||
|
||||
def __init__(self, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class StopStreaming:
|
||||
"""Tells microphone to stop recording."""
|
||||
|
||||
def __init__(self, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class StartRecordingToBuffer:
|
||||
"""Tells microphone to record audio data to named buffer."""
|
||||
|
||||
def __init__(self, buffer_name: str) -> None:
|
||||
self.buffer_name = buffer_name
|
||||
|
||||
|
||||
class StopRecordingToBuffer:
|
||||
"""Tells microphone to stop recording to buffer and emit AudioData."""
|
||||
|
||||
def __init__(
|
||||
self, buffer_name: str, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.buffer_name = buffer_name
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Command Listener
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ListenForCommand:
|
||||
"""Tell Rhasspy to listen for a voice command."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
timeout: Optional[float] = None,
|
||||
entities: List[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
self.timeout = timeout
|
||||
self.entities = entities or []
|
||||
|
||||
|
||||
class VoiceCommand:
|
||||
"""Response to ListenForCommand."""
|
||||
|
||||
def __init__(self, data: bytes, timeout: bool = False, handle: bool = True) -> None:
|
||||
self.data = data
|
||||
self.timeout = timeout
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Intent Recognition
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RecognizeIntent:
|
||||
"""Request to recognize an intent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
confidence: float = 1,
|
||||
) -> None:
|
||||
self.text = text
|
||||
self.confidence = confidence
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
|
||||
|
||||
class IntentRecognized:
|
||||
"""Response to RecognizeIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any], handle: bool = True) -> None:
|
||||
self.intent = intent
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Intent Handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class HandleIntent:
|
||||
"""Request to handle intent."""
|
||||
|
||||
def __init__(
|
||||
self, intent: Dict[str, Any], receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent = intent
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentHandled:
|
||||
"""Response to HandleIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any]) -> None:
|
||||
self.intent = intent
|
||||
|
||||
|
||||
class ForwardIntent:
|
||||
"""Request intent be forwarded to Home Assistant."""
|
||||
|
||||
def __init__(
|
||||
self, intent: Dict[str, Any], receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent = intent
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentForwarded:
|
||||
"""Response to ForwardIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any]) -> None:
|
||||
self.intent = intent
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Intent Handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TrainIntent:
|
||||
"""Request to train intent recognizer."""
|
||||
|
||||
def __init__(self, intent_fst, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentTrainingComplete:
|
||||
"""Response when training is successful."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntentTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MQTT
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MqttPublish:
|
||||
"""Request to publish payload to topic."""
|
||||
|
||||
def __init__(self, topic: str, payload: bytes) -> None:
|
||||
self.topic = topic
|
||||
self.payload = payload
|
||||
|
||||
|
||||
class MqttSubscribe:
|
||||
"""Request to subscribe to a topic."""
|
||||
|
||||
def __init__(self, topic: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.topic = topic
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class MqttConnected:
|
||||
"""Response when connected to broker."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MqttDisconnected:
|
||||
"""Response when disconnected from broker."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MqttMessage:
|
||||
"""Response when MQTT message is received."""
|
||||
|
||||
def __init__(self, topic: str, payload: bytes) -> None:
|
||||
self.topic = topic
|
||||
self.payload = payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Word Pronunciation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SpeakWord:
|
||||
"""Speak a word's pronunciation"""
|
||||
|
||||
def __init__(self, word: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.word = word
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordSpoken:
|
||||
"""Response to SpeakWord"""
|
||||
|
||||
def __init__(self, word: str, wav_data: bytes, phonemes: str) -> None:
|
||||
self.word = word
|
||||
self.wav_data = wav_data
|
||||
self.phonemes = phonemes
|
||||
|
||||
|
||||
class GetWordPhonemes:
|
||||
"""Get eSpeak phonemes for a word"""
|
||||
|
||||
def __init__(self, word: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.word = word
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordPhonemes:
|
||||
"""Response to GetWordPhonemes"""
|
||||
|
||||
def __init__(self, word: str, phonemes: str) -> None:
|
||||
self.word = word
|
||||
self.phonemes = phonemes
|
||||
|
||||
|
||||
class GetWordPronunciations:
|
||||
"""Look up or guess word pronunciation(s)"""
|
||||
|
||||
def __init__(
|
||||
self, words: List[str], n: int = 5, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.words = words
|
||||
self.n = n
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordPronunciations:
|
||||
"""Response to GetWordPronunciations"""
|
||||
|
||||
def __init__(self, pronunciations: Dict[str, Dict[str, Any]]) -> None:
|
||||
self.pronunciations = pronunciations
|
||||
|
||||
|
||||
class PronunciationFailed:
|
||||
"""Response when g2p fails"""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Speech to Text
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TranscribeWav:
|
||||
"""Request to transcribe text from WAV buffer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wav_data: bytes,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
) -> None:
|
||||
self.wav_data = wav_data
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
|
||||
|
||||
class WavTranscription:
|
||||
"""Response to TranscribeWav."""
|
||||
|
||||
def __init__(self, text: str, handle: bool = True, confidence: float = 1) -> None:
|
||||
self.text = text
|
||||
self.confidence = confidence
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Speech Training
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TrainSpeech:
|
||||
"""Request to train speech to text system."""
|
||||
|
||||
def __init__(
|
||||
self, intent_fst: fst.Fst, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class SpeechTrainingComplete:
|
||||
"""Response when training is successful."""
|
||||
|
||||
def __init__(self, intent_fst: fst.Fst) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
|
||||
|
||||
class SpeechTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Text to Speech
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SpeakSentence:
|
||||
"""Request to speak a sentence."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sentence: str,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
play: bool = True,
|
||||
voice: Optional[str] = None,
|
||||
language: Optional[str] = None,
|
||||
siteId: Optional[str] = None,
|
||||
) -> None:
|
||||
self.sentence = sentence
|
||||
self.receiver = receiver
|
||||
self.play = play
|
||||
self.voice = voice
|
||||
self.language = language
|
||||
self.siteId = siteId
|
||||
|
||||
|
||||
class SentenceSpoken:
|
||||
"""Response when sentence is spoken."""
|
||||
|
||||
def __init__(self, wav_data: Optional[bytes] = None):
|
||||
self.wav_data: bytes = wav_data or bytes()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dialogue
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GetMicrophones:
|
||||
"""Request list of micrphones."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class TestMicrophones:
|
||||
"""Request live microphones."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class GetSpeakers:
|
||||
"""Request list of audio players."""
|
||||
|
||||
def __init__(self, system: Optional[str] = None) -> None:
|
||||
self.system = system
|
||||
|
||||
|
||||
class TrainProfile:
|
||||
"""Request training for profile."""
|
||||
|
||||
def __init__(
|
||||
self, receiver: Optional[RhasspyActor] = None, reload_actors: bool = True
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.reload_actors = reload_actors
|
||||
|
||||
|
||||
class ProfileTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str):
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
return f"FAILED: {self.reason}"
|
||||
|
||||
|
||||
class ProfileTrainingComplete:
|
||||
"""Response when training succeeds."""
|
||||
|
||||
def __repr__(self):
|
||||
return "OK"
|
||||
|
||||
|
||||
class Ready:
|
||||
"""Emitted when all actors have been loaded."""
|
||||
|
||||
def __init__(
|
||||
self, timeout: bool = False, problems: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
self.timeout = timeout
|
||||
self.problems = problems or {}
|
||||
|
||||
|
||||
class GetVoiceCommand:
|
||||
"""Request to record a voice command."""
|
||||
|
||||
def __init__(
|
||||
self, receiver: Optional[RhasspyActor] = None, timeout: Optional[float] = None
|
||||
) -> None:
|
||||
self.receiver = receiver
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class GetActorStates:
|
||||
"""Request for actors' current states."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GetProblems:
|
||||
"""Request any problems during startup."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Problems:
|
||||
"""Response to GetProblems."""
|
||||
|
||||
def __init__(self, problems: Optional[Dict[str, Any]] = None):
|
||||
self.problems = problems or {}
|
||||
+85
-43
@@ -1,12 +1,14 @@
|
||||
"""Support for intent recognition."""
|
||||
import concurrent.futures
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Type
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import networkx as nx
|
||||
@@ -15,38 +17,9 @@ import requests
|
||||
from rhasspynlu import json_to_graph, recognize
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.tts import SpeakSentence
|
||||
from rhasspy.events import IntentRecognized, RecognizeIntent, SpeakSentence
|
||||
from rhasspy.utils import empty_intent, hass_request_kwargs
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RecognizeIntent:
|
||||
"""Request to recognize an intent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
confidence: float = 1,
|
||||
) -> None:
|
||||
self.text = text
|
||||
self.confidence = confidence
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
|
||||
|
||||
class IntentRecognized:
|
||||
"""Response to RecognizeIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any], handle: bool = True) -> None:
|
||||
self.intent = intent
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -62,7 +35,7 @@ def get_recognizer_class(system: str) -> Type[RhasspyActor]:
|
||||
"flair",
|
||||
"conversation",
|
||||
"command",
|
||||
], ("Invalid intent system: %s" % system)
|
||||
], f"Invalid intent system: {system}"
|
||||
|
||||
if system == "fsticuffs":
|
||||
# Use OpenFST locally
|
||||
@@ -164,6 +137,32 @@ class RemoteRecognizer(RhasspyActor):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CliConverter:
|
||||
"""Command-line converter for intent recognition"""
|
||||
|
||||
def __init__(self, name: str, command_path: Path):
|
||||
self.name = name
|
||||
self.command_path = command_path
|
||||
|
||||
def __call__(self, *args, converter_args=None):
|
||||
"""Runs external program to convert JSON values"""
|
||||
converter_args = converter_args or []
|
||||
proc = subprocess.Popen(
|
||||
[str(self.command_path)] + converter_args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
with io.StringIO() as input_file:
|
||||
for arg in args:
|
||||
json.dump(arg, input_file)
|
||||
|
||||
stdout, _ = proc.communicate(input=input_file.getvalue())
|
||||
|
||||
return [json.loads(line) for line in stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
class FsticuffsRecognizer(RhasspyActor):
|
||||
"""Recognize intents using OpenFST."""
|
||||
|
||||
@@ -173,6 +172,7 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
self.words: Set[str] = set()
|
||||
self.stop_words: Set[str] = set()
|
||||
self.fuzzy: bool = True
|
||||
self.converters: Dict[str, Callable[..., Any]] = {}
|
||||
self.preload: bool = False
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
@@ -186,6 +186,36 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
|
||||
# True if fuzzy search should be used (default)
|
||||
self.fuzzy = self.profile.get("intent.fsticuffs.fuzzy", True)
|
||||
|
||||
# Load user-defined converters
|
||||
converters_dir = Path(
|
||||
self.profile.read_path(
|
||||
self.profile.get("intent.fsticuffs.converters_dir", "converters")
|
||||
)
|
||||
)
|
||||
if converters_dir.is_dir():
|
||||
self._logger.debug("Loading converters from %s", converters_dir)
|
||||
for converter_path in converters_dir.glob("**/*"):
|
||||
if not converter_path.is_file():
|
||||
continue
|
||||
|
||||
# Retain directory structure in name
|
||||
converter_name = str(
|
||||
converter_path.relative_to(converters_dir).with_suffix("")
|
||||
)
|
||||
|
||||
# Run converter as external program.
|
||||
# Input arguments are encoded as JSON on individual lines.
|
||||
# Output values should be encoded as JSON on individual lines.
|
||||
converter = CliConverter(converter_name, converter_path)
|
||||
|
||||
# Key off name without file extension
|
||||
self.converters[converter_name] = converter
|
||||
|
||||
self._logger.debug(
|
||||
"Loaded converter %s from %s", converter_name, converter_path
|
||||
)
|
||||
|
||||
self.transition("loaded")
|
||||
|
||||
def in_loaded(self, message: Any, sender: RhasspyActor) -> None:
|
||||
@@ -203,7 +233,11 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
tokens = [w for w in tokens if w in self.words]
|
||||
|
||||
recognitions = recognize(
|
||||
tokens, self.graph, fuzzy=self.fuzzy, stop_words=self.stop_words
|
||||
tokens,
|
||||
self.graph,
|
||||
fuzzy=self.fuzzy,
|
||||
stop_words=self.stop_words,
|
||||
extra_converters=self.converters,
|
||||
)
|
||||
assert recognitions, "No intent recognized"
|
||||
|
||||
@@ -215,6 +249,8 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
except Exception:
|
||||
self._logger.exception("in_loaded")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
@@ -248,9 +284,7 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
self._logger.debug("Using stop words at %s", stop_words_path)
|
||||
with open(stop_words_path, "r") as stop_words_file:
|
||||
self.stop_words = {
|
||||
line.strip()
|
||||
for line in stop_words_file
|
||||
if len(line.strip()) > 0
|
||||
line.strip() for line in stop_words_file if line.strip()
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -312,6 +346,8 @@ class FuzzyWuzzyRecognizer(RhasspyActor):
|
||||
except Exception:
|
||||
self._logger.exception("in_loaded")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
@@ -324,7 +360,7 @@ class FuzzyWuzzyRecognizer(RhasspyActor):
|
||||
def recognize(self, text: str) -> Dict[str, Any]:
|
||||
"""Find sentence with lowest string-edit distance."""
|
||||
confidence = 0
|
||||
if len(text) > 0:
|
||||
if text:
|
||||
assert self.examples is not None, "No examples JSON"
|
||||
|
||||
choices: Dict[str, Tuple[str, Dict[str, Any]]] = {}
|
||||
@@ -334,7 +370,6 @@ class FuzzyWuzzyRecognizer(RhasspyActor):
|
||||
sentences = []
|
||||
for example in intent_examples:
|
||||
example_text = example.get("raw_text", example["text"])
|
||||
logging.debug(example_text)
|
||||
choices[example_text] = (example_text, example)
|
||||
sentences.append(example_text)
|
||||
|
||||
@@ -419,7 +454,7 @@ class RasaIntentRecognizer(RhasspyActor):
|
||||
rasa_config = self.profile.get("intent.rasa", {})
|
||||
url = rasa_config.get("url", "http://localhost:5005")
|
||||
self.project_name = rasa_config.get(
|
||||
"project_name", "rhasspy_%s" % self.profile.name
|
||||
"project_name", f"rhasspy_{self.profile.name}"
|
||||
)
|
||||
self.parse_url = urljoin(url, "model/parse")
|
||||
|
||||
@@ -433,6 +468,7 @@ class RasaIntentRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_started")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
@@ -493,6 +529,8 @@ class AdaptIntentRecognizer(RhasspyActor):
|
||||
except Exception:
|
||||
self._logger.exception("in_loaded")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
@@ -508,11 +546,11 @@ class AdaptIntentRecognizer(RhasspyActor):
|
||||
assert self.engine is not None, "Adapt engine not loaded"
|
||||
intents = [intent for intent in self.engine.determine_intent(text) if intent]
|
||||
|
||||
if len(intents) > 0:
|
||||
if intents:
|
||||
# Return the best intent only
|
||||
intent = max(intents, key=lambda x: x.get("confidence", 0))
|
||||
intent_type = intent["intent_type"]
|
||||
entity_prefix = "{0}.".format(intent_type)
|
||||
entity_prefix = f"{intent_type}."
|
||||
|
||||
slots = {}
|
||||
for key, value in intent.items():
|
||||
@@ -539,6 +577,7 @@ class AdaptIntentRecognizer(RhasspyActor):
|
||||
def load_engine(self) -> None:
|
||||
"""Configure Adapt engine if not already cached."""
|
||||
if self.engine is None:
|
||||
# pylint: disable=E0401
|
||||
from adapt.intent import IntentBuilder
|
||||
from adapt.engine import IntentDeterminationEngine
|
||||
|
||||
@@ -616,6 +655,7 @@ class FlairRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_started")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
@@ -634,14 +674,14 @@ class FlairRecognizer(RhasspyActor):
|
||||
assert self.intent_map is not None
|
||||
if self.class_model is not None:
|
||||
self.class_model.predict(sentence)
|
||||
assert len(sentence.labels) > 0, "No intent predicted"
|
||||
assert sentence.labels, "No intent predicted"
|
||||
|
||||
label = sentence.labels[0]
|
||||
intent_id = label.value
|
||||
intent["intent"]["confidence"] = label.score
|
||||
else:
|
||||
# Assume first intent
|
||||
intent_id = next(iter(self.intent_map.keys()))
|
||||
intent_id = next(iter(self.intent_map))
|
||||
intent["intent"]["confidence"] = 1
|
||||
|
||||
intent["intent"]["name"] = self.intent_map[intent_id]
|
||||
@@ -774,6 +814,7 @@ class HomeAssistantConversationRecognizer(RhasspyActor):
|
||||
# Return empty intent since conversation doesn't give it to us
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(message.receiver or sender, IntentRecognized(intent))
|
||||
|
||||
@@ -820,6 +861,7 @@ class CommandRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_started")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
|
||||
@@ -10,53 +10,17 @@ import pydash
|
||||
import requests
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.tts import SpeakSentence
|
||||
from rhasspy.events import (ForwardIntent, HandleIntent, IntentForwarded,
|
||||
IntentHandled, SpeakSentence)
|
||||
from rhasspy.utils import hass_request_kwargs
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class HandleIntent:
|
||||
"""Request to handle intent."""
|
||||
|
||||
def __init__(
|
||||
self, intent: Dict[str, Any], receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent = intent
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentHandled:
|
||||
"""Response to HandleIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any]) -> None:
|
||||
self.intent = intent
|
||||
|
||||
|
||||
class ForwardIntent:
|
||||
"""Request intent be forwarded to Home Assistant."""
|
||||
|
||||
def __init__(
|
||||
self, intent: Dict[str, Any], receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent = intent
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentForwarded:
|
||||
"""Response to ForwardIntent."""
|
||||
|
||||
def __init__(self, intent: Dict[str, Any]) -> None:
|
||||
self.intent = intent
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_intent_handler_class(system: str) -> Type[RhasspyActor]:
|
||||
"""Get type for profile intent handlers."""
|
||||
assert system in ["dummy", "hass", "remote", "command"], (
|
||||
"Invalid intent handler system: %s" % system
|
||||
f"Invalid intent handler system: {system}"
|
||||
)
|
||||
|
||||
if system == "hass":
|
||||
@@ -95,7 +59,12 @@ class DummyIntentHandler(RhasspyActor):
|
||||
|
||||
|
||||
class HomeAssistantHandleType(str, Enum):
|
||||
"""Method used to communicate intents to Home Assistnat"""
|
||||
|
||||
# Send events to /api/events
|
||||
EVENT = "event"
|
||||
|
||||
# Send intents to /api/intent
|
||||
INTENT = "intent"
|
||||
|
||||
|
||||
@@ -127,7 +96,7 @@ class HomeAssistantIntentHandler(RhasspyActor):
|
||||
|
||||
# PEM file for self-signed HA certificates
|
||||
self.pem_file = self.hass_config.get("pem_file", "")
|
||||
if (self.pem_file is not None) and (len(self.pem_file) > 0):
|
||||
if (self.pem_file is not None) and self.pem_file:
|
||||
self.pem_file = os.path.expandvars(self.pem_file)
|
||||
self._logger.debug("Using PEM file at %s", self.pem_file)
|
||||
else:
|
||||
|
||||
+23
-47
@@ -9,39 +9,15 @@ import tempfile
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from io import StringIO
|
||||
from typing import Any, Dict, List, Optional, Set, Type
|
||||
from typing import Any, Dict, List, Set, Type
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (IntentTrainingComplete, IntentTrainingFailed,
|
||||
TrainIntent)
|
||||
from rhasspy.utils import (lcm, make_sentences_by_intent,
|
||||
sample_sentences_by_intent)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TrainIntent:
|
||||
"""Request to train intent recognizer."""
|
||||
|
||||
def __init__(self, intent_fst, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class IntentTrainingComplete:
|
||||
"""Response when training is successful."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntentTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -59,7 +35,7 @@ def get_intent_trainer_class(
|
||||
"flair",
|
||||
"auto",
|
||||
"command",
|
||||
], ("Invalid intent training system: %s" % trainer_system)
|
||||
], f"Invalid intent training system: {trainer_system}"
|
||||
|
||||
if trainer_system == "auto":
|
||||
# Use intent recognizer system
|
||||
@@ -238,7 +214,7 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
) as training_file:
|
||||
|
||||
training_config = StringIO()
|
||||
training_config.write('language: "%s"\n' % language)
|
||||
training_config.write(f'language: "{language}"\n')
|
||||
training_config.write('pipeline: "pretrained_embeddings_spacy"\n')
|
||||
|
||||
# Write markdown directly into YAML.
|
||||
@@ -247,12 +223,12 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
blank_line = False
|
||||
for line in examples_md_file:
|
||||
line = line.strip()
|
||||
if len(line) > 0:
|
||||
if line:
|
||||
if blank_line:
|
||||
print("", file=training_file)
|
||||
blank_line = False
|
||||
|
||||
print(" %s" % line, file=training_file)
|
||||
print(f" {line}", file=training_file)
|
||||
else:
|
||||
blank_line = True
|
||||
|
||||
@@ -280,11 +256,7 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
# Rasa gives quite helpful error messages, so extract them from the response.
|
||||
raise Exception(
|
||||
"{0}: {1}".format(
|
||||
response.reason, json.loads(response.content)["message"]
|
||||
)
|
||||
)
|
||||
raise Exception(f'{response.reason}: {json.loads(response.content)["message"]}')
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -316,7 +288,7 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
if os.path.exists(stop_words_path):
|
||||
with open(stop_words_path, "r") as stop_words_file:
|
||||
stop_words = {
|
||||
line.strip() for line in stop_words_file if len(line.strip()) > 0
|
||||
line.strip() for line in stop_words_file if line.strip()
|
||||
}
|
||||
|
||||
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
|
||||
@@ -354,7 +326,7 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
# Add entities
|
||||
for entity_name, entity_values in slot_entities.items():
|
||||
# Prefix entity name with intent name
|
||||
entity_name = "{0}.{1}".format(intent_name, entity_name)
|
||||
entity_name = f"{intent_name}.{entity_name}"
|
||||
if entity_name not in entities:
|
||||
entities[entity_name] = set()
|
||||
|
||||
@@ -386,15 +358,15 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
# Word only exists in some sentences
|
||||
optional_words.add(word)
|
||||
|
||||
if len(required_words) > 0:
|
||||
if required_words:
|
||||
# Create entity for required keywords
|
||||
entity_name = "{0}RequiredKeyword".format(intent_name)
|
||||
entity_name = f"{intent_name}RequiredKeyword"
|
||||
entities[entity_name] = required_words
|
||||
intent["require"].append(entity_name)
|
||||
|
||||
if len(optional_words) > 0:
|
||||
if optional_words:
|
||||
# Create entity for required keywords
|
||||
entity_name = "{0}OptionalKeyword".format(intent_name)
|
||||
entity_name = f"{intent_name}OptionalKeyword"
|
||||
entities[entity_name] = optional_words
|
||||
intent["optionally"].append(entity_name)
|
||||
|
||||
@@ -453,16 +425,20 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
"""Train intent classifier and named entity recognizers."""
|
||||
# pylint: disable=E0401
|
||||
from flair.data import Sentence, Token
|
||||
|
||||
# pylint: disable=E0401
|
||||
from flair.models import SequenceTagger, TextClassifier
|
||||
|
||||
# pylint: disable=E0401
|
||||
from flair.embeddings import (
|
||||
FlairEmbeddings,
|
||||
StackedEmbeddings,
|
||||
DocumentRNNEmbeddings,
|
||||
)
|
||||
|
||||
# pylint: disable=E0401
|
||||
from flair.data import TaggedCorpus
|
||||
|
||||
# pylint: disable=E0401
|
||||
from flair.trainers import ModelTrainer
|
||||
|
||||
@@ -482,7 +458,7 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
shutil.rmtree(data_dir)
|
||||
|
||||
self.embeddings = self.profile.get("intent.flair.embeddings", [])
|
||||
assert len(self.embeddings) > 0, "No word embeddings"
|
||||
assert self.embeddings, "No word embeddings"
|
||||
|
||||
# Create directories to write training data to
|
||||
class_data_dir = os.path.join(data_dir, "classification")
|
||||
@@ -517,7 +493,7 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
|
||||
intent_fst_paths = {
|
||||
intent_id: os.path.join(fsts_dir, f"{intent_id}.fst")
|
||||
for intent_id in intent_map.keys()
|
||||
for intent_id in intent_map
|
||||
}
|
||||
|
||||
# Generate samples
|
||||
@@ -559,7 +535,7 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
|
||||
class_sentences.append(class_sent)
|
||||
|
||||
if len(intent_sent["entities"]) == 0:
|
||||
if not intent_sent["entities"]:
|
||||
continue # no entities, no sequence tagger
|
||||
|
||||
# Named entity recognition (NER) example
|
||||
@@ -604,7 +580,7 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
for e in self.embeddings
|
||||
]
|
||||
|
||||
if len(class_sentences) > 0:
|
||||
if class_sentences:
|
||||
self._logger.debug("Training intent classifier")
|
||||
|
||||
# Random 80/10/10 split
|
||||
@@ -633,7 +609,7 @@ class FlairIntentTrainer(RhasspyActor):
|
||||
else:
|
||||
self._logger.info("Skipping intent classifier training")
|
||||
|
||||
if len(ner_sentences) > 0:
|
||||
if ner_sentences:
|
||||
self._logger.debug("Training %s NER sequence tagger(s)", len(ner_sentences))
|
||||
|
||||
# Named entity recognition
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
# and on Bitbucket https://bitbucket.org/ssb22/lexconvert
|
||||
# although some early ones are missing.
|
||||
|
||||
# type: ignore
|
||||
|
||||
def Phonemes():
|
||||
"""Create phonemes by calling vowel(), consonant(),
|
||||
@@ -305,9 +306,9 @@ def LexFormats():
|
||||
lex_header=";; -*- mode: lisp -*-\n(eval (list voice_default))\n",
|
||||
lex_read_function=lambda *args: eval(
|
||||
"["
|
||||
+ commands.getoutput(
|
||||
"grep -vi parameter.set < ~/.festivalrc | grep -v '(eval' | sed -e 's/;.*//' -e 's/.lex.add.entry//' -e s/\"'\"'[(] *\"/[\"/' -e 's/\" [^ ]* /\",(\"/' -e 's/\".*$/&\"],/' -e 's/[()]/ /g' -e 's/ */ /g'"
|
||||
)
|
||||
# + commands.getoutput(
|
||||
# "grep -vi parameter.set < ~/.festivalrc | grep -v '(eval' | sed -e 's/;.*//' -e 's/.lex.add.entry//' -e s/\"'\"'[(] *\"/[\"/' -e 's/\" [^ ]* /\",(\"/' -e 's/\".*$/&\"],/' -e 's/[()]/ /g' -e 's/ */ /g'"
|
||||
# )
|
||||
+ "]"
|
||||
),
|
||||
safe_to_drop_characters=True, # TODO: really? (could instead give a string of known-safe characters)
|
||||
@@ -3317,7 +3318,7 @@ def make_dictionary(sourceName, destName):
|
||||
return d
|
||||
|
||||
|
||||
warnedAlready = set()
|
||||
warnedAlready = set() # type: ignore
|
||||
|
||||
|
||||
def convert(pronunc, source, dest):
|
||||
@@ -4569,7 +4570,7 @@ class MacBritish_System_Lexicon(object):
|
||||
so you can substitute these into your texts.
|
||||
Restores the lexicon on close()."""
|
||||
|
||||
instances = {}
|
||||
instances = {} # type: ignore
|
||||
|
||||
def __init__(self, text="", voice="Daniel"):
|
||||
"""text is the text you want to speak (so that any
|
||||
|
||||
+9
-39
@@ -5,49 +5,17 @@ import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from queue import Queue
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pydash
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (MqttConnected, MqttDisconnected, MqttMessage,
|
||||
MqttPublish, MqttSubscribe)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MqttPublish:
|
||||
"""Request to publish payload to topic."""
|
||||
|
||||
def __init__(self, topic: str, payload: bytes) -> None:
|
||||
self.topic = topic
|
||||
self.payload = payload
|
||||
|
||||
|
||||
class MqttSubscribe:
|
||||
"""Request to subscribe to a topic."""
|
||||
|
||||
def __init__(self, topic: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.topic = topic
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class MqttConnected:
|
||||
"""Response when connected to broker."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MqttDisconnected:
|
||||
"""Response when disconnected from broker."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MqttMessage:
|
||||
"""Response when MQTT message is received."""
|
||||
|
||||
def __init__(self, topic: str, payload: bytes) -> None:
|
||||
self.topic = topic
|
||||
self.payload = payload
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MessageReady:
|
||||
@@ -87,7 +55,7 @@ class HermesMqtt(RhasspyActor):
|
||||
"""Transition to started state."""
|
||||
# Load settings
|
||||
self.site_ids = self.profile.get("mqtt.site_id", "default").split(",")
|
||||
if len(self.site_ids) > 0:
|
||||
if self.site_ids:
|
||||
self.site_id = self.site_ids[0]
|
||||
else:
|
||||
self.site_id = "default"
|
||||
@@ -116,7 +84,7 @@ class HermesMqtt(RhasspyActor):
|
||||
self.client.on_message = self.on_message
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
|
||||
if len(self.username) > 0:
|
||||
if self.username:
|
||||
self._logger.debug("Logging in as %s", self.username)
|
||||
self.client.username_pw_set(self.username, self.password)
|
||||
|
||||
@@ -295,6 +263,8 @@ class HermesMqtt(RhasspyActor):
|
||||
}
|
||||
for ev in intent.get("entities", [])
|
||||
],
|
||||
"asrTokens": [],
|
||||
"asrConfidence": 1
|
||||
}
|
||||
).encode()
|
||||
|
||||
|
||||
+5
-65
@@ -7,71 +7,11 @@ from collections import defaultdict
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (GetWordPhonemes, GetWordPronunciations,
|
||||
PronunciationFailed, SpeakWord, WordPhonemes,
|
||||
WordPronunciations, WordSpoken)
|
||||
from rhasspy.utils import load_phoneme_map, read_dict
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SpeakWord:
|
||||
"""Speak a word's pronunciation"""
|
||||
|
||||
def __init__(self, word: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.word = word
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordSpoken:
|
||||
"""Response to SpeakWord"""
|
||||
|
||||
def __init__(self, word: str, wav_data: bytes, phonemes: str) -> None:
|
||||
self.word = word
|
||||
self.wav_data = wav_data
|
||||
self.phonemes = phonemes
|
||||
|
||||
|
||||
class GetWordPhonemes:
|
||||
"""Get eSpeak phonemes for a word"""
|
||||
|
||||
def __init__(self, word: str, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.word = word
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordPhonemes:
|
||||
"""Response to GetWordPhonemes"""
|
||||
|
||||
def __init__(self, word: str, phonemes: str) -> None:
|
||||
self.word = word
|
||||
self.phonemes = phonemes
|
||||
|
||||
|
||||
class GetWordPronunciations:
|
||||
"""Look up or guess word pronunciation(s)"""
|
||||
|
||||
def __init__(
|
||||
self, words: List[str], n: int = 5, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.words = words
|
||||
self.n = n
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class WordPronunciations:
|
||||
"""Response to GetWordPronunciations"""
|
||||
|
||||
def __init__(self, pronunciations: Dict[str, Dict[str, Any]]) -> None:
|
||||
self.pronunciations = pronunciations
|
||||
|
||||
|
||||
class PronunciationFailed:
|
||||
"""Response when g2p fails"""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dummy word pronouncer
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -171,7 +111,7 @@ class PhonetisaurusPronounce(RhasspyActor):
|
||||
def pronounce(self, words: List[str], n: int = 5) -> Dict[str, Dict[str, Any]]:
|
||||
"""Look up or guess word pronunciation(s)"""
|
||||
assert n > 0, "No pronunciations requested"
|
||||
assert len(words) > 0, "No words to look up"
|
||||
assert words, "No words to look up"
|
||||
|
||||
self._logger.debug("Getting pronunciations for %s", words)
|
||||
|
||||
@@ -251,7 +191,7 @@ class PhonetisaurusPronounce(RhasspyActor):
|
||||
unknown_words.add(word)
|
||||
|
||||
# Guess pronunciations for unknown word
|
||||
if len(unknown_words) > 0:
|
||||
if unknown_words:
|
||||
# Path to phonetisaurus FST
|
||||
g2p_path = self.profile.read_path(
|
||||
self.profile.get(
|
||||
|
||||
+3
-28
@@ -12,37 +12,12 @@ from urllib.parse import urljoin
|
||||
import requests
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import TranscribeWav, WavTranscription
|
||||
from rhasspy.utils import convert_wav, hass_request_kwargs, maybe_convert_wav
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TranscribeWav:
|
||||
"""Request to transcribe text from WAV buffer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
wav_data: bytes,
|
||||
receiver: Optional[RhasspyActor] = None,
|
||||
handle: bool = True,
|
||||
) -> None:
|
||||
self.wav_data = wav_data
|
||||
self.receiver = receiver
|
||||
self.handle = handle
|
||||
|
||||
|
||||
class WavTranscription:
|
||||
"""Response to TranscribeWav."""
|
||||
|
||||
def __init__(self, text: str, handle: bool = True, confidence: float = 1) -> None:
|
||||
self.text = text
|
||||
self.confidence = confidence
|
||||
self.handle = handle
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_decoder_class(system: str) -> Type[RhasspyActor]:
|
||||
"""Get type for profile speech to text decoder."""
|
||||
assert system in [
|
||||
@@ -52,7 +27,7 @@ def get_decoder_class(system: str) -> Type[RhasspyActor]:
|
||||
"remote",
|
||||
"hass_stt",
|
||||
"command",
|
||||
], ("Invalid speech to text system: %s" % system)
|
||||
], f"Invalid speech to text system: {system}"
|
||||
|
||||
if system == "pocketsphinx":
|
||||
# Use pocketsphinx locally
|
||||
@@ -590,7 +565,7 @@ class HomeAssistantSTTIntegration(RhasspyActor):
|
||||
audio_data = audio_data[self.chunk_size :]
|
||||
|
||||
# POST WAV data to STT
|
||||
response = requests.post(stt_url, data=generate_chunks(), **kwargs)
|
||||
response = requests.post(stt_url, data=generate_chunks(), **kwargs) # type: ignore
|
||||
response.raise_for_status()
|
||||
|
||||
response_json = response.json()
|
||||
|
||||
+3
-30
@@ -8,37 +8,10 @@ from typing import Any, Dict, List, Optional, Type
|
||||
import pywrapfst as fst
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (SpeechTrainingComplete, SpeechTrainingFailed,
|
||||
TrainSpeech)
|
||||
from rhasspy.train.jsgf2fst import fstprintall, symbols2intent
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TrainSpeech:
|
||||
"""Request to train speech to text system."""
|
||||
|
||||
def __init__(
|
||||
self, intent_fst: fst.Fst, receiver: Optional[RhasspyActor] = None
|
||||
) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
class SpeechTrainingComplete:
|
||||
"""Response when training is successful."""
|
||||
|
||||
def __init__(self, intent_fst: fst.Fst) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
|
||||
|
||||
class SpeechTrainingFailed:
|
||||
"""Response when training fails."""
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -47,7 +20,7 @@ def get_speech_trainer_class(
|
||||
) -> Type[RhasspyActor]:
|
||||
"""Get type for profile speech to text trainer."""
|
||||
assert trainer_system in ["dummy", "pocketsphinx", "kaldi", "auto", "command"], (
|
||||
"Invalid speech training system: %s" % trainer_system
|
||||
f"Invalid speech training system: {trainer_system}"
|
||||
)
|
||||
|
||||
if trainer_system == "auto":
|
||||
|
||||
+217
-31
@@ -7,8 +7,9 @@ import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Tuple
|
||||
from typing import Iterable, List, Optional, Tuple, Union
|
||||
|
||||
import attr
|
||||
from num2words import num2words
|
||||
import pywrapfst as fst
|
||||
|
||||
@@ -71,14 +72,15 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
kaldi_dir = Path(
|
||||
os.path.expandvars(profile.get(f"{stt_prefix}.kaldi_dir", "/opt/kaldi"))
|
||||
)
|
||||
kaldi_graph_dir = acoustic_model / profile.get(f"{stt_prefix}.graph", "graph")
|
||||
|
||||
if acoustic_model_type == "kaldi":
|
||||
# Kaldi acoustic models are inside model directory
|
||||
acoustic_model = ppath(f"{stt_prefix}.model_dir", "model")
|
||||
else:
|
||||
elif acoustic_model_type != "pocketsphinx":
|
||||
_LOGGER.warning("Unsupported acoustic model type: %s", acoustic_model_type)
|
||||
|
||||
kaldi_graph_dir = acoustic_model / profile.get(f"{stt_prefix}.graph", "graph")
|
||||
|
||||
# ignore/upper/lower
|
||||
word_casing = profile.get("speech_to_text.dictionary_casing", "ignore").lower()
|
||||
|
||||
@@ -106,6 +108,13 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
grammar_dir = ppath("speech_to_text.grammars_dir", "grammars", write=True)
|
||||
fsts_dir = ppath("speech_to_text.fsts_dir", "fsts", write=True)
|
||||
slots_dir = ppath("speech_to_text.slots_dir", "slots", write=True)
|
||||
system_slots_dir = Path(profile.system_profiles_dir) / profile.name / "slots"
|
||||
slot_programs_dir = ppath(
|
||||
"speech_to_text.slot_programs_dir", "slot_programs", write=True
|
||||
)
|
||||
system_slot_programs_dir = (
|
||||
Path(profile.system_profiles_dir) / profile.name / "slot_programs"
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -128,6 +137,18 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@attr.s
|
||||
class StaticSlotInfo:
|
||||
name: str = attr.ib()
|
||||
path: Path = attr.ib()
|
||||
|
||||
@attr.s
|
||||
class SlotProgramInfo:
|
||||
key: str = attr.ib()
|
||||
name: str = attr.ib()
|
||||
path: Path = attr.ib()
|
||||
args: Optional[List[str]] = attr.ib(default=None)
|
||||
|
||||
def get_slot_names(item):
|
||||
"""Yield referenced slot names."""
|
||||
if isinstance(item, jsgf.SlotReference):
|
||||
@@ -140,16 +161,104 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
for slot_name in get_slot_names(item.rule_body):
|
||||
yield slot_name
|
||||
|
||||
def split_slot_args(slot_name) -> Tuple[str, Optional[List[str]]]:
|
||||
# Check for arguments.
|
||||
# Slot name retains argument(s).
|
||||
if "," in slot_name:
|
||||
parts = slot_name.split(",")
|
||||
slot_name = parts[0]
|
||||
slot_args = parts[1:]
|
||||
else:
|
||||
slot_args = None
|
||||
|
||||
return slot_name, slot_args
|
||||
|
||||
def find_slot(slot_key: str) -> Union[StaticSlotInfo, SlotProgramInfo]:
|
||||
# Try static user slots
|
||||
slot_path = slots_dir / slot_key
|
||||
if slot_path.is_file():
|
||||
return StaticSlotInfo(name=slot_key, path=slot_path)
|
||||
|
||||
# Try user slot programs
|
||||
slot_name, slot_args = split_slot_args(slot_key)
|
||||
slot_path = slot_programs_dir / slot_name
|
||||
if slot_path.is_file():
|
||||
return SlotProgramInfo(
|
||||
key=slot_key, name=slot_name, path=slot_path, args=slot_args
|
||||
)
|
||||
|
||||
# Try static system slots
|
||||
if slots_dir != system_slots_dir:
|
||||
slot_path = system_slots_dir / slot_key
|
||||
if slot_path.is_file():
|
||||
return StaticSlotInfo(name=slot_key, path=slot_path)
|
||||
|
||||
# Try system slot programs
|
||||
if slot_programs_dir != system_slot_programs_dir:
|
||||
slot_path = system_slot_programs_dir / slot_name
|
||||
if slot_path.is_file():
|
||||
return SlotProgramInfo(
|
||||
key=slot_key, name=slot_name, path=slot_path, args=slot_args
|
||||
)
|
||||
|
||||
# Failed to find slot
|
||||
assert False, (
|
||||
f"Missing file/program for slot {slot_key}"
|
||||
+ f" (tried {slots_dir}, {slot_programs_dir},"
|
||||
+ f" {system_slots_dir}, {system_slot_programs_dir})"
|
||||
)
|
||||
|
||||
# 0..100, -100..100
|
||||
NUMBER_RANGE_PATTERN = re.compile(r"^(-?[0-9]+)\.\.(-?[0-9]+)(,[0-9]+)?$")
|
||||
NUMBER_PATTERN = re.compile(r"^(-?[0-9]+)$")
|
||||
|
||||
def number_range_transform(word):
|
||||
"""Automatically transform number ranges"""
|
||||
if not isinstance(word, jsgf.Word):
|
||||
# Skip anything besides words
|
||||
return
|
||||
|
||||
match = NUMBER_RANGE_PATTERN.match(word.text)
|
||||
|
||||
if not match:
|
||||
return
|
||||
|
||||
try:
|
||||
lower_bound = int(match.group(1))
|
||||
upper_bound = int(match.group(2))
|
||||
step = 1
|
||||
|
||||
if len(match.groups()) > 2:
|
||||
# Exclude ,
|
||||
step = int(match.group(3)[1:])
|
||||
|
||||
# Transform to $rhasspy/number
|
||||
return jsgf.SlotReference(
|
||||
text=word.text,
|
||||
slot_name=f"rhasspy/number,{lower_bound},{upper_bound},{step}",
|
||||
converters=["int"],
|
||||
)
|
||||
except ValueError:
|
||||
# Not a number
|
||||
pass
|
||||
except Exception:
|
||||
_LOGGER.exception("number_range_transform")
|
||||
|
||||
def number_transform(word):
|
||||
"""Automatically transform numbers"""
|
||||
if not isinstance(word, jsgf.Word):
|
||||
# Skip anything besides words
|
||||
return
|
||||
|
||||
try:
|
||||
n = int(word.text)
|
||||
match = NUMBER_PATTERN.match(word.text)
|
||||
|
||||
# 75 -> (seventy five):75
|
||||
if not match:
|
||||
return
|
||||
|
||||
try:
|
||||
n = int(match.group(1))
|
||||
|
||||
# 75 -> (seventy five):75!int
|
||||
number_text = num2words(n, lang=language).replace("-", " ").strip()
|
||||
assert number_text, f"Empty num2words result for {n}"
|
||||
number_words = number_text.split()
|
||||
@@ -158,46 +267,94 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
# Easy case, single word
|
||||
word.text = number_text
|
||||
word.substitution = str(n)
|
||||
else:
|
||||
# Hard case, split into mutliple Words
|
||||
return jsgf.Sequence(
|
||||
text=number_text,
|
||||
type=jsgf.SequenceType.GROUP,
|
||||
substitution=str(n),
|
||||
items=[jsgf.Word(w) for w in number_words],
|
||||
)
|
||||
word.converters = ["int"]
|
||||
return word
|
||||
|
||||
# Hard case, split into mutliple Words
|
||||
return jsgf.Sequence(
|
||||
text=number_text,
|
||||
type=jsgf.SequenceType.GROUP,
|
||||
substitution=str(n),
|
||||
converters=["int"],
|
||||
items=[jsgf.Word(w) for w in number_words],
|
||||
)
|
||||
except ValueError:
|
||||
# Not a number
|
||||
pass
|
||||
except Exception:
|
||||
_LOGGER.exception("number_transform")
|
||||
|
||||
def do_intents_to_graph(intents, slot_names, targets):
|
||||
sentences, replacements = ini_jsgf.split_rules(intents)
|
||||
class SlotProgram:
|
||||
"""Runs a program to generate slot values"""
|
||||
|
||||
# Load slot values
|
||||
for slot_name in slot_names:
|
||||
slot_path = slots_dir / slot_name
|
||||
assert slot_path.is_file(), f"Missing slot file at {slot_path}"
|
||||
def __init__(
|
||||
self, command_path: Path, command_args: Optional[List[str]] = None
|
||||
):
|
||||
self.command_path = command_path
|
||||
self.command_args = command_args
|
||||
self.values: Optional[List[str]] = None
|
||||
|
||||
# Parse each non-empty line as a JSGF sentence
|
||||
slot_values = []
|
||||
with open(slot_path, "r") as slot_file:
|
||||
for line in slot_file:
|
||||
def maybe_generate(self):
|
||||
if self.values is None:
|
||||
command_args = self.command_args or []
|
||||
command = [str(self.command_path)] + command_args
|
||||
|
||||
# Parse each non-empty line as a JSGF sentence
|
||||
self.values = []
|
||||
for line in subprocess.check_output(
|
||||
command, universal_newlines=True
|
||||
).splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
sentence = jsgf.Sentence.parse(line)
|
||||
slot_values.append(sentence)
|
||||
self.values.append(sentence)
|
||||
|
||||
# Replace $slot with sentences
|
||||
replacements[f"${slot_name}"] = slot_values
|
||||
def __iter__(self):
|
||||
self.maybe_generate()
|
||||
return iter(self.values)
|
||||
|
||||
def __len__(self):
|
||||
self.maybe_generate()
|
||||
return len(self.values)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.values[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def do_intents_to_graph(sentences, slot_names, replacements, targets):
|
||||
# Replace actual numbers
|
||||
if profile.get("intent.replace_numbers", True):
|
||||
# Replace numbers in parsed sentences
|
||||
for intent_sentences in sentences.values():
|
||||
for sentence in intent_sentences:
|
||||
jsgf.walk_expression(sentence, number_transform, replacements)
|
||||
|
||||
# Determine whether word casing has to be fixed
|
||||
transform = None
|
||||
if word_casing == "upper":
|
||||
transform = str.upper
|
||||
elif word_casing == "lower":
|
||||
transform = str.lower
|
||||
|
||||
if transform:
|
||||
|
||||
def fix_case(word):
|
||||
if isinstance(word, jsgf.Word):
|
||||
word.text = transform(word.text)
|
||||
|
||||
return word
|
||||
|
||||
# Fix casing
|
||||
for intent_sentences in sentences.values():
|
||||
for sentence in intent_sentences:
|
||||
jsgf.walk_expression(sentence, fix_case, replacements)
|
||||
|
||||
# Convert to directed graph
|
||||
graph = intents_to_graph(intents, replacements)
|
||||
graph = intents_to_graph(sentences, replacements)
|
||||
|
||||
# Write graph to JSON file
|
||||
json_graph = graph_to_json(graph)
|
||||
@@ -206,14 +363,43 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
|
||||
def task_ini_graph():
|
||||
"""sentences.ini -> intent.json"""
|
||||
sentences, replacements = ini_jsgf.split_rules(intents)
|
||||
|
||||
if profile.get("intent.replace_numbers", True):
|
||||
# Replace number ranges with rhasspy/number
|
||||
for intent_sentences in sentences.values():
|
||||
for sentence in intent_sentences:
|
||||
jsgf.walk_expression(sentence, number_range_transform, replacements)
|
||||
|
||||
# Gather used slot names
|
||||
slot_names = set()
|
||||
for intent_name in intents:
|
||||
for item in intents[intent_name]:
|
||||
for slot_name in get_slot_names(item):
|
||||
slot_names.add(slot_name)
|
||||
|
||||
# Load slot values
|
||||
for slot_key in slot_names:
|
||||
slot_info = find_slot(slot_key)
|
||||
|
||||
if isinstance(slot_info, StaticSlotInfo):
|
||||
# Parse each non-empty line as a JSGF sentence
|
||||
slot_values = []
|
||||
with open(slot_info.path, "r") as slot_file:
|
||||
for line in slot_file:
|
||||
line = line.strip()
|
||||
if line:
|
||||
sentence = jsgf.Sentence.parse(line)
|
||||
slot_values.append(sentence)
|
||||
elif isinstance(slot_info, SlotProgramInfo):
|
||||
# Program that will generate values
|
||||
slot_values = SlotProgram(slot_info.path, command_args=slot_info.args)
|
||||
|
||||
# Replace $slot with sentences
|
||||
replacements[f"${slot_key}"] = slot_values
|
||||
|
||||
# Add slot files as dependencies
|
||||
deps = [(slots_dir / slot_name) for slot_name in slot_names]
|
||||
deps = [find_slot(slot_key).path for slot_key in slot_names]
|
||||
|
||||
# Add profile itself as a dependency
|
||||
profile_json_path = profile_dir / "profile.json"
|
||||
@@ -223,7 +409,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
return {
|
||||
"file_dep": ini_paths + deps,
|
||||
"targets": [intent_graph],
|
||||
"actions": [(do_intents_to_graph, [intents, slot_names])],
|
||||
"actions": [(do_intents_to_graph, [sentences, slot_names, replacements])],
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -459,7 +645,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
# This better thing to do would be to create a custom TaskLoader.
|
||||
import inspect
|
||||
|
||||
inspect.getsourcelines = lambda obj: [0, 0]
|
||||
inspect.getsourcelines = lambda obj: [0, 0] # type: ignore
|
||||
|
||||
# Run doit main
|
||||
result = DoitMain(ModuleTaskLoader(locals())).run(sys.argv[1:])
|
||||
|
||||
@@ -43,17 +43,15 @@ def make_grammars(
|
||||
for k, v in config[sec_name].items():
|
||||
if v is None:
|
||||
# Collect non-valued keys as sentences
|
||||
sentences.append("({0})".format(k.strip()))
|
||||
sentences.append(f"({k.strip()})"
|
||||
else:
|
||||
# Collect key/value pairs as JSGF rules
|
||||
rule = "<{0}> = ({1});".format(k, v)
|
||||
rule = f"<{k}> = ({v});"
|
||||
rules.append(rule)
|
||||
|
||||
if len(sentences) > 0:
|
||||
if sentences:
|
||||
# Combine all sentences into one big rule (same name as section)
|
||||
sentences_rule = "public <{0}> = ({1});".format(
|
||||
sec_name, " | ".join(sentences)
|
||||
)
|
||||
sentences_rule = f'public <{sec_name}> = ({" | ".join(sentences)});'
|
||||
rules.insert(0, sentences_rule)
|
||||
|
||||
grammar_rules[sec_name] = rules
|
||||
@@ -69,11 +67,11 @@ def make_grammars(
|
||||
continue
|
||||
|
||||
# Only overwrite grammar file if it contains rules or doesn't yet exist
|
||||
if len(rules) > 0:
|
||||
if rules:
|
||||
with open(grammar_path, "w") as grammar_file:
|
||||
# JSGF header
|
||||
print(f"#JSGF V1.0;", file=grammar_file)
|
||||
print("grammar {0};".format(name), file=grammar_file)
|
||||
print(f"grammar {name};", file=grammar_file)
|
||||
print("", file=grammar_file)
|
||||
|
||||
# Grammar rules
|
||||
|
||||
@@ -143,7 +143,7 @@ class DependencyListener(JsgfListener):
|
||||
in_word = word.split(":", maxsplit=1)[0]
|
||||
|
||||
# Empty input word becomes <eps>
|
||||
if len(in_word) == 0:
|
||||
if not in_word:
|
||||
in_word = self.eps
|
||||
|
||||
# NOTE: Entire word (with ":") is used as output
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user