File size: 12,315 Bytes
dd52ff2
3b344b0
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce5c31a
3b344b0
 
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
ce5c31a
476dbfc
3b344b0
476dbfc
3b344b0
 
476dbfc
ce5c31a
 
 
 
 
 
 
 
 
 
 
 
3b344b0
 
476dbfc
ce5c31a
 
 
3b344b0
 
 
 
 
 
 
 
 
 
ce5c31a
 
3b344b0
ce5c31a
3b344b0
 
 
 
476dbfc
ce5c31a
 
476dbfc
ce5c31a
 
476dbfc
ce5c31a
 
 
 
 
 
 
 
 
476dbfc
3b344b0
 
476dbfc
ce5c31a
 
 
 
3b344b0
ce5c31a
3b344b0
 
 
476dbfc
3b344b0
 
 
 
476dbfc
 
3b344b0
 
 
 
 
 
 
 
 
 
 
 
476dbfc
 
3b344b0
 
 
 
 
 
fe8a606
3b344b0
dd52ff2
0e45878
3b344b0
 
 
0e45878
c644f84
6d8dfcc
c644f84
 
 
 
 
 
 
b4537ab
c644f84
 
 
 
b4537ab
6d8dfcc
c644f84
b4537ab
c644f84
 
 
 
6d8dfcc
 
c644f84
 
 
a0f68f2
 
c644f84
b4537ab
 
a0f68f2
 
 
6d8dfcc
 
fe8a606
3b344b0
fe8a606
3b344b0
fe8a606
3b344b0
 
 
 
 
 
 
 
 
 
 
 
7ad1213
3b344b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce5c31a
3b344b0
ce5c31a
 
 
 
3b344b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce5c31a
3b344b0
0e45878
 
3b344b0
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import gradio as gr
from openai import OpenAI
from huggingface_hub import InferenceClient
import os
import time
import requests
import urllib.parse
import pandas as pd
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from PIL import Image
from dotenv import load_dotenv

# --- 匯入組員的模組 ---
import emotion 
# ---------------------------

load_dotenv()

# ==========================================
# 0. 環境變數 & 1. 系統初始化 (保持不變)
# ==========================================
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
HF_TOKEN = os.getenv("HF_TOKEN")

global_df = None
global_mood_df = None
global_retriever = None
rag_initialized = False

def init_rag_system():
    global global_df, global_mood_df, global_retriever, rag_initialized
    if rag_initialized: return
    try:
        global_df = pd.read_csv('restaurants.csv')
        global_df['RAG_Content'] = global_df['RAG_Content'].fillna("")
        global_df['Category'] = global_df['Category'].fillna("其他")
        global_mood_df = pd.read_csv('mood_food_guide.csv')
    except Exception: pass
    if os.path.exists("faiss_index"):
        try:
            embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
            vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)
            global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
        except Exception: pass
    rag_initialized = True

# ==========================================
# 2. 核心功能 (修改:回傳分開的除錯資訊)
# ==========================================
def get_restaurant_data(mood_score_str, food_choice):
    init_rag_system()
    if global_df is None or global_df.empty: return None, True, "資料庫未載入", "無建議"
    try: score = int(str(mood_score_str).split(' ')[0])
    except: score = 3
    mood_info = global_mood_df[global_mood_df['分數'] == score]
    if not mood_info.empty:
        rec_categories = mood_info.iloc[0]['推薦料理類別']
        mood_reason = mood_info.iloc[0]['原因']
    else:
        rec_categories = ""
        mood_reason = "隨意探索"
    candidates = global_df.copy()
    if rec_categories:
        candidates = candidates[candidates['Category'].apply(lambda x: str(x) in str(rec_categories) or str(rec_categories) in str(x))]
    food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
    if food_keyword:
        candidates = candidates[candidates['Name'].str.contains(food_keyword, case=False, na=False) | candidates['RAG_Content'].str.contains(food_keyword, case=False, na=False)]
    if candidates.empty:
        result = global_df.sample(1).iloc[0]; is_random = True
    else:
        result = candidates.sample(1).iloc[0]; is_random = False
    return result, is_random, rec_categories, mood_reason

def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason, debug_mode=False):
    if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY", ""
    
    client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
    
    system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友..."
    user_msg = f"【狀態】心情分數:{mood_score}\n【日記】{user_diary}\n【建議原因】{mood_guide_reason}\n【餐廳】{restaurant_name}\n資料:{restaurant_detail}"
    
    # 準備除錯資訊
    debug_log = ""
    if debug_mode:
        debug_log = f"""
### 🔧 Groq Prompt Debug
**System Prompt:**
{system_prompt}

**User Message:**
{user_msg}
"""

    try:
        response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
        content = response.choices[0].message.content
        return content, debug_log # 回傳 (內容, 除錯資訊)
    except Exception as e: 
        return f"Groq Error: {str(e)}", debug_log

def generate_image_huggingface(prompt):
    if not HF_TOKEN: return None
    try:
        hf_client = InferenceClient(token=HF_TOKEN)
        return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
    except: return None

def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
    restaurant, is_random, rec_categories, mood_reason = get_restaurant_data(score_input, food_input)
    if restaurant is None: yield "資料庫讀取錯誤", None, "", gr.update(); return
    
    name = restaurant['Name']; address = restaurant['Address']; url = restaurant['URL']; img_prompt = restaurant.get('Visual_prompt')
    
    rag_info = str(restaurant.get('RAG_Content', ''))
    if global_retriever:
        docs = global_retriever.invoke(name)
        if docs: rag_info = "\n".join([d.page_content for d in docs])
        
    # 呼叫 Groq 生成內容與取得除錯資訊
    ai_text, groq_debug_log = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason, debug_mode)
    
    # 準備完整的除錯資訊 (包含圖片 Prompt)
    full_debug_log = ""
    if debug_mode:
        img_debug_log = f"""
### 🎨 Image Prompt Debug
**Visual Prompt:**
{img_prompt}
"""
        full_debug_log = groq_debug_log + "\n\n" + img_debug_log

    # 設定除錯視窗的狀態 (有勾選才顯示)
    debug_output_update = gr.update(value=full_debug_log, visible=debug_mode)

    final_response = f"### 🍽️ 推薦:{name}\n\n{ai_text}"
    map_html = f'<div style="text-align:center"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:8px 16px;border-radius:20px;text-decoration:none">🗺️ Google Map 導航</a></div>'
    
    # 先回傳文字與除錯資訊
    yield final_response, None, map_html, debug_output_update
    
    # 再回傳圖片
    image_output = generate_image_huggingface(img_prompt)
    yield final_response, image_output, map_html, debug_output_update


# ==========================================
# 3. 橋接邏輯 (Bridge Functions)
# ==========================================

def _score_to_radio_value(score):
    mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
    try: score = int(score)
    except: score = 3
    return mapping.get(score, "3 (普通)")

def bridge_start_click(st):
    res = emotion.on_restart(st)
    return res[1], res[0], gr.update(visible=True), res[5], res[4]

def bridge_stop_click(st):
    res = emotion.on_stop(st)
    return res[1], res[0], gr.update(visible=False), res[5], res[4]

def bridge_predict_frame(frame, st):
    res = emotion.on_stream(frame, st)
    out_cam = res[0]; out_result = res[1]; out_st = res[4]; out_btn_start = res[5]
    score_update = gr.update(); tabs_update = gr.update(); out_btn_stop = gr.update()

    if out_st.finished and hasattr(out_st, 'final_score'):
        new_val = _score_to_radio_value(out_st.final_score)
        score_update = gr.update(value=new_val)
        tabs_update = gr.Tabs(selected=1)
        out_btn_stop = gr.update(visible=False)
        
    return out_cam, out_result, out_st, out_btn_stop, out_btn_start, score_update, tabs_update


# ==========================================
# 4. Gradio 介面建構
# ==========================================

# 定義強制黑色文字與 Tab 修復的 CSS
force_black_css = """
/* 1. 針對 Tab 區域的背景容器:設為淺灰,確保黑字能被看見 */
.tab-nav {
    background-color: #e2e8f0 !important;
    border-radius: 8px 8px 0 0 !important;
}

/* 2. 【關鍵修正】針對「未選中」的 Tab 按鈕 */
.tab-nav button {
    color: #000000 !important;       /* 強制文字黑色 */
    background-color: transparent !important; /* 背景透明,透出下方的淺灰 */
    opacity: 1 !important;           /* 防止透明度造成看不清 */
    text-shadow: none !important;    /* 移除陰影 */
}

/* 3. 針對「已選中」的 Tab 按鈕 (圖片中左邊那個) */
.tab-nav button.selected {
    color: #000000 !important;       /* 文字黑色 (如果你想保留橘色,改成 #ff7f00) */
    background-color: #ffffff !important; /* 背景純白,做出區隔 */
    font-weight: bold !important;    /* 加粗 */
    border-bottom: 2px solid #ff7f00 !important; /* 底部橘線 */
}

/* 4. 強制 Tab 內的 Icon/Emoji 也變色 */
.tab-nav button span {
    color: inherit !important;
}

/* 5. 確保內容的大標題也是黑色 (H1-H3) */
h1, h2, h3, h4, h5, h6, .prose h1, .prose h2, .prose h3 {
    color: #000000 !important;
}
"""

# 合併原本的 CSS 與 新的強制黑色 CSS
combined_css = emotion.css + force_black_css

with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
    
    st_state = gr.State(emotion.AppState())

    with gr.Tabs() as tabs:
        
        # Tab 1: 情緒辨識
        with gr.TabItem("😊 情緒辨識 (Step 1)", id=0):
            with gr.Column(elem_id="app_container"):
                gr.Markdown("### 第一步:測測你的心情能量\n讓 AI 看看你的表情,自動幫你決定心情分數!(辨識完畢會自動跳轉)")
                
                with gr.Row():
                    btn_start = gr.Button("📸 開啟攝影機辨識", variant="primary")
                    btn_stop  = gr.Button("⏹️ 停止", variant="secondary", visible=False)

                cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
                result_markdown = gr.Markdown(emotion._hint_html("請按「開啟攝影機辨識」並允許瀏覽器使用相機。"))

        # Tab 2: 主功能區
        with gr.TabItem("🍽️ AI 心情食堂 (Step 2)", id=1):
            with gr.Column():
                gr.Markdown(f"## 🍱 今天想吃點什麼?")
                with gr.Row():
                    with gr.Column(scale=1):
                        score_input = gr.Radio(
                            ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], 
                            label="1. 心情分數 (由 Tab 1 自動填入)", 
                            value="3 (普通)"
                        )
                        food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
                        diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天發生的事...")
                        debug_mode_btn = gr.Checkbox(label="🔧 開啟除錯模式", value=False)
                        
                        submit_btn = gr.Button("🍱 送出給 Agent", variant="primary")
                        
                        # --- [新增] 除錯資訊顯示區 (位於按鈕下方) ---
                        debug_output = gr.Markdown(label="除錯資訊 (Debug Log)", visible=False)
                        # ----------------------------------------
                    
                    with gr.Column(scale=1):
                        agent_output = gr.Markdown(label="AI 回應")
                        image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
                        map_output = gr.HTML(label="地圖導航")

    # ==========================================
    # 事件綁定
    # ==========================================

    btn_start.click(
        fn=bridge_start_click,
        inputs=[st_state],
        outputs=[result_markdown, cam, btn_stop, btn_start, st_state],
        show_progress="minimal"
    )

    btn_stop.click(
        fn=bridge_stop_click,
        inputs=[st_state],
        outputs=[result_markdown, cam, btn_stop, btn_start, st_state],
        show_progress="minimal"
    )

    cam.stream(
        fn=bridge_predict_frame,
        inputs=[cam, st_state],
        outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs], 
        show_progress="minimal"
    )

    submit_btn.click(
        fn=mood_agent_logic,
        inputs=[score_input, food_input, diary_input, debug_mode_btn],
        outputs=[agent_output, image_output, map_output, debug_output] # 加入 debug_output
    )

if __name__ == "__main__":
    demo.launch(ssr_mode=False)