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() |