fetch video title
Browse files- app.py +148 -47
- requirements.txt +2 -1
app.py
CHANGED
@@ -8,6 +8,7 @@ import shutil
|
|
8 |
import tempfile
|
9 |
from google import genai
|
10 |
from google.genai import types
|
|
|
11 |
|
12 |
from initializer import initialize_clients, initialize_password
|
13 |
|
@@ -36,6 +37,30 @@ def mock_summary():
|
|
36 |
# 假資料模擬摘要
|
37 |
return "這份文件主要討論人工智慧在工作效率提升方面的應用,並提供了實際案例來說明其價值。"
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
def add_to_file_list(file, file_list):
|
40 |
if file:
|
41 |
temp_dir = tempfile.gettempdir()
|
@@ -46,10 +71,25 @@ def add_to_file_list(file, file_list):
|
|
46 |
return gr.update(choices=display_list), None
|
47 |
|
48 |
def add_youtube_to_list(youtube_link, file_list):
|
49 |
-
if youtube_link:
|
50 |
-
|
51 |
-
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
|
54 |
def generate_transcript(youtube_link):
|
55 |
print(f"\n開始生成 YouTube 逐字稿: {youtube_link}")
|
@@ -146,45 +186,100 @@ def generate_summary(transcript):
|
|
146 |
raise
|
147 |
|
148 |
def process_all_files(file_list):
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
151 |
|
152 |
-
|
153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
|
155 |
-
|
156 |
-
print(f"\n處理第 {index}/{len(file_list)} 個檔案: {file}")
|
157 |
-
|
158 |
-
if "youtube.com" in file or "youtu.be" in file:
|
159 |
-
print(f"檢測到 YouTube 連結,開始生成逐字稿...")
|
160 |
-
try:
|
161 |
-
transcript = generate_transcript(file)
|
162 |
-
print("✓ YouTube 逐字稿生成成功")
|
163 |
-
result_text += f"🟢 YouTube 影片處理完成: {file}\n"
|
164 |
-
transcript_text += f"\n=== {file} 的逐字稿 ===\n{transcript}\n"
|
165 |
-
except Exception as e:
|
166 |
-
print(f"✗ YouTube 逐字稿生成失敗: {str(e)}")
|
167 |
-
result_text += f"🔴 YouTube 影片處理失敗: {file}\n"
|
168 |
-
else:
|
169 |
-
print(f"處理一般檔案: {file}")
|
170 |
-
try:
|
171 |
-
# 這裡可以加入其他檔案的處理邏輯
|
172 |
-
print("✓ 檔案處理成功")
|
173 |
-
result_text += f"🟢 檔案處理完成: {file}\n"
|
174 |
-
except Exception as e:
|
175 |
-
print(f"✗ 檔案處理失敗: {str(e)}")
|
176 |
-
result_text += f"🔴 檔案處理失敗: {file}\n"
|
177 |
-
|
178 |
-
print("\n=== 檔案處理完成 ===")
|
179 |
-
return result_text, transcript_text
|
180 |
|
181 |
-
def process_with_auth(password, file_list):
|
182 |
-
"""
|
183 |
-
if not
|
|
|
|
|
|
|
184 |
return "請輸入正確的密碼", "", gr.update(visible=False)
|
185 |
|
186 |
-
|
187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
def on_summary_click(transcript):
|
190 |
if not transcript:
|
@@ -209,24 +304,31 @@ with gr.Blocks() as demo:
|
|
209 |
gr.Markdown("### 來源選單")
|
210 |
|
211 |
file_list = gr.State([])
|
|
|
212 |
|
213 |
with gr.Tab("YouTube 連結"):
|
214 |
youtube_link = gr.Textbox(label="輸入 YouTube 連結")
|
215 |
add_youtube_button = gr.Button("添加到來源列表")
|
216 |
add_youtube_button.click(add_youtube_to_list, inputs=[youtube_link, file_list], outputs=[file_list, youtube_link])
|
217 |
|
218 |
-
with gr.Tab("
|
219 |
upload_file = gr.File(label="從電腦添加文件", file_types=[".txt", ".pdf", ".docx"])
|
220 |
add_file_button = gr.Button("添加到來源列表")
|
221 |
add_file_button.click(add_to_file_list, inputs=[upload_file, file_list], outputs=[file_list, upload_file])
|
222 |
|
223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
|
225 |
process_files_button = gr.Button("處理檔案")
|
226 |
rag_result = gr.Textbox(label="處理狀態", interactive=False)
|
227 |
|
228 |
-
file_list.change(lambda x: gr.update(choices = [os.path.basename(path) if os.path.basename(path) else path for path in x]), inputs=file_list, outputs=file_display)
|
229 |
-
|
230 |
with gr.Column(visible=True) as chat_column:
|
231 |
gr.Markdown("### 對話區域")
|
232 |
chatbot = gr.Chatbot(label="聊天記錄", type="messages")
|
@@ -247,7 +349,7 @@ with gr.Blocks() as demo:
|
|
247 |
transcript_display = gr.Textbox(
|
248 |
label="YouTube 逐字稿",
|
249 |
interactive=False,
|
250 |
-
lines=
|
251 |
show_copy_button=True,
|
252 |
placeholder="處理 YouTube 影片後,逐字稿將顯示在這裡..."
|
253 |
)
|
@@ -261,10 +363,11 @@ with gr.Blocks() as demo:
|
|
261 |
# 更新處理檔案按鈕的事件處理
|
262 |
process_files_button.click(
|
263 |
fn=process_with_auth,
|
264 |
-
inputs=[password_input, file_list],
|
265 |
outputs=[
|
266 |
rag_result,
|
267 |
-
transcript_display
|
|
|
268 |
]
|
269 |
).then(
|
270 |
fn=on_summary_click,
|
@@ -280,6 +383,4 @@ with gr.Blocks() as demo:
|
|
280 |
outputs=[summary_output]
|
281 |
)
|
282 |
|
283 |
-
|
284 |
-
|
285 |
demo.launch(share=True)
|
|
|
8 |
import tempfile
|
9 |
from google import genai
|
10 |
from google.genai import types
|
11 |
+
import yt_dlp
|
12 |
|
13 |
from initializer import initialize_clients, initialize_password
|
14 |
|
|
|
37 |
# 假資料模擬摘要
|
38 |
return "這份文件主要討論人工智慧在工作效率提升方面的應用,並提供了實際案例來說明其價值。"
|
39 |
|
40 |
+
def get_youtube_title(url):
|
41 |
+
"""獲取 YouTube 影片標題"""
|
42 |
+
try:
|
43 |
+
# 確保 URL 格式完整
|
44 |
+
if not url.startswith('http'):
|
45 |
+
if 'watch?v=' in url:
|
46 |
+
url = f'https://www.youtube.com/{url}'
|
47 |
+
else:
|
48 |
+
url = f'https://www.youtube.com/watch?v={url}'
|
49 |
+
|
50 |
+
ydl_opts = {
|
51 |
+
'quiet': True,
|
52 |
+
'no_warnings': True,
|
53 |
+
'extract_flat': True
|
54 |
+
}
|
55 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
56 |
+
info = ydl.extract_info(url, download=False)
|
57 |
+
title = info.get('title', url)
|
58 |
+
print(f"YouTube title: {title}")
|
59 |
+
return title
|
60 |
+
except Exception as e:
|
61 |
+
print(f"Error fetching YouTube title: {str(e)}")
|
62 |
+
return url
|
63 |
+
|
64 |
def add_to_file_list(file, file_list):
|
65 |
if file:
|
66 |
temp_dir = tempfile.gettempdir()
|
|
|
71 |
return gr.update(choices=display_list), None
|
72 |
|
73 |
def add_youtube_to_list(youtube_link, file_list):
|
74 |
+
if not youtube_link:
|
75 |
+
return gr.update(choices=[item.split("|||")[0] if "|||" in item else os.path.basename(item) for item in file_list]), ""
|
76 |
+
|
77 |
+
# 獲取標題
|
78 |
+
title = get_youtube_title(youtube_link)
|
79 |
+
|
80 |
+
# 確保 URL 格式完整
|
81 |
+
if not youtube_link.startswith('http'):
|
82 |
+
if 'watch?v=' in youtube_link:
|
83 |
+
youtube_link = f'https://www.youtube.com/{youtube_link}'
|
84 |
+
else:
|
85 |
+
youtube_link = f'https://www.youtube.com/watch?v={youtube_link}'
|
86 |
+
|
87 |
+
# 存儲格式:[title]|||[url]
|
88 |
+
file_list.append(f"{title}|||{youtube_link}")
|
89 |
+
display_list = [item.split("|||")[0] if "|||" in item else os.path.basename(item) for item in file_list]
|
90 |
+
print(f"File list: {file_list}")
|
91 |
+
print(f"Display list: {display_list}")
|
92 |
+
return file_list, ""
|
93 |
|
94 |
def generate_transcript(youtube_link):
|
95 |
print(f"\n開始生成 YouTube 逐字稿: {youtube_link}")
|
|
|
186 |
raise
|
187 |
|
188 |
def process_all_files(file_list):
|
189 |
+
"""處理所有選中的文件"""
|
190 |
+
if not file_list:
|
191 |
+
return "請選擇要處理的文件", ""
|
192 |
+
|
193 |
+
all_text = []
|
194 |
+
status_messages = []
|
195 |
|
196 |
+
for item in file_list:
|
197 |
+
try:
|
198 |
+
if "|||" in item:
|
199 |
+
# YouTube 連結
|
200 |
+
title, url = item.split("|||")
|
201 |
+
print(f"處理 YouTube: {title}")
|
202 |
+
try:
|
203 |
+
transcript = generate_transcript(url)
|
204 |
+
if transcript:
|
205 |
+
all_text.append(f"=== {title} ===\n{transcript}")
|
206 |
+
status_messages.append(f"🟢 成功處理 YouTube 影片:{title}")
|
207 |
+
else:
|
208 |
+
status_messages.append(f"🔴 無法獲取影片逐字稿:{title}")
|
209 |
+
except Exception as e:
|
210 |
+
if "無法取得影片資訊" in str(e):
|
211 |
+
# 可能是影片標題問題,但還是有內容
|
212 |
+
all_text.append(f"=== YouTube 影片 ===\n{e.transcript if hasattr(e, 'transcript') else ''}")
|
213 |
+
status_messages.append(f"🟡 影片資訊不完整,但已處理內容:{url}")
|
214 |
+
else:
|
215 |
+
status_messages.append(f"🔴 處理失敗:{title}({str(e)})")
|
216 |
+
else:
|
217 |
+
# 本地文件
|
218 |
+
filename = os.path.basename(item)
|
219 |
+
print(f"處理文件: {filename}")
|
220 |
+
try:
|
221 |
+
with open(item, 'r', encoding='utf-8') as f:
|
222 |
+
content = f.read()
|
223 |
+
try:
|
224 |
+
# 嘗試解碼文件名
|
225 |
+
decoded_name = filename.encode('latin1').decode('utf-8')
|
226 |
+
all_text.append(f"=== {decoded_name} ===\n{content}")
|
227 |
+
status_messages.append(f"🟢 成功處理文件:{decoded_name}")
|
228 |
+
except:
|
229 |
+
# 文件名有問題,但內容可用
|
230 |
+
all_text.append(f"=== 文件內容 ===\n{content}")
|
231 |
+
status_messages.append(f"🟡 文件名稱無法正確顯示,但已處理內容:{filename}")
|
232 |
+
except UnicodeDecodeError:
|
233 |
+
try:
|
234 |
+
# 嘗試其他編碼
|
235 |
+
for encoding in ['big5', 'gbk', 'shift-jis']:
|
236 |
+
try:
|
237 |
+
with open(item, 'r', encoding=encoding) as f:
|
238 |
+
content = f.read()
|
239 |
+
all_text.append(f"=== {filename} ===\n{content}")
|
240 |
+
status_messages.append(f"🟡 使用 {encoding} 編碼成功讀取文件:{filename}")
|
241 |
+
break
|
242 |
+
except:
|
243 |
+
continue
|
244 |
+
else:
|
245 |
+
status_messages.append(f"🔴 無法讀取文件內容:{filename}(編碼問題)")
|
246 |
+
except Exception as e:
|
247 |
+
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
248 |
+
except Exception as e:
|
249 |
+
status_messages.append(f"🔴 讀取文件失敗:{filename}({str(e)})")
|
250 |
+
except Exception as e:
|
251 |
+
status_messages.append(f"🔴 處理失敗:{item}({str(e)})")
|
252 |
+
|
253 |
+
if not all_text:
|
254 |
+
return "❌ 沒有成功處理任何文件", ""
|
255 |
+
|
256 |
+
# 合併所有文本
|
257 |
+
combined_text = "\n\n".join(all_text)
|
258 |
+
status_text = "\n".join(status_messages)
|
259 |
|
260 |
+
return f"處理完成\n{status_text}", combined_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
+
def process_with_auth(password, file_list, file_display):
|
263 |
+
"""帶密碼驗證的文件處理"""
|
264 |
+
if not file_display: # 使用 file_display 而不是 file_list
|
265 |
+
return "請選擇要處理的文件", "", gr.update(visible=False)
|
266 |
+
|
267 |
+
if password != PASSWORD:
|
268 |
return "請輸入正確的密碼", "", gr.update(visible=False)
|
269 |
|
270 |
+
# 根據顯示的選項找到對應的完整項目
|
271 |
+
selected_files = []
|
272 |
+
for item in file_list:
|
273 |
+
if "|||" in item:
|
274 |
+
title = item.split("|||")[0]
|
275 |
+
if title in file_display:
|
276 |
+
selected_files.append(item)
|
277 |
+
else:
|
278 |
+
if os.path.basename(item) in file_display:
|
279 |
+
selected_files.append(item)
|
280 |
+
|
281 |
+
result_text, transcript_text = process_all_files(selected_files)
|
282 |
+
return result_text, transcript_text, gr.update(visible=True)
|
283 |
|
284 |
def on_summary_click(transcript):
|
285 |
if not transcript:
|
|
|
304 |
gr.Markdown("### 來源選單")
|
305 |
|
306 |
file_list = gr.State([])
|
307 |
+
file_display = gr.State([])
|
308 |
|
309 |
with gr.Tab("YouTube 連結"):
|
310 |
youtube_link = gr.Textbox(label="輸入 YouTube 連結")
|
311 |
add_youtube_button = gr.Button("添加到來源列表")
|
312 |
add_youtube_button.click(add_youtube_to_list, inputs=[youtube_link, file_list], outputs=[file_list, youtube_link])
|
313 |
|
314 |
+
with gr.Tab("上傳檔案(TODO)"):
|
315 |
upload_file = gr.File(label="從電腦添加文件", file_types=[".txt", ".pdf", ".docx"])
|
316 |
add_file_button = gr.Button("添加到來源列表")
|
317 |
add_file_button.click(add_to_file_list, inputs=[upload_file, file_list], outputs=[file_list, upload_file])
|
318 |
|
319 |
+
file_display_input = gr.CheckboxGroup(label="已上傳的文件", interactive=True)
|
320 |
+
|
321 |
+
# 更新顯示邏輯
|
322 |
+
def update_display(file_list):
|
323 |
+
display_list = [item.split("|||")[0] if "|||" in item else os.path.basename(item) for item in file_list]
|
324 |
+
print(f"Updating display with: {display_list}")
|
325 |
+
return gr.update(choices=display_list, value=[])
|
326 |
+
|
327 |
+
file_list.change(update_display, inputs=file_list, outputs=file_display_input)
|
328 |
|
329 |
process_files_button = gr.Button("處理檔案")
|
330 |
rag_result = gr.Textbox(label="處理狀態", interactive=False)
|
331 |
|
|
|
|
|
332 |
with gr.Column(visible=True) as chat_column:
|
333 |
gr.Markdown("### 對話區域")
|
334 |
chatbot = gr.Chatbot(label="聊天記錄", type="messages")
|
|
|
349 |
transcript_display = gr.Textbox(
|
350 |
label="YouTube 逐字稿",
|
351 |
interactive=False,
|
352 |
+
lines=20,
|
353 |
show_copy_button=True,
|
354 |
placeholder="處理 YouTube 影片後,逐字稿將顯示在這裡..."
|
355 |
)
|
|
|
363 |
# 更新處理檔案按鈕的事件處理
|
364 |
process_files_button.click(
|
365 |
fn=process_with_auth,
|
366 |
+
inputs=[password_input, file_list, file_display_input],
|
367 |
outputs=[
|
368 |
rag_result,
|
369 |
+
transcript_display,
|
370 |
+
summary_button
|
371 |
]
|
372 |
).then(
|
373 |
fn=on_summary_click,
|
|
|
383 |
outputs=[summary_output]
|
384 |
)
|
385 |
|
|
|
|
|
386 |
demo.launch(share=True)
|
requirements.txt
CHANGED
@@ -8,4 +8,5 @@ google-auth-oauthlib
|
|
8 |
google-cloud-storage
|
9 |
google-cloud-bigquery
|
10 |
google-generativeai
|
11 |
-
google-genai
|
|
|
|
8 |
google-cloud-storage
|
9 |
google-cloud-bigquery
|
10 |
google-generativeai
|
11 |
+
google-genai
|
12 |
+
yt-dlp
|