yiming-0120 commited on
Commit
9dd339c
·
verified ·
1 Parent(s): a52b880

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -87
app.py CHANGED
@@ -16,7 +16,7 @@ client = openai.OpenAI()
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 = [
@@ -26,45 +26,29 @@ CURRENT_TW_TOPICS = [
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}]"
47
 
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:
62
- response = client.chat.completions.create(
63
- model=LLM_MODEL,
64
- messages=openai_messages,
65
- max_tokens=250,
66
- temperature=0.7,
67
- )
68
  ai_response = response.choices[0].message.content.strip()
69
  return ai_response
70
  except Exception as e:
@@ -72,45 +56,33 @@ def call_llm(topic, user_stance, messages):
72
  return f"[AI 回應生成失敗: {e}]"
73
 
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
@@ -118,20 +90,19 @@ def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text,
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("["):
@@ -140,51 +111,35 @@ def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text,
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})
159
 
160
  ai_content = None
161
  if isinstance(ai_msg, str):
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})
172
 
173
- # 確保最後是 user message
174
  if not llm_messages or llm_messages[-1]["role"] == "assistant":
175
- if not user_text.startswith("["):
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:
@@ -192,9 +147,7 @@ def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text,
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:
@@ -203,23 +156,18 @@ def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_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)
@@ -232,33 +180,29 @@ with gr.Blocks(theme=gr.themes.Soft(), title="時事觀點對對碰 (OpenAI +
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)
 
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 = [
 
26
  "少子化下,延後退休年齡或引進更多外籍移工哪個更急迫?",
27
  "改善台灣交通亂象,應優先加強執法還是改善道路設計?",
28
  "健保制度改革:提高保費、部分負擔,或有其他永續方案?",
 
29
  ]
30
 
31
  # --- Helper 函數:呼叫 OpenAI API (call_asr, call_llm, call_tts 保持不變) ---
32
  def call_asr(audio_filepath):
33
+ if not audio_filepath: return ""
 
 
34
  try:
35
  with open(audio_filepath, "rb") as audio_file:
36
+ transcript = client.audio.transcriptions.create(model=ASR_MODEL, file=audio_file)
 
 
 
37
  return transcript.text
38
  except Exception as e:
39
  print(f"ASR Error (OpenAI): {e}")
40
  return f"[語音辨識失敗: {e}]"
41
 
42
  def call_llm(topic, user_stance, messages):
 
43
  ai_stance = "反方" if user_stance == "正方" else "正方"
 
44
  system_prompt = f"你正在參與一場關於「{topic}」的辯論。你扮演的是堅定的「{ai_stance}」。請根據對話歷史,針對使用者的最新論點,提出具有批判性、質疑性或反駁性的回應。保持簡潔有力,專注於論證,字數控制在150字以內。"
 
45
  openai_messages = [{"role": "system", "content": system_prompt}]
46
  for msg in messages:
47
  role = msg.get("role") if msg.get("role") in ["user", "assistant"] else "user"
48
  content = msg.get("content", "")
49
+ if content: openai_messages.append({"role": role, "content": content})
 
 
50
  try:
51
+ response = client.chat.completions.create(model=LLM_MODEL, messages=openai_messages, max_tokens=250, temperature=0.7)
 
 
 
 
 
52
  ai_response = response.choices[0].message.content.strip()
53
  return ai_response
54
  except Exception as e:
 
56
  return f"[AI 回應生成失敗: {e}]"
57
 
58
  def call_tts(text):
 
59
  try:
60
  if not text or not isinstance(text, str) or text.startswith("["):
61
  print(f"Skipping TTS for invalid text: {text}")
62
  return None
63
  speech_file_path = Path(f"/tmp/speech_{int(time.time() * 1000)}.mp3")
64
+ response = client.audio.speech.create(model=TTS_MODEL, voice=TTS_VOICE, input=text)
 
 
 
 
 
65
  response.stream_to_file(speech_file_path)
66
  return str(speech_file_path)
67
  except Exception as e:
68
  print(f"TTS Error (OpenAI): {e}")
69
  return None
70
 
71
+ # --- Gradio 主函數 (與上一版相同) ---
72
  def debate_turn(topic_from_dropdown, custom_topic, user_stance, user_input_text, user_input_audio, history):
 
 
 
73
  final_topic = ""
74
+ if custom_topic and custom_topic.strip():
75
  final_topic = custom_topic.strip()
76
  print(f"Using custom topic: {final_topic}")
77
  elif topic_from_dropdown:
78
  final_topic = topic_from_dropdown
79
  print(f"Using dropdown topic: {final_topic}")
80
  else:
 
81
  history.append(("[錯誤:請選擇或輸入一個辯論主題]", None))
82
+ return history, None, "", ""
83
 
 
84
  user_text = ""
85
  processed_audio_path = None
 
86
  if user_input_audio:
87
  print(f"Processing audio input: {user_input_audio}")
88
  processed_audio_path = user_input_audio
 
90
  history.append(((processed_audio_path,), None))
91
  if user_text.startswith("["):
92
  history.append((user_text, None))
93
+ return history, None, "", final_topic
94
 
95
  if not user_text or user_text.startswith("["):
96
  if user_input_text:
97
  user_text = user_input_text
98
+ if not processed_audio_path:
99
  history.append((user_text, None))
100
+ elif user_input_text:
101
  user_text = user_input_text
102
+ history.append((f"(改用文字輸入: {user_text})", None))
103
  else:
104
  if not processed_audio_path:
105
  history.append(("[錯誤:請提供文字或語音論點]", None))
 
106
  return history, None, "", final_topic
107
 
108
  if not isinstance(user_text, str) or user_text.startswith("["):
 
111
  history.append((f"[無法處理用戶輸入: {user_text}]", None))
112
  return history, None, "", final_topic
113
 
 
114
  llm_messages = []
 
115
  for i, turn in enumerate(history):
116
  user_msg, ai_msg = turn
117
  user_content = None
118
  if isinstance(user_msg, str):
119
+ if not user_msg.startswith("[") and not user_msg.startswith("(改用文字輸入:") and not user_msg.startswith("(語音辨識結果:"):
120
  user_content = user_msg
121
  elif isinstance(user_msg, tuple):
 
122
  if i == len(history) - 1 and not user_text.startswith("["):
123
  user_content = user_text
124
+ if user_content: llm_messages.append({"role": "user", "content": user_content})
 
 
125
 
126
  ai_content = None
127
  if isinstance(ai_msg, str):
128
+ if not ai_msg.startswith("["): ai_content = ai_msg
 
129
  elif isinstance(ai_msg, tuple) and len(ai_msg) > 0:
130
+ if isinstance(ai_msg[0], str) and ai_msg[0].endswith(".mp3"): pass
131
+ elif isinstance(ai_msg[0], str) and not ai_msg[0].startswith("["): ai_content = ai_msg[0]
132
+ if ai_content: llm_messages.append({"role": "assistant", "content": ai_content})
 
 
 
 
133
 
 
134
  if not llm_messages or llm_messages[-1]["role"] == "assistant":
135
+ if not user_text.startswith("["): llm_messages.append({"role": "user", "content": user_text})
 
136
  else:
137
  print("Skipping LLM call due to invalid final user text.")
138
  return history, None, "", final_topic
139
 
 
140
  ai_response_text = call_llm(final_topic, user_stance, llm_messages)
 
 
141
  ai_response_audio_path = call_tts(ai_response_text)
142
 
 
143
  last_user_turn_index = -1
144
  for i in range(len(history) - 1, -1, -1):
145
  if history[i][1] is None and not history[i][0] is None:
 
147
  break
148
 
149
  if last_user_turn_index != -1:
 
150
  history[last_user_turn_index] = (history[last_user_turn_index][0], ai_response_text)
 
151
  if ai_response_audio_path and not ai_response_text.startswith("["):
152
  history.append((None, (ai_response_audio_path,)))
153
  else:
 
156
  if ai_response_audio_path and not ai_response_text.startswith("["):
157
  history.append((None, (ai_response_audio_path,)))
158
 
159
+ return history, None, "", final_topic
 
 
160
 
161
+ # --- Gradio UI (與上一版相同) ---
162
  with gr.Blocks(theme=gr.themes.Soft(), title="時事觀點對對碰 (OpenAI + 自訂主題)") as demo:
163
  gr.Markdown("## 🗣️ 時事觀點對對碰 (OpenAI + 自訂主題)")
164
  gr.Markdown("選擇預設議題或輸入自訂議題,選擇立場,用文字或語音提出論點,AI 將扮演對手與你辯論!")
165
 
166
+ chat_history = gr.State([])
167
 
168
  with gr.Row():
 
169
  topic_dd = gr.Dropdown(CURRENT_TW_TOPICS, label="選擇預設辯論主題", value=CURRENT_TW_TOPICS[0])
 
170
  custom_topic_txt = gr.Textbox(label="或輸入自訂辯論主題", placeholder="若此處輸入,將優先使用此主題...")
 
171
  stance_radio = gr.Radio(["正方", "反方"], label="選擇你的立場", value="正方")
172
 
173
  chatbot_ui = gr.Chatbot(label="辯論區", height=500, render_markdown=True, bubble_full_width=False)
 
180
 
181
  submit_btn = gr.Button("送出論點", variant="primary")
182
 
183
+ # --- 事件綁定 (與上一版相同,但函數內部已修正 gr.skip) ---
184
  submit_btn.click(
185
  fn=debate_turn,
 
186
  inputs=[topic_dd, custom_topic_txt, stance_radio, user_txt, user_audio, chatbot_ui],
187
+ outputs=[chatbot_ui, user_audio, user_txt, custom_topic_txt]
 
188
  )
189
 
190
+ # 當選擇下拉選單時,清空自訂輸入框
191
  def clear_custom_topic(dropdown_value):
 
192
  if dropdown_value:
193
  return ""
194
+ return gr.skip() # Corrected: Use lowercase 's'
195
 
196
  topic_dd.change(fn=clear_custom_topic, inputs=[topic_dd], outputs=[custom_topic_txt])
197
 
198
+ # 當在自訂輸入框打字時,清除下拉選單的選擇
199
  def clear_dropdown(custom_text):
 
200
  if custom_text and custom_text.strip():
201
+ return None
202
+ return gr.skip() # Corrected: Use lowercase 's'
203
 
204
  custom_topic_txt.change(fn=clear_dropdown, inputs=[custom_topic_txt], outputs=[topic_dd])
205
 
206
 
207
  if __name__ == "__main__":
208
+ demo.launch(debug=True) # debug=True is helpful