diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f37eeb9..7d83ee5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,6 @@ repos: - id: check-added-large-files - id: detect-private-key - id: requirements-txt-fixer - - id: debug-statements - repo: https://github.com/pycqa/isort rev: 5.12.0 @@ -52,10 +51,3 @@ repos: - id: python-check-blanket-noqa - id: python-use-type-annotations - id: text-unicode-replacement-char - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 - hooks: - - id: mypy - additional_dependencies: [ types-PyYAML==6.0.10, types_requests==2.28.8, types-ujson==5.5.0 ] - args: [ --ignore-missing-imports, --warn-no-return, --warn-redundant-casts, --disallow-incomplete-defs ] diff --git a/.streamlit/config.toml b/.streamlit/config.toml index d10ebc7..e8425ee 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,3 +1,6 @@ +[global] +showWarningOnDirectExecution = false + [theme] primaryColor = "#0E8388" backgroundColor = "#2C3333" diff --git a/assets/icons/rec_on.gif b/assets/icons/rec_on.gif new file mode 100644 index 0000000..bf5c58a Binary files /dev/null and b/assets/icons/rec_on.gif differ diff --git a/chat.py b/chat.py index c9390fe..a39062c 100644 --- a/chat.py +++ b/chat.py @@ -10,17 +10,19 @@ from src.utils.conversation import get_user_input, show_chat_buttons, show_conve import streamlit as st # --- PATH SETTINGS --- -current_dir = Path(__file__).parent if "__file__" in locals() else Path.cwd() -css_file = current_dir / "src/styles/.css" -assets_dir = current_dir / "assets" -icons_dir = assets_dir / "icons" -img_dir = assets_dir / "img" -tg_svg = icons_dir / "tg.svg" +current_dir: Path = Path(__file__).parent if "__file__" in locals() else Path.cwd() +css_file: Path = current_dir / "src/styles/.css" +assets_dir: Path = current_dir / "assets" +icons_dir: Path = assets_dir / "icons" +img_dir: Path = assets_dir / "img" +tg_svg: Path = icons_dir / "tg.svg" # --- GENERAL SETTINGS --- -PAGE_TITLE = "AI Talks" -PAGE_ICON = "🤖" -AI_MODEL_OPTIONS = [ +PAGE_TITLE: str = "AI Talks" +PAGE_ICON: str = "🤖" +LANG_EN: str = "En" +LANG_RU: str = "Ru" +AI_MODEL_OPTIONS: list[str] = [ "gpt-3.5-turbo", "gpt-4", "gpt-4-32k", @@ -35,7 +37,7 @@ with open(css_file) as f: selected_lang = option_menu( menu_title=None, - options=["En", "Ru", ], + options=[LANG_EN, LANG_RU, ], icons=["globe2", "globe"], menu_icon="cast", default_index=0, @@ -44,36 +46,44 @@ selected_lang = option_menu( ) # Storing The Context +if "locale" not in st.session_state: + st.session_state.locale = en if "generated" not in st.session_state: - st.session_state["generated"] = [] + st.session_state.generated = [] if "past" not in st.session_state: - st.session_state["past"] = [] + st.session_state.past = [] if "messages" not in st.session_state: - st.session_state["messages"] = [] + st.session_state.messages = [] if "user_text" not in st.session_state: - st.session_state["user_text"] = "" + st.session_state.user_text = "" +if "input_kind" not in st.session_state: + st.session_state.input_kind = st.session_state.locale.input_kind_1 def main() -> None: - if st.session_state.user_text: - show_conversation() - st.session_state.user_text = "" - - c1, c2, c3 = st.columns(3) + c1, c2 = st.columns(2) with c1, c2: c1.selectbox(label=st.session_state.locale.select_placeholder1, key="model", options=AI_MODEL_OPTIONS) - role_kind = c2.radio( + st.session_state.input_kind = c2.radio( + label=st.session_state.locale.input_kind, + options=(st.session_state.locale.input_kind_1, st.session_state.locale.input_kind_2), + horizontal=True, + ) + role_kind = c1.radio( label=st.session_state.locale.radio_placeholder, options=(st.session_state.locale.radio_text1, st.session_state.locale.radio_text2), horizontal=True, ) match role_kind: case st.session_state.locale.radio_text1: - c3.selectbox(label=st.session_state.locale.select_placeholder2, key="role", + c2.selectbox(label=st.session_state.locale.select_placeholder2, key="role", options=st.session_state.locale.ai_role_options) case st.session_state.locale.radio_text2: - c3.text_input(label=st.session_state.locale.select_placeholder3, key="role") + c2.text_input(label=st.session_state.locale.select_placeholder3, key="role") + if st.session_state.user_text: + show_conversation() + st.session_state.user_text = "" get_user_input() show_chat_buttons() @@ -85,7 +95,7 @@ if __name__ == "__main__": case "Ru": st.session_state.locale = ru case _: - locale = en + st.session_state.locale = en st.markdown(f"

{st.session_state.locale.title}

", unsafe_allow_html=True) st.markdown("---") main() diff --git a/requirements.txt b/requirements.txt index b9ec276..baf82d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ -streamlit==1.20.0 +streamlit==1.21.0 streamlit-chat==0.0.2.2 streamlit_option_menu==0.3.2 -openai==0.27.2 +openai==0.27.4 gtts==2.3.1 pip==23.0.1 +bokeh==2.4.2 +streamlit-bokeh-events==0.1.2 watchdog==3.0.0 diff --git a/src/utils/agi/bard.py b/src/utils/agi/bard.py index 7dba87a..be961c3 100644 --- a/src/utils/agi/bard.py +++ b/src/utils/agi/bard.py @@ -12,7 +12,7 @@ import streamlit as st US_AG = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" -class Chatbot: +class BardChat: """ A class to interact with Google Bard. Parameters diff --git a/src/utils/conversation.py b/src/utils/conversation.py index 0fac216..211255b 100644 --- a/src/utils/conversation.py +++ b/src/utils/conversation.py @@ -3,20 +3,34 @@ from openai.error import InvalidRequestError, OpenAIError from requests.exceptions import TooManyRedirects from streamlit_chat import message -from src.utils.agi.bard import Chatbot +from src.utils.agi.bard import BardChat from src.utils.agi.chat_gpt import chat_gpt_request +from src.utils.stt import show_voice_input from src.utils.tts import show_player def clear_chat() -> None: - st.session_state["generated"] = [] - st.session_state["past"] = [] - st.session_state["messages"] = [] - st.session_state["user_text"] = "" + st.session_state.generated = [] + st.session_state.past = [] + st.session_state.messages = [] + st.session_state.user_text = "" + + +def show_text_input() -> None: + st.text_area(label=st.session_state.locale.chat_placeholder, value=st.session_state.user_text, key="user_text") def get_user_input(): - st.text_area(label=st.session_state.locale.chat_placeholder, value=st.session_state.user_text, key="user_text") + match st.session_state.input_kind: + case st.session_state.locale.input_kind_1: + clear_chat() + show_text_input() + case st.session_state.locale.input_kind_2: + clear_chat() + show_voice_input() + show_text_input() + case _: + show_text_input() def show_chat_buttons() -> None: @@ -39,7 +53,7 @@ def show_chat(ai_content: str, user_text: str) -> None: st.session_state.generated.append(ai_content) if st.session_state.generated: for i in range(len(st.session_state.generated)): - message(st.session_state["past"][i], is_user=True, key=str(i) + "_user", avatar_style="micah") + message(st.session_state.past[i], is_user=True, key=str(i) + "_user", avatar_style="micah") message("", key=str(i)) st.markdown(st.session_state.generated[i]) @@ -67,7 +81,7 @@ def chat_gpt_conversation() -> None: def bard_conversation() -> None: try: - bard = Chatbot(st.secrets.api_credentials.bard_session) + bard = BardChat(st.secrets.api_credentials.bard_session) ai_content = bard.ask(st.session_state.user_text) st.warning(ai_content.get("content")) except (TooManyRedirects, AttributeError) as err: diff --git a/src/utils/lang.py b/src/utils/lang.py index d4ca708..f2534e1 100644 --- a/src/utils/lang.py +++ b/src/utils/lang.py @@ -17,6 +17,10 @@ class Locale: chat_run_btn: str chat_clear_btn: str chat_save_btn: str + speak_btn: str + input_kind: str + input_kind_1: str + input_kind_2: str select_placeholder1: str select_placeholder2: str select_placeholder3: str @@ -44,9 +48,13 @@ class EnLocale(Locale): donates1: str = "Russia" donates2: str = "World" chat_placeholder: str = "Start Your Conversation With AI:" - chat_run_btn: str = "Run" + chat_run_btn: str = "Ask" chat_clear_btn: str = "Clear" chat_save_btn: str = "Save" + speak_btn: str = "Push to Speak" + input_kind: str = "Input Kind" + input_kind_1: str = "Text" + input_kind_2: str = "Voice [test mode]" select_placeholder1: str = "Select Model" select_placeholder2: str = "Select Role" select_placeholder3: str = "Create Role" @@ -83,9 +91,13 @@ class RuLocale(Locale): donates1: str = "Россия" donates2: str = "Остальной Мир" chat_placeholder: str = "Начните Вашу Беседу с ИИ:" - chat_run_btn: str = "Запустить" + chat_run_btn: str = "Спросить" chat_clear_btn: str = "Очистить" chat_save_btn: str = "Сохранить" + speak_btn: str = "Нажмите и Говорите" + input_kind: str = "Вид ввода" + input_kind_1: str = "Текст" + input_kind_2: str = "Голос [тестовый режим]" select_placeholder1: str = "Выберите Модель" select_placeholder2: str = "Выберите Роль" select_placeholder3: str = "Создайте Роль" diff --git a/src/utils/stt.py b/src/utils/stt.py index e69de29..5cff52c 100644 --- a/src/utils/stt.py +++ b/src/utils/stt.py @@ -0,0 +1,83 @@ +import streamlit as st +from bokeh.models import CustomJS +from bokeh.models.widgets import Button +from streamlit_bokeh_events import streamlit_bokeh_events + +REC_GIF = "assets/icons/rec_on.gif" + + +def get_js_code(lang: str) -> str: + return """ + var value = ""; + var rand = 0; + var recognition = new webkitSpeechRecognition(); + recognition.continuous = false; + recognition.interimResults = true; + """ + f"recognition.lang = '{lang}';" + """ + document.dispatchEvent(new CustomEvent("GET_ONREC", {detail: 'start'})); + + recognition.onspeechstart = function () { + document.dispatchEvent(new CustomEvent("GET_ONREC", {detail: 'running'})); + } + recognition.onsoundend = function () { + document.dispatchEvent(new CustomEvent("GET_ONREC", {detail: 'stop'})); + } + recognition.onresult = function (e) { + var value2 = ""; + for (var i = e.resultIndex; i < e.results.length; ++i) { + if (e.results[i].isFinal) { + value += e.results[i][0].transcript; + rand = Math.random(); + + } else { + value2 += e.results[i][0].transcript; + } + } + document.dispatchEvent(new CustomEvent("GET_TEXT", {detail: {t:value, s:rand}})); + document.dispatchEvent(new CustomEvent("GET_INTRM", {detail: value2})); + + } + recognition.onerror = function(e) { + document.dispatchEvent(new CustomEvent("GET_ONREC", {detail: 'stop'})); + } + recognition.start(); + """ + + +def show_speak_btn() -> Button: + stt_button = Button(label=st.session_state.locale.speak_btn, button_type="success", width=100) + stt_button.js_on_event("button_click", CustomJS(code=get_js_code(st.session_state.locale.lang_code))) + return stt_button + + +def get_bokeh_result() -> dict: + stt_button = show_speak_btn() + return streamlit_bokeh_events( + bokeh_plot=stt_button, + events="GET_TEXT,GET_ONREC,GET_INTRM", + key="listen", + refresh_on_update=False, + override_height=75, + debounce_time=0, + ) + + +def show_voice_input() -> None: + if "input" not in st.session_state: + st.session_state.input = {"text": "", "session": 0} + result = get_bokeh_result() + if result: + if "GET_TEXT" in result: + if result.get("GET_TEXT")["t"] != "" and result.get("GET_TEXT")["s"] != st.session_state.input["session"]: + st.session_state.input["text"] = result.get("GET_TEXT")["t"] + st.session_state.input["session"] = result.get("GET_TEXT")["s"] + if "GET_ONREC" in result: + placeholder = st.container() + if result.get("GET_ONREC") == "start": + placeholder.image(REC_GIF) + st.session_state.input["text"] = "" + elif result.get("GET_ONREC") == "running": + placeholder.image(REC_GIF) + elif result.get("GET_ONREC") == "stop": + if st.session_state.input["text"] != "": + st.session_state.user_text = st.session_state.input["text"]