diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml new file mode 100644 index 0000000..fb193b4 --- /dev/null +++ b/.github/workflows/license_tests.yml @@ -0,0 +1,12 @@ +name: Run License Tests +on: + push: + workflow_dispatch: + pull_request: + branches: + - master +jobs: + license_tests: + uses: neongeckocom/.github/.github/workflows/license_tests.yml@master + with: + packages-exclude: '^(neon-nodes|dnspython).*' diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml new file mode 100644 index 0000000..e10b4f0 --- /dev/null +++ b/.github/workflows/propose_release.yml @@ -0,0 +1,28 @@ +name: Propose Stable Release +on: + workflow_dispatch: + inputs: + release_type: + type: choice + description: Release Type + options: + - patch + - minor + - major +jobs: + update_version: + uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + with: + branch: dev + release_type: ${{ inputs.release_type }} + update_changelog: True + version_file: "neon_nodes/version.py" + pull_changes: + uses: neongeckocom/.github/.github/workflows/pull_master.yml@master + needs: update_version + with: + pr_reviewer: neonreviewers + pr_assignee: ${{ github.actor }} + pr_draft: false + pr_title: ${{ needs.update_version.outputs.version }} + pr_body: ${{ needs.update_version.outputs.changelog }} \ No newline at end of file diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000..2cd2be9 --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,24 @@ +# This workflow will generate a release distribution and upload it to PyPI + +name: Publish Build and GitHub Release +on: + push: + branches: + - master + +jobs: + tag_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Get Version + run: | + VERSION=$(python setup.py --version) + echo "VERSION=${VERSION}" >> $GITHUB_ENV + - uses: ncipollo/release-action@v1 + with: + token: ${{secrets.GITHUB_TOKEN}} + tag: ${{env.VERSION}} + build_and_publish_docker: + uses: neongeckocom/.github/.github/workflows/publish_docker.yml@master + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/publish_test_build.yml b/.github/workflows/publish_test_build.yml new file mode 100644 index 0000000..a48881c --- /dev/null +++ b/.github/workflows/publish_test_build.yml @@ -0,0 +1,21 @@ +# This workflow will generate a distribution and upload it to PyPI + +name: Publish Alpha Build +on: + push: + branches: + - dev + paths-ignore: + - 'neon_nodes/version.py' + +jobs: + publish_alpha_release: + uses: neongeckocom/.github/.github/workflows/publish_alpha_release.yml@master + secrets: inherit + with: + version_file: "neon_nodes/version.py" + publish_prerelease: true + build_and_publish_docker: + needs: publish_alpha_release + uses: neongeckocom/.github/.github/workflows/publish_docker.yml@master + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..2ffca45 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,10 @@ +name: Run Unit Tests +on: + pull_request: + workflow_dispatch: + +jobs: + py_build_tests: + uses: neongeckocom/.github/.github/workflows/python_build_tests.yml@master + with: + python_version: "3.9" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..307a15b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..de32b82 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Neon Nodes +Clients for connecting to a server running Hana. These are minimal classes that +are responsible for collecting a user's input, sending it to a remote system for +processing, and presenting a response to the user. + +## Voice Client +The voice client will start a service that listens for a wake word on the local +system, sends recorded speech to a HANA endpoint for processing, and plays back +the response. diff --git a/neon_nodes/__init__.py b/neon_nodes/__init__.py new file mode 100644 index 0000000..d782cbb --- /dev/null +++ b/neon_nodes/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/neon_nodes/configuration/system.yaml b/neon_nodes/configuration/system.yaml new file mode 100644 index 0000000..ee4bf1d --- /dev/null +++ b/neon_nodes/configuration/system.yaml @@ -0,0 +1,41 @@ +neon_node: + description: Neon Node +microphone: + module: ovos-microphone-plugin-alsa +listener: + wake_word: hey_mycroft + stand_up_word: "" + VAD: + silence_method: vad_only + speech_seconds: 0.1 + silence_seconds: 0.5 + before_seconds: 0.5 + min_seconds: 1 + max_current_ratio_threshold: 2 + initial_energy_threshold: 1000.0 + module: ovos-vad-plugin-silero + mute_during_output: true + instant_listen: false + enable_stt_api: false + # amount of time to wait for speech to start after WW detection + speech_begin: 0.5 + # amount of time without speech to wait before stopping listening + silence_end: 0.9 + # number of audio chunks from WW detection to include in STT audio + utterance_chunks_to_rewind: 1 + wakeword_chunks_to_save: 15 +hotwords: + hey_mycroft: + module: ovos-ww-plugin-precise-lite + model: /opt/neon/hey_mycroft.tflite + expected_duration: 3 + trigger_level: 3 + sensitivity: 0.5 + listen: true + wake_up: + enabled: false + wake_up_vosk: + enabled: false +logs: + path: stdout + level: DEBUG \ No newline at end of file diff --git a/neon_nodes/res/error.wav b/neon_nodes/res/error.wav new file mode 100644 index 0000000..758435e Binary files /dev/null and b/neon_nodes/res/error.wav differ diff --git a/neon_nodes/res/start_listening.wav b/neon_nodes/res/start_listening.wav new file mode 100644 index 0000000..c320218 Binary files /dev/null and b/neon_nodes/res/start_listening.wav differ diff --git a/neon_nodes/version.py b/neon_nodes/version.py new file mode 100644 index 0000000..e47fc1c --- /dev/null +++ b/neon_nodes/version.py @@ -0,0 +1,29 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +__version__ = "0.0.1a1" diff --git a/neon_nodes/voice_client.py b/neon_nodes/voice_client.py new file mode 100644 index 0000000..7e62d2f --- /dev/null +++ b/neon_nodes/voice_client.py @@ -0,0 +1,277 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 Neongecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import io +import requests + +from os.path import join, isfile, dirname +from threading import Thread, Event +from unittest.mock import Mock +from base64 import b64decode, b64encode + +from ovos_plugin_manager.microphone import OVOSMicrophoneFactory +from ovos_plugin_manager.vad import OVOSVADFactory +from ovos_dinkum_listener.voice_loop.voice_loop import DinkumVoiceLoop +from ovos_dinkum_listener.voice_loop.hotwords import HotwordContainer +from ovos_config.config import Configuration +from ovos_utils.messagebus import FakeBus +from ovos_utils.log import LOG +from ovos_bus_client.message import Message +from neon_utils.hana_utils import request_backend, ServerException +from neon_utils.net_utils import get_adapter_info +from neon_utils.user_utils import get_default_user_config +from speech_recognition import AudioData +from pydub import AudioSegment +from pydub.playback import play + + +class MockTransformers(Mock): + def transform(self, chunk): + return chunk, dict() + + +def on_ready(): + LOG.info("ready") + + +def on_stopping(): + LOG.info("stopping") + + +def on_error(e="unknown"): + LOG.error(e) + + +def on_alive(): + LOG.debug("alive") + + +def on_started(): + LOG.debug("started") + + +class NeonVoiceClient: + def __init__(self, bus=None, ready_hook=on_ready, error_hook=on_error, + stopping_hook=on_stopping, alive_hook=on_alive, + started_hook=on_started): + self.error_hook = error_hook + self.stopping_hook = stopping_hook + alive_hook() + self.config = Configuration() + self._device_data = self.config.get('neon_node', {}) + LOG.init(self.config.get("logging")) + self.bus = bus or FakeBus() + self.lang = self.config.get('lang') or "en-us" + self._mic = OVOSMicrophoneFactory.create(self.config) + self._mic.start() + self._hotwords = HotwordContainer(self.bus) + self._hotwords.load_hotword_engines() + self._vad = OVOSVADFactory.create(self.config) + + self._voice_loop = DinkumVoiceLoop(mic=self._mic, + hotwords=self._hotwords, + stt=Mock(), + fallback_stt=Mock(), + vad=self._vad, + transformers=MockTransformers(), + stt_audio_callback=self.on_stt_audio, + listenword_audio_callback= + self.on_hotword_audio) + self._voice_loop.start() + self._voice_thread = None + self._watchdog_event = Event() + + self._listening_sound = None + self._error_sound = None + + self._network_info = dict() + self._node_data = dict() + + started_hook() + self.run() + ready_hook() + + @property + def listening_sound(self) -> AudioSegment: + """ + Get an AudioSegment representation of the configured listening sound + """ + if not self._listening_sound: + res_file = Configuration().get('sounds').get('start_listening') + if not isfile(res_file): + res_file = join(dirname(__file__), "res", "start_listening.wav") + self._listening_sound = AudioSegment.from_file(res_file, + format="wav") + return self._listening_sound + + @property + def error_sound(self) -> AudioSegment: + """ + Get an AudioSegment representation of the configured error sound + """ + if not self._error_sound: + res_file = Configuration().get('sounds').get('error') + if not isfile(res_file): + res_file = join(dirname(__file__), "res", "error.wav") + self._error_sound = AudioSegment.from_file(res_file, format="wav") + return self._error_sound + + @property + def network_info(self) -> dict: + """ + Get networking information about this client, including IP addresses and + MAC address. + """ + if not self._network_info: + self._network_info = get_adapter_info() + public_ip = requests.get('https://api.ipify.org').text + self._network_info["public"] = public_ip + LOG.debug(f"Resolved network info: {self._network_info}") + return self._network_info + + @property + def node_data(self): + """ + Get information about this node from configuration and networking status + """ + if not self._node_data: + self._node_data = {"device_description": self._node_data.get( + 'description', 'node voice client'), + "networking": { + "local_ip": self.network_info.get('ipv4'), + "public_ip": self.network_info.get('public'), + "mac_address": self.network_info.get('mac')} + } + LOG.info(f"Resolved node_data: {self._node_data}") + return self._node_data + + @property + def user_profile(self) -> dict: + """ + Get a user profile from local disk + """ + return get_default_user_config() + + def run(self): + """ + Start the voice thread as a daemon and return + """ + try: + self._voice_thread = Thread(target=self._voice_loop.run, + daemon=True) + self._voice_thread.start() + except Exception as e: + self.error_hook(repr(e)) + raise e + + def watchdog(self): + """ + Runs in a loop to make sure the voice loop is running. If the loop is + unexpectedly stopped, raise an exception to kill this process. + """ + try: + while not self._watchdog_event.wait(30): + if not self._voice_thread.is_alive(): + self.error_hook("11") + raise RuntimeError("Voice Thread not alive") + if not self._voice_loop._is_running: + self.error_hook("12") + raise RuntimeError("Voice Loop not running") + except KeyboardInterrupt: + self.shutdown() + + def on_stt_audio(self, audio_bytes: bytes, context: dict): + """ + Callback when there is a recorded STT segment. + @param audio_bytes: bytes of recorded audio + @param context: dict context associated with recorded audio + """ + LOG.debug(f"Got {len(audio_bytes)} bytes of audio") + wav_data = AudioData(audio_bytes, self._mic.sample_rate, + self._mic.sample_width).get_wav_data() + try: + self.get_audio_response(wav_data) + except ServerException as e: + LOG.error(e) + play(self.error_sound) + + def on_hotword_audio(self, audio: bytes, context: dict): + """ + Callback when a hotword is detected. + @param audio: bytes of detected hotword audio + @param context: dict context associated with recorded hotword + """ + payload = context + msg_type = "recognizer_loop:wakeword" + play(self.listening_sound) + LOG.info(f"Emitting hotword event: {msg_type}") + # emit ww event + self.bus.emit(Message(msg_type, payload, context)) + # TODO: Optionally save/upload hotword audio + + def get_audio_response(self, audio: bytes): + """ + Handle recorded audio input and get/speak a response. + @param audio: bytes of STT audio + """ + audio_data = b64encode(audio).decode("utf-8") + transcript = request_backend("neon/get_stt", + {"encoded_audio": audio_data, + "lang_code": self.lang}) + transcribed = transcript['transcripts'][0] + LOG.info(transcribed) + response = request_backend("neon/get_response", + {"lang_code": self.lang, + "user_profile": self.user_profile, + "node_data": self.node_data, + "utterance": transcribed}) + answer = response['answer'] + LOG.info(answer) + audio = request_backend("neon/get_tts", {"lang_code": self.lang, + "to_speak": answer}) + audio_bytes = b64decode(audio['encoded_audio']) + play(AudioSegment.from_file(io.BytesIO(audio_bytes), format="wav")) + LOG.info(f"Playback completed") + + def shutdown(self): + """ + Cleanly stop all threads and shutdown this service + """ + self.stopping_hook() + self._watchdog_event.set() + self._voice_loop.stop() + self._voice_thread.join(30) + + +def main(*args, **kwargs): + client = NeonVoiceClient(*args, **kwargs) + client.watchdog() + + +if __name__ == "__main__": + # environ.setdefault("OVOS_CONFIG_BASE_FOLDER", "neon") + # environ.setdefault("OVOS_CONFIG_FILENAME", "diana.yaml") + main() diff --git a/requirements/voice_client.txt b/requirements/voice_client.txt new file mode 100644 index 0000000..8d3dccb --- /dev/null +++ b/requirements/voice_client.txt @@ -0,0 +1,10 @@ +neon-utils[network]~=1.8,>=1.8.3a4 +ovos-dinkum-listener~=0.0.2,>=0.0.3a27 +ovos-vad-plugin-silero~=0.0.1 +ovos-microphone-plugin-alsa~=0.0.0 +# ovos-microphone-plugin-sounddevice Not working on Mark2 where alsa is +ovos-ww-plugin-precise-lite[tflite]~=0.1 +pydub~=0.25 +SpeechRecognition~=3.10 +sdnotify~=0.3 +requests~=2.28 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9f295fc --- /dev/null +++ b/setup.py @@ -0,0 +1,83 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from setuptools import setup, find_packages +from os import getenv, path + +BASE_PATH = path.abspath(path.dirname(__file__)) + + +def get_requirements(requirements_filename: str): + requirements_file = path.join(BASE_PATH, "requirements", requirements_filename) + with open(requirements_file, 'r', encoding='utf-8') as r: + requirements = r.readlines() + requirements = [r.strip() for r in requirements if r.strip() and not r.strip().startswith("#")] + + for i in range(0, len(requirements)): + r = requirements[i] + if "@" in r: + parts = [p.lower() if p.strip().startswith("git+http") else p for p in r.split('@')] + r = "@".join(parts) + if getenv("GITHUB_TOKEN"): + if "github.com" in r: + requirements[i] = r.replace("github.com", f"{getenv('GITHUB_TOKEN')}@github.com") + return requirements + + +with open(path.join(BASE_PATH, "README.md"), "r") as f: + long_description = f.read() + +with open(path.join(BASE_PATH, "neon_nodes", + "version.py"), "r", encoding="utf-8") as v: + for line in v.readlines(): + if line.startswith("__version__"): + if '"' in line: + version = line.split('"')[1] + else: + version = line.split("'")[1] + + +setup( + name='neon-nodes', + version=version, + description='Neon node clients for Hana', + long_description=long_description, + long_description_content_type="text/markdown", + url='https://github.com/NeonGeckoCom/neon-nodes', + author='NeonGecko', + author_email='developers@neon.ai', + license='BSD-3-Clause', + packages=find_packages(), + extras_require={"voice-client": get_requirements("voice_client.txt")}, + package_data={'neon_nodes': ['res/*']}, + zip_safe=True, + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3', + ] +)