import gradio as gr import time import datetime from zoneinfo import ZoneInfo import os from openai import ( OpenAI, AuthenticationError, NotFoundError, BadRequestError ) # 各種設定値 MAX_TRIAL = int(os.environ["MAX_TRIAL"]) # メッセージ取得最大試行数 INTER_SEC = int(os.environ["INTER_SEC"]) # 試行間隔(秒) # sys_prompt_default = "あなたは優秀なアシスタントです。日本語で質問に回答してください。" # lang_code = {'Japanese': "ja", 'English': "en"} auto_play_bl = {'ON': True, 'OFF': False} voice_list = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] def set_state(state, openai_key, voice, auto_play, speed): state["openai_key"] = openai_key state["voice"] = voice state["auto_play"] = auto_play_bl[auto_play] state["speed"] = speed return state def add_history(history, text_msg): """ Chat履歴"history"に追加を行う関数 """ print(text_msg) print(history) # ユーザテキストをチャットに追加 history = history + [(text_msg, None)] # テキスト・オーディオ初期化 return history, gr.Textbox(value="", interactive=False), gr.components.Audio(value=None, interactive=False) def init(state, mode, text_msg, voice_msg): """ 初期処理(入力チェック・テキスト変換) """ print(state) err_msg = "" text = "" if text_msg == "" and voice_msg is None: # 何も入力がないならエラーメッセージを返す err_msg = "テキストまたは音声を入力してください。" return state, "", err_msg try: if state["client"] is None: # 初回起動時は初期処理をする os.environ["OPENAI_API_KEY"] = os.environ["TEST_OPENAI_KEY"] # テスト時 # os.environ["OPENAI_API_KEY"] = state["openai_key"] # クライアント新規作成 client = OpenAI() # client作成後は消す os.environ["OPENAI_API_KEY"] = "" # セッションにセット state["client"] = client # IDとして現在時刻をセット dt = datetime.datetime.now(ZoneInfo("Asia/Tokyo")) state["user_id"] = dt.strftime("%Y%m%d%H%M%S") else: # 既存のクライアントをセット client = state["client"] # モードが翻訳以外はアシスタント・スレッドセット if mode != 2: if state["thread_id"] == "": # スレッド作成・セット thread = client.beta.threads.create() state["thread_id"] = thread.id if state["assistant_id"] == "": # アシスタント作成 # assistant = client.beta.assistants.create( # name="codeinter_test", # instructions=state["system_prompt"], # # model="gpt-4-1106-preview", # model="gpt-3.5-turbo-1106", # tools=[{"type": "code_interpreter"}] # ) # state["assistant_id"] = assistant.id if mode == 0: # 日本語アシスタントをセット state["assistant_id"] = os.environ["ASSIST_JA"] elif mode == 1: # 英語教師アシスタントをセット state["assistant_id"] = os.environ["ASSIST_EN"] else: # アシスタント確認(IDが存在しないならエラーとなる) # assistant = client.beta.assistants.retrieve(state["assistant_id"]) pass # ユーザIDでフォルダ作成 os.makedirs(state["user_id"], exist_ok=True) if voice_msg is None: # 音声がないならテキストを返す text = text_msg else: # 音声があるならwhisperでテキストに変換 text = exec_transcript(client, voice_msg) pass except NotFoundError as e: err_msg = "アシスタントIDが間違っています。新しく作成する場合はアシスタントIDを空欄にして下さい。" print(e) except AuthenticationError as e: err_msg = "認証エラーとなりました。OpenAPIKeyが正しいか、支払い方法などが設定されているか確認して下さい。" print(e) except Exception as e: err_msg = "その他のエラーが発生しました。" print(e) finally: return state, text, err_msg def raise_exception(err_msg): """ エラーの場合例外を起こす関数 """ if err_msg != "": raise Exception() return def exec_transcript(client, voice_msg): """ whisperで文字に起こす関数 """ audio_file= open(voice_msg, "rb") transcript = client.audio.transcriptions.create( model="whisper-1", file=audio_file, language = "ja", response_format="text" ) return transcript def exec_translation(client, text): """ GPTで英語に翻訳する関数 """ response = client.chat.completions.create( model="gpt-3.5-turbo-1106", messages=[ {"role": "system", "content": "You are a translator. Please translate the Japanese you received into English."}, {"role": "user", "content": text}, ] ) return response.choices[0].message.content def exec_text_to_speech(client, voice , speed, folder, text): """ テキストを音声にする """ response = client.audio.speech.create( model= "tts-1", # "tts-1-hd", voice=voice, input=text, speed=speed ) # ファイル名は現在時刻 dt = datetime.datetime.now(ZoneInfo("Asia/Tokyo")) file_name = dt.strftime("%Y%m%d%H%M%S") + ".mp3" file_path = folder + "/" + file_name # 音声ファイルに出力 response.stream_to_file(file_path) return file_path def bot(state, mode, history): """ Chat返答処理 """ err_msg = "" file_path = None # セッション情報取得 client = state["client"] asist_id = state["assistant_id"] thread_id = state["thread_id"] last_msg_id = state["last_msg_id"] user_id = state["user_id"] voice = state["voice"] speed = state["speed"] # 最新のユーザからのメッセージ user_msg = history[-1][0] print(history) print(user_msg) if mode in (0, 1): # 日本語会話モードの場合 # メッセージ作成 message = client.beta.threads.messages.create( thread_id=thread_id, role="user", content=user_msg ) # RUNスタート run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=asist_id ) # "completed"となるまで繰り返す(指定秒おき) for i in range(0, MAX_TRIAL, 1): if i > 0: time.sleep(INTER_SEC) # メッセージ受け取り run = client.beta.threads.runs.retrieve( thread_id=thread_id, run_id=run.id ) # 前回のメッセージより後を昇順で取り出す messages = client.beta.threads.messages.list( thread_id=thread_id, after=last_msg_id, order="asc" ) # messageを取り出す for msg in messages: if msg.role == "assistant": assist_msg = msg.content[0].text.value if assist_msg != "": history[-1][1] = assist_msg # 音声を作成 file_path = exec_text_to_speech(client, voice, speed, user_id, assist_msg) if file_path is None: err_msg = "音声作成でエラーが発生しました。" history = history + [(None, err_msg)] else: history = history + [(None, (file_path,))] yield gr.Chatbot(label=run.status ,value=history), err_msg # 最終メッセージID更新 last_msg_id = msg.id # セッション更新 state["last_msg_id"] = last_msg_id # 完了なら終了 if run.status == "completed": yield gr.Chatbot(label=run.status ,value=history), err_msg break elif run.status == "failed": # エラーとして終了 err_msg = "※メッセージ取得に失敗しました。" yield gr.Chatbot(label=run.status ,value=history), err_msg break elif i == MAX_TRIAL: # エラーとして終了 err_msg = "※メッセージ取得の際にタイムアウトしました。" yield gr.Chatbot(label=run.status ,value=history), err_msg break elif mode == 2: # 英訳モードの場合 # GPTで英文にする bot_message = exec_translation(client, user_msg) file_path = exec_text_to_speech(client, voice, speed, user_id, bot_message) # bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"]) # bot_message = "まず、新宿駅南口に出ます。" # voice_path = "nova.mp3" history[-1][1] = bot_message # history[-1][1] = [None, (voice_path,)] history = history + [(None, (file_path,))] # history[-1][1] = (voice_path,) yield history, err_msg def finally_proc(state, history, err_msg): """ 最終処理 """ # テキスト・オーディオを使えるように interactive = gr.update(interactive = True) if err_msg == "": print(history[-1][1][0]) # 出力オーディオをセット audio = gr.update(value=history[-1][1][0], autoplay=state["auto_play"]) else: audio = None return interactive, interactive, audio def reset_chat(state): state["assistant_id"] = "" state["thread_id"] = "" state["last_msg_id"] = "" # Chatも初期化 return state, None with gr.Blocks() as demo: title = "

GPT音声対応チャット

" message = "

・WhisperとText to speechによりチャットを行います。
" message += "・テキストまたは音声を入力して下さい。モードの違いは「利用上の注意」タブをご覧ください。
" message += "・動画での紹介はこちら→https://www.youtube.com/watch?v=wMoAORg0Y5Q
" message += "※自動再生の音声は送信ボタンの下の”出力音声”から自動再生で流れています。(設定でOFFにできます)
" # message += "・テスト中でAPIKEY無しで動きます。フィードバックもお待ちしております。
" message += "

" gr.Markdown(title + message) # セッションの宣言 state = gr.State({ # "system_prompt": SYS_PROMPT_DEFAULT, "openai_key" : "", "client" : None, "assistant_id" : "", "thread_id" : "", "last_msg_id" : "", "user_id" : "", "auto_play" : True, "speed" : 0.8, "voice" : "nova" }) with gr.Tab("GPT-4V 音声入力対応チャット") as maintab: # モード選択 mode = gr.Radio(["通常(日本語)", "英会話", "英訳"], label="Mode", value="通常(日本語)", interactive=True, type="index") # 各コンポーネント定義 chatbot = gr.Chatbot(label="チャット画面") text_msg = gr.Textbox(label="テキスト") voice_msg=gr.components.Audio(label="音声入力", sources="microphone", type="filepath") with gr.Row(): btn = gr.Button("送信") clear = gr.ClearButton([chatbot, text_msg, voice_msg]) sys_msg = gr.Text(label="システムメッセージ", interactive = False) out_voice=gr.Audio(label="出力音声", type="filepath", interactive = False, autoplay = True) # 英訳保存用(非表示) # text_en = gr.Textbox(visible=False) # メッセージ送信時の処理 btn.click(init, [state, mode, text_msg, voice_msg],[state, text_msg, sys_msg], queue=False).then( raise_exception, sys_msg, None).success( add_history, [chatbot, text_msg], [chatbot, text_msg, voice_msg], queue=False).then( bot, [state, mode, chatbot], [chatbot, sys_msg]).then( finally_proc, [state, chatbot, sys_msg], [text_msg, voice_msg, out_voice], queue=False) # 録音終了時の処理 # voice_msg.stop_recording(init, [state, mode, text_msg, voice_msg],[state, text_msg, sys_msg], queue=False).then( # raise_exception, sys_msg, None).success( # add_history, [chatbot, text_msg], [chatbot, text_msg, voice_msg], queue=False).then( # bot, [state, mode, chatbot], [chatbot, sys_msg]).then( # finally_proc, [state, chatbot, sys_msg], [text_msg, voice_msg, out_voice], queue=False) with gr.Tab("設定"): openai_key = gr.Textbox(label="OpenAI API Key" ,visible=False) voice = gr.Dropdown(choices=voice_list, value = "nova", label="Voice", interactive = True) auto_play = gr.Dropdown(choices=["ON", "OFF"], value = "ON", label="Auto Play", interactive = True) speed = gr.Slider(0, 1, value=0.8, label="Speed", info="1に近づけるほど読むスピードが速くなります。", interactive = True) # sys_prompt = gr.Textbox(value = sys_prompt_default,lines = 5, label="Custom instructions", interactive = True) # モード変更時 mode.change(reset_chat, [state], [state, chatbot]) # 設定変更時 maintab.select(set_state, [state, openai_key, voice, auto_play, speed], state) with gr.Tab("利用上の注意"): caution = "・通常モードは日本人アシスタントとしてGPTが日本語で回答します。
" caution += "・英会話モードは英語の教師としてGPTが回答します。
" caution += "(英語で返答するように設定していますが、まれに日本語で回答するかもしれません。)
" caution += "・英訳モードはテキスト・音声ともに入力した日本語をそのまま英語に翻訳します。
" caution += "(GPTでの英訳となり、余計な文章などが含まれる可能性があります、)
" caution += "・モードを変更すると会話はリセットされます。
" caution += "・設定タブで声の種類(デフォルトはnova)、回答を自動再生にするか決めることができます。
" gr.Markdown("

" + caution + "

") with gr.Tab("声サンプル") as voice_chk: gr.Markdown("

Text to speechの声のサンプルです。(速度は0.8です)

") with gr.Row(): btn_alloy = gr.Button(value="alloy") btn_echo = gr.Button(value="echo") btn_fable = gr.Button(value="fable") with gr.Row(): btn_onyx = gr.Button(value="onyx") btn_nova = gr.Button(value="nova") btn_shimmer = gr.Button(value="shimmer") sample_voice=gr.Audio(type="filepath", interactive = False, autoplay = True) btn_alloy.click(lambda:"voice_sample/alloy.mp3", None, sample_voice) btn_echo.click(lambda:"voice_sample/echo.mp3", None, sample_voice) btn_fable.click(lambda:"voice_sample/fable.mp3", None, sample_voice) btn_onyx.click(lambda:"voice_sample/onyx.mp3", None, sample_voice) btn_nova.click(lambda:"voice_sample/nova.mp3", None, sample_voice) btn_shimmer.click(lambda:"voice_sample/shimmer.mp3", None, sample_voice) if __name__ == '__main__': demo.queue() demo.launch(debug=True)