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 @spaces.GPU # ========================================== # 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()