File size: 13,620 Bytes
7000866
 
 
 
 
 
 
 
 
 
 
 
fac38aa
7c6ecc9
fac38aa
 
 
7c6ecc9
 
 
7000866
 
71323b9
7000866
 
 
 
71323b9
30e3ce6
7000866
71323b9
0b1a66c
71323b9
7c6ecc9
7000866
7c6ecc9
 
 
7000866
 
71323b9
 
 
 
7000866
 
 
 
 
 
 
 
 
 
71323b9
 
7c6ecc9
71323b9
0b1a66c
7000866
71323b9
d8fab27
30e3ce6
71323b9
d8fab27
30e3ce6
 
 
 
71323b9
 
 
d8fab27
71323b9
7000866
7c6ecc9
 
 
 
 
 
 
 
 
71323b9
 
7000866
 
71323b9
7000866
 
 
 
71323b9
7000866
 
 
 
 
 
 
 
 
 
 
 
 
71323b9
 
7000866
 
71323b9
7c6ecc9
7000866
 
 
 
 
 
 
 
 
71323b9
0b1a66c
71323b9
7000866
0b1a66c
71323b9
 
 
 
 
 
 
 
 
 
 
 
7000866
 
d8fab27
 
7000866
ec0fbd4
7000866
d8fab27
7000866
 
 
 
 
 
 
 
71323b9
 
0b1a66c
71323b9
fac38aa
71323b9
fac38aa
 
7000866
 
71323b9
7000866
 
7c6ecc9
 
71323b9
 
 
7c6ecc9
7000866
7c6ecc9
 
 
ec0fbd4
7000866
71323b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b1a66c
71323b9
 
 
 
 
 
 
ec0fbd4
 
 
7000866
 
 
 
71323b9
fac38aa
 
71323b9
 
 
 
 
fac38aa
71323b9
fac38aa
 
 
 
 
 
 
 
 
 
71323b9
 
fac38aa
71323b9
 
 
 
 
0b1a66c
 
71323b9
fac38aa
71323b9
fac38aa
 
 
 
71323b9
fac38aa
 
71323b9
 
 
 
 
 
 
 
 
 
 
fac38aa
71323b9
 
 
0dc98c5
71323b9
fac38aa
 
dee93b4
7000866
 
 
0b1a66c
 
7000866
ec036ac
7000866
 
fac38aa
7000866
fac38aa
dee93b4
 
39f503c
dee93b4
ec036ac
dee93b4
 
71323b9
dee93b4
 
ec036ac
dee93b4
fac38aa
 
 
 
dee93b4
fac38aa
 
7000866
71323b9
ec0fbd4
71323b9
39f503c
7000866
dee93b4
7000866
 
 
 
 
ec036ac
 
dee93b4
71323b9
 
dee93b4
 
fac38aa
 
dee93b4
7000866
 
0b1a66c
7000866
 
 
 
 
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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_community.document_loaders import DataFrameLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from PIL import Image, ImageFont, ImageDraw
import warnings
import cv2
import numpy as np
from deepface import DeepFace

# 忽略 pandas 的一些警告
warnings.filterwarnings("ignore")

# ==========================================
# 0. 環境變數與 RAG 初始化
# ==========================================
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
HF_TOKEN = os.getenv("HF_TOKEN")

# --- 1. 資料讀取 (主要資料庫 - 使用 restaurants.csv) ---
df = pd.DataFrame()
try:
    # 確保你有 restaurants.csv 在專案根目錄
    df = pd.read_csv('restaurants.csv', engine='python') 
    df.columns = df.columns.str.strip() # 清理欄位空白

    def classify_mood(row):
        name_str = str(row.get('Name', ''))
        rag_str = str(row.get('RAG_Content', ''))
        text = (name_str + " " + rag_str).lower()
        tags = []
        rules = {
            "開心/慶祝": ["牛排", "steak", "pizza", "炸", "雞排", "甜點", "蛋糕", "cake", "冰", "waffle", "吃到飽", "buffet", "burger", "bistro", "餐酒館"],
            "傷心/疲憊": ["粥", "湯", "warm", "congee", "麵", "noodle", "小吃", "comfort food", "豆花", "關東煮", "soup"],
            "生氣/發洩": ["辣", "spicy", "麻辣", "鍋", "curry", "咖哩", "燒肉", "bbq", "臭豆腐", "fry"],
            "平靜/放鬆": ["cafe", "coffee", "tea", "茶", "素食", "vegetable", "早午餐", "brunch", "壽司", "sushi", "居酒屋"]
        }
        for mood, keywords in rules.items():
            for kw in keywords:
                if kw in text:
                    tags.append(mood)
        if not tags: tags.append("隨意/探索")
        return ", ".join(list(set(tags)))
        
    if 'Mood_Tags' not in df.columns:
        df['Mood_Tags'] = df.apply(classify_mood, axis=1)
    
    print("✅ 成功讀取 restaurants.csv 並建立 Mood_Tags")
except Exception as e:
    # 如果找不到檔案或讀取失敗,這裡會提醒
    print(f"⚠️ 讀取 restaurants.csv 失敗: {e}") 

# --- 2. 資料讀取 (Prompt 資料庫) ---
df_prompts = pd.DataFrame()
PROMPT_COL_NAME = 'Visual_prompt' 

try:
    if not df.empty:
        df_prompts = df.copy()
        if 'Name' in df_prompts.columns:
            df_prompts.set_index('Name', inplace=True)
except Exception as e:
    print(f"⚠️ 處理 Prompt 資料發生錯誤: {e}")


# --- RAG 建置 ---
retriever = None
if not df.empty and 'RAG_Content' in df.columns:
    try:
        loader = DataFrameLoader(df, page_content_column="RAG_Content")
        documents = loader.load()
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
        docs = text_splitter.split_documents(documents)
        embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
        vectorstore = FAISS.from_documents(docs, embeddings)
        retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
    except Exception as e:
        print(f"⚠️ RAG 初始化失敗: {e}")

# ==========================================
# 1. 核心功能函式 (餐廳推薦邏輯)
# ==========================================

def get_restaurant_data(mood_score_str, food_choice):
    if df.empty: return None, True
    # 定義分數與 Mood_Tags 的對應關係 (與舊版維持一致)
    score_map = {
        "1 (心情差)": ["傷心/疲憊", "生氣/發洩"],
        "2 (不太好)": ["傷心/疲憊", "生氣/發洩"],
        "3 (普通)": ["平靜/放鬆", "隨意/探索"],
        "4 (不錯)": ["開心/慶祝", "平靜/放鬆"],
        "5 (超棒)": ["開心/慶祝"]
    }
    target_moods = score_map.get(mood_score_str, [])
    food_keyword = ""
    if food_choice == "吃飯": food_keyword = "飯"
    elif food_choice == "吃麵": food_keyword = "麵"

    candidates = df.copy()
    if 'Mood_Tags' not in candidates.columns: return df.sample(1).iloc[0], True

    if target_moods:
        pattern = "|".join(target_moods)
        candidates = candidates[candidates['Mood_Tags'].str.contains(pattern, regex=True, na=False)]
    
    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: return df.sample(1).iloc[0], True
    return candidates.sample(1).iloc[0], False

# 注意:因為移除了測驗,這裡我們用 '無測驗' 作為預設結果
def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score):
    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}
    心情日記:{user_diary}
    【推薦餐廳】
    名稱:{restaurant_name}
    資料:{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

# --- 主邏輯 Agent ---
# 移除了 quiz_state 參數
def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
    # 確保 score_input 有值
    if not score_input:
        yield "⚠️ 請先選擇心情分數,或使用上方相機偵測!", None, ""
        return

    restaurant, is_random = get_restaurant_data(score_input, food_input)
    if restaurant is None:
        yield "資料庫讀取錯誤或為空", None, ""
        return

    name = str(restaurant['Name'])
    url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
    note = "(隨機推薦)" if is_random else ""

    # 1. 處理 RAG
    rag_info = str(restaurant.get('RAG_Content', ''))
    if retriever:
        try:
            docs = retriever.invoke(name)
            if docs: rag_info = "\n".join([d.page_content for d in docs])
        except: pass

    # 2. 準備圖片 Prompt
    img_prompt = f"Delicious food from {name}, cinematic lighting, 8k, photorealistic"
    prompt_source = "⚠️ 預設生成"

    if not df_prompts.empty and PROMPT_COL_NAME in df_prompts.columns:
        if name in df_prompts.index:
            try:
                csv_prompt = df_prompts.loc[name, PROMPT_COL_NAME]
                if isinstance(csv_prompt, pd.Series): csv_prompt = csv_prompt.iloc[0]
                if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
                    img_prompt = str(csv_prompt)
                    prompt_source = "✅ CSV 檔案"
            except: pass

    # 3. 呼叫 LLM (移除了 quiz_state)
    ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input)
    
    # 4. 組合回應
    debug_text = ""
    if debug_mode:
        debug_text = f"\n\n---\n**🛠️ Prompt 來源**: {prompt_source}\n**Prompt**: `{img_prompt}`"
    
    final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}{debug_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

# ==========================================
# 2. 情緒辨識整合模組 (維持不變)
# ==========================================

# 定義中文字典 (繪圖用)
emotion_text_obj = {
    'angry': '生氣', 'disgust': '噁心', 'fear': '害怕',
    'happy': '開心', 'sad': '難過', 'surprise': '驚訝', 'neutral': '正常'
}

# 繪圖函式 (維持不變)
def putText(img, x, y, text, size=50, color=(255, 255, 255)):
    try:
        fontpath = 'NotoSansTC-VariableFont_wght.ttf'
        if not os.path.exists(fontpath): return img
        font = ImageFont.truetype(fontpath, size)
        imgPil = Image.fromarray(img)
        draw = ImageDraw.Draw(imgPil)
        displayText = emotion_text_obj.get(text, text)
        draw.text((x, y), displayText, fill=color, font=font)
        return np.array(imgPil)
    except:
        return img

# 關鍵功能:偵測情緒並回傳「分數選項」 (維持不變)
def detect_emotion_and_map(frame):
    if frame is None:
        return frame, None
    
    detected_emotion = "neutral" 
    mapped_score = "3 (普通)"    

    try:
        # 1. 辨識情緒
        analyze = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
        if isinstance(analyze, list): analyze = analyze[0]
        detected_emotion = analyze['dominant_emotion']
        
        # 2. 畫在圖片上
        frame = putText(frame, 20, 40, detected_emotion)

        # 3. 橋樑:將情緒轉換為餐廳系統的分數
        if detected_emotion == 'happy':
            mapped_score = "5 (超棒)"
        elif detected_emotion == 'surprise':
            mapped_score = "4 (不錯)"
        elif detected_emotion == 'neutral':
            mapped_score = "3 (普通)"
        elif detected_emotion in ['sad', 'fear']:
            mapped_score = "2 (不太好)"
        elif detected_emotion in ['angry', 'disgust']:
            mapped_score = "1 (心情差)"

    except Exception as e:
        print(f"DeepFace Error: {e}")
        pass
    
    return frame, mapped_score

# ==========================================
# 5. Gradio 介面建構 (修改版:鏡頭與結果分離)
# ==========================================

with gr.Blocks(title="AI 心情食堂") as demo:
    
    with gr.Column(visible=True) as main_app_col: 
        gr.Markdown(f"## 🍱 AI 心情食堂導航")
        gr.Markdown("請看著鏡頭,讓 AI 幫你判斷今天的心情分數!")

        with gr.Row():
            # 左側:輸入區
            with gr.Column(scale=1):
                
                # ★ 修改重點:將鏡頭與結果分開顯示 ★
                gr.Markdown("### 📸 步驟 1:心情偵測 (選用)")
                
                # 這裡用 Row 把兩個影像並排 (左邊鏡頭,右邊截圖)
                with gr.Row():
                    # 左邊:永遠是即時鏡頭 (不設為 output)
                    webcam_input = gr.Image(sources=["webcam"], label="即時鏡頭 (請看這裡)", streaming=True)
                    
                    # 右邊:顯示偵測後的「靜態截圖」
                    captured_image = gr.Image(label="偵測結果截圖", interactive=False)
                
                detect_btn = gr.Button("📸 截圖並偵測心情 👇", variant="secondary")
                
                gr.Markdown("### 📝 步驟 2:確認與補充")
                score_input = gr.Radio(
                    ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], 
                    label="1. 心情分數 (AI 會自動填入,也可手動改)", 
                    value="3 (普通)"
                )
                food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
                diary_input = gr.Textbox(lines=3, label="3. 心情日記 (選填)", placeholder="例如:今天被老闆罵了,想吃點好料的...")
                
                debug_mode_btn = gr.Checkbox(label="🔧 顯示 Prompt 除錯資訊", value=False)
                submit_btn = gr.Button("🚀 送出給 Agent", variant="primary")

            # 右側:Agent 輸出區 (維持不變)
            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="地圖導航")

    # Events 
    
    # ★ 修改重點:按鈕點擊後,輸出目標改為 captured_image,不再覆蓋 webcam_input
    detect_btn.click(
        fn=detect_emotion_and_map,
        inputs=[webcam_input],
        outputs=[captured_image, score_input] # 輸出到「右邊的截圖」和「分數選項」
    )

    # 提交按鈕
    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()