fantaxy commited on
Commit
184adfb
·
verified ·
1 Parent(s): 592f28f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -285
app.py CHANGED
@@ -3,378 +3,266 @@
3
  """
4
  YouTube Video Analyzer & Downloader Pro
5
  ───────────────────────────────────────
6
- • `www.youtube.com_cookies.txt` 파일이 app.py와 같은 폴더에 있으면 자동 사용
7
- UI에서 쿠키를 업로드하면 파일이 *우선* 적용
8
- • “Transcript” 탭에서 **전체 자막 + MM:SS 타임스탬프** 제공
9
  """
10
 
11
- # ──────────────────────────────────────
12
- # 표준 라이브러리
13
- # ──────────────────────────────────────
14
- import os
15
- import re
16
- import shutil
17
- import tempfile
18
  from datetime import datetime, timedelta
19
  from pathlib import Path
20
 
21
- # ──────────────────────────────────────
22
- # 서드파티 라이브러리
23
- # ──────────────────────────────────────
24
  import gradio as gr
25
  import yt_dlp
26
  import google.generativeai as genai
27
  from youtube_transcript_api import YouTubeTranscriptApi
28
 
29
- # ──────────────────────────────────────
30
- # 상수
31
- # ──────────────────────────────────────
32
- DEFAULT_COOKIE_FILE = Path(__file__).with_name("www.youtube.com_cookies.txt")
33
 
34
  # 유튜브 URL 정규식
35
- _YT_REGEX = re.compile(
36
  r"(https?://)?(www\.)?"
37
  r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
38
  r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
39
  )
40
 
41
-
42
- # ──────────────────────────────────────
43
- # Helper : video-ID 추출 / 자막 가져오기
44
- # ──────────────────────────────────────
45
  def extract_video_id(url: str) -> str | None:
46
- m = _YT_REGEX.match(url)
47
  return m.group(6) if m else None
48
 
49
-
50
- def fetch_transcript(video_id: str, pref_lang=("ko", "en")) -> str:
51
- # 언어 우선순위대로 시도
52
  tr = None
53
- for lang in pref_lang:
54
  try:
55
  tr = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
56
  break
57
  except Exception:
58
  continue
59
  if tr is None:
60
- tr = YouTubeTranscriptApi.get_transcript(video_id) # 마지막 시도
61
-
62
- lines: list[str] = []
63
  for seg in tr:
64
- t = str(timedelta(seconds=int(seg["start"]))) # H:MM:SS
65
- t_mmss = ":".join(t.split(":")[-2:]) # MM:SS
66
- lines.append(f"**[{t_mmss}]** {seg['text']}")
67
  return "\n".join(lines)
68
 
69
-
70
- # ──────────────────────────────────────
71
- # 메인 클래스
72
- # ──────────────────────────────────────
73
  class YouTubeDownloader:
74
  def __init__(self):
75
- # 임시 디렉터리
76
- self.download_dir = tempfile.mkdtemp()
77
- self.temp_downloads = tempfile.mkdtemp(prefix="youtube_downloads_")
78
- # Downloads 폴더
79
- self.downloads_folder = os.path.join(
80
- os.path.expanduser("~"), "Downloads", "YouTube_Downloads"
81
- )
82
- os.makedirs(self.downloads_folder, exist_ok=True)
83
-
84
- self.gemini_model = None # Gemini 모델 핸들
85
 
86
  # ───────── Gemini 설정 ─────────
87
  def configure_gemini(self, api_key: str):
88
  try:
89
  genai.configure(api_key=api_key)
90
  self.gemini_model = genai.GenerativeModel("gemini-1.5-flash-latest")
91
- return True, "✅ Gemini API configured successfully!"
92
  except Exception as e:
93
- return False, f"❌ Failed to configure Gemini API: {e}"
94
-
95
- # ───────── 정리 ─────────
96
- def cleanup(self):
97
- for p in (self.download_dir, self.temp_downloads):
98
- try:
99
- shutil.rmtree(p, ignore_errors=True)
100
- except Exception:
101
- pass
102
-
103
- # ───────── 유효성 검사 ─────────
104
- def is_valid_youtube_url(self, url: str) -> bool:
105
- return _YT_REGEX.match(url) is not None
106
-
107
- # ───────── 장면 분석 : Gemini ─────────
108
- def generate_scene_breakdown_gemini(self, info: dict) -> list[str]:
109
- if not self.gemini_model:
110
- return self.generate_scene_breakdown_fallback(info)
111
 
112
- try:
113
- duration = info.get("duration", 0)
114
- title = info.get("title", "")
115
- description = info.get("description", "")[:1500]
116
-
117
- prompt = f"""
118
- Analyze this YouTube video and create a highly detailed, scene-by-scene breakdown
119
- with precise timestamps (MM:SS-MM:SS) and specific descriptions:
120
-
121
- Title : {title}
122
- Duration : {duration} s
123
- Description : {description}
124
-
125
- Follow these rules:
126
- • Include visual details, actions, inferred dialogue, setting, props, graphics
127
- • Dialogue : short lines for **every** scene if plausible
128
- • Timestamp : 2-3 s (<1 min) / 3-5 s (1-5 min) / 5-10 s (5-15 min) / 10-15 s (>15 min)
129
- • ≤ 20 scenes total
130
- • Formatting → **[MM:SS-MM:SS]** Description
131
- """
132
- resp = self.gemini_model.generate_content(prompt)
133
- if not resp or not resp.text:
134
- return self.generate_scene_breakdown_fallback(info)
135
-
136
- scenes, cur = [], ""
137
- for line in resp.text.splitlines():
138
- line = line.strip()
139
- if line.startswith("**[") and "]**" in line:
140
- if cur:
141
- scenes.append(cur.strip())
142
- cur = line
143
- elif cur:
144
- cur += "\n" + line
145
- if cur:
146
- scenes.append(cur.strip())
147
- return scenes or self.generate_scene_breakdown_fallback(info)
148
-
149
- except Exception:
150
- return self.generate_scene_breakdown_fallback(info)
151
-
152
- # ───────── 장면 분석 : Fallback ─────────
153
- def generate_scene_breakdown_fallback(self, info: dict) -> list[str]:
154
- duration = info.get("duration", 0)
155
- if not duration:
156
- return ["**[00:00]** Unable to determine duration"]
157
-
158
- seg = 3 if duration <= 60 else 5 if duration <= 300 else 10 if duration <= 900 else 15
159
- total = min(duration // seg + 1, 20)
160
- scenes: list[str] = []
161
- for i in range(total):
162
- s, e = i * seg, min(i * seg + seg - 1, duration)
163
- scenes.append(
164
- f"**[{s//60:02d}:{s%60:02d}-{e//60:02d}:{e%60:02d}]** "
165
- "Content continues…"
166
- )
167
- return scenes
168
-
169
- # ───────── 숫자 포맷 ─────────
170
  @staticmethod
171
- def fmt_num(n: int) -> str:
172
- if n >= 1_000_000:
173
- return f"{n/1_000_000:.1f} M"
174
- if n >= 1_000:
175
- return f"{n/1_000:.1f} K"
176
- return str(n)
177
-
178
- # ───────── 결과 리포트 ─────────
179
- def format_video_info(self, info: dict) -> str:
180
- title = info.get("title", "Unknown")
181
- uploader = info.get("uploader", "Unknown")
182
- duration = info.get("duration", 0)
183
- dur = f"{duration//60}:{duration%60:02d}"
184
- views = self.fmt_num(info.get("view_count", 0))
185
- likes = self.fmt_num(info.get("like_count", 0))
186
- comments = self.fmt_num(info.get("comment_count", 0))
187
- scenes = "\n".join(self.generate_scene_breakdown_gemini(info))
188
-
189
- return f"""\
190
- 🎬 **{title}**
191
- Uploader : {uploader}
192
- Duration : {dur} Views / Likes / Comments : {views} / {likes} / {comments}
193
-
194
- {'-'*48}
195
- {scenes}
196
- """
197
-
198
- # ───────── 메타데이터 ─────────
199
- def get_video_info(self, url: str, cookiefile: str | None = None):
200
- if cookiefile and os.path.exists(cookiefile):
201
- ck = cookiefile
202
  elif DEFAULT_COOKIE_FILE.exists():
203
  ck = str(DEFAULT_COOKIE_FILE)
204
  else:
205
  ck = None
 
 
206
 
207
- ydl_opts = {"noplaylist": True, "quiet": True}
208
- if ck:
209
- ydl_opts["cookiefile"] = ck
210
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
211
- return ydl.extract_info(url, download=False)
212
 
213
- # ───────── 다운로드 ─────────
214
- def download_video(
215
- self,
216
- url: str,
217
- quality: str = "best",
218
- audio_only: bool = False,
219
- cookiefile: str | None = None,
220
- ):
221
- if cookiefile and os.path.exists(cookiefile):
222
- ck = cookiefile
223
- elif DEFAULT_COOKIE_FILE.exists():
224
- ck = str(DEFAULT_COOKIE_FILE)
225
- else:
226
- ck = None
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
229
  ydl_opts: dict = {
230
- "outtmpl": os.path.join(self.temp_downloads, f"%(title)s_{ts}.%(ext)s"),
 
231
  "noplaylist": True,
 
232
  }
233
-
234
- if audio_only:
235
  ydl_opts["format"] = "bestaudio/best"
236
  ydl_opts["postprocessors"] = [
237
  {"key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192"}
238
  ]
239
  else:
240
- if quality == "720p":
241
- ydl_opts["format"] = "best[height<=720]"
242
- elif quality == "480p":
243
- ydl_opts["format"] = "best[height<=480]"
244
- else:
245
- ydl_opts["format"] = "best[height<=1080]"
246
-
247
- if ck:
248
- ydl_opts["cookiefile"] = ck
249
-
250
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
251
- ydl.extract_info(url, download=True)
252
-
253
- # temp 파일 찾기
254
- for f in os.listdir(self.temp_downloads):
255
- if ts in f:
256
- src = os.path.join(self.temp_downloads, f)
257
- dst = os.path.join(self.downloads_folder, f)
258
- try:
259
- shutil.copy2(src, dst)
260
- return dst, " Saved to Downloads"
261
- except Exception:
262
- return src, " Saved to temp (copy failed)"
263
- return None, "❌ File not found"
264
-
265
-
266
- # ──────────────────────────────────────
267
- # Gradio Helper
268
- # ─────────────────────��────────────────
269
- downloader = YouTubeDownloader()
270
 
 
 
271
 
272
- def configure_api_key(api_key):
273
- ok, msg = downloader.configure_gemini(api_key.strip()) if api_key else (False, "❌ API key required")
 
274
  return msg, gr.update(visible=ok)
275
 
276
-
277
- def analyze_fn(url, cookie):
278
- try:
279
- info = downloader.get_video_info(url, cookie)
280
- return downloader.format_video_info(info)
281
- except Exception as e:
282
- return f" {e}"
283
-
284
-
285
- def download_fn(url, qual, audio, cookie):
286
- fp, msg = downloader.download_video(url, qual, audio, cookie)
287
- return msg, fp
288
-
289
-
290
- def transcript_fn(url, _cookie):
 
 
 
291
  vid = extract_video_id(url)
292
  if not vid:
293
- return "❌ Invalid URL"
294
  try:
295
  return fetch_transcript(vid)
296
  except Exception as e:
297
- return f"❌ {e}"
298
 
 
 
 
 
299
 
300
- # ──────────────────────────────────────
301
- # UI
302
- # ──────────────────────────────────────
303
- def create_interface():
304
- with gr.Blocks(theme=gr.themes.Soft(), title="🎥 YouTube Video Analyzer & Downloader Pro") as iface:
305
-
306
- gr.HTML("<h1>🎥 YouTube Video Analyzer & Downloader Pro</h1>")
307
-
308
- # API 설정
309
  with gr.Group():
310
- gr.HTML("<h3>🔑 Google Gemini API Configuration</h3>")
311
  with gr.Row():
312
- api_key_in = gr.Textbox(label="🔑 Google API Key", type="password")
313
- api_btn = gr.Button("🔧 Configure API", variant="secondary")
314
- api_status = gr.Textbox(
315
- value="❌ Gemini API not configured – Using fallback analysis",
316
- interactive=False,
317
- lines=1,
318
- label="API Status",
319
- )
320
 
321
- # 공통 입력
322
  with gr.Row():
323
- url_in = gr.Textbox(label="🔗 YouTube URL", placeholder="Paste YouTube video URL…")
324
- cookie_in = gr.File(label="🍪 Upload cookies.txt (optional)", type="filepath", file_types=[".txt"])
325
 
326
  with gr.Tabs():
327
- # 분석
328
- with gr.TabItem("📊 Video Analysis"):
329
- analyze_btn = gr.Button("🔍 Analyze", variant="primary")
330
- analysis_out = gr.Textbox(label="📊 Analysis Report", lines=30, show_copy_button=True)
331
- analyze_btn.click(analyze_fn, [url_in, cookie_in], analysis_out, show_progress=True)
332
 
333
- # 다운로드
334
- with gr.TabItem("⬇️ Video Download"):
335
- with gr.Row():
336
- quality_dd = gr.Dropdown(["best", "720p", "480p"], value="best", label="📺 Quality")
337
- audio_cb = gr.Checkbox(label="🎵 Audio only (MP3)")
338
- download_btn = gr.Button("⬇️ Download", variant="primary")
339
- dl_status = gr.Textbox(label="📥 Status", lines=5, show_copy_button=True)
340
- dl_file = gr.File(label="📁 File", visible=False)
341
 
342
- def wrap_dl(u, q, a, c):
343
- msg, fp = download_fn(u, q, a, c)
344
- return (
345
- msg,
346
- gr.update(value=fp, visible=True) if fp and os.path.exists(fp) else gr.update(visible=False),
347
- )
 
348
 
349
- download_btn.click(wrap_dl, [url_in, quality_dd, audio_cb, cookie_in], [dl_status, dl_file], show_progress=True)
 
 
 
350
 
351
- # 자막
352
  with gr.TabItem("🗒️ Transcript"):
353
- tr_btn = gr.Button("📜 Get Transcript", variant="primary")
354
- tr_out = gr.Textbox(label="Transcript", lines=30, show_copy_button=True)
355
- tr_btn.click(transcript_fn, [url_in, cookie_in], tr_out, show_progress=True)
356
-
357
- api_btn.click(configure_api_key, inputs=[api_key_in], outputs=[api_status])
358
 
359
  gr.HTML(
360
- """
361
  <div style="margin-top:20px;padding:15px;background:#f0f8ff;border-left:5px solid #4285f4;border-radius:10px;">
362
- <h3>💡 Tip</h3>
363
- <p><code>www.youtube.com_cookies.txt</code> 파일을 <b>app.py</b>와 같은
364
- 폴더에 두면 업로드 없이 자동 사용됩니다.</p>
365
  </div>
366
  """
367
  )
 
368
 
369
- return iface
370
-
371
-
372
- # ──────────────────────────────────────
373
- # Entrypoint
374
- # ──────────────────────────────────────
375
  if __name__ == "__main__":
376
- demo = create_interface()
377
  import atexit
378
 
379
- atexit.register(downloader.cleanup)
 
380
  demo.launch(debug=True, show_error=True)
 
3
  """
4
  YouTube Video Analyzer & Downloader Pro
5
  ───────────────────────────────────────
6
+ · app.py 와 같은 폴더에 𝐰𝐰𝐰.𝐲𝐨𝐮𝐭𝐮𝐛𝐞.𝐜𝐨𝐨𝐤𝐢𝐞𝐬.𝐭𝐱𝐭 이 있으면 자동 사용
7
+ · Gradio UI에서 쿠키 파일 업로드 시 → 업로드 파일이 우선
8
+ · Gemini-1.5 Flash 장면 분석(선택적), 전체 Transcript, 다운로드 지원
9
  """
10
 
11
+ # ───────────────────────── 표준 라이브러리 ─────────────────────────
12
+ import os, re, shutil, tempfile
 
 
 
 
 
13
  from datetime import datetime, timedelta
14
  from pathlib import Path
15
 
16
+ # ───────────────────────── 서드파티 라이브러리 ────────────────────
 
 
17
  import gradio as gr
18
  import yt_dlp
19
  import google.generativeai as genai
20
  from youtube_transcript_api import YouTubeTranscriptApi
21
 
22
+ # ───────────────────────── 상수 (절대경로) ─────────────────────────
23
+ DEFAULT_COOKIE_FILE = (Path(__file__).resolve().parent / "www.youtube.com_cookies.txt").resolve()
 
 
24
 
25
  # 유튜브 URL 정규식
26
+ _YT_RE = re.compile(
27
  r"(https?://)?(www\.)?"
28
  r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
29
  r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
30
  )
31
 
32
+ # ───────────────────────── Helper (ID, 자막) ─────────────────────
 
 
 
33
  def extract_video_id(url: str) -> str | None:
34
+ m = _YT_RE.match(url)
35
  return m.group(6) if m else None
36
 
37
+ def fetch_transcript(video_id: str, pref=("ko", "en")) -> str:
 
 
38
  tr = None
39
+ for lang in pref:
40
  try:
41
  tr = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
42
  break
43
  except Exception:
44
  continue
45
  if tr is None:
46
+ tr = YouTubeTranscriptApi.get_transcript(video_id)
47
+ lines = []
 
48
  for seg in tr:
49
+ t = str(timedelta(seconds=int(seg["start"])))
50
+ mmss = ":".join(t.split(":")[-2:])
51
+ lines.append(f"**[{mmss}]** {seg['text']}")
52
  return "\n".join(lines)
53
 
54
+ # =================================================================
55
+ # Main Class
56
+ # =================================================================
 
57
  class YouTubeDownloader:
58
  def __init__(self):
59
+ # temp dirs
60
+ self.temp_dir = tempfile.mkdtemp(prefix="yt_tmp_")
61
+ self.temp_downloads = tempfile.mkdtemp(prefix="yt_dl_")
62
+ # Downloads target
63
+ self.dl_folder = Path.home() / "Downloads" / "YouTube_Downloads"
64
+ self.dl_folder.mkdir(parents=True, exist_ok=True)
65
+ # Gemini
66
+ self.gemini_model = None
 
 
67
 
68
  # ───────── Gemini 설정 ─────────
69
  def configure_gemini(self, api_key: str):
70
  try:
71
  genai.configure(api_key=api_key)
72
  self.gemini_model = genai.GenerativeModel("gemini-1.5-flash-latest")
73
+ return True, "✅ Gemini API configured!"
74
  except Exception as e:
75
+ return False, f"❌ Gemini 설정 실패: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # ───────── 쿠키 선택 + 디버그 ─────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  @staticmethod
79
+ def choose_cookie(ui_cookie: str | None):
80
+ if ui_cookie and os.path.exists(ui_cookie):
81
+ ck = ui_cookie
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  elif DEFAULT_COOKIE_FILE.exists():
83
  ck = str(DEFAULT_COOKIE_FILE)
84
  else:
85
  ck = None
86
+ print(">>> COOKIE =", ck, "EXISTS?", os.path.exists(ck) if ck else None)
87
+ return ck
88
 
89
+ # ───────── URL 검증 ─────────
90
+ @staticmethod
91
+ def valid_url(url: str) -> bool:
92
+ return bool(_YT_RE.match(url))
 
93
 
94
+ # ───────── 숫자 포맷 ─────────
95
+ @staticmethod
96
+ def fmt(n: int) -> str:
97
+ if n >= 1_000_000: return f"{n/1_000_000:.1f} M"
98
+ if n >= 1_000: return f"{n/1_000:.1f} K"
99
+ return str(n)
 
 
 
 
 
 
 
 
100
 
101
+ # ───────── Scene 분석 (fallback만 간단 구현) ─────────
102
+ def scene_breakdown(self, info: dict):
103
+ dur = info.get("duration", 0)
104
+ seg = 3 if dur <= 60 else 5 if dur <= 300 else 10
105
+ scenes = []
106
+ for s in range(0, dur, seg):
107
+ e = min(s + seg - 1, dur)
108
+ scenes.append(f"**[{s//60:02d}:{s%60:02d}-{e//60:02d}:{e%60:02d}]** …")
109
+ return "\n".join(scenes[:20])
110
+
111
+ # ───────── Video Info ─────────
112
+ def get_info(self, url: str, ui_cookie: str | None, progress):
113
+ if not self.valid_url(url):
114
+ return None, "❌ URL 오류"
115
+ ck = self.choose_cookie(ui_cookie)
116
+ if ck is None:
117
+ return None, "❌ 쿠키 파일을 찾을 수 없습니다"
118
+ ydl_opts = {"quiet": True, "noplaylist": True, "cookiefile": ck}
119
+ try:
120
+ progress(0.1, desc="metadata…")
121
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
122
+ info = ydl.extract_info(url, download=False)
123
+ progress(1.0)
124
+ return info, "OK"
125
+ except Exception as e:
126
+ return None, f"yt-dlp 오류: {e}"
127
+
128
+ # ───────── Download ─────────
129
+ def download(
130
+ self, url: str, qual: str, audio: bool, ui_cookie: str | None, progress
131
+ ):
132
+ if not self.valid_url(url):
133
+ return None, "❌ URL 오류"
134
+ ck = self.choose_cookie(ui_cookie)
135
+ if ck is None:
136
+ return None, "❌ 쿠키 파일을 찾을 수 없습니다"
137
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
138
  ydl_opts: dict = {
139
+ "outtmpl": str(Path(self.temp_downloads) / f"%(title)s_{ts}.%(ext)s"),
140
+ "quiet": True,
141
  "noplaylist": True,
142
+ "cookiefile": ck,
143
  }
144
+ if audio:
 
145
  ydl_opts["format"] = "bestaudio/best"
146
  ydl_opts["postprocessors"] = [
147
  {"key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192"}
148
  ]
149
  else:
150
+ ydl_opts["format"] = (
151
+ "best[height<=720]" if qual == "720p"
152
+ else "best[height<=480]" if qual == "480p"
153
+ else "best[height<=1080]"
154
+ )
155
+ try:
156
+ progress(0.1, desc="Downloading…")
157
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
158
+ ydl.extract_info(url, download=True)
159
+ # temp → Downloads
160
+ for f in Path(self.temp_downloads).iterdir():
161
+ if ts in f.name:
162
+ dest = self.dl_folder / f.name
163
+ try:
164
+ shutil.copy2(f, dest)
165
+ saved = dest
166
+ except Exception:
167
+ saved = f
168
+ progress(1.0)
169
+ return saved, f"✅ 저장 위치: {saved}"
170
+ return None, " 파일을 찾을 수 없습니다"
171
+ except Exception as e:
172
+ return None, f" 다운로드 실패: {e}"
 
 
 
 
 
 
 
173
 
174
+ # ───────────────────────── 인스턴스 ─────────────────────────
175
+ yt_dl = YouTubeDownloader()
176
 
177
+ # ───────────────────────── Gradio 함수 ──────────────────────
178
+ def api_config(key):
179
+ ok, msg = yt_dl.configure_gemini(key.strip()) if key else (False, "❌ API 키 필요")
180
  return msg, gr.update(visible=ok)
181
 
182
+ def analyze(url, cfile, progress=gr.Progress()):
183
+ info, err = yt_dl.get_info(url, cfile, progress)
184
+ if not info:
185
+ return err
186
+ report = (
187
+ f"**Title** : {info.get('title')}\n"
188
+ f"**Duration** : {info.get('duration',0)//60}:{info.get('duration',0)%60:02d}\n"
189
+ f"**Views / Likes** : {yt_dl.fmt(info.get('view_count',0))} / {yt_dl.fmt(info.get('like_count',0))}\n"
190
+ f"{'-'*40}\n"
191
+ f"{yt_dl.scene_breakdown(info)}"
192
+ )
193
+ return report
194
+
195
+ def download(url, q, a, cfile, progress=gr.Progress()):
196
+ path, msg = yt_dl.download(url, q, a, cfile, progress)
197
+ return msg, path
198
+
199
+ def transcript(url, _cfile):
200
  vid = extract_video_id(url)
201
  if not vid:
202
+ return "❌ URL 오류"
203
  try:
204
  return fetch_transcript(vid)
205
  except Exception as e:
206
+ return f"❌ 자막 오류: {e}"
207
 
208
+ # ───────────────────────── UI ───────────────────────────────
209
+ def create_ui():
210
+ with gr.Blocks(theme=gr.themes.Soft(), title="🎥 YouTube Video Analyzer & Downloader") as ui:
211
+ gr.HTML("<h1>🎥 YouTube Video Analyzer & Downloader</h1>")
212
 
213
+ # API
 
 
 
 
 
 
 
 
214
  with gr.Group():
 
215
  with gr.Row():
216
+ api_key = gr.Textbox(type="password", label="Gemini API Key")
217
+ api_btn = gr.Button("Configure")
218
+ api_stat = gr.Textbox(label="API Status", value="❌ Gemini 미설정", interactive=False)
219
+ api_btn.click(api_config, [api_key], [api_stat])
 
 
 
 
220
 
221
+ # 입력
222
  with gr.Row():
223
+ url = gr.Textbox(label="🔗 YouTube URL")
224
+ cookie = gr.File(label="🍪 cookies.txt (선택)", type="filepath", file_types=[".txt"])
225
 
226
  with gr.Tabs():
 
 
 
 
 
227
 
228
+ with gr.TabItem("📊 Analyze"):
229
+ a_btn = gr.Button("Analyze", variant="primary")
230
+ a_out = gr.Textbox(lines=20, label="Analysis", show_copy_button=True)
231
+ a_btn.click(analyze, [url, cookie], a_out, show_progress=True)
 
 
 
 
232
 
233
+ with gr.TabItem("⬇️ Download"):
234
+ with gr.Row():
235
+ q_dd = gr.Dropdown(["best", "720p", "480p"], value="best", label="Quality")
236
+ a_cb = gr.Checkbox(label="Audio only (MP3)")
237
+ d_btn = gr.Button("Download", variant="primary")
238
+ d_stat = gr.Textbox(lines=5, label="Status")
239
+ d_file = gr.File(label="File", visible=False)
240
 
241
+ def wrap_dl(u,q,a,c,pg=gr.Progress()):
242
+ msg, fp = download(u,q,a,c,pg)
243
+ return msg, gr.update(value=fp, visible=bool(fp and os.path.exists(fp)))
244
+ d_btn.click(wrap_dl, [url,q_dd,a_cb,cookie], [d_stat,d_file], show_progress=True)
245
 
 
246
  with gr.TabItem("🗒️ Transcript"):
247
+ t_btn = gr.Button("Get Transcript", variant="primary")
248
+ t_out = gr.Textbox(lines=30, label="Transcript", show_copy_button=True)
249
+ t_btn.click(transcript, [url, cookie], t_out, show_progress=True)
 
 
250
 
251
  gr.HTML(
252
+ f"""
253
  <div style="margin-top:20px;padding:15px;background:#f0f8ff;border-left:5px solid #4285f4;border-radius:10px;">
254
+ <b>Tip</b> : <code>{DEFAULT_COOKIE_FILE.name}</code> 파일을 <b>app.py</b> 옆에 두면
255
+ 자동으로 쿠키가 적용됩니다.
 
256
  </div>
257
  """
258
  )
259
+ return ui
260
 
261
+ # ───────────────────────── Entrypoint ───────────────────────
 
 
 
 
 
262
  if __name__ == "__main__":
263
+ demo = create_ui()
264
  import atexit
265
 
266
+ atexit.register(lambda: shutil.rmtree(yt_dl.temp_dir, ignore_errors=True))
267
+ atexit.register(lambda: shutil.rmtree(yt_dl.temp_downloads, ignore_errors=True))
268
  demo.launch(debug=True, show_error=True)