Upload app.py
Browse files
app.py
CHANGED
@@ -45,18 +45,47 @@ TEMP_DIR = '/tmp'
|
|
45 |
|
46 |
# --- バックエンド処理関数 ---
|
47 |
|
|
|
|
|
48 |
def download_and_extract_audio(youtube_url):
|
49 |
"""
|
50 |
yt-dlpを使って動画をダウンロードし、音声をMP3形式で抽出する。
|
51 |
-
|
52 |
"""
|
53 |
-
# <<< 修正 >>> 一時ディレクトリを使用
|
54 |
output_dir = os.path.join(TEMP_DIR, 'downloads')
|
55 |
os.makedirs(output_dir, exist_ok=True)
|
56 |
logging.debug(f"音声保存ディレクトリ: {os.path.abspath(output_dir)}")
|
57 |
|
58 |
output_template = os.path.join(output_dir, '%(id)s.%(ext)s')
|
59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
ydl_opts = {
|
61 |
'format': 'bestaudio/best',
|
62 |
'postprocessors': [{
|
@@ -67,7 +96,11 @@ def download_and_extract_audio(youtube_url):
|
|
67 |
'outtmpl': output_template,
|
68 |
'noplaylist': True,
|
69 |
'logger': logging.getLogger('yt_dlp'),
|
70 |
-
'verbose': False,
|
|
|
|
|
|
|
|
|
71 |
}
|
72 |
|
73 |
audio_file_path = None
|
@@ -75,65 +108,71 @@ def download_and_extract_audio(youtube_url):
|
|
75 |
|
76 |
try:
|
77 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
78 |
-
logging.info(f"yt-dlp: {youtube_url} のダウンロードと音声抽出を開始")
|
79 |
info_dict = ydl.extract_info(youtube_url, download=True)
|
|
|
|
|
80 |
video_id = info_dict.get('id', 'unknown_id')
|
81 |
-
# 動画情報も返すようにする
|
82 |
info_dict_result = {
|
83 |
'id': video_id,
|
84 |
'title': info_dict.get('title', f'動画 {video_id}'),
|
85 |
-
'thumbnail': info_dict.get('thumbnail'),
|
86 |
'uploader': info_dict.get('uploader'),
|
87 |
'duration': info_dict.get('duration'),
|
88 |
}
|
89 |
logging.info(f"yt-dlp: 動画情報取得完了 (ID: {video_id}, Title: {info_dict_result['title']})")
|
90 |
|
91 |
base_filename = ydl.prepare_filename(info_dict)
|
92 |
-
# yt-dlp の prepare_filename は絶対パスを返すことがあるので、
|
93 |
-
# 確実に /tmp 配下のパスにするため、ファイル名だけ取得して結合する
|
94 |
-
# expected_mp3_path = os.path.splitext(base_filename)[0] + '.mp3'
|
95 |
expected_mp3_filename = os.path.splitext(os.path.basename(base_filename))[0] + '.mp3'
|
96 |
expected_mp3_path = os.path.join(output_dir, expected_mp3_filename)
|
97 |
logging.debug(f"yt-dlp: 期待されるMP3ファイルパス -> {expected_mp3_path}")
|
98 |
|
99 |
-
|
100 |
wait_time = 0
|
101 |
-
max_wait = 15
|
102 |
while not os.path.exists(expected_mp3_path) and wait_time < max_wait:
|
103 |
logging.debug(f"MP3ファイル待機中: {expected_mp3_path} (待機時間: {wait_time}秒)")
|
104 |
time.sleep(1)
|
105 |
wait_time += 1
|
106 |
|
107 |
if os.path.exists(expected_mp3_path):
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
else:
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
return audio_file_path, info_dict_result
|
120 |
-
else:
|
121 |
logging.error("yt-dlp: 音声抽出後のMP3ファイル特定に失敗しました。")
|
122 |
logging.error(f"Downloads directory ({output_dir}) contents: {os.listdir(output_dir)}")
|
123 |
-
return None, info_dict_result
|
124 |
|
125 |
except yt_dlp.utils.DownloadError as e:
|
126 |
logging.error(f"yt-dlp ダウンロードエラー: {e}")
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
elif "
|
132 |
-
|
|
|
|
|
|
|
133 |
return None, None
|
134 |
except Exception as e:
|
135 |
logging.error("yt-dlp: 音声抽出中に予期せぬエラーが発生しました。", exc_info=True)
|
136 |
return None, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
def transcribe_audio(audio_path):
|
139 |
"""
|
|
|
45 |
|
46 |
# --- バックエンド処理関数 ---
|
47 |
|
48 |
+
TEMP_DIR = '/tmp' # Spacesで書き込み可能な一時ディレクトリ
|
49 |
+
|
50 |
def download_and_extract_audio(youtube_url):
|
51 |
"""
|
52 |
yt-dlpを使って動画をダウンロードし、音声をMP3形式で抽出する。
|
53 |
+
環境変数から読み込んだCookieを使用する。
|
54 |
"""
|
|
|
55 |
output_dir = os.path.join(TEMP_DIR, 'downloads')
|
56 |
os.makedirs(output_dir, exist_ok=True)
|
57 |
logging.debug(f"音声保存ディレクトリ: {os.path.abspath(output_dir)}")
|
58 |
|
59 |
output_template = os.path.join(output_dir, '%(id)s.%(ext)s')
|
60 |
|
61 |
+
# --- Cookie処理 ---
|
62 |
+
cookie_secret_name = 'YOUTUBE_COOKIES' # 手順2で登録したSecret名
|
63 |
+
cookie_content = os.getenv(cookie_secret_name)
|
64 |
+
cookie_file_path = None
|
65 |
+
temp_cookie_file_handle = None # finallyで閉じるため
|
66 |
+
|
67 |
+
if cookie_content:
|
68 |
+
try:
|
69 |
+
# 一時ファイルとしてCookieを書き出す
|
70 |
+
# delete=False にして、yt-dlpが読み込めるようにファイルを保持する
|
71 |
+
temp_cookie_file_handle = tempfile.NamedTemporaryFile(
|
72 |
+
mode='w', encoding='utf-8', dir=TEMP_DIR, suffix='.txt', delete=False
|
73 |
+
)
|
74 |
+
temp_cookie_file_handle.write(cookie_content)
|
75 |
+
cookie_file_path = temp_cookie_file_handle.name
|
76 |
+
temp_cookie_file_handle.close() # 書き込み後に一度閉じる(yt-dlpがアクセスできるように)
|
77 |
+
logging.info(f"環境変数 '{cookie_secret_name}' からCookieを一時ファイル '{cookie_file_path}' に書き出しました。")
|
78 |
+
except Exception as e:
|
79 |
+
logging.error(f"一時Cookieファイルの書き出し中にエラー: {e}", exc_info=True)
|
80 |
+
cookie_file_path = None # エラー時はCookieを使用しない
|
81 |
+
if temp_cookie_file_handle:
|
82 |
+
try:
|
83 |
+
temp_cookie_file_handle.close() # エラー時も閉じる試み
|
84 |
+
except: pass
|
85 |
+
else:
|
86 |
+
logging.warning(f"環境変数 '{cookie_secret_name}' が未設定です。Cookieなしで試行します。")
|
87 |
+
# ---------------
|
88 |
+
|
89 |
ydl_opts = {
|
90 |
'format': 'bestaudio/best',
|
91 |
'postprocessors': [{
|
|
|
96 |
'outtmpl': output_template,
|
97 |
'noplaylist': True,
|
98 |
'logger': logging.getLogger('yt_dlp'),
|
99 |
+
'verbose': False, # デバッグ時にTrueにすると詳細ログが出力される
|
100 |
+
# --- Cookieオプションを追加 ---
|
101 |
+
'cookiefile': cookie_file_path, # yt-dlpにCookieファイルのパスを渡す
|
102 |
+
# --------------------------
|
103 |
+
# '--cookies-from-browser' はサーバー環境では使えないので注意
|
104 |
}
|
105 |
|
106 |
audio_file_path = None
|
|
|
108 |
|
109 |
try:
|
110 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
111 |
+
logging.info(f"yt-dlp: {youtube_url} のダウンロードと音声抽出を開始 (Cookie使用: {'あり' if cookie_file_path else 'なし'})")
|
112 |
info_dict = ydl.extract_info(youtube_url, download=True)
|
113 |
+
|
114 |
+
# ... (動画情報取得、ファイルパス特定、ファイル待機処理は変更なし) ...
|
115 |
video_id = info_dict.get('id', 'unknown_id')
|
|
|
116 |
info_dict_result = {
|
117 |
'id': video_id,
|
118 |
'title': info_dict.get('title', f'動画 {video_id}'),
|
119 |
+
'thumbnail': info_dict.get('thumbnail'),
|
120 |
'uploader': info_dict.get('uploader'),
|
121 |
'duration': info_dict.get('duration'),
|
122 |
}
|
123 |
logging.info(f"yt-dlp: 動画情報取得完了 (ID: {video_id}, Title: {info_dict_result['title']})")
|
124 |
|
125 |
base_filename = ydl.prepare_filename(info_dict)
|
|
|
|
|
|
|
126 |
expected_mp3_filename = os.path.splitext(os.path.basename(base_filename))[0] + '.mp3'
|
127 |
expected_mp3_path = os.path.join(output_dir, expected_mp3_filename)
|
128 |
logging.debug(f"yt-dlp: 期待されるMP3ファイルパス -> {expected_mp3_path}")
|
129 |
|
|
|
130 |
wait_time = 0
|
131 |
+
max_wait = 15
|
132 |
while not os.path.exists(expected_mp3_path) and wait_time < max_wait:
|
133 |
logging.debug(f"MP3ファイル待機中: {expected_mp3_path} (待機時間: {wait_time}秒)")
|
134 |
time.sleep(1)
|
135 |
wait_time += 1
|
136 |
|
137 |
if os.path.exists(expected_mp3_path):
|
138 |
+
audio_file_path = expected_mp3_path
|
139 |
+
logging.info(f"yt-dlp: 音声抽出完了 -> {audio_file_path}")
|
140 |
+
return audio_file_path, info_dict_result
|
141 |
else:
|
142 |
+
logging.warning(f"期待されたMP3ファイルが見つかりません: {expected_mp3_path}")
|
143 |
+
potential_files = [f for f in os.listdir(output_dir) if f.startswith(video_id) and f.endswith('.mp3')]
|
144 |
+
if potential_files:
|
145 |
+
audio_file_path = os.path.join(output_dir, potential_files[0])
|
146 |
+
logging.info(f"yt-dlp: 代替検索で見つかった音声ファイル -> {audio_file_path}")
|
147 |
+
return audio_file_path, info_dict_result
|
148 |
+
else:
|
|
|
|
|
149 |
logging.error("yt-dlp: 音声抽出後のMP3ファイル特定に失敗しました。")
|
150 |
logging.error(f"Downloads directory ({output_dir}) contents: {os.listdir(output_dir)}")
|
151 |
+
return None, info_dict_result
|
152 |
|
153 |
except yt_dlp.utils.DownloadError as e:
|
154 |
logging.error(f"yt-dlp ダウンロードエラー: {e}")
|
155 |
+
err_str = str(e).lower()
|
156 |
+
# エラーメッセージに基づいて、より具体的なログを出力
|
157 |
+
if "sign in to confirm" in err_str or "confirm your age" in err_str:
|
158 |
+
logging.error("ボット確認/年齢確認エラー。提供されたCookieが無効か期限切れの可能性があります。再度Cookieを取得してSecretを更新してください。")
|
159 |
+
elif "video unavailable" in err_str:
|
160 |
+
logging.error("動画が利用不可です。URLが正しいか確認してください。")
|
161 |
+
elif "private video" in err_str:
|
162 |
+
logging.error("非公開動画です。Cookieがその動画へのアクセス権を持っているか確認してください。")
|
163 |
+
# 他にも 'livestream'、'members-only' などのエラーがありうる
|
164 |
return None, None
|
165 |
except Exception as e:
|
166 |
logging.error("yt-dlp: 音声抽出中に予期せぬエラーが発生しました。", exc_info=True)
|
167 |
return None, None
|
168 |
+
finally:
|
169 |
+
# --- 一時Cookieファイルを削除 ---
|
170 |
+
if cookie_file_path and os.path.exists(cookie_file_path):
|
171 |
+
try:
|
172 |
+
os.remove(cookie_file_path)
|
173 |
+
logging.info(f"一時Cookieファイル {cookie_file_path} を削除しました。")
|
174 |
+
except OSError as rm_err:
|
175 |
+
logging.error(f"一時Cookieファイル {cookie_file_path} の削除に失敗: {rm_err}")
|
176 |
|
177 |
def transcribe_audio(audio_path):
|
178 |
"""
|