idkWhatToUse commited on
Commit
557db1b
·
verified ·
1 Parent(s): b3a3701

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -72
app.py CHANGED
@@ -12,8 +12,7 @@ from transformers import (
12
  # 1. Load recycle data
13
  # =======================================
14
  recycle_data = json.load(open("recycle_data.json", "r", encoding="utf-8"))
15
- label_texts = []
16
- items = []
17
 
18
  for item in recycle_data:
19
  zh = item.get("name", "")
@@ -23,7 +22,7 @@ for item in recycle_data:
23
 
24
 
25
  # =======================================
26
- # 2. Load Q&A data (RAG)
27
  # =======================================
28
  qas = json.load(open("qas.json", "r", encoding="utf-8"))
29
  qa_questions = [q["question"] for q in qas]
@@ -33,7 +32,7 @@ qa_embeddings = embedder.encode(qa_questions, convert_to_tensor=True)
33
 
34
 
35
  # =======================================
36
- # 3. CLIP model
37
  # =======================================
38
  clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
39
  clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
@@ -48,25 +47,82 @@ with torch.no_grad():
48
 
49
 
50
  # =======================================
51
- # 4. Qwen 0.5B CPU-friendly LLM
52
  # =======================================
53
  LLM = "Qwen/Qwen2.5-0.5B-Instruct"
54
-
55
  tok = AutoTokenizer.from_pretrained(LLM)
56
- llm = AutoModelForCausalLM.from_pretrained(
57
- LLM,
58
- torch_dtype=torch.float32
59
- ).to("cpu")
60
 
61
  def llm_reply(prompt):
62
  inputs = tok(prompt, return_tensors="pt")
63
- outputs = llm.generate(**inputs, max_new_tokens=150)
64
  return tok.decode(outputs[0], skip_special_tokens=True)
65
 
66
 
67
  # =======================================
68
- # Helper functions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # =======================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  def classify_image(pil):
71
  inputs = clip_processor(images=pil, return_tensors="pt")
72
  with torch.no_grad():
@@ -74,125 +130,118 @@ def classify_image(pil):
74
  img_emb = img_emb / img_emb.norm(p=2, dim=-1, keepdim=True)
75
  logits = img_emb @ text_embeds.T
76
  probs = logits.softmax(dim=-1)[0]
77
-
78
  idx = torch.argmax(probs).item()
79
  score = float(probs[idx])
80
  return idx, score
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def search_recycle_name(text):
84
- for item in recycle_data:
85
  if item["name"] in text:
86
  return item
87
  return None
88
 
89
 
 
 
 
90
  def rag_search(text):
91
  q_emb = embedder.encode(text, convert_to_tensor=True)
92
  scores = util.cos_sim(q_emb, qa_embeddings)[0]
93
  best_idx = torch.argmax(scores).item()
94
- if scores[best_idx] > 0.7:
 
95
  return qas[best_idx]["answer"]
96
  return None
97
 
98
 
99
- def general_rules():
100
- return (
101
- "以下是一般垃圾分類原則:\n"
102
- "1. 乾淨可分離材質 → 可回收\n"
103
- "2. 污損混合材質 → 一般垃圾\n"
104
- "3. 電器/電池/燈管 → 指定回收\n"
105
- "4. 不確定 → 打 1999 或問清潔隊\n"
106
- )
107
-
108
-
109
  # =======================================
110
- # 5. Main chatbot logic
111
  # =======================================
112
 
113
- # 用來記錄最近一張圖片
114
  global_image = None
115
 
116
  def bot(message, history):
117
-
118
  global global_image
119
 
120
- # -------------------------------
121
- # Case 1: 使用者傳入圖片訊息 (dict)
122
- # -------------------------------
123
  if isinstance(message, dict):
124
-
125
  img = message.get("image", None)
126
  text = message.get("text", "").strip()
127
 
128
- # 若有圖片 → 更新 global_image
129
  if img is not None:
130
  global_image = Image.fromarray(img)
131
 
132
- # 直接做圖片分類
133
  idx, score = classify_image(global_image)
134
  item = items[idx]
 
135
 
136
- reply = (
137
- f"🔍 我推測這是 **{item['name']}** (相似度 {score:.2f})\n\n"
138
- f"♻ {item.get('recyclable','')}\n\n"
139
- f"{item.get('notes','')}"
140
- )
141
-
142
- return reply
143
-
144
- # 若「沒有圖片但有文字」→ 當一般文字詢問處理
145
  message = text
146
 
147
- # -------------------------------
148
- # Case 2: 純文字訊息(string)
149
- # -------------------------------
150
  if isinstance(message, str):
151
-
152
  text = message.strip()
153
 
154
- # 1) 使用者剛傳過圖片 → 使用 global_image 做 context
155
  if global_image is not None:
156
- # 讓使用者追問圖片的分類結果
157
- img_item_idx, _ = classify_image(global_image)
158
- current_item = items[img_item_idx]
159
  if current_item["name"] in text:
160
- return (
161
- f"你是指剛剛的「{current_item['name']}」嗎?\n\n"
162
- f"♻ {current_item.get('recyclable','')}\n\n"
163
- f"{current_item.get('notes','')}"
164
- )
165
 
166
- # 2) 先看 recycle_data 名稱是否命中
167
  item = search_recycle_name(text)
168
  if item:
169
- return (
170
- f"🔍 你詢問的是:{item['name']}\n\n"
171
- f"♻ {item.get('recyclable','')}\n\n"
172
- f"{item.get('notes','')}"
173
- )
174
 
175
- # 3) QAS
176
  ans = rag_search(text)
177
  if ans:
178
- return f"📘 官方資料:\n{ans}"
179
 
180
- # 4) Fallback → LLM
181
- return llm_reply(f"你是一位台灣垃圾分類助理,請回答:{text}")
182
 
183
- return "我無法理解你的訊息"
184
 
185
 
186
  # =======================================
187
- # 6. Chat UI
188
  # =======================================
189
  ui = gr.ChatInterface(
190
  fn=bot,
191
  title="台南垃圾分類智慧助理(圖片 + 多輪聊天)",
192
- description=(
193
- "你可以傳圖片或提問文字,我會查 271 類回收資料、Q&A、並能多輪對話。\n"
194
- "上傳圖片後,你可以繼續追問:例如「那這個托盤呢?」"
195
- ),
196
  multimodal=True,
197
  )
198
 
 
12
  # 1. Load recycle data
13
  # =======================================
14
  recycle_data = json.load(open("recycle_data.json", "r", encoding="utf-8"))
15
+ label_texts, items = [], []
 
16
 
17
  for item in recycle_data:
18
  zh = item.get("name", "")
 
22
 
23
 
24
  # =======================================
25
+ # 2. Load Q&A (RAG)
26
  # =======================================
27
  qas = json.load(open("qas.json", "r", encoding="utf-8"))
28
  qa_questions = [q["question"] for q in qas]
 
32
 
33
 
34
  # =======================================
35
+ # 3. CLIP 用於圖片分類
36
  # =======================================
37
  clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
38
  clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
 
47
 
48
 
49
  # =======================================
50
+ # 4. LLM(Qwen 0.5B)+回答模板
51
  # =======================================
52
  LLM = "Qwen/Qwen2.5-0.5B-Instruct"
 
53
  tok = AutoTokenizer.from_pretrained(LLM)
54
+ llm = AutoModelForCausalLM.from_pretrained(LLM, torch_dtype=torch.float32).to("cpu")
 
 
 
55
 
56
  def llm_reply(prompt):
57
  inputs = tok(prompt, return_tensors="pt")
58
+ outputs = llm.generate(**inputs, max_new_tokens=200)
59
  return tok.decode(outputs[0], skip_special_tokens=True)
60
 
61
 
62
  # =======================================
63
+ # 5. 回答品質:加入「專業垃圾分類助理模板」
64
+ # =======================================
65
+
66
+ def expert_llm_reply(text):
67
+ prompt = f"""
68
+ 你是一位「台灣垃圾分類專家助理」。
69
+ 請用 **自然、生活化、清楚、條列式、友善語氣** 回答問題。
70
+ 遵守規則:
71
+
72
+ - 使用台灣常見分類(紙類、塑膠類、鐵鋁罐、玻璃、其他可回收、一般垃圾、廚餘…)
73
+ - 如可能需要清洗 → 提醒「保持乾淨、不要油膩」
74
+ - 如可能需要壓扁、拆蓋 → 主動提醒
75
+ - 如不同縣市規則不同 → 說「各縣市略有差異」
76
+ - 最後提供 1 個附加小提醒
77
+
78
+ 使用者問題:{text}
79
+
80
+ 請直接回答:
81
+ """
82
+ return llm_reply(prompt)
83
+
84
+
85
+ # =======================================
86
+ # 6. 額外知識庫(讓回答更像真人)
87
  # =======================================
88
+
89
+ extra_rules = {
90
+ "寶特瓶": [
91
+ "瓶身要簡單沖洗乾淨",
92
+ "可壓扁節省空間",
93
+ "瓶蓋需旋開分開丟(塑膠類)",
94
+ "標籤可保留或拆除都可以"
95
+ ],
96
+ "鋁箔包": [
97
+ "要沖洗乾淨避免發臭",
98
+ "記得壓扁更好回收",
99
+ "屬於飲料紙容器類,可回收"
100
+ ],
101
+ "外帶杯": [
102
+ "杯身要沖乾淨",
103
+ "若是紙杯 → 紙類回收",
104
+ "若是塑膠杯 → 塑膠類回收",
105
+ "吸管為一般垃圾"
106
+ ],
107
+ "餐盒": [
108
+ "若為乾淨塑膠 → 可回收",
109
+ "若油膩、難清洗 → 一般垃圾",
110
+ "盒蓋通常可回收(塑膠)"
111
+ ],
112
+ }
113
+
114
+
115
+ def add_extra_tips(item_name):
116
+ if item_name not in extra_rules:
117
+ return ""
118
+ tips = "\n".join(f"- {t}" for t in extra_rules[item_name])
119
+ return f"\n🔧 **小提醒:**\n{tips}"
120
+
121
+
122
+ # =======================================
123
+ # 7. 圖片分類 + 回答模板
124
+ # =======================================
125
+
126
  def classify_image(pil):
127
  inputs = clip_processor(images=pil, return_tensors="pt")
128
  with torch.no_grad():
 
130
  img_emb = img_emb / img_emb.norm(p=2, dim=-1, keepdim=True)
131
  logits = img_emb @ text_embeds.T
132
  probs = logits.softmax(dim=-1)[0]
 
133
  idx = torch.argmax(probs).item()
134
  score = float(probs[idx])
135
  return idx, score
136
 
137
 
138
+ def smart_answer(item, score):
139
+ name = item["name"]
140
+ rec = item.get("recyclable", "")
141
+ notes = item.get("notes", "")
142
+
143
+ return f"""
144
+ 🟢 **辨識結果**
145
+ 我推測這張照片中的物品是 **{name}**
146
+ (相似度:**{score:.2f}**)
147
+
148
+ ♻ **是否可回收**
149
+ {rec}
150
+
151
+ 📌 **補充說明**
152
+ {notes}
153
+ {add_extra_tips(name)}
154
+
155
+ 有需要我可以繼續告訴你:
156
+ - 要不要清洗?
157
+ - 要不要壓扁?
158
+ - 某些配件要不要拆?
159
+ 都可以問我喔!
160
+ """
161
+
162
+
163
+ # =======================================
164
+ # 8. 搜尋 recycle_data 名稱
165
+ # =======================================
166
  def search_recycle_name(text):
167
+ for item in items:
168
  if item["name"] in text:
169
  return item
170
  return None
171
 
172
 
173
+ # =======================================
174
+ # 9. RAG 搜尋官方 Q&A
175
+ # =======================================
176
  def rag_search(text):
177
  q_emb = embedder.encode(text, convert_to_tensor=True)
178
  scores = util.cos_sim(q_emb, qa_embeddings)[0]
179
  best_idx = torch.argmax(scores).item()
180
+
181
+ if float(scores[best_idx]) > 0.70:
182
  return qas[best_idx]["answer"]
183
  return None
184
 
185
 
 
 
 
 
 
 
 
 
 
 
186
  # =======================================
187
+ # 10. Chatbot 主邏輯
188
  # =======================================
189
 
 
190
  global_image = None
191
 
192
  def bot(message, history):
 
193
  global global_image
194
 
195
+ # 如果含圖片
 
 
196
  if isinstance(message, dict):
 
197
  img = message.get("image", None)
198
  text = message.get("text", "").strip()
199
 
200
+ # 上傳圖片 → 更新 context
201
  if img is not None:
202
  global_image = Image.fromarray(img)
203
 
 
204
  idx, score = classify_image(global_image)
205
  item = items[idx]
206
+ return smart_answer(item, score)
207
 
208
+ # 無圖片但有文字 → 當一般文字處理
 
 
 
 
 
 
 
 
209
  message = text
210
 
211
+ # 純文字
 
 
212
  if isinstance(message, str):
 
213
  text = message.strip()
214
 
215
+ # 若有上一張圖片 → 可以追問
216
  if global_image is not None:
217
+ idx, _ = classify_image(global_image)
218
+ current_item = items[idx]
 
219
  if current_item["name"] in text:
220
+ return smart_answer(current_item, 0.99)
 
 
 
 
221
 
222
+ # recycle_data 查詢
223
  item = search_recycle_name(text)
224
  if item:
225
+ return smart_answer(item, 0.99)
 
 
 
 
226
 
227
+ # RAG官方資料
228
  ans = rag_search(text)
229
  if ans:
230
+ return f"📘 **官方資料:**\n{ans}"
231
 
232
+ # fallback → LLM 專業回答
233
+ return expert_llm_reply(text)
234
 
235
+ return "我好像不太理解你的訊息,可以再說一次嗎?"
236
 
237
 
238
  # =======================================
239
+ # 11. Gradio Chat UI
240
  # =======================================
241
  ui = gr.ChatInterface(
242
  fn=bot,
243
  title="台南垃圾分類智慧助理(圖片 + 多輪聊天)",
244
+ description="你可以傳圖片或提問,我會查看 270+ 類回收資料 + 官方 Q&A + 多輪對話記憶。",
 
 
 
245
  multimodal=True,
246
  )
247