76 Commits

Author SHA1 Message Date
Michael Hansen d310989555 Fix dictionary with multiple pronunciations 2020-01-07 19:40:59 -05:00
Michael Hansen ed581ecf9d Fixing fuzzywuzzy and others with converters 2020-01-05 20:43:15 -05:00
Michael Hansen f8aedd4ef5 Update Docker update docs 2020-01-05 16:55:37 -05:00
Michael Hansen 14c1386496 Possible fix for threading issues 2020-01-05 16:46:14 -05:00
Michael Hansen 153b642057 Add Rpi Zero to docs 2020-01-05 15:14:45 -05:00
Michael Hansen dec32102dd Merge pull request #146 from esdeboer/freebsd
sed -i is not POSIX compliant, instead make a temp copy and rename to…
2020-01-05 14:50:38 -05:00
Michael Hansen f365c69265 Merge pull request #142 from maxbachmann/cleanup
code cleanup
2020-01-05 14:50:02 -05:00
Michael Hansen 1724c328b7 Trying to fix Docker image 2020-01-05 11:14:56 -05:00
Eric de Boer 6db4a8d341 sed -i is not POSIX compliant, instead make a temp copy and rename to original. 2020-01-05 16:53:38 +01:00
Michael Hansen b70e8a8569 Copying profiles in Docker 2020-01-04 22:41:04 -05:00
Michael Hansen 2e4828da06 Fix dockerignore 2020-01-04 22:34:15 -05:00
Michael Hansen 96cfe69753 Re-generated Dockerfile 2020-01-04 22:02:31 -05:00
maxbachmann 3e8e246c1c swap vars without temp var 2020-01-05 01:02:35 +01:00
Michael Hansen 80a5008b93 Copy built-in slots to Docker 2020-01-04 16:55:38 -05:00
Michael Hansen e26ecf82f1 Bump rhasspy-nly to 0.1.4.1 2020-01-04 16:41:10 -05:00
Michael Hansen e4db52f845 Merge pull request #138 from esdeboer/master
Install dependencies before running yarn build.
2020-01-04 16:38:41 -05:00
Michael Hansen 846313e236 Documented slot programs, number ranges, converters 2020-01-04 16:28:03 -05:00
Michael Hansen b68a3fac4a Add rhasspy/days and rhasspy/months slots 2020-01-04 16:15:34 -05:00
Michael Hansen 7459f0d9d9 Add rhasspy/number 2020-01-04 15:53:30 -05:00
Michael Hansen 617b789d89 Add locales to profiles 2020-01-04 15:53:13 -05:00
Michael Hansen bb20cd280b Transforming number ranges to rhasspy/number 2020-01-04 15:04:52 -05:00
Michael Hansen ce780feb74 Add en rhasspy/number and rhasspy/days slots 2020-01-04 12:31:46 -05:00
Michael Hansen 2225262a53 Add system slots/slot programs 2020-01-04 12:28:29 -05:00
Michael Hansen 5b5529339b Minor clean up in tutorial 2020-01-04 10:46:02 -05:00
Michael Hansen 90f5c5aef7 Touch up tutorials 2020-01-04 10:42:55 -05:00
Eric de Boer 78f263582d Install dependencies before running yarn build. 2020-01-04 09:44:02 +01:00
Michael Hansen 6c8608f1a1 Merge pull request #136 from esdeboer/master
Only download Kaldi when it is requested to be installed.
2020-01-03 17:21:57 -05:00
Michael Hansen 3e5437856b Merge pull request #137 from xLAva/Feature_Fuzzywuzzy_SpeedUp
Fuzzywuzzy: major speed improvement by disabling the debug log spam
2020-01-03 17:21:17 -05:00
Michael Hansen 15aaea2810 Support siteId in /api/text-to-speech 2020-01-03 17:19:27 -05:00
xLAva 61d8930e38 Fuzzywuzzy: major speed improvement by disabling the debug log spam 2020-01-03 22:43:26 +01:00
Eric de Boer 5748b2dc3a Only download Kaldi when it is requested to be installed. 2020-01-03 20:57:21 +01:00
Michael Hansen 8f7158f7cc Only allow a single hotword to be detected by snowboy (single_detection) 2020-01-03 14:11:01 -05:00
Michael Hansen 97226286e3 Fix phonetisaurus download link in build-from-source.sh 2020-01-03 11:46:41 -05:00
Michael Hansen 896b3ddfba Run isort 2020-01-03 11:18:45 -05:00
Michael Hansen 1772f6e740 Add slot programs 2020-01-03 11:17:42 -05:00
Michael Hansen b5dfd6518b Add converter args 2020-01-03 10:36:06 -05:00
Michael Hansen 2730c131d0 Merge pull request #133 from maxbachmann/cleanup
do some code cleanup
2020-01-03 09:37:41 -05:00
maxbachmann 05ded030c8 do some code cleanup 2020-01-03 08:52:49 +01:00
Michael Hansen 3b90383145 Trying to fix Jekyll build errors 2020-01-02 23:22:02 -05:00
Michael Hansen 1bb5462150 Merge pull request #130 from kroka/patch-1
add missing ASR fields for Hermes MQTT publishing
2020-01-02 23:13:26 -05:00
Michael Hansen 95a354e2a3 Merge pull request #132 from maxbachmann/master
correct spelling mistake
2020-01-02 23:13:08 -05:00
Michael Hansen d203a3ed75 Add custom converters (programs) 2020-01-02 17:16:34 -05:00
Michael Hansen 59d473b931 Add number ranges 2020-01-02 16:37:16 -05:00
Michael Hansen 17737f7fed Bump version 2020-01-02 16:22:29 -05:00
Michael Hansen 76cf173849 Doing int conversion with built-in number conversion 2020-01-02 16:20:05 -05:00
maxbachmann 55d1cfacdd correct spelling mistake 2020-01-02 18:52:14 +01:00
Michael Hansen 4f6d02169c Force casing on slot inputs 2020-01-02 11:40:04 -05:00
Michael Hansen 74761b942f Fix overwrite_all in slot params 2020-01-02 10:57:10 -05:00
Michael Hansen b88acb3a34 Parse JSON from requests with json5 2020-01-02 10:47:58 -05:00
kroka 7b323a08bb add missing fields for Hermes publishing
prevents a null pointer access in hermes-python
2020-01-02 16:42:28 +01:00
Michael Hansen ec55dbfa5b Remove --yes from apt-get install commands 2020-01-01 22:37:23 -05:00
Michael Hansen 15af0ae3c1 Merge pull request #121 from jthomasdewald/master
Home Assistant Template Example
2020-01-01 22:35:06 -05:00
Michael Hansen f8542f7ac1 Merge pull request #126 from Romkabouter/remove-x-hassio-key
Change X-HASSIO-KEY to Authorization
2020-01-01 09:06:14 -05:00
Paul Romkes de67b3318c Change X-HASSIO-KEY to Authorization 2020-01-01 11:32:09 +01:00
jthomasdewald b47dca03aa Update command-listener doc
Changed vad_mode description to match webrtcvad docs
2019-12-31 15:03:47 -08:00
Michael Hansen 89a1921c3e Merge 2019-12-31 13:00:48 -05:00
Michael Hansen e5fe2a31b3 Add mypy to check, code cleanup 2019-12-31 12:54:10 -05:00
Michael Hansen afdd241c57 Add awake webhook 2019-12-31 12:40:56 -05:00
Michael Hansen bea38cc64f Fix wakeword issues on TTS pause 2019-12-30 21:49:20 -05:00
Michael Hansen c2562aa674 Don't disable wake system by default with TTS 2019-12-30 17:09:52 -05:00
Michael Hansen 7dec472ec4 Add update instructions to docs 2019-12-28 20:51:07 -05:00
Michael Hansen 007ea4266e Bump version 2019-12-27 22:12:38 -05:00
Michael Hansen a627f8746c Code cleanup 2019-12-27 21:19:46 -05:00
Michael Hansen 13f183afd4 Reset PyAudio on error 2019-12-27 21:17:03 -05:00
Michael Hansen 130cbeb7a8 Consolidate actor events. Stop wake on TTS speak. 2019-12-27 21:07:00 -05:00
jthomasdewald 358e7b087e Home Assistant Template Example
Example files to get Rhasspy to control any light with only one automation and one script.
2019-12-27 15:25:36 -08:00
Michael Hansen 8e2d2f2352 Play error sound when intent not recognized 2019-12-27 11:16:38 -05:00
Michael Hansen ac3c92e24a Fix pop sound in pico2wave 2019-12-27 10:59:52 -05:00
Michael Hansen a501c52954 Add /api/speech-to-text endpoint to docs 2019-12-26 22:29:22 -05:00
Michael Hansen f8f0b48140 Setting text and raw_text when intent is not recognized 2019-12-26 22:11:58 -05:00
Michael Hansen 2a8972fb99 Merge branch 'master' of https://github.com/synesthesiam/rhasspy 2019-12-26 11:10:37 -05:00
Michael Hansen cbbfc23395 Merge pull request #118 from jthomasdewald/master
Clarify intent handling for sever / client setup
2019-12-25 18:04:10 -05:00
Michael Hansen 0c2a1931f6 Merge pull request #115 from frkos/patch-1
Porcupine optimizer tool is deprecated
2019-12-25 18:02:52 -05:00
jthomasdewald 414457f150 Clarify intent handling for sever / client setup 2019-12-25 10:08:18 -08:00
frkos 640be7b0ac Update wake-word.md 2019-12-24 13:02:26 +03:00
frkos 421f59518a Update Porcupine wake-word docs
Porcupine optimizer tool is retired, so the link doesn't work
Accroding to the link https://github.com/Picovoice/porcupine#picovoice-console :
The console succeeds the (now retired) optimizer tool, as it can be used to train custom wake-words (Porcupine .ppn files).
2019-12-24 12:58:52 +03:00
120 changed files with 2804 additions and 1210 deletions
+171 -22
View File
@@ -1,26 +1,175 @@
.git/
.venv/
node_modules/
__pycache__/
test/
tools/
etc/test/
download/precise-engine/
download/kaldi/
opt/
*
!etc/qemu-*
etc/homeassistant/config/.storage
examples/typical/home-assistant/config/.storage
examples/typical-intent/home-assistant/config/.storage
examples/client-server/home-assistant/config/.storage
examples/mqtt-hermes/home-assistant/config/.storage
!download/rhasspy-tools*
!download/pocketsphinx-python.tar.gz
!download/snowboy*
!download/kaldi*
profiles/*/base_dictionary.txt
profiles/*/base_language_model.txt
profiles/*/acoustic_model/
profiles/*/g2p.fst
!requirements.txt
!dist/
!etc/wav
profiles/en-kaldi/
profiles/en-zamia/
!docker/run.sh
!docker/rhasspy
profiles/*/download/
!profiles/defaults.json
!profiles/zh/profile.json
!profiles/zh/custom_words.txt
!profiles/zh/espeak_phonemes.txt
!profiles/zh/phoneme_examples.txt
!profiles/zh/frequent_words.txt
!profiles/zh/sentences.ini
!profiles/zh/stop_words.txt
!profiles/zh/slots
!profiles/zh/slot_programs
!profiles/hi/profile.json
!profiles/hi/custom_words.txt
!profiles/hi/espeak_phonemes.txt
!profiles/hi/phoneme_examples.txt
!profiles/hi/frequent_words.txt
!profiles/hi/sentences.ini
!profiles/hi/stop_words.txt
!profiles/hi/slots
!profiles/hi/slot_programs
!profiles/el/profile.json
!profiles/el/custom_words.txt
!profiles/el/espeak_phonemes.txt
!profiles/el/phoneme_examples.txt
!profiles/el/frequent_words.txt
!profiles/el/sentences.ini
!profiles/el/stop_words.txt
!profiles/el/slots
!profiles/el/slot_programs
!profiles/es/profile.json
!profiles/es/custom_words.txt
!profiles/es/espeak_phonemes.txt
!profiles/es/phoneme_examples.txt
!profiles/es/frequent_words.txt
!profiles/es/sentences.ini
!profiles/es/stop_words.txt
!profiles/es/slots
!profiles/es/slot_programs
!profiles/it/profile.json
!profiles/it/custom_words.txt
!profiles/it/espeak_phonemes.txt
!profiles/it/phoneme_examples.txt
!profiles/it/frequent_words.txt
!profiles/it/sentences.ini
!profiles/it/stop_words.txt
!profiles/it/slots
!profiles/it/slot_programs
!profiles/ru/profile.json
!profiles/ru/custom_words.txt
!profiles/ru/espeak_phonemes.txt
!profiles/ru/phoneme_examples.txt
!profiles/ru/frequent_words.txt
!profiles/ru/sentences.ini
!profiles/ru/stop_words.txt
!profiles/ru/slots
!profiles/ru/slot_programs
!profiles/pt/profile.json
!profiles/pt/custom_words.txt
!profiles/pt/espeak_phonemes.txt
!profiles/pt/phoneme_examples.txt
!profiles/pt/frequent_words.txt
!profiles/pt/sentences.ini
!profiles/pt/stop_words.txt
!profiles/pt/slots
!profiles/pt/slot_programs
!profiles/sv/profile.json
!profiles/sv/custom_words.txt
!profiles/sv/espeak_phonemes.txt
!profiles/sv/phoneme_examples.txt
!profiles/sv/frequent_words.txt
!profiles/sv/sentences.ini
!profiles/sv/stop_words.txt
!profiles/sv/slots
!profiles/sv/slot_programs
!profiles/vi/profile.json
!profiles/vi/custom_words.txt
!profiles/vi/espeak_phonemes.txt
!profiles/vi/phoneme_examples.txt
!profiles/vi/frequent_words.txt
!profiles/vi/sentences.ini
!profiles/vi/stop_words.txt
!profiles/vi/slots
!profiles/vi/slot_programs
!profiles/ca/profile.json
!profiles/ca/custom_words.txt
!profiles/ca/espeak_phonemes.txt
!profiles/ca/phoneme_examples.txt
!profiles/ca/frequent_words.txt
!profiles/ca/sentences.ini
!profiles/ca/stop_words.txt
!profiles/ca/slots
!profiles/ca/slot_programs
!profiles/nl/profile.json
!profiles/nl/custom_words.txt
!profiles/nl/espeak_phonemes.txt
!profiles/nl/phoneme_examples.txt
!profiles/nl/frequent_words.txt
!profiles/nl/sentences.ini
!profiles/nl/stop_words.txt
!profiles/nl/slots
!profiles/nl/slot_programs
!profiles/nl/kaldi/custom_words.txt
!profiles/nl/kaldi/espeak_phonemes.txt
!profiles/nl/kaldi/phoneme_examples.txt
!profiles/de/profile.json
!profiles/de/custom_words.txt
!profiles/de/espeak_phonemes.txt
!profiles/de/phoneme_examples.txt
!profiles/de/frequent_words.txt
!profiles/de/sentences.ini
!profiles/de/stop_words.txt
!profiles/de/slots
!profiles/de/slot_programs
!profiles/de/kaldi/custom_words.txt
!profiles/de/kaldi/espeak_phonemes.txt
!profiles/de/kaldi/phoneme_examples.txt
!profiles/fr/profile.json
!profiles/fr/custom_words.txt
!profiles/fr/espeak_phonemes.txt
!profiles/fr/phoneme_examples.txt
!profiles/fr/frequent_words.txt
!profiles/fr/sentences.ini
!profiles/fr/stop_words.txt
!profiles/fr/slots
!profiles/fr/slot_programs
!profiles/fr/kaldi/custom_words.txt
!profiles/fr/kaldi/espeak_phonemes.txt
!profiles/fr/kaldi/phoneme_examples.txt
!profiles/en/profile.json
!profiles/en/custom_words.txt
!profiles/en/espeak_phonemes.txt
!profiles/en/phoneme_examples.txt
!profiles/en/frequent_words.txt
!profiles/en/sentences.ini
!profiles/en/stop_words.txt
!profiles/en/slots
!profiles/en/slot_programs
!profiles/en/kaldi/custom_words.txt
!profiles/en/kaldi/espeak_phonemes.txt
!profiles/en/kaldi/phoneme_examples.txt
!rhasspy/profile_schema.json
!rhasspy/*.py
!rhasspy/train/*.py
!rhasspy/train/jsgf2fst/*.py
!*.py
!VERSION
+4 -4
View File
@@ -5,7 +5,9 @@ SHELL := bash
# Docker
# -----------------------------------------------------------------------------
docker: web-dist docker-amd64 docker-armhf docker-aarch64 docker-push manifest
docker: web-dist docker-amd64 docker-armhf docker-aarch64
docker-deploy: docker-push manifest
docker-amd64:
docker build . -f docker/templates/dockerfiles/Dockerfile.prebuilt.alsa.all \
@@ -81,9 +83,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
+1 -1
View File
@@ -1 +1 @@
2.4.14
2.4.16
+6
View File
@@ -0,0 +1,6 @@
defaults:
-
scope:
path: ""
values:
render_with_liquid: false
+31 -17
View File
@@ -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")
+7 -7
View File
@@ -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)
+36
View File
@@ -0,0 +1,36 @@
#!/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)
print(locale_name)
slots_dir = profile_dir / "slots" / "rhasspy"
slots_dir.mkdir(parents=True, exist_ok=True)
# Day names
(slots_dir / "days").write_text('\n'.join(calendar.day_name))
# Month names
(slots_dir / "months").write_text('\n'.join(filter(None, calendar.month_name)))
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+1 -3
View File
@@ -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:
+3 -3
View 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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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)
+18 -9
View File
@@ -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"
@@ -329,31 +329,40 @@ case "${CPU_ARCH}" in
esac
requirements_file="${temp_dir}/requirements.txt"
temp_requirements_file="${temp_dir}/temp_requirements.txt"
cp "${this_dir}/requirements.txt" "${requirements_file}"
# Exclude requirements
if [[ -n "${no_flair}" ]]; then
echo "Excluding flair from virtual environment"
sed -i '/^flair/d' "${requirements_file}"
sed '/^flair/d' "${requirements_file}" > "${temp_requirements_file}" &&
mv "${temp_requirements_file}" "${requirements_file}"
fi
if [[ -n "${no_precise}" ]]; then
echo "Excluding Mycroft Precise from virtual environment"
sed -i '/^precise-runner/d' "${requirements_file}"
sed '/^precise-runner/d' "${requirements_file}" > "${temp_requirements_file}" &&
mv "${temp_requirements_file}" "${requirements_file}"
fi
if [[ -n "${no_adapt}" ]]; then
echo "Excluding Mycroft Adapt from virtual environment"
sed -i '/^adapt-parser/d' "${requirements_file}"
sed '/^adapt-parser/d' "${requirements_file}" > "${temp_requirements_file}" &&
mv "${temp_requirements_file}" "${requirements_file}"
fi
if [[ -n "${no_google}" ]]; then
echo "Excluding Google Text to Speech from virtual environment"
sed -i '/^google-cloud-texttospeech/d' "${requirements_file}"
sed '/^google-cloud-texttospeech/d' "${requirements_file}" > "${temp_requirements_file}" &&
mv "${temp_requirements_file}" "${requirements_file}"
fi
# Install everything except openfst first
sed -i '/^openfst/d' "${requirements_file}"
sed '/^openfst/d' "${requirements_file}" > "${temp_requirements_file}" &&
mv "${temp_requirements_file}" "${requirements_file}"
"${python}" -m pip install -r "${requirements_file}"
@@ -398,5 +407,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
View File
@@ -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 \
+1 -132
View File
@@ -1,132 +1 @@
COPY profiles/zh/profile.json \
profiles/zh/custom_words.txt \
profiles/zh/espeak_phonemes.txt \
profiles/zh/phoneme_examples.txt \
profiles/zh/frequent_words.txt \
profiles/zh/sentences.ini \
profiles/zh/stop_words.txt ${RHASSPY_APP}/profiles/zh/
COPY profiles/hi/ \
profiles/hi/profile.json \
profiles/hi/custom_words.txt \
profiles/hi/espeak_phonemes.txt \
profiles/hi/phoneme_examples.txt \
profiles/hi/frequent_words.txt \
profiles/hi/sentences.ini \
profiles/hi/stop_words.txt ${RHASSPY_APP}/profiles/hi/
COPY profiles/el/profile.json \
profiles/el/custom_words.txt \
profiles/el/espeak_phonemes.txt \
profiles/el/phoneme_examples.txt \
profiles/el/frequent_words.txt \
profiles/el/sentences.ini \
profiles/el/stop_words.txt ${RHASSPY_APP}/profiles/el/
COPY profiles/de/profile.json \
profiles/de/custom_words.txt \
profiles/de/espeak_phonemes.txt \
profiles/de/phoneme_examples.txt \
profiles/de/frequent_words.txt \
profiles/de/sentences.ini \
profiles/de/stop_words.txt ${RHASSPY_APP}/profiles/de/
COPY profiles/de/kaldi/custom_words.txt \
profiles/de/kaldi/espeak_phonemes.txt \
profiles/de/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/de/kaldi/
COPY profiles/it/profile.json \
profiles/it/custom_words.txt \
profiles/it/espeak_phonemes.txt \
profiles/it/phoneme_examples.txt \
profiles/it/frequent_words.txt \
profiles/it/sentences.ini \
profiles/it/stop_words.txt ${RHASSPY_APP}/profiles/it/
COPY profiles/es/profile.json \
profiles/es/custom_words.txt \
profiles/es/espeak_phonemes.txt \
profiles/es/phoneme_examples.txt \
profiles/es/frequent_words.txt \
profiles/es/sentences.ini \
profiles/es/stop_words.txt ${RHASSPY_APP}/profiles/es/
COPY profiles/fr/profile.json \
profiles/fr/custom_words.txt \
profiles/fr/espeak_phonemes.txt \
profiles/fr/phoneme_examples.txt \
profiles/fr/frequent_words.txt \
profiles/fr/sentences.ini \
profiles/fr/stop_words.txt ${RHASSPY_APP}/profiles/fr/
COPY profiles/fr/kaldi/custom_words.txt \
profiles/fr/kaldi/espeak_phonemes.txt \
profiles/fr/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/fr/kaldi/
COPY profiles/ru/profile.json \
profiles/ru/custom_words.txt \
profiles/ru/espeak_phonemes.txt \
profiles/ru/phoneme_examples.txt \
profiles/ru/frequent_words.txt \
profiles/ru/sentences.ini \
profiles/ru/stop_words.txt ${RHASSPY_APP}/profiles/ru/
COPY profiles/nl/profile.json \
profiles/nl/custom_words.txt \
profiles/nl/espeak_phonemes.txt \
profiles/nl/phoneme_examples.txt \
profiles/nl/frequent_words.txt \
profiles/nl/sentences.ini \
profiles/nl/stop_words.txt ${RHASSPY_APP}/profiles/nl/
COPY profiles/nl/kaldi/custom_words.txt \
profiles/nl/kaldi/espeak_phonemes.txt \
profiles/nl/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/nl/kaldi/
COPY profiles/vi/profile.json \
profiles/vi/custom_words.txt \
profiles/vi/espeak_phonemes.txt \
profiles/vi/phoneme_examples.txt \
profiles/vi/frequent_words.txt \
profiles/vi/sentences.ini \
profiles/vi/stop_words.txt ${RHASSPY_APP}/profiles/vi/
COPY profiles/pt/profile.json \
profiles/pt/custom_words.txt \
profiles/pt/espeak_phonemes.txt \
profiles/pt/phoneme_examples.txt \
profiles/pt/frequent_words.txt \
profiles/pt/sentences.ini \
profiles/pt/stop_words.txt ${RHASSPY_APP}/profiles/pt/
COPY profiles/sv/profile.json \
profiles/sv/custom_words.txt \
profiles/sv/espeak_phonemes.txt \
profiles/sv/phoneme_examples.txt \
profiles/sv/frequent_words.txt \
profiles/sv/sentences.ini \
profiles/sv/stop_words.txt ${RHASSPY_APP}/profiles/sv/
COPY profiles/ca/profile.json \
profiles/ca/custom_words.txt \
profiles/ca/espeak_phonemes.txt \
profiles/ca/phoneme_examples.txt \
profiles/ca/frequent_words.txt \
profiles/ca/sentences.ini \
profiles/ca/stop_words.txt ${RHASSPY_APP}/profiles/ca/
COPY profiles/en/profile.json \
profiles/en/custom_words.txt \
profiles/en/espeak_phonemes.txt \
profiles/en/phoneme_examples.txt \
profiles/en/frequent_words.txt \
profiles/en/sentences.ini \
profiles/en/stop_words.txt ${RHASSPY_APP}/profiles/en/
COPY profiles/en/kaldi/custom_words.txt \
profiles/en/kaldi/espeak_phonemes.txt \
profiles/en/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/en/kaldi/
COPY profiles/ ${RHASSPY_APP}/profiles/
@@ -72,138 +72,7 @@ RUN chmod +x /run.sh
COPY profiles/zh/profile.json \
profiles/zh/custom_words.txt \
profiles/zh/espeak_phonemes.txt \
profiles/zh/phoneme_examples.txt \
profiles/zh/frequent_words.txt \
profiles/zh/sentences.ini \
profiles/zh/stop_words.txt ${RHASSPY_APP}/profiles/zh/
COPY profiles/hi/ \
profiles/hi/profile.json \
profiles/hi/custom_words.txt \
profiles/hi/espeak_phonemes.txt \
profiles/hi/phoneme_examples.txt \
profiles/hi/frequent_words.txt \
profiles/hi/sentences.ini \
profiles/hi/stop_words.txt ${RHASSPY_APP}/profiles/hi/
COPY profiles/el/profile.json \
profiles/el/custom_words.txt \
profiles/el/espeak_phonemes.txt \
profiles/el/phoneme_examples.txt \
profiles/el/frequent_words.txt \
profiles/el/sentences.ini \
profiles/el/stop_words.txt ${RHASSPY_APP}/profiles/el/
COPY profiles/de/profile.json \
profiles/de/custom_words.txt \
profiles/de/espeak_phonemes.txt \
profiles/de/phoneme_examples.txt \
profiles/de/frequent_words.txt \
profiles/de/sentences.ini \
profiles/de/stop_words.txt ${RHASSPY_APP}/profiles/de/
COPY profiles/de/kaldi/custom_words.txt \
profiles/de/kaldi/espeak_phonemes.txt \
profiles/de/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/de/kaldi/
COPY profiles/it/profile.json \
profiles/it/custom_words.txt \
profiles/it/espeak_phonemes.txt \
profiles/it/phoneme_examples.txt \
profiles/it/frequent_words.txt \
profiles/it/sentences.ini \
profiles/it/stop_words.txt ${RHASSPY_APP}/profiles/it/
COPY profiles/es/profile.json \
profiles/es/custom_words.txt \
profiles/es/espeak_phonemes.txt \
profiles/es/phoneme_examples.txt \
profiles/es/frequent_words.txt \
profiles/es/sentences.ini \
profiles/es/stop_words.txt ${RHASSPY_APP}/profiles/es/
COPY profiles/fr/profile.json \
profiles/fr/custom_words.txt \
profiles/fr/espeak_phonemes.txt \
profiles/fr/phoneme_examples.txt \
profiles/fr/frequent_words.txt \
profiles/fr/sentences.ini \
profiles/fr/stop_words.txt ${RHASSPY_APP}/profiles/fr/
COPY profiles/fr/kaldi/custom_words.txt \
profiles/fr/kaldi/espeak_phonemes.txt \
profiles/fr/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/fr/kaldi/
COPY profiles/ru/profile.json \
profiles/ru/custom_words.txt \
profiles/ru/espeak_phonemes.txt \
profiles/ru/phoneme_examples.txt \
profiles/ru/frequent_words.txt \
profiles/ru/sentences.ini \
profiles/ru/stop_words.txt ${RHASSPY_APP}/profiles/ru/
COPY profiles/nl/profile.json \
profiles/nl/custom_words.txt \
profiles/nl/espeak_phonemes.txt \
profiles/nl/phoneme_examples.txt \
profiles/nl/frequent_words.txt \
profiles/nl/sentences.ini \
profiles/nl/stop_words.txt ${RHASSPY_APP}/profiles/nl/
COPY profiles/nl/kaldi/custom_words.txt \
profiles/nl/kaldi/espeak_phonemes.txt \
profiles/nl/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/nl/kaldi/
COPY profiles/vi/profile.json \
profiles/vi/custom_words.txt \
profiles/vi/espeak_phonemes.txt \
profiles/vi/phoneme_examples.txt \
profiles/vi/frequent_words.txt \
profiles/vi/sentences.ini \
profiles/vi/stop_words.txt ${RHASSPY_APP}/profiles/vi/
COPY profiles/pt/profile.json \
profiles/pt/custom_words.txt \
profiles/pt/espeak_phonemes.txt \
profiles/pt/phoneme_examples.txt \
profiles/pt/frequent_words.txt \
profiles/pt/sentences.ini \
profiles/pt/stop_words.txt ${RHASSPY_APP}/profiles/pt/
COPY profiles/sv/profile.json \
profiles/sv/custom_words.txt \
profiles/sv/espeak_phonemes.txt \
profiles/sv/phoneme_examples.txt \
profiles/sv/frequent_words.txt \
profiles/sv/sentences.ini \
profiles/sv/stop_words.txt ${RHASSPY_APP}/profiles/sv/
COPY profiles/ca/profile.json \
profiles/ca/custom_words.txt \
profiles/ca/espeak_phonemes.txt \
profiles/ca/phoneme_examples.txt \
profiles/ca/frequent_words.txt \
profiles/ca/sentences.ini \
profiles/ca/stop_words.txt ${RHASSPY_APP}/profiles/ca/
COPY profiles/en/profile.json \
profiles/en/custom_words.txt \
profiles/en/espeak_phonemes.txt \
profiles/en/phoneme_examples.txt \
profiles/en/frequent_words.txt \
profiles/en/sentences.ini \
profiles/en/stop_words.txt ${RHASSPY_APP}/profiles/en/
COPY profiles/en/kaldi/custom_words.txt \
profiles/en/kaldi/espeak_phonemes.txt \
profiles/en/kaldi/phoneme_examples.txt \
${RHASSPY_APP}/profiles/en/kaldi/
COPY profiles/ ${RHASSPY_APP}/profiles/
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
COPY docker/rhasspy ${RHASSPY_APP}/bin/
+1 -1
View File
@@ -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`.
+4 -1
View File
@@ -4,6 +4,9 @@ Rhasspy is designed to be run on different kinds of hardware, such as:
* Raspberry Pi 2-3 B/B+ (`armhf`/`aarch64`)
* Desktop/laptop/server (`amd64`)
* Raspberry Pi Zero (`armv6l`)
* You must use a [virtual environment](installation.md#virtual-environment)
* The [Kaldi speech recognizer](speech-to-text.md#kaldi) is **not** supported
The table below summarizes architecture compatibility with Rhasspy's components:
@@ -30,7 +33,7 @@ The table below summarizes architecture compatibility with Rhasspy's components:
To run Rhasspy on a Raspberry Pi, you'll need at least a 4 GB SD card and a good power supply. I highly recommend the [CanaKit Starter Kit](https://www.amazon.com/CanaKit-Raspberry-Starter-Premium-Black/dp/B07BCC8PK7), which includes a 32 GB SD card, a 2.5 A power supply, and a case.
Some components of Rhasspy will not work on the Raspberry Pi 3 B+ model (`aarch64`). As of the time of this writing, these are:
Some components of Rhasspy will not work on the Raspberry Pi 3 B+ model with a 64-bit operating system (`aarch64`). As of the time of this writing, these are:
* [snowboy](wake-word.md#snowboy) (wake word)
* [Mycroft Precise](wake-word.md#mycroft-precise) (wake word)
+56 -6
View File
@@ -46,10 +46,25 @@ 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. This may require running something like:
```bash
docker rm <container-name>
```
before doing a `docker run...`
## 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 +78,55 @@ Before starting the add-on, make sure to give it access to your microphone and s
![Audio settings for Hass.io](img/hass-io-audio.png)
### 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, and then update your Python dependencies:
```bash
source .venv/bin/activate
pip3 install -r requirements.txt
```
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 +197,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
View File
@@ -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)
+96 -1
View File
@@ -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,75 @@ 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`.
#### Built-in Slots
Rhasspy includes a few built-in slots for each language:
* `$rhasspy/days` - day names of the week
* `$rhasspy/months` - month names of the year
### 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
View File
@@ -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
View File
@@ -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.
+3
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,7 @@
{
"language": "ca",
"name": "ca",
"locale": "ca_ES",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
dilluns
dimarts
dimecres
dijous
divendres
dissabte
diumenge
+12
View File
@@ -0,0 +1,12 @@
de gener
de febrer
de març
dabril
de maig
de juny
de juliol
dagost
de setembre
doctubre
de novembre
de desembre
+1
View File
@@ -1,6 +1,7 @@
{
"language": "de",
"name": "de",
"locale": "de_DE",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower",
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
Montag
Dienstag
Mittwoch
Donnerstag
Freitag
Samstag
Sonntag
+12
View File
@@ -0,0 +1,12 @@
Januar
Februar
März
April
Mai
Juni
Juli
August
September
Oktober
November
Dezember
+11 -4
View File
@@ -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
View File
@@ -1,6 +1,7 @@
{
"language": "el",
"name": "el",
"locale": "el_GR",
"speech_to_text": {
"g2p_casing": "lower",
"system": "pocketsphinx",
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
Δευτέρα
Τρίτη
Τετάρτη
Πέμπτη
Παρασκευή
Σάββατο
Κυριακή
+12
View File
@@ -0,0 +1,12 @@
Ιανουαρίου
Φεβρουαρίου
Μαρτίου
Απριλίου
Μαΐου
Ιουνίου
Ιουλίου
Αυγούστου
Σεπτεμβρίου
Οκτωβρίου
Νοεμβρίου
Δεκεμβρίου
+1
View File
@@ -1,6 +1,7 @@
{
"language": "en",
"name": "en",
"locale": "en_US",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower",
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
+12
View File
@@ -0,0 +1,12 @@
January
February
March
April
May
June
July
August
September
October
November
December
+1
View File
@@ -1,6 +1,7 @@
{
"language": "es",
"name": "es",
"locale": "es_ES",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
lunes
martes
miércoles
jueves
viernes
sábado
domingo
+12
View File
@@ -0,0 +1,12 @@
enero
febrero
marzo
abril
mayo
junio
julio
agosto
septiembre
octubre
noviembre
diciembre
+1
View File
@@ -1,6 +1,7 @@
{
"language": "fr",
"name": "fr",
"locale": "fr_FR",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower",
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
lundi
mardi
mercredi
jeudi
vendredi
samedi
dimanche
+12
View File
@@ -0,0 +1,12 @@
janvier
février
mars
avril
mai
juin
juillet
août
septembre
octobre
novembre
décembre
+1
View File
@@ -1,6 +1,7 @@
{
"language": "hi",
"name": "hi",
"locale": "hi_IN",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
सोमवार
मंगलवार
बुधवार
गुरुवार
शुक्रवार
शनिवार
रविवार
+12
View File
@@ -0,0 +1,12 @@
जनवरी
फ़रवरी
मार्च
अप्रैल
मई
जून
जुलाई
अगस्त
सितंबर
अक्तूबर
नवंबर
दिसंबर
+1
View File
@@ -1,6 +1,7 @@
{
"name": "it",
"language": "it",
"locale": "it_IT",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
lunedì
martedì
mercoledì
giovedì
venerdì
sabato
domenica
+12
View File
@@ -0,0 +1,12 @@
gennaio
febbraio
marzo
aprile
maggio
giugno
luglio
agosto
settembre
ottobre
novembre
dicembre
+1
View File
@@ -1,6 +1,7 @@
{
"language": "nl",
"name": "nl",
"locale": "nl_NL",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower",
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
maandag
dinsdag
woensdag
donderdag
vrijdag
zaterdag
zondag
+12
View File
@@ -0,0 +1,12 @@
januari
februari
maart
april
mei
juni
juli
augustus
september
oktober
november
december
+1
View File
@@ -1,6 +1,7 @@
{
"language": "pt",
"name": "pt",
"locale": "pt_BR",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
segunda
terça
quarta
quinta
sexta
sábado
domingo
+12
View File
@@ -0,0 +1,12 @@
janeiro
fevereiro
março
abril
maio
junho
julho
agosto
setembro
outubro
novembro
dezembro
+1 -1
View File
@@ -1,7 +1,7 @@
{
"language": "ru",
"name": "ru",
"locale": "ru_RU",
"speech_to_text": {
"system": "pocketsphinx",
"dictionary_casing": "lower"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
Понедельник
Вторник
Среда
Четверг
Пятница
Суббота
Воскресенье
+12
View File
@@ -0,0 +1,12 @@
января
февраля
марта
апреля
мая
июня
июля
августа
сентября
октября
ноября
декабря
+1
View File
@@ -1,6 +1,7 @@
{
"language": "sv",
"name": "sv",
"locale": "sv_SE",
"speech_to_text": {
"kaldi": {
"compatible": true
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
måndag
tisdag
onsdag
torsdag
fredag
lördag
söndag
+12
View File
@@ -0,0 +1,12 @@
januari
februari
mars
april
maj
juni
juli
augusti
september
oktober
november
december
+1
View File
@@ -1,6 +1,7 @@
{
"language": "vi",
"name": "vi",
"locale": "vi_VN",
"speech_to_text": {
"kaldi": {
"compatible": true
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
Thứ hai
Thứ ba
Thứ tư
Thứ năm
Thứ sáu
Thứ bảy
Chủ nhật
+12
View File
@@ -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
View File
@@ -1,6 +1,7 @@
{
"name": "zh",
"language": "zh",
"locale": "zh_CN",
"speech_to_text": {
"g2p_casing": "n/a",
"dictionary_casing": "n/a"
+29
View File
@@ -0,0 +1,29 @@
#!/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:
lower, upper = upper, lower
for n in range(lower, upper + 1, step):
print(n)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
星期一
星期二
星期三
星期四
星期五
星期六
星期日
+12
View File
@@ -0,0 +1,12 @@
一月
二月
三月
四月
五月
六月
七月
八月
九月
十月
十一月
十二月
+5
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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:
+10 -4
View File
@@ -116,9 +116,6 @@ class RhasspyActor:
def stop(self, block=True):
"""Stop this actor and its children."""
for child_actor in self._actors:
child_actor.stop(block=block)
self.send(self, ActorExitRequest())
if block:
self._thread.join()
@@ -127,6 +124,15 @@ class RhasspyActor:
"""Main loop for this actor."""
while self._running:
message_dict = self._queue.get()
message = message_dict.get("message")
if isinstance(message, ActorExitRequest):
for child in self._actors:
self.send(child, ActorExitRequest())
self._running = False
self.transition("stopped")
self.send(self._parent, ChildActorExited(self))
self.on_receive(message_dict)
@property
@@ -296,7 +302,7 @@ class InboxActor(RhasspyActor):
return self
def __exit__(self, *args):
self.stop(block=False)
self.stop(block=True)
class ActorSystem:
+15 -37
View File
@@ -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
View File
@@ -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:
+4 -31
View File
@@ -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
View File
@@ -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
+103 -127
View File
@@ -6,125 +6,70 @@ from datetime import timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Type
import pywrapfst as fst
import pydash
import requests
import rhasspynlu
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)
@@ -582,12 +555,14 @@ class DialogueManager(RhasspyActor):
self.transition("training_intent")
intent_fst_path = self.profile.read_path(
self.profile.get("intent.fsticuffs.intent_fst", "intent.fst")
intent_graph_path = self.profile.read_path(
self.profile.get("intent.fsticuffs.intent_graph", "intent.json")
)
intent_fst = fst.Fst.read(str(intent_fst_path))
self.send(self.intent_trainer, TrainIntent(intent_fst))
with open(intent_graph_path, "r") as graph_file:
json_graph = json.load(graph_file)
intent_graph = rhasspynlu.json_to_graph(json_graph)
self.send(self.intent_trainer, TrainIntent(intent_graph))
except Exception as e:
self.transition("ready")
self.send(self.training_receiver, ProfileTrainingFailed(str(e)))
@@ -654,7 +629,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 +701,7 @@ class DialogueManager(RhasspyActor):
play=message.play,
voice=message.voice,
language=message.language,
siteId=message.siteId,
),
)
elif isinstance(message, TrainProfile):
@@ -762,7 +738,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 +767,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 +898,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)
+549
View File
@@ -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_graph, receiver: Optional[RhasspyActor] = None) -> None:
self.intent_graph = intent_graph
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 {}
+32 -44
View File
@@ -6,7 +6,7 @@ import os
import re
import shutil
import subprocess
from typing import Any, Dict, List, Optional, Set, Tuple, Type
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urljoin
import networkx as nx
@@ -15,37 +15,8 @@ import requests
from rhasspynlu import json_to_graph, recognize
from rhasspy.actor import RhasspyActor
from rhasspy.tts import 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
from rhasspy.events import IntentRecognized, RecognizeIntent, SpeakSentence
from rhasspy.utils import empty_intent, hass_request_kwargs, load_converters
# -----------------------------------------------------------------------------
@@ -62,7 +33,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
@@ -173,6 +144,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 +158,10 @@ class FsticuffsRecognizer(RhasspyActor):
# True if fuzzy search should be used (default)
self.fuzzy = self.profile.get("intent.fsticuffs.fuzzy", True)
# Load user-defined converters
self.converters = load_converters(self.profile)
self.transition("loaded")
def in_loaded(self, message: Any, sender: RhasspyActor) -> None:
@@ -203,7 +179,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 +195,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 +230,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 +292,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 +306,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 +316,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 +400,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 +414,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 +475,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 +492,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 +523,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 +601,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 +620,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 +760,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 +807,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(
+9 -40
View File
@@ -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:
+54 -52
View File
@@ -9,38 +9,12 @@ 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, Callable, Dict, List, Set, Type
from urllib.parse import urljoin
from rhasspy.actor import RhasspyActor
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
from rhasspy.events import IntentTrainingComplete, IntentTrainingFailed, TrainIntent
from rhasspy.utils import lcm, make_sentences_by_intent, load_converters
# -----------------------------------------------------------------------------
@@ -59,7 +33,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
@@ -136,23 +110,30 @@ class FsticuffsIntentTrainer(DummyIntentTrainer):
class FuzzyWuzzyIntentTrainer(RhasspyActor):
"""Save examples to JSON for fuzzy string matching later."""
def __init__(self):
RhasspyActor.__init__(self)
self.converters: Dict[str, Callable[..., Any]] = {}
def in_started(self, message: Any, sender: RhasspyActor) -> None:
"""Handle messages in started state."""
if isinstance(message, TrainIntent):
try:
self.train(message.intent_fst)
self.train(message.intent_graph)
self.send(message.receiver or sender, IntentTrainingComplete())
except Exception as e:
self._logger.exception("train")
self.send(message.receiver or sender, IntentTrainingFailed(repr(e)))
def train(self, intent_fst) -> None:
def train(self, intent_graph) -> None:
"""Save examples to JSON file."""
examples_path = self.profile.write_path(
self.profile.get("intent.fuzzywuzzy.examples_json")
)
sentences_by_intent: Dict[str, Any] = make_sentences_by_intent(intent_fst)
converters = load_converters(self.profile)
sentences_by_intent = make_sentences_by_intent(
intent_graph, extra_converters=converters
)
with open(examples_path, "w") as examples_file:
json.dump(sentences_by_intent, examples_file, indent=4)
@@ -168,6 +149,10 @@ class FuzzyWuzzyIntentTrainer(RhasspyActor):
class RasaIntentTrainer(RhasspyActor):
"""Uses Rasa NLU HTTP API to train a recognizer."""
def __init__(self):
RhasspyActor.__init__(self)
self.converters: Dict[str, Callable[..., Any]] = {}
def in_started(self, message: Any, sender: RhasspyActor) -> None:
"""Handle messages in started state."""
if isinstance(message, TrainIntent):
@@ -238,7 +223,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 +232,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
@@ -281,9 +266,7 @@ class RasaIntentTrainer(RhasspyActor):
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"]
)
f'{response.reason}: {json.loads(response.content)["message"]}'
)
@@ -296,6 +279,14 @@ class RasaIntentTrainer(RhasspyActor):
class AdaptIntentTrainer(RhasspyActor):
"""Configure a Mycroft Adapt engine."""
def __init__(self):
RhasspyActor.__init__(self)
self.converters: Dict[str, Callable[..., Any]] = {}
def to_started(self, from_state: str) -> None:
# Load user-defined converters
self.converters = load_converters(self.profile)
def in_started(self, message: Any, sender: RhasspyActor) -> None:
"""Handle messages in started state."""
if isinstance(message, TrainIntent):
@@ -315,9 +306,7 @@ class AdaptIntentTrainer(RhasspyActor):
stop_words_path = self.profile.read_path("stop_words.txt")
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
}
stop_words = {line.strip() for line in stop_words_file if line.strip()}
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
sentences_by_intent: Dict[str, Any] = make_sentences_by_intent(intent_fst)
@@ -354,7 +343,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 +375,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)
@@ -438,6 +427,11 @@ class FlairIntentTrainer(RhasspyActor):
def __init__(self):
RhasspyActor.__init__(self)
self.embeddings = []
self.converters: Dict[str, Callable[..., Any]] = {}
def to_started(self, from_state: str) -> None:
# Load user-defined converters
self.converters = load_converters(self.profile)
def in_started(self, message: Any, sender: RhasspyActor) -> None:
"""Handle messages in started state."""
@@ -453,16 +447,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 +480,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 +515,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 +557,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 +602,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 +631,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
@@ -695,6 +693,7 @@ class CommandIntentTrainer(RhasspyActor):
def __init__(self):
RhasspyActor.__init__(self)
self.command: List[str] = []
self.converters: Dict[str, Callable[..., Any]] = {}
def to_started(self, from_state: str) -> None:
"""Transition to started state."""
@@ -706,6 +705,9 @@ class CommandIntentTrainer(RhasspyActor):
for a in self.profile.get("training.intent.command.arguments", [])
]
# Load user-defined converters
self.converters = load_converters(self.profile)
self.command = [program] + arguments
def in_started(self, message: Any, sender: RhasspyActor) -> None:
+6 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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(

Some files were not shown because too many files have changed in this diff Show More