PhilipL commited on
Commit
fdcdf9e
·
verified ·
1 Parent(s): 203d188

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +44 -52
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
- # 防止因缺少 emotion 模組而無法執行
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") # 請確保 .env 中有此項
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
- all_restaurant_names = "、".join(global_df['Name'].tolist())
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, all_restaurant_names
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 = "你是一個幽默、懂吃且善解人意的 AI 朋友,會根據對方的心情推薦合適的餐廳。請用溫暖且具備食記風格的口吻來描述。"
146
- user_msg = f"【狀態】心情分數:{mood_score}\n【日記】{user_diary}\n【建議原因】{mood_guide_reason}\n【推薦餐廳】{restaurant_name}\n餐廳資料:{restaurant_detail}"
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"Groq Error: {str(e)}"
151
 
152
- def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
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}』,已從名單「{all_names}」中挑選最適合您今日心情的一間!\n\n"
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
- # 產生 AI 文案
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
- # --- 關鍵:使用 Google Maps Embed API 替代圖片 ---
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:15px; overflow:hidden; border:1px solid #ddd;">
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;'>⚠️ 尚未設定 GOOGLE_MAPS_API_KEY,無法顯示地圖預覽。</p>"
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;">📍 前往 Google Maps 導航</a></div>'
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
- # Tab 1: 情緒辨識
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
- # Tab 2: 主功能區
249
- with gr.TabItem("🍽️ 第二步:心情食堂推薦", id=1):
250
  with gr.Column():
251
  gr.Markdown("### 🔗 同步 Google Maps 分享清單")
252
  with gr.Row():
253
- map_url = gr.Textbox(label="清單連結", placeholder="https://maps.app.goo.gl/...")
254
- sync_btn = gr.Button("🔄 同步我的口袋名單", variant="secondary")
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="心情日記 (選填)", placeholder="寫下現在的想法...")
268
- submit_btn = gr.Button("🍱 推薦今日驚喜", variant="primary")
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, gr.Checkbox(value=False, visible=False)], outputs=[agent_output, map_embed_output, map_nav_output])
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()