Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,6 +11,7 @@ from dotenv import load_dotenv
|
|
| 11 |
|
| 12 |
# --- 1. 環境變數 & Playwright 初始化 ---
|
| 13 |
load_dotenv()
|
|
|
|
| 14 |
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
|
| 15 |
|
| 16 |
try:
|
|
@@ -27,18 +28,17 @@ from langchain_huggingface import HuggingFaceEmbeddings
|
|
| 27 |
from langchain_community.vectorstores import FAISS
|
| 28 |
from openai import OpenAI
|
| 29 |
|
| 30 |
-
#
|
| 31 |
try:
|
| 32 |
import emotion
|
| 33 |
except ImportError:
|
| 34 |
-
|
| 35 |
-
print("警告:找不到 emotion 模組,請確保該檔案存在。")
|
| 36 |
|
| 37 |
# ==========================================
|
| 38 |
# 0. 全域設定
|
| 39 |
# ==========================================
|
| 40 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 41 |
-
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
|
| 42 |
|
| 43 |
global_df = None
|
| 44 |
global_mood_df = None
|
|
@@ -58,7 +58,7 @@ def init_static_data():
|
|
| 58 |
})
|
| 59 |
|
| 60 |
# ==========================================
|
| 61 |
-
# 1. Google Maps 爬蟲
|
| 62 |
# ==========================================
|
| 63 |
def sync_google_maps(url):
|
| 64 |
global global_df, global_retriever, rag_initialized
|
|
@@ -74,11 +74,11 @@ def sync_google_maps(url):
|
|
| 74 |
page = browser.new_page()
|
| 75 |
yield "🌐 [2/4] 連線中..."
|
| 76 |
page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
|
| 77 |
-
yield "⏳ [3/4] 等待加載
|
| 78 |
time.sleep(10)
|
| 79 |
yield "📄 [4/4] 解析餐廳資訊..."
|
| 80 |
|
| 81 |
-
#
|
| 82 |
titles = page.locator('div.fontHeadlineSmall').all_inner_texts()
|
| 83 |
details = page.locator('div.fontBodyMedium').all_inner_texts()
|
| 84 |
|
|
@@ -86,7 +86,6 @@ def sync_google_maps(url):
|
|
| 86 |
for i, name in enumerate(titles):
|
| 87 |
name = name.strip()
|
| 88 |
if name:
|
| 89 |
-
# 擷取地址或評價資訊
|
| 90 |
addr = details[i].strip() if i < len(details) else ""
|
| 91 |
restaurant_list.append({
|
| 92 |
"Name": name,
|
|
@@ -97,7 +96,7 @@ def sync_google_maps(url):
|
|
| 97 |
browser.close()
|
| 98 |
|
| 99 |
if not restaurant_list:
|
| 100 |
-
yield "⚠️ 找不到餐廳。請確認連結
|
| 101 |
return
|
| 102 |
|
| 103 |
global_df = pd.DataFrame(restaurant_list)
|
|
@@ -110,15 +109,14 @@ def sync_google_maps(url):
|
|
| 110 |
yield f"❌ 系統錯誤:{str(e)}"
|
| 111 |
|
| 112 |
# ==========================================
|
| 113 |
-
# 2. 推薦邏輯與 Embed API
|
| 114 |
# ==========================================
|
| 115 |
def get_restaurant_data(mood_score_str, food_choice):
|
| 116 |
init_static_data()
|
| 117 |
if global_df is None or global_df.empty:
|
| 118 |
return None, True, "資料庫未載入", "尚未同步地圖資料", ""
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
try: score = int(str(mood_score_str).split(' ')[0])
|
| 123 |
except: score = 3
|
| 124 |
|
|
@@ -137,62 +135,55 @@ def get_restaurant_data(mood_score_str, food_choice):
|
|
| 137 |
is_match = False
|
| 138 |
|
| 139 |
result = candidates.sample(1).iloc[0]
|
| 140 |
-
return result, is_match, "個人清單", mood_reason,
|
| 141 |
|
| 142 |
def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason):
|
| 143 |
-
if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
|
| 144 |
client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
|
| 145 |
-
system_prompt = "你是一個幽默
|
| 146 |
-
user_msg = f"
|
| 147 |
try:
|
| 148 |
response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
|
| 149 |
return response.choices[0].message.content
|
| 150 |
-
except Exception as e: return f"
|
| 151 |
|
| 152 |
-
def mood_agent_logic(score_input, food_input, diary_input
|
| 153 |
if not rag_initialized:
|
| 154 |
-
yield "⚠️ 請先
|
| 155 |
return
|
| 156 |
|
| 157 |
restaurant, is_match, _, mood_reason, all_names = get_restaurant_data(score_input, food_input)
|
| 158 |
|
| 159 |
prefix = ""
|
| 160 |
if not is_match and food_input != "隨便":
|
| 161 |
-
prefix = f"> 💡 **溫馨提示**:清單中暫無『{food_input}』,已從名單
|
| 162 |
|
| 163 |
name = restaurant['Name']
|
| 164 |
addr = restaurant.get('Address', '')
|
| 165 |
url = restaurant.get('URL', '#')
|
| 166 |
-
rag_info = str(restaurant.get('RAG_Content', ''))
|
| 167 |
|
| 168 |
-
|
| 169 |
-
ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason)
|
| 170 |
final_markdown = f"{prefix}### 🍽️ 今日推薦:{name}\n\n{ai_text}"
|
| 171 |
|
| 172 |
-
#
|
| 173 |
-
# query 參數包含 名稱 與 地址 以提高準確度
|
| 174 |
query_str = urllib.parse.quote(f"{name} {addr}")
|
| 175 |
if GOOGLE_MAPS_API_KEY:
|
| 176 |
embed_html = f"""
|
| 177 |
-
<div style="border-radius:
|
| 178 |
-
<iframe
|
| 179 |
-
width="100%"
|
| 180 |
-
height="350"
|
| 181 |
-
frameborder="0" style="border:0"
|
| 182 |
src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={query_str}"
|
| 183 |
-
allowfullscreen>
|
| 184 |
-
</iframe>
|
| 185 |
</div>
|
| 186 |
"""
|
| 187 |
else:
|
| 188 |
-
embed_html = "<p style='color:red;'>⚠️
|
| 189 |
|
| 190 |
-
nav_button = f'<div style="text-align:center; margin-top:10px;"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:10px 20px;border-radius:25px;text-decoration:none;font-weight:bold;">📍
|
| 191 |
|
| 192 |
yield final_markdown, embed_html, nav_button
|
| 193 |
|
| 194 |
# ==========================================
|
| 195 |
-
# 3. 橋接情緒辨識
|
| 196 |
# ==========================================
|
| 197 |
def _score_to_radio_value(score):
|
| 198 |
mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
|
|
@@ -228,48 +219,49 @@ def bridge_predict_upload(img, st):
|
|
| 228 |
return out_result, score_update, tabs_update, out_st
|
| 229 |
|
| 230 |
# ==========================================
|
| 231 |
-
# 4. Gradio 介面
|
| 232 |
# ==========================================
|
| 233 |
with gr.Blocks(title="AI 心情食堂", css=getattr(emotion, 'css', '')) as demo:
|
| 234 |
st_state = gr.State(emotion.AppState())
|
| 235 |
|
| 236 |
with gr.Tabs() as tabs:
|
| 237 |
-
#
|
| 238 |
with gr.TabItem("😊 第一步:測測心情", id=0):
|
| 239 |
with gr.Column():
|
| 240 |
-
gr.Markdown("### 📸
|
| 241 |
with gr.Row():
|
| 242 |
btn_start = gr.Button("開啟攝影機", variant="primary")
|
| 243 |
btn_stop = gr.Button("停止", variant="secondary", visible=False)
|
| 244 |
cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
|
| 245 |
-
result_markdown = gr.Markdown("請按上方按鈕開始
|
| 246 |
-
upload_img = gr.Image(sources=["upload"], type="numpy", label="
|
| 247 |
|
| 248 |
-
#
|
| 249 |
-
with gr.TabItem("🍽️ 第二步:心情食堂
|
| 250 |
with gr.Column():
|
| 251 |
gr.Markdown("### 🔗 同步 Google Maps 分享清單")
|
| 252 |
with gr.Row():
|
| 253 |
-
map_url = gr.Textbox(label="清單連結", placeholder="
|
| 254 |
-
sync_btn = gr.Button("🔄 同步
|
| 255 |
-
sync_msg = gr.Markdown("ℹ️ 尚未
|
| 256 |
|
| 257 |
-
gr.Divider()
|
|
|
|
| 258 |
|
| 259 |
with gr.Row():
|
| 260 |
with gr.Column(scale=1):
|
| 261 |
score_input = gr.Radio(
|
| 262 |
["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
|
| 263 |
-
label="心情分數
|
| 264 |
value="3 (普通)"
|
| 265 |
)
|
| 266 |
food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="想吃什麼?", value="隨便")
|
| 267 |
-
diary_input = gr.Textbox(lines=3, label="心情日記
|
| 268 |
-
submit_btn = gr.Button("🍱 推薦
|
| 269 |
|
| 270 |
with gr.Column(scale=1):
|
| 271 |
-
agent_output = gr.Markdown(label="AI
|
| 272 |
-
map_embed_output = gr.HTML(label="地圖
|
| 273 |
map_nav_output = gr.HTML()
|
| 274 |
|
| 275 |
# 事件綁定
|
|
@@ -278,7 +270,7 @@ with gr.Blocks(title="AI 心情食堂", css=getattr(emotion, 'css', '')) as demo
|
|
| 278 |
btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
|
| 279 |
cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
|
| 280 |
upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
|
| 281 |
-
submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input
|
| 282 |
|
| 283 |
if __name__ == "__main__":
|
| 284 |
demo.launch()
|
|
|
|
| 11 |
|
| 12 |
# --- 1. 環境變數 & Playwright 初始化 ---
|
| 13 |
load_dotenv()
|
| 14 |
+
# 強制設定 Playwright 瀏覽器存放路徑
|
| 15 |
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
|
| 16 |
|
| 17 |
try:
|
|
|
|
| 28 |
from langchain_community.vectorstores import FAISS
|
| 29 |
from openai import OpenAI
|
| 30 |
|
| 31 |
+
# 嘗試匯入組員的模組
|
| 32 |
try:
|
| 33 |
import emotion
|
| 34 |
except ImportError:
|
| 35 |
+
print("警告:找不到 emotion 模組,請確保同級目錄下有 emotion.py。")
|
|
|
|
| 36 |
|
| 37 |
# ==========================================
|
| 38 |
# 0. 全域設定
|
| 39 |
# ==========================================
|
| 40 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 41 |
+
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
|
| 42 |
|
| 43 |
global_df = None
|
| 44 |
global_mood_df = None
|
|
|
|
| 58 |
})
|
| 59 |
|
| 60 |
# ==========================================
|
| 61 |
+
# 1. Google Maps 爬蟲功能
|
| 62 |
# ==========================================
|
| 63 |
def sync_google_maps(url):
|
| 64 |
global global_df, global_retriever, rag_initialized
|
|
|
|
| 74 |
page = browser.new_page()
|
| 75 |
yield "🌐 [2/4] 連線中..."
|
| 76 |
page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
|
| 77 |
+
yield "⏳ [3/4] 等待列表加載..."
|
| 78 |
time.sleep(10)
|
| 79 |
yield "📄 [4/4] 解析餐廳資訊..."
|
| 80 |
|
| 81 |
+
# 抓取餐廳名稱與基本資訊
|
| 82 |
titles = page.locator('div.fontHeadlineSmall').all_inner_texts()
|
| 83 |
details = page.locator('div.fontBodyMedium').all_inner_texts()
|
| 84 |
|
|
|
|
| 86 |
for i, name in enumerate(titles):
|
| 87 |
name = name.strip()
|
| 88 |
if name:
|
|
|
|
| 89 |
addr = details[i].strip() if i < len(details) else ""
|
| 90 |
restaurant_list.append({
|
| 91 |
"Name": name,
|
|
|
|
| 96 |
browser.close()
|
| 97 |
|
| 98 |
if not restaurant_list:
|
| 99 |
+
yield "⚠️ 找不到餐廳。請確認連結格式正確且已公開。"
|
| 100 |
return
|
| 101 |
|
| 102 |
global_df = pd.DataFrame(restaurant_list)
|
|
|
|
| 109 |
yield f"❌ 系統錯誤:{str(e)}"
|
| 110 |
|
| 111 |
# ==========================================
|
| 112 |
+
# 2. 推薦邏輯與 Embed API 嵌入
|
| 113 |
# ==========================================
|
| 114 |
def get_restaurant_data(mood_score_str, food_choice):
|
| 115 |
init_static_data()
|
| 116 |
if global_df is None or global_df.empty:
|
| 117 |
return None, True, "資料庫未載入", "尚未同步地圖資料", ""
|
| 118 |
|
| 119 |
+
all_names = "、".join(global_df['Name'].tolist())
|
|
|
|
| 120 |
try: score = int(str(mood_score_str).split(' ')[0])
|
| 121 |
except: score = 3
|
| 122 |
|
|
|
|
| 135 |
is_match = False
|
| 136 |
|
| 137 |
result = candidates.sample(1).iloc[0]
|
| 138 |
+
return result, is_match, "個人清單", mood_reason, all_names
|
| 139 |
|
| 140 |
def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason):
|
| 141 |
+
if not GROQ_API_KEY: return "⚠️ 請在 .env 設定 GROQ_API_KEY"
|
| 142 |
client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
|
| 143 |
+
system_prompt = "你是一個幽默且暖心的美食達人,會根據心情日記推薦餐廳。"
|
| 144 |
+
user_msg = f"心情分數:{mood_score}\n日記:{user_diary}\n餐廳:{restaurant_name}\n資訊:{restaurant_detail}"
|
| 145 |
try:
|
| 146 |
response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
|
| 147 |
return response.choices[0].message.content
|
| 148 |
+
except Exception as e: return f"AI 生成失敗: {str(e)}"
|
| 149 |
|
| 150 |
+
def mood_agent_logic(score_input, food_input, diary_input):
|
| 151 |
if not rag_initialized:
|
| 152 |
+
yield "⚠️ 請先同步地圖清單!", "", ""
|
| 153 |
return
|
| 154 |
|
| 155 |
restaurant, is_match, _, mood_reason, all_names = get_restaurant_data(score_input, food_input)
|
| 156 |
|
| 157 |
prefix = ""
|
| 158 |
if not is_match and food_input != "隨便":
|
| 159 |
+
prefix = f"> 💡 **溫馨提示**:清單中暫無『{food_input}』,已從現有名單挑選最適合的店!\n\n"
|
| 160 |
|
| 161 |
name = restaurant['Name']
|
| 162 |
addr = restaurant.get('Address', '')
|
| 163 |
url = restaurant.get('URL', '#')
|
|
|
|
| 164 |
|
| 165 |
+
ai_text = generate_content_with_groq(name, restaurant['RAG_Content'], diary_input, score_input, mood_reason)
|
|
|
|
| 166 |
final_markdown = f"{prefix}### 🍽️ 今日推薦:{name}\n\n{ai_text}"
|
| 167 |
|
| 168 |
+
# Google Maps Embed API HTML
|
|
|
|
| 169 |
query_str = urllib.parse.quote(f"{name} {addr}")
|
| 170 |
if GOOGLE_MAPS_API_KEY:
|
| 171 |
embed_html = f"""
|
| 172 |
+
<div style="border-radius:12px; overflow:hidden; border:1px solid #ddd;">
|
| 173 |
+
<iframe width="100%" height="350" frameborder="0" style="border:0"
|
|
|
|
|
|
|
|
|
|
| 174 |
src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={query_str}"
|
| 175 |
+
allowfullscreen></iframe>
|
|
|
|
| 176 |
</div>
|
| 177 |
"""
|
| 178 |
else:
|
| 179 |
+
embed_html = "<p style='color:red;'>⚠️ 缺少 GOOGLE_MAPS_API_KEY</p>"
|
| 180 |
|
| 181 |
+
nav_button = f'<div style="text-align:center; margin-top:10px;"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:10px 20px;border-radius:25px;text-decoration:none;font-weight:bold;">📍 Google Maps 導航</a></div>'
|
| 182 |
|
| 183 |
yield final_markdown, embed_html, nav_button
|
| 184 |
|
| 185 |
# ==========================================
|
| 186 |
+
# 3. 橋接情緒辨識 (依賴 emotion.py)
|
| 187 |
# ==========================================
|
| 188 |
def _score_to_radio_value(score):
|
| 189 |
mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
|
|
|
|
| 219 |
return out_result, score_update, tabs_update, out_st
|
| 220 |
|
| 221 |
# ==========================================
|
| 222 |
+
# 4. Gradio UI 介面
|
| 223 |
# ==========================================
|
| 224 |
with gr.Blocks(title="AI 心情食堂", css=getattr(emotion, 'css', '')) as demo:
|
| 225 |
st_state = gr.State(emotion.AppState())
|
| 226 |
|
| 227 |
with gr.Tabs() as tabs:
|
| 228 |
+
# Step 1
|
| 229 |
with gr.TabItem("😊 第一步:測測心情", id=0):
|
| 230 |
with gr.Column():
|
| 231 |
+
gr.Markdown("### 📸 臉部情緒辨識")
|
| 232 |
with gr.Row():
|
| 233 |
btn_start = gr.Button("開啟攝影機", variant="primary")
|
| 234 |
btn_stop = gr.Button("停止", variant="secondary", visible=False)
|
| 235 |
cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
|
| 236 |
+
result_markdown = gr.Markdown("請按上方按鈕開始")
|
| 237 |
+
upload_img = gr.Image(sources=["upload"], type="numpy", label="上傳照片辨識")
|
| 238 |
|
| 239 |
+
# Step 2
|
| 240 |
+
with gr.TabItem("🍽️ 第二步:心情食堂", id=1):
|
| 241 |
with gr.Column():
|
| 242 |
gr.Markdown("### 🔗 同步 Google Maps 分享清單")
|
| 243 |
with gr.Row():
|
| 244 |
+
map_url = gr.Textbox(label="清單連結", placeholder="貼上 Google Maps 分享連結")
|
| 245 |
+
sync_btn = gr.Button("🔄 同步清單", variant="secondary")
|
| 246 |
+
sync_msg = gr.Markdown("ℹ️ 尚未同步")
|
| 247 |
|
| 248 |
+
# 修正處:gr.Divider() 改為 gr.Separator()
|
| 249 |
+
gr.Separator()
|
| 250 |
|
| 251 |
with gr.Row():
|
| 252 |
with gr.Column(scale=1):
|
| 253 |
score_input = gr.Radio(
|
| 254 |
["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
|
| 255 |
+
label="心情分數",
|
| 256 |
value="3 (普通)"
|
| 257 |
)
|
| 258 |
food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="想吃什麼?", value="隨便")
|
| 259 |
+
diary_input = gr.Textbox(lines=3, label="心情日記", placeholder="今天發生了什麼事?")
|
| 260 |
+
submit_btn = gr.Button("🍱 推薦美食", variant="primary")
|
| 261 |
|
| 262 |
with gr.Column(scale=1):
|
| 263 |
+
agent_output = gr.Markdown(label="AI 建議")
|
| 264 |
+
map_embed_output = gr.HTML(label="地圖預覽")
|
| 265 |
map_nav_output = gr.HTML()
|
| 266 |
|
| 267 |
# 事件綁定
|
|
|
|
| 270 |
btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
|
| 271 |
cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
|
| 272 |
upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
|
| 273 |
+
submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input], outputs=[agent_output, map_embed_output, map_nav_output])
|
| 274 |
|
| 275 |
if __name__ == "__main__":
|
| 276 |
demo.launch()
|