hchcsuim commited on
Commit
ab13a4e
·
1 Parent(s): bef2896

upadte app.py +youtube api input

Browse files
Files changed (4) hide show
  1. app.py +157 -69
  2. requirements.txt +3 -0
  3. requirements_local.txt +3 -0
  4. youtube_api.py +247 -0
app.py CHANGED
@@ -14,6 +14,7 @@ import traceback # For printing full errors
14
  import platform
15
  import re
16
  import subprocess
 
17
 
18
  # --- 硬體檢查函數 ---
19
  def get_hardware_info():
@@ -117,59 +118,7 @@ MIC_PROMPT = """**Try Reading / 試著朗讀:**
117
  "Success is stumbling from failure to failure with no loss of enthusiasm." - Winston Churchill
118
  「成功是在一次又一次失敗中,依然熱情不減地前行。」 - 溫斯頓・邱吉爾"""
119
 
120
- # --- YouTube Audio Download Function ---
121
- def download_youtube_audio(url):
122
- # 使用固定的目錄來存儲下載的音訊文件,這樣它們就不會被刪除
123
- download_dir = os.path.join(tempfile.gettempdir(), "youtube_downloads")
124
- os.makedirs(download_dir, exist_ok=True)
125
-
126
- # 從 URL 中提取視頻 ID 作為文件名的一部分
127
- video_id = url.split("v=")[-1].split("&")[0] if "v=" in url else str(int(time.time()))
128
- filename = f"youtube_{video_id}_{int(time.time())}"
129
-
130
- temp_dir = tempfile.mkdtemp()
131
- downloaded_path = None
132
- try:
133
- temp_filepath_tmpl = os.path.join(download_dir, f"{filename}.%(ext)s")
134
- ydl_opts = {
135
- 'format': 'bestaudio/best',
136
- 'outtmpl': temp_filepath_tmpl,
137
- 'noplaylist': True,
138
- 'quiet': True,
139
- 'postprocessors': [{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3','preferredquality': '192',}],
140
- 'ffmpeg_location': shutil.which("ffmpeg"),
141
- }
142
- if not ydl_opts['ffmpeg_location']: print("Warning: ffmpeg not found... / 警告:找不到 ffmpeg...")
143
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
144
- info_dict = ydl.extract_info(url, download=True)
145
- duration = info_dict.get('duration')
146
- title = info_dict.get('title', 'unknown')
147
-
148
- final_filepath = ydl.prepare_filename(info_dict)
149
- if not final_filepath.endswith('.mp3'):
150
- base_name = final_filepath.rsplit('.', 1)[0]
151
- final_filepath = base_name + '.mp3'
152
-
153
- if os.path.exists(final_filepath):
154
- downloaded_path = final_filepath
155
- print(f"YouTube audio downloaded: {downloaded_path}")
156
- print(f"Title: {title}, Duration: {duration}s")
157
- else:
158
- potential_files = [os.path.join(download_dir, f) for f in os.listdir(download_dir) if f.startswith(filename) and f.endswith(".mp3")]
159
- if potential_files:
160
- downloaded_path = potential_files[0]
161
- print(f"Warning: Could not find expected MP3, using fallback: {downloaded_path}")
162
- duration = None
163
- else:
164
- raise FileNotFoundError(f"Audio file not found after download in {download_dir}")
165
-
166
- return downloaded_path, temp_dir, duration
167
- except Exception as e:
168
- print(f"Error processing YouTube URL: {e}")
169
- if temp_dir and os.path.exists(temp_dir):
170
- try: shutil.rmtree(temp_dir)
171
- except Exception as cleanup_e: print(f"Error cleaning temp directory {temp_dir}: {cleanup_e}")
172
- return None, None, None
173
 
174
  # --- Timestamp Formatting ---
175
  def format_timestamp(seconds):
@@ -191,24 +140,62 @@ def update_download_file(filepath):
191
  return None
192
 
193
  # --- YouTube 音訊處理 ---
194
- def process_youtube_url(youtube_url):
195
- """處理 YouTube URL,下載音訊並返回播放器和下載按鈕的更新"""
 
 
 
 
 
196
  if not youtube_url or not youtube_url.strip():
197
  return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
198
 
 
 
 
 
 
 
 
199
  try:
 
 
 
 
 
 
 
 
200
  print(f"Processing YouTube URL: {youtube_url}")
201
- # 只使用我們需要的返回值
202
- audio_path, _, _ = download_youtube_audio(youtube_url)
 
 
 
 
 
 
203
 
204
  if audio_path and os.path.exists(audio_path):
 
 
 
 
 
 
205
  # 返回音訊播放器和下載按鈕的更新
206
  return gr.update(visible=True, value=audio_path), gr.update(visible=True, value=audio_path)
207
  else:
208
  return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
209
  except Exception as e:
210
  print(f"Error processing YouTube URL: {e}")
211
- return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
 
 
 
 
 
 
212
 
213
  # --- Load ASR Pipeline ---
214
  def load_asr_pipeline(model_id):
@@ -347,7 +334,7 @@ def load_phi4_model(model_id):
347
  def transcribe_audio(mic_input, file_input, youtube_url, selected_model_identifier,
348
  task, language, return_timestamps,
349
  phi4_prompt_text, device_choice,
350
- previous_output_text, active_tab):
351
  global pipe, phi4_model, phi4_processor, current_model_name, current_device
352
  audio_source = None
353
  source_type_en = ""
@@ -453,13 +440,30 @@ def transcribe_audio(mic_input, file_input, youtube_url, selected_model_identifi
453
  source_type_zh = "YouTube"
454
  status_update_str = f"Downloading YouTube Audio / 正在下載 YouTube 音訊..."
455
  output_text_accumulated = status_update_prefix + status_update_str
456
- audio_path, temp_dir, duration_yt = download_youtube_audio(youtube_url)
457
- if audio_path:
458
- audio_source = audio_path
459
- temp_dir_to_clean = temp_dir
460
- audio_duration = duration_yt
461
- else:
462
- output_text_accumulated = status_update_prefix + "Error: Failed to download YouTube audio. / 錯誤:無法下載 YouTube 音訊。"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  return (output_text_accumulated, gr.update(), gr.update(), gr.update())
464
  else:
465
  # 如果沒有選擇任何輸入源或當前標籤沒有有效輸入
@@ -750,6 +754,22 @@ def update_language_ui(model_id, task):
750
  compact_css = """
751
  .tabitem { margin: 0rem !important; padding: 0rem !important;}
752
  .compact-file > div { min-height: unset !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  """
754
 
755
  # 移除 JavaScript 代碼,改用純 CSS 解決方案
@@ -782,14 +802,82 @@ with gr.Blocks(css=compact_css, theme=gr.themes.Default(spacing_size=gr.themes.s
782
  file_audio_player = gr.Audio(label="Audio Preview / 音訊預覽", interactive=False, visible=False)
783
 
784
  with gr.TabItem("▶️ YouTube") as youtube_tab:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  youtube_input = gr.Textbox(label="YouTube URL / 網址", placeholder="Paste YouTube link here / 在此貼上 YouTube 連結")
786
- gr.Examples(examples=[["https://www.youtube.com/watch?v=5D7l0tqQJ7k"]], inputs=[youtube_input], label="Example YouTube URL / 範例 YouTube 網址")
787
 
788
  # 添加 YouTube 音訊播放器和下載按鈕
789
  with gr.Row():
790
  youtube_audio_player = gr.Audio(label="YouTube Audio / YouTube 音訊", interactive=False, visible=False)
791
  youtube_download = gr.File(label="Download YouTube Audio / 下載 YouTube 音訊", interactive=False, visible=False, elem_classes="compact-file")
792
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  # 添加標籤切換事件
794
  mic_tab.select(fn=lambda: set_active_tab("mic"), inputs=[], outputs=[active_tab])
795
  file_tab.select(fn=lambda: set_active_tab("file"), inputs=[], outputs=[active_tab])
@@ -846,7 +934,7 @@ with gr.Blocks(css=compact_css, theme=gr.themes.Default(spacing_size=gr.themes.s
846
  # 連接 YouTube 處理功能
847
  youtube_input.change(
848
  fn=process_youtube_url,
849
- inputs=youtube_input,
850
  outputs=[youtube_audio_player, youtube_download],
851
  show_progress=True
852
  )
@@ -935,7 +1023,7 @@ with gr.Blocks(css=compact_css, theme=gr.themes.Default(spacing_size=gr.themes.s
935
  # Main submit action - Corrected outputs list
936
  submit_button.click(
937
  fn=transcribe_audio_with_error_handling,
938
- inputs=[mic_input, file_input, youtube_input, model_select, task_input, language_input, timestamp_input, phi4_prompt_input, device_input, output_text, active_tab],
939
  outputs=[output_text, mic_input, file_input, youtube_input], # 保持原始輸出
940
  show_progress="full" # 顯示完整進度條
941
  )
 
14
  import platform
15
  import re
16
  import subprocess
17
+ import youtube_api # 已安裝相關依賴,可以使用
18
 
19
  # --- 硬體檢查函數 ---
20
  def get_hardware_info():
 
118
  "Success is stumbling from failure to failure with no loss of enthusiasm." - Winston Churchill
119
  「成功是在一次又一次失敗中,依然熱情不減地前行。」 - 溫斯頓・邱吉爾"""
120
 
121
+ # YouTube 音訊處理現在由 youtube_api.py 模塊處理
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  # --- Timestamp Formatting ---
124
  def format_timestamp(seconds):
 
140
  return None
141
 
142
  # --- YouTube 音訊處理 ---
143
+ def process_youtube_url(youtube_url, user_api_key=None):
144
+ """處理 YouTube URL,下載音訊並返回播放器和下載按鈕的更新
145
+
146
+ Args:
147
+ youtube_url: YouTube 視頻 URL
148
+ user_api_key: 用戶輸入的 API 金鑰(可選)
149
+ """
150
  if not youtube_url or not youtube_url.strip():
151
  return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
152
 
153
+ # 檢查是否在 Hugging Face Spaces 環境中
154
+ import os
155
+ is_spaces = os.environ.get("SPACE_ID") is not None
156
+
157
+ # 優先使用用戶輸入的 API 金鑰,如果沒有則使用環境變量中的 API 金鑰
158
+ youtube_api_key = user_api_key if user_api_key and user_api_key.strip() else os.environ.get("YOUTUBE_API_KEY")
159
+
160
  try:
161
+ # 如果有 API 金鑰,設置它
162
+ if youtube_api_key:
163
+ youtube_api.set_api_key(youtube_api_key)
164
+ print(f"Using YouTube Data API with {'user-provided' if user_api_key else 'environment'} API key")
165
+ else:
166
+ print("No YouTube API key found, falling back to direct download")
167
+
168
+ # 處理 YouTube URL
169
  print(f"Processing YouTube URL: {youtube_url}")
170
+
171
+ if is_spaces and not youtube_api_key:
172
+ # 在 Spaces 環境中且沒有 API 金鑰,顯示警告
173
+ print("Warning: YouTube download is not supported in Hugging Face Spaces without an API key.")
174
+ raise gr.Error("YouTube 下載在 Hugging Face Spaces 中需要 API 金鑰。請在上方的 'YouTube API Key Settings' 中輸入您的 API 金鑰。\n\nYouTube download in Hugging Face Spaces requires an API key. Please enter your API key in the 'YouTube API Key Settings' section above.")
175
+
176
+ # 使用 API 模塊處理 URL
177
+ audio_path, video_info = youtube_api.process_youtube_url(youtube_url)
178
 
179
  if audio_path and os.path.exists(audio_path):
180
+ # 如果有視頻信息,顯示它
181
+ if video_info:
182
+ title = video_info.get("title", "Unknown")
183
+ duration = video_info.get("duration", "Unknown")
184
+ print(f"Title: {title}, Duration: {duration}s")
185
+
186
  # 返回音訊播放器和下載按鈕的更新
187
  return gr.update(visible=True, value=audio_path), gr.update(visible=True, value=audio_path)
188
  else:
189
  return gr.update(visible=False, value=None), gr.update(visible=False, value=None)
190
  except Exception as e:
191
  print(f"Error processing YouTube URL: {e}")
192
+ error_message = str(e)
193
+ if "quota" in error_message.lower():
194
+ raise gr.Error(f"YouTube API 配額已用盡。請嘗試使用其他 API 金鑰。\n\nYouTube API quota exceeded. Please try using a different API key.\n\nError: {error_message}")
195
+ elif "invalid" in error_message.lower() and "api key" in error_message.lower():
196
+ raise gr.Error(f"無效的 API 金鑰。請檢查您的 API 金鑰是否正確。\n\nInvalid API key. Please check if your API key is correct.\n\nError: {error_message}")
197
+ else:
198
+ raise gr.Error(f"處理 YouTube URL 時發生錯誤。\n\nError processing YouTube URL.\n\nError: {error_message}")
199
 
200
  # --- Load ASR Pipeline ---
201
  def load_asr_pipeline(model_id):
 
334
  def transcribe_audio(mic_input, file_input, youtube_url, selected_model_identifier,
335
  task, language, return_timestamps,
336
  phi4_prompt_text, device_choice,
337
+ previous_output_text, active_tab, youtube_api_key_input=None):
338
  global pipe, phi4_model, phi4_processor, current_model_name, current_device
339
  audio_source = None
340
  source_type_en = ""
 
440
  source_type_zh = "YouTube"
441
  status_update_str = f"Downloading YouTube Audio / 正在下載 YouTube 音訊..."
442
  output_text_accumulated = status_update_prefix + status_update_str
443
+
444
+ # 使用傳入的 API 金鑰
445
+ user_api_key = youtube_api_key_input
446
+
447
+ try:
448
+ # 使用 API 模塊處理 URL,傳入用戶輸入的 API 金鑰
449
+ audio_path, video_info = youtube_api.process_youtube_url(youtube_url, user_api_key)
450
+
451
+ if audio_path and os.path.exists(audio_path):
452
+ audio_source = audio_path
453
+ # 從視頻信息中獲取時長
454
+ if video_info and "duration" in video_info:
455
+ audio_duration = video_info["duration"]
456
+ if video_info.get("title"):
457
+ print(f"Processing YouTube video: {video_info['title']}")
458
+ else:
459
+ # 如果沒有時長信息,稍後會嘗試從音頻文件獲取
460
+ audio_duration = None
461
+ else:
462
+ output_text_accumulated = status_update_prefix + "Error: Failed to download YouTube audio. / 錯誤:無法下載 YouTube 音訊。"
463
+ return (output_text_accumulated, gr.update(), gr.update(), gr.update())
464
+ except Exception as e:
465
+ error_message = str(e)
466
+ output_text_accumulated = status_update_prefix + f"Error: {error_message}"
467
  return (output_text_accumulated, gr.update(), gr.update(), gr.update())
468
  else:
469
  # 如果沒有選擇任何輸入源或當前標籤沒有有效輸入
 
754
  compact_css = """
755
  .tabitem { margin: 0rem !important; padding: 0rem !important;}
756
  .compact-file > div { min-height: unset !important; }
757
+ .warning-box {
758
+ background-color: #fff3cd;
759
+ color: #856404;
760
+ padding: 10px;
761
+ border-radius: 5px;
762
+ border-left: 5px solid #ffc107;
763
+ margin-bottom: 15px;
764
+ }
765
+ .info-box {
766
+ background-color: #d1ecf1;
767
+ color: #0c5460;
768
+ padding: 10px;
769
+ border-radius: 5px;
770
+ border-left: 5px solid #17a2b8;
771
+ margin-bottom: 15px;
772
+ }
773
  """
774
 
775
  # 移除 JavaScript 代碼,改用純 CSS 解決方案
 
802
  file_audio_player = gr.Audio(label="Audio Preview / 音訊預覽", interactive=False, visible=False)
803
 
804
  with gr.TabItem("▶️ YouTube") as youtube_tab:
805
+ # 檢查是否在 Hugging Face Spaces 環境中
806
+ is_spaces = os.environ.get("SPACE_ID") is not None
807
+
808
+ # 如果在 Spaces 環境中,顯示警告訊息
809
+ if is_spaces:
810
+ # 檢查是否有 API 金鑰
811
+ youtube_api_key = os.environ.get("YOUTUBE_API_KEY")
812
+ if youtube_api_key:
813
+ gr.Markdown("""
814
+ ℹ️ **注意:YouTube 下載使用 YouTube Data API**
815
+
816
+ 在 Hugging Face Spaces 中,YouTube 下載功能使用 YouTube Data API。這提供了更穩定的體驗。
817
+
818
+ ℹ️ **Note: YouTube download uses YouTube Data API**
819
+
820
+ In Hugging Face Spaces, YouTube download functionality uses the YouTube Data API. This provides a more stable experience.
821
+ """, elem_classes="info-box")
822
+ else:
823
+ gr.Markdown("""
824
+ ⚠️ **注意:YouTube 下載在 Hugging Face Spaces 中需要 API 金鑰**
825
+
826
+ 由於安全限制,Spaces 環境需要 YouTube Data API 金鑰才能下載 YouTube 視頻。請在環境變量中設置 YOUTUBE_API_KEY,或在本地環境中使用此功能。
827
+
828
+ ⚠️ **Note: YouTube download in Hugging Face Spaces requires an API key**
829
+
830
+ Due to security restrictions, Spaces environment requires a YouTube Data API key to download YouTube videos. Please set YOUTUBE_API_KEY in environment variables, or use this feature in a local environment.
831
+ """, elem_classes="warning-box")
832
+
833
+ # API 金鑰輸入框
834
+ with gr.Accordion("YouTube API Key Settings / YouTube API 金鑰設置", open=False):
835
+ gr.Markdown("""
836
+ ### YouTube API 金鑰設置 / YouTube API Key Settings
837
+
838
+ 您可以在此輸入您自己的 YouTube API 金鑰。如果您沒有 API 金鑰,您可以在 [Google Cloud Console](https://console.cloud.google.com/) 中創建一個。
839
+
840
+ You can enter your own YouTube API key here. If you don't have an API key, you can create one in the [Google Cloud Console](https://console.cloud.google.com/).
841
+
842
+ **步驟 / Steps:**
843
+ 1. 前往 [Google Cloud Console](https://console.cloud.google.com/)
844
+ 2. 創建一個新項目(或選擇現有項目)/ Create a new project (or select an existing one)
845
+ 3. 啟用 YouTube Data API v3 / Enable YouTube Data API v3
846
+ 4. 創建 API 金鑰 / Create an API key
847
+ 5. 將 API 金鑰複製到下方輸入框 / Copy the API key to the input box below
848
+
849
+ **注意 / Note:** API 金鑰僅在當前會話中有效,頁面刷新後需要重新輸入。/ The API key is only valid for the current session and needs to be re-entered after page refresh.
850
+ """)
851
+ youtube_api_key_input = gr.Textbox(
852
+ label="YouTube API Key / YouTube API 金鑰",
853
+ placeholder="Enter your YouTube API key here / 在此輸入您的 YouTube API 金鑰",
854
+ type="password"
855
+ )
856
+
857
+ # YouTube URL 輸入框
858
  youtube_input = gr.Textbox(label="YouTube URL / 網址", placeholder="Paste YouTube link here / 在此貼上 YouTube 連結")
 
859
 
860
  # 添加 YouTube 音訊播放器和下載按鈕
861
  with gr.Row():
862
  youtube_audio_player = gr.Audio(label="YouTube Audio / YouTube 音訊", interactive=False, visible=False)
863
  youtube_download = gr.File(label="Download YouTube Audio / 下載 YouTube 音訊", interactive=False, visible=False, elem_classes="compact-file")
864
 
865
+ # 添加範例,點擊時自動處理
866
+ def process_example_url(url):
867
+ """處理範例 URL 的函數"""
868
+ # 獲取 API 金鑰
869
+ api_key = youtube_api_key_input.value if hasattr(youtube_api_key_input, 'value') else None
870
+ # 處理 URL
871
+ return process_youtube_url(url, api_key)
872
+
873
+ gr.Examples(
874
+ examples=[["https://www.youtube.com/watch?v=5D7l0tqQJ7k"]],
875
+ inputs=[youtube_input],
876
+ label="Example YouTube URL / 範例 YouTube 網址",
877
+ fn=process_example_url, # 點擊範例時自動處理 URL
878
+ outputs=[youtube_audio_player, youtube_download]
879
+ )
880
+
881
  # 添加標籤切換事件
882
  mic_tab.select(fn=lambda: set_active_tab("mic"), inputs=[], outputs=[active_tab])
883
  file_tab.select(fn=lambda: set_active_tab("file"), inputs=[], outputs=[active_tab])
 
934
  # 連接 YouTube 處理功能
935
  youtube_input.change(
936
  fn=process_youtube_url,
937
+ inputs=[youtube_input, youtube_api_key_input],
938
  outputs=[youtube_audio_player, youtube_download],
939
  show_progress=True
940
  )
 
1023
  # Main submit action - Corrected outputs list
1024
  submit_button.click(
1025
  fn=transcribe_audio_with_error_handling,
1026
+ inputs=[mic_input, file_input, youtube_input, model_select, task_input, language_input, timestamp_input, phi4_prompt_input, device_input, output_text, active_tab, youtube_api_key_input],
1027
  outputs=[output_text, mic_input, file_input, youtube_input], # 保持原始輸出
1028
  show_progress="full" # 顯示完整進度條
1029
  )
requirements.txt CHANGED
@@ -14,6 +14,9 @@ safetensors>=0.3.0
14
  yt-dlp>=2023.0.0
15
  soundfile>=0.12.0
16
  pydub>=0.25.0
 
 
 
17
 
18
  # Data processing
19
  numpy>=2.0.0
 
14
  yt-dlp>=2023.0.0
15
  soundfile>=0.12.0
16
  pydub>=0.25.0
17
+ google-api-python-client>=2.0.0
18
+ google-auth-httplib2>=0.1.0
19
+ google-auth-oauthlib>=0.5.0
20
 
21
  # Data processing
22
  numpy>=2.0.0
requirements_local.txt CHANGED
@@ -17,6 +17,9 @@ safetensors>=0.3.0
17
  yt-dlp>=2023.0.0
18
  soundfile>=0.12.0
19
  pydub>=0.25.0
 
 
 
20
 
21
  # Data processing
22
  numpy>=2.0.0
 
17
  yt-dlp>=2023.0.0
18
  soundfile>=0.12.0
19
  pydub>=0.25.0
20
+ google-api-python-client>=2.0.0
21
+ google-auth-httplib2>=0.1.0
22
+ google-auth-oauthlib>=0.5.0
23
 
24
  # Data processing
25
  numpy>=2.0.0
youtube_api.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YouTube API 處理模塊
3
+ 使用 YouTube Data API 獲取視頻信息,並使用 yt-dlp 下載音頻
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import tempfile
9
+ import shutil
10
+ import yt_dlp
11
+ from googleapiclient.discovery import build
12
+ from googleapiclient.errors import HttpError
13
+
14
+ # YouTube API 配置
15
+ YOUTUBE_API_SERVICE_NAME = "youtube"
16
+ YOUTUBE_API_VERSION = "v3"
17
+ YOUTUBE_API_KEY = None # 將在運行時設置
18
+
19
+ def set_api_key(api_key):
20
+ """設置 YouTube API 金鑰"""
21
+ global YOUTUBE_API_KEY
22
+ YOUTUBE_API_KEY = api_key
23
+ return YOUTUBE_API_KEY is not None
24
+
25
+ def extract_video_id(youtube_url):
26
+ """從 YouTube URL 中提取視頻 ID"""
27
+ if "youtube.com/watch" in youtube_url:
28
+ # 標準 YouTube URL
29
+ video_id = youtube_url.split("v=")[1].split("&")[0]
30
+ elif "youtu.be/" in youtube_url:
31
+ # 短 URL
32
+ video_id = youtube_url.split("youtu.be/")[1].split("?")[0]
33
+ else:
34
+ # 不支持的 URL 格式
35
+ return None
36
+ return video_id
37
+
38
+ def get_video_info(video_id):
39
+ """使用 YouTube Data API 獲取視頻信息"""
40
+ if not YOUTUBE_API_KEY:
41
+ raise ValueError("YouTube API 金鑰未設置。請先調用 set_api_key() 函數。")
42
+
43
+ try:
44
+ youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=YOUTUBE_API_KEY)
45
+
46
+ # 獲取視頻詳細信息
47
+ video_response = youtube.videos().list(
48
+ part="snippet,contentDetails,statistics",
49
+ id=video_id
50
+ ).execute()
51
+
52
+ # 檢查是否找到視頻
53
+ if not video_response.get("items"):
54
+ return None
55
+
56
+ video_info = video_response["items"][0]
57
+ snippet = video_info["snippet"]
58
+ content_details = video_info["contentDetails"]
59
+
60
+ # 解析時長
61
+ duration_str = content_details["duration"] # 格式: PT#H#M#S
62
+ duration_seconds = parse_duration(duration_str)
63
+
64
+ # 返回視頻信息
65
+ return {
66
+ "title": snippet["title"],
67
+ "description": snippet["description"],
68
+ "channel": snippet["channelTitle"],
69
+ "published_at": snippet["publishedAt"],
70
+ "duration": duration_seconds,
71
+ "thumbnail": snippet["thumbnails"]["high"]["url"] if "high" in snippet["thumbnails"] else snippet["thumbnails"]["default"]["url"]
72
+ }
73
+
74
+ except HttpError as e:
75
+ print(f"YouTube API 錯誤: {e}")
76
+ return None
77
+ except Exception as e:
78
+ print(f"獲取視頻信息時發生錯誤: {e}")
79
+ return None
80
+
81
+ def parse_duration(duration_str):
82
+ """解析 ISO 8601 時長格式 (PT#H#M#S)"""
83
+ duration_str = duration_str[2:] # 移除 "PT"
84
+ hours, minutes, seconds = 0, 0, 0
85
+
86
+ # 解析小時
87
+ if "H" in duration_str:
88
+ hours_part = duration_str.split("H")[0]
89
+ hours = int(hours_part)
90
+ duration_str = duration_str.split("H")[1]
91
+
92
+ # 解析分鐘
93
+ if "M" in duration_str:
94
+ minutes_part = duration_str.split("M")[0]
95
+ minutes = int(minutes_part)
96
+ duration_str = duration_str.split("M")[1]
97
+
98
+ # 解析秒
99
+ if "S" in duration_str:
100
+ seconds_part = duration_str.split("S")[0]
101
+ seconds = int(seconds_part)
102
+
103
+ # 計算總秒數
104
+ total_seconds = hours * 3600 + minutes * 60 + seconds
105
+ return total_seconds
106
+
107
+ def download_audio(video_id, api_info=None):
108
+ """下載 YouTube 視頻的音頻
109
+
110
+ Args:
111
+ video_id: YouTube 視頻 ID
112
+ api_info: 從 API 獲取的視頻信息 (可選)
113
+
114
+ Returns:
115
+ tuple: (音頻文件路徑, 臨時目錄, 視頻時長)
116
+ """
117
+ # 使用固定的目錄來存儲下載的音訊文件
118
+ download_dir = os.path.join(tempfile.gettempdir(), "youtube_downloads")
119
+ os.makedirs(download_dir, exist_ok=True)
120
+
121
+ # 使用視頻 ID 和時間戳作為文件名
122
+ filename = f"youtube_{video_id}_{int(time.time())}"
123
+ temp_dir = tempfile.mkdtemp()
124
+
125
+ try:
126
+ # 準備下載路徑
127
+ temp_filepath_tmpl = os.path.join(download_dir, f"{filename}.%(ext)s")
128
+
129
+ # 設置 yt-dlp 選項
130
+ ydl_opts = {
131
+ 'format': 'bestaudio/best',
132
+ 'outtmpl': temp_filepath_tmpl,
133
+ 'noplaylist': True,
134
+ 'quiet': True,
135
+ 'postprocessors': [{
136
+ 'key': 'FFmpegExtractAudio',
137
+ 'preferredcodec': 'mp3',
138
+ 'preferredquality': '192',
139
+ }],
140
+ 'ffmpeg_location': shutil.which("ffmpeg"),
141
+ }
142
+
143
+ # 檢查 ffmpeg
144
+ if not ydl_opts['ffmpeg_location']:
145
+ print("Warning: ffmpeg not found... / 警告:找不到 ffmpeg...")
146
+
147
+ # 如果已經有 API 信息,使用它
148
+ duration = api_info["duration"] if api_info else None
149
+ title = api_info["title"] if api_info else "Unknown"
150
+
151
+ # 下載音頻
152
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
153
+ # 如��沒有 API 信息,從 yt-dlp 獲取
154
+ if not api_info:
155
+ info_dict = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=True)
156
+ duration = info_dict.get('duration')
157
+ title = info_dict.get('title', 'unknown')
158
+ else:
159
+ # 有 API 信息,直接下載
160
+ ydl.download([f"https://www.youtube.com/watch?v={video_id}"])
161
+
162
+ # 確定最終文件路徑
163
+ final_filepath = os.path.join(download_dir, f"{filename}.mp3")
164
+
165
+ # 檢查文件是否存在
166
+ if os.path.exists(final_filepath):
167
+ print(f"YouTube audio downloaded: {final_filepath}")
168
+ print(f"Title: {title}, Duration: {duration}s")
169
+ return final_filepath, temp_dir, duration
170
+ else:
171
+ # 嘗試查找可能的文件
172
+ potential_files = [
173
+ os.path.join(download_dir, f)
174
+ for f in os.listdir(download_dir)
175
+ if f.startswith(filename) and f.endswith(".mp3")
176
+ ]
177
+ if potential_files:
178
+ downloaded_path = potential_files[0]
179
+ print(f"Warning: Could not find expected MP3, using fallback: {downloaded_path}")
180
+ return downloaded_path, temp_dir, duration
181
+ else:
182
+ raise FileNotFoundError(f"Audio file not found after download in {download_dir}")
183
+
184
+ except Exception as e:
185
+ print(f"Error downloading YouTube audio: {e}")
186
+ if temp_dir and os.path.exists(temp_dir):
187
+ try:
188
+ shutil.rmtree(temp_dir)
189
+ except Exception as cleanup_e:
190
+ print(f"Error cleaning temp directory {temp_dir}: {cleanup_e}")
191
+ return None, None, None
192
+
193
+ def process_youtube_url(youtube_url):
194
+ """處理 YouTube URL,獲取信息並下載音頻
195
+
196
+ Args:
197
+ youtube_url: YouTube 視頻 URL
198
+
199
+ Returns:
200
+ tuple: (音頻文件路徑, 視頻信息)
201
+ """
202
+ # 檢查 URL 是否有效
203
+ if not youtube_url or not youtube_url.strip():
204
+ return None, None
205
+
206
+ # 提取視頻 ID
207
+ video_id = extract_video_id(youtube_url)
208
+ if not video_id:
209
+ print(f"Invalid YouTube URL: {youtube_url}")
210
+ return None, None
211
+
212
+ # 檢查是否設置了 API 金鑰
213
+ if YOUTUBE_API_KEY:
214
+ # 使用 API 獲取視頻信息
215
+ video_info = get_video_info(video_id)
216
+ if not video_info:
217
+ print(f"Could not get video info from API for: {video_id}")
218
+ # 如果 API 失敗,嘗試直接下載
219
+ audio_path, temp_dir, duration = download_audio(video_id)
220
+ return audio_path, {"title": "Unknown", "duration": duration}
221
+
222
+ # 使用 API 信息下載音頻
223
+ audio_path, temp_dir, _ = download_audio(video_id, video_info)
224
+ return audio_path, video_info
225
+ else:
226
+ # 沒有 API 金鑰,直接使用 yt-dlp
227
+ print("No YouTube API key set, using yt-dlp directly")
228
+ audio_path, temp_dir, duration = download_audio(video_id)
229
+ return audio_path, {"title": "Unknown", "duration": duration}
230
+
231
+ # 測試代碼
232
+ if __name__ == "__main__":
233
+ # 設置 API 金鑰(實際使用時應從環境變量或配置文件獲取)
234
+ api_key = "YOUR_API_KEY"
235
+ set_api_key(api_key)
236
+
237
+ # 測試 URL
238
+ test_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
239
+
240
+ # 處理 URL
241
+ audio_path, video_info = process_youtube_url(test_url)
242
+
243
+ if audio_path and video_info:
244
+ print(f"Downloaded: {audio_path}")
245
+ print(f"Video info: {video_info}")
246
+ else:
247
+ print("Failed to process YouTube URL")