PhilipL's picture
Update app.py
dee93b4 verified
Raw
History Blame Contribute Delete
13.6 kB
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()