Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import re | |
| import hashlib | |
| import mysql.connector as mydb | |
| import time | |
| #import cohere | |
| import os | |
| from dotenv import load_dotenv | |
| import numpy as np | |
| import psycopg2 | |
| from google import genai | |
| from google.genai import types | |
| import uuid | |
| import base64 | |
| import requests | |
| import asyncio | |
| import random | |
| import pandas as pd | |
| from docx import Document | |
| from llama_parse import LlamaParse | |
| from llama_index.core import SimpleDirectoryReader | |
| load_dotenv(verbose=True) | |
| client = genai.Client(api_key=os.environ["GEMINI_API_KEY"]) | |
| chat_history = [] | |
| intreme = [] | |
| basic_format = "" | |
| class DoSwitch: | |
| def __init__(self): | |
| # 状態(state)をインスタンス変数として保持 | |
| self.is_logged = False | |
| self.first_exec = False | |
| self.lastlog = "" | |
| def log_on(self): | |
| self.is_logged = True | |
| def log_oout(self): | |
| self.log_on = False | |
| def first_executed(self): | |
| self.first_exec = True | |
| my_switch = DoSwitch() | |
| def open_signup_window(): | |
| """ | |
| gr.HTMLコンポーネントを使って、HTMLのリンクをボタンのように表示します。 | |
| """ | |
| # HTMLのaタグを生成し、target="_blank"で新しいタブを開くように指定します。 | |
| # このリンクをCSSでボタンのようにスタイルします。 | |
| # この文字列全体がHTMLとしてGradioにレンダリングされます。 | |
| html_content = """ | |
| <div style="text-align: center; padding: 20px;"> | |
| <a href="https://accounts.google.com/v3/signin/identifier?authuser=0&continue=https://www.google.com/&ec=futura_exp_og_si_72776762_e&hl=ja&flowName=GlifWebSignIn&flowEntry=AddSession&dsh=S-1088503595%3A1757031827842666" target="_blank" | |
| style=" | |
| display: inline-block; | |
| padding: 15px 30px; | |
| background-color: #4CAF50; | |
| color: white; | |
| text-decoration: none; | |
| font-weight: bold; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| transition: all 0.3s ease; | |
| " | |
| onmouseover="this.style.backgroundColor='#45a049';" | |
| onmouseout="this.style.backgroundColor='#4CAF50';" | |
| onclick="this.style.display='none';" | |
| > | |
| サインアップページを開く | |
| </a> | |
| </div> | |
| """ | |
| return gr.HTML(html_content) | |
| def clean_text(text): | |
| # 改行とタブを削除 | |
| return text.replace('\n', '').replace('\t', '').replace(' ', '') | |
| def update_file_type(file): | |
| global filepath | |
| filepath = file | |
| if file is not None: | |
| # アップロードされたファイルの拡張子を取得 | |
| _, ext = os.path.splitext(file.name) | |
| return ext | |
| return "" | |
| def respond(ctype, msg, username, response_check): | |
| print("response_check:",response_check) | |
| if response_check == True: | |
| prompt = f"""「答え」をウェブで調べて、必ず、日本語で答えてください。 | |
| 「答え」={msg} | |
| """ | |
| else: | |
| prompt = f"""以下の「レポート」を「基本形」に沿って正しく明記されているかをチェックし、レポートがどれ位「基本形」に沿った形でカバーしているかを教えてください。尚、漏れている項目を表示し、その内容をウェブから検索し、アドバイスしてください。最終的に内容がわかりやすかったか否かを教えてください。「基本形」にどれ位沿っているかも教えてください。必ず、日本語で答えてください。 | |
| 「レポート」={msg} | |
| 「基本形」={basic_format} | |
| """ | |
| gresponse = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[prompt] | |
| ) | |
| summary = gresponse.text | |
| print("summary1:",summary) | |
| return summary | |
| def validate_username(username): | |
| if len(username) < 4: | |
| return "ユーザー名は4文字以上である必要があります" | |
| if not re.match("^[a-zA-Z0-9_]+$", username): | |
| return "ユーザー名には文字、数字、アンダースコアのみ使用できます" | |
| return None | |
| def validate_email(email): | |
| if not re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email): | |
| return "有効なメールアドレスを入力してください" | |
| return None | |
| def validate_password(password): | |
| if len(password) < 8: | |
| return "パスワードは8文字以上でなければなりません" | |
| if not any(char.isdigit() for char in password): | |
| return "パスワードには少なくとも1つの数字を含める必要があります" | |
| if not any(char.isupper() for char in password): | |
| return "パスワードには少なくとも1つの大文字を含める必要があります" | |
| return None | |
| def validate_phone(phone): | |
| if phone and not re.match(r"^\+?[0-9\s\-]+$", phone): | |
| return "有効な電話番号を入力してください" | |
| return None | |
| def hash_password(password): | |
| return hashlib.sha256(password.encode()).hexdigest() | |
| def login_user(username, password): | |
| conn = mydb.connect( | |
| host='www.ryhintl.com', | |
| port='36000', | |
| user='smairuser', | |
| password='smairuser', | |
| database='smair' | |
| ) | |
| c = conn.cursor() | |
| hashed_pw = hash_password(password) | |
| sqlcmd = "SELECT * FROM gyoseki_users WHERE username = '"+username+"' AND password = '"+hashed_pw+"'" | |
| c.execute(sqlcmd) | |
| user = c.fetchone() | |
| conn.close() | |
| if user: | |
| return True, user | |
| return False, "ユーザー名またはパスワードが無効です" | |
| def process_logout(): | |
| time.sleep(1) | |
| return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) | |
| # Login Logic | |
| def process_login(username, password, current_user_info_state): | |
| username_error = validate_username(username) | |
| password_error = validate_password(password) | |
| if username_error: | |
| return gr.update(value=f"<p class='error-message'>{username_error}</p>"), False, None, None, gr.update(visible=True), gr.update(visible=False) | |
| elif password_error: | |
| return gr.update(value=f"<p class='error-message'>{password_error}</p>"), False, None, None, gr.update(visible=True), gr.update(visible=False) | |
| else: | |
| success, result = login_user(username, password) | |
| if success: | |
| user_info = { | |
| "id": result[0], | |
| "username": result[1], | |
| "email": result[2], | |
| "phone": result[4], | |
| "basic_format": result[5] | |
| } | |
| uid = result[5] | |
| conn = mydb.connect( | |
| host='www.ryhintl.com', | |
| port='36000', | |
| user='smairuser', | |
| password='smairuser', | |
| database='smair' | |
| ) | |
| c = conn.cursor() | |
| sqlcmd = "SELECT contents FROM gyoseki_basic_format WHERE id = "+str(uid) | |
| c.execute(sqlcmd) | |
| content = c.fetchone() | |
| global basic_format | |
| basic_format = content | |
| conn.close() | |
| return gr.update(value="<p class='success-message'></p>"), True, username, user_info, gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False) | |
| #return gr.update(value="<p class='success-message'></p>"), True, username, user_info, gr.update(visible=False), gr.update(visible=True) | |
| else: | |
| #return gr.update(visible=False), gr.update(visible=True) | |
| return gr.update(value=f"<p class='error-message'>{result}</p>"), False, None, None, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) | |
| #return gr.update(value=f"<p class='error-message'>{result}</p>"), False, None, None, gr.update(visible=True), gr.update(visible=False) | |
| def filecomp_change(prompt): | |
| if prompt == None: | |
| return gr.update(visible=False),gr.update(visible=True),gr.update(visible=True) | |
| else: | |
| return gr.update(visible=True),gr.update(visible=True),gr.update(visible=False) | |
| def summary_change(prompt): | |
| if len(prompt) == 0: | |
| return gr.update(visible=False) | |
| else: | |
| return gr.update(visible=True) | |
| def prompt_change(prompt): | |
| if len(prompt["files"]) > 0: | |
| print("cnt:",len(prompt["files"])) | |
| for x in prompt["files"]: | |
| file_path = prompt['files'][0] | |
| print("file path",file_path) | |
| return gr.update(visible=False),gr.update(visible=True),gr.update(visible=True) | |
| else: | |
| print("prompt:",prompt["text"]) | |
| return gr.update(visible=True),gr.update(visible=False),gr.update(visible=True) | |
| def show_tab_1(): | |
| return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) | |
| def show_tab_2(): | |
| return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) | |
| def show_tab_3(): | |
| return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) | |
| def embed(texts, input_type): | |
| # 改行または句点で分割(任意で調整可能) | |
| if isinstance(texts, str): | |
| inputs = [t.strip() for t in texts.split("\n") if t.strip()] | |
| else: | |
| inputs = texts | |
| result = client.models.embed_content( | |
| model="gemini-embedding-001", | |
| contents=inputs, | |
| config=types.EmbedContentConfig(output_dimensionality=1024) | |
| ) | |
| # 結果を整形して表示 | |
| formatted = "" | |
| for i, embedding in enumerate(result.embeddings): | |
| #formatted += f"Embedding (Array{i}: {embedding.values} \n\n" | |
| formatted += f"{embedding.values}" | |
| return formatted | |
| def summarize_text(long_text, username, summarize_text): | |
| if not long_text: | |
| return "要約するテキストがありません。" | |
| prompt = f"次の文章をuserとassistantに分けて的確に要約してください:\n{long_text}" | |
| gresponse = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[prompt] | |
| ) | |
| summary = gresponse.text | |
| if (summarize_text == '保存'): | |
| response = embed(summary,"search_document") | |
| embedding_str = response | |
| conn = psycopg2.connect( | |
| dbname="smair", | |
| user="smairuser", | |
| password="smairuser", | |
| host="www.ryhintl.com", | |
| port=10629 | |
| ) | |
| cur = conn.cursor() | |
| sql = "INSERT INTO dailog_logs (userid, content, embedding) VALUES (%s, %s, %s)" | |
| cur.execute(sql, (username, summary, f"{embedding_str}")) | |
| conn.commit() | |
| cur.close() | |
| conn.close() | |
| return summary | |
| else: | |
| return summary | |
| def generate_summary(input_text, username, summary_save): | |
| if not username: | |
| return "ログインしてください。" | |
| summary = summarize_text(input_text, username, summary_save) | |
| return summary | |
| def tnews(): | |
| res = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=["東証のtopixに関するニュースを教えてください。必ず、日本語で答えてください。"] | |
| ) | |
| return res.text | |
| def generate_response(dtype, file_name, file_type, msg, username, report_check): | |
| #if (file_name == None): # Should not happen if UI is correctly managed | |
| if (msg == '' and file_name == ''): # Should not happen if UI is correctly managed | |
| ret = [] | |
| ret.append(["レポートをアップロードしてください。","Please Upload report."]) | |
| yield ret | |
| elif (msg == '' and file_name != ''): | |
| if (my_switch.first_exec == True): | |
| intreme.append(["レポートを処理中...",tnews()]) | |
| yield intreme | |
| parser = LlamaParse( | |
| api_key=os.environ.get("LLAMA_API_KEY"), | |
| language="ja", | |
| high_res_ocr=True, | |
| user_prompt="提供された文書はスライドである。1ページにはファイルのタイトル、2ページは目次となっており、それ以降のページは各ページごとの副題を持つ。資料は、テキスト、図、表、グラフ、イラストを含む。", | |
| result_type="markdown" # "markdown" または "text" が選択可能 | |
| ) | |
| file_extractor = {file_type: parser} | |
| documents = SimpleDirectoryReader(input_files=[file_name], file_extractor=file_extractor, recursive=False).load_data() | |
| print("documents:",documents) | |
| texts = [doc.text for doc in documents] | |
| print("texts:",texts) | |
| combined_text = clean_text(texts[0]) | |
| msg = texts[0] | |
| bot_message = respond(file_type, msg, username, report_check) | |
| print("botmsg:",bot_message) | |
| chat_history.append([msg, bot_message]) | |
| intreme.clear() | |
| yield chat_history | |
| else: | |
| #gotresp = requests.get('') | |
| conn = psycopg2.connect( | |
| dbname="smair", | |
| user="smairuser", | |
| password="smairuser", | |
| host="www.ryhintl.com", | |
| port=10629 | |
| ) | |
| cur = conn.cursor() | |
| sql = "select content from dailog_logs where userid = '"+username+"'" | |
| cur.execute(sql) | |
| plog = cur.fetchone() | |
| conn.close() | |
| my_switch.first_exec = True | |
| my_switch.lastlog = plog | |
| prompt = f"""「前回の会話ログ」でユーザーの予測や仮説などから結果を核にする項目があれば、確認する質問を列挙してください。例えば、ハイブリッド車の販売が堅調でコスト削減効果が期待できることに言及したが、具体的な過去のコストや利益率の推移データは提示せず、トヨタ自動車の公式財務報告書などを参照するよう促した。などについてハイブリッド車の販売好調がコスト増加抑制に寄与しているという当初の推測について、その後、何か追加で確認できた点はありますか?」のように質問を列挙してください。 | |
| 「前回の会話ログ」={my_switch.lastlog} | |
| """ | |
| gresponse = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[prompt] | |
| ) | |
| lastqa = gresponse.text | |
| msg = lastqa | |
| intreme.append(["レポートを処理中...",lastqa]) | |
| #yield intreme | |
| '''parser = LlamaParse( | |
| api_key=os.environ.get("LLAMA_API_KEY"), | |
| language="ja", | |
| high_res_ocr=True, | |
| user_prompt="提供された文書はスライドである。1ページにはファイルのタイトル、2ページは目次となっており、それ以降のページは各ページごとの副題を持つ。資料は、テキスト、図、表、グラフ、イラストを含む。", | |
| result_type="markdown" # "markdown" または "text" が選択可能 | |
| ) | |
| file_extractor = {file_type: parser} | |
| documents = SimpleDirectoryReader(input_files=[file_name], file_extractor=file_extractor, recursive=False).load_data() | |
| texts = [doc.text for doc in documents] | |
| combined_text = clean_text(texts[0]) | |
| msg = texts[0]''' | |
| #bot_message = respond(file_type, msg, username, report_check) + "\n" + lastqa | |
| bot_message = respond(file_type, msg, username, report_check) | |
| chat_history.append([msg, bot_message]) | |
| intreme.clear() | |
| yield chat_history | |
| else: | |
| intreme.append(["レポートを処理中...",tnews]) | |
| yield intreme | |
| bot_message = respond(file_type, msg, username, report_check) | |
| chat_history.append([msg, bot_message]) | |
| intreme.clear() | |
| yield chat_history | |
| def summary_change(prompt): | |
| if len(prompt) == 0: | |
| return gr.update(visible=False) | |
| else: | |
| return gr.update(visible=True) | |
| with gr.Blocks(title="Fund Manager Academic Buddy", css="""footer {visibility: hidden;} #header {display: flex; justify-content: space-between; align-items: center; font-size: 24px; font-weight: bold;} #logo {width: 50px; height: 50px;} | |
| .gradio-container { | |
| background-color: #f8f9fa; | |
| /*width: 100%; | |
| height: 100%; | |
| background-image: url('https://images.unsplash.com/photo-1527181152855-fc03fc7949c8?auto=format&w=1000&dpr=2'); | |
| background-size: cover; | |
| background-repeat: no-repeat; | |
| background-position: center center;*/ | |
| } | |
| .main { | |
| background-color: #f8f9fa; | |
| } | |
| .logo-container { | |
| position: absolute; | |
| top: 1px; | |
| left: 20px; | |
| z-index: 1000; | |
| } | |
| .logo-container img { | |
| height: 30px; | |
| width: auto; | |
| } | |
| .title { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: #2c3e50; | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .subtitle { | |
| font-size: 0.1rem; | |
| color: #7f8c8d; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| .rcard { | |
| background: #000055; | |
| border-radius: 15px; | |
| padding: 2rem; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| margin-bottom: 1rem; | |
| height: 650px; | |
| overflow: hidden; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 2rem; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| margin-bottom: 1rem; | |
| } | |
| .success-message { | |
| color: #27ae60; | |
| text-align: center; | |
| margin-top: 1rem; | |
| } | |
| .error-message { | |
| color: #e74c3c; | |
| text-align: center; | |
| margin-top: 1rem; | |
| } | |
| .footer { | |
| text-align: center; | |
| margin-top: 1rem; | |
| color: #95a5a6; | |
| font-size: 0.8rem; | |
| } | |
| .avatar { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| margin: 0 auto 1rem auto; | |
| display: block; | |
| object-fit: cover; | |
| border: 3px solid #4a90e2; | |
| } | |
| .avatar:hover { | |
| transform: scale(1.2); | |
| border: 3px solid #4a90e2; | |
| } | |
| .gr-button { | |
| width: 100%; | |
| border-radius: 10px; | |
| padding: 10px; | |
| background-color: #4a90e2; | |
| color: white; | |
| border: none; | |
| font-weight: 500; | |
| transition: all 0.3s; | |
| } | |
| .gr-button:hover { | |
| background-color: #357abd; | |
| color: lightyellow; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
| } | |
| .gr-textinput, .gr-textbox { | |
| border-radius: 10px; | |
| padding: 10px; | |
| border: 1px solid #ced4da; | |
| } | |
| .my-custom-button { | |
| background-color: transparent !important; /* 緑色 */ | |
| color: black !important; | |
| padding: 10px 20px !important; | |
| border: solid thin black !important; | |
| border-radius: 5px !important; | |
| cursor: pointer !important; | |
| font-size: 16px !important; | |
| width: 100%; | |
| transition: background-color 0.3s ease !important; | |
| } | |
| .my-custom-button:hover { | |
| background-color: #fff5ee !important; /* ホバー時の色 */ | |
| } | |
| """) as demo: | |
| # State variables | |
| current_username = gr.State(None) | |
| current_user_info = gr.State(None) | |
| logged_in_state = gr.State(False) | |
| with gr.Sidebar(position="left", open=True): | |
| gr.Markdown("©️ FUND MANAGER BUDDY 2025") | |
| signin = gr.Button("サインイン", visible=True, elem_classes=["my-custom-button"], icon="https://img.icons8.com/fluency/48/azure-1.png") | |
| query = gr.Button("クエリー", visible=False, elem_classes=["my-custom-button"], icon="https://img.icons8.com/fluency/48/orthogonal-view.png") | |
| smry = gr.Button("ダイアログ・サマリー", visible=False, elem_classes=["my-custom-button"], icon="https://img.icons8.com/3d-fluency/48/domain.png") | |
| #with gr.Tabs() as tabs: | |
| # Set the first tab to be visible by default | |
| with gr.Column("タブ1", visible=False, elem_classes="card") as b1: | |
| signup_btn = gr.Button("サインアップ", elem_classes=["my-custom-button"], icon="https://img.icons8.com/color/48/000000/google-logo.png") | |
| login_username = gr.Textbox(label="ユーザー名", placeholder="ユーザー名を入力してください") | |
| login_password = gr.Textbox(label="パスワード", type="password", placeholder="パスワードを入力してください") | |
| login_status = gr.Markdown("") | |
| login_btn = gr.Button("サインイン") | |
| output_html = gr.HTML() | |
| signup_btn.click( | |
| fn=open_signup_window, | |
| inputs=[], | |
| outputs=[output_html] | |
| ) | |
| login_btn.click( | |
| process_login, | |
| inputs=[login_username, login_password, current_user_info], | |
| outputs=[login_status, logged_in_state, current_username, current_user_info, signin, query, smry, b1] | |
| ) | |
| with gr.Column("タブ2", visible=False) as b2: | |
| with gr.TabItem("クエリー"): | |
| logout_btn = gr.Button("ログアウト") | |
| dialog_ctype = gr.Dropdown(["デフォルト"], label="会話形式", value="デフォルト") | |
| with gr.Row(visible=True) as filecomp: | |
| file_input = gr.File(label="レポートをアップロード(PDF,pptx,xlsx,docx,txt,mdなど)") | |
| file_type = gr.Textbox(label="ファイルタイプ") | |
| with gr.Row(visible=True) as reportcomp: | |
| report_check = gr.Checkbox(label="レポート質問", info="レポート質問に対する答えの時にチェックしてください。") | |
| report_content = gr.Textbox(label="レポート", lines=10, show_copy_button=True) | |
| '''report_content = gr.MultimodalTextbox( | |
| interactive=True, | |
| file_count="single", | |
| placeholder="レポートの内容や質問を入力してください", | |
| label="レポート", | |
| lines=10, | |
| show_label=True, | |
| sources=["upload", "microphone"], | |
| )''' | |
| dialog_output = gr.Chatbot(label="対話履歴", height=400, show_copy_all_button=True) | |
| dialog_submit_btn = gr.Button("生成", visible=True) | |
| file_input.change(fn=update_file_type, inputs=file_input, outputs=file_type) | |
| #file_input.change( | |
| #filecomp_change, | |
| #inputs=[file_input], | |
| #outputs=[dialog_submit_btn, filecomp, reportcomp] | |
| #) | |
| report_content.change( | |
| prompt_change, | |
| #handle_change, | |
| inputs=[report_content], | |
| outputs=[dialog_submit_btn, filecomp, reportcomp] | |
| ) | |
| dialog_submit_btn.click( | |
| generate_response, | |
| inputs=[dialog_ctype, file_input, file_type, report_content, current_username, report_check], | |
| outputs=[dialog_output] | |
| ) | |
| with gr.Column("タブ3", visible=False) as b3: | |
| with gr.TabItem("ダイアログ・サマリー"): | |
| summary_input = gr.Textbox(label="要約したいユーザー・ダイアログを入力", lines=5) | |
| summary_save = gr.Dropdown(["保存","保存しない"], label="DB保存", value="保存しない") | |
| summary_output = gr.Markdown(label="要約結果", height=400, show_copy_button=True) | |
| summary_submit_btn = gr.Button("要約", visible=False) | |
| summary_input.change( | |
| summary_change, | |
| inputs=[summary_input], | |
| outputs=[summary_submit_btn] | |
| ) | |
| summary_submit_btn.click( | |
| generate_summary, | |
| inputs=[summary_input, current_username, summary_save], | |
| outputs=summary_output | |
| ) | |
| signin.click(show_tab_1, inputs=None, outputs=[b1, b2, b3]) | |
| query.click(show_tab_2, inputs=None, outputs=[b1, b2, b3]) | |
| smry.click(show_tab_3, inputs=None, outputs=[b1, b2, b3]) | |
| logout_btn.click( | |
| process_logout, | |
| inputs=[], | |
| outputs=[signin,query,smry,b2] | |
| ) | |
| #with gr.Sidebar(position="left"): | |
| #gr.Markdown("## サイドバー") | |
| #gr.Button("サインイン", elem_classes=["my-custom-button"], icon="https://img.icons8.com/fluency/48/azure-1.png").click(show_tab_1, inputs=None, outputs=[b1, b2, b3]) | |
| #query = gr.Button("クエリー", visible=False, elem_classes=["my-custom-button"], icon="https://img.icons8.com/fluency/48/orthogonal-view.png").click(show_tab_2, inputs=None, outputs=[b1, b2, b3]) | |
| #smry = gr.Button("ダイアログ・サマリー", visible=False, elem_classes=["my-custom-button"], icon="https://img.icons8.com/3d-fluency/48/domain.png").click(show_tab_3, inputs=None, outputs=[b1, b2, b3]) | |
| demo.launch() | |