refactor
Browse files
app.py
CHANGED
@@ -16,26 +16,55 @@ from initializer import initialize_clients, initialize_password
|
|
16 |
GCS_SERVICE, GENAI_CLIENT = initialize_clients()
|
17 |
GCS_CLIENT = GCS_SERVICE.client
|
18 |
|
|
|
19 |
PASSWORD = initialize_password()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
|
|
21 |
def toggle_visibility(toggle_value):
|
22 |
return gr.update(visible=toggle_value)
|
23 |
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
"
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
def get_youtube_title_from_gemini(url):
|
41 |
"""使用 Gemini 獲取 YouTube 標題"""
|
@@ -108,6 +137,7 @@ def get_youtube_title(url):
|
|
108 |
print(f"獲取標題失敗: {str(e)}")
|
109 |
return url
|
110 |
|
|
|
111 |
def add_to_file_list(file, file_list):
|
112 |
if file:
|
113 |
temp_dir = tempfile.gettempdir()
|
@@ -117,27 +147,96 @@ def add_to_file_list(file, file_list):
|
|
117 |
display_list = [os.path.basename(path) if os.path.basename(path) else path for path in file_list]
|
118 |
return gr.update(choices=display_list), None
|
119 |
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
126 |
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
|
|
|
|
|
141 |
def generate_transcript(youtube_link):
|
142 |
print(f"\n開始生成 YouTube 逐字稿: {youtube_link}")
|
143 |
try:
|
@@ -198,25 +297,40 @@ def generate_summary(transcript):
|
|
198 |
try:
|
199 |
print("\n開始生成摘要...")
|
200 |
model = "gemini-2.0-flash-exp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
contents = [
|
202 |
types.Content(
|
203 |
role="user",
|
204 |
parts=[
|
205 |
-
types.Part.from_text(
|
206 |
-
f"""請根據以下逐字稿生成重點摘要,以條列方式呈現主要觀點:
|
207 |
-
|
208 |
-
{transcript}
|
209 |
-
|
210 |
-
請以下列格式輸出:
|
211 |
-
# 主要觀點:
|
212 |
-
1. [重點1]
|
213 |
-
2. [重點2]
|
214 |
-
...
|
215 |
-
|
216 |
-
# 結論:
|
217 |
-
[整體結論]
|
218 |
-
"""
|
219 |
-
)
|
220 |
]
|
221 |
)
|
222 |
]
|
@@ -227,107 +341,12 @@ def generate_summary(transcript):
|
|
227 |
)
|
228 |
|
229 |
print("摘要生成完成!")
|
230 |
-
|
|
|
231 |
except Exception as e:
|
232 |
print(f"\n生成摘要時發生錯誤: {str(e)}")
|
233 |
raise
|
234 |
|
235 |
-
def process_all_files(file_list):
|
236 |
-
"""處理所有選中的文件"""
|
237 |
-
if not file_list:
|
238 |
-
return "請選擇要處理的文件", ""
|
239 |
-
|
240 |
-
all_text = []
|
241 |
-
status_messages = []
|
242 |
-
|
243 |
-
for item in file_list:
|
244 |
-
try:
|
245 |
-
if "|||" in item:
|
246 |
-
# YouTube 連結
|
247 |
-
title, url = item.split("|||")
|
248 |
-
print(f"處理 YouTube: {title}")
|
249 |
-
try:
|
250 |
-
transcript = generate_transcript(url)
|
251 |
-
if transcript:
|
252 |
-
all_text.append(f"=== {title} ===\n{transcript}")
|
253 |
-
status_messages.append(f"🟢 成功處理 YouTube 影片:{title}")
|
254 |
-
else:
|
255 |
-
status_messages.append(f"🔴 無法獲取影片逐字稿:{title}")
|
256 |
-
except Exception as e:
|
257 |
-
if "無法取得影片資訊" in str(e):
|
258 |
-
# 可能是影片標題問題,但還是有內容
|
259 |
-
all_text.append(f"=== YouTube 影片 ===\n{e.transcript if hasattr(e, 'transcript') else ''}")
|
260 |
-
status_messages.append(f"🟡 影片資訊不完整,但已處理內容:{url}")
|
261 |
-
else:
|
262 |
-
status_messages.append(f"🔴 處理失敗:{title}({str(e)})")
|
263 |
-
else:
|
264 |
-
# 本地文件
|
265 |
-
filename = os.path.basename(item)
|
266 |
-
print(f"處理文件: {filename}")
|
267 |
-
try:
|
268 |
-
with open(item, 'r', encoding='utf-8') as f:
|
269 |
-
content = f.read()
|
270 |
-
try:
|
271 |
-
# 嘗試解碼文件名
|
272 |
-
decoded_name = filename.encode('latin1').decode('utf-8')
|
273 |
-
all_text.append(f"=== {decoded_name} ===\n{content}")
|
274 |
-
status_messages.append(f"🟢 成功處理文件:{decoded_name}")
|
275 |
-
except:
|
276 |
-
# 文件名有問題,但內容可用
|
277 |
-
all_text.append(f"=== 文件內容 ===\n{content}")
|
278 |
-
status_messages.append(f"🟡 文件名稱無法正確顯示,但已處理內容:{filename}")
|
279 |
-
except UnicodeDecodeError:
|
280 |
-
try:
|
281 |
-
# 嘗試其他編碼
|
282 |
-
for encoding in ['big5', 'gbk', 'shift-jis']:
|
283 |
-
try:
|
284 |
-
with open(item, 'r', encoding=encoding) as f:
|
285 |
-
content = f.read()
|
286 |
-
all_text.append(f"=== {filename} ===\n{content}")
|
287 |
-
status_messages.append(f"🟡 使用 {encoding} 編碼成功讀取文件:{filename}")
|
288 |
-
break
|
289 |
-
except:
|
290 |
-
continue
|
291 |
-
else:
|
292 |
-
status_messages.append(f"🔴 無法讀取文件內容:{filename}(編碼問題)")
|
293 |
-
except Exception as e:
|
294 |
-
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
295 |
-
except Exception as e:
|
296 |
-
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
297 |
-
except Exception as e:
|
298 |
-
status_messages.append(f"🔴 處理失敗:{item}({str(e)})")
|
299 |
-
|
300 |
-
if not all_text:
|
301 |
-
return "❌ 沒有成功處理任何文件", ""
|
302 |
-
|
303 |
-
# 合併所有文本
|
304 |
-
combined_text = "\n\n".join(all_text)
|
305 |
-
status_text = "\n".join(status_messages)
|
306 |
-
|
307 |
-
return f"處理完成\n{status_text}", combined_text
|
308 |
-
|
309 |
-
def process_with_auth(password, file_list, file_display):
|
310 |
-
"""帶密碼驗證的文件處理"""
|
311 |
-
if not file_display: # 使用 file_display 而不是 file_list
|
312 |
-
return "請選擇要處理的文件", "", gr.update(visible=False)
|
313 |
-
|
314 |
-
if password != PASSWORD:
|
315 |
-
return "���輸入正確的密碼", "", gr.update(visible=False)
|
316 |
-
|
317 |
-
# 根據顯示的選項找到對應的完整項目
|
318 |
-
selected_files = []
|
319 |
-
for item in file_list:
|
320 |
-
if "|||" in item:
|
321 |
-
title = item.split("|||")[0]
|
322 |
-
if title in file_display:
|
323 |
-
selected_files.append(item)
|
324 |
-
else:
|
325 |
-
if os.path.basename(item) in file_display:
|
326 |
-
selected_files.append(item)
|
327 |
-
|
328 |
-
result_text, transcript_text = process_all_files(selected_files)
|
329 |
-
return result_text, transcript_text, gr.update(visible=True)
|
330 |
-
|
331 |
def on_summary_click(transcript):
|
332 |
if not transcript:
|
333 |
return "請先上傳文件或輸入 YouTube 連結並處理完成後再生成摘要。"
|
|
|
16 |
GCS_SERVICE, GENAI_CLIENT = initialize_clients()
|
17 |
GCS_CLIENT = GCS_SERVICE.client
|
18 |
|
19 |
+
# 密碼
|
20 |
PASSWORD = initialize_password()
|
21 |
+
def process_with_auth(password, file_list, file_display):
|
22 |
+
"""帶密碼驗證的文件處理"""
|
23 |
+
if not file_display: # 使用 file_display 而不是 file_list
|
24 |
+
return "請選擇要處理的文件", "", gr.update(visible=False)
|
25 |
+
|
26 |
+
if password != PASSWORD:
|
27 |
+
return "請輸入正確的密碼", "", gr.update(visible=False)
|
28 |
+
|
29 |
+
# 根據顯示的選項找到對應的完整項目
|
30 |
+
selected_files = []
|
31 |
+
for item in file_list:
|
32 |
+
if "|||" in item:
|
33 |
+
title = item.split("|||")[0]
|
34 |
+
if title in file_display:
|
35 |
+
selected_files.append(item)
|
36 |
+
else:
|
37 |
+
if os.path.basename(item) in file_display:
|
38 |
+
selected_files.append(item)
|
39 |
+
|
40 |
+
result_text, transcript_text = process_all_files(selected_files)
|
41 |
+
return result_text, transcript_text, gr.update(visible=True)
|
42 |
|
43 |
+
# UI 收合
|
44 |
def toggle_visibility(toggle_value):
|
45 |
return gr.update(visible=toggle_value)
|
46 |
|
47 |
+
# 來源 youtube
|
48 |
+
def add_youtube_to_list(youtube_link, file_list):
|
49 |
+
if not youtube_link:
|
50 |
+
return gr.update(choices=[item.split("|||")[0] if "|||" in item else os.path.basename(item) for item in file_list]), ""
|
51 |
+
|
52 |
+
# 獲取標題
|
53 |
+
title = get_youtube_title(youtube_link)
|
54 |
+
|
55 |
+
# 確保 URL 格式完整
|
56 |
+
if not youtube_link.startswith('http'):
|
57 |
+
if 'watch?v=' in youtube_link:
|
58 |
+
youtube_link = f'https://www.youtube.com/{youtube_link}'
|
59 |
+
else:
|
60 |
+
youtube_link = f'https://www.youtube.com/watch?v={youtube_link}'
|
61 |
+
|
62 |
+
# 存儲格式:[title]|||[url]
|
63 |
+
file_list.append(f"{title}|||{youtube_link}")
|
64 |
+
display_list = [item.split("|||")[0] if "|||" in item else os.path.basename(item) for item in file_list]
|
65 |
+
print(f"File list: {file_list}")
|
66 |
+
print(f"Display list: {display_list}")
|
67 |
+
return file_list, ""
|
68 |
|
69 |
def get_youtube_title_from_gemini(url):
|
70 |
"""使用 Gemini 獲取 YouTube 標題"""
|
|
|
137 |
print(f"獲取標題失敗: {str(e)}")
|
138 |
return url
|
139 |
|
140 |
+
# 上傳檔案
|
141 |
def add_to_file_list(file, file_list):
|
142 |
if file:
|
143 |
temp_dir = tempfile.gettempdir()
|
|
|
147 |
display_list = [os.path.basename(path) if os.path.basename(path) else path for path in file_list]
|
148 |
return gr.update(choices=display_list), None
|
149 |
|
150 |
+
# RAG 處理
|
151 |
+
def process_all_files(file_list):
|
152 |
+
"""處理所有選中的文件"""
|
153 |
+
if not file_list:
|
154 |
+
return "請選擇要處理的文件", ""
|
155 |
+
|
156 |
+
all_text = []
|
157 |
+
status_messages = []
|
158 |
|
159 |
+
for item in file_list:
|
160 |
+
try:
|
161 |
+
if "|||" in item:
|
162 |
+
# YouTube 連結
|
163 |
+
title, url = item.split("|||")
|
164 |
+
print(f"處理 YouTube: {title}")
|
165 |
+
try:
|
166 |
+
transcript = generate_transcript(url)
|
167 |
+
if transcript:
|
168 |
+
all_text.append(f"=== {title} ===\n{transcript}")
|
169 |
+
status_messages.append(f"🟢 成功處理 YouTube 影片:{title}")
|
170 |
+
else:
|
171 |
+
status_messages.append(f"🔴 無法獲取影片逐字稿:{title}")
|
172 |
+
except Exception as e:
|
173 |
+
if "無法取得影片資訊" in str(e):
|
174 |
+
# 可能是影片標題問題,但還是有內容
|
175 |
+
all_text.append(f"=== YouTube 影片 ===\n{e.transcript if hasattr(e, 'transcript') else ''}")
|
176 |
+
status_messages.append(f"🟡 影片資訊不完整,但已處理內容:{url}")
|
177 |
+
else:
|
178 |
+
status_messages.append(f"🔴 處理失敗:{title}({str(e)})")
|
179 |
+
else:
|
180 |
+
# 本地文件
|
181 |
+
filename = os.path.basename(item)
|
182 |
+
print(f"處理文件: {filename}")
|
183 |
+
try:
|
184 |
+
with open(item, 'r', encoding='utf-8') as f:
|
185 |
+
content = f.read()
|
186 |
+
try:
|
187 |
+
# 嘗試解碼文件名
|
188 |
+
decoded_name = filename.encode('latin1').decode('utf-8')
|
189 |
+
all_text.append(f"=== {decoded_name} ===\n{content}")
|
190 |
+
status_messages.append(f"🟢 成功處理文件:{decoded_name}")
|
191 |
+
except:
|
192 |
+
# 文件名有問題,但內容可用
|
193 |
+
all_text.append(f"=== 文件內容 ===\n{content}")
|
194 |
+
status_messages.append(f"🟡 文件名稱無法正確顯示,但已處理內容:{filename}")
|
195 |
+
except UnicodeDecodeError:
|
196 |
+
try:
|
197 |
+
# 嘗試其他編碼
|
198 |
+
for encoding in ['big5', 'gbk', 'shift-jis']:
|
199 |
+
try:
|
200 |
+
with open(item, 'r', encoding=encoding) as f:
|
201 |
+
content = f.read()
|
202 |
+
all_text.append(f"=== {filename} ===\n{content}")
|
203 |
+
status_messages.append(f"🟡 使用 {encoding} 編碼成功讀取文件:{filename}")
|
204 |
+
break
|
205 |
+
except:
|
206 |
+
continue
|
207 |
+
else:
|
208 |
+
status_messages.append(f"🔴 無法讀取文件內容:{filename}(編碼問題)")
|
209 |
+
except Exception as e:
|
210 |
+
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
211 |
+
except Exception as e:
|
212 |
+
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
213 |
+
except Exception as e:
|
214 |
+
status_messages.append(f"🔴 處理失敗:{item}({str(e)})")
|
215 |
+
|
216 |
+
if not all_text:
|
217 |
+
return "❌ 沒有成功處理任何文件", ""
|
218 |
+
|
219 |
+
# 合併所有文本
|
220 |
+
combined_text = "\n\n".join(all_text)
|
221 |
+
status_text = "\n".join(status_messages)
|
222 |
|
223 |
+
return f"處理完成\n{status_text}", combined_text
|
224 |
+
|
225 |
+
# 對話
|
226 |
+
def mock_question_answer(question, history):
|
227 |
+
# 假資料模擬回答
|
228 |
+
answers = {
|
229 |
+
"文件的核心觀點是什麼?": "這份文件的核心觀點是關於人工智慧如何提升工作效率。",
|
230 |
+
"有哪些關鍵詞或數據?": "關鍵詞包括:人工智慧、工作效率、數據分析。",
|
231 |
+
"文件的摘要是什麼?": "這份文件討論了如何利用人工智慧工具,提升企業的運營效率和決策速度。"
|
232 |
+
}
|
233 |
+
response = answers.get(question, "抱歉,我無法回答這個問題。請嘗試其他問題!")
|
234 |
+
history.append({"role": "user", "content": question})
|
235 |
+
history.append({"role": "assistant", "content": response})
|
236 |
+
return history, ""
|
237 |
|
238 |
+
|
239 |
+
# 功能卡片
|
240 |
def generate_transcript(youtube_link):
|
241 |
print(f"\n開始生成 YouTube 逐字稿: {youtube_link}")
|
242 |
try:
|
|
|
297 |
try:
|
298 |
print("\n開始生成摘要...")
|
299 |
model = "gemini-2.0-flash-exp"
|
300 |
+
prompt = f"""
|
301 |
+
Inputs:
|
302 |
+
- 請根據以下逐字稿或文本生成重點摘要:{transcript}
|
303 |
+
|
304 |
+
Rules:
|
305 |
+
- 如果有課程名稱,請圍繞「課程名稱」為學習重點,進行重點整理,不要整理跟情境故事相關的問題
|
306 |
+
- 整體摘要在一百字以內
|
307 |
+
- 重點概念列出 bullet points,至少三個,最多五個
|
308 |
+
- 以及可能的結論與結尾延伸小問題提供學生作反思
|
309 |
+
- 敘述中,請把數學或是專業術語,用 Latex 包覆($...$)
|
310 |
+
- 加減乘除、根號、次方等等的運算式口語也換成 LATEX 數學符號
|
311 |
+
|
312 |
+
Example:
|
313 |
+
請以下列 markdown 格式輸出:
|
314 |
+
## 🌟 主題: (如果沒有 title 就省略)
|
315 |
+
## 📚 整體摘要
|
316 |
+
- (一個 bullet point....)
|
317 |
+
|
318 |
+
## 🔖 重點概念
|
319 |
+
- xxx
|
320 |
+
- xxx
|
321 |
+
- xxx
|
322 |
+
|
323 |
+
## 💡 為什麼我們要學這個?
|
324 |
+
- (一個 bullet point....)
|
325 |
+
|
326 |
+
## ❓ 延伸小問題
|
327 |
+
- (一個 bullet point....請圍繞學習重點,進行重點延伸思考,不要整理跟情境故事相關的問題)
|
328 |
+
"""
|
329 |
contents = [
|
330 |
types.Content(
|
331 |
role="user",
|
332 |
parts=[
|
333 |
+
types.Part.from_text(prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
]
|
335 |
)
|
336 |
]
|
|
|
341 |
)
|
342 |
|
343 |
print("摘要生成完成!")
|
344 |
+
summary = response.text
|
345 |
+
return summary
|
346 |
except Exception as e:
|
347 |
print(f"\n生成摘要時發生錯誤: {str(e)}")
|
348 |
raise
|
349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
350 |
def on_summary_click(transcript):
|
351 |
if not transcript:
|
352 |
return "請先上傳文件或輸入 YouTube 連結並處理完成後再生成摘要。"
|