3v324v23 commited on
Commit
6c765e5
·
1 Parent(s): 622d516
Files changed (2) hide show
  1. example01.py +288 -0
  2. gemini.py +19 -4
example01.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===東吳大學資料系 2025 年 LINEBOT ===
2
+ import logging
3
+ import os
4
+ import tempfile
5
+ import uuid
6
+ from io import BytesIO
7
+
8
+ import markdown
9
+ from bs4 import BeautifulSoup
10
+ from flask import Flask, abort, request, send_from_directory
11
+
12
+ from google import genai
13
+ from google.genai import types
14
+ from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
15
+
16
+ from linebot.v3 import WebhookHandler
17
+ from linebot.v3.exceptions import InvalidSignatureError
18
+ from linebot.v3.messaging import (
19
+ ApiClient,
20
+ Configuration,
21
+ ImageMessage,
22
+ MessagingApi,
23
+ MessagingApiBlob,
24
+ ReplyMessageRequest,
25
+ TextMessage,
26
+ )
27
+ from linebot.v3.webhooks import (
28
+ ImageMessageContent,
29
+ MessageEvent,
30
+ TextMessageContent,
31
+ )
32
+
33
+ from PIL import Image
34
+ from linebot.v3.webhooks import VideoMessageContent
35
+ import requests
36
+
37
+ # === 初始化 Google Gemini ===
38
+ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
39
+ client = genai.Client(api_key=GOOGLE_API_KEY)
40
+
41
+ google_search_tool = Tool(
42
+ google_search=GoogleSearch()
43
+ )
44
+
45
+ chat = client.chats.create(
46
+ model="gemini-2.5-pro-preview-05-06",
47
+ config=GenerateContentConfig(
48
+ system_instruction="你是一個中文的AI助手,關於所有問題,請用繁體中文文言文回答",
49
+ tools=[google_search_tool],
50
+ response_modalities=["TEXT"],
51
+ )
52
+ )
53
+
54
+ # === 初始設定 ===
55
+ static_tmp_path = tempfile.gettempdir()
56
+ os.makedirs(static_tmp_path, exist_ok=True)
57
+ base_url = os.getenv("SPACE_HOST") # e.g., "your-space-name.hf.space"
58
+
59
+ # === Flask 應用初始化 ===
60
+ app = Flask(__name__)
61
+ logging.basicConfig(
62
+ level=logging.INFO,
63
+ format="%(asctime)s - %(levelname)s - %(message)s"
64
+ )
65
+ app.logger.setLevel(logging.INFO)
66
+
67
+ channel_secret = os.environ.get("YOUR_CHANNEL_SECRET")
68
+ channel_access_token = os.environ.get("YOUR_CHANNEL_ACCESS_TOKEN")
69
+
70
+ configuration = Configuration(access_token=channel_access_token)
71
+ handler = WebhookHandler(channel_secret)
72
+
73
+
74
+ # === AI Query 包裝 ===
75
+ def query(payload):
76
+ response = chat.send_message(message=payload)
77
+ return response.text
78
+
79
+
80
+ # === 靜態圖檔路由 ===
81
+ @app.route("/images/<filename>")
82
+ def serve_image(filename):
83
+ return send_from_directory(static_tmp_path, filename)
84
+
85
+
86
+ # === LINE Webhook 接收端點 ===
87
+ @app.route("/")
88
+ def home():
89
+ return {"message": "Line Webhook Server"}
90
+
91
+
92
+ @app.route("/", methods=["POST"])
93
+ def callback():
94
+ signature = request.headers.get("X-Line-Signature")
95
+ body = request.get_data(as_text=True)
96
+ app.logger.info(f"Request body: {body}")
97
+
98
+ try:
99
+ handler.handle(body, signature)
100
+ except InvalidSignatureError:
101
+ app.logger.warning("Invalid signature. Please check channel credentials.")
102
+ abort(400)
103
+
104
+ return "OK"
105
+
106
+
107
+ # === 處理文字訊息 ===
108
+ @handler.add(MessageEvent, message=TextMessageContent)
109
+ def handle_text_message(event):
110
+ user_input = event.message.text.strip()
111
+ if user_input.startswith("AI "):
112
+ prompt = user_input[3:].strip()
113
+ try:
114
+ # 使用 Gemini 生成圖片
115
+ response = client.models.generate_content(
116
+ model="gemini-2.0-flash-exp-image-generation",
117
+ contents=prompt,
118
+ config=types.GenerateContentConfig(
119
+ response_modalities=["TEXT", "IMAGE"]
120
+ ),
121
+ )
122
+
123
+ # 處理回應中的圖片
124
+ for part in response.candidates[0].content.parts:
125
+ if part.inline_data is not None:
126
+ image = Image.open(BytesIO(part.inline_data.data))
127
+ filename = f"{uuid.uuid4().hex}.png"
128
+ image_path = os.path.join(static_tmp_path, filename)
129
+ image.save(image_path)
130
+
131
+ # 建立圖片的公開 URL
132
+ image_url = f"https://{base_url}/images/{filename}"
133
+ app.logger.info(f"Image URL: {image_url}")
134
+
135
+ # 回傳圖片給 LINE 使用者
136
+ with ApiClient(configuration) as api_client:
137
+ line_bot_api = MessagingApi(api_client)
138
+ line_bot_api.reply_message(
139
+ ReplyMessageRequest(
140
+ reply_token=event.reply_token,
141
+ messages=[
142
+ ImageMessage(
143
+ original_content_url=image_url,
144
+ preview_image_url=image_url,
145
+ )
146
+ ],
147
+ )
148
+ )
149
+
150
+ except Exception as e:
151
+ app.logger.error(f"Gemini API error: {e}")
152
+ with ApiClient(configuration) as api_client:
153
+ line_bot_api = MessagingApi(api_client)
154
+ line_bot_api.reply_message(
155
+ ReplyMessageRequest(
156
+ reply_token=event.reply_token,
157
+ messages=[TextMessage(text="抱歉,生成圖片時發生錯誤。")],
158
+ )
159
+ )
160
+ else:
161
+ with ApiClient(configuration) as api_client:
162
+ line_bot_api = MessagingApi(api_client)
163
+ response = query(event.message.text)
164
+ html_msg = markdown.markdown(response)
165
+ soup = BeautifulSoup(html_msg, "html.parser")
166
+
167
+ line_bot_api.reply_message_with_http_info(
168
+ ReplyMessageRequest(
169
+ reply_token=event.reply_token,
170
+ messages=[TextMessage(text=soup.get_text())],
171
+ )
172
+ )
173
+
174
+
175
+ # === 處理圖片訊息 ===
176
+ @handler.add(MessageEvent, message=ImageMessageContent)
177
+ def handle_image_message(event):
178
+ # === 以下是處理圖片回傳部分 === #
179
+ with ApiClient(configuration) as api_client:
180
+ blob_api = MessagingApiBlob(api_client)
181
+ content = blob_api.get_message_content(message_id=event.message.id)
182
+
183
+ # Step 4:將圖片存到本地端
184
+ with tempfile.NamedTemporaryFile(
185
+ dir=static_tmp_path, suffix=".jpg", delete=False
186
+ ) as tf:
187
+ tf.write(content)
188
+ filename = os.path.basename(tf.name)
189
+
190
+ image_url = f"https://{base_url}/images/{filename}"
191
+
192
+ app.logger.info(f"Image URL: {image_url}")
193
+
194
+ # === 以下是解釋圖片 === #
195
+ image = Image.open(tf.name)
196
+ response = client.models.generate_content(
197
+ model="gemini-2.0-flash",
198
+ config=types.GenerateContentConfig(
199
+ system_instruction="你是一個資深的面相命理師,如果有人上手掌的照片,就幫他解釋手相,如果上傳正面臉部的照片,就幫他解釋面相,照片要先去背,如果是一般的照片,就正常說明照片不用算命,請用繁體中文回答",
200
+ response_modalities=["TEXT"],
201
+ tools=[google_search_tool],
202
+ ),
203
+ contents=[image, "用繁體中文描述這張圖片"],
204
+ )
205
+ app.logger.info(response.text)
206
+
207
+ # === 以下是回傳圖片部分 === #
208
+ with ApiClient(configuration) as api_client:
209
+ line_bot_api = MessagingApi(api_client)
210
+ line_bot_api.reply_message(
211
+ ReplyMessageRequest(
212
+ reply_token=event.reply_token,
213
+ messages=[
214
+ ImageMessage(
215
+ original_content_url=image_url, preview_image_url=image_url
216
+ ),
217
+ TextMessage(text=response.text),
218
+ ],
219
+ )
220
+ )
221
+
222
+ # === 處理影片訊息 ===
223
+
224
+ @handler.add(MessageEvent, message=VideoMessageContent)
225
+ def handle_video_message(event):
226
+ # 下載影片內容
227
+ with ApiClient(configuration) as api_client:
228
+ blob_api = MessagingApiBlob(api_client)
229
+ video_data = blob_api.get_message_content(message_id=event.message.id)
230
+
231
+ # 儲存影片到本地
232
+ if video_data is None:
233
+ err_msg = "抱歉,無法取得影片內容。"
234
+ app.logger.error(err_msg)
235
+ with ApiClient(configuration) as api_client:
236
+ line_bot_api = MessagingApi(api_client)
237
+ line_bot_api.reply_message(
238
+ ReplyMessageRequest(
239
+ reply_token=event.reply_token,
240
+ messages=[TextMessage(text=err_msg)]
241
+ )
242
+ )
243
+ return
244
+
245
+ with tempfile.NamedTemporaryFile(
246
+ dir=static_tmp_path, suffix=".mp4", delete=False
247
+ ) as tf:
248
+ tf.write(video_data)
249
+ filename = os.path.basename(tf.name)
250
+
251
+ video_url = f"https://{base_url}/images/{filename}"
252
+ app.logger.info(f"Video URL: {video_url}")
253
+
254
+ # 影片說明
255
+ try:
256
+ video_response = requests.get(video_url)
257
+ video_response.raise_for_status()
258
+ video_data = video_response.content
259
+
260
+ response = client.models.generate_content(
261
+ model="gemini-2.5-flash-preview-05-20",
262
+ config=types.GenerateContentConfig(
263
+ system_instruction="你是一個專業的影片解說員,請用繁體中文簡要說明這段影片的內容。",
264
+ response_modalities=["TEXT"],
265
+ tools=[google_search_tool],
266
+ ),
267
+ contents=[{"mime_type": "video/mp4", "data": video_data}, "用繁體中文描述這段影片"],
268
+ )
269
+ description = response.text
270
+ except Exception as e:
271
+ app.logger.error(f"Gemini API error (video): {e}")
272
+ description = "抱歉,無法解釋這段影片內容。"
273
+ except Exception as e:
274
+ app.logger.error(f"Gemini API error (video): {e}")
275
+ description = "抱歉,無法解釋這段影片內容。"
276
+
277
+ # 回傳影片連結與說明
278
+ with ApiClient(configuration) as api_client:
279
+ line_bot_api = MessagingApi(api_client)
280
+ line_bot_api.reply_message(
281
+ ReplyMessageRequest(
282
+ reply_token=event.reply_token,
283
+ messages=[
284
+ TextMessage(text=f"影片連結:{video_url}"),
285
+ TextMessage(text=description),
286
+ ],
287
+ )
288
+ )
gemini.py CHANGED
@@ -225,13 +225,26 @@ def handle_video_message(event):
225
  # 下載影片內容
226
  with ApiClient(configuration) as api_client:
227
  blob_api = MessagingApiBlob(api_client)
228
- content = blob_api.get_message_content(message_id=event.message.id)
229
 
230
  # 儲存影片到本地
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  with tempfile.NamedTemporaryFile(
232
  dir=static_tmp_path, suffix=".mp4", delete=False
233
  ) as tf:
234
- tf.write(content)
235
  filename = os.path.basename(tf.name)
236
 
237
  video_url = f"https://{base_url}/images/{filename}"
@@ -239,14 +252,16 @@ def handle_video_message(event):
239
 
240
  # 影片說明
241
  try:
 
 
242
  response = client.models.generate_content(
243
- model="gemini-2.0-flash",
244
  config=types.GenerateContentConfig(
245
  system_instruction="你是一個專業的影片解說員,請用繁體中文簡要說明這段影片的內容。",
246
  response_modalities=["TEXT"],
247
  tools=[google_search_tool],
248
  ),
249
- contents=[{"mime_type": "video/mp4", "data": content}, "用繁體中文描述這段影片"],
250
  )
251
  description = response.text
252
  except Exception as e:
 
225
  # 下載影片內容
226
  with ApiClient(configuration) as api_client:
227
  blob_api = MessagingApiBlob(api_client)
228
+ video_data = blob_api.get_message_content(message_id=event.message.id)
229
 
230
  # 儲存影片到本地
231
+ if video_data is None:
232
+ err_msg = "抱歉,無法取得影片內容。"
233
+ app.logger.error(err_msg)
234
+ with ApiClient(configuration) as api_client:
235
+ line_bot_api = MessagingApi(api_client)
236
+ line_bot_api.reply_message(
237
+ ReplyMessageRequest(
238
+ reply_token=event.reply_token,
239
+ messages=[TextMessage(text=err_msg)]
240
+ )
241
+ )
242
+ return
243
+
244
  with tempfile.NamedTemporaryFile(
245
  dir=static_tmp_path, suffix=".mp4", delete=False
246
  ) as tf:
247
+ tf.write(video_data)
248
  filename = os.path.basename(tf.name)
249
 
250
  video_url = f"https://{base_url}/images/{filename}"
 
252
 
253
  # 影片說明
254
  try:
255
+ from io import BytesIO
256
+ video_bytes = BytesIO(video_data)
257
  response = client.models.generate_content(
258
+ model="gemini-2.5-flash-preview-05-20",
259
  config=types.GenerateContentConfig(
260
  system_instruction="你是一個專業的影片解說員,請用繁體中文簡要說明這段影片的內容。",
261
  response_modalities=["TEXT"],
262
  tools=[google_search_tool],
263
  ),
264
+ contents=[video_bytes, "用繁體中文描述這段影片"],
265
  )
266
  description = response.text
267
  except Exception as e: