Spaces:
Runtime error
Runtime error
| import spaces | |
| import os | |
| import re | |
| import json | |
| import requests | |
| import gradio as gr | |
| import yt_dlp | |
| from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip | |
| from google import genai | |
| from google.genai import types | |
| from google.oauth2.credentials import Credentials | |
| from googleapiclient.discovery import build | |
| from googleapiclient.http import MediaFileUpload | |
| # ========================================== | |
| # 1. KREDENSIAL DARI SECRETS | |
| # ========================================== | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| YT_CLIENT_ID = os.getenv("YT_CLIENT_ID") | |
| YT_CLIENT_SECRET = os.getenv("YT_CLIENT_SECRET") | |
| YT_REFRESH_TOKEN = os.getenv("YT_REFRESH_TOKEN") | |
| gemini_client = genai.Client(api_key=GEMINI_API_KEY) if GEMINI_API_KEY else None | |
| def extract_youtube_id(url): | |
| pattern = r'(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})' | |
| match = re.search(pattern, url) | |
| return match.group(1) if match else None | |
| # ========================================== | |
| # 2. DOWNLOADER DENGAN PIPED API + YT-DLP | |
| # ========================================== | |
| def download_youtube_video(youtube_url, output_filename="downloaded_temp.mp4"): | |
| """Mengunduh video menggunakan Piped API untuk menembus IP datacenter.""" | |
| video_id = extract_youtube_id(youtube_url) | |
| # Instans Piped API | |
| piped_instances = [ | |
| "https://pipedapi.kavin.rocks", | |
| "https://api.piped.privacydev.net", | |
| "https://pipedapi.tokhmi.xyz" | |
| ] | |
| if video_id: | |
| for instance in piped_instances: | |
| try: | |
| print(f"Mencoba mengunduh via Piped API: {instance}") | |
| resp = requests.get(f"{instance}/streams/{video_id}", timeout=10) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| video_streams = data.get("videoStreams", []) | |
| # Cari stream video mp4 | |
| stream_url = None | |
| for stream in video_streams: | |
| if stream.get("container") == "mp4" and not stream.get("videoOnly"): | |
| stream_url = stream.get("url") | |
| break | |
| if not stream_url and video_streams: | |
| stream_url = video_streams[0].get("url") | |
| if stream_url: | |
| video_bytes = requests.get(stream_url, stream=True, timeout=30) | |
| with open(output_filename, "wb") as f: | |
| for chunk in video_bytes.iter_content(chunk_size=1024*1024): | |
| if chunk: | |
| f.write(chunk) | |
| print("✅ Berhasil mengunduh via Piped API!") | |
| return output_filename | |
| except Exception as e: | |
| print(f"Gagal via Piped {instance}: {e}") | |
| print("Mencoba fallback via yt-dlp...") | |
| ydl_opts = { | |
| 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', | |
| 'outtmpl': output_filename, | |
| 'overwrites': True, | |
| 'quiet': True, | |
| 'extractor_args': { | |
| 'youtube': { | |
| 'player_client': ['mweb', 'android', 'ios'] | |
| } | |
| } | |
| } | |
| if os.path.exists("cookies.txt"): | |
| ydl_opts['cookiefile'] = 'cookies.txt' | |
| with yt_dlp.YoutubeDL(ydl_opts) as ydl: | |
| ydl.download([youtube_url]) | |
| return output_filename | |
| def get_youtube_metadata(youtube_url): | |
| """Mengambil judul dan deskripsi video.""" | |
| video_id = extract_youtube_id(youtube_url) | |
| if video_id: | |
| try: | |
| resp = requests.get(f"https://pipedapi.kavin.rocks/streams/{video_id}", timeout=5) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| return data.get("title", "Video YouTube"), data.get("description", "") | |
| except Exception: | |
| pass | |
| return "Konten Video Viral", "Deskripsi Konten Video" | |
| # ========================================== | |
| # 3. ANALISIS GEMINI AI (PERBAIKAN PARSING JSON) | |
| # ========================================== | |
| def analyze_with_gemini(video_title, video_description): | |
| if not gemini_client: | |
| return "🔥 SAKSIKAN MOMEN INI!", f"{video_title[:50]} #Shorts", "#shorts #viral #trending" | |
| # Bersihkan input dari karakter kontrol | |
| clean_title = video_title.replace('"', "'").replace('\n', ' ') | |
| clean_desc = video_description[:500].replace('"', "'").replace('\n', ' ') | |
| prompt = f""" | |
| Anda adalah pakar pembuat konten YouTube Shorts viral. | |
| Analisis data video berikut: | |
| - Judul: {clean_title} | |
| - Deskripsi: {clean_desc} | |
| Buatkan: | |
| 1. HOOK: 1 kalimat teks pendek Bahasa Indonesia untuk ditempel di atas video (maksimal 6 kata, HURUF KAPITAL SEMUA, TANPA TANDA KUTIP). | |
| 2. TITLE: Judul Shorts viral (maksimal 60 karakter, TANPA TANDA KUTIP). | |
| 3. TAGS: 5 hashtag populer dipisahkan spasi. | |
| Respon HANYA dalam format JSON valid berikut: | |
| {{\n "hook": "TEKS HOOK KAPITAL",\n "title": "Judul Shorts Viral",\n "tags": "#shorts #tag2 #tag3 #tag4 #tag5"\n}} | |
| """ | |
| try: | |
| response = gemini_client.models.generate_content( | |
| model='gemini-3.5-flash-lite', | |
| contents=prompt, | |
| config=types.GenerateContentConfig(response_mime_type="application/json") | |
| ) | |
| # Penanganan Parsing JSON Aman | |
| raw_text = response.text.strip() | |
| # Ambil hanya blok JSON jika ada karakter liar di luar {} | |
| json_match = re.search(r'\{.*\}', raw_text, re.DOTALL) | |
| if json_match: | |
| raw_text = json_match.group(0) | |
| data = json.loads(raw_text) | |
| return ( | |
| data.get("hook", "JANGAN LEWATKAN INI!"), | |
| data.get("title", f"{clean_title[:50]} #Shorts"), | |
| data.get("tags", "#shorts #viral #trending") | |
| ) | |
| except Exception as e: | |
| print(f"Error Gemini API: {e}") | |
| return "🔥 SAKSIKAN MOMEN INI!", f"{clean_title[:50]} #Shorts", "#shorts #viral #trending" | |
| # ========================================== | |
| # 4. EDITING & CLIPPING (MOVIEPY) | |
| # ========================================== | |
| def process_and_clip_video(youtube_url, start_time, end_time, hook_text): | |
| output_input_file = "downloaded_temp.mp4" | |
| output_clipped_file = "output_short_916.mp4" | |
| download_youtube_video(youtube_url, output_input_file) | |
| clip = VideoFileClip(output_input_file).subclip(start_time, end_time) | |
| # Convert ke 9:16 (Shorts) | |
| w, h = clip.size | |
| target_aspect = 9 / 16 | |
| current_aspect = w / h | |
| if current_aspect > target_aspect: | |
| new_w = int(h * target_aspect) | |
| clip_cropped = clip.crop(x1=int((w - new_w) / 2), width=new_w, height=h) | |
| else: | |
| new_h = int(w / target_aspect) | |
| clip_cropped = clip.crop(y1=int((h - new_h) / 2), width=w, height=new_h) | |
| # Teks Overlay Hook | |
| txt_clip = TextClip( | |
| hook_text, | |
| fontsize=40, | |
| color='yellow', | |
| font='Arial-Bold', | |
| method='caption', | |
| align='center', | |
| size=(int(clip_cropped.w * 0.85), None) | |
| ) | |
| txt_clip = txt_clip.set_pos(('center', int(clip_cropped.h * 0.12))).set_duration(clip_cropped.duration) | |
| final_video = CompositeVideoClip([clip_cropped, txt_clip]) | |
| final_video.write_videofile( | |
| output_clipped_file, | |
| codec='libx264', | |
| audio_codec='aac', | |
| fps=30, | |
| preset='ultrafast' | |
| ) | |
| clip.close() | |
| final_video.close() | |
| if os.path.exists(output_input_file): | |
| os.remove(output_input_file) | |
| return output_clipped_file | |
| # ========================================== | |
| # 5. UPLOAD KE YOUTUBE | |
| # ========================================== | |
| def upload_to_youtube(video_path, title, description, tags_str, privacy_status="private"): | |
| if not all([YT_CLIENT_ID, YT_CLIENT_SECRET, YT_REFRESH_TOKEN]): | |
| return "❌ Gagal Upload: Secrets YouTube belum dikonfigurasi!" | |
| try: | |
| creds = Credentials( | |
| token=None, | |
| refresh_token=YT_REFRESH_TOKEN, | |
| token_uri="https://oauth2.googleapis.com/token", | |
| client_id=YT_CLIENT_ID, | |
| client_secret=YT_CLIENT_SECRET | |
| ) | |
| youtube = build('youtube', 'v3', credentials=creds) | |
| tags_list = [t.strip().replace("#", "") for t in tags_str.split() if t.strip()] | |
| body = { | |
| 'snippet': { | |
| 'title': title[:100], | |
| 'description': f"{description}\n\n{tags_str}", | |
| 'tags': tags_list, | |
| 'categoryId': '22' | |
| }, | |
| 'status': { | |
| 'privacyStatus': privacy_status, | |
| 'selfDeclaredMadeForKids': False | |
| } | |
| } | |
| media = MediaFileUpload(video_path, chunksize=-1, resumable=True, mimetype='video/mp4') | |
| request = youtube.videos().insert(part=','.join(body.keys()), body=body, media_body=media) | |
| response = None | |
| while response is None: | |
| _, response = request.next_chunk() | |
| video_id = response.get("id") | |
| return f"✅ Berhasil Diunggah! ID Video: {video_id}\nURL: https://youtu.be/{video_id}" | |
| except Exception as e: | |
| return f"❌ Error Upload: {str(e)}" | |
| # ========================================== | |
| # 6. WORKFLOW & UI GRADIO | |
| # ========================================== | |
| def full_workflow(youtube_url, start_sec, end_sec, privacy_choice, do_upload): | |
| if not youtube_url: | |
| return None, "⚠️ Masukkan URL YouTube terlebih dahulu!" | |
| try: | |
| print("1/4 Mengambil Metadata Video...") | |
| orig_title, orig_desc = get_youtube_metadata(youtube_url) | |
| print("2/4 Analisis Gemini AI...") | |
| hook_text, gen_title, gen_tags = analyze_with_gemini(orig_title, orig_desc) | |
| print("3/4 Clipping & Editing Video...") | |
| clipped_path = process_and_clip_video(youtube_url, start_sec, end_sec, hook_text) | |
| upload_status = "Status Upload: Dibatalkan (Checkbox tidak diaktifkan)." | |
| if do_upload: | |
| print("4/4 Mengunggah ke YouTube...") | |
| upload_status = upload_to_youtube( | |
| clipped_path, gen_title, f"Klip dari {youtube_url}", gen_tags, privacy_choice | |
| ) | |
| summary = f""" | |
| ### 🤖 Hasil Analisis Gemini AI | |
| - **Judul Asli:** {orig_title} | |
| - **Hook Teks (Overlay):** `{hook_text}` | |
| - **Judul Shorts AI:** `{gen_title}` | |
| - **Hashtag:** `{gen_tags}` | |
| --- | |
| ### 📤 Status Publikasi | |
| {upload_status} | |
| """ | |
| return clipped_path, summary | |
| except Exception as e: | |
| return None, f"❌ Terjadi kesalahan sistem: {str(e)}" | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# 🎬 Auto YouTube Clipper with Gemini AI") | |
| gr.Markdown("Cukup tempelkan **URL YouTube**, Gemini AI akan menganalisis isi video, membuat teks hook, judul viral, dan hashtag secara otomatis.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| url_input = gr.Textbox(label="URL Video YouTube", placeholder="https://www.youtube.com/watch?v=...") | |
| with gr.Row(): | |
| start_input = gr.Number(label="Detik Mulai", value=10) | |
| end_input = gr.Number(label="Detik Selesai", value=40) | |
| privacy_dropdown = gr.Dropdown(choices=["private", "unlisted", "public"], value="private", label="Privasi Upload") | |
| upload_checkbox = gr.Checkbox(label="Unggah Otomatis ke YouTube Saya", value=False) | |
| btn_process = gr.Button("⚡ Proses & Buat Shorts Otomatis", variant="primary") | |
| with gr.Column(): | |
| video_output = gr.Video(label="Preview Shorts 9:16") | |
| info_output = gr.Markdown(label="Laporan Analisis AI") | |
| btn_process.click( | |
| fn=full_workflow, | |
| inputs=[url_input, start_input, end_input, privacy_dropdown, upload_checkbox], | |
| outputs=[video_output, info_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |