File size: 9,214 Bytes
350bd0d
 
a52b880
350bd0d
 
 
 
 
 
 
4110c58
 
 
9dd339c
a52b880
4110c58
a52b880
 
 
4110c58
a52b880
4110c58
a52b880
4110c58
 
350bd0d
 
4110c58
350bd0d
9dd339c
350bd0d
 
9dd339c
a52b880
350bd0d
 
 
 
 
 
 
 
 
a52b880
350bd0d
9dd339c
350bd0d
9dd339c
350bd0d
 
 
 
 
 
 
 
 
 
 
a52b880
9dd339c
350bd0d
a52b880
350bd0d
 
 
 
4110c58
a52b880
 
9dd339c
a52b880
 
 
 
 
 
 
9dd339c
a52b880
350bd0d
a52b880
350bd0d
 
a52b880
350bd0d
a52b880
 
 
9dd339c
a52b880
350bd0d
 
 
9dd339c
8fee980
9dd339c
8fee980
9dd339c
350bd0d
a52b880
350bd0d
a52b880
350bd0d
 
 
8fee980
 
a52b880
350bd0d
 
 
 
 
8fee980
9dd339c
350bd0d
8fee980
a52b880
350bd0d
9dd339c
350bd0d
 
8fee980
9dd339c
8fee980
9dd339c
 
 
350bd0d
8fee980
9dd339c
8fee980
 
a52b880
350bd0d
a52b880
350bd0d
 
 
 
a52b880
350bd0d
 
 
 
8fee980
 
a52b880
350bd0d
8fee980
 
 
 
 
9dd339c
350bd0d
9dd339c
a52b880
4110c58
a52b880
350bd0d
9dd339c
350bd0d
 
a52b880
 
350bd0d
 
 
 
 
 
 
 
 
 
 
 
4110c58
350bd0d
 
a52b880
9dd339c
350bd0d
 
9dd339c
a52b880
 
 
4110c58
a52b880
 
 
9dd339c
a52b880
 
9dd339c
 
a52b880
 
 
 
350bd0d
4110c58
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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)