Update app.py
Browse files
app.py
CHANGED
|
@@ -1,50 +1,46 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
| 3 |
-
import openai
|
| 4 |
import time
|
| 5 |
from pathlib import Path
|
| 6 |
-
# from pydub import AudioSegment
|
| 7 |
|
| 8 |
# --- OpenAI API 設定 ---
|
| 9 |
-
# **強烈建議** 將 API Key 設為環境變數 OPENAI_API_KEY
|
| 10 |
-
# 或在使用 Hugging Face Spaces 部署時設為 Secret
|
| 11 |
-
# 不要直接寫在程式碼中!
|
| 12 |
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 13 |
if not openai.api_key:
|
| 14 |
raise ValueError("請設定 OpenAI API Key 環境變數 OPENAI_API_KEY")
|
| 15 |
|
| 16 |
-
# Initialize OpenAI client (new SDK style)
|
| 17 |
client = openai.OpenAI()
|
| 18 |
|
| 19 |
# --- 模型選擇 (OpenAI Models) ---
|
| 20 |
-
LLM_MODEL = "gpt-3.5-turbo"
|
| 21 |
ASR_MODEL = "whisper-1"
|
| 22 |
-
TTS_MODEL = "tts-1"
|
| 23 |
-
TTS_VOICE = "
|
| 24 |
-
|
| 25 |
-
# --- 預設辯論主題 ---
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
| 31 |
-
"
|
|
|
|
|
|
|
| 32 |
]
|
| 33 |
|
| 34 |
-
# --- Helper 函數:呼叫 OpenAI API ---
|
| 35 |
-
|
| 36 |
def call_asr(audio_filepath):
|
| 37 |
"""將音訊檔案轉換為文字 (使用 OpenAI Whisper)"""
|
| 38 |
if not audio_filepath:
|
| 39 |
return ""
|
| 40 |
try:
|
| 41 |
-
# OpenAI Whisper API 需要文件句柄
|
| 42 |
with open(audio_filepath, "rb") as audio_file:
|
| 43 |
transcript = client.audio.transcriptions.create(
|
| 44 |
model=ASR_MODEL,
|
| 45 |
file=audio_file
|
| 46 |
)
|
| 47 |
-
return transcript.text
|
| 48 |
except Exception as e:
|
| 49 |
print(f"ASR Error (OpenAI): {e}")
|
| 50 |
return f"[語音辨識失敗: {e}]"
|
|
@@ -52,15 +48,14 @@ def call_asr(audio_filepath):
|
|
| 52 |
def call_llm(topic, user_stance, messages):
|
| 53 |
"""呼叫 OpenAI LLM 進行辯論回應"""
|
| 54 |
ai_stance = "反方" if user_stance == "正方" else "正方"
|
|
|
|
| 55 |
system_prompt = f"你正在參與一場關於「{topic}」的辯論。你扮演的是堅定的「{ai_stance}」。請根據對話歷史,針對使用者的最新論點,提出具有批判性、質疑性或反駁性的回應。保持簡潔有力,專注於論證,字數控制在150字以內。"
|
| 56 |
|
| 57 |
-
# 準備 OpenAI messages 格式
|
| 58 |
openai_messages = [{"role": "system", "content": system_prompt}]
|
| 59 |
for msg in messages:
|
| 60 |
-
|
| 61 |
-
role = msg.get("role") if msg.get("role") in ["user", "assistant"] else "user" # 預設為 user
|
| 62 |
content = msg.get("content", "")
|
| 63 |
-
if content:
|
| 64 |
openai_messages.append({"role": role, "content": content})
|
| 65 |
|
| 66 |
try:
|
|
@@ -79,95 +74,85 @@ def call_llm(topic, user_stance, messages):
|
|
| 79 |
def call_tts(text):
|
| 80 |
"""將文字轉換為語音 (使用 OpenAI TTS)"""
|
| 81 |
try:
|
| 82 |
-
# 檢查文字是否有效
|
| 83 |
if not text or not isinstance(text, str) or text.startswith("["):
|
| 84 |
print(f"Skipping TTS for invalid text: {text}")
|
| 85 |
return None
|
| 86 |
-
|
| 87 |
-
# 產生唯一的暫存檔案路徑
|
| 88 |
-
speech_file_path = Path(f"/tmp/speech_{int(time.time() * 1000)}.mp3") # OpenAI TTS 輸出 mp3
|
| 89 |
-
|
| 90 |
response = client.audio.speech.create(
|
| 91 |
model=TTS_MODEL,
|
| 92 |
voice=TTS_VOICE,
|
| 93 |
input=text
|
| 94 |
)
|
| 95 |
-
|
| 96 |
-
# 將音訊流式寫入檔案
|
| 97 |
response.stream_to_file(speech_file_path)
|
| 98 |
-
|
| 99 |
-
return str(speech_file_path) # 返回檔案路徑
|
| 100 |
except Exception as e:
|
| 101 |
print(f"TTS Error (OpenAI): {e}")
|
| 102 |
return None
|
| 103 |
|
| 104 |
-
# --- Gradio 主函數 (
|
| 105 |
-
def debate_turn(
|
| 106 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
user_text = ""
|
| 108 |
-
processed_audio_path = None
|
| 109 |
|
| 110 |
-
# 優先處理語音輸入
|
| 111 |
if user_input_audio:
|
| 112 |
print(f"Processing audio input: {user_input_audio}")
|
| 113 |
-
processed_audio_path = user_input_audio
|
| 114 |
user_text = call_asr(processed_audio_path)
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
#
|
| 119 |
-
|
| 120 |
-
elif user_text.startswith("["): # 辨識失敗
|
| 121 |
-
history.append((user_text, None)) # 顯示錯誤訊息
|
| 122 |
-
# 清空輸入並返回
|
| 123 |
-
return history, None, ""
|
| 124 |
-
|
| 125 |
-
# 如果沒有有效語音輸入 或 語音辨識失敗,則使用文字輸入
|
| 126 |
if not user_text or user_text.startswith("["):
|
| 127 |
if user_input_text:
|
| 128 |
user_text = user_input_text
|
| 129 |
-
#
|
| 130 |
-
# 如果前面已經因為 audio 加入了 history,這裡就不重複加
|
| 131 |
-
if not processed_audio_path:
|
| 132 |
history.append((user_text, None))
|
| 133 |
-
|
| 134 |
-
# 這裡選擇使用文字輸入(如果有的話)
|
| 135 |
-
elif user_input_text:
|
| 136 |
user_text = user_input_text
|
| 137 |
-
|
| 138 |
-
history.append((user_text, None))
|
| 139 |
-
|
| 140 |
-
|
| 141 |
else:
|
| 142 |
-
|
| 143 |
-
if not processed_audio_path: # 僅當連音訊都沒嘗試時才報錯
|
| 144 |
history.append(("[錯誤:請提供文字或語音論點]", None))
|
| 145 |
# 如果是語音辨識失敗,前面已加入錯誤訊息
|
| 146 |
-
return history, None, ""
|
| 147 |
|
| 148 |
-
# 確保 user_text 是有效的字串才繼續
|
| 149 |
if not isinstance(user_text, str) or user_text.startswith("["):
|
| 150 |
print("Invalid user text, stopping turn.")
|
| 151 |
-
# 如果 history 最後一筆是 audio tuple,且辨識失敗,避免錯誤繼續
|
| 152 |
if history and isinstance(history[-1][0], tuple) and history[-1][1] is None:
|
| 153 |
history.append((f"[無法處理用戶輸入: {user_text}]", None))
|
| 154 |
-
return history, None, ""
|
| 155 |
|
| 156 |
# --- 準備呼叫 LLM ---
|
| 157 |
llm_messages = []
|
| 158 |
-
# 從 history 整理出 LLM
|
| 159 |
for i, turn in enumerate(history):
|
| 160 |
user_msg, ai_msg = turn
|
| 161 |
user_content = None
|
| 162 |
if isinstance(user_msg, str):
|
| 163 |
-
if not user_msg.startswith("[") and not user_msg.startswith("(語音辨識結果:"):
|
| 164 |
user_content = user_msg
|
| 165 |
elif isinstance(user_msg, tuple):
|
| 166 |
-
#
|
| 167 |
-
|
| 168 |
-
if i == len(history) - 1 and not user_text.startswith("["): # 假設 user_text 是剛辨識的
|
| 169 |
user_content = user_text
|
| 170 |
-
# 注意:如果歷史中有多次語音,這裡的邏輯需要加強才能正確配對
|
| 171 |
|
| 172 |
if user_content:
|
| 173 |
llm_messages.append({"role": "user", "content": user_content})
|
|
@@ -177,12 +162,10 @@ def debate_turn(topic, user_stance, user_input_text, user_input_audio, history):
|
|
| 177 |
if not ai_msg.startswith("["):
|
| 178 |
ai_content = ai_msg
|
| 179 |
elif isinstance(ai_msg, tuple) and len(ai_msg) > 0:
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
elif isinstance(ai_msg[0], str) and ai_msg[0].endswith(".mp3"):
|
| 185 |
-
pass # 這是音訊,不是文本內容
|
| 186 |
|
| 187 |
if ai_content:
|
| 188 |
llm_messages.append({"role": "assistant", "content": ai_content})
|
|
@@ -193,75 +176,89 @@ def debate_turn(topic, user_stance, user_input_text, user_input_audio, history):
|
|
| 193 |
llm_messages.append({"role": "user", "content": user_text})
|
| 194 |
else:
|
| 195 |
print("Skipping LLM call due to invalid final user text.")
|
| 196 |
-
return history, None, ""
|
| 197 |
|
| 198 |
-
# --- 呼叫 LLM ---
|
| 199 |
-
ai_response_text = call_llm(
|
| 200 |
|
| 201 |
# --- 呼叫 TTS ---
|
| 202 |
ai_response_audio_path = call_tts(ai_response_text)
|
| 203 |
|
| 204 |
-
# --- 格式化 AI 回應並更新歷史 ---
|
| 205 |
-
# 找到用戶最新輸入的那一條記錄(它還沒有 AI 回應)
|
| 206 |
last_user_turn_index = -1
|
| 207 |
for i in range(len(history) - 1, -1, -1):
|
| 208 |
-
if history[i][1] is None and not history[i][0] is None:
|
| 209 |
last_user_turn_index = i
|
| 210 |
break
|
| 211 |
|
| 212 |
if last_user_turn_index != -1:
|
| 213 |
-
#
|
| 214 |
-
# 1. 更新找到的那一回合,填入 AI 的文字回應
|
| 215 |
history[last_user_turn_index] = (history[last_user_turn_index][0], ai_response_text)
|
| 216 |
-
|
| 217 |
-
# 2. 如果 TTS 成功,*追加*一個新的回合,只包含 AI 的音訊
|
| 218 |
if ai_response_audio_path and not ai_response_text.startswith("["):
|
| 219 |
-
|
| 220 |
-
history.append((None, (ai_response_audio_path,))) # User 設為 None, AI 設為音訊元組
|
| 221 |
-
# ****** 修改結束 ******
|
| 222 |
else:
|
| 223 |
-
# 如果找不到用戶回合(理論上不該發生),直接追加
|
| 224 |
print("Warning: Could not find user's turn. Appending AI response.")
|
| 225 |
history.append(("[用戶回合丟失?]", ai_response_text))
|
| 226 |
if ai_response_audio_path and not ai_response_text.startswith("["):
|
| 227 |
history.append((None, (ai_response_audio_path,)))
|
| 228 |
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
# 清空輸入框
|
| 231 |
-
return history, None, ""
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
gr.Markdown("
|
| 237 |
-
gr.Markdown("選擇一個議題和你的立場,用文字或語音提出論點,AI 將扮演對手與你辯論!")
|
| 238 |
|
| 239 |
chat_history = gr.State([]) # 儲存對話歷史
|
| 240 |
|
| 241 |
with gr.Row():
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
stance_radio = gr.Radio(["正方", "反方"], label="選擇你的立場", value="正方")
|
| 244 |
|
| 245 |
-
# Chatbot 現在可以顯示 Audio 元件
|
| 246 |
chatbot_ui = gr.Chatbot(label="辯論區", height=500, render_markdown=True, bubble_full_width=False)
|
| 247 |
|
| 248 |
with gr.Row():
|
| 249 |
with gr.Column(scale=7):
|
| 250 |
user_txt = gr.Textbox(label="輸入你的論點 (文字)", placeholder="在此輸入文字...")
|
| 251 |
with gr.Column(scale=3):
|
| 252 |
-
# 讓 Audio 元件接收麥克風輸入,輸出為檔案路徑
|
| 253 |
user_audio = gr.Audio(sources=["microphone"], type="filepath", label="或錄製你的論點 (語音)")
|
| 254 |
|
| 255 |
submit_btn = gr.Button("送出論點", variant="primary")
|
| 256 |
|
| 257 |
-
# --- 事件綁定 ---
|
| 258 |
submit_btn.click(
|
| 259 |
fn=debate_turn,
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
| 262 |
)
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
if __name__ == "__main__":
|
| 265 |
-
|
| 266 |
-
# 在 Colab 或本地 Jupyter Notebook 可能需要
|
| 267 |
-
demo.launch(debug=True) # 開啟 debug 可以在控制台看到更詳細的錯誤
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
| 3 |
+
import openai
|
| 4 |
import time
|
| 5 |
from pathlib import Path
|
| 6 |
+
# from pydub import AudioSegment
|
| 7 |
|
| 8 |
# --- OpenAI API 設定 ---
|
|
|
|
|
|
|
|
|
|
| 9 |
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 10 |
if not openai.api_key:
|
| 11 |
raise ValueError("請設定 OpenAI API Key 環境變數 OPENAI_API_KEY")
|
| 12 |
|
|
|
|
| 13 |
client = openai.OpenAI()
|
| 14 |
|
| 15 |
# --- 模型選擇 (OpenAI Models) ---
|
| 16 |
+
LLM_MODEL = "gpt-3.5-turbo"
|
| 17 |
ASR_MODEL = "whisper-1"
|
| 18 |
+
TTS_MODEL = "tts-1"
|
| 19 |
+
TTS_VOICE = "nova" # 改用聽起來可能較清晰的聲音
|
| 20 |
+
|
| 21 |
+
# --- 預設辯論主題 (更新為台灣時事) ---
|
| 22 |
+
CURRENT_TW_TOPICS = [
|
| 23 |
+
"台灣是否應提高核能發電比例以應對能源需求?",
|
| 24 |
+
"面對高房價,興建社會住宅是最佳解方嗎?",
|
| 25 |
+
"如何平衡經濟發展與環境保護(例如:能源轉型、生態保育)?",
|
| 26 |
+
"少子化下,延後退休年齡或引進更多外籍移工哪個更急迫?",
|
| 27 |
+
"改善台灣交通亂象,應優先加強執法還是改善道路設計?",
|
| 28 |
+
"健保制度改革:提高保費、部分負擔,或有其他永續方案?",
|
| 29 |
+
# 可以再加入其他近期熱門議題
|
| 30 |
]
|
| 31 |
|
| 32 |
+
# --- Helper 函數:呼叫 OpenAI API (call_asr, call_llm, call_tts 保持不變) ---
|
|
|
|
| 33 |
def call_asr(audio_filepath):
|
| 34 |
"""將音訊檔案轉換為文字 (使用 OpenAI Whisper)"""
|
| 35 |
if not audio_filepath:
|
| 36 |
return ""
|
| 37 |
try:
|
|
|
|
| 38 |
with open(audio_filepath, "rb") as audio_file:
|
| 39 |
transcript = client.audio.transcriptions.create(
|
| 40 |
model=ASR_MODEL,
|
| 41 |
file=audio_file
|
| 42 |
)
|
| 43 |
+
return transcript.text
|
| 44 |
except Exception as e:
|
| 45 |
print(f"ASR Error (OpenAI): {e}")
|
| 46 |
return f"[語音辨識失敗: {e}]"
|
|
|
|
| 48 |
def call_llm(topic, user_stance, messages):
|
| 49 |
"""呼叫 OpenAI LLM 進行辯論回應"""
|
| 50 |
ai_stance = "反方" if user_stance == "正方" else "正方"
|
| 51 |
+
# System prompt 保持不變,因為 topic 會動態傳入
|
| 52 |
system_prompt = f"你正在參與一場關於「{topic}」的辯論。你扮演的是堅定的「{ai_stance}」。請根據對話歷史,針對使用者的最新論點,提出具有批判性、質疑性或反駁性的回應。保持簡潔有力,專注於論證,字數控制在150字以內。"
|
| 53 |
|
|
|
|
| 54 |
openai_messages = [{"role": "system", "content": system_prompt}]
|
| 55 |
for msg in messages:
|
| 56 |
+
role = msg.get("role") if msg.get("role") in ["user", "assistant"] else "user"
|
|
|
|
| 57 |
content = msg.get("content", "")
|
| 58 |
+
if content:
|
| 59 |
openai_messages.append({"role": role, "content": content})
|
| 60 |
|
| 61 |
try:
|
|
|
|
| 74 |
def call_tts(text):
|
| 75 |
"""將文字轉換為語音 (使用 OpenAI TTS)"""
|
| 76 |
try:
|
|
|
|
| 77 |
if not text or not isinstance(text, str) or text.startswith("["):
|
| 78 |
print(f"Skipping TTS for invalid text: {text}")
|
| 79 |
return None
|
| 80 |
+
speech_file_path = Path(f"/tmp/speech_{int(time.time() * 1000)}.mp3")
|
|
|
|
|
|
|
|
|
|
| 81 |
response = client.audio.speech.create(
|
| 82 |
model=TTS_MODEL,
|
| 83 |
voice=TTS_VOICE,
|
| 84 |
input=text
|
| 85 |
)
|
| 86 |
+
# 使用 stream_to_file (即使有 DeprecationWarning,目前仍可用)
|
|
|
|
| 87 |
response.stream_to_file(speech_file_path)
|
| 88 |
+
return str(speech_file_path)
|
|
|
|
| 89 |
except Exception as e:
|
| 90 |
print(f"TTS Error (OpenAI): {e}")
|
| 91 |
return None
|
| 92 |
|
| 93 |
+
# --- Gradio 主函數 (修改 inputs 和 topic 決定邏輯) ---
|
| 94 |
+
def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text, user_input_audio, history):
|
| 95 |
+
"""處理一輪辯論,增加自訂主題處理"""
|
| 96 |
+
|
| 97 |
+
# --- 決定最終辯論主題 ---
|
| 98 |
+
final_topic = ""
|
| 99 |
+
if custom_topic and custom_topic.strip(): # 檢查自訂主題是否非空
|
| 100 |
+
final_topic = custom_topic.strip()
|
| 101 |
+
print(f"Using custom topic: {final_topic}")
|
| 102 |
+
elif topic_from_dropdown:
|
| 103 |
+
final_topic = topic_from_dropdown
|
| 104 |
+
print(f"Using dropdown topic: {final_topic}")
|
| 105 |
+
else:
|
| 106 |
+
# 如果兩者都無效,給一個預設或錯誤提示
|
| 107 |
+
history.append(("[錯誤:請選擇或輸入一個辯論主題]", None))
|
| 108 |
+
return history, None, "", "" # 返回時也要對應 outputs 數量
|
| 109 |
+
|
| 110 |
+
# --- 處理用戶輸入 (文字或語音) ---
|
| 111 |
user_text = ""
|
| 112 |
+
processed_audio_path = None
|
| 113 |
|
|
|
|
| 114 |
if user_input_audio:
|
| 115 |
print(f"Processing audio input: {user_input_audio}")
|
| 116 |
+
processed_audio_path = user_input_audio
|
| 117 |
user_text = call_asr(processed_audio_path)
|
| 118 |
+
history.append(((processed_audio_path,), None))
|
| 119 |
+
if user_text.startswith("["):
|
| 120 |
+
history.append((user_text, None))
|
| 121 |
+
return history, None, "", final_topic # 返回 history, 清空 audio, 清空 text, 保持 custom_topic 不變
|
| 122 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
if not user_text or user_text.startswith("["):
|
| 124 |
if user_input_text:
|
| 125 |
user_text = user_input_text
|
| 126 |
+
if not processed_audio_path: # 僅當沒有語音輸入時才新增文字輸入歷史
|
|
|
|
|
|
|
| 127 |
history.append((user_text, None))
|
| 128 |
+
elif user_input_text: # 如果語音辨識失敗但有文字輸入
|
|
|
|
|
|
|
| 129 |
user_text = user_input_text
|
| 130 |
+
history.append((f"(改用文字輸入: {user_text})", None)) # 標註一下
|
|
|
|
|
|
|
|
|
|
| 131 |
else:
|
| 132 |
+
if not processed_audio_path:
|
|
|
|
| 133 |
history.append(("[錯誤:請提供文字或語音論點]", None))
|
| 134 |
# 如果是語音辨識失敗,前面已加入錯誤訊息
|
| 135 |
+
return history, None, "", final_topic
|
| 136 |
|
|
|
|
| 137 |
if not isinstance(user_text, str) or user_text.startswith("["):
|
| 138 |
print("Invalid user text, stopping turn.")
|
|
|
|
| 139 |
if history and isinstance(history[-1][0], tuple) and history[-1][1] is None:
|
| 140 |
history.append((f"[無法處理用戶輸入: {user_text}]", None))
|
| 141 |
+
return history, None, "", final_topic
|
| 142 |
|
| 143 |
# --- 準備呼叫 LLM ---
|
| 144 |
llm_messages = []
|
| 145 |
+
# (從 history 整理出 LLM messages 的邏輯保持不變)
|
| 146 |
for i, turn in enumerate(history):
|
| 147 |
user_msg, ai_msg = turn
|
| 148 |
user_content = None
|
| 149 |
if isinstance(user_msg, str):
|
| 150 |
+
if not user_msg.startswith("[") and not user_msg.startswith("(改用文字輸入:") and not user_msg.startswith("(語音辨識結果:"): # 避免將提示文字加入對話
|
| 151 |
user_content = user_msg
|
| 152 |
elif isinstance(user_msg, tuple):
|
| 153 |
+
# 假設 user_text 是剛辨識的結果
|
| 154 |
+
if i == len(history) - 1 and not user_text.startswith("["):
|
|
|
|
| 155 |
user_content = user_text
|
|
|
|
| 156 |
|
| 157 |
if user_content:
|
| 158 |
llm_messages.append({"role": "user", "content": user_content})
|
|
|
|
| 162 |
if not ai_msg.startswith("["):
|
| 163 |
ai_content = ai_msg
|
| 164 |
elif isinstance(ai_msg, tuple) and len(ai_msg) > 0:
|
| 165 |
+
if isinstance(ai_msg[0], str) and ai_msg[0].endswith(".mp3"):
|
| 166 |
+
pass # 音訊檔,略過
|
| 167 |
+
elif isinstance(ai_msg[0], str) and not ai_msg[0].startswith("["):
|
| 168 |
+
ai_content = ai_msg[0] # 假設是 (文字, (音訊路徑,)) 結構中的文字
|
|
|
|
|
|
|
| 169 |
|
| 170 |
if ai_content:
|
| 171 |
llm_messages.append({"role": "assistant", "content": ai_content})
|
|
|
|
| 176 |
llm_messages.append({"role": "user", "content": user_text})
|
| 177 |
else:
|
| 178 |
print("Skipping LLM call due to invalid final user text.")
|
| 179 |
+
return history, None, "", final_topic
|
| 180 |
|
| 181 |
+
# --- 呼叫 LLM (使用 final_topic) ---
|
| 182 |
+
ai_response_text = call_llm(final_topic, user_stance, llm_messages)
|
| 183 |
|
| 184 |
# --- 呼叫 TTS ---
|
| 185 |
ai_response_audio_path = call_tts(ai_response_text)
|
| 186 |
|
| 187 |
+
# --- 格式化 AI 回應並更新歷史 (使用之前修正的邏輯) ---
|
|
|
|
| 188 |
last_user_turn_index = -1
|
| 189 |
for i in range(len(history) - 1, -1, -1):
|
| 190 |
+
if history[i][1] is None and not history[i][0] is None:
|
| 191 |
last_user_turn_index = i
|
| 192 |
break
|
| 193 |
|
| 194 |
if last_user_turn_index != -1:
|
| 195 |
+
# 1. 更新找到的回合,填入 AI 文字回應
|
|
|
|
| 196 |
history[last_user_turn_index] = (history[last_user_turn_index][0], ai_response_text)
|
| 197 |
+
# 2. 如果 TTS 成功,追加一個只包含 AI 音訊的回合
|
|
|
|
| 198 |
if ai_response_audio_path and not ai_response_text.startswith("["):
|
| 199 |
+
history.append((None, (ai_response_audio_path,)))
|
|
|
|
|
|
|
| 200 |
else:
|
|
|
|
| 201 |
print("Warning: Could not find user's turn. Appending AI response.")
|
| 202 |
history.append(("[用戶回合丟失?]", ai_response_text))
|
| 203 |
if ai_response_audio_path and not ai_response_text.startswith("["):
|
| 204 |
history.append((None, (ai_response_audio_path,)))
|
| 205 |
|
| 206 |
+
# 清空本輪的文字和語音輸入,不清空自訂主題輸入框
|
| 207 |
+
return history, None, "", final_topic # 返回 history, 清空 audio, 清空 text, custom_topic 保持不變
|
| 208 |
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
# --- Gradio UI (增加自訂主題輸入框) ---
|
| 211 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="時事觀點對對碰 (OpenAI + 自訂主題)") as demo:
|
| 212 |
+
gr.Markdown("## 🗣️ 時事觀點對對碰 (OpenAI + 自訂主題)")
|
| 213 |
+
gr.Markdown("選擇預設議題或輸入自訂議題,選擇立場,用文字或語音提出論點,AI 將扮演對手與你辯論!")
|
|
|
|
| 214 |
|
| 215 |
chat_history = gr.State([]) # 儲存對話歷史
|
| 216 |
|
| 217 |
with gr.Row():
|
| 218 |
+
# 選項一:下拉選單
|
| 219 |
+
topic_dd = gr.Dropdown(CURRENT_TW_TOPICS, label="選擇預設辯論主題", value=CURRENT_TW_TOPICS[0])
|
| 220 |
+
# 選項二:文字輸入框
|
| 221 |
+
custom_topic_txt = gr.Textbox(label="或輸入自訂辯論主題", placeholder="若此處輸入,將優先使用此主題...")
|
| 222 |
+
# 立場選擇
|
| 223 |
stance_radio = gr.Radio(["正方", "反方"], label="選擇你的立場", value="正方")
|
| 224 |
|
|
|
|
| 225 |
chatbot_ui = gr.Chatbot(label="辯論區", height=500, render_markdown=True, bubble_full_width=False)
|
| 226 |
|
| 227 |
with gr.Row():
|
| 228 |
with gr.Column(scale=7):
|
| 229 |
user_txt = gr.Textbox(label="輸入你的論點 (文字)", placeholder="在此輸入文字...")
|
| 230 |
with gr.Column(scale=3):
|
|
|
|
| 231 |
user_audio = gr.Audio(sources=["microphone"], type="filepath", label="或錄製你的論點 (語音)")
|
| 232 |
|
| 233 |
submit_btn = gr.Button("送出論點", variant="primary")
|
| 234 |
|
| 235 |
+
# --- 事件綁定 (更新 inputs 和 outputs) ---
|
| 236 |
submit_btn.click(
|
| 237 |
fn=debate_turn,
|
| 238 |
+
# inputs 順序需要與 debate_turn 函數參數對應
|
| 239 |
+
inputs=[topic_dd, custom_topic_txt, stance_radio, user_txt, user_audio, chatbot_ui],
|
| 240 |
+
# outputs: 更新 chatbot, 清空 audio 輸入, 清空 text 輸入, custom_topic 不變所以不用輸出更新
|
| 241 |
+
outputs=[chatbot_ui, user_audio, user_txt, custom_topic_txt] # custom_topic_txt 也作為輸出,使其保持不變
|
| 242 |
)
|
| 243 |
|
| 244 |
+
# 當選擇下拉選單時,清空自訂輸入框 (可選,提升體驗)
|
| 245 |
+
def clear_custom_topic(dropdown_value):
|
| 246 |
+
# 只有當下拉選單有值被選擇時才觸發清空
|
| 247 |
+
if dropdown_value:
|
| 248 |
+
return ""
|
| 249 |
+
return gr.Skip() # 如果 dropdown 被清空,則不改變 custom_topic_txt
|
| 250 |
+
|
| 251 |
+
topic_dd.change(fn=clear_custom_topic, inputs=[topic_dd], outputs=[custom_topic_txt])
|
| 252 |
+
|
| 253 |
+
# 當在自訂輸入框打字時,清除下拉選單的選擇 (可選,提升體驗)
|
| 254 |
+
def clear_dropdown(custom_text):
|
| 255 |
+
# 只有當 custom_text 有內容時才觸發清空
|
| 256 |
+
if custom_text and custom_text.strip():
|
| 257 |
+
return None # 返回 None 會清除 Dropdown 的選擇
|
| 258 |
+
return gr.Skip() # 如果 custom_text 被清空,不改變 dropdown
|
| 259 |
+
|
| 260 |
+
custom_topic_txt.change(fn=clear_dropdown, inputs=[custom_topic_txt], outputs=[topic_dd])
|
| 261 |
+
|
| 262 |
+
|
| 263 |
if __name__ == "__main__":
|
| 264 |
+
demo.launch(debug=True)
|
|
|
|
|
|