File size: 11,387 Bytes
dd52ff2
3b344b0
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
 
 
 
 
 
 
0e45878
3b344b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe8a606
3b344b0
 
fe8a606
3b344b0
dd52ff2
3b344b0
 
 
 
 
 
 
 
 
 
dd52ff2
3b344b0
 
 
 
 
 
fe8a606
3b344b0
fe8a606
0e45878
3b344b0
 
 
0e45878
3b344b0
fe8a606
3b344b0
fe8a606
3b344b0
fe8a606
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
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}"
    try:
        response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
        return response.choices[0].message.content
    except Exception as e: return f"Groq Error: {str(e)}"

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, ""; 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])
    ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason, 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
    image_output = generate_image_huggingface(img_prompt)
    yield final_response, image_output, map_html


# ==========================================
# 3. [修正版] 橋接邏輯 (Bridge Functions)
# ==========================================

# 這是 app.py 自己的格式 (注意:這裡是半形括號)
def _score_to_radio_value(score):
    mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
    # 預防萬一 score 是 None 或其他怪怪的值
    try:
        score = int(score)
    except:
        score = 3
    return mapping.get(score, "3 (普通)")

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

# 橋接 2:停止
def bridge_stop_click(st):
    res = emotion.on_stop(st)
    return res[1], res[0], gr.update(visible=False), res[5], res[4]

# 橋接 3:Webcam 串流 (修正分數填入問題)
def bridge_predict_frame(frame, st):
    res = emotion.on_stream(frame, st)
    
    out_cam = res[0]
    out_result = res[1]
    # out_mood = res[2] # ❌ 我們不使用隊友回傳的 UI 更新,因為括號格式不同
    out_st = res[4]
    out_btn_start = res[5]
    
    score_update = gr.update()
    tabs_update = gr.update()
    out_btn_stop = gr.update()

    # 如果辨識完成,我們自己產生一個符合 app.py 格式的 UI 更新
    if out_st.finished and hasattr(out_st, 'final_score'):
        # ✅ 使用 _score_to_radio_value 將整數轉換為半形括號字串
        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:圖片上傳 (修正分數填入問題)
def bridge_predict_upload(img, st):
    res = emotion.on_upload(img, st)
    
    out_result = res[0]
    # out_mood = res[1] # ❌ 同樣忽略隊友的格式
    out_st = res[2]
    
    score_update = gr.update()
    tabs_update = gr.update()

    if out_st.finished and hasattr(out_st, 'final_score'):
        # ✅ 使用 _score_to_radio_value 將整數轉換為半形括號字串
        new_val = _score_to_radio_value(out_st.final_score)
        score_update = gr.update(value=new_val)
        
        tabs_update = gr.Tabs(selected=1)
    
    return out_result, score_update, tabs_update, out_st


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

combined_css = emotion.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("請按「開啟攝影機辨識」或下方上傳照片。"))
                
                gr.Markdown("---")
                gr.Markdown("### 或者:上傳照片")
                upload_img = gr.Image(sources=["upload"], type="numpy", label="上傳照片")

        # Tab 2: 主功能區
        with gr.TabItem("🍽️ AI 心情食堂 (Step 2)", id=1):
            with gr.Column():
                gr.Markdown(f"## 🍱 今天想吃點什麼?")
                with gr.Row():
                    with gr.Column(scale=1):
                        # 这里的 Radio 选项使用的是半形括号
                        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")
                    
                    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"
    )

    upload_img.change(
        fn=bridge_predict_upload,
        inputs=[upload_img, st_state],
        outputs=[result_markdown, score_input, tabs, st_state], 
        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]
    )

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