Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c37c88e61 | |||
| 50f05506a2 | |||
| b62068da69 | |||
| f69d7484ba | |||
| e478765b4e | |||
| 8bf673c412 | |||
| 8854f26dc1 | |||
| cf8f2319b8 | |||
| 782721fab3 | |||
| 43dc7a07c9 | |||
| be42447620 | |||
| 3a884888c4 | |||
| 3133666390 | |||
| ff0aea4177 | |||
| fa418d3129 | |||
| d4314a85d9 | |||
| 3d42551bfc | |||
| 6411924cf2 | |||
| 77d2196f00 | |||
| 3c53cd0236 | |||
| 46a75ec83e | |||
| 3e9c4bef68 | |||
| 465d1e61e0 | |||
| 563fcfcd49 | |||
| 0ff965a965 | |||
| 998eff8708 | |||
| 1dcf984669 | |||
| 21c2b2ef9d | |||
| 958c12cfb4 | |||
| 04efe9f151 | |||
| 6dbc8dc35e | |||
| 2cac48292a | |||
| a4528d40fb | |||
| 4cd8b307c7 | |||
| de0824e49a | |||
| 74ba8c1c4a | |||
| 9428abdd40 | |||
| a6c425d65f | |||
| 015c37fa5d | |||
| 32f7e37657 | |||
| 54063feb03 | |||
| 3bc36f2fb1 | |||
| e0401d4c18 | |||
| 73124075ae | |||
| c410f4eea7 | |||
| 3f0545ed0f | |||
| 331138f300 | |||
| 33b847b828 | |||
| d770679373 | |||
| 86e695a7a4 | |||
| 07d1cc4e43 | |||
| b68e5caf01 | |||
| da4d994e75 | |||
| 627e6e8b3d | |||
| f0ec0486f7 | |||
| 1febc3d1d8 | |||
| 2879802f2f | |||
| 21a2a8f9b4 | |||
| a96f80237e | |||
| fc68d04f29 | |||
| e00c1448cb | |||
| f04ad3bfeb | |||
| eb11f90cab | |||
| 2c612ee669 | |||
| dfe92f9d0e | |||
| c59e7b42ab | |||
| 948705a87b | |||
| 6b0b5c1799 | |||
| 9553691e88 | |||
| 997456631e | |||
| 104165198b | |||
| f405b827f4 | |||
| 089568cf9f | |||
| a92d88ff8f | |||
| c60030b48f | |||
| b400d651f6 | |||
| 2dfa9aa782 | |||
| bd2c065415 | |||
| 1b95144b05 | |||
| 3f60936471 | |||
| 9a3c2f8a3f | |||
| 1fb75f24d7 | |||
| 6c0187e606 | |||
| 16262ec896 | |||
| 9d1303ed21 | |||
| a12e537110 | |||
| 63fb3cf046 | |||
| f5e6666931 | |||
| 44a9c84bc7 | |||
| 9a076936c5 | |||
| 102b29ecf6 | |||
| 51455bfd97 | |||
| 5bf6086164 | |||
| 08ebaf0914 | |||
| 4b3f26c12f | |||
| 707c31e4d3 | |||
| 509d47ea0f | |||
| 1d2b08df6e | |||
| 0608443482 | |||
| 9a1a41385c | |||
| 2d8095f0e1 | |||
| 8f3c1c5d61 | |||
| deb742d768 | |||
| fa24588ea4 | |||
| ed581ecf9d | |||
| f8aedd4ef5 | |||
| 14c1386496 | |||
| 153b642057 | |||
| dec32102dd | |||
| f365c69265 | |||
| 1724c328b7 | |||
| 6db4a8d341 | |||
| b70e8a8569 | |||
| 2e4828da06 | |||
| 96cfe69753 | |||
| 3e8e246c1c | |||
| 80a5008b93 |
+172
-22
@@ -1,26 +1,176 @@
|
||||
.git/
|
||||
.venv/
|
||||
node_modules/
|
||||
__pycache__/
|
||||
test/
|
||||
tools/
|
||||
etc/test/
|
||||
download/precise-engine/
|
||||
download/kaldi/
|
||||
opt/
|
||||
*
|
||||
|
||||
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/
|
||||
|
||||
profiles/*/base_dictionary.txt
|
||||
profiles/*/base_language_model.txt
|
||||
profiles/*/acoustic_model/
|
||||
profiles/*/g2p.fst
|
||||
!requirements.txt
|
||||
!dist/
|
||||
!etc/wav
|
||||
!etc/shflags
|
||||
!create-venv.sh
|
||||
!download-dependencies.sh
|
||||
|
||||
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
|
||||
|
||||
!pip
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
## [2.4.20] - 2020 Apr 10
|
||||
|
||||
### Added
|
||||
|
||||
- libasound2-plugins to Docker image (for Hass.IO)
|
||||
- MQTT TLS support (thanks https://github.com/ofekd)
|
||||
- Mycroft Precise 0.3.0 added to Docker image
|
||||
|
||||
### Changed
|
||||
|
||||
- Properly accept websocket connections
|
||||
- Don't error out on missing porcupine files
|
||||
- Fix rawValue in MQTT messages
|
||||
|
||||
## [2.4.19] - 2020 Mar 04
|
||||
|
||||
### Added
|
||||
|
||||
- Support for Google Cloud speech to text
|
||||
- Rasa NLU minimum confidence parameter
|
||||
|
||||
### Changed
|
||||
|
||||
- Using tagged version of porcupine wake models to avoid incompatibilities
|
||||
- Fix Rasa NLU first entity only bug
|
||||
- Fix siteId null bug
|
||||
|
||||
## [2.4.18] - 2020 Feb 07
|
||||
|
||||
### Added
|
||||
|
||||
- /api/listen-for-wake accepts "on" and "off" as POST data to enable/disable wake word
|
||||
- /api/events/wake websocket endpoint reports wake up events
|
||||
- /api/events/text websocket endpoint reports transcription events
|
||||
- Rhasspy logo changes in web UI when wake word is detected
|
||||
- espeak arguments list for text to speech
|
||||
|
||||
### Changed
|
||||
|
||||
- STT output casing is fixed outside of HTTP API calls
|
||||
- All voice commands show up in web UI test page
|
||||
- Play last voice command button in web UI works for any command
|
||||
- Fixed commas in numbers with thousand separators
|
||||
- Words from Pocketsphinx wake keyphrase are added to dictionary
|
||||
- Pocketsphinx wake word keyphrase casing is fixed
|
||||
|
||||
## [2.4.17] - 2020 Jan 21
|
||||
|
||||
### Added
|
||||
|
||||
- Button to web UI to play last recorded voice command
|
||||
- RHASSPY_LOG_LEVEL environment variable
|
||||
- Web UI feedback during download
|
||||
- Add "asoundrc" config option to Hass.IO add-on
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved $profile/kaldi/custom_words.txt to $profile/kaldi_custom_words.txt
|
||||
- Slot substitution casing is kept during training/recognition
|
||||
- Fixed fuzzywuzzy and other intent recognizer training after addition of converters
|
||||
- Fix thread max count issue
|
||||
- Hide web UI alerts after 10 seconds
|
||||
- Delete partially downloaded profile files
|
||||
- Force slot programs to run each training cycle
|
||||
- Fix _raw_text in Hass event being same as _text
|
||||
|
||||
### Removed
|
||||
|
||||
- Flair intent recognizer
|
||||
|
||||
## [2.4.16] - 2020 Jan 5
|
||||
|
||||
### Added
|
||||
|
||||
- Number ranges (0..100)
|
||||
- Converters for transforming JSON values in intents (!int)
|
||||
- Slot programs for generating slot values
|
||||
- $rhasspy/days and $rhasspy/months built-in slots
|
||||
|
||||
## [2.4.15] - 2019 Dec 27
|
||||
|
||||
### Added
|
||||
|
||||
- Preliminary support for Raspberry Pi Zero (no Kaldi)
|
||||
- Play error sound when intent not recognized
|
||||
- _text and _raw_text to Home Assistant events
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable wake word when TTS is speaking
|
||||
- Use json5 library to parse profile
|
||||
- Remove picotts pop sound
|
||||
- Don't open/close microphone after wake-up
|
||||
|
||||
## [2.4.14] - 2019 Dec 19
|
||||
|
||||
### Added
|
||||
|
||||
- Ability to split sentences across multiple .ini file in intents directory
|
||||
- Support (future) /api/intent for Home Assistant
|
||||
- Support for Home Assistant TTS system
|
||||
- Emulate MaryTTS /process API in web API
|
||||
- Include wakeId/siteId in JSON intent (MQTT/Websocket)
|
||||
- ?voice and ?language query parameters to /api/text-to-speech
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
FROM ubuntu:eoan as build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
ENV RHASSPY_VENV ${RHASSPY_APP}/.venv
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
python3 python3-dev python3-setuptools python3-pip python3-venv \
|
||||
build-essential swig portaudio19-dev libatlas-base-dev
|
||||
|
||||
COPY etc/shflags ${RHASSPY_APP}/etc/
|
||||
COPY download/rhasspy-tools_*.tar.gz \
|
||||
download/kaldi_*.tar.gz \
|
||||
download/pocketsphinx-python.tar.gz \
|
||||
download/snowboy-1.3.0.tar.gz \
|
||||
download/precise-engine_0.3.0_*.tar.gz \
|
||||
${RHASSPY_APP}/download/
|
||||
COPY create-venv.sh download-dependencies.sh requirements.txt ${RHASSPY_APP}/
|
||||
RUN cd ${RHASSPY_APP} && ./create-venv.sh --nosystem --noweb
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
FROM ubuntu:eoan
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
ENV RHASSPY_VENV ${RHASSPY_APP}/.venv
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=build ${RHASSPY_VENV} ${RHASSPY_VENV}
|
||||
COPY --from=build ${RHASSPY_APP}/opt/kaldi/ ${RHASSPY_APP}/opt/kaldi/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
python3 python3-dev python3-setuptools python3-pip python3-venv \
|
||||
bash jq unzip curl perl \
|
||||
libportaudio2 libatlas3-base \
|
||||
libgfortran4 ca-certificates \
|
||||
sox espeak flite libttspico-utils alsa-utils lame \
|
||||
libasound2-plugins \
|
||||
libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-good \
|
||||
mosquitto-clients
|
||||
|
||||
# Web interface
|
||||
ADD download/rhasspy-web-dist.tar.gz ${RHASSPY_APP}/
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
COPY profiles/ ${RHASSPY_APP}/profiles/
|
||||
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
COPY VERSION ${RHASSPY_APP}/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX ${RHASSPY_APP}/opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,47 +1,19 @@
|
||||
.PHONY: web-dist docker manifest docs-uml g2p check
|
||||
SHELL := bash
|
||||
|
||||
DOCKER_PLATFORMS = linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
all: docker
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Docker
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
docker: web-dist docker-amd64 docker-armhf docker-aarch64 docker-push manifest
|
||||
|
||||
docker-amd64:
|
||||
docker build . -f docker/templates/dockerfiles/Dockerfile.prebuilt.alsa.all \
|
||||
--build-arg BUILD_ARCH=amd64 \
|
||||
--build-arg CPU_ARCH=x86_64 \
|
||||
--build-arg BUILD_FROM=ubuntu:bionic \
|
||||
-t synesthesiam/rhasspy-server:amd64
|
||||
|
||||
docker-armhf:
|
||||
docker build . -f docker/templates/dockerfiles/Dockerfile.prebuilt.alsa.all \
|
||||
--build-arg BUILD_ARCH=armhf \
|
||||
--build-arg CPU_ARCH=armv7l \
|
||||
--build-arg BUILD_FROM=arm32v7/ubuntu:bionic \
|
||||
-t synesthesiam/rhasspy-server:armhf
|
||||
|
||||
docker-aarch64:
|
||||
docker build . -f docker/templates/dockerfiles/Dockerfile.prebuilt.alsa.all \
|
||||
--build-arg BUILD_ARCH=aarch64 \
|
||||
--build-arg CPU_ARCH=arm64v8 \
|
||||
--build-arg BUILD_FROM=arm64v8/ubuntu:bionic \
|
||||
-t synesthesiam/rhasspy-server:aarch64
|
||||
|
||||
docker-push:
|
||||
docker push synesthesiam/rhasspy-server:amd64
|
||||
docker push synesthesiam/rhasspy-server:armhf
|
||||
docker push synesthesiam/rhasspy-server:aarch64
|
||||
|
||||
manifest:
|
||||
docker manifest push --purge synesthesiam/rhasspy-server:latest
|
||||
docker manifest create --amend synesthesiam/rhasspy-server:latest \
|
||||
synesthesiam/rhasspy-server:amd64 \
|
||||
synesthesiam/rhasspy-server:armhf \
|
||||
synesthesiam/rhasspy-server:aarch64
|
||||
docker manifest annotate synesthesiam/rhasspy-server:latest synesthesiam/rhasspy-server:armhf --os linux --arch arm
|
||||
docker manifest annotate synesthesiam/rhasspy-server:latest synesthesiam/rhasspy-server:aarch64 --os linux --arch arm64
|
||||
docker manifest push synesthesiam/rhasspy-server:latest
|
||||
docker: web-dist
|
||||
docker buildx build . \
|
||||
--platform $(DOCKER_PLATFORMS) \
|
||||
--tag synesthesiam/rhasspy-server:latest \
|
||||
--push
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Yarn (Vue)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||

|
||||
# Rhasspy has [moved and improved!](https://github.com/rhasspy/rhasspy)
|
||||
|
||||
Rhasspy (pronounced RAH-SPEE) is an offline, [multilingual](#supported-languages) voice assistant toolkit inspired by [Jasper](https://jasperproject.github.io/) that works well with [Home Assistant](https://www.home-assistant.io/), [Hass.io](https://www.home-assistant.io/hassio/), and [Node-RED](https://nodered.org).
|
||||
This repository contains code for an older version of Rhasspy (2.4).
|
||||
|
||||
* [Documentation](https://rhasspy.readthedocs.io/)
|
||||
---
|
||||
|
||||
Rhasspy (pronounced RAH-SPEE) is an offline voice assistant toolkit inspired by [Jasper](https://jasperproject.github.io/) that [supports many languages](#supported-languages). It works well with [Home Assistant](https://www.home-assistant.io/), [Hass.io](https://www.home-assistant.io/hassio/), and [Node-RED](https://nodered.org).
|
||||
|
||||
**A newer version of Rhasspy (2.5) is available at [https://github.com/rhasspy/rhasspy](https://github.com/rhasspy/rhasspy)**
|
||||
|
||||
* [Documentation](https://rhasspy.readthedocs.io/en/v2.4.20/)
|
||||
* [Discussion](https://community.rhasspy.org)
|
||||
* [Video Introduction](https://www.youtube.com/watch?v=ijKTR_GqWwA)
|
||||
* [Hass.IO Add-On Repository](https://github.com/synesthesiam/hassio-addons)
|
||||
@@ -58,7 +64,7 @@ The table below summarizes language support across the various supporting techno
|
||||
| | [rasaNLU](https://rhasspy.readthedocs.io/en/latest/intent-recognition/#rasanlu) | *needs extra software* | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| **Text to Speech** | [espeak](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#espeak) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| | [flite](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#flite) | ✓ | ✓ | | | | | | | | ✓ | | | | | |
|
||||
| | [picotts](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#picotts) | ✓ | ✓ | | | | | | | | | | | | | |
|
||||
| | [picotts](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#picotts) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | | | | | | |
|
||||
| | [marytts](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#marytts) | ✓ | ✓ | ✓ | | ✓ | ✓ | | ✓ | | | | | | | |
|
||||
| | [wavenet](https://rhasspy.readthedocs.io/en/latest/text-to-speech/#google-wavenet) | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | ✓ | ✓ | |
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
from uuid import uuid4
|
||||
|
||||
import attr
|
||||
@@ -29,7 +31,13 @@ from swagger_ui import quart_api_doc
|
||||
|
||||
from rhasspy.actor import ActorSystem, ConfigureEvent, RhasspyActor
|
||||
from rhasspy.core import RhasspyCore
|
||||
from rhasspy.events import IntentRecognized, ProfileTrainingFailed
|
||||
from rhasspy.events import (
|
||||
IntentRecognized,
|
||||
ProfileTrainingFailed,
|
||||
VoiceCommand,
|
||||
WakeWordDetected,
|
||||
WavTranscription,
|
||||
)
|
||||
from rhasspy.utils import (
|
||||
FunctionLoggingHandler,
|
||||
buffer_to_wav,
|
||||
@@ -53,6 +61,10 @@ app = Quart("rhasspy")
|
||||
app.secret_key = str(uuid4())
|
||||
app = cors(app)
|
||||
|
||||
# WAV data from last voice command
|
||||
last_voice_wav: Optional[bytes] = None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Parse Arguments
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -91,8 +103,12 @@ parser.add_argument("--log-level", default="DEBUG", help="Set logging level")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Set log level
|
||||
log_level = getattr(logging, args.log_level.upper())
|
||||
logging.basicConfig(level=log_level)
|
||||
if "RHASSPY_LOG_LEVEL" in os.environ:
|
||||
log_level = os.environ["RHASSPY_LOG_LEVEL"]
|
||||
else:
|
||||
log_level = args.log_level
|
||||
|
||||
logging.basicConfig(level=getattr(logging, log_level.upper()))
|
||||
|
||||
|
||||
logger.debug(args)
|
||||
@@ -206,6 +222,14 @@ async def api_download_profile() -> str:
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.route("/api/download-status", methods=["GET"])
|
||||
async def api_download_status() -> str:
|
||||
"""Get status of profile download"""
|
||||
assert core is not None
|
||||
|
||||
return "\n".join(core.download_status)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -256,8 +280,11 @@ async def api_speakers() -> Response:
|
||||
async def api_listen_for_wake() -> str:
|
||||
"""Make Rhasspy listen for a wake word"""
|
||||
assert core is not None
|
||||
core.listen_for_wake()
|
||||
return "OK"
|
||||
enabled_str = (await request.data).decode().strip().lower()
|
||||
enabled = enabled_str not in ["false", "off"]
|
||||
core.listen_for_wake(enabled)
|
||||
|
||||
return str(enabled)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -278,6 +305,10 @@ async def api_listen_for_command() -> Response:
|
||||
entity = request.args.get("entity")
|
||||
value = request.args.get("value")
|
||||
|
||||
# Emulate wake
|
||||
wake_json = json.dumps({"wakewordId": "default", "siteId": core.siteId})
|
||||
await add_ws_event("wake", wake_json)
|
||||
|
||||
return jsonify(
|
||||
await core.listen_for_command(
|
||||
handle=(not no_hass), timeout=timeout, entity=entity, value=value
|
||||
@@ -369,7 +400,7 @@ async def api_pronounce() -> Union[Response, str]:
|
||||
|
||||
if download:
|
||||
# Return WAV
|
||||
return Response(wav_data) # , mimetype="audio/wav")
|
||||
return Response(wav_data, mimetype="audio/wav")
|
||||
|
||||
# Play through speakers
|
||||
core.play_wav_data(wav_data)
|
||||
@@ -524,6 +555,26 @@ async def api_custom_words():
|
||||
assert core is not None
|
||||
speech_system = core.profile.get("speech_to_text.system", "pocketsphinx")
|
||||
|
||||
# Temporary fix for kaldi/custom_words -> kaldi_custom_words.txt
|
||||
old_kaldi_words_path = Path(core.profile.read_path("kaldi/custom_words.txt"))
|
||||
if old_kaldi_words_path.is_file():
|
||||
new_kaldi_words_path = Path(
|
||||
core.profile.write_path(
|
||||
core.profile.get(
|
||||
"speech_to_text.kaldi.custom_words", "custom_words.txt"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
new_kaldi_words_path != old_kaldi_words_path
|
||||
and not new_kaldi_words_path.is_file()
|
||||
):
|
||||
logger.warning(
|
||||
"Moving %s to %s", str(old_kaldi_words_path), str(new_kaldi_words_path)
|
||||
)
|
||||
shutil.move(old_kaldi_words_path, new_kaldi_words_path)
|
||||
|
||||
if request.method == "POST":
|
||||
custom_words_path = Path(
|
||||
core.profile.write_path(
|
||||
@@ -618,6 +669,7 @@ async def api_restart() -> str:
|
||||
@app.route("/api/speech-to-text", methods=["POST"])
|
||||
async def api_speech_to_text() -> str:
|
||||
"""Transcribe speech from WAV file."""
|
||||
global last_voice_wav
|
||||
no_header = request.args.get("noheader", "false").lower() == "true"
|
||||
assert core is not None
|
||||
|
||||
@@ -627,10 +679,20 @@ async def api_speech_to_text() -> str:
|
||||
# Wrap in WAV
|
||||
wav_data = buffer_to_wav(wav_data)
|
||||
|
||||
last_voice_wav = wav_data
|
||||
|
||||
start_time = time.perf_counter()
|
||||
result = await core.transcribe_wav(wav_data)
|
||||
end_time = time.perf_counter()
|
||||
|
||||
# Send to websocket
|
||||
await add_ws_event(
|
||||
"transcription",
|
||||
json.dumps(
|
||||
{"text": result.text, "wakewordId": "default", "siteId": core.siteId}
|
||||
),
|
||||
)
|
||||
|
||||
if prefers_json():
|
||||
return jsonify(
|
||||
{
|
||||
@@ -665,7 +727,7 @@ async def api_text_to_intent():
|
||||
|
||||
intent_json = json.dumps(intent)
|
||||
logger.debug(intent_json)
|
||||
await add_ws_event(WS_EVENT_INTENT, intent_json)
|
||||
await add_ws_event("intent", intent_json)
|
||||
|
||||
if not no_hass:
|
||||
# Send intent to Home Assistant
|
||||
@@ -680,11 +742,13 @@ async def api_text_to_intent():
|
||||
@app.route("/api/speech-to-intent", methods=["POST"])
|
||||
async def api_speech_to_intent() -> Response:
|
||||
"""Transcribe speech, recognize intent, and optionally handle."""
|
||||
global last_voice_wav
|
||||
assert core is not None
|
||||
no_hass = request.args.get("nohass", "false").lower() == "true"
|
||||
|
||||
# Prefer 16-bit 16Khz mono, but will convert with sox if needed
|
||||
wav_data = await request.data
|
||||
last_voice_wav = wav_data
|
||||
|
||||
# speech -> text
|
||||
start_time = time.time()
|
||||
@@ -692,6 +756,12 @@ async def api_speech_to_intent() -> Response:
|
||||
text = transcription.text
|
||||
logger.debug(text)
|
||||
|
||||
# Send to websocket
|
||||
await add_ws_event(
|
||||
"transcription",
|
||||
json.dumps({"text": text, "wakewordId": "default", "siteId": core.siteId}),
|
||||
)
|
||||
|
||||
# text -> intent
|
||||
intent = (await core.recognize_intent(text)).intent
|
||||
intent["speech_confidence"] = transcription.confidence
|
||||
@@ -701,7 +771,7 @@ async def api_speech_to_intent() -> Response:
|
||||
|
||||
intent_json = json.dumps(intent)
|
||||
logger.debug(intent_json)
|
||||
await add_ws_event(WS_EVENT_INTENT, intent_json)
|
||||
await add_ws_event("intent", intent_json)
|
||||
|
||||
if not no_hass:
|
||||
# Send intent to Home Assistant
|
||||
@@ -726,6 +796,7 @@ async def api_start_recording() -> str:
|
||||
@app.route("/api/stop-recording", methods=["POST"])
|
||||
async def api_stop_recording() -> Response:
|
||||
"""End recording voice command. Transcribe and handle."""
|
||||
global last_voice_wav
|
||||
assert core is not None
|
||||
no_hass = request.args.get("nohass", "false").lower() == "true"
|
||||
|
||||
@@ -739,20 +810,43 @@ async def api_stop_recording() -> Response:
|
||||
text = transcription.text
|
||||
logger.debug(text)
|
||||
|
||||
# Send to websocket
|
||||
await add_ws_event(
|
||||
"transcription",
|
||||
json.dumps({"text": text, "wakewordId": "default", "siteId": core.siteId}),
|
||||
)
|
||||
|
||||
intent = (await core.recognize_intent(text)).intent
|
||||
intent["speech_confidence"] = transcription.confidence
|
||||
|
||||
intent_json = json.dumps(intent)
|
||||
logger.debug(intent_json)
|
||||
await add_ws_event(WS_EVENT_INTENT, intent_json)
|
||||
await add_ws_event("intent", intent_json)
|
||||
|
||||
if not no_hass:
|
||||
# Send intent to Home Assistant
|
||||
intent = (await core.handle_intent(intent)).intent
|
||||
|
||||
# Save last voice command WAV data
|
||||
last_voice_wav = wav_data
|
||||
|
||||
return jsonify(intent)
|
||||
|
||||
|
||||
@app.route("/api/play-recording", methods=["POST"])
|
||||
async def api_play_recording() -> str:
|
||||
"""Play last recorded voice command through the configured audio output system"""
|
||||
global last_voice_wav
|
||||
assert core is not None
|
||||
|
||||
if last_voice_wav:
|
||||
# Play through speakers
|
||||
logger.debug("Playing %s byte(s)", len(last_voice_wav))
|
||||
core.play_wav_data(last_voice_wav)
|
||||
|
||||
return "OK"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -806,7 +900,7 @@ async def api_text_to_speech() -> Union[bytes, str]:
|
||||
|
||||
if not play:
|
||||
# Return WAV data instead of speaking
|
||||
return result.wav_data
|
||||
return Response(result.wav_data, mimetype="audio/wav")
|
||||
|
||||
return sentence
|
||||
|
||||
@@ -823,16 +917,6 @@ async def api_slots() -> Union[str, Response]:
|
||||
overwrite_all = request.args.get("overwrite_all", "false").lower() == "true"
|
||||
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(
|
||||
core.profile.get("speech_to_text.slots_dir", "slots")
|
||||
@@ -859,11 +943,10 @@ async def api_slots() -> Union[str, Response]:
|
||||
slots_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Merge with existing values
|
||||
values = {word_transform(v.strip()) for v in values}
|
||||
values = {v.strip() for v in values}
|
||||
if slots_path.is_file():
|
||||
values.update(
|
||||
word_transform(line.strip())
|
||||
for line in slots_path.read_text().splitlines()
|
||||
line.strip() for line in slots_path.read_text().splitlines()
|
||||
)
|
||||
|
||||
# Write merged values
|
||||
@@ -989,7 +1072,7 @@ def api_intents():
|
||||
|
||||
|
||||
@app.route("/process", methods=["GET"])
|
||||
async def marytts_process():
|
||||
async def marytts_process() -> Response:
|
||||
"""Emulate MaryTTS /process API"""
|
||||
global last_sentence
|
||||
|
||||
@@ -1001,7 +1084,7 @@ async def marytts_process():
|
||||
sentence, play=False, voice=voice, language=locale
|
||||
)
|
||||
|
||||
return spoken.wav_data
|
||||
return Response(spoken.wav_data, mimetype="audio/wav")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1073,26 +1156,26 @@ async def swagger_yaml() -> Response:
|
||||
# WebSocket API
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
WS_EVENT_INTENT = 0
|
||||
WS_EVENT_LOG = 1
|
||||
|
||||
ws_queues: List[List[asyncio.Queue]] = [[], []]
|
||||
ws_locks: List[asyncio.Lock] = [asyncio.Lock(), asyncio.Lock()]
|
||||
user_queues: Set[asyncio.Queue] = set()
|
||||
logging_queues: Set[asyncio.Queue] = set()
|
||||
|
||||
|
||||
async def add_ws_event(event_type: int, text: str):
|
||||
"""Send text out to all websockets for a specific event."""
|
||||
async with ws_locks[event_type]:
|
||||
for q in ws_queues[event_type]:
|
||||
await q.put(text)
|
||||
async def add_ws_event(message_type: str, text: str):
|
||||
"""Send text out to all user websockets for a specific event."""
|
||||
for q in user_queues:
|
||||
await q.put((message_type, text))
|
||||
|
||||
|
||||
async def log_ws_event(text: str):
|
||||
"""Send logging message out to websockets."""
|
||||
for q in logging_queues:
|
||||
await q.put(text)
|
||||
|
||||
|
||||
# Send logging messages out to websocket
|
||||
logging.root.addHandler(
|
||||
FunctionLoggingHandler(
|
||||
lambda msg: asyncio.run_coroutine_threadsafe(
|
||||
add_ws_event(WS_EVENT_LOG, msg), loop
|
||||
)
|
||||
lambda msg: asyncio.run_coroutine_threadsafe(log_ws_event(msg), loop)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1102,6 +1185,8 @@ class WebSocketObserver(RhasspyActor):
|
||||
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in started state."""
|
||||
global last_voice_wav
|
||||
|
||||
if isinstance(message, IntentRecognized):
|
||||
# Add slots
|
||||
intent_slots = {}
|
||||
@@ -1113,38 +1198,91 @@ class WebSocketObserver(RhasspyActor):
|
||||
# Convert to JSON
|
||||
intent_json = json.dumps(message.intent)
|
||||
self._logger.debug(intent_json)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
add_ws_event(WS_EVENT_INTENT, intent_json), loop
|
||||
asyncio.run_coroutine_threadsafe(add_ws_event("intent", intent_json), loop)
|
||||
elif isinstance(message, WakeWordDetected):
|
||||
assert core is not None
|
||||
wake_json = json.dumps({"wakewordId": message.name, "siteId": core.siteId})
|
||||
asyncio.run_coroutine_threadsafe(add_ws_event("wake", wake_json), loop)
|
||||
elif isinstance(message, WavTranscription):
|
||||
assert core is not None
|
||||
transcription_json = json.dumps(
|
||||
{
|
||||
"text": message.text,
|
||||
"wakewordId": message.wakewordId,
|
||||
"siteId": core.siteId,
|
||||
}
|
||||
)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
add_ws_event("transcription_json", transcription_json), loop
|
||||
)
|
||||
elif isinstance(message, VoiceCommand):
|
||||
# Save last voice command
|
||||
last_voice_wav = buffer_to_wav(message.data)
|
||||
|
||||
|
||||
def api_websocket(func):
|
||||
"""Wraps a websocket route to use a user websocket queue"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*_args, **kwargs):
|
||||
global user_queues
|
||||
queue = asyncio.Queue()
|
||||
user_queues.add(queue)
|
||||
try:
|
||||
return await func(queue, *_args, **kwargs)
|
||||
except Exception:
|
||||
logger.exception("api_websocket")
|
||||
finally:
|
||||
user_queues.discard(queue)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.websocket("/api/events/intent")
|
||||
async def api_events_intent() -> None:
|
||||
@api_websocket
|
||||
async def api_events_intent(queue) -> None:
|
||||
"""Websocket endpoint to receive intents as JSON."""
|
||||
# Add new queue for websocket
|
||||
q: asyncio.Queue = asyncio.Queue()
|
||||
async with ws_locks[WS_EVENT_INTENT]:
|
||||
ws_queues[WS_EVENT_INTENT].append(q)
|
||||
await websocket.accept()
|
||||
|
||||
try:
|
||||
while True:
|
||||
text = await q.get()
|
||||
while True:
|
||||
message_type, text = await queue.get()
|
||||
if message_type == "intent":
|
||||
await websocket.send(text)
|
||||
except Exception:
|
||||
logger.exception("api_events_intent")
|
||||
|
||||
# Remove queue
|
||||
async with ws_locks[WS_EVENT_INTENT]:
|
||||
ws_queues[WS_EVENT_INTENT].remove(q)
|
||||
|
||||
@app.websocket("/api/events/text")
|
||||
@api_websocket
|
||||
async def api_events_text(queue) -> None:
|
||||
"""Websocket endpoint for transcriptions."""
|
||||
await websocket.accept()
|
||||
|
||||
while True:
|
||||
message_type, text = await queue.get()
|
||||
if message_type == "transcription":
|
||||
await websocket.send(text)
|
||||
|
||||
|
||||
@app.websocket("/api/events/wake")
|
||||
@api_websocket
|
||||
async def api_events_wake(queue) -> None:
|
||||
"""Websocket endpoint to report wake up."""
|
||||
await websocket.accept()
|
||||
|
||||
while True:
|
||||
message_type, text = await queue.get()
|
||||
if message_type == "wake":
|
||||
await websocket.send(text)
|
||||
|
||||
|
||||
@app.websocket("/api/events/log")
|
||||
async def api_events_log() -> None:
|
||||
"""Websocket endpoint to receive logging messages as text."""
|
||||
await websocket.accept()
|
||||
|
||||
# Add new queue for websocket
|
||||
q: asyncio.Queue = asyncio.Queue()
|
||||
async with ws_locks[WS_EVENT_LOG]:
|
||||
ws_queues[WS_EVENT_LOG].append(q)
|
||||
logging_queues.add(q)
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -1152,12 +1290,9 @@ async def api_events_log() -> None:
|
||||
await websocket.send(text)
|
||||
except concurrent.futures.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("api_events_log")
|
||||
|
||||
# Remove queue
|
||||
async with ws_locks[WS_EVENT_LOG]:
|
||||
ws_queues[WS_EVENT_LOG].remove(q)
|
||||
logging_queues.discard(q)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1193,6 +1328,9 @@ loop.run_until_complete(start_rhasspy())
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Disable useless logging messages
|
||||
logging.getLogger("wsproto").setLevel(logging.CRITICAL)
|
||||
|
||||
# Start web server
|
||||
if args.ssl is not None:
|
||||
logger.debug("Using SSL with certfile, keyfile = %s", args.ssl)
|
||||
|
||||
+10
-14
@@ -18,22 +18,18 @@ def main():
|
||||
profile = json.load(profile_file)
|
||||
locale_name = profile["locale"] + ".UTF-8"
|
||||
locale.setlocale(locale.LC_ALL, locale_name)
|
||||
|
||||
slots_dir = profile_dir / "slots" / "rhasspy"
|
||||
slots_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Day names
|
||||
with open(slots_dir / "days", "w") as days_file:
|
||||
for day_num in range(7):
|
||||
print(calendar.day_name[day_num], file=days_file)
|
||||
|
||||
# Month names
|
||||
with open(slots_dir / "months", "w") as month_file:
|
||||
for month_num in range(1, 13):
|
||||
print(calendar.month_name[month_num], file=month_file)
|
||||
|
||||
print(locale_name)
|
||||
|
||||
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__":
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import json
|
||||
import random
|
||||
import datetime
|
||||
|
||||
|
||||
def speech(text):
|
||||
global o
|
||||
o["speech"] = {"text": text}
|
||||
|
||||
|
||||
# get json from stdin and load into python dict
|
||||
o = json.loads(sys.stdin.read())
|
||||
|
||||
intent = o["intent"]["name"]
|
||||
|
||||
if intent == "GetTime":
|
||||
now = datetime.datetime.now()
|
||||
speech("It's %s %d %s." % (now.strftime('%H'), now.minute, now.strftime('%p')))
|
||||
|
||||
elif intent == "Hello":
|
||||
replies = ['Hi!', 'Hello!', 'Hey there!', 'Greetings.']
|
||||
speech(random.choice(replies))
|
||||
|
||||
# convert dict to json and print to stdout
|
||||
print(json.dumps(o))
|
||||
+14
-5
@@ -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}"
|
||||
|
||||
|
||||
+43
-21
@@ -11,10 +11,12 @@ DEFINE_string 'venv' "${this_dir}/.venv" 'Path to create virtual environment'
|
||||
DEFINE_string 'download-dir' "${this_dir}/download" 'Directory to cache downloaded files'
|
||||
DEFINE_boolean 'system' true 'Install system dependencies'
|
||||
DEFINE_boolean 'flair' false 'Install flair'
|
||||
DEFINE_boolean 'precise' false 'Install Mycroft Precise'
|
||||
DEFINE_boolean 'precise' true 'Install Mycroft Precise'
|
||||
DEFINE_boolean 'adapt' true 'Install Mycroft Adapt'
|
||||
DEFINE_boolean 'google' false 'Install Google Text to Speech'
|
||||
DEFINE_boolean 'google' true 'Install Google Text to Speech'
|
||||
DEFINE_boolean 'kaldi' true 'Install Kaldi'
|
||||
DEFINE_boolean 'tools' true 'Install Rhasspy tools'
|
||||
DEFINE_boolean 'web' true 'Install web UI'
|
||||
DEFINE_boolean 'offline' false "Don't download anything"
|
||||
DEFINE_integer 'make-threads' 4 'Number of threads to use with make' 'j'
|
||||
DEFINE_string 'python' '' 'Path to Python executable'
|
||||
@@ -60,6 +62,14 @@ if [[ "${FLAGS_offline}" -eq "${FLAGS_TRUE}" ]]; then
|
||||
offline='true'
|
||||
fi
|
||||
|
||||
if [[ "${FLAGS_tools}" -eq "${FLAGS_FALSE}" ]]; then
|
||||
no_tools='true'
|
||||
fi
|
||||
|
||||
if [[ "${FLAGS_web}" -eq "${FLAGS_FALSE}" ]]; then
|
||||
no_web='true'
|
||||
fi
|
||||
|
||||
make_threads="${FLAGS_make_threads}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -160,7 +170,7 @@ case "${CPU_ARCH}" in
|
||||
FRIENDLY_ARCH=armhf
|
||||
;;
|
||||
|
||||
arm64v8)
|
||||
aarch64|arm64v8)
|
||||
FRIENDLY_ARCH=aarch64
|
||||
;;
|
||||
|
||||
@@ -183,6 +193,14 @@ if [[ -n "${no_kaldi}" ]]; then
|
||||
download_args+=('--nokaldi')
|
||||
fi
|
||||
|
||||
if [[ -n "${no_tools}" ]]; then
|
||||
download_args+=('--notools')
|
||||
fi
|
||||
|
||||
if [[ -n "${no_web}" ]]; then
|
||||
download_args+=('--noweb')
|
||||
fi
|
||||
|
||||
bash download-dependencies.sh "${download_args[@]}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -202,10 +220,12 @@ echo "Creating new virtual environment"
|
||||
mkdir -p "${venv}"
|
||||
"${PYTHON}" -m venv "${venv}"
|
||||
|
||||
# Extract Rhasspy tools
|
||||
rhasspy_tools_file="${download_dir}/rhasspy-tools_${FRIENDLY_ARCH}.tar.gz"
|
||||
echo "Extracting tools (${rhasspy_tools_file})"
|
||||
tar -C "${venv}" -xf "${rhasspy_tools_file}"
|
||||
if [[ -z "${no_tools}" ]]; then
|
||||
# Extract Rhasspy tools
|
||||
rhasspy_tools_file="${download_dir}/rhasspy-tools_${FRIENDLY_ARCH}.tar.gz"
|
||||
echo "Extracting tools (${rhasspy_tools_file})"
|
||||
tar -C "${venv}" -xf "${rhasspy_tools_file}"
|
||||
fi
|
||||
|
||||
# Force .venv/lib to be used
|
||||
export LD_LIBRARY_PATH="${venv}/lib:${LD_LIBRARY_PATH}"
|
||||
@@ -214,11 +234,11 @@ export LD_LIBRARY_PATH="${venv}/lib:${LD_LIBRARY_PATH}"
|
||||
source "${venv}/bin/activate"
|
||||
|
||||
echo "Upgrading pip"
|
||||
"${PYTHON}" -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
echo "Installing Python requirements"
|
||||
"${PYTHON}" -m pip install wheel setuptools
|
||||
"${PYTHON}" -m pip install requests
|
||||
python3 -m pip install wheel setuptools
|
||||
python3 -m pip install requests
|
||||
|
||||
# pytorch is not available on ARM
|
||||
case "${CPU_ARCH}" in
|
||||
@@ -255,18 +275,18 @@ sed -i '/^openfst/d' "${requirements_file}"
|
||||
python3 -m pip install -r "${requirements_file}"
|
||||
|
||||
# Install Python openfst wrapper
|
||||
"${PYTHON}" -m pip install \
|
||||
--global-option=build_ext \
|
||||
--global-option="-I${venv}/include" \
|
||||
--global-option="-L${venv}/lib" \
|
||||
-r <(grep '^openfst' "${this_dir}/requirements.txt")
|
||||
python3 -m pip install \
|
||||
--global-option=build_ext \
|
||||
--global-option="-I${venv}/include" \
|
||||
--global-option="-L${venv}/lib" \
|
||||
-r <(grep '^openfst' "${this_dir}/requirements.txt")
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pocketsphinx for Python
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
pocketsphinx_file="${download_dir}/pocketsphinx-python.tar.gz"
|
||||
"${PYTHON}" -m pip install "${pocketsphinx_file}"
|
||||
python3 -m pip install "${pocketsphinx_file}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Snowboy
|
||||
@@ -275,7 +295,7 @@ pocketsphinx_file="${download_dir}/pocketsphinx-python.tar.gz"
|
||||
case "${CPU_ARCH}" in
|
||||
x86_64|armv7l)
|
||||
snowboy_file="${download_dir}/snowboy-1.3.0.tar.gz"
|
||||
"${PYTHON}" -m pip install "${snowboy_file}"
|
||||
python3 -m pip install "${snowboy_file}"
|
||||
;;
|
||||
|
||||
*)
|
||||
@@ -288,7 +308,7 @@ esac
|
||||
|
||||
if [[ -z "${no_precise}" && -z "$(command -v precise-engine)" ]]; then
|
||||
case "${CPU_ARCH}" in
|
||||
x86_64|armv7l)
|
||||
x86_64|armv7l|aarch64)
|
||||
echo "Installing Mycroft Precise"
|
||||
precise_file="${download_dir}/precise-engine_0.3.0_${CPU_ARCH}.tar.gz"
|
||||
precise_install="${venv}/lib"
|
||||
@@ -316,9 +336,11 @@ fi
|
||||
# Web Interface
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
rhasspy_web_file="${download_dir}/rhasspy-web-dist.tar.gz"
|
||||
echo "Extracting web interface (${rhasspy_web_file})"
|
||||
tar -C "${this_dir}" -xf "${rhasspy_web_file}"
|
||||
if [[ -z "${no_web}" ]]; then
|
||||
rhasspy_web_file="${download_dir}/rhasspy-web-dist.tar.gz"
|
||||
echo "Extracting web interface (${rhasspy_web_file})"
|
||||
tar -C "${this_dir}" -xf "${rhasspy_web_file}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
+26
-5
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
DIR="$( cd "$( dirname "$0" )" && pwd )"
|
||||
|
||||
# Try to detemine where Rhasspy is located
|
||||
if [[ -z "${RHASSPY_APP}" ]]; then
|
||||
@@ -16,13 +15,35 @@ if [[ -f "${CONFIG_PATH}" ]]; then
|
||||
# Hass.IO configuration
|
||||
profile_name="$(jq --raw-output '.profile_name' "${CONFIG_PATH}")"
|
||||
profile_dir="$(jq --raw-output '.profile_dir' "${CONFIG_PATH}")"
|
||||
RHASSPY_ARGS="--profile \"${profile_name}\" --user-profiles \"${profile_dir}\""
|
||||
RHASSPY_ARGS=('--profile' "${profile_name}" '--user-profiles' "${profile_dir}")
|
||||
|
||||
# Copy user-defined asoundrc to root
|
||||
asoundrc="$(jq --raw-output '.asoundrc' "${CONFIG_PATH}")"
|
||||
if [[ ! -z "${asoundrc}" ]]; then
|
||||
echo "${asoundrc}" > /root/.asoundrc
|
||||
fi
|
||||
|
||||
# Add SSL settings
|
||||
ssl="$(jq --raw-output '.ssl' "${CONFIG_PATH}")"
|
||||
if [[ "${ssl}" == 'true' ]]; then
|
||||
certfile="$(jq --raw-output '.certfile' "${CONFIG_PATH}")"
|
||||
keyfile="$(jq --raw-output '.keyfile' "${CONFIG_PATH}")"
|
||||
RHASSPY_ARGS+=('--ssl' "/ssl/${certfile}" "/ssl/${keyfile}")
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "${RHASSPY_APP}"
|
||||
RHASSPY_VENV="${RHASSPY_APP}/.venv"
|
||||
if [[ -d "${RHASSPY_VENV}" ]]; then
|
||||
source "${RHASSPY_VENV}/bin/activate"
|
||||
|
||||
if [[ -z "${RHASSPY_ARGS}" ]]; then
|
||||
# Force .venv/lib to be used
|
||||
export LD_LIBRARY_PATH="${RHASSPY_VENV}/lib:${LD_LIBRARY_PATH}"
|
||||
fi
|
||||
|
||||
cd "${RHASSPY_APP}" || exit 1
|
||||
|
||||
if [[ -z "${RHASSPY_ARGS[*]}" ]]; then
|
||||
python3 app.py "$@"
|
||||
else
|
||||
python3 app.py "${RHASSPY_ARGS}" "$@"
|
||||
python3 app.py "${RHASSPY_ARGS[@]}" "$@"
|
||||
fi
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
QEMU
|
||||
|
||||
SYSTEM_DEPENDENCIES
|
||||
|
||||
RHASSPY_TOOLS
|
||||
|
||||
PYTHON_REQUIREMENTS
|
||||
|
||||
PYTHON_POCKETSPHINX
|
||||
|
||||
SNOWBOY
|
||||
|
||||
TTS
|
||||
|
||||
KALDI
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
GSTREAMER
|
||||
|
||||
PULSEAUDIO
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
RHASSPY_USER
|
||||
|
||||
PROFILES
|
||||
|
||||
RHASSPY_CODE
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,132 +0,0 @@
|
||||
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/
|
||||
@@ -1 +0,0 @@
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
@@ -1,207 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/openfst-1.6.2-1_${BUILD_ARCH}.deb /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then \
|
||||
apt-get install --no-install-recommends --yes libfst-dev libfst-tools; \
|
||||
else \
|
||||
dpkg -i /openfst-1.6.2-1_${BUILD_ARCH}.deb; \
|
||||
rm /openfst*.deb; \
|
||||
fi
|
||||
|
||||
RHASSPY_TOOLS
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
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/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/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/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/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,96 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/openfst-1.6.2-1_${BUILD_ARCH}.deb /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then \
|
||||
apt-get install --no-install-recommends --yes libfst-dev libfst-tools; \
|
||||
else \
|
||||
dpkg -i /openfst-1.6.2-1_${BUILD_ARCH}.deb; \
|
||||
rm /openfst*.deb; \
|
||||
fi
|
||||
|
||||
RHASSPY_TOOLS
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
|
||||
|
||||
COPY profiles/en/ ${RHASSPY_APP}/profiles/en/
|
||||
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,113 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/openfst-1.6.2-1_${BUILD_ARCH}.deb /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then \
|
||||
apt-get install --no-install-recommends --yes libfst-dev libfst-tools; \
|
||||
else \
|
||||
dpkg -i /openfst-1.6.2-1_${BUILD_ARCH}.deb; \
|
||||
rm /openfst*.deb; \
|
||||
fi
|
||||
|
||||
RHASSPY_TOOLS
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
RUN apt-get install -y pulseaudio
|
||||
COPY etc/pulseaudio.client.conf /etc/pulse/client.conf
|
||||
|
||||
# Create new user
|
||||
ENV UNAME=rhasspy
|
||||
RUN export UNAME=$UNAME UID=1000 GID=1000 && \
|
||||
mkdir -p "/home/${UNAME}" && \
|
||||
echo "${UNAME}:x:${UID}:${GID}:${UNAME} User,,,:/home/${UNAME}:/bin/bash" >> /etc/passwd && \
|
||||
echo "${UNAME}:x:${UID}:" >> /etc/group && \
|
||||
mkdir -p /etc/sudoers.d && \
|
||||
echo "${UNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${UNAME} && \
|
||||
chmod 0440 /etc/sudoers.d/${UNAME} && \
|
||||
chown ${UID}:${GID} -R /home/${UNAME} && \
|
||||
gpasswd -a ${UNAME} audio
|
||||
|
||||
ENV RHASSPY_APP /home/${UNAME}
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
# Switch to new user
|
||||
USER $UNAME
|
||||
ENV HOME /home/${UNAME}
|
||||
|
||||
COPY profiles/en/ ${RHASSPY_APP}/profiles/en/
|
||||
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,222 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/rhasspy-tools_${BUILD_ARCH}.tar.gz /
|
||||
RUN tar -C /usr -xvf /rhasspy-tools_${BUILD_ARCH}.tar.gz && \
|
||||
rm -f /rhasspy-tools_${BUILD_ARCH}.tar.gz
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils lame
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
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/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
COPY VERSION ${RHASSPY_APP}/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,91 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libfst-dev libfst-tools \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/rhasspy-tools_${BUILD_ARCH}.tar.gz /
|
||||
RUN tar -C /usr -xvf /rhasspy-tools_${BUILD_ARCH}.tar.gz && \
|
||||
rm -f /rhasspy-tools_${BUILD_ARCH}.tar.gz
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
ENV RHASSPY_APP /usr/share/rhasspy
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
|
||||
|
||||
COPY profiles/en/ ${RHASSPY_APP}/profiles/en/
|
||||
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1,108 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
LABEL maintainer="Michael Hansen <hansen.mike@gmail.com>"
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG CPU_ARCH
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG MAKE_THREADS=4
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libfst-dev libfst-tools \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/rhasspy-tools_${BUILD_ARCH}.tar.gz /
|
||||
RUN tar -C /usr -xvf /rhasspy-tools_${BUILD_ARCH}.tar.gz && \
|
||||
rm -f /rhasspy-tools_${BUILD_ARCH}.tar.gz
|
||||
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils
|
||||
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
# Copy bw and mllr_solve to /usr/bin
|
||||
RUN find / -name bw -exec cp '{}' /usr/bin/ \;
|
||||
RUN find / -name mllr_solve -exec cp '{}' /usr/bin/ \;
|
||||
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
|
||||
RUN apt-get install -y pulseaudio
|
||||
COPY etc/pulseaudio.client.conf /etc/pulse/client.conf
|
||||
|
||||
# Create new user
|
||||
ENV UNAME=rhasspy
|
||||
RUN export UNAME=$UNAME UID=1000 GID=1000 && \
|
||||
mkdir -p "/home/${UNAME}" && \
|
||||
echo "${UNAME}:x:${UID}:${GID}:${UNAME} User,,,:/home/${UNAME}:/bin/bash" >> /etc/passwd && \
|
||||
echo "${UNAME}:x:${UID}:" >> /etc/group && \
|
||||
mkdir -p /etc/sudoers.d && \
|
||||
echo "${UNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${UNAME} && \
|
||||
chmod 0440 /etc/sudoers.d/${UNAME} && \
|
||||
chown ${UID}:${GID} -R /home/${UNAME} && \
|
||||
gpasswd -a ${UNAME} audio
|
||||
|
||||
ENV RHASSPY_APP /home/${UNAME}
|
||||
|
||||
# Copy script to run
|
||||
COPY docker/run.sh /run.sh
|
||||
RUN chmod +x /run.sh
|
||||
|
||||
# Switch to new user
|
||||
USER $UNAME
|
||||
ENV HOME /home/${UNAME}
|
||||
|
||||
COPY profiles/en/ ${RHASSPY_APP}/profiles/en/
|
||||
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
|
||||
ENV CONFIG_PATH /data/options.json
|
||||
ENV KALDI_PREFIX /opt
|
||||
|
||||
ENTRYPOINT ["/run.sh"]
|
||||
@@ -1 +0,0 @@
|
||||
COPY profiles/en/ ${RHASSPY_APP}/profiles/en/
|
||||
@@ -1,7 +0,0 @@
|
||||
COPY download/phonetisaurus-2019.tar.gz /phonetisaurus.tar.gz
|
||||
RUN cd / && tar -xf phonetisaurus.tar.gz
|
||||
RUN cd /phonetisaurus && \
|
||||
./configure && \
|
||||
make -j $MAKE_THREADS && \
|
||||
make install && \
|
||||
rm -rf /phonetisaurus*
|
||||
@@ -1,18 +0,0 @@
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
|
||||
COPY download/openfst-1.6.2-1_${BUILD_ARCH}.deb /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then \
|
||||
apt-get install --no-install-recommends --yes libfst-dev libfst-tools; \
|
||||
else \
|
||||
dpkg -i /openfst-1.6.2-1_${BUILD_ARCH}.deb; \
|
||||
rm /openfst*.deb; \
|
||||
fi
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Directory of *this* script
|
||||
DIR="$( cd "$( dirname "$0" )" && pwd )"
|
||||
template="$DIR/Dockerfile.template"
|
||||
out="$DIR/dockerfiles"
|
||||
mkdir -p "$out"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Uppercases an input string
|
||||
function upper {
|
||||
tr '[[:lower:]]' '[[:upper:]]'
|
||||
}
|
||||
|
||||
# Creates m4 "define" statements from text files in one or more directories.
|
||||
# The contents of dir/file.txt will be the value of variable FILE.
|
||||
function set_variables {
|
||||
echo "divert(-1)"
|
||||
while [[ ! -z "$1" ]]; do
|
||||
if [[ -d "$1" ]]; then
|
||||
for var_file in $(find "$1" -type f -name "*.txt"); do
|
||||
var_name=$(basename "$var_file" .txt | upper)
|
||||
echo "define(\`$var_name', \`$(cat $var_file)')"
|
||||
done
|
||||
elif [[ -f "$1" ]]; then
|
||||
var_file="$1"
|
||||
var_name=$(basename "$var_file" .txt | upper)
|
||||
echo "define(\`$var_name', \`$(cat $var_file)')"
|
||||
fi
|
||||
|
||||
shift
|
||||
done
|
||||
echo "divert(0)dnl"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
#------------
|
||||
# From source
|
||||
#------------
|
||||
# set_variables "$DIR/shared/" "$DIR/from-source/" \
|
||||
# "$DIR/alsa/" "$DIR/en_profile/" \
|
||||
# | cat - "$template" | m4 > "$out/Dockerfile.from-source.alsa.en"
|
||||
|
||||
# set_variables "$DIR/shared/" "$DIR/from-source/" \
|
||||
# "$DIR/pulseaudio/" "$DIR/en_profile/" \
|
||||
# | cat - "$template" | m4 > "$out/Dockerfile.from-source.pulseaudio.en"
|
||||
|
||||
# set_variables "$DIR/shared/" "$DIR/from-source/" \
|
||||
# "$DIR/alsa/" "$DIR/all_profiles/" \
|
||||
# | cat - "$template" | m4 > "$out/Dockerfile.from-source.alsa.all"
|
||||
|
||||
#-----------
|
||||
# Pre-built
|
||||
#-----------
|
||||
# set_variables "$DIR/shared/" "$DIR/prebuilt/" \
|
||||
# "$DIR/alsa/" "$DIR/en_profile/" \
|
||||
# | cat - "$template" | m4 > "$out/Dockerfile.prebuilt.alsa.en"
|
||||
|
||||
# set_variables "$DIR/shared/" "$DIR/prebuilt/" \
|
||||
# "$DIR/pulseaudio/" "$DIR/en_profile/" \
|
||||
# | cat - "$template" | m4 > "$out/Dockerfile.prebuilt.pulseaudio.en"
|
||||
|
||||
set_variables "$DIR/shared/" "$DIR/prebuilt/" \
|
||||
"$DIR/alsa/" "$DIR/all_profiles/" \
|
||||
| cat - "$template" | m4 > "$out/Dockerfile.prebuilt.alsa.all"
|
||||
@@ -1,3 +0,0 @@
|
||||
COPY download/phonetisaurus-2019_${BUILD_ARCH}.deb /phonetisaurus.deb
|
||||
RUN dpkg -i /phonetisaurus.deb && \
|
||||
rm /phonetisaurus.deb
|
||||
@@ -1,3 +0,0 @@
|
||||
COPY download/rhasspy-tools_${BUILD_ARCH}.tar.gz /
|
||||
RUN tar -C /usr -xvf /rhasspy-tools_${BUILD_ARCH}.tar.gz && \
|
||||
rm -f /rhasspy-tools_${BUILD_ARCH}.tar.gz
|
||||
@@ -1,10 +0,0 @@
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --yes \
|
||||
bash jq unzip \
|
||||
python3 python3-pip python3-dev \
|
||||
build-essential portaudio19-dev swig \
|
||||
libatlas-base-dev \
|
||||
sox espeak flite alsa-utils \
|
||||
git curl \
|
||||
autoconf libtool automake bison \
|
||||
sphinxbase-utils sphinxtrain
|
||||
@@ -1,16 +0,0 @@
|
||||
RUN apt-get install -y pulseaudio
|
||||
COPY etc/pulseaudio.client.conf /etc/pulse/client.conf
|
||||
|
||||
# Create new user
|
||||
ENV UNAME=rhasspy
|
||||
RUN export UNAME=$UNAME UID=1000 GID=1000 && \
|
||||
mkdir -p "/home/${UNAME}" && \
|
||||
echo "${UNAME}:x:${UID}:${GID}:${UNAME} User,,,:/home/${UNAME}:/bin/bash" >> /etc/passwd && \
|
||||
echo "${UNAME}:x:${UID}:" >> /etc/group && \
|
||||
mkdir -p /etc/sudoers.d && \
|
||||
echo "${UNAME} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${UNAME} && \
|
||||
chmod 0440 /etc/sudoers.d/${UNAME} && \
|
||||
chown ${UID}:${GID} -R /home/${UNAME} && \
|
||||
gpasswd -a ${UNAME} audio
|
||||
|
||||
ENV RHASSPY_APP /home/${UNAME}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Switch to new user
|
||||
USER $UNAME
|
||||
ENV HOME /home/${UNAME}
|
||||
@@ -1,2 +0,0 @@
|
||||
# Install gstreamer and plugins
|
||||
RUN apt-get install --no-install-recommends --yes gstreamer1.0-tools gstreamer1.0-plugins-good
|
||||
@@ -1,4 +0,0 @@
|
||||
COPY download/kaldi_${BUILD_ARCH}.tar.gz /kaldi.tar.gz
|
||||
RUN mkdir -p /opt && \
|
||||
tar -C /opt -xzf /kaldi.tar.gz && \
|
||||
rm /kaldi.tar.gz
|
||||
@@ -1,9 +0,0 @@
|
||||
# Install mitlm
|
||||
RUN apt-get install --no-install-recommends --yes gfortran
|
||||
COPY download/mitlm-0.4.2.tar.xz /
|
||||
RUN cd / && tar -xf mitlm-0.4.2.tar.xz && cd mitlm-0.4.2/ && \
|
||||
./configure && \
|
||||
make -j $MAKE_THREADS && \
|
||||
make install && \
|
||||
rm -rf /mitlm-0.4.2*
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Install Mycroft Precise
|
||||
COPY download/precise-engine_0.3.0_${CPU_ARCH}.tar.gz /precise-engine.tar.gz
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then \
|
||||
cd / && tar -xzf /precise-engine.tar.gz && \
|
||||
ln -s /precise-engine/precise-engine /usr/bin/precise-engine && \
|
||||
rm /precise-engine.tar.gz; \
|
||||
fi
|
||||
@@ -1,9 +0,0 @@
|
||||
# Install Opengrm
|
||||
COPY download/opengrm-ngram-1.3.3.tar.gz /
|
||||
RUN cd / && tar -xf opengrm-ngram-1.3.3.tar.gz && \
|
||||
cd opengrm-ngram-1.3.3 && \
|
||||
./configure && \
|
||||
make -j $MAKE_THREADS && \
|
||||
make install && \
|
||||
ldconfig && \
|
||||
rm -rf /opengrm*
|
||||
@@ -1,4 +0,0 @@
|
||||
# Install Pocketsphinx Python module with no sound
|
||||
COPY download/pocketsphinx-python.tar.gz /
|
||||
RUN python3 -m pip install --no-cache-dir /pocketsphinx-python.tar.gz && \
|
||||
rm -rf /pocketsphinx-python*
|
||||
@@ -1,10 +0,0 @@
|
||||
# Install Python dependencies
|
||||
RUN python3 -m pip install --no-cache-dir setuptools wheel
|
||||
|
||||
RUN apt-get install -y libfreetype6-dev libpng-dev pkg-config libffi-dev libssl-dev
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN if [ "$BUILD_ARCH" != "amd64" ]; then \
|
||||
grep -v flair /requirements.txt > /requirements-noflair.txt; \
|
||||
mv /requirements-noflair.txt /requirements.txt; \
|
||||
fi
|
||||
RUN python3 -m pip install --no-cache-dir -r /requirements.txt
|
||||
@@ -1,2 +0,0 @@
|
||||
COPY etc/qemu-arm-static /usr/bin/
|
||||
COPY etc/qemu-aarch64-static /usr/bin/
|
||||
@@ -1,10 +0,0 @@
|
||||
COPY profiles/defaults.json ${RHASSPY_APP}/profiles/
|
||||
COPY docker/rhasspy ${RHASSPY_APP}/bin/
|
||||
COPY dist/ ${RHASSPY_APP}/dist/
|
||||
COPY etc/wav/* ${RHASSPY_APP}/etc/wav/
|
||||
COPY rhasspy/profile_schema.json ${RHASSPY_APP}/rhasspy/
|
||||
COPY rhasspy/train/jsgf2fst/*.py ${RHASSPY_APP}/rhasspy/train/jsgf2fst/
|
||||
COPY rhasspy/train/*.py ${RHASSPY_APP}/rhasspy/train/
|
||||
COPY *.py ${RHASSPY_APP}/
|
||||
COPY rhasspy/*.py ${RHASSPY_APP}/rhasspy/
|
||||
COPY VERSION ${RHASSPY_APP}/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Install snowboy
|
||||
COPY download/snowboy-1.3.0.tar.gz /
|
||||
RUN if [ "$BUILD_ARCH" != "aarch64" ]; then pip3 install --no-cache-dir /snowboy-1.3.0.tar.gz; fi
|
||||
@@ -1 +0,0 @@
|
||||
RUN apt-get install --no-install-recommends --yes flite libttspico-utils lame
|
||||
+9
-1
@@ -69,7 +69,15 @@ Add to your [profile](profiles.md):
|
||||
"username": "",
|
||||
"port": 1883,
|
||||
"password": "",
|
||||
"site_id": "default"
|
||||
"site_id": "default",
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -44,7 +44,15 @@ Add to your [profile](profiles.md):
|
||||
"username": "",
|
||||
"port": 1883,
|
||||
"password": "",
|
||||
"site_id": "default"
|
||||
"site_id": "default",
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -86,7 +86,15 @@ Add to your [profile](profiles.md):
|
||||
"username": "",
|
||||
"port": 1883,
|
||||
"password": "",
|
||||
"site_id": "default"
|
||||
"site_id": "default",
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-1
@@ -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)
|
||||
|
||||
+16
-3
@@ -54,7 +54,13 @@ To update your Rhasspy Docker image, just run:
|
||||
```bash
|
||||
docker pull synesthesiam/rhasspy-server:latest
|
||||
```
|
||||
on your Rhasspy server and restart the Docker container.
|
||||
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
|
||||
|
||||
@@ -108,10 +114,17 @@ To update your Rhasspy virtual environment to the latest version, run:
|
||||
git pull origin master
|
||||
```
|
||||
|
||||
in your `rhasspy` directory. You should also re-build the web interface:
|
||||
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
|
||||
2. Run `yarn install && yarn build` in the `rhasspy` directory
|
||||
3. Restart any running instances of Rhasspy
|
||||
|
||||
### Running as a Service
|
||||
|
||||
+36
-9
@@ -65,6 +65,25 @@ In order to do something with the `rhasspy_ChangeLightColor` event, create an au
|
||||
|
||||
See the documentation on [actions](https://www.home-assistant.io/docs/automation/action/) for the different things you can do with Home Assistant.
|
||||
|
||||
### Intents
|
||||
|
||||
More recent versions of Home Assistant can accept intents directly. Add the following to your `configuration.yaml` file:
|
||||
|
||||
```yaml
|
||||
intent:
|
||||
```
|
||||
|
||||
This will enable intents over the HTTP API. Next, write [intent scripts](https://www.home-assistant.io/integrations/intent_script) to handle each Rhasspy intent:
|
||||
|
||||
```yaml
|
||||
intent_script:
|
||||
ChangeLightColor:
|
||||
action:
|
||||
...
|
||||
```
|
||||
|
||||
The possible [actions](https://www.home-assistant.io/docs/automation/action/) are the same as in automations.
|
||||
|
||||
### MQTT
|
||||
|
||||
In addition to events, Rhasspy can also publish intents through MQTT ([Hermes protocol](https://docs.snips.ai/reference/dialogue#intent)).
|
||||
@@ -74,14 +93,22 @@ Add to your [profile](profiles.md):
|
||||
|
||||
```json
|
||||
"mqtt": {
|
||||
"enabled": true,
|
||||
"host": "localhost",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 1883,
|
||||
"reconnect_sec": 5,
|
||||
"site_id": "default",
|
||||
"publish_intents": true
|
||||
"enabled": true,
|
||||
"host": "localhost",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 1883,
|
||||
"reconnect_sec": 5,
|
||||
"site_id": "default",
|
||||
"publish_intents": true,
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -207,7 +234,7 @@ The following environment variables are available to your program:
|
||||
* `$RHASSPY_PROFILE` - name of the current profile (e.g., "en")
|
||||
* `$RHASSPY_PROFILE_DIR` - directory of the current profile (where `profile.json` is)
|
||||
|
||||
See [handle.sh](https://github.com/synesthesiam/rhasspy/blob/master/bin/mock-commands/handle.sh) for an example program.
|
||||
See [handle.sh](https://github.com/synesthesiam/rhasspy/blob/master/bin/mock-commands/handle.sh) or [handle.py](https://github.com/synesthesiam/rhasspy/blob/master/bin/mock-commands/handle.py) for example programs.
|
||||
|
||||
### Speech
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ The following table summarizes the trade-offs of using each intent recognizer:
|
||||
| [fsticuffs](intent-recognition.md#fsticuffs) | 1M+ | very fast | very fast | ignores unknown words |
|
||||
| [fuzzywuzzy](intent-recognition.md#fuzzywuzzy) | 12-100 | fast | fast | fuzzy string matching |
|
||||
| [adapt](intent-recognition.md#mycroft-adapt) | 100-1K | moderate | fast | ignores unknown words |
|
||||
| [flair](intent-recognition.md#flair) | 1K-100K | very slow | moderate | handles unseen words |
|
||||
| [rasaNLU](intent-recognition.md#rasanlu) | 1K-100K | very slow | moderate | handles unseen words |
|
||||
| [flair](intent-recognition.md#flair) | 1K-100K | very slow | moderate | handles unseen words |
|
||||
|
||||
## Fsticuffs
|
||||
|
||||
@@ -55,7 +55,7 @@ See `rhasspy.intent.FuzzyWuzzyRecognizer` for details.
|
||||
|
||||
## Mycroft Adapt
|
||||
|
||||
Recognizes intents using [Mycroft Adapt](https://github.com/MycroftAI/adapt). Works best when you have a medium number of sentences (hundreds to thousands) and need to be able to recognize sentences not seen during training (no new words, though).
|
||||
Recognizes intents using [Mycroft Adapt](https://github.com/MycroftAI/adapt). Works best when you have a medium number of sentences (hundreds to thousands) and need to be able to recognize sentences not seen during training (no new words, though). This recognizer does not support converters, i.e. numbers are not converted back to integers.
|
||||
|
||||
Add to your [profile](profiles.md):
|
||||
|
||||
|
||||
+3
-2
@@ -52,8 +52,9 @@ Application authors may want to use the [rhasspy-client](https://pypi.org/projec
|
||||
* `?nohass=true` - stop Rhasspy from handling the intent
|
||||
* `?timeout=<seconds>` - override default command timeout
|
||||
* `?entity=<entity>&value=<value>` - set custom entity/value in recognized intent
|
||||
* `/api/listen-for-wake-word`
|
||||
* POST to wake Rhasspy up and return immediately
|
||||
* `/api/listen-for-wake`
|
||||
* POST "on" to have Rhasspy listen for a wake word
|
||||
* POST "off" to disable wake word
|
||||
* `/api/lookup`
|
||||
* POST word as plain text to look up or guess pronunciation
|
||||
* `?n=<number>` - return at most `n` guessed pronunciations
|
||||
|
||||
@@ -8,6 +8,7 @@ The following table summarizes language support for the various speech to text s
|
||||
| ------ | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |
|
||||
| [pocketsphinx](speech-to-text.md#pocketsphinx) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ |
|
||||
| [kaldi](speech-to-text.md#kaldi) | ✓ | ✓ | | ✓ | | ✓ | | | | | ✓ | | |
|
||||
| [google](speech-to-text.md#google-cloud) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
## Pocketsphinx
|
||||
|
||||
@@ -77,6 +78,29 @@ Rhasspy expects a Kaldi-compatible profile to contain a `model` directory with a
|
||||
|
||||
If you just want to use Rhasspy for general speech to text, you can set `speech_to_text.kaldi.open_transcription` to `true` in your profile. This will use the included general language model (much slower) and ignore any custom voice commands you've specified.
|
||||
|
||||
## Google Cloud
|
||||
|
||||
Does speech recognition using [Google Cloud Speech-to-Text](https://cloud.google.com/speech-to-text) service.
|
||||
You will need an active Google Cloud subscription and a JSON private key connected to a service account enabled to use
|
||||
the speech-to-text API. The locale configured in your profile will be used for speech recognition.
|
||||
|
||||
```json
|
||||
{
|
||||
"locale": "en_US",
|
||||
"speech_to_text": {
|
||||
"system": "google",
|
||||
"google": {
|
||||
"credentials": "api-project-xxxxxxxx-abcdef.json",
|
||||
"min_confidence": 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Please note that this module sends the recorded audio after it's completed, so no streaming support.
|
||||
|
||||
See `rhasspy.stt.GoogleCloudDecoder` for details.
|
||||
|
||||
## Remote HTTP Server
|
||||
|
||||
Uses a remote HTTP server to transform speech (WAV) to text.
|
||||
|
||||
+16
-1
@@ -29,6 +29,19 @@ Add to your [profile](profiles.md):
|
||||
|
||||
Remove the `voice` option to have `espeak` use your profile's language automatically.
|
||||
|
||||
You may also pass additional arguments to the `espeak` command. For example,
|
||||
|
||||
```json
|
||||
"text_to_speech": {
|
||||
"system": "espeak",
|
||||
"espeak": {
|
||||
"arguments": ["-s", "80"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
will speak the sentence more slowly.
|
||||
|
||||
See `rhasspy.tts.EspeakSentenceSpeaker` for more details.
|
||||
|
||||
## Flite
|
||||
@@ -52,7 +65,9 @@ See `rhasspy.tts.FliteSentenceSpeaker` for details.
|
||||
|
||||
## PicoTTS
|
||||
|
||||
Uses SVOX's [picotts](https://en.wikipedia.org/wiki/SVOX) for text to speech. Sounds a bit better (to me) than `flite` or `espeak`, but only has a single English voice.
|
||||
Uses SVOX's [picotts](https://en.wikipedia.org/wiki/SVOX) for text to speech. Sounds a bit better (to me) than `flite` or `espeak`.
|
||||
|
||||
Included languages are `en-US`, `en-GB`, `de-DE`, `es-ES`, `fr-FR` and `it-IT`.
|
||||
|
||||
Add to your [profile](profiles.md):
|
||||
|
||||
|
||||
+8
-1
@@ -247,7 +247,7 @@ Add a file in `slot_programs` with the name of your slot, e.g. `colors`. Write a
|
||||
|
||||
```bash
|
||||
cat <<EOF > "${slot_programs}/colors"
|
||||
#/usr/bin/env bash
|
||||
#!/usr/bin/env bash
|
||||
echo 'red'
|
||||
echo 'green'
|
||||
echo 'blue'
|
||||
@@ -262,6 +262,13 @@ You can pass **arguments** to your program using the syntax `$name,arg1,arg2,...
|
||||
|
||||
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:
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
* [RGB Light Example](#rgb-light-example)
|
||||
* [Client/Server Setup](#clientserver-setup)
|
||||
* MATRIX Labs
|
||||
* [Rhasspy Voice Assistant on MATRIX Voice and MATRIX Creator](https://www.hackster.io/matrix-labs/rhasspy-voice-assistant-on-matrix-voice-and-matrix-creator-97f92e)
|
||||
* [Adding Intents for Rhasspy Offline Voice Assistant](https://www.hackster.io/matrix-labs/adding-intents-for-rhasspy-offline-voice-assistant-faa221)
|
||||
* Rendered Obsolete
|
||||
* [Home Assistant Voice Recognition with Rhasspy](https://rendered-obsolete.github.io/2020/01/02/rhasspy.html)
|
||||
|
||||
## RGB Light Example
|
||||
|
||||
|
||||
+39
-1
@@ -142,7 +142,18 @@ More example flows are available [on Github](https://github.com/synesthesiam/rha
|
||||
|
||||
### WebSocket Events
|
||||
|
||||
Whenever a voice command is recognized, Rhasspy emits JSON events over a websocket connection available at `ws://rhasspy:12101/api/events/intent` (replace `ws://` with `wss://` if you're using [secure hosting](usage.md#secure-hosting-with-https)).
|
||||
Rhasspy supports multiple websocket event endpoints:
|
||||
|
||||
* `/api/events/intent`
|
||||
* Intent recognized or not
|
||||
* `/api/events/wake`
|
||||
* Wake word detected
|
||||
* `/api/events/text`
|
||||
* Speech transcription
|
||||
|
||||
#### WebSocket Intents
|
||||
|
||||
Whenever a voice command is recognized, Rhasspy emits JSON events over a websocket connection available at `ws://YOUR_SERVER:12101/api/events/intent` (replace `ws://` with `wss://` if you're using [secure hosting](usage.md#secure-hosting-with-https)).
|
||||
You can listen to these events in a [Node-RED](https://nodered.org) flow, and easily add offline, private voice commands to your home automation set up!
|
||||
|
||||
For the `ChangLightState` intent from the [RGB Light Example](index.md#rgb-light-example), Rhasspy will emit a JSON event like this over the websocket:
|
||||
@@ -171,6 +182,33 @@ For the `ChangLightState` intent from the [RGB Light Example](index.md#rgb-light
|
||||
}
|
||||
```
|
||||
|
||||
#### WebSocket Wake
|
||||
|
||||
When the wake word is detected, or Rhasspy is woken up via the `/api/listen-for-command` HTTP endpoint, a JSON event is emitted at `ws://YOUR_SERVER:12101/api/events/wake` (`wss://` if using HTTPS) like:
|
||||
|
||||
```json
|
||||
{
|
||||
"wakewordId": "default",
|
||||
"siteId": "default"
|
||||
}
|
||||
```
|
||||
|
||||
The `wakewordId` is set using the model or file name of your wakeword model (e.g., `porcupine` for `porcupine.ppn`). The `siteId` comes from your `mqtt.siteId` profile setting.
|
||||
|
||||
#### WebSocket Transcriptions
|
||||
|
||||
Each time a voice command is transcribed, Rhasspy emits a JSON event at `ws://YOUR_SERVER:12101/api/events/text` (`wss://` if using HTTPS) like:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "text from voice command",
|
||||
"wakewordId": "default",
|
||||
"siteId": "default"
|
||||
}
|
||||
```
|
||||
|
||||
The transcription is contained in the `text` property. `wakewordId` is the id of the wakeword that initiated the voice command (or `default`). The `siteId` comes from your `mqtt.siteId` profile setting.
|
||||
|
||||
## MQTT and Snips
|
||||
|
||||
Rhasspy is able to interoperate with Snips.AI services using the [Hermes protocol](https://docs.snips.ai/reference/hermes) over [MQTT](http://mqtt.org). The following components are Snips/Hermes compatible:
|
||||
|
||||
+9
-1
@@ -181,7 +181,15 @@ Add to your [profile](profiles.md):
|
||||
"username": "",
|
||||
"port": 1883,
|
||||
"password": "",
|
||||
"site_id": "default"
|
||||
"site_id": "default",
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ cpu_arch=$(uname --m)
|
||||
DEFINE_string 'download-dir' "${this_dir}/download" 'Directory to cache downloaded files'
|
||||
DEFINE_boolean 'precise' true 'Install Mycroft Precise'
|
||||
DEFINE_boolean 'kaldi' true 'Install Kaldi'
|
||||
DEFINE_boolean 'web' true "Install web UI"
|
||||
DEFINE_boolean 'offline' false "Don't download anything"
|
||||
DEFINE_boolean 'all-cpu' false 'Download dependencies for all CPU architectures'
|
||||
DEFINE_string 'cpu-arch' "${cpu_arch}" 'CPU architecture (x86_64, armv7l, arm64v8, armv6l)'
|
||||
@@ -44,6 +45,10 @@ if [[ "${FLAGS_kaldi}" -eq "${FLAGS_FALSE}" ]]; then
|
||||
no_kaldi='true'
|
||||
fi
|
||||
|
||||
if [[ "${FLAGS_web}" -eq "${FLAGS_FALSE}" ]]; then
|
||||
no_web='true'
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
function maybe_download {
|
||||
@@ -65,8 +70,15 @@ declare -A CPU_TO_FRIENDLY
|
||||
CPU_TO_FRIENDLY["x86_64"]="amd64"
|
||||
CPU_TO_FRIENDLY["armv7l"]="armhf"
|
||||
CPU_TO_FRIENDLY["arm64v8"]="aarch64"
|
||||
CPU_TO_FRIENDLY["aarch64"]="aarch64"
|
||||
CPU_TO_FRIENDLY["armv6l"]="armv6l"
|
||||
|
||||
declare -A FRIENDLY_TO_DOCKER
|
||||
FRIENDLY_TO_DOCKER["amd64"]="amd64"
|
||||
FRIENDLY_TO_DOCKER["armhf"]="armv7"
|
||||
FRIENDLY_TO_DOCKER["aarch64"]="arm64"
|
||||
FRIENDLY_TO_DOCKER["armv6l"]="armv6"
|
||||
|
||||
# CPU architecture
|
||||
if [[ -n "${all_cpu}" ]]; then
|
||||
CPU_ARCHS=("x86_64" "armv7l" "arm64v8")
|
||||
@@ -81,12 +93,32 @@ fi
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
for FRIENDLY_ARCH in "${FRIENDLY_ARCHS[@]}"; do
|
||||
rhasspy_files=("rhasspy-tools_${FRIENDLY_ARCH}.tar.gz" "rhasspy-web-dist.tar.gz")
|
||||
rhasspy_files=()
|
||||
|
||||
if [[ -z "${no_tools}" ]]; then
|
||||
# Install Rhasspy tools
|
||||
rhasspy_files+=("rhasspy-tools_${FRIENDLY_ARCH}.tar.gz")
|
||||
fi
|
||||
|
||||
if [[ -z "${no_web}" ]]; then
|
||||
# Install web UI
|
||||
rhasspy_files+=('rhasspy-web-dist.tar.gz')
|
||||
fi
|
||||
|
||||
for rhasspy_file_name in "${rhasspy_files[@]}"; do
|
||||
rhasspy_file="${download_dir}/${rhasspy_file_name}"
|
||||
rhasspy_file_url="https://github.com/synesthesiam/rhasspy/releases/download/v2.0/${rhasspy_file_name}"
|
||||
maybe_download "${rhasspy_file_url}" "${rhasspy_file}"
|
||||
done
|
||||
|
||||
if [[ -z "${no_tools}" ]]; then
|
||||
# Create link for docker buildx
|
||||
DOCKER_ARCH="${FRIENDLY_TO_DOCKER[${FRIENDLY_ARCH}]}"
|
||||
if [[ "${FRIENDLY_ARCH}" != "${DOCKER_ARCH}" ]]; then
|
||||
ln -f "${download_dir}/rhasspy-tools_${FRIENDLY_ARCH}.tar.gz" \
|
||||
"${download_dir}/rhasspy-tools_${DOCKER_ARCH}.tar.gz"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -112,10 +144,18 @@ maybe_download "${snowboy_url}" "${snowboy_file}"
|
||||
if [[ -z "${no_precise}" ]]; then
|
||||
for CPU_ARCH in "${CPU_ARCHS[@]}"; do
|
||||
case $CPU_ARCH in
|
||||
x86_64|armv7l)
|
||||
x86_64|armv7l|aarch64)
|
||||
precise_file="${download_dir}/precise-engine_0.3.0_${CPU_ARCH}.tar.gz"
|
||||
precise_url="https://github.com/MycroftAI/mycroft-precise/releases/download/v0.3.0/precise-engine_0.3.0_${CPU_ARCH}.tar.gz"
|
||||
maybe_download "${precise_url}" "${precise_file}"
|
||||
|
||||
# Create link for docker buildx
|
||||
FRIENDLY_ARCH="${CPU_TO_FRIENDLY[${CPU_ARCH}]}"
|
||||
DOCKER_ARCH="${FRIENDLY_TO_DOCKER[${FRIENDLY_ARCH}]}"
|
||||
if [[ "${CPU_ARCH}" != "${DOCKER_ARCH}" ]]; then
|
||||
ln -f "${download_dir}/precise-engine_0.3.0_${CPU_ARCH}.tar.gz" \
|
||||
"${download_dir}/precise-engine_0.3.0_${DOCKER_ARCH}.tar.gz"
|
||||
fi
|
||||
esac
|
||||
done
|
||||
fi
|
||||
@@ -126,10 +166,19 @@ fi
|
||||
|
||||
if [[ -z "${no_kaldi}" ]]; then
|
||||
for FRIENDLY_ARCH in "${FRIENDLY_ARCHS[@]}"; do
|
||||
# Install pre-built package
|
||||
kaldi_file="${download_dir}/kaldi_${FRIENDLY_ARCH}.tar.gz"
|
||||
kaldi_url="https://github.com/synesthesiam/kaldi-docker/releases/download/v1.0/kaldi_${FRIENDLY_ARCH}.tar.gz"
|
||||
maybe_download "${kaldi_url}" "${kaldi_file}"
|
||||
if [[ "${FRIENDLY_ARCH}" != "armv6l" ]]; then
|
||||
# Install pre-built package
|
||||
kaldi_file="${download_dir}/kaldi_${FRIENDLY_ARCH}.tar.gz"
|
||||
kaldi_url="https://github.com/synesthesiam/kaldi-docker/releases/download/v1.0/kaldi_${FRIENDLY_ARCH}.tar.gz"
|
||||
maybe_download "${kaldi_url}" "${kaldi_file}"
|
||||
|
||||
# Create link for docker buildx
|
||||
DOCKER_ARCH="${FRIENDLY_TO_DOCKER[${FRIENDLY_ARCH}]}"
|
||||
if [[ "${FRIENDLY_ARCH}" != "${DOCKER_ARCH}" ]]; then
|
||||
ln -f "${download_dir}/kaldi_${FRIENDLY_ARCH}.tar.gz" \
|
||||
"${download_dir}/kaldi_${DOCKER_ARCH}.tar.gz"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
+61
-48
@@ -1,17 +1,12 @@
|
||||
#
|
||||
# Copyright 2018 Picovoice Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
|
||||
# file accompanying this source.
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
@@ -20,7 +15,7 @@ from enum import Enum
|
||||
|
||||
|
||||
class Porcupine(object):
|
||||
"""Python binding for Picovoice's wake word detection (aka Porcupine) library."""
|
||||
"""Python binding for Picovoice's wake word detection (Porcupine) engine."""
|
||||
|
||||
class PicovoiceStatuses(Enum):
|
||||
"""Status codes corresponding to 'pv_status_t' defined in 'include/picovoice.h'"""
|
||||
@@ -29,11 +24,17 @@ class Porcupine(object):
|
||||
OUT_OF_MEMORY = 1
|
||||
IO_ERROR = 2
|
||||
INVALID_ARGUMENT = 3
|
||||
STOP_ITERATION = 4
|
||||
KEY_ERROR = 5
|
||||
INVALID_STATE = 6
|
||||
|
||||
_PICOVOICE_STATUS_TO_EXCEPTION = {
|
||||
PicovoiceStatuses.OUT_OF_MEMORY: MemoryError,
|
||||
PicovoiceStatuses.IO_ERROR: IOError,
|
||||
PicovoiceStatuses.INVALID_ARGUMENT: ValueError
|
||||
PicovoiceStatuses.INVALID_ARGUMENT: ValueError,
|
||||
PicovoiceStatuses.STOP_ITERATION: StopIteration,
|
||||
PicovoiceStatuses.KEY_ERROR: KeyError,
|
||||
PicovoiceStatuses.INVALID_STATE: ValueError,
|
||||
}
|
||||
|
||||
class CPorcupine(Structure):
|
||||
@@ -48,9 +49,9 @@ class Porcupine(object):
|
||||
keyword_file_paths=None,
|
||||
sensitivities=None):
|
||||
"""
|
||||
Loads Porcupine's shared library and creates an instance of wake word detection object.
|
||||
Constructor.
|
||||
|
||||
:param library_path: Absolute path to Porcupine's shared library.
|
||||
:param library_path: Absolute path to Porcupine's dynamic library.
|
||||
:param model_file_path: Absolute path to file containing model parameters.
|
||||
:param keyword_file_path: Absolute path to keyword file containing hyper-parameters. If not present then
|
||||
'keyword_file_paths' will be used.
|
||||
@@ -64,38 +65,38 @@ class Porcupine(object):
|
||||
"""
|
||||
|
||||
if not os.path.exists(library_path):
|
||||
raise IOError(f"Could not find Porcupine's library at '{library_path}'")
|
||||
raise IOError("could'nt find Porcupine's library at '%s'" % library_path)
|
||||
|
||||
library = cdll.LoadLibrary(library_path)
|
||||
|
||||
if not os.path.exists(model_file_path):
|
||||
raise IOError(f"Could not find model file at '{model_file_path}'")
|
||||
raise IOError("could'nt find model file at '%s'" % 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(f"Could not find keyword file at '{keyword_file_path}'")
|
||||
raise IOError("could'nt' find keyword file at '%s'" % keyword_file_path)
|
||||
keyword_file_paths = [keyword_file_path]
|
||||
|
||||
if not (0 <= sensitivity <= 1):
|
||||
raise ValueError('Sensitivity should be within [0, 1]')
|
||||
raise ValueError('sensitivity should be within [0, 1]')
|
||||
sensitivities = [sensitivity]
|
||||
elif sensitivities is not None and keyword_file_paths is not None:
|
||||
if len(keyword_file_paths) != len(sensitivities):
|
||||
raise ValueError("Different number of sensitivity and keyword file path parameters are provided.")
|
||||
raise ValueError("different number of sensitivity and keyword file path parameters are provided.")
|
||||
|
||||
for x in keyword_file_paths:
|
||||
if not os.path.exists(os.path.expanduser(x)):
|
||||
raise IOError(f"Could not find keyword file at '{x}'")
|
||||
raise IOError("could not find keyword file at '%s'" % x)
|
||||
|
||||
for x in sensitivities:
|
||||
if not (0 <= x <= 1):
|
||||
raise ValueError('Sensitivity should be within [0, 1]')
|
||||
raise ValueError('sensitivity should be within [0, 1]')
|
||||
else:
|
||||
raise ValueError("Sensitivity and/or keyword file path is missing")
|
||||
raise ValueError("sensitivity and/or keyword file path is missing")
|
||||
|
||||
self._num_keywords = len(keyword_file_paths)
|
||||
|
||||
init_func = library.pv_porcupine_multiple_keywords_init
|
||||
init_func = library.pv_porcupine_init
|
||||
init_func.argtypes = [
|
||||
c_char_p,
|
||||
c_int,
|
||||
@@ -107,44 +108,43 @@ class Porcupine(object):
|
||||
self._handle = POINTER(self.CPorcupine)()
|
||||
|
||||
status = init_func(
|
||||
model_file_path.encode(),
|
||||
model_file_path.encode('utf-8'),
|
||||
self._num_keywords,
|
||||
(c_char_p * self._num_keywords)(*[os.path.expanduser(x).encode() for x in keyword_file_paths]),
|
||||
(c_char_p * self._num_keywords)(*[os.path.expanduser(x).encode('utf-8') for x in keyword_file_paths]),
|
||||
(c_float * self._num_keywords)(*sensitivities),
|
||||
byref(self._handle))
|
||||
if status is not self.PicovoiceStatuses.SUCCESS:
|
||||
raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]('Initialization failed')
|
||||
|
||||
self.process_func = library.pv_porcupine_multiple_keywords_process
|
||||
self.process_func.argtypes = [POINTER(self.CPorcupine), POINTER(c_short), POINTER(c_int)]
|
||||
self.process_func.restype = self.PicovoiceStatuses
|
||||
raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]('initialization failed')
|
||||
|
||||
self._delete_func = library.pv_porcupine_delete
|
||||
self._delete_func.argtypes = [POINTER(self.CPorcupine)]
|
||||
self._delete_func.restype = None
|
||||
|
||||
self._sample_rate = library.pv_sample_rate()
|
||||
self.process_func = library.pv_porcupine_process
|
||||
self.process_func.argtypes = [POINTER(self.CPorcupine), POINTER(c_short), POINTER(c_int)]
|
||||
self.process_func.restype = self.PicovoiceStatuses
|
||||
|
||||
version_func = library.pv_porcupine_version
|
||||
version_func.argtypes = []
|
||||
version_func.restype = c_char_p
|
||||
self._version = version_func().decode('utf-8')
|
||||
|
||||
self._frame_length = library.pv_porcupine_frame_length()
|
||||
|
||||
@property
|
||||
def sample_rate(self):
|
||||
"""Audio sample rate accepted by Porcupine library."""
|
||||
self._sample_rate = library.pv_sample_rate()
|
||||
|
||||
return self._sample_rate
|
||||
def delete(self):
|
||||
"""Releases resources acquired by Porcupine's library."""
|
||||
|
||||
@property
|
||||
def frame_length(self):
|
||||
"""Number of audio samples per frame expected by C library."""
|
||||
|
||||
return self._frame_length
|
||||
self._delete_func(self._handle)
|
||||
|
||||
def process(self, pcm):
|
||||
"""
|
||||
Monitors incoming audio stream for given wake word(s).
|
||||
Processes a frame of the incoming audio stream and emits the detection result.
|
||||
|
||||
:param pcm: An array (or array-like) of consecutive audio samples. For more information regarding required audio
|
||||
properties (i.e. sample rate, number of channels encoding, and number of samples per frame) please refer to
|
||||
'include/pv_porcupine.h'.
|
||||
:param pcm: A frame of audio samples. The number of samples per frame can be attained by calling
|
||||
'.frame_length'. The incoming audio needs to have a sample rate equal to '.sample_rate' and be 16-bit
|
||||
linearly-encoded. Porcupine operates on single-channel audio.
|
||||
:return: For a single wake-word use cse True if wake word is detected. For multiple wake-word use case it
|
||||
returns the index of detected wake-word. Indexing is 0-based and according to ordering of input keyword file
|
||||
paths. It returns -1 when no keyword is detected.
|
||||
@@ -153,7 +153,7 @@ class Porcupine(object):
|
||||
result = c_int()
|
||||
status = self.process_func(self._handle, (c_short * len(pcm))(*pcm), byref(result))
|
||||
if status is not self.PicovoiceStatuses.SUCCESS:
|
||||
raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]('Processing failed')
|
||||
raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]()
|
||||
|
||||
keyword_index = result.value
|
||||
|
||||
@@ -162,7 +162,20 @@ class Porcupine(object):
|
||||
else:
|
||||
return keyword_index
|
||||
|
||||
def delete(self):
|
||||
"""Releases resources acquired by Porcupine's library."""
|
||||
@property
|
||||
def version(self):
|
||||
"""Getter for version"""
|
||||
|
||||
self._delete_func(self._handle)
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def frame_length(self):
|
||||
"""Getter for number of audio samples per frame."""
|
||||
|
||||
return self._frame_length
|
||||
|
||||
@property
|
||||
def sample_rate(self):
|
||||
"""Audio sample rate accepted by Picovoice."""
|
||||
|
||||
return self._sample_rate
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"base_language_model": "kaldi/base_language_model.txt",
|
||||
"base_language_model_fst": "kaldi/base_language_model.fst",
|
||||
"compatible": true,
|
||||
"custom_words": "kaldi/custom_words.txt",
|
||||
"custom_words": "kaldi_custom_words.txt",
|
||||
"dictionary": "kaldi/dictionary.txt",
|
||||
"graph": "graph",
|
||||
"language_model": "kaldi/language_model.txt",
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
+18
-9
@@ -76,7 +76,8 @@
|
||||
"rasa": {
|
||||
"examples_markdown": "intent_examples.md",
|
||||
"project_name": "rhasspy",
|
||||
"url": "http://localhost:5005/"
|
||||
"url": "http://localhost:5005/",
|
||||
"model_dir": "/app/models"
|
||||
},
|
||||
"remote": {
|
||||
"url": "http://my-server:12101/api/text-to-intent"
|
||||
@@ -118,7 +119,15 @@
|
||||
"publish_intents": true,
|
||||
"reconnect_sec": 5,
|
||||
"site_id": "default",
|
||||
"username": ""
|
||||
"username": "",
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
},
|
||||
"rhasspy": {
|
||||
"listen_on_start": true,
|
||||
@@ -323,31 +332,31 @@
|
||||
"cache": false
|
||||
},
|
||||
"porcupine_params.pv": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/lib/common/porcupine_params.pv",
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/lib/common/porcupine_params.pv",
|
||||
"cache": false
|
||||
},
|
||||
"porcupine.ppn": {
|
||||
"cache": false,
|
||||
"x86_64": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/resources/keyword_files/linux/porcupine_linux.ppn"
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/v1.7/resources/keyword_files/linux/porcupine_linux.ppn"
|
||||
},
|
||||
"armv7l": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/resources/keyword_files/raspberrypi/porcupine_raspberrypi.ppn"
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/resources/keyword_files/raspberry-pi/porcupine_raspberry-pi.ppn"
|
||||
},
|
||||
"aarch64": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/resources/keyword_files/raspberrypi/porcupine_raspberrypi.ppn"
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/resources/keyword_files/raspberry-pi/porcupine_raspberry-pi.ppn"
|
||||
}
|
||||
},
|
||||
"libpv_porcupine.so": {
|
||||
"cache": false,
|
||||
"x86_64": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/lib/linux/x86_64/libpv_porcupine.so"
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/lib/linux/x86_64/libpv_porcupine.so"
|
||||
},
|
||||
"armv7l": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/lib/raspberry-pi/cortex-a53/libpv_porcupine.so"
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/lib/raspberry-pi/cortex-a53/libpv_porcupine.so"
|
||||
},
|
||||
"aarch64": {
|
||||
"url": "https://github.com/Picovoice/Porcupine/raw/master/lib/raspberry-pi/cortex-a53/libpv_porcupine.so"
|
||||
"url": "https://github.com/Picovoice/porcupine/raw/v1.7/lib/raspberry-pi/cortex-a53/libpv_porcupine.so"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"base_language_model": "kaldi/base_language_model.txt",
|
||||
"base_language_model_fst": "kaldi/base_language_model.fst",
|
||||
"compatible": true,
|
||||
"custom_words": "kaldi/custom_words.txt",
|
||||
"custom_words": "kaldi_custom_words.txt",
|
||||
"dictionary": "kaldi/dictionary.txt",
|
||||
"graph": "graph",
|
||||
"language_model": "kaldi/language_model.txt",
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"base_language_model": "kaldi/base_language_model.txt",
|
||||
"base_language_model_fst": "kaldi/base_language_model.fst",
|
||||
"compatible": true,
|
||||
"custom_words": "kaldi/custom_words.txt",
|
||||
"custom_words": "kaldi_custom_words.txt",
|
||||
"dictionary": "kaldi/dictionary.txt",
|
||||
"graph": "graph",
|
||||
"language_model": "kaldi/language_model.txt",
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"base_dictionary": "kaldi/base_dictionary.txt",
|
||||
"base_language_model": "kaldi/base_language_model.txt",
|
||||
"compatible": true,
|
||||
"custom_words": "kaldi/custom_words.txt",
|
||||
"custom_words": "kaldi_custom_words.txt",
|
||||
"dictionary": "kaldi/dictionary.txt",
|
||||
"graph": "graph",
|
||||
"language_model": "kaldi/language_model.txt",
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -17,9 +17,7 @@ def main():
|
||||
step = int(rest_args[0])
|
||||
|
||||
if upper < lower:
|
||||
temp_lower = lower
|
||||
lower = upper
|
||||
upper = temp_lower
|
||||
lower, upper = upper, lower
|
||||
|
||||
for n in range(lower, upper + 1, step):
|
||||
print(n)
|
||||
|
||||
@@ -13,6 +13,11 @@ body {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#logo {
|
||||
border-color: red;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.response {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -538,3 +538,15 @@ paths:
|
||||
description: intents
|
||||
schema:
|
||||
type: object
|
||||
/api/play-recording:
|
||||
post:
|
||||
summary: 'Play the last recorded voice command from web API'
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
+4
-2
@@ -2,19 +2,21 @@ adapt-parser==0.3.4
|
||||
aiohttp==3.6.2
|
||||
doit==0.31.1
|
||||
fuzzywuzzy[speedup]==0.17.0
|
||||
google-cloud-speech==1.3.1
|
||||
google-cloud-texttospeech==0.5.0
|
||||
html5lib==1.0.1
|
||||
json5==0.8.5
|
||||
json5==0.7.0
|
||||
multidict==4.6.1
|
||||
networkx>=2.0
|
||||
num2words==0.5.10
|
||||
openfst==1.6.9
|
||||
paho-mqtt==1.5.0
|
||||
precise-runner==0.3.1
|
||||
PyAudio==0.2.11
|
||||
pydash==4.7.6
|
||||
quart==0.6.15
|
||||
quart-cors==0.1.3
|
||||
requests==2.22.0
|
||||
rhasspy-nlu==0.1.4.1
|
||||
rhasspy-nlu==0.1.6
|
||||
swagger-ui-py==0.1.7
|
||||
webrtcvad==2.0.10
|
||||
|
||||
+1
-1
@@ -618,7 +618,7 @@ async def wav2mqtt(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
|
||||
async def text2wav(core: RhasspyCore, profile: Profile, args: Any) -> None:
|
||||
"""Speak a sentence and output WAV data"""
|
||||
result = await core.speak_sentence(args)
|
||||
result = await core.speak_sentence(args.sentence)
|
||||
sys.stdout.buffer.write(result.wav_data)
|
||||
|
||||
|
||||
|
||||
+10
-4
@@ -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:
|
||||
|
||||
+49
-11
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import ssl
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
@@ -39,6 +40,7 @@ from rhasspy.events import (
|
||||
SentenceSpoken,
|
||||
SpeakSentence,
|
||||
SpeakWord,
|
||||
StopListeningForWakeWord,
|
||||
StartRecordingToBuffer,
|
||||
StopRecordingToBuffer,
|
||||
TestMicrophones,
|
||||
@@ -85,9 +87,13 @@ class RhasspyCore:
|
||||
self.defaults = Profile.load_defaults(system_profiles_dir)
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
self.ssl_context = ssl.SSLContext()
|
||||
self._session: Optional[aiohttp.ClientSession] = aiohttp.ClientSession()
|
||||
self.dialogue_manager: Optional[RhasspyActor] = None
|
||||
|
||||
self.download_status: List[str] = []
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
@@ -96,6 +102,14 @@ class RhasspyCore:
|
||||
assert self._session is not None
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def siteId(self) -> str:
|
||||
"""Get default MQTT siteId"""
|
||||
try:
|
||||
return self.profile.get("mqtt.siteId", "default").split(",")[0]
|
||||
except Exception:
|
||||
return "default"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def start(
|
||||
@@ -160,10 +174,14 @@ class RhasspyCore:
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def listen_for_wake(self) -> None:
|
||||
def listen_for_wake(self, enabled: bool = True) -> None:
|
||||
"""Tell Rhasspy to start listening for a wake word."""
|
||||
assert self.actor_system is not None
|
||||
self.actor_system.tell(self.dialogue_manager, ListenForWakeWord())
|
||||
|
||||
if enabled:
|
||||
self.actor_system.tell(self.dialogue_manager, ListenForWakeWord())
|
||||
else:
|
||||
self.actor_system.tell(self.dialogue_manager, StopListeningForWakeWord())
|
||||
|
||||
async def listen_for_command(
|
||||
self,
|
||||
@@ -344,9 +362,10 @@ class RhasspyCore:
|
||||
"""Generate speech/intent artifacts for profile."""
|
||||
if no_cache:
|
||||
# Delete doit database
|
||||
db_path = Path(self.profile.write_path(".doit.db"))
|
||||
if db_path.is_file():
|
||||
db_path.unlink()
|
||||
profile_dir = Path(self.profile.write_path())
|
||||
for db_path in profile_dir.glob(".doit.db*"):
|
||||
if db_path.is_file():
|
||||
db_path.unlink()
|
||||
|
||||
assert self.actor_system is not None
|
||||
with self.actor_system.private() as sys:
|
||||
@@ -480,6 +499,8 @@ class RhasspyCore:
|
||||
|
||||
async def download_profile(self, delete=False, chunk_size=4096) -> None:
|
||||
"""Download all necessary profile files from the internet and extract them."""
|
||||
self.download_status = []
|
||||
|
||||
output_dir = Path(self.profile.write_path())
|
||||
download_dir = Path(
|
||||
self.profile.write_path(self.profile.get("download.cache_dir", "download"))
|
||||
@@ -500,18 +521,31 @@ class RhasspyCore:
|
||||
|
||||
async def download_file(url, filename):
|
||||
try:
|
||||
self._logger.debug("Downloading %s to %s", url, filename)
|
||||
status = f"Downloading {url} to {filename}"
|
||||
self.download_status.append(status)
|
||||
self._logger.debug(status)
|
||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||
|
||||
async with self.session.get(url) as response:
|
||||
async with self.session.get(url, ssl=self.ssl_context) as response:
|
||||
with open(filename, "wb") as out_file:
|
||||
async for chunk in response.content.iter_chunked(chunk_size):
|
||||
out_file.write(chunk)
|
||||
|
||||
self._logger.debug("Downloaded %s", filename)
|
||||
status = f"Downloaded {filename}"
|
||||
self.download_status.append(status)
|
||||
self._logger.debug(status)
|
||||
except Exception:
|
||||
self._logger.exception(url)
|
||||
|
||||
# Try to delete partially downloaded file
|
||||
try:
|
||||
status = f"Failed to download {filename}"
|
||||
self.download_status.append(status)
|
||||
self._logger.debug(status)
|
||||
os.unlink(filename)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check conditions
|
||||
machine_type = platform.machine()
|
||||
download_tasks = []
|
||||
@@ -550,7 +584,7 @@ class RhasspyCore:
|
||||
src_path = dest_path
|
||||
|
||||
# Check if file is already in cache
|
||||
if os.path.exists(src_path) and (os.path.getsize(src_path) > 0):
|
||||
if not delete and os.path.exists(src_path) and (os.path.getsize(src_path) > 0):
|
||||
self._logger.debug(
|
||||
"Using cached %s for %s", src_path, dest_name
|
||||
)
|
||||
@@ -595,7 +629,9 @@ class RhasspyCore:
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
|
||||
# Copy file/directory as is
|
||||
self._logger.debug("Copying %s to %s", src_path, dest_path)
|
||||
status = f"Copying {src_path} to {dest_path}"
|
||||
self.download_status.append(status)
|
||||
self._logger.debug(status)
|
||||
if os.path.isdir(src_path):
|
||||
shutil.copytree(src_path, dest_path)
|
||||
else:
|
||||
@@ -668,7 +704,9 @@ class RhasspyCore:
|
||||
extract_path = os.path.join(temp_dir, src_extract)
|
||||
|
||||
# Copy specific file/directory
|
||||
self._logger.debug("Copying %s to %s", extract_path, dest_path)
|
||||
status = f"Copying {extract_path} to {dest_path}"
|
||||
self.download_status.append(status)
|
||||
self._logger.debug(status)
|
||||
if os.path.isdir(extract_path):
|
||||
if src_exclude:
|
||||
# Ignore some files
|
||||
|
||||
+33
-6
@@ -7,8 +7,8 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
import pydash
|
||||
import pywrapfst as fst
|
||||
import requests
|
||||
import rhasspynlu
|
||||
|
||||
from rhasspy.actor import (
|
||||
ActorExitRequest,
|
||||
@@ -386,6 +386,10 @@ class DialogueManager(RhasspyActor):
|
||||
for hook_url in awake_hooks:
|
||||
self._logger.debug("POST-ing to %s", hook_url)
|
||||
requests.post(hook_url, json=hook_json)
|
||||
|
||||
# Forward to observer
|
||||
if self.observer:
|
||||
self.send(self.observer, message)
|
||||
elif isinstance(message, WakeWordNotDetected):
|
||||
self._logger.debug("Wake word NOT detected. Staying asleep.")
|
||||
self.transition("ready")
|
||||
@@ -423,6 +427,10 @@ class DialogueManager(RhasspyActor):
|
||||
wav_data = buffer_to_wav(message.data)
|
||||
self.send(self.decoder, TranscribeWav(wav_data, handle=message.handle))
|
||||
self.transition("decoding")
|
||||
|
||||
# Forward to observer
|
||||
if self.observer:
|
||||
self.send(self.observer, message)
|
||||
else:
|
||||
self.handle_any(message, sender)
|
||||
|
||||
@@ -433,6 +441,15 @@ class DialogueManager(RhasspyActor):
|
||||
def in_decoding(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in decoding state."""
|
||||
if isinstance(message, WavTranscription):
|
||||
message.wakewordId = self.wake_detected_name or "default"
|
||||
|
||||
# Fix casing
|
||||
dict_casing = self.profile.get("speech_to_text.dictionary_casing", "")
|
||||
if dict_casing == "lower":
|
||||
message.text = message.text.lower()
|
||||
elif dict_casing == "upper":
|
||||
message.text = message.text.upper()
|
||||
|
||||
# text -> intent
|
||||
self._logger.debug("%s (confidence=%s)", message.text, message.confidence)
|
||||
|
||||
@@ -447,7 +464,8 @@ class DialogueManager(RhasspyActor):
|
||||
"text": message.text,
|
||||
"likelihood": 1,
|
||||
"seconds": 0,
|
||||
"wakeId": self.wake_detected_name or "",
|
||||
"wakeId": message.wakewordId,
|
||||
"wakewordId": message.wakewordId,
|
||||
}
|
||||
).encode()
|
||||
|
||||
@@ -460,6 +478,10 @@ class DialogueManager(RhasspyActor):
|
||||
)
|
||||
self.send(self.mqtt, MqttPublish("hermes/asr/textCaptured", payload))
|
||||
|
||||
# Forward to observer
|
||||
if self.observer:
|
||||
self.send(self.observer, message)
|
||||
|
||||
# Pass to intent recognizer
|
||||
self.send(
|
||||
self.recognizer,
|
||||
@@ -555,12 +577,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)))
|
||||
@@ -730,6 +754,9 @@ class DialogueManager(RhasspyActor):
|
||||
elif isinstance(message, GetProblems):
|
||||
# Report problems from child actors
|
||||
self.send(sender, Problems(self.problems))
|
||||
elif isinstance(message, (ListenForWakeWord, StopListeningForWakeWord)):
|
||||
# Forward to wake actor
|
||||
self.send(self.wake, message)
|
||||
else:
|
||||
self.handle_forward(message, sender)
|
||||
|
||||
|
||||
+10
-3
@@ -246,8 +246,8 @@ class IntentForwarded:
|
||||
class TrainIntent:
|
||||
"""Request to train intent recognizer."""
|
||||
|
||||
def __init__(self, intent_fst, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.intent_fst = intent_fst
|
||||
def __init__(self, intent_graph, receiver: Optional[RhasspyActor] = None) -> None:
|
||||
self.intent_graph = intent_graph
|
||||
self.receiver = receiver
|
||||
|
||||
|
||||
@@ -390,10 +390,17 @@ class TranscribeWav:
|
||||
class WavTranscription:
|
||||
"""Response to TranscribeWav."""
|
||||
|
||||
def __init__(self, text: str, handle: bool = True, confidence: float = 1) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
handle: bool = True,
|
||||
confidence: float = 1,
|
||||
wakewordId: str = "default",
|
||||
) -> None:
|
||||
self.text = text
|
||||
self.confidence = confidence
|
||||
self.handle = handle
|
||||
self.wakewordId = wakewordId
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+18
-209
@@ -1,13 +1,11 @@
|
||||
"""Support for intent recognition."""
|
||||
import concurrent.futures
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -18,7 +16,7 @@ from rhasspynlu import json_to_graph, recognize
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import IntentRecognized, RecognizeIntent, SpeakSentence
|
||||
from rhasspy.utils import empty_intent, hass_request_kwargs
|
||||
from rhasspy.utils import empty_intent, hass_request_kwargs, load_converters
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -32,7 +30,6 @@ def get_recognizer_class(system: str) -> Type[RhasspyActor]:
|
||||
"adapt",
|
||||
"rasa",
|
||||
"remote",
|
||||
"flair",
|
||||
"conversation",
|
||||
"command",
|
||||
], f"Invalid intent system: {system}"
|
||||
@@ -56,10 +53,6 @@ def get_recognizer_class(system: str) -> Type[RhasspyActor]:
|
||||
# Use remote rhasspy server
|
||||
return RemoteRecognizer
|
||||
|
||||
if system == "flair":
|
||||
# Use flair locally
|
||||
return FlairRecognizer
|
||||
|
||||
if system == "conversation":
|
||||
# Use HA conversation
|
||||
return HomeAssistantConversationRecognizer
|
||||
@@ -137,32 +130,6 @@ class RemoteRecognizer(RhasspyActor):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CliConverter:
|
||||
"""Command-line converter for intent recognition"""
|
||||
|
||||
def __init__(self, name: str, command_path: Path):
|
||||
self.name = name
|
||||
self.command_path = command_path
|
||||
|
||||
def __call__(self, *args, converter_args=None):
|
||||
"""Runs external program to convert JSON values"""
|
||||
converter_args = converter_args or []
|
||||
proc = subprocess.Popen(
|
||||
[str(self.command_path)] + converter_args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
with io.StringIO() as input_file:
|
||||
for arg in args:
|
||||
json.dump(arg, input_file)
|
||||
|
||||
stdout, _ = proc.communicate(input=input_file.getvalue())
|
||||
|
||||
return [json.loads(line) for line in stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
class FsticuffsRecognizer(RhasspyActor):
|
||||
"""Recognize intents using OpenFST."""
|
||||
|
||||
@@ -188,33 +155,7 @@ class FsticuffsRecognizer(RhasspyActor):
|
||||
self.fuzzy = self.profile.get("intent.fsticuffs.fuzzy", True)
|
||||
|
||||
# Load user-defined converters
|
||||
converters_dir = Path(
|
||||
self.profile.read_path(
|
||||
self.profile.get("intent.fsticuffs.converters_dir", "converters")
|
||||
)
|
||||
)
|
||||
if converters_dir.is_dir():
|
||||
self._logger.debug("Loading converters from %s", converters_dir)
|
||||
for converter_path in converters_dir.glob("**/*"):
|
||||
if not converter_path.is_file():
|
||||
continue
|
||||
|
||||
# Retain directory structure in name
|
||||
converter_name = str(
|
||||
converter_path.relative_to(converters_dir).with_suffix("")
|
||||
)
|
||||
|
||||
# Run converter as external program.
|
||||
# Input arguments are encoded as JSON on individual lines.
|
||||
# Output values should be encoded as JSON on individual lines.
|
||||
converter = CliConverter(converter_name, converter_path)
|
||||
|
||||
# Key off name without file extension
|
||||
self.converters[converter_name] = converter
|
||||
|
||||
self._logger.debug(
|
||||
"Loaded converter %s from %s", converter_name, converter_path
|
||||
)
|
||||
self.converters = load_converters(self.profile)
|
||||
|
||||
self.transition("loaded")
|
||||
|
||||
@@ -347,8 +288,8 @@ class FuzzyWuzzyRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_loaded")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["raw_text"] = message.text
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
@@ -448,6 +389,7 @@ class RasaIntentRecognizer(RhasspyActor):
|
||||
RhasspyActor.__init__(self)
|
||||
self.project_name = ""
|
||||
self.parse_url = ""
|
||||
self.min_confidence: float = 0
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
"""Transition to started state."""
|
||||
@@ -456,6 +398,7 @@ class RasaIntentRecognizer(RhasspyActor):
|
||||
self.project_name = rasa_config.get(
|
||||
"project_name", f"rhasspy_{self.profile.name}"
|
||||
)
|
||||
self.min_confidence = rasa_config.get("min_confidence", 0)
|
||||
self.parse_url = urljoin(url, "model/parse")
|
||||
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
@@ -463,13 +406,23 @@ class RasaIntentRecognizer(RhasspyActor):
|
||||
if isinstance(message, RecognizeIntent):
|
||||
try:
|
||||
intent = self.recognize(message.text)
|
||||
intent["intent"]["name"] = intent["intent"]["name"] or ""
|
||||
logging.debug(repr(intent))
|
||||
confidence = intent["intent"]["confidence"]
|
||||
if confidence < self.min_confidence:
|
||||
intent["intent"]["name"] = ""
|
||||
|
||||
self._logger.warning(
|
||||
"Intent did not meet confidence threshold: %s < %s",
|
||||
confidence,
|
||||
self.min_confidence,
|
||||
)
|
||||
except Exception:
|
||||
self._logger.exception("in_started")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["raw_text"] = message.text
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
IntentRecognized(intent, handle=message.handle),
|
||||
@@ -530,8 +483,8 @@ class AdaptIntentRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_loaded")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["raw_text"] = message.text
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
@@ -612,150 +565,6 @@ class AdaptIntentRecognizer(RhasspyActor):
|
||||
self._logger.debug("Loaded engine from config file %s", config_path)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Flair Intent Recognizer
|
||||
# https://github.com/zalandoresearch/flair
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FlairRecognizer(RhasspyActor):
|
||||
"""Flair based recognizer"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
RhasspyActor.__init__(self)
|
||||
|
||||
try:
|
||||
# pylint: disable=E0401
|
||||
from flair.models import TextClassifier, SequenceTagger
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.class_model: Optional[TextClassifier] = None
|
||||
self.ner_models: Optional[Dict[str, SequenceTagger]] = None
|
||||
self.intent_map: Optional[Dict[str, str]] = None
|
||||
self.preload = False
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
"""Transition to started state."""
|
||||
self.preload = self.config.get("preload", False)
|
||||
if self.preload:
|
||||
try:
|
||||
# Pre-load models
|
||||
self.load_models()
|
||||
except Exception as e:
|
||||
self._logger.warning("preload: %s", e)
|
||||
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in started state."""
|
||||
if isinstance(message, RecognizeIntent):
|
||||
try:
|
||||
self.load_models()
|
||||
intent = self.recognize(message.text)
|
||||
except Exception:
|
||||
self._logger.exception("in_started")
|
||||
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, handle=message.handle),
|
||||
)
|
||||
|
||||
def recognize(self, text: str) -> Dict[str, Any]:
|
||||
"""Run intent classifier and then named-entity recognizer."""
|
||||
# pylint: disable=E0401
|
||||
from flair.data import Sentence
|
||||
|
||||
intent = empty_intent()
|
||||
sentence = Sentence(text)
|
||||
|
||||
assert self.intent_map is not None
|
||||
if self.class_model is not None:
|
||||
self.class_model.predict(sentence)
|
||||
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))
|
||||
intent["intent"]["confidence"] = 1
|
||||
|
||||
intent["intent"]["name"] = self.intent_map[intent_id]
|
||||
|
||||
assert self.ner_models is not None
|
||||
if intent_id in self.ner_models:
|
||||
# Predict entities
|
||||
self.ner_models[intent_id].predict(sentence)
|
||||
ner_dict = sentence.to_dict(tag_type="ner")
|
||||
for named_entity in ner_dict["entities"]:
|
||||
intent["entities"].append(
|
||||
{
|
||||
"entity": named_entity["type"],
|
||||
"value": named_entity["text"],
|
||||
"start": named_entity["start_pos"],
|
||||
"end": named_entity["end_pos"],
|
||||
"confidence": named_entity["confidence"],
|
||||
}
|
||||
)
|
||||
|
||||
return intent
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def load_models(self) -> None:
|
||||
"""Load intent classifier and named entity recognizers."""
|
||||
# pylint: disable=E0401
|
||||
from flair.models import TextClassifier, SequenceTagger
|
||||
|
||||
# Load mapping from intent id to user intent name
|
||||
if self.intent_map is None:
|
||||
intent_map_path = self.profile.read_path(
|
||||
self.profile.get("training.intent.intent_map", "intent_map.json")
|
||||
)
|
||||
|
||||
with open(intent_map_path, "r") as intent_map_file:
|
||||
self.intent_map = json.load(intent_map_file)
|
||||
|
||||
data_dir = self.profile.read_path(
|
||||
self.profile.get("intent.flair.data_dir", "flair_data")
|
||||
)
|
||||
|
||||
# Only load intent classifier if there is more than one intent
|
||||
if (self.class_model is None) and (len(self.intent_map) > 1):
|
||||
class_model_path = os.path.join(
|
||||
data_dir, "classification", "final-model.pt"
|
||||
)
|
||||
self._logger.debug("Loading classification model from %s", class_model_path)
|
||||
self.class_model = TextClassifier.load_from_file(class_model_path)
|
||||
self._logger.debug("Loaded classification model")
|
||||
|
||||
if self.ner_models is None:
|
||||
ner_models = {}
|
||||
ner_data_dir = os.path.join(data_dir, "ner")
|
||||
for file_name in os.listdir(ner_data_dir):
|
||||
ner_model_dir = os.path.join(ner_data_dir, file_name)
|
||||
if os.path.isdir(ner_model_dir):
|
||||
# Assume directory is intent name
|
||||
intent_name = file_name
|
||||
if intent_name not in self.intent_map:
|
||||
self._logger.warning(
|
||||
"%s was not found in intent map", intent_name
|
||||
)
|
||||
|
||||
ner_model_path = os.path.join(ner_model_dir, "final-model.pt")
|
||||
self._logger.debug("Loading NER model from %s", ner_model_path)
|
||||
ner_models[intent_name] = SequenceTagger.load_from_file(
|
||||
ner_model_path
|
||||
)
|
||||
|
||||
self._logger.debug("Loaded NER model(s)")
|
||||
self.ner_models = ner_models
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Home Assistant Conversation
|
||||
# https://www.home-assistant.io/integrations/conversation
|
||||
@@ -861,8 +670,8 @@ class CommandRecognizer(RhasspyActor):
|
||||
self._logger.exception("in_started")
|
||||
intent = empty_intent()
|
||||
intent["text"] = message.text
|
||||
intent["raw_text"] = message.text
|
||||
|
||||
intent["raw_text"] = message.text
|
||||
intent["speech_confidence"] = message.confidence
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
|
||||
@@ -77,10 +77,12 @@ class HomeAssistantIntentHandler(RhasspyActor):
|
||||
self.event_type_format = ""
|
||||
self.pem_file = ""
|
||||
self.handle_type: HomeAssistantHandleType = HomeAssistantHandleType.EVENT
|
||||
self.speech_actor: Optional[RhasspyActor] = None
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
"""Transition to started state."""
|
||||
self.hass_config = self.profile.get("home_assistant", {})
|
||||
self.speech_actor = self.config.get("speech")
|
||||
|
||||
# Python format string for generating event type name
|
||||
self.event_type_format = self.hass_config.get(
|
||||
@@ -170,6 +172,15 @@ class HomeAssistantIntentHandler(RhasspyActor):
|
||||
kwargs["verify"] = self.pem_file
|
||||
|
||||
response = requests.post(post_url, **kwargs)
|
||||
response.raise_for_status()
|
||||
|
||||
intent = response.json()
|
||||
self._logger.debug(intent)
|
||||
|
||||
# Check for speech
|
||||
speech_text = intent.get("speech", {}).get("plain", {}).get("speech", "")
|
||||
if speech_text and self.speech_actor:
|
||||
self.send(self.speech_actor, SpeakSentence(speech_text))
|
||||
else:
|
||||
# Send event
|
||||
post_url = urljoin(self.hass_config["url"], "api/events/" + event_type)
|
||||
@@ -184,7 +195,7 @@ class HomeAssistantIntentHandler(RhasspyActor):
|
||||
response = requests.post(post_url, **kwargs)
|
||||
self._logger.debug("POSTed intent to %s", post_url)
|
||||
|
||||
response.raise_for_status()
|
||||
response.raise_for_status()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
+112
-329
@@ -1,22 +1,16 @@
|
||||
"""Training for intent recognizers."""
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from io import StringIO
|
||||
from typing import Any, Dict, List, Set, Type
|
||||
from typing import Any, Callable, Dict, List, Set, Type
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (IntentTrainingComplete, IntentTrainingFailed,
|
||||
TrainIntent)
|
||||
from rhasspy.utils import (lcm, make_sentences_by_intent,
|
||||
sample_sentences_by_intent)
|
||||
from rhasspy.events import IntentTrainingComplete, IntentTrainingFailed, TrainIntent
|
||||
from rhasspy.utils import make_sentences_by_intent, load_converters
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -32,7 +26,6 @@ def get_intent_trainer_class(
|
||||
"fuzzywuzzy",
|
||||
"adapt",
|
||||
"rasa",
|
||||
"flair",
|
||||
"auto",
|
||||
"command",
|
||||
], f"Invalid intent training system: {trainer_system}"
|
||||
@@ -48,9 +41,6 @@ def get_intent_trainer_class(
|
||||
if recognizer_system == "adapt":
|
||||
# Use Mycroft Adapt locally
|
||||
return AdaptIntentTrainer
|
||||
if recognizer_system == "flair":
|
||||
# Use flair locally
|
||||
return FlairIntentTrainer
|
||||
if recognizer_system == "rasa":
|
||||
# Use Rasa NLU remotely
|
||||
return RasaIntentTrainer
|
||||
@@ -69,9 +59,6 @@ def get_intent_trainer_class(
|
||||
if trainer_system == "rasa":
|
||||
# Use Rasa NLU remotely
|
||||
return RasaIntentTrainer
|
||||
if trainer_system == "flair":
|
||||
# Use flair RNN locally
|
||||
return FlairIntentTrainer
|
||||
if trainer_system == "command":
|
||||
# Use command-line intent trainer
|
||||
return CommandIntentTrainer
|
||||
@@ -98,7 +85,7 @@ class DummyIntentTrainer(RhasspyActor):
|
||||
|
||||
|
||||
class FsticuffsIntentTrainer(DummyIntentTrainer):
|
||||
"""No training needed. Intent FST will be used directly during recognition."""
|
||||
"""No training needed. Intent graph will be used directly during recognition."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -112,23 +99,33 @@ 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 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):
|
||||
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)
|
||||
sentences_by_intent = make_sentences_by_intent(
|
||||
intent_graph, extra_converters=self.converters
|
||||
)
|
||||
with open(examples_path, "w") as examples_file:
|
||||
json.dump(sentences_by_intent, examples_file, indent=4)
|
||||
|
||||
@@ -144,11 +141,19 @@ 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 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):
|
||||
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")
|
||||
@@ -156,9 +161,8 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def train(self, intent_fst) -> None:
|
||||
def train(self, intent_graph) -> None:
|
||||
"""Convert examples to Markdown and POST to RasaNLU server."""
|
||||
from rhasspy.train.jsgf2fst import fstprintall
|
||||
import requests
|
||||
|
||||
# Load settings
|
||||
@@ -174,39 +178,59 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
)
|
||||
|
||||
# Build Markdown sentences
|
||||
sentences_by_intent: Dict[str, Any] = defaultdict(list)
|
||||
for symbols in fstprintall(intent_fst, exclude_meta=False):
|
||||
intent_name = ""
|
||||
strings = []
|
||||
for sym in symbols:
|
||||
if sym.startswith("<"):
|
||||
continue # <eps>
|
||||
sentences_by_intent = make_sentences_by_intent(
|
||||
intent_graph, extra_converters=self.converters
|
||||
)
|
||||
|
||||
if sym.startswith("__label__"):
|
||||
intent_name = sym[9:]
|
||||
elif sym.startswith("__begin__"):
|
||||
strings.append("[")
|
||||
elif sym.startswith("__end__"):
|
||||
strings[-1] = strings[-1].strip()
|
||||
tag = sym[7:]
|
||||
strings.append(f"]({tag})")
|
||||
strings.append(" ")
|
||||
else:
|
||||
strings.append(sym)
|
||||
strings.append(" ")
|
||||
|
||||
sentence = "".join(strings).strip()
|
||||
sentences_by_intent[intent_name].append(sentence)
|
||||
|
||||
# Write to YAML file
|
||||
# Write to YAML/Markdown file
|
||||
with open(examples_md_path, "w") as examples_md_file:
|
||||
for intent_name, intent_sents in sentences_by_intent.items():
|
||||
# Rasa Markdown training format
|
||||
print(f"## intent:{intent_name}", file=examples_md_file)
|
||||
for intent_sent in intent_sents:
|
||||
print("-", intent_sent, file=examples_md_file)
|
||||
raw_index = 0
|
||||
index_entity = {e["raw_start"]: e for e in intent_sent["entities"]}
|
||||
entity = None
|
||||
sentence_tokens = []
|
||||
entity_tokens = []
|
||||
for raw_token in intent_sent["raw_tokens"]:
|
||||
token = raw_token
|
||||
if entity and (raw_index >= entity["raw_end"]):
|
||||
# Finish current entity
|
||||
last_token = entity_tokens[-1]
|
||||
entity_tokens[-1] = f"{last_token}]({entity['entity']})"
|
||||
sentence_tokens.extend(entity_tokens)
|
||||
entity = None
|
||||
entity_tokens = []
|
||||
|
||||
print("", file=examples_md_file)
|
||||
new_entity = index_entity.get(raw_index)
|
||||
if new_entity:
|
||||
# Begin new entity
|
||||
assert entity is None, "Unclosed entity"
|
||||
entity = new_entity
|
||||
entity_tokens = []
|
||||
token = f"[{token}"
|
||||
|
||||
if entity:
|
||||
# Add to current entity
|
||||
entity_tokens.append(token)
|
||||
else:
|
||||
# Add directly to sentence
|
||||
sentence_tokens.append(token)
|
||||
|
||||
raw_index += len(raw_token) + 1
|
||||
|
||||
if entity:
|
||||
# Finish final entity
|
||||
last_token = entity_tokens[-1]
|
||||
entity_tokens[-1] = f"{last_token}]({entity['entity']})"
|
||||
sentence_tokens.extend(entity_tokens)
|
||||
|
||||
# Print single example
|
||||
print("-", " ".join(sentence_tokens), file=examples_md_file)
|
||||
|
||||
# Newline between intents
|
||||
print("", file=examples_md_file)
|
||||
|
||||
# Create training YAML file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
@@ -254,9 +278,19 @@ class RasaIntentTrainer(RhasspyActor):
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
|
||||
model_dir = rasa_config.get("model_dir", "")
|
||||
model_file = os.path.join(model_dir, response.headers["filename"])
|
||||
self._logger.debug("Received model %s", model_file)
|
||||
|
||||
# Replace model
|
||||
model_url = urljoin(url, "model")
|
||||
requests.put(model_url, json={"model_file": model_file})
|
||||
except Exception:
|
||||
# Rasa gives quite helpful error messages, so extract them from the response.
|
||||
raise Exception(f'{response.reason}: {json.loads(response.content)["message"]}')
|
||||
raise Exception(
|
||||
f'{response.reason}: {json.loads(response.content)["message"]}'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -268,11 +302,19 @@ 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):
|
||||
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")
|
||||
@@ -280,19 +322,19 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def train(self, intent_fst) -> None:
|
||||
def train(self, intent_graph) -> None:
|
||||
"""Create intents, entities, and keywords."""
|
||||
# Load "stop" words (common words that are excluded from training)
|
||||
stop_words: Set[str] = set()
|
||||
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 line.strip()
|
||||
}
|
||||
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)
|
||||
sentences_by_intent = make_sentences_by_intent(
|
||||
intent_graph, extra_converters=self.converters
|
||||
)
|
||||
|
||||
# Generate intent configuration
|
||||
entities: Dict[str, Set[str]] = {}
|
||||
@@ -311,17 +353,12 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
|
||||
# Process sentences for this intent
|
||||
for intent_sent in intent_sents:
|
||||
_, slots, word_tokens = (
|
||||
intent_sent.get("raw_text", intent_sent["text"]),
|
||||
intent_sent["entities"],
|
||||
intent_sent["tokens"],
|
||||
)
|
||||
entity_tokens: Set[str] = set()
|
||||
|
||||
# Group slot values by entity
|
||||
slot_entities: Dict[str, List[str]] = defaultdict(list)
|
||||
for sent_ent in slots:
|
||||
slot_entities[sent_ent["entity"]].append(sent_ent["value"])
|
||||
for sent_ent in intent_sent["entities"]:
|
||||
slot_entities[sent_ent["entity"]].append(sent_ent["raw_value"])
|
||||
|
||||
# Add entities
|
||||
for entity_name, entity_values in slot_entities.items():
|
||||
@@ -335,10 +372,10 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
|
||||
# Split entity values by whitespace
|
||||
for value in entity_values:
|
||||
entity_tokens.update(re.split(r"\s", value))
|
||||
entity_tokens.update(value.split())
|
||||
|
||||
# Get all non-stop words that are not part of entity values
|
||||
words = set(word_tokens) - entity_tokens - stop_words
|
||||
words = set(intent_sent["raw_tokens"]) - entity_tokens - stop_words
|
||||
|
||||
# Increment count for words
|
||||
for word in words:
|
||||
@@ -398,268 +435,6 @@ class AdaptIntentTrainer(RhasspyActor):
|
||||
self._logger.debug("Wrote adapt configuration to %s", config_path)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Flair Intent Trainer
|
||||
# https://github.com/zalandoresearch/flair
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FlairIntentTrainer(RhasspyActor):
|
||||
"""Trains a classification and NER model using flair"""
|
||||
|
||||
def __init__(self):
|
||||
RhasspyActor.__init__(self)
|
||||
self.embeddings = []
|
||||
|
||||
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.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:
|
||||
"""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
|
||||
|
||||
# Directory to look for downloaded embeddings
|
||||
cache_dir = self.profile.read_path(
|
||||
self.profile.get("intent.flair.cache_dir", "flair/cache")
|
||||
)
|
||||
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
# Directory to store generated models
|
||||
data_dir = self.profile.write_path(
|
||||
self.profile.get("intent.flair.data_dir", "flair/data")
|
||||
)
|
||||
|
||||
if os.path.exists(data_dir):
|
||||
shutil.rmtree(data_dir)
|
||||
|
||||
self.embeddings = self.profile.get("intent.flair.embeddings", [])
|
||||
assert self.embeddings, "No word embeddings"
|
||||
|
||||
# Create directories to write training data to
|
||||
class_data_dir = os.path.join(data_dir, "classification")
|
||||
ner_data_dir = os.path.join(data_dir, "ner")
|
||||
os.makedirs(class_data_dir, exist_ok=True)
|
||||
os.makedirs(ner_data_dir, exist_ok=True)
|
||||
|
||||
# Convert FST to training data
|
||||
# ----------------------------
|
||||
|
||||
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
|
||||
sentences_by_intent: Dict[str, Any] = {}
|
||||
|
||||
# Get sentences for training
|
||||
do_sampling = self.profile.get("intent.flair.do_sampling", True)
|
||||
start_time = time.time()
|
||||
|
||||
if do_sampling:
|
||||
# Sample from each intent FST
|
||||
num_samples = int(self.profile.get("intent.flair.num_samples", 10000))
|
||||
intent_map_path = self.profile.read_path(
|
||||
self.profile.get("training.intent.intent_map", "intent_map.json")
|
||||
)
|
||||
|
||||
with open(intent_map_path, "r") as intent_map_file:
|
||||
intent_map = json.load(intent_map_file)
|
||||
|
||||
# Gather FSTs for all known intents
|
||||
fsts_dir = self.profile.write_dir(
|
||||
self.profile.get("speech_to_text.fsts_dir")
|
||||
)
|
||||
|
||||
intent_fst_paths = {
|
||||
intent_id: os.path.join(fsts_dir, f"{intent_id}.fst")
|
||||
for intent_id in intent_map
|
||||
}
|
||||
|
||||
# Generate samples
|
||||
self._logger.debug(
|
||||
"Generating %s sample(s) from %s intent(s)",
|
||||
num_samples,
|
||||
len(intent_fst_paths),
|
||||
)
|
||||
|
||||
sentences_by_intent = sample_sentences_by_intent(
|
||||
intent_fst_paths, num_samples
|
||||
)
|
||||
else:
|
||||
# Exhaustively generate all sentences
|
||||
self._logger.debug(
|
||||
"Generating all possible sentences (may take a long time)"
|
||||
)
|
||||
sentences_by_intent = make_sentences_by_intent(intent_fst)
|
||||
|
||||
sentence_time = time.time() - start_time
|
||||
self._logger.debug("Generated sentences in %s second(s)", sentence_time)
|
||||
|
||||
# Get least common multiple in order to balance sentences by intent
|
||||
lcm_sentences = lcm(*(len(sents) for sents in sentences_by_intent.values()))
|
||||
|
||||
# Generate examples
|
||||
class_sentences = []
|
||||
ner_sentences: Dict[str, List[Sentence]] = defaultdict(list)
|
||||
for intent_name, intent_sents in sentences_by_intent.items():
|
||||
num_repeats = max(1, lcm_sentences // len(intent_sents))
|
||||
for intent_sent in intent_sents:
|
||||
# Only train an intent classifier if there's more than one intent
|
||||
if len(sentences_by_intent) > 1:
|
||||
# Add balanced copies
|
||||
for _ in range(num_repeats):
|
||||
class_sent = Sentence(labels=[intent_name])
|
||||
for word in intent_sent["tokens"]:
|
||||
class_sent.add_token(Token(word))
|
||||
|
||||
class_sentences.append(class_sent)
|
||||
|
||||
if not intent_sent["entities"]:
|
||||
continue # no entities, no sequence tagger
|
||||
|
||||
# Named entity recognition (NER) example
|
||||
token_idx = 0
|
||||
entity_start = {ev["start"]: ev for ev in intent_sent["entities"]}
|
||||
entity_end = {ev["end"]: ev for ev in intent_sent["entities"]}
|
||||
entity = None
|
||||
|
||||
word_tags = []
|
||||
for word in intent_sent["tokens"]:
|
||||
# Determine tag label
|
||||
tag = "O" if not entity else f"I-{entity}"
|
||||
if token_idx in entity_start:
|
||||
entity = entity_start[token_idx]["entity"]
|
||||
tag = f"B-{entity}"
|
||||
|
||||
word_tags.append((word, tag))
|
||||
|
||||
# word ner
|
||||
token_idx += len(word) + 1
|
||||
|
||||
if (token_idx - 1) in entity_end:
|
||||
entity = None
|
||||
|
||||
# Add balanced copies
|
||||
for _ in range(num_repeats):
|
||||
ner_sent = Sentence()
|
||||
for word, tag in word_tags:
|
||||
token = Token(word)
|
||||
token.add_tag("ner", tag)
|
||||
ner_sent.add_token(token)
|
||||
|
||||
ner_sentences[intent_name].append(ner_sent)
|
||||
|
||||
# Start training
|
||||
max_epochs = int(self.profile.get("intent.flair.max_epochs", 100))
|
||||
|
||||
# Load word embeddings
|
||||
self._logger.debug("Loading word embeddings from %s", cache_dir)
|
||||
word_embeddings = [
|
||||
FlairEmbeddings(os.path.join(cache_dir, "embeddings", e))
|
||||
for e in self.embeddings
|
||||
]
|
||||
|
||||
if class_sentences:
|
||||
self._logger.debug("Training intent classifier")
|
||||
|
||||
# Random 80/10/10 split
|
||||
class_train, class_dev, class_test = self._split_data(class_sentences)
|
||||
class_corpus = TaggedCorpus(class_train, class_dev, class_test)
|
||||
|
||||
# Intent classification
|
||||
doc_embeddings = DocumentRNNEmbeddings(
|
||||
word_embeddings,
|
||||
hidden_size=512,
|
||||
reproject_words=True,
|
||||
reproject_words_dimension=256,
|
||||
)
|
||||
|
||||
classifier = TextClassifier(
|
||||
doc_embeddings,
|
||||
label_dictionary=class_corpus.make_label_dictionary(),
|
||||
multi_label=False,
|
||||
)
|
||||
|
||||
self._logger.debug(
|
||||
"Intent classifier has %s example(s)", len(class_sentences)
|
||||
)
|
||||
trainer = ModelTrainer(classifier, class_corpus)
|
||||
trainer.train(class_data_dir, max_epochs=max_epochs)
|
||||
else:
|
||||
self._logger.info("Skipping intent classifier training")
|
||||
|
||||
if ner_sentences:
|
||||
self._logger.debug("Training %s NER sequence tagger(s)", len(ner_sentences))
|
||||
|
||||
# Named entity recognition
|
||||
stacked_embeddings = StackedEmbeddings(word_embeddings)
|
||||
|
||||
for intent_name, intent_ner_sents in ner_sentences.items():
|
||||
ner_train, ner_dev, ner_test = self._split_data(intent_ner_sents)
|
||||
ner_corpus = TaggedCorpus(ner_train, ner_dev, ner_test)
|
||||
|
||||
tagger = SequenceTagger(
|
||||
hidden_size=256,
|
||||
embeddings=stacked_embeddings,
|
||||
tag_dictionary=ner_corpus.make_tag_dictionary(tag_type="ner"),
|
||||
tag_type="ner",
|
||||
use_crf=True,
|
||||
)
|
||||
|
||||
ner_intent_dir = os.path.join(ner_data_dir, intent_name)
|
||||
os.makedirs(ner_intent_dir, exist_ok=True)
|
||||
|
||||
self._logger.debug(
|
||||
"NER tagger for %s has %s example(s)",
|
||||
intent_name,
|
||||
len(intent_ner_sents),
|
||||
)
|
||||
trainer = ModelTrainer(tagger, ner_corpus)
|
||||
trainer.train(ner_intent_dir, max_epochs=max_epochs)
|
||||
else:
|
||||
self._logger.info("Skipping NER sequence tagger training")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _split_data(self, data, split=0.1):
|
||||
"""Randomly splits a data set into train, dev, and test sets"""
|
||||
|
||||
random.shuffle(data)
|
||||
split_index = int(len(data) * split)
|
||||
|
||||
# 1 - (2*split)
|
||||
train = data[(split_index * 2) :]
|
||||
|
||||
# split
|
||||
dev = data[:split_index]
|
||||
|
||||
# split
|
||||
test = data[split_index : (split_index * 2)]
|
||||
|
||||
return train, dev, test
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Command-line Based Intent Trainer
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -671,6 +446,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."""
|
||||
@@ -682,6 +458,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:
|
||||
@@ -700,10 +479,14 @@ class CommandIntentTrainer(RhasspyActor):
|
||||
self._logger.debug(self.command)
|
||||
|
||||
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
|
||||
sentences_by_intent: Dict[str, Any] = make_sentences_by_intent(intent_fst)
|
||||
sentences_by_intent = make_sentences_by_intent(intent_fst)
|
||||
json_sentences = {
|
||||
intent: [r.asdict() for r in sentences_by_intent[intent]]
|
||||
for intent in sentences_by_intent
|
||||
}
|
||||
|
||||
# JSON -> STDIN
|
||||
json_input = json.dumps({sentences_by_intent}).encode()
|
||||
json_input = json.dumps(json_sentences).encode()
|
||||
|
||||
subprocess.run(self.command, input=json_input, check=True)
|
||||
except Exception:
|
||||
|
||||
+33
-4
@@ -10,8 +10,13 @@ from typing import Any, Dict, List
|
||||
import pydash
|
||||
|
||||
from rhasspy.actor import RhasspyActor
|
||||
from rhasspy.events import (MqttConnected, MqttDisconnected, MqttMessage,
|
||||
MqttPublish, MqttSubscribe)
|
||||
from rhasspy.events import (
|
||||
MqttConnected,
|
||||
MqttDisconnected,
|
||||
MqttMessage,
|
||||
MqttPublish,
|
||||
MqttSubscribe,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Events
|
||||
@@ -48,6 +53,7 @@ class HermesMqtt(RhasspyActor):
|
||||
self.password = None
|
||||
self.reconnect_sec = 5
|
||||
self.publish_intents = True
|
||||
self.tls = {"enabled": False}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -66,6 +72,7 @@ class HermesMqtt(RhasspyActor):
|
||||
self.password = self.profile.get("mqtt.password", None)
|
||||
self.reconnect_sec = self.profile.get("mqtt.reconnect_sec", 5)
|
||||
self.publish_intents = self.profile.get("mqtt.publish_intents", True)
|
||||
self.tls = self.profile.get("mqtt.tls", {"enabled": False})
|
||||
|
||||
if self.profile.get("mqtt.enabled", False):
|
||||
self.transition("connecting")
|
||||
@@ -84,6 +91,28 @@ class HermesMqtt(RhasspyActor):
|
||||
self.client.on_message = self.on_message
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
|
||||
if pydash.get(self.tls, "enabled", False):
|
||||
import ssl
|
||||
|
||||
allowed_cert_reqs = {
|
||||
"CERT_REQUIRED": ssl.CERT_REQUIRED,
|
||||
"CERT_OPTIONAL": ssl.CERT_OPTIONAL,
|
||||
"CERT_NONE": ssl.CERT_NONE,
|
||||
}
|
||||
|
||||
self.client.tls_set(
|
||||
ca_certs=pydash.get(self.tls, "ca_certs", None),
|
||||
cert_reqs=pydash.get(
|
||||
allowed_cert_reqs,
|
||||
pydash.get(self.tls, "cert_reqs", "CERT_REQUIRED"),
|
||||
ssl.CERT_REQUIRED,
|
||||
),
|
||||
certfile=pydash.get(self.tls, "certfile", None),
|
||||
ciphers=pydash.get(self.tls, "ciphers", None),
|
||||
keyfile=pydash.get(self.tls, "keyfile", None),
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
)
|
||||
|
||||
if self.username:
|
||||
self._logger.debug("Logging in as %s", self.username)
|
||||
self.client.username_pw_set(self.username, self.password)
|
||||
@@ -259,12 +288,12 @@ class HermesMqtt(RhasspyActor):
|
||||
"slotName": ev["entity"],
|
||||
"confidence": 1,
|
||||
"value": {"kind": ev["entity"], "value": ev["value"]},
|
||||
"rawValue": ev["value"],
|
||||
"rawValue": ev.get("raw_value", ev["value"]),
|
||||
}
|
||||
for ev in intent.get("entities", [])
|
||||
],
|
||||
"asrTokens": [],
|
||||
"asrConfidence": 1
|
||||
"asrConfidence": 1,
|
||||
}
|
||||
).encode()
|
||||
|
||||
|
||||
@@ -417,7 +417,18 @@
|
||||
"reconnect_sec": { "type": "integer", "min": 0 },
|
||||
"site_id": { "type": "string" },
|
||||
"username": { "type": "string" },
|
||||
"publish_intents": { "type": "boolean" }
|
||||
"publish_intents": { "type": "boolean" },
|
||||
"tls": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"ca_certs": { "type": "string" },
|
||||
"cert_reqs": { "type": "string" },
|
||||
"certfile": { "type": "string" },
|
||||
"ciphers": { "type": "string" },
|
||||
"keyfile": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+91
-1
@@ -25,6 +25,7 @@ def get_decoder_class(system: str) -> Type[RhasspyActor]:
|
||||
"pocketsphinx",
|
||||
"kaldi",
|
||||
"remote",
|
||||
"google",
|
||||
"hass_stt",
|
||||
"command",
|
||||
], f"Invalid speech to text system: {system}"
|
||||
@@ -38,6 +39,9 @@ def get_decoder_class(system: str) -> Type[RhasspyActor]:
|
||||
if system == "remote":
|
||||
# Use remote Rhasspy server
|
||||
return RemoteDecoder
|
||||
if system == "google":
|
||||
# Use remote Google Cloud
|
||||
return GoogleCloudDecoder
|
||||
if system == "hass_stt":
|
||||
# Use Home Assistant STT platform
|
||||
return HomeAssistantSTTIntegration
|
||||
@@ -320,6 +324,90 @@ class RemoteDecoder(RhasspyActor):
|
||||
return response.text
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Google Cloud Speech-to-text decoder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class GoogleCloudDecoder(RhasspyActor):
|
||||
"""Forwards speech to text request to Google Cloud STT service"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
RhasspyActor.__init__(self)
|
||||
self.client = None
|
||||
self.language_code = None
|
||||
self.min_confidence: float = 0
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
"""Transition to started state."""
|
||||
from google.cloud import speech
|
||||
|
||||
credentials_file = self.profile.get("speech_to_text.google.credentials")
|
||||
self.min_confidence = self.profile.get("speech_to_text.google.min_confidence")
|
||||
self.language_code = self.profile.get("locale").replace("_", "-")
|
||||
from google.auth import environment_vars
|
||||
|
||||
os.environ[environment_vars.CREDENTIALS] = credentials_file
|
||||
self.client = speech.SpeechClient()
|
||||
|
||||
def in_started(self, message: Any, sender: RhasspyActor) -> None:
|
||||
"""Handle messages in started state."""
|
||||
if isinstance(message, TranscribeWav):
|
||||
try:
|
||||
text, confidence = self.transcribe_wav(message.wav_data)
|
||||
self._logger.debug(text)
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
WavTranscription(
|
||||
text, confidence=confidence, handle=message.handle
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
self._logger.exception("transcribing wav")
|
||||
|
||||
# Send empty transcription back
|
||||
self.send(
|
||||
message.receiver or sender,
|
||||
WavTranscription("", confidence=0, handle=message.handle),
|
||||
)
|
||||
|
||||
def transcribe_wav(self, wav_data: bytes) -> Tuple[str, float]:
|
||||
"""POST to remote server and return response."""
|
||||
from google.cloud.speech import enums
|
||||
from google.cloud.speech import types
|
||||
|
||||
self._logger.debug(
|
||||
"POSTing %d byte(s) of WAV data to Google Cloud STT", len(wav_data)
|
||||
)
|
||||
|
||||
audio = types.RecognitionAudio(content=wav_data)
|
||||
config = types.RecognitionConfig(
|
||||
encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16,
|
||||
sample_rate_hertz=16000,
|
||||
model="command_and_search",
|
||||
language_code=self.language_code,
|
||||
)
|
||||
|
||||
response = self.client.recognize(config, audio)
|
||||
if len(response.results) == 0:
|
||||
self._logger.debug("No results returned.")
|
||||
return "", 0
|
||||
|
||||
result = response.results[0].alternatives[0]
|
||||
|
||||
self._logger.debug("Transcription confidence: %s", result.confidence)
|
||||
if result.confidence >= self.min_confidence:
|
||||
return result.transcript, result.confidence
|
||||
|
||||
self._logger.warning(
|
||||
"Transcription did not meet confidence threshold: %s < %s",
|
||||
result.confidence,
|
||||
self.min_confidence,
|
||||
)
|
||||
|
||||
return "", 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Kaldi Decoder
|
||||
# http://kaldi-asr.org
|
||||
@@ -565,7 +653,9 @@ class HomeAssistantSTTIntegration(RhasspyActor):
|
||||
audio_data = audio_data[self.chunk_size :]
|
||||
|
||||
# POST WAV data to STT
|
||||
response = requests.post(stt_url, data=generate_chunks(), **kwargs) # type: ignore
|
||||
response = requests.post(
|
||||
stt_url, data=generate_chunks(), **kwargs
|
||||
) # type: ignore
|
||||
response.raise_for_status()
|
||||
|
||||
response_json = response.json()
|
||||
|
||||
+32
-22
@@ -165,9 +165,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
# Check for arguments.
|
||||
# Slot name retains argument(s).
|
||||
if "," in slot_name:
|
||||
parts = slot_name.split(",")
|
||||
slot_name = parts[0]
|
||||
slot_args = parts[1:]
|
||||
slot_name, *slot_args = slot_name.split(",")
|
||||
else:
|
||||
slot_args = None
|
||||
|
||||
@@ -228,7 +226,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
upper_bound = int(match.group(2))
|
||||
step = 1
|
||||
|
||||
if len(match.groups()) > 2:
|
||||
if len(match.groups()) > 3:
|
||||
# Exclude ,
|
||||
step = int(match.group(3)[1:])
|
||||
|
||||
@@ -259,7 +257,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
n = int(match.group(1))
|
||||
|
||||
# 75 -> (seventy five):75!int
|
||||
number_text = num2words(n, lang=language).replace("-", " ").strip()
|
||||
number_text = re.sub(r"[-,]\s*", " ", num2words(n, lang=language)).strip()
|
||||
assert number_text, f"Empty num2words result for {n}"
|
||||
number_words = number_text.split()
|
||||
|
||||
@@ -270,7 +268,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
word.converters = ["int"]
|
||||
return word
|
||||
|
||||
# Hard case, split into mutliple Words
|
||||
# Hard case, split into multiple Words
|
||||
return jsgf.Sequence(
|
||||
text=number_text,
|
||||
type=jsgf.SequenceType.GROUP,
|
||||
@@ -323,6 +321,19 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
def __setitem__(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
# Determine whether word casing has to be fixed
|
||||
word_transform = None
|
||||
if word_casing == "upper":
|
||||
word_transform = str.upper
|
||||
elif word_casing == "lower":
|
||||
word_transform = str.lower
|
||||
|
||||
def fix_word_case(word):
|
||||
if isinstance(word, jsgf.Word):
|
||||
word.text = word_transform(word.text)
|
||||
|
||||
return word
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def do_intents_to_graph(sentences, slot_names, replacements, targets):
|
||||
@@ -333,25 +344,11 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
for sentence in intent_sentences:
|
||||
jsgf.walk_expression(sentence, number_transform, replacements)
|
||||
|
||||
# Determine whether word casing has to be fixed
|
||||
transform = None
|
||||
if word_casing == "upper":
|
||||
transform = str.upper
|
||||
elif word_casing == "lower":
|
||||
transform = str.lower
|
||||
|
||||
if transform:
|
||||
|
||||
def fix_case(word):
|
||||
if isinstance(word, jsgf.Word):
|
||||
word.text = transform(word.text)
|
||||
|
||||
return word
|
||||
|
||||
if word_transform:
|
||||
# Fix casing
|
||||
for intent_sentences in sentences.values():
|
||||
for sentence in intent_sentences:
|
||||
jsgf.walk_expression(sentence, fix_case, replacements)
|
||||
jsgf.walk_expression(sentence, fix_word_case, replacements)
|
||||
|
||||
# Convert to directed graph
|
||||
graph = intents_to_graph(sentences, replacements)
|
||||
@@ -379,6 +376,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
slot_names.add(slot_name)
|
||||
|
||||
# Load slot values
|
||||
has_slot_program = False
|
||||
for slot_key in slot_names:
|
||||
slot_info = find_slot(slot_key)
|
||||
|
||||
@@ -390,9 +388,13 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
line = line.strip()
|
||||
if line:
|
||||
sentence = jsgf.Sentence.parse(line)
|
||||
if word_transform:
|
||||
jsgf.walk_expression(sentence, fix_word_case)
|
||||
|
||||
slot_values.append(sentence)
|
||||
elif isinstance(slot_info, SlotProgramInfo):
|
||||
# Program that will generate values
|
||||
has_slot_program = True
|
||||
slot_values = SlotProgram(slot_info.path, command_args=slot_info.args)
|
||||
|
||||
# Replace $slot with sentences
|
||||
@@ -410,6 +412,7 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
"file_dep": ini_paths + deps,
|
||||
"targets": [intent_graph],
|
||||
"actions": [(do_intents_to_graph, [sentences, slot_names, replacements])],
|
||||
"uptodate": [False if has_slot_program else None],
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -523,6 +526,13 @@ def train_profile(profile_dir: Path, profile: Profile) -> Tuple[int, List[str]]:
|
||||
for word in read_dict(dict_file):
|
||||
print(word, file=vocab_file)
|
||||
|
||||
if profile.get("wake.system", "dummy") == "pocketsphinx":
|
||||
# Add words from Pocketsphinx wake keyphrase
|
||||
keyphrase = profile.get("wake.pocketsphinx.keyphrase", "")
|
||||
if keyphrase:
|
||||
for word in re.split(r"\s+", keyphrase):
|
||||
print(word, file=vocab_file)
|
||||
|
||||
@create_after(executed="language_model")
|
||||
def task_vocab():
|
||||
"""Writes all vocabulary words to a file from intent.fst."""
|
||||
|
||||
@@ -91,7 +91,7 @@ def make_dict(
|
||||
if (i < 1) or no_number:
|
||||
print(word, pronounce, file=dictionary_file)
|
||||
else:
|
||||
print(f"{word, i + 1}({pronounce})", file=dictionary_file)
|
||||
print(f"{word}({i + 1})", pronounce, file=dictionary_file)
|
||||
|
||||
words_in_dict.add(word)
|
||||
|
||||
|
||||
+4
-2
@@ -94,6 +94,7 @@ class EspeakSentenceSpeaker(RhasspyActor):
|
||||
self.disable_wake = True
|
||||
self.enable_wake = False
|
||||
self.wake: Optional[RhasspyActor] = None
|
||||
self.espeak_args: List[str] = []
|
||||
|
||||
def to_started(self, from_state: str) -> None:
|
||||
"""Transition to started state."""
|
||||
@@ -104,6 +105,7 @@ class EspeakSentenceSpeaker(RhasspyActor):
|
||||
self.wake = self.config.get("wake")
|
||||
self.wake_on_start = self.profile.get("rhasspy.listen_on_start", False)
|
||||
self.disable_wake = self.profile.get("text_to_speech.disable_wake", True)
|
||||
self.espeak_args = list(self.profile.get("text_to_speech.espeak.arguments", []))
|
||||
self.transition("ready")
|
||||
|
||||
def in_ready(self, message: Any, sender: RhasspyActor) -> None:
|
||||
@@ -143,7 +145,7 @@ class EspeakSentenceSpeaker(RhasspyActor):
|
||||
def speak(self, sentence: str, voice: Optional[str] = None) -> bytes:
|
||||
"""Get WAV buffer for sentence."""
|
||||
try:
|
||||
espeak_cmd = ["espeak"]
|
||||
espeak_cmd = ["espeak"] + self.espeak_args
|
||||
if voice:
|
||||
espeak_cmd.extend(["-v", str(voice)])
|
||||
|
||||
@@ -896,7 +898,7 @@ class HomeAssistantSentenceSpeaker(RhasspyActor):
|
||||
|
||||
# Convert to WAV
|
||||
if audio_url.endswith(".mp3"):
|
||||
lame_command = ["lame", "--decode", "-", "-"]
|
||||
lame_command = ["lame", "--decode", "--mp3input", "-", "-"]
|
||||
self._logger.debug(lame_command)
|
||||
|
||||
return subprocess.run(
|
||||
|
||||
+97
-45
@@ -1,23 +1,24 @@
|
||||
"""Rhasspy utility functions."""
|
||||
import collections
|
||||
import concurrent.futures
|
||||
import gzip
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
import wave
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import (Any, Callable, Dict, Iterable, List, Mapping, Optional,
|
||||
Set, Tuple)
|
||||
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple
|
||||
|
||||
import pywrapfst as fst
|
||||
import networkx as nx
|
||||
import rhasspynlu
|
||||
|
||||
from num2words import num2words
|
||||
|
||||
WHITESPACE_PATTERN = re.compile(r"\s+")
|
||||
@@ -329,55 +330,45 @@ def grouper(iterable, n, fillvalue=None):
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_sentences_by_intent(intent_fst: fst.Fst) -> Dict[str, Any]:
|
||||
"""Get all sentences from an FST."""
|
||||
from rhasspy.train.jsgf2fst import fstprintall, symbols2intent
|
||||
def make_sentences_by_intent(
|
||||
intent_graph: nx.DiGraph, num_samples: Optional[int] = None, extra_converters=None
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Get all sentences from a graph."""
|
||||
|
||||
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
|
||||
sentences_by_intent: Dict[str, Any] = defaultdict(list)
|
||||
|
||||
for symbols in fstprintall(intent_fst, exclude_meta=False):
|
||||
intent = symbols2intent(symbols)
|
||||
intent_name = intent["intent"]["name"]
|
||||
sentences_by_intent[intent_name].append(intent)
|
||||
start_node = None
|
||||
end_node = None
|
||||
for node, node_data in intent_graph.nodes(data=True):
|
||||
if node_data.get("start", False):
|
||||
start_node = node
|
||||
elif node_data.get("final", False):
|
||||
end_node = node
|
||||
|
||||
return sentences_by_intent
|
||||
if start_node and end_node:
|
||||
break
|
||||
|
||||
assert (start_node is not None) and (
|
||||
end_node is not None
|
||||
), "Missing start/end node(s)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sample_sentences_by_intent(
|
||||
intent_fst_paths: Dict[str, str], num_samples: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate random intents"""
|
||||
from rhasspy.train.jsgf2fst import fstprintall, symbols2intent
|
||||
|
||||
def sample_sentences(intent_name: str, intent_fst_path: str):
|
||||
rand_fst = fst.Fst.read_from_string(
|
||||
subprocess.check_output(
|
||||
["fstrandgen", f"--npath={num_samples}", intent_fst_path]
|
||||
)
|
||||
if num_samples is not None:
|
||||
# Randomly sample
|
||||
paths = random.sample(
|
||||
list(nx.all_simple_paths(intent_graph, start_node, end_node)), num_samples
|
||||
)
|
||||
else:
|
||||
# Use generator
|
||||
paths = nx.all_simple_paths(intent_graph, start_node, end_node)
|
||||
|
||||
sentences: List[Dict[str, Any]] = []
|
||||
for symbols in fstprintall(rand_fst, exclude_meta=False):
|
||||
intent = symbols2intent(symbols)
|
||||
sentences.append(intent)
|
||||
|
||||
return sentences
|
||||
|
||||
# Generate samples in parallel
|
||||
future_to_intent = {}
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
for intent_name, intent_fst_path in intent_fst_paths.items():
|
||||
future = executor.submit(sample_sentences, intent_name, intent_fst_path)
|
||||
future_to_intent[future] = intent_name
|
||||
|
||||
# { intent: [ { 'text': ..., 'entities': { ... } }, ... ] }
|
||||
sentences_by_intent: Dict[str, Any] = {}
|
||||
for future, intent_name in future_to_intent.items():
|
||||
sentences_by_intent[intent_name] = future.result()
|
||||
# TODO: Add converters
|
||||
for path in paths:
|
||||
_, recognition = rhasspynlu.fsticuffs.path_to_recognition(
|
||||
path, intent_graph, extra_converters=extra_converters
|
||||
)
|
||||
assert recognition, "Path failed"
|
||||
sentences_by_intent[recognition.intent.name].append(recognition.asdict())
|
||||
|
||||
return sentences_by_intent
|
||||
|
||||
@@ -416,7 +407,7 @@ def numbers_to_words(sentence: str, language: Optional[str] = None) -> str:
|
||||
number = float(word)
|
||||
|
||||
# 75 -> seventy-five -> seventy five
|
||||
words[i] = num2words(number, lang=language).replace("-", " ")
|
||||
words[i] = re.sub(r"[-,]\s*", " ", num2words(number, lang=language))
|
||||
changed = True
|
||||
except ValueError:
|
||||
pass # not a number
|
||||
@@ -507,3 +498,64 @@ def get_all_intents(ini_paths: List[Path]) -> Dict[str, Any]:
|
||||
_LOGGER.exception("Failed to parse %s", ini_paths)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CliConverter:
|
||||
"""Command-line converter for intent recognition"""
|
||||
|
||||
def __init__(self, name: str, command_path: Path):
|
||||
self.name = name
|
||||
self.command_path = command_path
|
||||
|
||||
def __call__(self, *args, converter_args=None):
|
||||
"""Runs external program to convert JSON values"""
|
||||
converter_args = converter_args or []
|
||||
proc = subprocess.Popen(
|
||||
[str(self.command_path)] + converter_args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
with io.StringIO() as input_file:
|
||||
for arg in args:
|
||||
json.dump(arg, input_file)
|
||||
|
||||
stdout, _ = proc.communicate(input=input_file.getvalue())
|
||||
|
||||
return [json.loads(line) for line in stdout.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def load_converters(profile) -> Dict[str, Any]:
|
||||
# Load user-defined converters
|
||||
converters = {}
|
||||
|
||||
converters_dir = Path(
|
||||
profile.read_path(profile.get("intent.fsticuffs.converters_dir", "converters"))
|
||||
)
|
||||
|
||||
if converters_dir.is_dir():
|
||||
_LOGGER.debug("Loading converters from %s", converters_dir)
|
||||
for converter_path in converters_dir.glob("**/*"):
|
||||
if not converter_path.is_file():
|
||||
continue
|
||||
|
||||
# Retain directory structure in name
|
||||
converter_name = str(
|
||||
converter_path.relative_to(converters_dir).with_suffix("")
|
||||
)
|
||||
|
||||
# Run converter as external program.
|
||||
# Input arguments are encoded as JSON on individual lines.
|
||||
# Output values should be encoded as JSON on individual lines.
|
||||
converter = CliConverter(converter_name, converter_path)
|
||||
|
||||
# Key off name without file extension
|
||||
converters[converter_name] = converter
|
||||
|
||||
_LOGGER.debug("Loaded converter %s from %s", converter_name, converter_path)
|
||||
|
||||
return converters
|
||||
|
||||
+12
-8
@@ -227,18 +227,19 @@ class PocketsphinxWakeListener(RhasspyActor):
|
||||
self.keyphrase = self.profile.get("wake.pocketsphinx.keyphrase", "")
|
||||
assert self.keyphrase, "No wake keyphrase"
|
||||
|
||||
# Fix casing
|
||||
dict_casing = self.profile.get("speech_to_text.dictionary_casing", "")
|
||||
if dict_casing == "lower":
|
||||
self.keyphrase = self.keyphrase.lower()
|
||||
elif dict_casing == "upper":
|
||||
self.keyphrase = self.keyphrase.upper()
|
||||
|
||||
# Verify that keyphrase words are in dictionary
|
||||
keyphrase_words = re.split(r"\s+", self.keyphrase)
|
||||
with open(dict_path, "r") as dict_file:
|
||||
word_dict = read_dict(dict_file)
|
||||
|
||||
dict_upper = self.profile.get("speech_to_text.dictionary_upper", False)
|
||||
for word in keyphrase_words:
|
||||
if dict_upper:
|
||||
word = word.upper()
|
||||
else:
|
||||
word = word.lower()
|
||||
|
||||
if word not in word_dict:
|
||||
self._logger.warning("%s not in dictionary", word)
|
||||
|
||||
@@ -570,7 +571,9 @@ class PreciseWakeListener(RhasspyActor):
|
||||
self.prediction_sem = threading.Semaphore()
|
||||
for _ in range(num_chunks):
|
||||
chunk = self.audio_buffer[: self.chunk_size]
|
||||
self.stream.write(chunk)
|
||||
if chunk:
|
||||
self.stream.write(chunk)
|
||||
|
||||
self.audio_buffer = self.audio_buffer[self.chunk_size :]
|
||||
|
||||
if self.send_not_detected:
|
||||
@@ -922,7 +925,8 @@ class PorcupineWakeListener(RhasspyActor):
|
||||
"""Load porcupine library."""
|
||||
if self.handle is None:
|
||||
for kw_path in self.keyword_paths:
|
||||
assert kw_path.is_file(), f"Missing {kw_path}"
|
||||
if not kw_path.is_file():
|
||||
self._logger.error("Missing porcupine keyword at {kw_path}")
|
||||
|
||||
from porcupine import Porcupine
|
||||
|
||||
|
||||
+47
-2
@@ -3,7 +3,7 @@
|
||||
<!-- Top Bar -->
|
||||
<nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top">
|
||||
<a href="/">
|
||||
<img class="navbar-brand" v-bind:class="spinnerClass" src="/img/logo.png">
|
||||
<img id="logo" class="navbar-brand" v-bind:class="spinnerClass" src="/img/logo.png">
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@@ -119,6 +119,9 @@
|
||||
Rhasspy will not work correctly until these files are downloaded.
|
||||
</p>
|
||||
<tree-view :data="missingFiles" :options="{ rootObjectKey: 'missing'}"></tree-view>
|
||||
<br>
|
||||
<label for="downloadStatus">Status:</label>
|
||||
<textarea id="downloadStatus" v-model="this.downloadStatus" style="width: 100%;" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
@@ -186,7 +189,11 @@
|
||||
|
||||
missingFiles: {},
|
||||
|
||||
version: ''
|
||||
version: '',
|
||||
|
||||
downloadStatus: '',
|
||||
|
||||
wakeSocket: null
|
||||
}
|
||||
},
|
||||
|
||||
@@ -209,6 +216,9 @@
|
||||
this.hasAlert = true
|
||||
this.alertText = text
|
||||
this.alertClass = 'alert-' + level
|
||||
|
||||
// Hide alert after 20 seconds
|
||||
setTimeout(this.clearAlert, 20000)
|
||||
},
|
||||
|
||||
beginAsync: function() {
|
||||
@@ -334,6 +344,8 @@
|
||||
downloadProfile: function() {
|
||||
this.beginAsync()
|
||||
this.downloading = true
|
||||
this.downloadStatus = ''
|
||||
setTimeout(this.updateDownloadStatus, 1000)
|
||||
ProfileService.downloadProfile()
|
||||
.then(() => {
|
||||
alert("Download is complete. Rhasspy will now restart. Make sure to train before using your profile!")
|
||||
@@ -344,6 +356,38 @@
|
||||
this.downloading = false
|
||||
this.endAsync()
|
||||
})
|
||||
},
|
||||
|
||||
updateDownloadStatus: function() {
|
||||
ProfileService.downloadStatus()
|
||||
.then((request) => {
|
||||
this.downloadStatus = request.data
|
||||
})
|
||||
|
||||
if (this.downloading) {
|
||||
setTimeout(this.updateDownloadStatus, 1000)
|
||||
}
|
||||
},
|
||||
|
||||
connectWakeSocket: function() {
|
||||
// Connect to /api/events/intent websocket
|
||||
var wsProtocol = 'ws://'
|
||||
if (window.location.protocol == 'https:') {
|
||||
wsProtocol = 'wss://'
|
||||
}
|
||||
|
||||
var wsURL = wsProtocol + window.location.host + '/api/events/wake'
|
||||
this.wakeSocket = new WebSocket(wsURL)
|
||||
this.wakeSocket.onmessage = (evt) => {
|
||||
$('#logo').css('filter', 'invert()')
|
||||
setTimeout(() => {
|
||||
$('#logo').css('filter', 'initial')
|
||||
}, 2000)
|
||||
}
|
||||
this.wakeSocket.onclose = () => {
|
||||
// Try to reconnect
|
||||
setTimeout(this.connectWakeSocket, 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -355,6 +399,7 @@
|
||||
this.getCustomWords()
|
||||
this.getUnknownWords()
|
||||
this.getProblems()
|
||||
this.connectWakeSocket()
|
||||
this.$options.sockets.onmessage = function(event) {
|
||||
this.rhasspyLog = event.data + '\n' + this.rhasspyLog
|
||||
}
|
||||
|
||||
@@ -109,7 +109,15 @@ const profileDefaults = {
|
||||
"reconnect_sec": 5,
|
||||
"site_id": "default",
|
||||
"username": "",
|
||||
"publish_intents": true
|
||||
"publish_intents": true,
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"ca_certs": "",
|
||||
"cert_reqs": "CERT_REQUIRED",
|
||||
"certfile": "",
|
||||
"ciphers": "",
|
||||
"keyfile": ""
|
||||
}
|
||||
},
|
||||
"rhasspy": {
|
||||
"default_profile": "en",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-success"
|
||||
v-if="sentences"
|
||||
:disabled="sentences[newKey] || newKey.length == 0">Add File</button>
|
||||
:disabled="sentences[newKey] || newKey.length == 0">New File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
title="Record a voice command while held, interpret when released"
|
||||
:disabled="interpreting || (holdRecording && !tapRecording)">{{ tapRecording ? 'Tap to Stop' : 'Tap to Record' }}</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-success" @click="this.playLastVoiceCommand"
|
||||
title="Play last voice command"><i class="fas fa-play"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -132,7 +136,9 @@
|
||||
audioContext: null,
|
||||
recorder: null,
|
||||
|
||||
sendHass: true
|
||||
sendHass: true,
|
||||
|
||||
intentSocket: null
|
||||
}
|
||||
},
|
||||
|
||||
@@ -267,7 +273,35 @@
|
||||
event.preventDefault()
|
||||
PronounceService.saySentence(this.sentence)
|
||||
.catch(err => this.$parent.error(err))
|
||||
},
|
||||
|
||||
playLastVoiceCommand: function(event) {
|
||||
TranscribeService.playRecording()
|
||||
.catch(err => this.$parent.error(err))
|
||||
},
|
||||
|
||||
connectIntentSocket: function() {
|
||||
// Connect to /api/events/intent websocket
|
||||
var wsProtocol = 'ws://'
|
||||
if (window.location.protocol == 'https:') {
|
||||
wsProtocol = 'wss://'
|
||||
}
|
||||
|
||||
var wsURL = wsProtocol + window.location.host + '/api/events/intent'
|
||||
this.intentSocket = new WebSocket(wsURL)
|
||||
this.intentSocket.onmessage = (evt) => {
|
||||
this.jsonSource = JSON.parse(evt.data)
|
||||
this.sentence = this.jsonSource.raw_text
|
||||
}
|
||||
this.intentSocket.onclose = () => {
|
||||
// Try to reconnect
|
||||
setTimeout(this.connectIntentSocket, 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.connectIntentSocket()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user