PhilipL commited on
Commit
0dc98c5
·
verified ·
1 Parent(s): 8c9b916

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -135
app.py CHANGED
@@ -20,17 +20,16 @@ from deepface import DeepFace
20
  warnings.filterwarnings("ignore")
21
 
22
  # ==========================================
23
- # 環境變數與 RAG 初始化
24
  # ==========================================
25
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
26
  HF_TOKEN = os.getenv("HF_TOKEN")
27
 
28
- # --- 1. 資料讀取 (主要資料庫 - 使用 restaurants.csv) ---
29
  df = pd.DataFrame()
30
  try:
31
- # 確保你有 restaurants.csv 在專案根目錄
32
  df = pd.read_csv('restaurants.csv', engine='python')
33
- df.columns = df.columns.str.strip() # 清理欄位空白
34
 
35
  def classify_mood(row):
36
  name_str = str(row.get('Name', ''))
@@ -38,10 +37,10 @@ try:
38
  text = (name_str + " " + rag_str).lower()
39
  tags = []
40
  rules = {
41
- "開心/慶祝": ["牛排", "steak", "pizza", "炸", "雞排", "甜點", "蛋糕", "cake", "冰", "waffle", "吃到飽", "buffet", "burger", "bistro", "餐酒館"],
42
- "傷心/疲憊": ["粥", "湯", "warm", "congee", "麵", "noodle", "小吃", "comfort food", "豆花", "關東煮", "soup"],
43
- "生氣/發洩": ["辣", "spicy", "麻辣", "鍋", "curry", "咖哩", "燒肉", "bbq", "臭豆腐", "fry"],
44
- "平靜/放鬆": ["cafe", "coffee", "tea", "茶", "素食", "vegetable", "早午餐", "brunch", "壽司", "sushi", "居酒屋"]
45
  }
46
  for mood, keywords in rules.items():
47
  for kw in keywords:
@@ -52,26 +51,21 @@ try:
52
 
53
  if 'Mood_Tags' not in df.columns:
54
  df['Mood_Tags'] = df.apply(classify_mood, axis=1)
55
-
56
- print("✅ 成功讀取 restaurants.csv 並建立 Mood_Tags")
57
  except Exception as e:
58
- # 如果找不到檔案或讀取失敗,這裡會提醒
59
  print(f"⚠️ 讀取 restaurants.csv 失敗: {e}")
60
 
61
- # --- 2. 資料讀取 (Prompt 資料庫) ---
62
  df_prompts = pd.DataFrame()
63
  PROMPT_COL_NAME = 'Visual_prompt'
64
-
65
  try:
66
  if not df.empty:
67
  df_prompts = df.copy()
68
  if 'Name' in df_prompts.columns:
69
  df_prompts.set_index('Name', inplace=True)
70
- except Exception as e:
71
- print(f"⚠️ 處理 Prompt 資料發生錯誤: {e}")
72
 
73
-
74
- # --- RAG 建置 ---
75
  retriever = None
76
  if not df.empty and 'RAG_Content' in df.columns:
77
  try:
@@ -82,16 +76,14 @@ if not df.empty and 'RAG_Content' in df.columns:
82
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
83
  vectorstore = FAISS.from_documents(docs, embeddings)
84
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
85
- except Exception as e:
86
- print(f"⚠️ RAG 初始化失敗: {e}")
87
 
88
  # ==========================================
89
- # 核心功能函式 (餐廳推薦邏輯)
90
  # ==========================================
91
 
92
  def get_restaurant_data(mood_score_str, food_choice):
93
  if df.empty: return None, True
94
- # 定義分數與 Mood_Tags 的對應關係 (與舊版維持一致)
95
  score_map = {
96
  "1 (心情差)": ["傷心/疲憊", "生氣/發洩"],
97
  "2 (不太好)": ["傷心/疲憊", "生氣/發洩"],
@@ -105,11 +97,10 @@ def get_restaurant_data(mood_score_str, food_choice):
105
  elif food_choice == "吃麵": food_keyword = "麵"
106
 
107
  candidates = df.copy()
108
- if 'Mood_Tags' not in candidates.columns: return df.sample(1).iloc[0], True
109
-
110
  if target_moods:
111
  pattern = "|".join(target_moods)
112
- candidates = candidates[candidates['Mood_Tags'].str.contains(pattern, regex=True, na=False)]
 
113
 
114
  if food_keyword:
115
  candidates = candidates[
@@ -120,23 +111,11 @@ def get_restaurant_data(mood_score_str, food_choice):
120
  if candidates.empty: return df.sample(1).iloc[0], True
121
  return candidates.sample(1).iloc[0], False
122
 
123
- # 注意:因為移除了測驗,這裡我們用 '無測驗' 作為預設結果
124
  def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score):
125
- if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY (系統變數)"
126
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
127
  system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記和心情來推薦餐廳。"
128
- user_msg = f"""
129
- 【使用者狀態】
130
- 心情分數:{mood_score}
131
- 心情日記:{user_diary}
132
- 【推薦餐廳】
133
- 名稱:{restaurant_name}
134
- 資料:{restaurant_detail}
135
- 請完成任務:
136
- 【回應內容】:用繁體中文寫一段溫暖有趣的回覆。
137
- - 結合「日記」與「心情」給予回應。
138
- - 推薦上述餐廳,說明為什麼這家店適合他。
139
- """
140
  try:
141
  response = client.chat.completions.create(
142
  model="llama-3.3-70b-versatile",
@@ -153,24 +132,18 @@ def generate_image_huggingface(prompt):
153
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
154
  except: return None
155
 
156
- # --- 主邏輯 Agent ---
157
- # 移除了 quiz_state 參數
158
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
159
- # 確保 score_input 有值
160
  if not score_input:
161
- yield "⚠️ 請先選擇心情分數,或使用上方相機偵測!", None, ""
162
  return
163
 
164
  restaurant, is_random = get_restaurant_data(score_input, food_input)
165
  if restaurant is None:
166
- yield "資料庫讀取錯誤或為空", None, ""
167
  return
168
 
169
  name = str(restaurant['Name'])
170
  url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
171
- note = "(隨機推薦)" if is_random else ""
172
-
173
- # 1. 處理 RAG
174
  rag_info = str(restaurant.get('RAG_Content', ''))
175
  if retriever:
176
  try:
@@ -178,29 +151,17 @@ def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
178
  if docs: rag_info = "\n".join([d.page_content for d in docs])
179
  except: pass
180
 
181
- # 2. 準備圖片 Prompt
182
- img_prompt = f"Delicious food from {name}, cinematic lighting, 8k, photorealistic"
183
- prompt_source = "⚠️ 預設生成"
184
-
185
- if not df_prompts.empty and PROMPT_COL_NAME in df_prompts.columns:
186
- if name in df_prompts.index:
187
- try:
188
- csv_prompt = df_prompts.loc[name, PROMPT_COL_NAME]
189
- if isinstance(csv_prompt, pd.Series): csv_prompt = csv_prompt.iloc[0]
190
- if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
191
- img_prompt = str(csv_prompt)
192
- prompt_source = "✅ CSV 檔案"
193
- except: pass
194
-
195
- # 3. 呼叫 LLM (移除了 quiz_state)
196
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input)
197
-
198
- # 4. 組合回應
199
- debug_text = ""
200
- if debug_mode:
201
- debug_text = f"\n\n---\n**🛠️ Prompt 來源**: {prompt_source}\n**Prompt**: `{img_prompt}`"
202
-
203
- final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}{debug_text}"
204
  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>'
205
 
206
  yield final_response, None, map_html
@@ -208,16 +169,11 @@ def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
208
  yield final_response, image_output, map_html
209
 
210
  # ==========================================
211
- # [修改] 情緒辨識整合模組
212
  # ==========================================
213
 
214
- # 定義中文字典 (繪圖用)
215
- emotion_text_obj = {
216
- 'angry': '生氣', 'disgust': '噁心', 'fear': '害怕',
217
- 'happy': '開心', 'sad': '難過', 'surprise': '驚訝', 'neutral': '正常'
218
- }
219
 
220
- # 繪圖函式 (維持不變)
221
  def putText(img, x, y, text, size=50, color=(255, 255, 255)):
222
  try:
223
  fontpath = 'NotoSansTC-VariableFont_wght.ttf'
@@ -228,64 +184,68 @@ def putText(img, x, y, text, size=50, color=(255, 255, 255)):
228
  displayText = emotion_text_obj.get(text, text)
229
  draw.text((x, y), displayText, fill=color, font=font)
230
  return np.array(imgPil)
231
- except:
232
- return img
233
 
234
- # ★ 新增:按鈕切換函式 (控制按鈕的文字與顏色)
235
- def toggle_recording(is_recording):
236
- # 切換狀態 (True <-> False)
237
- new_state = not is_recording
238
-
239
- if new_state:
240
- # 如果變成錄製中,按鈕變成紅色「停止」
241
- return new_state, gr.Button(value="🛑 停止錄製 (鎖定結果)", variant="stop")
242
- else:
243
- # 如果變成停止,按鈕變成綠色「開始」
244
- return new_state, gr.Button(value="📸 開始即時偵測", variant="secondary")
245
-
246
- # ★ 修改:偵測邏輯 (加入 is_recording 判斷)
247
- def detect_emotion_stream(frame, is_recording):
248
- # 如果沒有在錄製,或是沒有畫面,就什麼都不更新 (讓畫面凍結在最後一幀)
249
- if not is_recording or frame is None:
250
- return gr.update(), gr.update()
251
-
252
  detected_emotion = "neutral"
253
  mapped_score = "3 (普通)"
254
-
255
  try:
256
- # 1. 辨識情緒
257
  analyze = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
258
  if isinstance(analyze, list): analyze = analyze[0]
259
  detected_emotion = analyze['dominant_emotion']
260
 
261
- # 2. 畫在圖片上
262
  frame = putText(frame, 20, 40, detected_emotion)
263
 
264
- # 3. 分數轉換
265
- if detected_emotion == 'happy':
266
- mapped_score = "5 (超棒)"
267
- elif detected_emotion == 'surprise':
268
- mapped_score = "4 (不錯)"
269
- elif detected_emotion == 'neutral':
270
- mapped_score = "3 (普通)"
271
- elif detected_emotion in ['sad', 'fear']:
272
- mapped_score = "2 (不太好)"
273
- elif detected_emotion in ['angry', 'disgust']:
274
- mapped_score = "1 (心情差)"
275
 
276
- except Exception as e:
277
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- return frame, mapped_score
 
 
 
 
 
 
 
 
 
280
 
281
  # ==========================================
282
- # Gradio 介面建構 (修改版)
283
  # ==========================================
284
 
285
  with gr.Blocks(title="AI 心情食堂") as demo:
286
 
287
- # ★ 1. 定義一個變數來記錄狀態 (預設是 False 停止中)
288
- recording_state = gr.State(value=False)
 
289
 
290
  with gr.Column(visible=True) as main_app_col:
291
  gr.Markdown(f"## 🍱 AI 心情食堂導航")
@@ -294,16 +254,19 @@ with gr.Blocks(title="AI 心情食堂") as demo:
294
  with gr.Row():
295
  # 左側:輸入區
296
  with gr.Column(scale=1):
 
297
 
298
- gr.Markdown("### 📸 步驟 1:心情偵測 (選用)")
299
  with gr.Row():
300
  # 左邊:即時鏡頭
301
  webcam_input = gr.Image(sources=["webcam"], label="即時鏡頭", streaming=True)
302
- # 右邊:結果畫面 (互動模式設為 False)
303
- captured_image = gr.Image(label="即時偵測結果", interactive=False)
304
 
305
- # 按鈕預設為「開始」
306
- detect_btn = gr.Button("📸 開始即時偵測", variant="secondary")
 
 
 
307
 
308
  gr.Markdown("### 📝 步驟 2:確認與補充")
309
  score_input = gr.Radio(
@@ -312,35 +275,42 @@ with gr.Blocks(title="AI 心情食堂") as demo:
312
  value="3 (普通)"
313
  )
314
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
315
- diary_input = gr.Textbox(lines=3, label="3. 心情日記 (選填)", placeholder="例如:今天被老闆罵了,想吃點好料的...")
316
 
317
- debug_mode_btn = gr.Checkbox(label="🔧 顯示 Prompt 除錯資訊", value=False)
318
  submit_btn = gr.Button("🚀 送出給 Agent", variant="primary")
319
 
320
- # 右側:Agent 輸出區
321
  with gr.Column(scale=1):
322
  agent_output = gr.Markdown(label="AI 回應")
323
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
324
  map_output = gr.HTML(label="地圖導航")
325
 
326
- # Events
327
-
328
- # ★ 事件 1:按鈕點擊 -> 切換錄製狀態 -> 更新按鈕樣子
329
- detect_btn.click(
330
- fn=toggle_recording,
331
- inputs=[recording_state],
332
- outputs=[recording_state, detect_btn]
333
  )
334
 
335
- # ★ 事件 2:鏡頭串流 (Stream) -> 只要鏡頭有畫面就會觸發
336
- # 它會檢查 recording_state,如果是 True 才會更新右邊的圖
337
  webcam_input.stream(
338
- fn=detect_emotion_stream,
339
- inputs=[webcam_input, recording_state],
 
 
 
 
 
 
 
 
340
  outputs=[captured_image, score_input]
341
  )
342
 
343
- # 提交按鈕
344
  submit_btn.click(
345
  fn=mood_agent_logic,
346
  inputs=[score_input, food_input, diary_input, debug_mode_btn],
 
20
  warnings.filterwarnings("ignore")
21
 
22
  # ==========================================
23
+ # 0. 環境變數與 RAG 初始化 (維持不變)
24
  # ==========================================
25
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
26
  HF_TOKEN = os.getenv("HF_TOKEN")
27
 
28
+ # --- 資料讀取 ---
29
  df = pd.DataFrame()
30
  try:
 
31
  df = pd.read_csv('restaurants.csv', engine='python')
32
+ df.columns = df.columns.str.strip()
33
 
34
  def classify_mood(row):
35
  name_str = str(row.get('Name', ''))
 
37
  text = (name_str + " " + rag_str).lower()
38
  tags = []
39
  rules = {
40
+ "開心/慶祝": ["牛排", "steak", "pizza", "炸", "甜點", "蛋糕", "buffet", "bistro"],
41
+ "傷心/疲憊": ["粥", "湯", "warm", "麵", "小吃", "豆花", "soup"],
42
+ "生氣/發洩": ["辣", "spicy", "麻辣", "鍋", "curry", "燒肉", "bbq", "fry"],
43
+ "平靜/放鬆": ["cafe", "coffee", "tea", "素食", "brunch", "sushi", "居酒屋"]
44
  }
45
  for mood, keywords in rules.items():
46
  for kw in keywords:
 
51
 
52
  if 'Mood_Tags' not in df.columns:
53
  df['Mood_Tags'] = df.apply(classify_mood, axis=1)
54
+ print("✅ 成功讀取 restaurants.csv")
 
55
  except Exception as e:
 
56
  print(f"⚠️ 讀取 restaurants.csv 失敗: {e}")
57
 
58
+ # --- Prompt 資料庫 ---
59
  df_prompts = pd.DataFrame()
60
  PROMPT_COL_NAME = 'Visual_prompt'
 
61
  try:
62
  if not df.empty:
63
  df_prompts = df.copy()
64
  if 'Name' in df_prompts.columns:
65
  df_prompts.set_index('Name', inplace=True)
66
+ except: pass
 
67
 
68
+ # --- RAG ---
 
69
  retriever = None
70
  if not df.empty and 'RAG_Content' in df.columns:
71
  try:
 
76
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
77
  vectorstore = FAISS.from_documents(docs, embeddings)
78
  retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
79
+ except: pass
 
80
 
81
  # ==========================================
82
+ # 1. 核心功能函式 (餐廳推薦 - 維持不變)
83
  # ==========================================
84
 
85
  def get_restaurant_data(mood_score_str, food_choice):
86
  if df.empty: return None, True
 
87
  score_map = {
88
  "1 (心情差)": ["傷心/疲憊", "生氣/發洩"],
89
  "2 (不太好)": ["傷心/疲憊", "生氣/發洩"],
 
97
  elif food_choice == "吃麵": food_keyword = "麵"
98
 
99
  candidates = df.copy()
 
 
100
  if target_moods:
101
  pattern = "|".join(target_moods)
102
+ if 'Mood_Tags' in candidates.columns:
103
+ candidates = candidates[candidates['Mood_Tags'].str.contains(pattern, regex=True, na=False)]
104
 
105
  if food_keyword:
106
  candidates = candidates[
 
111
  if candidates.empty: return df.sample(1).iloc[0], True
112
  return candidates.sample(1).iloc[0], False
113
 
 
114
  def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score):
115
+ if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
116
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
117
  system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記和心情來推薦餐廳。"
118
+ user_msg = f"心情:{mood_score}\n日記:{user_diary}\n餐廳:{restaurant_name}\n資料:{restaurant_detail}\n請推薦這家餐廳並給予安慰或慶祝。"
 
 
 
 
 
 
 
 
 
 
 
119
  try:
120
  response = client.chat.completions.create(
121
  model="llama-3.3-70b-versatile",
 
132
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
133
  except: return None
134
 
 
 
135
  def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
 
136
  if not score_input:
137
+ yield "⚠️ 請先選擇心情分數!", None, ""
138
  return
139
 
140
  restaurant, is_random = get_restaurant_data(score_input, food_input)
141
  if restaurant is None:
142
+ yield "資料庫讀取錯誤", None, ""
143
  return
144
 
145
  name = str(restaurant['Name'])
146
  url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
 
 
 
147
  rag_info = str(restaurant.get('RAG_Content', ''))
148
  if retriever:
149
  try:
 
151
  if docs: rag_info = "\n".join([d.page_content for d in docs])
152
  except: pass
153
 
154
+ img_prompt = f"Delicious food from {name}, cinematic lighting, 8k"
155
+ if not df_prompts.empty and name in df_prompts.index:
156
+ try:
157
+ val = df_prompts.loc[name, PROMPT_COL_NAME]
158
+ if isinstance(val, pd.Series): val = val.iloc[0]
159
+ if pd.notna(val): img_prompt = str(val)
160
+ except: pass
161
+
 
 
 
 
 
 
 
162
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input)
163
+ debug_text = f"\n\nPrompt: `{img_prompt}`" if debug_mode else ""
164
+ final_response = f"### 🍽️ 推薦:{name}\n\n{ai_text}{debug_text}"
 
 
 
 
 
165
  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>'
166
 
167
  yield final_response, None, map_html
 
169
  yield final_response, image_output, map_html
170
 
171
  # ==========================================
172
+ # 2. 情緒辨識整合模組
173
  # ==========================================
174
 
175
+ emotion_text_obj = {'angry': '生氣', 'disgust': '噁心', 'fear': '害怕', 'happy': '開心', 'sad': '難過', 'surprise': '驚訝', 'neutral': '正常'}
 
 
 
 
176
 
 
177
  def putText(img, x, y, text, size=50, color=(255, 255, 255)):
178
  try:
179
  fontpath = 'NotoSansTC-VariableFont_wght.ttf'
 
184
  displayText = emotion_text_obj.get(text, text)
185
  draw.text((x, y), displayText, fill=color, font=font)
186
  return np.array(imgPil)
187
+ except: return img
 
188
 
189
+ # 核心辨識函式 (重構以供重複使用)
190
+ def process_emotion(frame):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  detected_emotion = "neutral"
192
  mapped_score = "3 (普通)"
 
193
  try:
 
194
  analyze = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False)
195
  if isinstance(analyze, list): analyze = analyze[0]
196
  detected_emotion = analyze['dominant_emotion']
197
 
 
198
  frame = putText(frame, 20, 40, detected_emotion)
199
 
200
+ if detected_emotion == 'happy': mapped_score = "5 (超棒)"
201
+ elif detected_emotion == 'surprise': mapped_score = "4 (不錯)"
202
+ elif detected_emotion == 'neutral': mapped_score = "3 (普通)"
203
+ elif detected_emotion in ['sad', 'fear']: mapped_score = "2 (不太好)"
204
+ elif detected_emotion in ['angry', 'disgust']: mapped_score = "1 (心情差)"
205
+ except: pass
206
+ return frame, mapped_score
 
 
 
 
207
 
208
+ # 單次手動截圖
209
+ def manual_detect(frame):
210
+ if frame is None: return None, None
211
+ return process_emotion(frame)
212
+
213
+ # ★ 新增:切換錄製狀態按鈕邏輯
214
+ def toggle_recording_state(is_recording):
215
+ # 切換 True/False
216
+ new_state = not is_recording
217
+ # 根據新狀態改變按鈕文字
218
+ if new_state:
219
+ return new_state, "⏹️ 停止錄製 (即時偵測中)"
220
+ else:
221
+ return new_state, "🔴 開始錄製 (即時偵測)"
222
+
223
+ # ★ 新增:自動串流偵測 (包含限速邏輯)
224
+ def auto_stream_detect(frame, is_recording, last_time):
225
+ # 如果沒開錄製,或畫面空的,就跳過不做事 (節省資源)
226
+ if not is_recording or frame is None:
227
+ return gr.skip(), gr.skip(), last_time
228
 
229
+ # 限速:距離上次執行不到 1.0 秒,就跳過
230
+ current_time = time.time()
231
+ if current_time - last_time < 1.0:
232
+ return gr.skip(), gr.skip(), last_time
233
+
234
+ # 執行辨識
235
+ processed_frame, score = process_emotion(frame)
236
+
237
+ # 回傳:更新圖片, 更新分數, 更新最後執行時間
238
+ return processed_frame, score, current_time
239
 
240
  # ==========================================
241
+ # 3. Gradio 介面建構
242
  # ==========================================
243
 
244
  with gr.Blocks(title="AI 心情食堂") as demo:
245
 
246
+ # 定義狀態變數 (看不見的變數)
247
+ is_recording = gr.State(False) # 是否正在錄製
248
+ last_process_time = gr.State(0) # 上次偵測的時間
249
 
250
  with gr.Column(visible=True) as main_app_col:
251
  gr.Markdown(f"## 🍱 AI 心情食堂導航")
 
254
  with gr.Row():
255
  # 左側:輸入區
256
  with gr.Column(scale=1):
257
+ gr.Markdown("### 📸 步驟 1:心情偵測")
258
 
 
259
  with gr.Row():
260
  # 左邊:即時鏡頭
261
  webcam_input = gr.Image(sources=["webcam"], label="即時鏡頭", streaming=True)
262
+ # 右邊:偵測結果
263
+ captured_image = gr.Image(label="偵測結果", interactive=False)
264
 
265
+ with gr.Row():
266
+ # 按鈕 1:錄製開關 (會變色變字)
267
+ record_btn = gr.Button("🔴 開始錄製 (即時偵測)", variant="stop")
268
+ # 按鈕 2:單次截圖 (維持不變)
269
+ snapshot_btn = gr.Button("📸 單次截圖", variant="secondary")
270
 
271
  gr.Markdown("### 📝 步驟 2:確認與補充")
272
  score_input = gr.Radio(
 
275
  value="3 (普通)"
276
  )
277
  food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
278
+ diary_input = gr.Textbox(lines=3, label="3. 心情日記", placeholder="選填...")
279
 
280
+ debug_mode_btn = gr.Checkbox(label="🔧 除錯資訊", value=False)
281
  submit_btn = gr.Button("🚀 送出給 Agent", variant="primary")
282
 
283
+ # 右側:輸出區
284
  with gr.Column(scale=1):
285
  agent_output = gr.Markdown(label="AI 回應")
286
  image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
287
  map_output = gr.HTML(label="地圖導航")
288
 
289
+ # --- 事件綁定 ---
290
+
291
+ # 1. 錄製按鈕邏輯:點擊 -> 切換狀態 -> 改變按鈕文字
292
+ record_btn.click(
293
+ fn=toggle_recording_state,
294
+ inputs=[is_recording],
295
+ outputs=[is_recording, record_btn]
296
  )
297
 
298
+ # 2. 串流偵測邏輯:鏡頭畫面改變 -> 檢查狀態 -> (如果錄製中) 執行偵測 -> 更新右邊圖片
 
299
  webcam_input.stream(
300
+ fn=auto_stream_detect,
301
+ inputs=[webcam_input, is_recording, last_process_time],
302
+ outputs=[captured_image, score_input, last_process_time],
303
+ show_progress=False # 隱藏進度條,讓畫面更順暢
304
+ )
305
+
306
+ # 3. 單次截圖邏輯 (維持不變)
307
+ snapshot_btn.click(
308
+ fn=manual_detect,
309
+ inputs=[webcam_input],
310
  outputs=[captured_image, score_input]
311
  )
312
 
313
+ # 4. 送出 Agent
314
  submit_btn.click(
315
  fn=mood_agent_logic,
316
  inputs=[score_input, food_input, diary_input, debug_mode_btn],