fantaxy commited on
Commit
9dd9c8e
Β·
verified Β·
1 Parent(s): 11f53e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +927 -38
app.py CHANGED
@@ -26,7 +26,27 @@ from pathlib import Path
26
  import gradio as gr
27
  import yt_dlp
28
  import google.generativeai as genai
 
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # ──────────────────────────────────────────────────────────────
31
  # κΈ°λ³Έ μΏ ν‚€ 파일 경둜 ― 파일λͺ…이 λ™μΌν•˜λ©΄ μžλ™ μ‚¬μš©
32
  # ──────────────────────────────────────────────────────────────
@@ -623,20 +643,410 @@ def download_with_cookies(url, quality, audio_only, cookies_file, progress=gr.Pr
623
  # =================================================================
624
  # Gradio UI
625
  # =================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  def create_interface():
627
  with gr.Blocks(
628
  theme=gr.themes.Soft(), title="πŸŽ₯ YouTube Video Analyzer & Downloader Pro"
629
  ) as iface:
630
  gr.HTML("<h1>πŸŽ₯ YouTube Video Analyzer & Downloader Pro</h1>")
631
 
632
- # API μ„Ήμ…˜
633
  with gr.Group():
634
  gr.HTML("<h3>πŸ”‘ Google Gemini API Configuration</h3>")
635
  with gr.Row():
636
  api_key_in = gr.Textbox(
637
- label="πŸ”‘ Google API Key",
638
- placeholder="Paste your Google API key…",
639
- type="password",
640
  )
641
  api_btn = gr.Button("πŸ”§ Configure API", variant="secondary")
642
  api_status = gr.Textbox(
@@ -646,71 +1056,550 @@ def create_interface():
646
  lines=1,
647
  )
648
 
649
- # 메인 UI
650
  with gr.Row():
651
- url_in = gr.Textbox(
652
- label="πŸ”— YouTube URL",
653
- placeholder="Paste YouTube video URL…",
654
- )
655
  cookies_in = gr.File(
656
- label="πŸͺ Upload cookies.txt (optional)",
657
- file_types=[".txt"],
658
- type="filepath",
659
  )
660
 
661
  with gr.Tabs():
 
662
  with gr.TabItem("πŸ“Š Video Analysis"):
663
  analyze_btn = gr.Button("πŸ” Analyze Video", variant="primary")
664
- analysis_out = gr.Textbox(
665
- label="πŸ“Š Analysis Report", lines=25, show_copy_button=True
666
- )
667
  analyze_btn.click(
668
- fn=analyze_with_cookies,
669
- inputs=[url_in, cookies_in],
670
- outputs=analysis_out,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  show_progress=True,
672
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  with gr.TabItem("⬇️ Video Download"):
675
  with gr.Row():
676
  quality_dd = gr.Dropdown(
677
- choices=["best", "720p", "480p"],
678
- value="best",
679
- label="πŸ“Ί Quality",
680
  )
681
  audio_cb = gr.Checkbox(label="🎡 Audio only (MP3)")
682
  download_btn = gr.Button("⬇️ Download Video", variant="primary")
683
- dl_status = gr.Textbox(
684
- label="πŸ“₯ Download Status", lines=5, show_copy_button=True
685
- )
686
  dl_file = gr.File(label="πŸ“ Downloaded File", visible=False)
687
 
688
- def wrapped_download(url, q, a, cfile, progress=gr.Progress()):
689
- fp, st = download_with_cookies(url, q, a, cfile, progress)
690
- if fp and os.path.exists(fp):
691
- return st, gr.update(value=fp, visible=True)
692
- return st, gr.update(visible=False)
 
693
 
694
  download_btn.click(
695
- fn=wrapped_download,
696
  inputs=[url_in, quality_dd, audio_cb, cookies_in],
697
  outputs=[dl_status, dl_file],
698
  show_progress=True,
699
  )
 
 
 
 
 
 
 
 
 
700
 
701
- # API λ²„νŠΌ λ™μž‘
702
- api_btn.click(
703
- fn=configure_api_key,
704
- inputs=[api_key_in],
705
- outputs=[api_status],
706
- )
707
 
708
  gr.HTML(
709
  """
710
  <div style="margin-top:20px;padding:15px;background:#f0f8ff;border-left:5px solid #4285f4;border-radius:10px;">
711
  <h3>πŸ’‘ Tip: μΏ ν‚€ 파일 μžλ™ μ‚¬μš©</h3>
712
  <p><code>www.youtube.com_cookies.txt</code> νŒŒμΌμ„ <strong>app.py</strong>와 같은
713
- 폴더에 두면 μžλ™μœΌλ‘œ μ‚¬μš©λ©λ‹ˆλ‹€. 주기적으둜 μƒˆ 파일둜 ꡐ체해 μ£Όμ„Έμš”.</p>
714
  </div>
715
  """
716
  )
@@ -718,7 +1607,7 @@ def create_interface():
718
 
719
 
720
  # =================================================================
721
- # Entrypoint
722
  # =================================================================
723
  if __name__ == "__main__":
724
  demo = create_interface()
 
26
  import gradio as gr
27
  import yt_dlp
28
  import google.generativeai as genai
29
+ # ───────── transcript_utils.py ─────────
30
+ from youtube_transcript_api import YouTubeTranscriptApi
31
+ from datetime import timedelta
32
 
33
+ def fetch_transcript(video_id, lang_pref=("ko","en")):
34
+ # available_transcripts()κ°€ 1.1.0λΆ€ν„° 좔가됨
35
+ for lang in lang_pref:
36
+ try:
37
+ transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
38
+ break
39
+ except Exception:
40
+ continue
41
+ else:
42
+ raise RuntimeError("μžλ§‰μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
43
+
44
+ lines = []
45
+ for seg in transcript:
46
+ t = str(timedelta(seconds=int(seg["start"]))) # 0:01:23
47
+ t_mmss = ":".join(t.split(":")[-2:]) # 01:23
48
+ lines.append(f"**[{t_mmss}]** {seg['text']}")
49
+ return "\n".join(lines)
50
  # ──────────────────────────────────────────────────────────────
51
  # κΈ°λ³Έ μΏ ν‚€ 파일 경둜 ― 파일λͺ…이 λ™μΌν•˜λ©΄ μžλ™ μ‚¬μš©
52
  # ──────────────────────────────────────────────────────────────
 
643
  # =================================================================
644
  # Gradio UI
645
  # =================================================================
646
+ #!/usr/bin/env python3
647
+ """
648
+ YouTube Video Analyzer & Downloader Pro
649
+ ───────────────────────────────────────
650
+ β€’ `www.youtube.com_cookies.txt` κ°€ app.py 와 같은 폴더에 있으면 μžλ™μœΌλ‘œ μ‚¬μš©
651
+ β€’ UIμ—μ„œ μΏ ν‚€λ₯Ό μ—…λ‘œλ“œν•˜λ©΄ κ·Έ 파일이 *μš°μ„ * 적용
652
+ β€’ β€œTranscript” 탭을 μΆ”κ°€ν•΄ **전체 μžλ§‰ + MM:SS νƒ€μž„μŠ€νƒ¬ν”„** 좜λ ₯
653
+ """
654
+
655
+ # ── ν‘œμ€€ 라이브러리 ───────────────────────────────────────────
656
+ import os, re, json, shutil, tempfile
657
+ from datetime import datetime, timedelta
658
+ from pathlib import Path
659
+
660
+ # ── μ„œλ“œνŒŒν‹° ──────────────────────────────────────────────────
661
+ import gradio as gr
662
+ import yt_dlp
663
+ import google.generativeai as genai
664
+ from youtube_transcript_api import YouTubeTranscriptApi # NEW
665
+
666
+ # ── μƒμˆ˜ ──────────────────────────────────────────────────────
667
+ DEFAULT_COOKIE_FILE = Path(__file__).with_name("www.youtube.com_cookies.txt")
668
+
669
+ # YouTube URL μ •κ·œμ‹(캑처 κ·Έλ£Ή 6이 μ˜μƒ ID)
670
+ _YT_REGEX = re.compile(
671
+ r"(https?://)?(www\.)?"
672
+ r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
673
+ r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
674
+ )
675
+
676
+
677
+ # =================================================================
678
+ # Helper : video-ID μΆ”μΆœ + μžλ§‰ κ°€μ Έμ˜€κΈ°
679
+ # =================================================================
680
+ def extract_video_id(url: str) -> str | None:
681
+ """유튜브 URLμ—μ„œ 11-κΈ€μž λΉ„λ””μ˜€ ID λ°˜ν™˜(μ—†μœΌλ©΄ None)"""
682
+ m = _YT_REGEX.match(url)
683
+ return m.group(6) if m else None
684
+
685
+
686
+ def fetch_transcript(video_id: str, pref_lang=("ko", "en")) -> str:
687
+ """
688
+ 유튜브 μžλ§‰μ„ 가져와
689
+ **[MM:SS]** line ν˜•μ‹μœΌλ‘œ κ²°ν•©ν•œ λ’€ λ¬Έμžμ—΄λ‘œ λ°˜ν™˜.
690
+ """
691
+ transcript = None
692
+ # μ–Έμ–΄ μš°μ„ μˆœμœ„λŒ€λ‘œ μ‹œλ„
693
+ for lang in pref_lang:
694
+ try:
695
+ transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
696
+ break
697
+ except Exception:
698
+ continue
699
+ # κ·Έλž˜λ„ μ‹€νŒ¨ν•˜λ©΄ μž„μ˜ μ–Έμ–΄
700
+ if transcript is None:
701
+ transcript = YouTubeTranscriptApi.get_transcript(video_id)
702
+
703
+ lines = []
704
+ for seg in transcript:
705
+ t = str(timedelta(seconds=int(seg["start"]))) # H:MM:SS
706
+ t_mmss = ":".join(t.split(":")[-2:]) # MM:SS
707
+ lines.append(f"**[{t_mmss}]** {seg['text']}")
708
+ return "\n".join(lines)
709
+
710
+
711
+ # =================================================================
712
+ # 메인 클래슀
713
+ # =================================================================
714
+ class YouTubeDownloader:
715
+ def __init__(self):
716
+ self.download_dir = tempfile.mkdtemp()
717
+ self.temp_downloads = tempfile.mkdtemp(prefix="youtube_downloads_")
718
+ self.downloads_folder = os.path.join(
719
+ os.path.expanduser("~"), "Downloads", "YouTube_Downloads"
720
+ )
721
+ os.makedirs(self.downloads_folder, exist_ok=True)
722
+ self.gemini_model = None
723
+
724
+ # ───────── Gemini ─────────
725
+ def configure_gemini(self, api_key):
726
+ try:
727
+ genai.configure(api_key=api_key)
728
+ self.gemini_model = genai.GenerativeModel(
729
+ model_name="gemini-1.5-flash-latest"
730
+ )
731
+ return True, "βœ… Gemini API configured successfully!"
732
+ except Exception as e:
733
+ return False, f"❌ Failed to configure Gemini API: {e}"
734
+
735
+ # ───────── 정리 ─────────
736
+ def cleanup(self):
737
+ try:
738
+ if os.path.exists(self.download_dir):
739
+ shutil.rmtree(self.download_dir)
740
+ if os.path.exists(self.temp_downloads):
741
+ shutil.rmtree(self.temp_downloads)
742
+ except Exception:
743
+ pass
744
+
745
+ # ───────── URL 검증 ──────
746
+ def is_valid_youtube_url(self, url):
747
+ return _YT_REGEX.match(url) is not None
748
+
749
+ # ───────── Gemini scene breakdown (μƒλž΅ 없이 전체 κ΅¬ν˜„) ──────
750
+ def generate_scene_breakdown_gemini(self, video_info):
751
+ if not self.gemini_model:
752
+ return self.generate_scene_breakdown_fallback(video_info)
753
+ try:
754
+ duration = video_info.get("duration", 0)
755
+ title = video_info.get("title", "")
756
+ description = video_info.get("description", "")[:1500]
757
+ if not duration:
758
+ return [
759
+ "**[Duration Unknown]**: Unable to generate timestamped breakdown – "
760
+ "video duration not available"
761
+ ]
762
+
763
+ prompt = f"""
764
+ Analyze this YouTube video and create a highly detailed, scene-by-scene breakdown
765
+ with precise timestamps and specific descriptions:
766
+
767
+ Title: {title}
768
+ Duration: {duration} seconds
769
+ Description: {description}
770
+
771
+ IMPORTANT INSTRUCTIONS:
772
+ 1. Create detailed scene descriptions that include:
773
+ - Physical appearance of people (age, gender, clothing, hair, etc.)
774
+ - Exact actions being performed
775
+ - Dialogue or speech (include actual lines if audible, or infer probable spoken
776
+ lines based on actions and setting; format them as "Character: line…")
777
+ - Setting and environment details
778
+ - Props, objects, or products being shown
779
+ - Visual effects, text overlays, or graphics
780
+ - Mood, tone, and atmosphere
781
+ - Camera movements or angles (if apparent)
782
+ 2. Dialogue Emphasis:
783
+ - Include short dialogue lines in **every scene** wherever plausible.
784
+ - Write lines like: Character: "Actual or inferred line…"
785
+ - If dialogue is not available, intelligently infer probable phrases
786
+ 3. Timestamp Guidelines:
787
+ - <1 min : 2–3 s | 1–5 min : 3–5 s | 5–15 min : 5–10 s | >15 min : 10–15 s
788
+ - Max 20 scenes
789
+ 4. Format: **[MM:SS-MM:SS]** description
790
+ """
791
+ resp = self.gemini_model.generate_content(prompt)
792
+ if not resp or not resp.text:
793
+ return self.generate_scene_breakdown_fallback(video_info)
794
+
795
+ scenes, cur = [], ""
796
+ for line in resp.text.splitlines():
797
+ line = line.strip()
798
+ if line.startswith("**[") and "]**:" in line:
799
+ if cur:
800
+ scenes.append(cur.strip())
801
+ cur = line
802
+ elif cur:
803
+ cur += "\n" + line
804
+ if cur:
805
+ scenes.append(cur.strip())
806
+ return scenes if scenes else self.generate_scene_breakdown_fallback(video_info)
807
+ except Exception:
808
+ return self.generate_scene_breakdown_fallback(video_info)
809
+
810
+ # ───────── fallback breakdown ──────
811
+ def generate_scene_breakdown_fallback(self, video_info):
812
+ duration = video_info.get("duration", 0)
813
+ if not duration:
814
+ return ["**[Duration Unknown]**: Unable to generate timestamped breakdown"]
815
+
816
+ if duration <= 60:
817
+ seg = 3
818
+ elif duration <= 300:
819
+ seg = 5
820
+ elif duration <= 900:
821
+ seg = 10
822
+ else:
823
+ seg = 15
824
+
825
+ total = min(duration // seg + 1, 20)
826
+ vtype = self.detect_video_type_detailed(
827
+ video_info.get("title", ""), video_info.get("description", "")
828
+ )
829
+ scenes = []
830
+ for i in range(total):
831
+ s, e = i * seg, min(i * seg + seg - 1, duration)
832
+ scenes.append(
833
+ f"**[{s//60:02d}:{s%60:02d}-{e//60:02d}:{e%60:02d}]**: "
834
+ f"{self.generate_contextual_description(i, total, vtype, '', video_info.get('title',''))}"
835
+ )
836
+ return scenes
837
+
838
+ # ───────── detect helpers (상세) ──────
839
+ def detect_video_type_detailed(self, title, desc):
840
+ t = (title + " " + desc).lower()
841
+ if any(x in t for x in ["tutorial", "how to", "guide", "diy"]):
842
+ return "tutorial"
843
+ if any(x in t for x in ["review", "unboxing", "comparison"]):
844
+ return "review"
845
+ if any(x in t for x in ["vlog", "daily", "routine"]):
846
+ return "vlog"
847
+ if any(x in t for x in ["music", "song", "cover"]):
848
+ return "music"
849
+ if any(x in t for x in ["comedy", "prank", "challenge"]):
850
+ return "entertainment"
851
+ if any(x in t for x in ["news", "update", "report"]):
852
+ return "news"
853
+ if any(x in t for x in ["cooking", "recipe", "food"]):
854
+ return "cooking"
855
+ if any(x in t for x in ["workout", "fitness", "yoga"]):
856
+ return "fitness"
857
+ return "general"
858
+
859
+ def generate_contextual_description(
860
+ self, idx, total, vtype, uploader, title
861
+ ):
862
+ if idx == 0:
863
+ return "The creator greets viewers and introduces the video."
864
+ if idx == total - 1:
865
+ return "The creator wraps up and thanks viewers."
866
+ return "Content continues according to the video type."
867
+
868
+ # ───────── quick-detect helpers (μš”μ•½) ──────
869
+ def detect_video_type(self, title, desc):
870
+ t = (title + " " + desc).lower()
871
+ if any(x in t for x in ["music", "song", "album"]):
872
+ return "🎡 Music"
873
+ if any(x in t for x in ["tutorial", "guide"]):
874
+ return "πŸ“š Tutorial"
875
+ if any(x in t for x in ["comedy", "vlog"]):
876
+ return "🎭 Entertainment"
877
+ if any(x in t for x in ["news", "report"]):
878
+ return "πŸ“° News"
879
+ if any(x in t for x in ["review", "unboxing"]):
880
+ return "⭐ Review"
881
+ return "🎬 General"
882
+
883
+ def detect_background_music(self, video_info):
884
+ title = video_info.get("title", "").lower()
885
+ if "music" in title:
886
+ return "🎡 Original music"
887
+ if "tutorial" in title:
888
+ return "πŸ”‡ Minimal music"
889
+ return "🎼 Background music"
890
+
891
+ def detect_influencer_status(self, video_info):
892
+ subs = video_info.get("channel_followers", 0)
893
+ if subs > 10_000_000:
894
+ return "🌟 Mega (10 M+)"
895
+ if subs > 1_000_000:
896
+ return "⭐ Major (1 M+)"
897
+ if subs > 100_000:
898
+ return "🎯 Mid (100 K+)"
899
+ return "πŸ‘€"
900
+
901
+ @staticmethod
902
+ def format_number(n):
903
+ if n >= 1_000_000:
904
+ return f"{n/1_000_000:.1f} M"
905
+ if n >= 1_000:
906
+ return f"{n/1_000:.1f} K"
907
+ return str(n)
908
+
909
+ # ───────── 리포트 ──────
910
+ def format_video_info(self, info):
911
+ title = info.get("title", "")
912
+ uploader = info.get("uploader", "")
913
+ duration = info.get("duration", 0)
914
+ dur = f"{duration//60}:{duration%60:02d}"
915
+ views = info.get("view_count", 0)
916
+ likes = info.get("like_count", 0)
917
+ comments = info.get("comment_count", 0)
918
+ scenes = self.generate_scene_breakdown_gemini(info)
919
+
920
+ return f"""
921
+ 🎬 **{title}**
922
+ Uploader: {uploader} Duration: {dur}
923
+
924
+ Views / Likes / Comments: {self.format_number(views)} / {self.format_number(likes)} / {self.format_number(comments)}
925
+
926
+ {'-'*48}
927
+ {"".join(scenes)}
928
+ """
929
+
930
+ # ───────── 메타데이터 μΆ”μΆœ ──────
931
+ def get_video_info(self, url, progress=gr.Progress(), cookiefile=None):
932
+ if not self.is_valid_youtube_url(url):
933
+ return None, "❌ Invalid URL"
934
+
935
+ if cookiefile and os.path.exists(cookiefile):
936
+ cookiefile = cookiefile
937
+ elif DEFAULT_COOKIE_FILE.exists():
938
+ cookiefile = str(DEFAULT_COOKIE_FILE)
939
+ else:
940
+ cookiefile = None
941
+
942
+ try:
943
+ ydl_opts = {"noplaylist": True, "quiet": True}
944
+ if cookiefile:
945
+ ydl_opts["cookiefile"] = cookiefile
946
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
947
+ info = ydl.extract_info(url, download=False)
948
+ return info, "OK"
949
+ except Exception as e:
950
+ return None, f"yt-dlp error: {e}"
951
+
952
+ # ───────── λ‹€μš΄λ‘œλ“œ ──────
953
+ def download_video(
954
+ self, url, quality="best", audio_only=False, progress=gr.Progress(), cookiefile=None
955
+ ):
956
+ if not self.is_valid_youtube_url(url):
957
+ return None, "❌ Invalid URL"
958
+ if cookiefile and os.path.exists(cookiefile):
959
+ cookiefile = cookiefile
960
+ elif DEFAULT_COOKIE_FILE.exists():
961
+ cookiefile = str(DEFAULT_COOKIE_FILE)
962
+ else:
963
+ cookiefile = None
964
+
965
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
966
+ ydl_opts = {
967
+ "outtmpl": os.path.join(self.temp_downloads, f"%(title)s_{ts}.%(ext)s"),
968
+ "noplaylist": True,
969
+ }
970
+ if audio_only:
971
+ ydl_opts["format"] = "bestaudio/best"
972
+ ydl_opts["postprocessors"] = [
973
+ {"key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192"}
974
+ ]
975
+ else:
976
+ if quality == "720p":
977
+ ydl_opts["format"] = "best[height<=720]"
978
+ elif quality == "480p":
979
+ ydl_opts["format"] = "best[height<=480]"
980
+ else:
981
+ ydl_opts["format"] = "best[height<=1080]"
982
+ if cookiefile:
983
+ ydl_opts["cookiefile"] = cookiefile
984
+
985
+ try:
986
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
987
+ ydl.extract_info(url, download=True)
988
+ # 첫 파일 찾기
989
+ for f in os.listdir(self.temp_downloads):
990
+ if ts in f:
991
+ temp_fp = os.path.join(self.temp_downloads, f)
992
+ final_fp = os.path.join(self.downloads_folder, f)
993
+ try:
994
+ shutil.copy2(temp_fp, final_fp)
995
+ saved = final_fp
996
+ except Exception:
997
+ saved = temp_fp
998
+ return temp_fp, f"βœ… Saved: {saved}"
999
+ return None, "❌ Downloaded file not found"
1000
+ except Exception as e:
1001
+ return None, f"❌ Download failed: {e}"
1002
+
1003
+
1004
+ # =================================================================
1005
+ # Gradio Helper ν•¨μˆ˜
1006
+ # =================================================================
1007
+ downloader = YouTubeDownloader()
1008
+
1009
+
1010
+ def configure_api_key(api_key):
1011
+ ok, msg = downloader.configure_gemini(api_key.strip()) if api_key else (False, "❌ API key required")
1012
+ return msg, gr.update(visible=ok)
1013
+
1014
+
1015
+ def analyze_with_cookies(url, cookies_file, progress=gr.Progress()):
1016
+ info, err = downloader.get_video_info(url, progress, cookies_file)
1017
+ return downloader.format_video_info(info) if info else f"❌ {err}"
1018
+
1019
+
1020
+ def download_with_cookies(url, qual, audio, cookies_file, progress=gr.Progress()):
1021
+ fp, msg = downloader.download_video(url, qual, audio, progress, cookies_file)
1022
+ return fp, msg
1023
+
1024
+
1025
+ def get_transcript(url, cookies_file):
1026
+ vid = extract_video_id(url)
1027
+ if not vid:
1028
+ return "❌ Invalid YouTube URL"
1029
+ try:
1030
+ return fetch_transcript(vid)
1031
+ except Exception as e:
1032
+ return f"❌ {e}"
1033
+
1034
+
1035
+ # =================================================================
1036
+ # UI
1037
+ # =================================================================
1038
  def create_interface():
1039
  with gr.Blocks(
1040
  theme=gr.themes.Soft(), title="πŸŽ₯ YouTube Video Analyzer & Downloader Pro"
1041
  ) as iface:
1042
  gr.HTML("<h1>πŸŽ₯ YouTube Video Analyzer & Downloader Pro</h1>")
1043
 
1044
+ # API μ„€μ •
1045
  with gr.Group():
1046
  gr.HTML("<h3>πŸ”‘ Google Gemini API Configuration</h3>")
1047
  with gr.Row():
1048
  api_key_in = gr.Textbox(
1049
+ label="πŸ”‘ Google API Key", type="password", placeholder="Paste your Google API key…"
 
 
1050
  )
1051
  api_btn = gr.Button("πŸ”§ Configure API", variant="secondary")
1052
  api_status = gr.Textbox(
 
1056
  lines=1,
1057
  )
1058
 
1059
+ # 곡톡 μž…λ ₯
1060
  with gr.Row():
1061
+ url_in = gr.Textbox(label="πŸ”— YouTube URL", placeholder="Paste YouTube video URL…")
 
 
 
1062
  cookies_in = gr.File(
1063
+ label="πŸͺ Upload cookies.txt (optional)", file_types=[".txt"], type="filepath"
 
 
1064
  )
1065
 
1066
  with gr.Tabs():
1067
+ # 뢄석 νƒ­
1068
  with gr.TabItem("πŸ“Š Video Analysis"):
1069
  analyze_btn = gr.Button("πŸ” Analyze Video", variant="primary")
1070
+ analysis_out = gr.Textbox(label="πŸ“Š Analysis Report", lines=30, show_copy_button=True)
 
 
1071
  analyze_btn.click(
1072
+ analyze_with_cookies, inputs=[url_in, cookies_in], outputs=analysis_out, show_progress=True
1073
+ )
1074
+ # λ‹€μš΄λ‘œλ“œ νƒ­
1075
+ with gr.TabItem("⬇️ Video Download"):
1076
+ with gr.Row():
1077
+ quality_dd = gr.Dropdown(
1078
+ choices=["best", "720p", "480p"], value="best", label="πŸ“Ί Quality"
1079
+ )
1080
+ audio_cb = gr.Checkbox(label="🎡 Audio only (MP3)")
1081
+ download_btn = gr.Button("⬇️ Download Video", variant="primary")
1082
+ dl_status = gr.Textbox(label="πŸ“₯ Download Status", lines=5, show_copy_button=True)
1083
+ dl_file = gr.File(label="πŸ“ Downloaded File", visible=False)
1084
+
1085
+ def wrapped_dl(u, q, a, c, prog=gr.Progress()):
1086
+ fp, st = download_with_cookies(u, q, a, c, prog)
1087
+ return (st, gr.update(value=fp, visible=True)) if fp and os.path.exists(fp) else (
1088
+ st,
1089
+ gr.update(visible=False),
1090
+ )
1091
+
1092
+ download_btn.click(
1093
+ wrapped_dl,
1094
+ inputs=[url_in, quality_dd, audio_cb, cookies_in],
1095
+ outputs=[dl_status, dl_file],
1096
  show_progress=True,
1097
  )
1098
+ # μžλ§‰ νƒ­ NEW
1099
+ with gr.TabItem("πŸ—’οΈ Transcript"):
1100
+ tr_btn = gr.Button("πŸ“œ Get Full Transcript", variant="primary")
1101
+ tr_out = gr.Textbox(
1102
+ label="πŸ—’οΈ Transcript (full)", lines=30, show_copy_button=True
1103
+ )
1104
+ tr_btn.click(
1105
+ get_transcript, inputs=[url_in, cookies_in], outputs=tr_out, show_progress=True
1106
+ )
1107
+
1108
+ # API λ²„νŠΌ
1109
+ api_btn.click(configure_api_key, inputs=[api_key_in], outputs=[api_status])
1110
+
1111
+ gr.HTML(
1112
+ """
1113
+ <div style="margin-top:20px;padding:15px;background:#f0f8ff;border-left:5px solid #4285f4;border-radius:10px;">
1114
+ <h3>πŸ’‘ Tip: μΏ ν‚€ 파일 μžλ™ μ‚¬μš©</h3>
1115
+ <p><code>www.youtube.com_cookies.txt</code> νŒŒμΌμ„ <strong>app.py</strong>와 같은
1116
+ 폴더에 두면 μ—…λ‘œλ“œ 없이 μžλ™ μ‚¬μš©λ©λ‹ˆλ‹€.</p>
1117
+ </div>
1118
+ """
1119
+ )
1120
+ return iface
1121
+
1122
+
1123
+ # =================================================================
1124
+ # μ‹€ν–‰
1125
+ # =================================================================
1126
+ if __name__ == "__main__":
1127
+ demo = create_interface()
1128
+ import atexit
1129
+
1130
+ atexit.register(downloader.cleanup)
1131
+ demo.launch(debug=True, show_error=True)
1132
+ #!/usr/bin/env python3
1133
+ """
1134
+ YouTube Video Analyzer & Downloader Pro
1135
+ ───────────────────────────────────────
1136
+ β€’ `www.youtube.com_cookies.txt` κ°€ app.py 와 같은 폴더에 있으면 μžλ™μœΌλ‘œ μ‚¬μš©
1137
+ β€’ UIμ—μ„œ μΏ ν‚€λ₯Ό μ—…λ‘œλ“œν•˜λ©΄ κ·Έ 파일이 *μš°μ„ * 적용
1138
+ β€’ β€œTranscript” 탭을 μΆ”κ°€ν•΄ **전체 μžλ§‰ + MM:SS νƒ€μž„μŠ€νƒ¬ν”„** 좜λ ₯
1139
+ """
1140
+
1141
+ # ── ν‘œμ€€ 라이브러리 ───────────────────────────────────────────
1142
+ import os, re, json, shutil, tempfile
1143
+ from datetime import datetime, timedelta
1144
+ from pathlib import Path
1145
+
1146
+ # ── μ„œλ“œνŒŒν‹° ──────────────────────────────────────────────────
1147
+ import gradio as gr
1148
+ import yt_dlp
1149
+ import google.generativeai as genai
1150
+ from youtube_transcript_api import YouTubeTranscriptApi # NEW
1151
+
1152
+ # ── μƒμˆ˜ ──────────────────────────────────────────────────────
1153
+ DEFAULT_COOKIE_FILE = Path(__file__).with_name("www.youtube.com_cookies.txt")
1154
+
1155
+ # YouTube URL μ •κ·œμ‹(캑처 κ·Έλ£Ή 6이 μ˜μƒ ID)
1156
+ _YT_REGEX = re.compile(
1157
+ r"(https?://)?(www\.)?"
1158
+ r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
1159
+ r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
1160
+ )
1161
+
1162
+
1163
+ # =================================================================
1164
+ # Helper : video-ID μΆ”μΆœ + μžλ§‰ κ°€μ Έμ˜€κΈ°
1165
+ # =================================================================
1166
+ def extract_video_id(url: str) -> str | None:
1167
+ """유튜브 URLμ—μ„œ 11-κΈ€μž λΉ„λ””μ˜€ ID λ°˜ν™˜(μ—†μœΌλ©΄ None)"""
1168
+ m = _YT_REGEX.match(url)
1169
+ return m.group(6) if m else None
1170
+
1171
+
1172
+ def fetch_transcript(video_id: str, pref_lang=("ko", "en")) -> str:
1173
+ """
1174
+ 유튜브 μžλ§‰μ„ 가져와
1175
+ **[MM:SS]** line ν˜•μ‹μœΌλ‘œ κ²°ν•©ν•œ λ’€ λ¬Έμžμ—΄λ‘œ λ°˜ν™˜.
1176
+ """
1177
+ transcript = None
1178
+ # μ–Έμ–΄ μš°μ„ μˆœμœ„λŒ€λ‘œ μ‹œλ„
1179
+ for lang in pref_lang:
1180
+ try:
1181
+ transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
1182
+ break
1183
+ except Exception:
1184
+ continue
1185
+ # κ·Έλž˜λ„ μ‹€νŒ¨ν•˜λ©΄ μž„μ˜ μ–Έμ–΄
1186
+ if transcript is None:
1187
+ transcript = YouTubeTranscriptApi.get_transcript(video_id)
1188
+
1189
+ lines = []
1190
+ for seg in transcript:
1191
+ t = str(timedelta(seconds=int(seg["start"]))) # H:MM:SS
1192
+ t_mmss = ":".join(t.split(":")[-2:]) # MM:SS
1193
+ lines.append(f"**[{t_mmss}]** {seg['text']}")
1194
+ return "\n".join(lines)
1195
+
1196
+
1197
+ # =================================================================
1198
+ # 메인 클래슀
1199
+ # =================================================================
1200
+ class YouTubeDownloader:
1201
+ def __init__(self):
1202
+ self.download_dir = tempfile.mkdtemp()
1203
+ self.temp_downloads = tempfile.mkdtemp(prefix="youtube_downloads_")
1204
+ self.downloads_folder = os.path.join(
1205
+ os.path.expanduser("~"), "Downloads", "YouTube_Downloads"
1206
+ )
1207
+ os.makedirs(self.downloads_folder, exist_ok=True)
1208
+ self.gemini_model = None
1209
+
1210
+ # ───────── Gemini ─────────
1211
+ def configure_gemini(self, api_key):
1212
+ try:
1213
+ genai.configure(api_key=api_key)
1214
+ self.gemini_model = genai.GenerativeModel(
1215
+ model_name="gemini-1.5-flash-latest"
1216
+ )
1217
+ return True, "βœ… Gemini API configured successfully!"
1218
+ except Exception as e:
1219
+ return False, f"❌ Failed to configure Gemini API: {e}"
1220
+
1221
+ # ───────── 정리 ─────────
1222
+ def cleanup(self):
1223
+ try:
1224
+ if os.path.exists(self.download_dir):
1225
+ shutil.rmtree(self.download_dir)
1226
+ if os.path.exists(self.temp_downloads):
1227
+ shutil.rmtree(self.temp_downloads)
1228
+ except Exception:
1229
+ pass
1230
+
1231
+ # ───────── URL 검증 ──────
1232
+ def is_valid_youtube_url(self, url):
1233
+ return _YT_REGEX.match(url) is not None
1234
+
1235
+ # ───────── Gemini scene breakdown (μƒλž΅ 없이 전체 κ΅¬ν˜„) ──────
1236
+ def generate_scene_breakdown_gemini(self, video_info):
1237
+ if not self.gemini_model:
1238
+ return self.generate_scene_breakdown_fallback(video_info)
1239
+ try:
1240
+ duration = video_info.get("duration", 0)
1241
+ title = video_info.get("title", "")
1242
+ description = video_info.get("description", "")[:1500]
1243
+ if not duration:
1244
+ return [
1245
+ "**[Duration Unknown]**: Unable to generate timestamped breakdown – "
1246
+ "video duration not available"
1247
+ ]
1248
+
1249
+ prompt = f"""
1250
+ Analyze this YouTube video and create a highly detailed, scene-by-scene breakdown
1251
+ with precise timestamps and specific descriptions:
1252
+
1253
+ Title: {title}
1254
+ Duration: {duration} seconds
1255
+ Description: {description}
1256
+
1257
+ IMPORTANT INSTRUCTIONS:
1258
+ 1. Create detailed scene descriptions that include:
1259
+ - Physical appearance of people (age, gender, clothing, hair, etc.)
1260
+ - Exact actions being performed
1261
+ - Dialogue or speech (include actual lines if audible, or infer probable spoken
1262
+ lines based on actions and setting; format them as "Character: line…")
1263
+ - Setting and environment details
1264
+ - Props, objects, or products being shown
1265
+ - Visual effects, text overlays, or graphics
1266
+ - Mood, tone, and atmosphere
1267
+ - Camera movements or angles (if apparent)
1268
+ 2. Dialogue Emphasis:
1269
+ - Include short dialogue lines in **every scene** wherever plausible.
1270
+ - Write lines like: Character: "Actual or inferred line…"
1271
+ - If dialogue is not available, intelligently infer probable phrases
1272
+ 3. Timestamp Guidelines:
1273
+ - <1 min : 2–3 s | 1–5 min : 3–5 s | 5–15 min : 5–10 s | >15 min : 10–15 s
1274
+ - Max 20 scenes
1275
+ 4. Format: **[MM:SS-MM:SS]** description
1276
+ """
1277
+ resp = self.gemini_model.generate_content(prompt)
1278
+ if not resp or not resp.text:
1279
+ return self.generate_scene_breakdown_fallback(video_info)
1280
+
1281
+ scenes, cur = [], ""
1282
+ for line in resp.text.splitlines():
1283
+ line = line.strip()
1284
+ if line.startswith("**[") and "]**:" in line:
1285
+ if cur:
1286
+ scenes.append(cur.strip())
1287
+ cur = line
1288
+ elif cur:
1289
+ cur += "\n" + line
1290
+ if cur:
1291
+ scenes.append(cur.strip())
1292
+ return scenes if scenes else self.generate_scene_breakdown_fallback(video_info)
1293
+ except Exception:
1294
+ return self.generate_scene_breakdown_fallback(video_info)
1295
+
1296
+ # ───────── fallback breakdown ──────
1297
+ def generate_scene_breakdown_fallback(self, video_info):
1298
+ duration = video_info.get("duration", 0)
1299
+ if not duration:
1300
+ return ["**[Duration Unknown]**: Unable to generate timestamped breakdown"]
1301
+
1302
+ if duration <= 60:
1303
+ seg = 3
1304
+ elif duration <= 300:
1305
+ seg = 5
1306
+ elif duration <= 900:
1307
+ seg = 10
1308
+ else:
1309
+ seg = 15
1310
+
1311
+ total = min(duration // seg + 1, 20)
1312
+ vtype = self.detect_video_type_detailed(
1313
+ video_info.get("title", ""), video_info.get("description", "")
1314
+ )
1315
+ scenes = []
1316
+ for i in range(total):
1317
+ s, e = i * seg, min(i * seg + seg - 1, duration)
1318
+ scenes.append(
1319
+ f"**[{s//60:02d}:{s%60:02d}-{e//60:02d}:{e%60:02d}]**: "
1320
+ f"{self.generate_contextual_description(i, total, vtype, '', video_info.get('title',''))}"
1321
+ )
1322
+ return scenes
1323
+
1324
+ # ───────── detect helpers (상세) ──────
1325
+ def detect_video_type_detailed(self, title, desc):
1326
+ t = (title + " " + desc).lower()
1327
+ if any(x in t for x in ["tutorial", "how to", "guide", "diy"]):
1328
+ return "tutorial"
1329
+ if any(x in t for x in ["review", "unboxing", "comparison"]):
1330
+ return "review"
1331
+ if any(x in t for x in ["vlog", "daily", "routine"]):
1332
+ return "vlog"
1333
+ if any(x in t for x in ["music", "song", "cover"]):
1334
+ return "music"
1335
+ if any(x in t for x in ["comedy", "prank", "challenge"]):
1336
+ return "entertainment"
1337
+ if any(x in t for x in ["news", "update", "report"]):
1338
+ return "news"
1339
+ if any(x in t for x in ["cooking", "recipe", "food"]):
1340
+ return "cooking"
1341
+ if any(x in t for x in ["workout", "fitness", "yoga"]):
1342
+ return "fitness"
1343
+ return "general"
1344
+
1345
+ def generate_contextual_description(
1346
+ self, idx, total, vtype, uploader, title
1347
+ ):
1348
+ if idx == 0:
1349
+ return "The creator greets viewers and introduces the video."
1350
+ if idx == total - 1:
1351
+ return "The creator wraps up and thanks viewers."
1352
+ return "Content continues according to the video type."
1353
+
1354
+ # ───────── quick-detect helpers (μš”μ•½) ──────
1355
+ def detect_video_type(self, title, desc):
1356
+ t = (title + " " + desc).lower()
1357
+ if any(x in t for x in ["music", "song", "album"]):
1358
+ return "🎡 Music"
1359
+ if any(x in t for x in ["tutorial", "guide"]):
1360
+ return "πŸ“š Tutorial"
1361
+ if any(x in t for x in ["comedy", "vlog"]):
1362
+ return "🎭 Entertainment"
1363
+ if any(x in t for x in ["news", "report"]):
1364
+ return "πŸ“° News"
1365
+ if any(x in t for x in ["review", "unboxing"]):
1366
+ return "⭐ Review"
1367
+ return "🎬 General"
1368
+
1369
+ def detect_background_music(self, video_info):
1370
+ title = video_info.get("title", "").lower()
1371
+ if "music" in title:
1372
+ return "🎡 Original music"
1373
+ if "tutorial" in title:
1374
+ return "πŸ”‡ Minimal music"
1375
+ return "🎼 Background music"
1376
+
1377
+ def detect_influencer_status(self, video_info):
1378
+ subs = video_info.get("channel_followers", 0)
1379
+ if subs > 10_000_000:
1380
+ return "🌟 Mega (10 M+)"
1381
+ if subs > 1_000_000:
1382
+ return "⭐ Major (1 M+)"
1383
+ if subs > 100_000:
1384
+ return "🎯 Mid (100 K+)"
1385
+ return "πŸ‘€"
1386
+
1387
+ @staticmethod
1388
+ def format_number(n):
1389
+ if n >= 1_000_000:
1390
+ return f"{n/1_000_000:.1f} M"
1391
+ if n >= 1_000:
1392
+ return f"{n/1_000:.1f} K"
1393
+ return str(n)
1394
+
1395
+ # ───────── 리포트 ──────
1396
+ def format_video_info(self, info):
1397
+ title = info.get("title", "")
1398
+ uploader = info.get("uploader", "")
1399
+ duration = info.get("duration", 0)
1400
+ dur = f"{duration//60}:{duration%60:02d}"
1401
+ views = info.get("view_count", 0)
1402
+ likes = info.get("like_count", 0)
1403
+ comments = info.get("comment_count", 0)
1404
+ scenes = self.generate_scene_breakdown_gemini(info)
1405
+
1406
+ return f"""
1407
+ 🎬 **{title}**
1408
+ Uploader: {uploader} Duration: {dur}
1409
+
1410
+ Views / Likes / Comments: {self.format_number(views)} / {self.format_number(likes)} / {self.format_number(comments)}
1411
+
1412
+ {'-'*48}
1413
+ {"".join(scenes)}
1414
+ """
1415
+
1416
+ # ───────── 메타데이터 μΆ”μΆœ ──────
1417
+ def get_video_info(self, url, progress=gr.Progress(), cookiefile=None):
1418
+ if not self.is_valid_youtube_url(url):
1419
+ return None, "❌ Invalid URL"
1420
+
1421
+ if cookiefile and os.path.exists(cookiefile):
1422
+ cookiefile = cookiefile
1423
+ elif DEFAULT_COOKIE_FILE.exists():
1424
+ cookiefile = str(DEFAULT_COOKIE_FILE)
1425
+ else:
1426
+ cookiefile = None
1427
+
1428
+ try:
1429
+ ydl_opts = {"noplaylist": True, "quiet": True}
1430
+ if cookiefile:
1431
+ ydl_opts["cookiefile"] = cookiefile
1432
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
1433
+ info = ydl.extract_info(url, download=False)
1434
+ return info, "OK"
1435
+ except Exception as e:
1436
+ return None, f"yt-dlp error: {e}"
1437
+
1438
+ # ───────── λ‹€μš΄λ‘œλ“œ ──────
1439
+ def download_video(
1440
+ self, url, quality="best", audio_only=False, progress=gr.Progress(), cookiefile=None
1441
+ ):
1442
+ if not self.is_valid_youtube_url(url):
1443
+ return None, "❌ Invalid URL"
1444
+ if cookiefile and os.path.exists(cookiefile):
1445
+ cookiefile = cookiefile
1446
+ elif DEFAULT_COOKIE_FILE.exists():
1447
+ cookiefile = str(DEFAULT_COOKIE_FILE)
1448
+ else:
1449
+ cookiefile = None
1450
+
1451
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
1452
+ ydl_opts = {
1453
+ "outtmpl": os.path.join(self.temp_downloads, f"%(title)s_{ts}.%(ext)s"),
1454
+ "noplaylist": True,
1455
+ }
1456
+ if audio_only:
1457
+ ydl_opts["format"] = "bestaudio/best"
1458
+ ydl_opts["postprocessors"] = [
1459
+ {"key": "FFmpegExtractAudio", "preferredcodec": "mp3", "preferredquality": "192"}
1460
+ ]
1461
+ else:
1462
+ if quality == "720p":
1463
+ ydl_opts["format"] = "best[height<=720]"
1464
+ elif quality == "480p":
1465
+ ydl_opts["format"] = "best[height<=480]"
1466
+ else:
1467
+ ydl_opts["format"] = "best[height<=1080]"
1468
+ if cookiefile:
1469
+ ydl_opts["cookiefile"] = cookiefile
1470
+
1471
+ try:
1472
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
1473
+ ydl.extract_info(url, download=True)
1474
+ # 첫 파일 찾기
1475
+ for f in os.listdir(self.temp_downloads):
1476
+ if ts in f:
1477
+ temp_fp = os.path.join(self.temp_downloads, f)
1478
+ final_fp = os.path.join(self.downloads_folder, f)
1479
+ try:
1480
+ shutil.copy2(temp_fp, final_fp)
1481
+ saved = final_fp
1482
+ except Exception:
1483
+ saved = temp_fp
1484
+ return temp_fp, f"βœ… Saved: {saved}"
1485
+ return None, "❌ Downloaded file not found"
1486
+ except Exception as e:
1487
+ return None, f"❌ Download failed: {e}"
1488
+
1489
+
1490
+ # =================================================================
1491
+ # Gradio Helper ν•¨μˆ˜
1492
+ # =================================================================
1493
+ downloader = YouTubeDownloader()
1494
+
1495
+
1496
+ def configure_api_key(api_key):
1497
+ ok, msg = downloader.configure_gemini(api_key.strip()) if api_key else (False, "❌ API key required")
1498
+ return msg, gr.update(visible=ok)
1499
+
1500
+
1501
+ def analyze_with_cookies(url, cookies_file, progress=gr.Progress()):
1502
+ info, err = downloader.get_video_info(url, progress, cookies_file)
1503
+ return downloader.format_video_info(info) if info else f"❌ {err}"
1504
+
1505
+
1506
+ def download_with_cookies(url, qual, audio, cookies_file, progress=gr.Progress()):
1507
+ fp, msg = downloader.download_video(url, qual, audio, progress, cookies_file)
1508
+ return fp, msg
1509
+
1510
+
1511
+ def get_transcript(url, cookies_file):
1512
+ vid = extract_video_id(url)
1513
+ if not vid:
1514
+ return "❌ Invalid YouTube URL"
1515
+ try:
1516
+ return fetch_transcript(vid)
1517
+ except Exception as e:
1518
+ return f"❌ {e}"
1519
+
1520
+
1521
+ # =================================================================
1522
+ # UI
1523
+ # =================================================================
1524
+ def create_interface():
1525
+ with gr.Blocks(
1526
+ theme=gr.themes.Soft(), title="πŸŽ₯ YouTube Video Analyzer & Downloader Pro"
1527
+ ) as iface:
1528
+ gr.HTML("<h1>πŸŽ₯ YouTube Video Analyzer & Downloader Pro</h1>")
1529
+
1530
+ # API μ„€μ •
1531
+ with gr.Group():
1532
+ gr.HTML("<h3>πŸ”‘ Google Gemini API Configuration</h3>")
1533
+ with gr.Row():
1534
+ api_key_in = gr.Textbox(
1535
+ label="πŸ”‘ Google API Key", type="password", placeholder="Paste your Google API key…"
1536
+ )
1537
+ api_btn = gr.Button("πŸ”§ Configure API", variant="secondary")
1538
+ api_status = gr.Textbox(
1539
+ label="API Status",
1540
+ value="❌ Gemini API not configured – Using fallback analysis",
1541
+ interactive=False,
1542
+ lines=1,
1543
+ )
1544
 
1545
+ # 곡톡 μž…λ ₯
1546
+ with gr.Row():
1547
+ url_in = gr.Textbox(label="πŸ”— YouTube URL", placeholder="Paste YouTube video URL…")
1548
+ cookies_in = gr.File(
1549
+ label="πŸͺ Upload cookies.txt (optional)", file_types=[".txt"], type="filepath"
1550
+ )
1551
+
1552
+ with gr.Tabs():
1553
+ # 뢄석 νƒ­
1554
+ with gr.TabItem("πŸ“Š Video Analysis"):
1555
+ analyze_btn = gr.Button("πŸ” Analyze Video", variant="primary")
1556
+ analysis_out = gr.Textbox(label="πŸ“Š Analysis Report", lines=30, show_copy_button=True)
1557
+ analyze_btn.click(
1558
+ analyze_with_cookies, inputs=[url_in, cookies_in], outputs=analysis_out, show_progress=True
1559
+ )
1560
+ # λ‹€μš΄λ‘œλ“œ νƒ­
1561
  with gr.TabItem("⬇️ Video Download"):
1562
  with gr.Row():
1563
  quality_dd = gr.Dropdown(
1564
+ choices=["best", "720p", "480p"], value="best", label="πŸ“Ί Quality"
 
 
1565
  )
1566
  audio_cb = gr.Checkbox(label="🎡 Audio only (MP3)")
1567
  download_btn = gr.Button("⬇️ Download Video", variant="primary")
1568
+ dl_status = gr.Textbox(label="πŸ“₯ Download Status", lines=5, show_copy_button=True)
 
 
1569
  dl_file = gr.File(label="πŸ“ Downloaded File", visible=False)
1570
 
1571
+ def wrapped_dl(u, q, a, c, prog=gr.Progress()):
1572
+ fp, st = download_with_cookies(u, q, a, c, prog)
1573
+ return (st, gr.update(value=fp, visible=True)) if fp and os.path.exists(fp) else (
1574
+ st,
1575
+ gr.update(visible=False),
1576
+ )
1577
 
1578
  download_btn.click(
1579
+ wrapped_dl,
1580
  inputs=[url_in, quality_dd, audio_cb, cookies_in],
1581
  outputs=[dl_status, dl_file],
1582
  show_progress=True,
1583
  )
1584
+ # μžλ§‰ νƒ­ NEW
1585
+ with gr.TabItem("πŸ—’οΈ Transcript"):
1586
+ tr_btn = gr.Button("πŸ“œ Get Full Transcript", variant="primary")
1587
+ tr_out = gr.Textbox(
1588
+ label="πŸ—’οΈ Transcript (full)", lines=30, show_copy_button=True
1589
+ )
1590
+ tr_btn.click(
1591
+ get_transcript, inputs=[url_in, cookies_in], outputs=tr_out, show_progress=True
1592
+ )
1593
 
1594
+ # API λ²„νŠΌ
1595
+ api_btn.click(configure_api_key, inputs=[api_key_in], outputs=[api_status])
 
 
 
 
1596
 
1597
  gr.HTML(
1598
  """
1599
  <div style="margin-top:20px;padding:15px;background:#f0f8ff;border-left:5px solid #4285f4;border-radius:10px;">
1600
  <h3>πŸ’‘ Tip: μΏ ν‚€ 파일 μžλ™ μ‚¬μš©</h3>
1601
  <p><code>www.youtube.com_cookies.txt</code> νŒŒμΌμ„ <strong>app.py</strong>와 같은
1602
+ 폴더에 두면 μ—…λ‘œλ“œ 없이 μžλ™ μ‚¬μš©λ©λ‹ˆλ‹€.</p>
1603
  </div>
1604
  """
1605
  )
 
1607
 
1608
 
1609
  # =================================================================
1610
+ # μ‹€ν–‰
1611
  # =================================================================
1612
  if __name__ == "__main__":
1613
  demo = create_interface()