From 2e6268bd997f0752bc97fdc8e57301176cdc66ba Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Nov 2023 18:58:56 -0700 Subject: [PATCH] Initial Gradio web UI implementation with Dockerfile --- Dockerfile | 18 +++++ docker_overlay/etc/neon/diana.yaml | 7 ++ neon_iris/cli.py | 7 ++ neon_iris/client.py | 6 ++ neon_iris/web_client.py | 107 +++++++++++++++++++++++++++++ requirements/docker.txt | 1 + setup.py | 1 + 7 files changed, 147 insertions(+) create mode 100644 Dockerfile create mode 100644 docker_overlay/etc/neon/diana.yaml create mode 100644 neon_iris/web_client.py create mode 100644 requirements/docker.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..51996a1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.8-slim + +LABEL vendor=neon.ai \ + ai.neon.name="neon-iris" + +ENV OVOS_CONFIG_BASE_FOLDER neon +ENV OVOS_CONFIG_FILENAME diana.yaml +ENV XDG_CONFIG_HOME /config + +ADD . /neon_iris +WORKDIR /neon_iris + +RUN pip install wheel && \ + pip install .[docker] + +COPY docker_overlay/ / + +CMD ["iris", "start-gradio"] \ No newline at end of file diff --git a/docker_overlay/etc/neon/diana.yaml b/docker_overlay/etc/neon/diana.yaml new file mode 100644 index 0000000..11fe449 --- /dev/null +++ b/docker_overlay/etc/neon/diana.yaml @@ -0,0 +1,7 @@ +MQ: + server: neon-rabbitmq + port: 5672 + users: + mq_handler: + user: neon_api_utils + password: Klatchat2021 diff --git a/neon_iris/cli.py b/neon_iris/cli.py index f26f785..efdfe7e 100644 --- a/neon_iris/cli.py +++ b/neon_iris/cli.py @@ -118,6 +118,13 @@ def start_client(mq_config, user_config, lang, audio): client.shutdown() +@neon_iris_cli.command(help="Create a GradIO Client session") +def start_gradio(): + from neon_iris.web_client import GradIOClient + chat = GradIOClient() + chat.run() + + @neon_iris_cli.command(help="Transcribe an audio file") @click.option('--lang', '-l', default='en-us', help="language of input audio") diff --git a/neon_iris/client.py b/neon_iris/client.py index 63eb2e5..a88a728 100644 --- a/neon_iris/client.py +++ b/neon_iris/client.py @@ -74,6 +74,10 @@ def uid(self) -> str: """ return self._uid + @property + def default_username(self) -> str: + return self._user_config["user"]["username"] + @property def user_config(self) -> dict: """ @@ -260,6 +264,8 @@ def _build_message(self, msg_type: str, data: dict, def _send_utterance(self, utterance: str, lang: str, username: str, user_profiles: list): + username = username or self.default_username + user_profiles = user_profiles or [self.user_config] message = self._build_message("recognizer_loop:utterance", {"utterances": [utterance], "lang": lang}, username, user_profiles) diff --git a/neon_iris/web_client.py b/neon_iris/web_client.py new file mode 100644 index 0000000..0031a10 --- /dev/null +++ b/neon_iris/web_client.py @@ -0,0 +1,107 @@ +# 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. +from os.path import isfile + +import gradio + +from threading import Event +from ovos_bus_client import Message +from ovos_config import Configuration +from ovos_utils import LOG + +from neon_utils.file_utils import decode_base64_string_to_file + +from neon_iris.client import NeonAIClient + + +class GradIOClient(NeonAIClient): + def __init__(self, lang: str = None): + self.config = Configuration() + NeonAIClient.__init__(self, self.config.get("MQ")) + self._await_response = Event() + self._response = None + self.lang = lang or self.config.get('lang', 'en-us') + self.chat_ui = gradio.Blocks() + + def on_user_input(self, utterance: str, *args, **kwargs): + LOG.info(args) + LOG.info(kwargs) + self._await_response.clear() + self._response = None + self.send_utterance(utterance, self.lang) + self._await_response.wait(30) + LOG.info(f"Response={self._response}") + return self._response + + def run(self): + title = "Neon AI" + description = "Chat With Neon" + placeholder = "Ask me something" + audio_input = gradio.Audio(source="microphone", type="filepath") + chatbot = gradio.Chatbot(label=description) + textbox = gradio.Textbox(placeholder=placeholder) + with self.chat_ui as blocks: + gradio.ChatInterface(self.on_user_input, + chatbot=chatbot, + textbox=textbox, + additional_inputs=audio_input, + title=title, + retry_btn=None, + undo_btn=None) + blocks.launch(server_name="0.0.0.0", server_port=7860) + + def handle_klat_response(self, message: Message): + LOG.debug(f"Response_data={message.data}") + resp_data = message.data["responses"] + files = [] + sentences = [] + for lang, response in resp_data.items(): + sentences.append(response.get("sentence")) + if response.get("audio"): + for gender, data in response["audio"].items(): + filepath = "/".join([self.audio_cache_dir] + + response[gender].split('/')[-4:]) + files.append(filepath) + if not isfile(filepath): + decode_base64_string_to_file(data, filepath) + self._response = "\n".join(sentences) + self._await_response.set() + + def handle_complete_intent_failure(self, message: Message): + self._response = "ERROR" + self._await_response.set() + + def handle_api_response(self, message: Message): + pass + + def handle_error_response(self, message: Message): + pass + + def clear_caches(self, message: Message): + pass + + def clear_media(self, message: Message): + pass diff --git a/requirements/docker.txt b/requirements/docker.txt new file mode 100644 index 0000000..8f4ec9d --- /dev/null +++ b/requirements/docker.txt @@ -0,0 +1 @@ +gradio~=3.28 diff --git a/setup.py b/setup.py index 1a20b6a..dca819b 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ def get_requirements(requirements_filename: str): ], python_requires='>=3.6', install_requires=get_requirements("requirements.txt"), + extras_require={"docker": get_requirements("docker.txt")}, entry_points={ 'console_scripts': ['iris=neon_iris.cli:neon_iris_cli'] }