yiming-0120's picture
Update app.py
4110c58 verified
import gradio as gr
import os
import openai
import time
from pathlib import Path
openai.api_key = os.getenv("OPENAI_API_KEY")
client = openai.OpenAI()
LLM_MODEL = "gpt-4o"
ASR_MODEL = "gpt-4o-transcribe"
TTS_MODEL = "gpt-4o-mini-tts"
TTS_VOICE = "nova"
# --- 預設辯論主題 (台灣時事) ---
CURRENT_TW_TOPICS = [
"台灣是否應提高核能發電比例以應對能源需求?",
"面對高房價,興建社會住宅是最佳解方嗎?",
"台灣是否應放寬代理孕母的限制? (涉及生育權、倫理爭議、法律規範)",
"少子化下,延後退休年齡或引進更多外籍移工哪個更急迫?",
"台灣是否應積極推動雙語教育(中英文)作為主要教學語言?",
"健保制度改革:提高保費、部分負擔,或有其他永續方案?",
"台灣是否應對網路言論進行更嚴格的管制?",
"台灣的死刑制度是否應廢除?",
]
# --- Helper 函數:呼叫 OpenAI API ---
def call_asr(audio_filepath):
if not audio_filepath: return ""
try:
with open(audio_filepath, "rb") as audio_file:
transcript = client.audio.transcriptions.create(model=ASR_MODEL, file=audio_file)
return transcript.text
except Exception as e:
print(f"ASR Error (OpenAI): {e}")
return f"[語音辨識失敗: {e}]"
def call_llm(topic, user_stance, messages):
ai_stance = "反方" if user_stance == "正方" else "正方"
system_prompt = f"你正在參與一場關於「{topic}」的辯論。你扮演的是堅定的「{ai_stance}」。請根據對話歷史,針對使用者的最新論點,提出具有批判性、質疑性或反駁性的回應。保持簡潔有力,專注於論證,字數控制在150字以內。"
openai_messages = [{"role": "system", "content": system_prompt}]
for msg in messages:
role = msg.get("role") if msg.get("role") in ["user", "assistant"] else "user"
content = msg.get("content", "")
if content: openai_messages.append({"role": role, "content": content})
try:
response = client.chat.completions.create(model=LLM_MODEL, messages=openai_messages, max_tokens=250, temperature=0.7)
ai_response = response.choices[0].message.content.strip()
return ai_response
except Exception as e:
print(f"LLM Error (OpenAI): {e}")
return f"[AI 回應生成失敗: {e}]"
def call_tts(text):
try:
if not text or not isinstance(text, str) or text.startswith("["):
print(f"Skipping TTS for invalid text: {text}")
return None
speech_file_path = Path(f"/tmp/speech_{int(time.time() * 1000)}.mp3")
response = client.audio.speech.create(model=TTS_MODEL, voice=TTS_VOICE, input=text)
response.stream_to_file(speech_file_path)
return str(speech_file_path)
except Exception as e:
print(f"TTS Error (OpenAI): {e}")
return None
# --- Gradio 主函數 ---
def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text, user_input_audio, history):
final_topic = ""
if custom_topic and custom_topic.strip():
final_topic = custom_topic.strip()
print(f"Using custom topic: {final_topic}")
elif topic_from_dropdown:
final_topic = topic_from_dropdown
print(f"Using dropdown topic: {final_topic}")
else:
history.append(("[錯誤:請選擇或輸入一個辯論主題]", None))
return history, None, "", ""
user_text = ""
processed_audio_path = None
if user_input_audio:
print(f"Processing audio input: {user_input_audio}")
processed_audio_path = user_input_audio
user_text = call_asr(processed_audio_path)
history.append(((processed_audio_path,), None))
if user_text.startswith("["):
history.append((user_text, None))
return history, None, "", final_topic
if not user_text or user_text.startswith("["):
if user_input_text:
user_text = user_input_text
if not processed_audio_path:
history.append((user_text, None))
elif user_input_text:
user_text = user_input_text
history.append((f"(改用文字輸入: {user_text})", None))
else:
if not processed_audio_path:
history.append(("[錯誤:請提供文字或語音論點]", None))
return history, None, "", final_topic
if not isinstance(user_text, str) or user_text.startswith("["):
print("Invalid user text, stopping turn.")
if history and isinstance(history[-1][0], tuple) and history[-1][1] is None:
history.append((f"[無法處理用戶輸入: {user_text}]", None))
return history, None, "", final_topic
llm_messages = []
for i, turn in enumerate(history):
user_msg, ai_msg = turn
user_content = None
if isinstance(user_msg, str):
if not user_msg.startswith("[") and not user_msg.startswith("(改用文字輸入:") and not user_msg.startswith("(語音辨識結果:"):
user_content = user_msg
elif isinstance(user_msg, tuple):
if i == len(history) - 1 and not user_text.startswith("["):
user_content = user_text
if user_content: llm_messages.append({"role": "user", "content": user_content})
ai_content = None
if isinstance(ai_msg, str):
if not ai_msg.startswith("["): ai_content = ai_msg
elif isinstance(ai_msg, tuple) and len(ai_msg) > 0:
if isinstance(ai_msg[0], str) and ai_msg[0].endswith(".mp3"): pass
elif isinstance(ai_msg[0], str) and not ai_msg[0].startswith("["): ai_content = ai_msg[0]
if ai_content: llm_messages.append({"role": "assistant", "content": ai_content})
if not llm_messages or llm_messages[-1]["role"] == "assistant":
if not user_text.startswith("["): llm_messages.append({"role": "user", "content": user_text})
else:
print("Skipping LLM call due to invalid final user text.")
return history, None, "", final_topic
ai_response_text = call_llm(final_topic, user_stance, llm_messages)
ai_response_audio_path = call_tts(ai_response_text)
last_user_turn_index = -1
for i in range(len(history) - 1, -1, -1):
if history[i][1] is None and not history[i][0] is None:
last_user_turn_index = i
break
if last_user_turn_index != -1:
history[last_user_turn_index] = (history[last_user_turn_index][0], ai_response_text)
if ai_response_audio_path and not ai_response_text.startswith("["):
history.append((None, (ai_response_audio_path,)))
else:
print("Warning: Could not find user's turn. Appending AI response.")
history.append(("[用戶回合丟失?]", ai_response_text))
if ai_response_audio_path and not ai_response_text.startswith("["):
history.append((None, (ai_response_audio_path,)))
return history, None, "", final_topic
# --- Gradio UI (與上一版相同) ---
with gr.Blocks(theme=gr.themes.Soft(), title="時事觀點對對碰 (OpenAI + 自訂主題)") as demo:
gr.Markdown("## 🗣️ 時事觀點對對碰")
gr.Markdown("選擇預設議題或輸入自訂議題,選擇立場,用文字或語音提出論點,AI 將扮演對手與你辯論!")
chat_history = gr.State([])
with gr.Row():
topic_dd = gr.Dropdown(CURRENT_TW_TOPICS, label="選擇預設辯論主題", value=CURRENT_TW_TOPICS[0])
custom_topic_txt = gr.Textbox(label="或輸入自訂辯論主題", placeholder="若此處輸入,將優先使用此主題...")
stance_radio = gr.Radio(["正方", "反方"], label="選擇你的立場", value="正方")
chatbot_ui = gr.Chatbot(label="辯論區", height=500, render_markdown=True, bubble_full_width=False)
with gr.Row():
with gr.Column(scale=7):
user_txt = gr.Textbox(label="輸入你的論點 (文字)", placeholder="在此輸入文字...")
with gr.Column(scale=3):
user_audio = gr.Audio(sources=["microphone"], type="filepath", label="或錄製你的論點 (語音)")
submit_btn = gr.Button("送出論點", variant="primary")
# --- 事件綁定 ---
submit_btn.click(
fn=debate_turn,
inputs=[topic_dd, custom_topic_txt, stance_radio, user_txt, user_audio, chatbot_ui],
outputs=[chatbot_ui, user_audio, user_txt, custom_topic_txt]
)
# 當選擇下拉選單時,清空自訂輸入框
def clear_custom_topic(dropdown_value):
if dropdown_value:
return ""
return gr.skip()
topic_dd.change(fn=clear_custom_topic, inputs=[topic_dd], outputs=[custom_topic_txt])
# 當在自訂輸入框打字時,清除下拉選單的選擇
def clear_dropdown(custom_text):
if custom_text and custom_text.strip():
return None
return gr.skip() # Corrected: Use lowercase 's'
custom_topic_txt.change(fn=clear_dropdown, inputs=[custom_topic_txt], outputs=[topic_dd])
if __name__ == "__main__":
demo.launch(debug=True)