PhilipL commited on
Commit
14030ee
·
verified ·
1 Parent(s): ddeadeb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -125
app.py CHANGED
@@ -4,30 +4,16 @@
4
 
5
  import os
6
  import subprocess
7
-
8
- # 強制設定 Playwright 瀏覽器存放於 user 目錄
9
- os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
10
-
11
- try:
12
- if not os.path.exists(os.environ["PLAYWRIGHT_BROWSERS_PATH"]):
13
- subprocess.run(["python3", "-m", "playwright", "install", "chromium"], check=True)
14
- except Exception as e:
15
- print(f"Playwright 初始化提示: {e}")
16
-
17
- # --- 3. 匯入必要的模組 ---
18
  import gradio as gr
19
  from playwright.sync_api import sync_playwright
20
- from dotenv import load_dotenv
21
- import pandas as pd
22
- import urllib.parse # 用於處理網址編碼
23
- import time # ✅ 確保這行存在
24
  from langchain_huggingface import HuggingFaceEmbeddings
25
  from langchain_community.vectorstores import FAISS
26
  from PIL import Image
27
- from openai import OpenAI
28
-
29
- # ✅ 這裡就是漏掉的地方,必須加入這行:
30
  from dotenv import load_dotenv
 
31
 
32
  # --- 匯入組員的模組 ---
33
  import emotion
@@ -35,17 +21,17 @@ import emotion
35
 
36
  load_dotenv()
37
 
38
- # 自動安裝 Playwright 瀏覽器 (HF 環境必要步驟)
39
- try:
40
- import playwright
41
- except ImportError:
42
- subprocess.run(["pip", "install", "playwright"])
43
- subprocess.run(["playwright", "install", "chromium"])
44
 
45
- from playwright.sync_api import sync_playwright
 
 
 
 
46
 
47
  # ==========================================
48
- # 0. 環境變數 & 系統初始化
49
  # ==========================================
50
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
51
  HF_TOKEN = os.getenv("HF_TOKEN")
@@ -57,6 +43,7 @@ rag_initialized = False
57
 
58
  def init_static_data():
59
  global global_mood_df
 
60
  try:
61
  global_mood_df = pd.read_csv('mood_food_guide.csv')
62
  except:
@@ -67,7 +54,7 @@ def init_static_data():
67
  })
68
 
69
  # ==========================================
70
- # 1. 優化後的動態同步功能
71
  # ==========================================
72
  def sync_google_maps(url):
73
  global global_df, global_retriever, rag_initialized
@@ -77,50 +64,60 @@ def sync_google_maps(url):
77
  return
78
 
79
  try:
80
- yield "🚀 [1/4] 啟動瀏覽器並準備抓取實景圖..."
81
  with sync_playwright() as p:
82
  browser = p.chromium.launch(headless=True)
83
  context = browser.new_context(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
84
  page = context.new_page()
85
- yield "🌐 [2/4] 連線中..."
 
86
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
87
- yield "⏳ [3/4] 等待網頁內容渲染 (10秒)..."
88
- time.sleep(10)
89
 
90
- yield "📄 [4/4] 正在解析餐廳資訊與圖片網址..."
91
- # 【關鍵修改】改用名稱作為基準,再去找對應的圖片,避免因為 role="article" 抓不到
92
- name_elements = page.locator('div.fontHeadlineSmall').all()
 
 
 
 
 
 
 
 
93
 
94
  restaurant_list = []
95
- for el in name_elements:
96
  try:
97
- name = el.inner_text().strip()
98
- if not name: continue
 
 
99
 
100
- # 尋找同一層級或父層級中的圖片與地址
101
- # Google Maps 的結構通常是名稱在一個 div,圖片在旁邊的 div
102
- container = page.locator(f"//div[contains(., '{name}')]/ancestor::div[@role='article']").first
103
 
104
- # 抓取地址
105
- addr = "位置詳見地圖"
106
- addr_el = container.locator('div.fontBodyMedium').first
107
- if addr_el.count() > 0:
108
- addr = addr_el.inner_text().strip()
109
 
110
- # 【新增】抓取實景圖 URL
111
- img_url = "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=400" # 預設圖
112
- img_el = container.locator('img').first
113
  if img_el.count() > 0:
114
  src = img_el.get_attribute('src')
 
115
  if src and not src.startswith('data:'):
116
- # 將縮圖網址改為較大尺寸
117
- img_url = src.replace('=w80-h80', '=w400-h400').split('=')[0] + '=w400-h400'
 
 
 
118
 
119
  restaurant_list.append({
120
  "Name": name,
121
  "Address": addr,
122
- "Image_URL": img_url, # 【新增】存入圖片網址
123
- "RAG_Content": f"餐廳名稱:{name},地址:{addr}",
124
  "URL": f"https://www.google.com/search?q={urllib.parse.quote(name)}"
125
  })
126
  except: continue
@@ -128,110 +125,92 @@ def sync_google_maps(url):
128
  browser.close()
129
 
130
  if not restaurant_list:
131
- yield "⚠️ 找不到餐廳資訊。請確認:1.連結是『分享清單』產生的 2.清單已設為公開。"
132
  return
133
 
134
- # 更新向量資料庫
135
  global_df = pd.DataFrame(restaurant_list)
136
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
137
- texts = global_df['RAG_Content'].tolist()
138
- metadatas = global_df.to_dict('records')
139
-
140
- vectorstore = FAISS.from_texts(texts, embeddings, metadatas=metadatas)
141
  global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
142
  rag_initialized = True
143
- yield f"✅ 同步成功!已載入 {len(restaurant_list)} 間包含實景圖的餐廳。"
 
144
  except Exception as e:
145
  yield f"❌ 系統錯誤:{str(e)}"
146
 
147
  # ==========================================
148
- # 2. 核心推薦邏輯 (確保 fallback 機制)
149
  # ==========================================
150
  def get_restaurant_data(mood_score_str, food_choice):
151
  init_static_data()
152
  if global_df is None or global_df.empty:
153
- return None, True, "資料庫未載入", "尚未同步地圖資料", ""
154
-
155
- # 【新增】列出目前資料庫中所有餐廳名稱
156
- all_names = "、".join(global_df['Name'].tolist())
157
-
158
- # 取得目前資料庫中所有餐廳的名稱清單,方便回報給使用者
159
- all_restaurant_names = "、".join(global_df['Name'].tolist())
160
 
 
161
  try: score = int(str(mood_score_str).split(' ')[0])
162
  except: score = 3
163
 
164
- mood_info = global_mood_df[global_mood_df['分數'] == score]
165
- mood_reason = mood_info.iloc[0]['原因'] if not mood_info.empty else "隨意探索"
166
-
167
  candidates = global_df.copy()
168
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
169
 
170
  is_match = True
171
  if food_keyword:
172
- # 過濾包含關鍵字的餐廳
173
  filtered = candidates[candidates['Name'].str.contains(food_keyword) | candidates['RAG_Content'].str.contains(food_keyword)]
174
  if not filtered.empty:
175
  candidates = filtered
176
  else:
177
- # ❗ 關鍵修改:如果找不到符合的,標記為「未命中」並使用全部資料隨機抽樣
178
- is_match = False
179
 
180
- # 從候選(或全部)中隨機挑選一間
181
  result = candidates.sample(1).iloc[0]
182
- return result, is_match, "個人清單", mood_reason, all_restaurant_names
183
-
184
- # (其餘 generate_content_with_groq, mood_agent_logic 與橋接邏輯保持不變...)
185
- def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason, debug_mode=False):
186
- if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
187
- client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
188
- system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友..."
189
- user_msg = f"【狀態】心情分數:{mood_score}\n【日記】{user_diary}\n【建議原因】{mood_guide_reason}\n【餐廳】{restaurant_name}\n資料:{restaurant_detail}"
190
- try:
191
- response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
192
- return response.choices[0].message.content
193
- except Exception as e: return f"Groq Error: {str(e)}"
194
-
195
- def generate_image_huggingface(prompt):
196
- if not HF_TOKEN: return None
197
- try:
198
- hf_client = InferenceClient(token=HF_TOKEN)
199
- return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
200
- except: return None
201
 
 
 
 
202
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
203
  if not rag_initialized:
204
- yield "⚠️ 請先同步地圖清單!", None, ""
205
  return
206
 
207
- # 取得推薦資料
208
  restaurant, is_match, rec_categories, mood_reason, all_names = get_restaurant_data(score_input, food_input)
209
 
210
- # 【新增】處理「找不到符合要求」時的告知訊息
211
  prefix = ""
212
  status_hint = ""
213
  if not is_match and food_input != "隨便":
214
- prefix = f"> 💡 **小提醒**:在您的清單中沒找到『{food_input}』相關的店,目前名單有:**{all_names}**。所以我先推薦這間給您:\n\n"
215
- status_hint = f"\n(注意:使用者的清單中無 '{food_input}',請幽默解釋為何這間也值得一試。)"
216
 
217
  name = restaurant['Name']
218
  url = restaurant.get('URL', '#')
219
- # 【修改】使用抓取到的真實圖片 URL
220
  real_img_url = restaurant.get('Image_URL')
221
  rag_info = str(restaurant.get('RAG_Content', '')) + status_hint
222
 
223
- # 產生 AI 回應
224
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason)
225
 
226
  final_response = f"{prefix}### 🍽️ 推薦:{name}\n\n{ai_text}"
227
- 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>'
228
 
229
- # 【修改】直接回傳實景圖網址給 Gradio
230
  yield final_response, real_img_url, map_html
231
 
232
  # ==========================================
233
- # 3. 橋接邏輯 (保留原本情緒辨識部分)
234
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
235
  def _score_to_radio_value(score):
236
  mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
237
  try: score = int(score)
@@ -267,16 +246,11 @@ def bridge_predict_upload(img, st):
267
  tabs_update = gr.Tabs(selected=1)
268
  return out_result, score_update, tabs_update, out_st
269
 
270
- # ==========================================
271
- # 4. Gradio 介面建構
272
- # ==========================================
273
  combined_css = emotion.css
274
-
275
  with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
276
  st_state = gr.State(emotion.AppState())
277
-
278
  with gr.Tabs() as tabs:
279
- # Tab 1: 情緒辨識
280
  with gr.TabItem("😊 情緒辨識 (Step 1)", id=0):
281
  with gr.Column(elem_id="app_container"):
282
  gr.Markdown("### 第一步:測測你的心情能量")
@@ -284,36 +258,27 @@ with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
284
  btn_start = gr.Button("📸 開啟攝影機辨識", variant="primary")
285
  btn_stop = gr.Button("⏹️ 停止", variant="secondary", visible=False)
286
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
287
- result_markdown = gr.Markdown(emotion._hint_html("請按「開啟攝影機辨識」或下方上傳照片。"))
288
  gr.Markdown("---")
289
  upload_img = gr.Image(sources=["upload"], type="numpy", label="上傳照片")
290
 
291
- # Tab 2: 主功能區
292
  with gr.TabItem("🍽️ AI 心情食堂 (Step 2)", id=1):
293
  with gr.Column():
294
- gr.Markdown("## 🍱 同步口袋名單\n請貼上你的 Google Maps「已儲存地點」分享連結(需設為共用)。")
295
  with gr.Row():
296
- map_url = gr.Textbox(label="Google Maps 分享連結", placeholder="https://goo.gl/maps/...")
297
  sync_btn = gr.Button("🔄 同步我的清單", variant="secondary")
298
  sync_msg = gr.Markdown("ℹ️ 尚未同步資料庫")
299
-
300
  gr.Markdown("---")
301
-
302
  with gr.Row():
303
  with gr.Column(scale=1):
304
- score_input = gr.Radio(
305
- ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
306
- label="1. 心情分數",
307
- value="3 (普通)"
308
- )
309
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
310
- diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天的心情...")
311
- debug_mode_btn = gr.Checkbox(label="🔧 開啟除錯模式", value=False)
312
  submit_btn = gr.Button("🍱 推薦今天的驚喜", variant="primary")
313
-
314
  with gr.Column(scale=1):
315
  agent_output = gr.Markdown(label="AI 回應")
316
- image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
317
  map_output = gr.HTML(label="地圖導航")
318
 
319
  # 事件綁定
@@ -322,7 +287,7 @@ with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
322
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
323
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
324
  upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
325
- submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input, debug_mode_btn], outputs=[agent_output, image_output, map_output])
326
 
327
  if __name__ == "__main__":
328
  demo.launch(ssr_mode=False)
 
4
 
5
  import os
6
  import subprocess
7
+ import time
8
+ import urllib.parse
9
+ import pandas as pd
 
 
 
 
 
 
 
 
10
  import gradio as gr
11
  from playwright.sync_api import sync_playwright
 
 
 
 
12
  from langchain_huggingface import HuggingFaceEmbeddings
13
  from langchain_community.vectorstores import FAISS
14
  from PIL import Image
 
 
 
15
  from dotenv import load_dotenv
16
+ from openai import OpenAI
17
 
18
  # --- 匯入組員的模組 ---
19
  import emotion
 
21
 
22
  load_dotenv()
23
 
24
+ # 強制設定 Playwright 瀏覽器存放於 user 目錄
25
+ os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "/home/user/.cache/ms-playwright"
 
 
 
 
26
 
27
+ try:
28
+ if not os.path.exists(os.environ["PLAYWRIGHT_BROWSERS_PATH"]):
29
+ subprocess.run(["python3", "-m", "playwright", "install", "chromium"], check=True)
30
+ except Exception as e:
31
+ print(f"Playwright 初始化提示: {e}")
32
 
33
  # ==========================================
34
+ # 0. 全域變數初始化
35
  # ==========================================
36
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
37
  HF_TOKEN = os.getenv("HF_TOKEN")
 
43
 
44
  def init_static_data():
45
  global global_mood_df
46
+ if global_mood_df is not None: return
47
  try:
48
  global_mood_df = pd.read_csv('mood_food_guide.csv')
49
  except:
 
54
  })
55
 
56
  # ==========================================
57
+ # 1. 【核心修正】動態同步功能:強化圖片抓取穩定性
58
  # ==========================================
59
  def sync_google_maps(url):
60
  global global_df, global_retriever, rag_initialized
 
64
  return
65
 
66
  try:
67
+ yield "🚀 [1/4] 啟動瀏覽器中..."
68
  with sync_playwright() as p:
69
  browser = p.chromium.launch(headless=True)
70
  context = browser.new_context(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
71
  page = context.new_page()
72
+
73
+ yield "🌐 [2/4] 正在開啟網頁..."
74
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
 
 
75
 
76
+ # 【新增】自動捲動機制:Google Maps 需要捲動才能載入隱藏的圖片
77
+ yield "⏳ [3/4] 正在觸發圖片載入 (自動捲動與等待)..."
78
+ for _ in range(3):
79
+ page.mouse.wheel(0, 1000) # 向下捲動
80
+ time.sleep(2)
81
+
82
+ yield "📄 [4/4] 正在提取餐廳資料與真實照片..."
83
+
84
+ # 【優化選擇器】改用更穩定的區塊定位
85
+ # Google Maps 清單中的每一項通常都在 role="article" 的 div 內
86
+ items = page.locator('div[role="article"]').all()
87
 
88
  restaurant_list = []
89
+ for item in items:
90
  try:
91
+ # 1. 抓取店名 (通常在 fontHeadlineSmall 中)
92
+ name_el = item.locator('div.fontHeadlineSmall').first
93
+ if name_el.count() == 0: continue
94
+ name = name_el.inner_text().strip()
95
 
96
+ # 2. 抓取地址/細節
97
+ addr_el = item.locator('div.fontBodyMedium').first
98
+ addr = addr_el.inner_text().strip() if addr_el.count() > 0 else "位置見地圖"
99
 
100
+ # 3. 【關鍵修正】多重路徑抓取圖片 URL
101
+ # 先找直接的 img 標籤,如果沒有,找背景圖片
102
+ img_url = "https://images.unsplash.com/photo-1555396273-367ea4eb4db5?q=80&w=400" # 預設改為更中性的餐廳圖
 
 
103
 
104
+ # 嘗試抓取 src
105
+ img_el = item.locator('img').first
 
106
  if img_el.count() > 0:
107
  src = img_el.get_attribute('src')
108
+ # 過濾掉 base64 透明佔位圖 (通常以 data: 開頭)
109
  if src and not src.startswith('data:'):
110
+ # 【新增】將縮圖 URL 轉為大圖模式 (Google 的 URL 規則)
111
+ if "=w" in src:
112
+ img_url = src.split('=w')[0] + "=w400-h400-p-k-no"
113
+ else:
114
+ img_url = src
115
 
116
  restaurant_list.append({
117
  "Name": name,
118
  "Address": addr,
119
+ "Image_URL": img_url,
120
+ "RAG_Content": f"餐廳:{name},細節:{addr}",
121
  "URL": f"https://www.google.com/search?q={urllib.parse.quote(name)}"
122
  })
123
  except: continue
 
125
  browser.close()
126
 
127
  if not restaurant_list:
128
+ yield "⚠️ 抓取失敗:找不到地點資訊。請確認分享連結是否正確。"
129
  return
130
 
131
+ # 更新 RAG 系統
132
  global_df = pd.DataFrame(restaurant_list)
133
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
134
+ vectorstore = FAISS.from_texts(global_df['RAG_Content'].tolist(), embeddings, metadatas=global_df.to_dict('records'))
 
 
 
135
  global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
136
  rag_initialized = True
137
+ yield f"✅ 同步成功!已載入 {len(restaurant_list)} 間餐廳實景照片。"
138
+
139
  except Exception as e:
140
  yield f"❌ 系統錯誤:{str(e)}"
141
 
142
  # ==========================================
143
+ # 2. 【修改】推薦邏輯:支援未匹配告知
144
  # ==========================================
145
  def get_restaurant_data(mood_score_str, food_choice):
146
  init_static_data()
147
  if global_df is None or global_df.empty:
148
+ return None, False, "", "", ""
 
 
 
 
 
 
149
 
150
+ all_names = "、".join(global_df['Name'].tolist())
151
  try: score = int(str(mood_score_str).split(' ')[0])
152
  except: score = 3
153
 
154
+ mood_reason = global_mood_df[global_mood_df['分數'] == score].iloc[0]['原因']
 
 
155
  candidates = global_df.copy()
156
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
157
 
158
  is_match = True
159
  if food_keyword:
 
160
  filtered = candidates[candidates['Name'].str.contains(food_keyword) | candidates['RAG_Content'].str.contains(food_keyword)]
161
  if not filtered.empty:
162
  candidates = filtered
163
  else:
164
+ is_match = False
 
165
 
 
166
  result = candidates.sample(1).iloc[0]
167
+ return result, is_match, "口袋名單", mood_reason, all_names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ # ==========================================
170
+ # 3. 【修改】Agent 邏輯:整合實景照片輸出
171
+ # ==========================================
172
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
173
  if not rag_initialized:
174
+ yield "⚠️ 請先點擊同步按鈕!", None, ""
175
  return
176
 
 
177
  restaurant, is_match, rec_categories, mood_reason, all_names = get_restaurant_data(score_input, food_input)
178
 
179
+ # 組合回報訊息
180
  prefix = ""
181
  status_hint = ""
182
  if not is_match and food_input != "隨便":
183
+ prefix = f"> 💡 **提醒**:名單中沒找到『{food_input}』,目前有:**{all_names}**。推薦這間試試:\n\n"
184
+ status_hint = f"\n(注意:清單中無 '{food_input}',請解釋推薦原因。)"
185
 
186
  name = restaurant['Name']
187
  url = restaurant.get('URL', '#')
188
+ # 【修正】直接使用抓取到的 Image_URL
189
  real_img_url = restaurant.get('Image_URL')
190
  rag_info = str(restaurant.get('RAG_Content', '')) + status_hint
191
 
 
192
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason)
193
 
194
  final_response = f"{prefix}### 🍽️ 推薦:{name}\n\n{ai_text}"
195
+ 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>'
196
 
197
+ # 回傳圖片
198
  yield final_response, real_img_url, map_html
199
 
200
  # ==========================================
201
+ # 4. 輔助函數與 Gradio (保持穩定版本)
202
  # ==========================================
203
+ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, mood_guide_reason):
204
+ if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
205
+ client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
206
+ system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友..."
207
+ user_msg = f"【餐廳】{restaurant_name}\n【資料】{restaurant_detail}\n【心情分數】{mood_score}\n【日記】{user_diary}"
208
+ try:
209
+ response = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}])
210
+ return response.choices[0].message.content
211
+ except Exception as e: return f"Groq Error: {str(e)}"
212
+
213
+ # --- 橋接邏輯 ---
214
  def _score_to_radio_value(score):
215
  mapping = {1: "1 (心情差)", 2: "2 (不太好)", 3: "3 (普通)", 4: "4 (不錯)", 5: "5 (超棒)"}
216
  try: score = int(score)
 
246
  tabs_update = gr.Tabs(selected=1)
247
  return out_result, score_update, tabs_update, out_st
248
 
249
+ # --- Gradio 介面 ---
 
 
250
  combined_css = emotion.css
 
251
  with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
252
  st_state = gr.State(emotion.AppState())
 
253
  with gr.Tabs() as tabs:
 
254
  with gr.TabItem("😊 情緒辨識 (Step 1)", id=0):
255
  with gr.Column(elem_id="app_container"):
256
  gr.Markdown("### 第一步:測測你的心情能量")
 
258
  btn_start = gr.Button("📸 開啟攝影機辨識", variant="primary")
259
  btn_stop = gr.Button("⏹️ 停止", variant="secondary", visible=False)
260
  cam = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="攝影機畫面", visible=False)
261
+ result_markdown = gr.Markdown(emotion._hint_html("請按「開啟攝影機辨識」。"))
262
  gr.Markdown("---")
263
  upload_img = gr.Image(sources=["upload"], type="numpy", label="上傳照片")
264
 
 
265
  with gr.TabItem("🍽️ AI 心情食堂 (Step 2)", id=1):
266
  with gr.Column():
267
+ gr.Markdown("## 🍱 同步口袋名單\n請貼上您的 Google Maps 分享連結。")
268
  with gr.Row():
269
+ map_url = gr.Textbox(label="分享連結", placeholder="https://goo.gl/maps/...")
270
  sync_btn = gr.Button("🔄 同步我的清單", variant="secondary")
271
  sync_msg = gr.Markdown("ℹ️ 尚未同步資料庫")
 
272
  gr.Markdown("---")
 
273
  with gr.Row():
274
  with gr.Column(scale=1):
275
+ score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
 
 
 
 
276
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
277
+ diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下心情...")
 
278
  submit_btn = gr.Button("🍱 推薦今天的驚喜", variant="primary")
 
279
  with gr.Column(scale=1):
280
  agent_output = gr.Markdown(label="AI 回應")
281
+ image_output = gr.Image(label="餐廳照片", type="filepath", width=400)
282
  map_output = gr.HTML(label="地圖導航")
283
 
284
  # 事件綁定
 
287
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
288
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, tabs])
289
  upload_img.change(fn=bridge_predict_upload, inputs=[upload_img, st_state], outputs=[result_markdown, score_input, tabs, st_state])
290
+ submit_btn.click(fn=mood_agent_logic, inputs=[score_input, food_input, diary_input], outputs=[agent_output, image_output, map_output])
291
 
292
  if __name__ == "__main__":
293
  demo.launch(ssr_mode=False)