PhilipL commited on
Commit
fac38aa
·
verified ·
1 Parent(s): a0c4a25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -53
app.py CHANGED
@@ -10,8 +10,11 @@ from langchain_community.document_loaders import DataFrameLoader
10
  from langchain_text_splitters import RecursiveCharacterTextSplitter
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
- from PIL import Image
14
  import warnings
 
 
 
15
 
16
  # 忽略 pandas 的一些警告
17
  warnings.filterwarnings("ignore")
@@ -25,7 +28,6 @@ HF_TOKEN = os.getenv("HF_TOKEN")
25
  # --- 1. 資料讀取 (主要資料庫 - 使用 restaurants.csv) ---
26
  df = pd.DataFrame()
27
  try:
28
- # 修改:讀取 restaurants.csv
29
  df = pd.read_csv('restaurants.csv', engine='python')
30
  df.columns = df.columns.str.strip() # 清理欄位空白
31
 
@@ -54,26 +56,15 @@ try:
54
  except Exception as e:
55
  print(f"⚠️ 讀取 restaurants.csv 失敗: {e}")
56
 
57
- # --- 2. 資料讀取 (Prompt 資料庫 - 使用 restaurants.csv) ---
58
  df_prompts = pd.DataFrame()
59
- # 修改:您的 CSV 中 Prompt 欄位名稱為 Visual_prompt
60
  PROMPT_COL_NAME = 'Visual_prompt'
61
 
62
  try:
63
- # 直接使用已讀取的 df 來建立 Prompt 索引
64
  if not df.empty:
65
  df_prompts = df.copy()
66
-
67
  if 'Name' in df_prompts.columns:
68
  df_prompts.set_index('Name', inplace=True)
69
- else:
70
- print(f"⚠️ Prompts 處理警告: 找不到 'Name' 欄位")
71
-
72
- if PROMPT_COL_NAME not in df_prompts.columns:
73
- print(f"⚠️ Prompts 處理警告: 找不到 '{PROMPT_COL_NAME}' 欄位")
74
- else:
75
- print("⚠️ 主資料庫為空,無法建立 Prompt 索引")
76
-
77
  except Exception as e:
78
  print(f"⚠️ 處理 Prompt 資料發生錯誤: {e}")
79
 
@@ -108,7 +99,7 @@ custom_css = """
108
  display: flex !important;
109
  }
110
  """
111
-
112
  img_path_1 = "images/image_0.png"
113
  img_path_2 = "images/image_1.png"
114
  if not os.path.exists("images"): os.makedirs("images")
@@ -132,11 +123,12 @@ q2_options_map = {
132
  }
133
 
134
  # ==========================================
135
- # 2. 核心功能函式
136
  # ==========================================
137
 
138
  def get_restaurant_data(mood_score_str, food_choice):
139
  if df.empty: return None, True
 
140
  score_map = {
141
  "1 (心情差)": ["傷心/疲憊", "生氣/發洩"],
142
  "2 (不太好)": ["傷心/疲憊", "生氣/發洩"],
@@ -200,15 +192,19 @@ def generate_image_huggingface(prompt):
200
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
201
  except: return None
202
 
203
- # --- 主邏輯 Agent (新增 debug_mode 參數) ---
204
  def mood_agent_logic(score_input, food_input, diary_input, quiz_state, debug_mode):
 
 
 
 
 
205
  restaurant, is_random = get_restaurant_data(score_input, food_input)
206
  if restaurant is None:
207
  yield "資料庫讀取錯誤或為空", None, ""
208
  return
209
 
210
  name = str(restaurant['Name'])
211
- address = str(restaurant.get('Address', '未知地址'))
212
  url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
213
  note = "(隨機推薦)" if is_random else ""
214
 
@@ -220,56 +216,102 @@ def mood_agent_logic(score_input, food_input, diary_input, quiz_state, debug_mod
220
  if docs: rag_info = "\n".join([d.page_content for d in docs])
221
  except: pass
222
 
223
- # 2. 準備圖片 Prompt (提前執行以便顯示除錯資訊)
224
- img_prompt = f"Delicious food from {name}, cinematic lighting, 8k, photorealistic" # 預設值
225
- prompt_source = "⚠️ 預設生成 (未找到 CSV 對應)"
226
 
227
  if not df_prompts.empty and PROMPT_COL_NAME in df_prompts.columns:
228
  if name in df_prompts.index:
229
  try:
230
  csv_prompt = df_prompts.loc[name, PROMPT_COL_NAME]
231
  if isinstance(csv_prompt, pd.Series): csv_prompt = csv_prompt.iloc[0]
232
-
233
  if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
234
  img_prompt = str(csv_prompt)
235
- # 修改:更新來源顯示為 restaurants.csv
236
- prompt_source = "✅ CSV 檔案 (restaurants.csv)"
237
- else:
238
- prompt_source = "⚠️ CSV 有此餐廳但 Prompt 空白"
239
- except Exception as e:
240
- prompt_source = f"❌ 讀取錯誤: {e}"
241
- else:
242
- prompt_source = "ℹ️ CSV 中無此餐廳"
243
 
244
  # 3. 呼叫 LLM
245
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state)
246
 
247
- # 4. 組合回應 (包含除錯資訊)
248
  debug_text = ""
249
  if debug_mode:
250
- debug_text = f"""
251
- \n\n---
252
- **🛠️ Prompt 除錯模式**
253
- * **餐廳名稱**: `{name}`
254
- * **Prompt 來源**: {prompt_source}
255
- * **實際使用 Prompt**:
256
- ```
257
- {img_prompt}
258
- ```
259
- """
260
 
261
  final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}{debug_text}"
262
  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>'
263
 
264
- # 5. 先回傳文字
265
  yield final_response, None, map_html
266
-
267
- # 6. 生成圖片
268
  image_output = generate_image_huggingface(img_prompt)
269
  yield final_response, image_output, map_html
270
 
271
  # ==========================================
272
- # 3. 頁面切換邏輯
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  # ==========================================
274
 
275
  def handle_q1_change(selected_label, current_state):
@@ -286,7 +328,7 @@ def go_to_page_2(): return gr.update(visible=False), gr.update(visible=True)
286
  def go_to_main_app(): return gr.update(visible=False), gr.update(visible=True)
287
 
288
  # ==========================================
289
- # 4. Gradio 介面建構
290
  # ==========================================
291
 
292
  with gr.Blocks(title="AI 心情食堂") as demo:
@@ -312,19 +354,32 @@ with gr.Blocks(title="AI 心情食堂") as demo:
312
  # Page 3 (Main)
313
  with gr.Column(visible=False) as main_app_col:
314
  gr.Markdown(f"## 🍱 AI 心情食堂導航")
315
- gr.Markdown("測驗結束!現在,告訴我你真正想吃什麼吧!")
316
 
317
  with gr.Row():
 
318
  with gr.Column(scale=1):
319
- score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
321
- diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天發生的事...")
322
 
323
- # [新增] 除錯模式開關
324
  debug_mode_btn = gr.Checkbox(label="🔧 顯示 Prompt 除錯資訊", value=False)
325
-
326
- submit_btn = gr.Button("送出給 Agent", variant="primary")
327
 
 
328
  with gr.Column(scale=1):
329
  agent_output = gr.Markdown(label="AI 回應")
330
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
@@ -336,7 +391,13 @@ with gr.Blocks(title="AI 心情食堂") as demo:
336
  radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_finish])
337
  btn_q2_finish.click(fn=go_to_main_app, outputs=[page_2_col, main_app_col])
338
 
339
- # [修改] 加入 debug_mode_btn 到 inputs
 
 
 
 
 
 
340
  submit_btn.click(
341
  fn=mood_agent_logic,
342
  inputs=[score_input, food_input, diary_input, quiz_state, debug_mode_btn],
 
10
  from langchain_text_splitters import RecursiveCharacterTextSplitter
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
+ from PIL import Image, ImageFont, ImageDraw
14
  import warnings
15
+ import cv2
16
+ import numpy as np
17
+ from deepface import DeepFace
18
 
19
  # 忽略 pandas 的一些警告
20
  warnings.filterwarnings("ignore")
 
28
  # --- 1. 資料讀取 (主要資料庫 - 使用 restaurants.csv) ---
29
  df = pd.DataFrame()
30
  try:
 
31
  df = pd.read_csv('restaurants.csv', engine='python')
32
  df.columns = df.columns.str.strip() # 清理欄位空白
33
 
 
56
  except Exception as e:
57
  print(f"⚠️ 讀取 restaurants.csv 失敗: {e}")
58
 
59
+ # --- 2. 資料讀取 (Prompt 資料庫) ---
60
  df_prompts = pd.DataFrame()
 
61
  PROMPT_COL_NAME = 'Visual_prompt'
62
 
63
  try:
 
64
  if not df.empty:
65
  df_prompts = df.copy()
 
66
  if 'Name' in df_prompts.columns:
67
  df_prompts.set_index('Name', inplace=True)
 
 
 
 
 
 
 
 
68
  except Exception as e:
69
  print(f"⚠️ 處理 Prompt 資料發生錯誤: {e}")
70
 
 
99
  display: flex !important;
100
  }
101
  """
102
+ # 若無圖片自動生成空白圖避免報錯
103
  img_path_1 = "images/image_0.png"
104
  img_path_2 = "images/image_1.png"
105
  if not os.path.exists("images"): os.makedirs("images")
 
123
  }
124
 
125
  # ==========================================
126
+ # 2. 核心功能函式 (餐廳推薦邏輯)
127
  # ==========================================
128
 
129
  def get_restaurant_data(mood_score_str, food_choice):
130
  if df.empty: return None, True
131
+ # 定義分數與 Mood_Tags 的對應關係
132
  score_map = {
133
  "1 (心情差)": ["傷心/疲憊", "生氣/發洩"],
134
  "2 (不太好)": ["傷心/疲憊", "生氣/發洩"],
 
192
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
193
  except: return None
194
 
195
+ # --- 主邏輯 Agent ---
196
  def mood_agent_logic(score_input, food_input, diary_input, quiz_state, debug_mode):
197
+ # 確保 score_input 有值
198
+ if not score_input:
199
+ yield "⚠️ 請先選擇心情分數,或使用上方相機偵測!", None, ""
200
+ return
201
+
202
  restaurant, is_random = get_restaurant_data(score_input, food_input)
203
  if restaurant is None:
204
  yield "資料庫讀取錯誤或為空", None, ""
205
  return
206
 
207
  name = str(restaurant['Name'])
 
208
  url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
209
  note = "(隨機推薦)" if is_random else ""
210
 
 
216
  if docs: rag_info = "\n".join([d.page_content for d in docs])
217
  except: pass
218
 
219
+ # 2. 準備圖片 Prompt
220
+ img_prompt = f"Delicious food from {name}, cinematic lighting, 8k, photorealistic"
221
+ prompt_source = "⚠️ 預設生成"
222
 
223
  if not df_prompts.empty and PROMPT_COL_NAME in df_prompts.columns:
224
  if name in df_prompts.index:
225
  try:
226
  csv_prompt = df_prompts.loc[name, PROMPT_COL_NAME]
227
  if isinstance(csv_prompt, pd.Series): csv_prompt = csv_prompt.iloc[0]
 
228
  if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
229
  img_prompt = str(csv_prompt)
230
+ prompt_source = "✅ CSV 檔案"
231
+ except: pass
 
 
 
 
 
 
232
 
233
  # 3. 呼叫 LLM
234
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state)
235
 
236
+ # 4. 組合回應
237
  debug_text = ""
238
  if debug_mode:
239
+ debug_text = f"\n\n---\n**🛠️ Prompt 來源**: {prompt_source}\n**Prompt**: `{img_prompt}`"
 
 
 
 
 
 
 
 
 
240
 
241
  final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}{debug_text}"
242
  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>'
243
 
 
244
  yield final_response, None, map_html
 
 
245
  image_output = generate_image_huggingface(img_prompt)
246
  yield final_response, image_output, map_html
247
 
248
  # ==========================================
249
+ # 3. [新增] 情緒辨識整合模組
250
+ # ==========================================
251
+
252
+ # 定義中文字典 (繪圖用)
253
+ emotion_text_obj = {
254
+ 'angry': '生氣', 'disgust': '噁心', 'fear': '害怕',
255
+ 'happy': '開心', 'sad': '難過', 'surprise': '驚訝', 'neutral': '正常'
256
+ }
257
+
258
+ # 繪圖函式
259
+ def putText(img, x, y, text, size=50, color=(255, 255, 255)):
260
+ try:
261
+ fontpath = 'NotoSansTC-VariableFont_wght.ttf'
262
+ # 確保字型存在,若無則回傳原圖
263
+ if not os.path.exists(fontpath): return img
264
+ font = ImageFont.truetype(fontpath, size)
265
+ imgPil = Image.fromarray(img)
266
+ draw = ImageDraw.Draw(imgPil)
267
+ displayText = emotion_text_obj.get(text, text)
268
+ draw.text((x, y), displayText, fill=color, font=font)
269
+ return np.array(imgPil)
270
+ except:
271
+ return img
272
+
273
+ # ★ 關鍵功能:偵測情緒並回傳「分數選項」
274
+ def detect_emotion_and_map(frame):
275
+ if frame is None:
276
+ return frame, None
277
+
278
+ detected_emotion = "neutral" # 預設
279
+ mapped_score = "3 (普通)" # 預設
280
+
281
+ try:
282
+ # 1. 辨識情緒
283
+ analyze = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
284
+ if isinstance(analyze, list): analyze = analyze[0]
285
+ detected_emotion = analyze['dominant_emotion']
286
+
287
+ # 2. 畫在圖片上
288
+ frame = putText(frame, 20, 40, detected_emotion)
289
+
290
+ # 3. 橋樑:將情緒轉換為餐廳系統的分數
291
+ # happy -> 5
292
+ # surprise -> 4
293
+ # neutral -> 3
294
+ # sad, fear -> 2
295
+ # angry, disgust -> 1
296
+ if detected_emotion == 'happy':
297
+ mapped_score = "5 (超棒)"
298
+ elif detected_emotion == 'surprise':
299
+ mapped_score = "4 (不錯)"
300
+ elif detected_emotion == 'neutral':
301
+ mapped_score = "3 (普通)"
302
+ elif detected_emotion in ['sad', 'fear']:
303
+ mapped_score = "2 (不太好)"
304
+ elif detected_emotion in ['angry', 'disgust']:
305
+ mapped_score = "1 (心情差)"
306
+
307
+ except Exception as e:
308
+ print(f"DeepFace Error: {e}")
309
+ pass
310
+
311
+ return frame, mapped_score
312
+
313
+ # ==========================================
314
+ # 4. 頁面切換與 UI 邏輯
315
  # ==========================================
316
 
317
  def handle_q1_change(selected_label, current_state):
 
328
  def go_to_main_app(): return gr.update(visible=False), gr.update(visible=True)
329
 
330
  # ==========================================
331
+ # 5. Gradio 介面建構
332
  # ==========================================
333
 
334
  with gr.Blocks(title="AI 心情食堂") as demo:
 
354
  # Page 3 (Main)
355
  with gr.Column(visible=False) as main_app_col:
356
  gr.Markdown(f"## 🍱 AI 心情食堂導航")
357
+ gr.Markdown("測驗結束!讓 AI 看看你的臉色,幫你決定吃什麼!")
358
 
359
  with gr.Row():
360
+ # 左側:輸入區
361
  with gr.Column(scale=1):
362
+
363
+ # ★ 新增:情緒偵測區塊
364
+ gr.Markdown("### 📸 步驟 1:心情偵測 (選用)")
365
+ with gr.Group():
366
+ webcam_input = gr.Image(sources=["webcam"], label="看著鏡頭", streaming=True)
367
+ detect_btn = gr.Button("📸 偵測心情並自動填入 👇", variant="secondary")
368
+
369
+ gr.Markdown("### 📝 步驟 2:確認與補充")
370
+ # score_input 會被 detect_btn 更新
371
+ score_input = gr.Radio(
372
+ ["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"],
373
+ label="1. 心情分數 (可手動修改)",
374
+ value="3 (普通)"
375
+ )
376
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
377
+ diary_input = gr.Textbox(lines=3, label="3. 心情日記 (選填)", placeholder="例如:今天被老闆罵了,想吃點好料的...")
378
 
 
379
  debug_mode_btn = gr.Checkbox(label="🔧 顯示 Prompt 除錯資訊", value=False)
380
+ submit_btn = gr.Button("🚀 送出給 Agent", variant="primary")
 
381
 
382
+ # 右側:輸出區
383
  with gr.Column(scale=1):
384
  agent_output = gr.Markdown(label="AI 回應")
385
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
 
391
  radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_finish])
392
  btn_q2_finish.click(fn=go_to_main_app, outputs=[page_2_col, main_app_col])
393
 
394
+ # ★ 新增事件:點擊偵測按鈕 -> 執行 DeepFace -> 更新 score_input
395
+ detect_btn.click(
396
+ fn=detect_emotion_and_map,
397
+ inputs=[webcam_input],
398
+ outputs=[webcam_input, score_input] # 同時更新圖片(畫字)和分數(Radio)
399
+ )
400
+
401
  submit_btn.click(
402
  fn=mood_agent_logic,
403
  inputs=[score_input, food_input, diary_input, quiz_state, debug_mode_btn],