import gradio as gr import subprocess import os import uuid import json # Auto-install Node modules if not os.path.exists("node_modules"): subprocess.run(["npm", "install"], check=True) def get_video_dimensions(url): try: cmd = [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "json", url ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) data = json.loads(result.stdout) return data['streams'][0]['width'], data['streams'][0]['height'] except: return 0, 0 def process_video(query, start_time, end_time, mute_audio, progress=gr.Progress()): if not query: return None, "⚠️ Enter a keyword." progress(0, desc="🔍 Searching Pinterest...") search_query = query if "video" in query.lower() else f"{query} video" try: # 1. Scrape URLs result = subprocess.run( ["node", "scraper.js", search_query], capture_output=True, text=True, timeout=240 ) all_urls = [line.strip() for line in result.stdout.split('\n') if line.strip().startswith("http")] if not all_urls: return None, "❌ No videos found." selected_url = None needs_crop = False # --- PHASE 1: SEARCH FOR PORTRAIT FIRST --- progress(0.2, desc="🔍 Phase 1: Looking for Portrait video...") for url in all_urls: w, h = get_video_dimensions(url) if h > w: # Found Portrait selected_url = url needs_crop = False break # --- PHASE 2: SEARCH FOR LANDSCAPE FALLBACK --- if not selected_url: progress(0.5, desc="🔍 Phase 2: No Portrait found. Looking for Landscape to crop...") for url in all_urls: w, h = get_video_dimensions(url) if w > h: # Found Landscape selected_url = url needs_crop = True break if not selected_url: return None, "❌ No videos found in any orientation." # 3. Process Video progress(0.8, desc="⚙️ Final Processing...") output_filename = f"output_{uuid.uuid4().hex[:5]}.mp4" # Define Filter Logic if needs_crop: # Crop middle to 9:16 (ih*9/16 is the width based on height) # We then scale it to a standard height like 1280 to keep it clean vf_filter = "crop=ih*9/16:ih,scale=-1:1280,unsharp=3:3:1.2:3:3:0.0,format=yuv420p" status_msg = "✅ Found Landscape: Cropped to 9:16" else: # Already Portrait: Just sharpen and standardize vf_filter = "scale=-1:1280,unsharp=3:3:1.2:3:3:0.0,format=yuv420p" status_msg = "✅ Found Portrait: Keeping original ratio" ffmpeg_cmd = [ "ffmpeg", "-ss", str(start_time), "-to", str(end_time) if end_time > 0 else "999", "-i", selected_url, "-vf", vf_filter, "-c:v", "libx264", "-crf", "18", "-preset", "veryfast", "-y", output_filename ] if mute_audio: ffmpeg_cmd.insert(-1, "-an") else: ffmpeg_cmd.insert(-1, "-c:a") ffmpeg_cmd.insert(-1, "aac") subprocess.run(ffmpeg_cmd, check=True) return output_filename, status_msg except Exception as e: return None, f"❌ Error: {str(e)}" # --- UI --- with gr.Blocks(theme=gr.themes.Soft()) as demo: gr.Markdown("# 📌 Pinterest Smart 9:16 Downloader") gr.Markdown("Prioritizes Vertical videos. If only Horizontal are found, it crops the center to 9:16.") with gr.Row(): with gr.Column(): q = gr.Textbox(label="Topic", placeholder="e.g. Minecraft Gameplay") with gr.Row(): s = gr.Number(label="Start (sec)", value=0) e = gr.Number(label="End (sec)", value=5) mute = gr.Checkbox(label="Mute Audio", value=False) btn = gr.Button("🚀 Smart Search & Process", variant="primary") with gr.Column(): out_v = gr.Video(label="9:16 Result") out_s = gr.Textbox(label="Status") btn.click(process_video, [q, s, e, mute], [out_v, out_s]) if __name__ == "__main__": demo.launch()