import gradio as gr import zipfile import tempfile import shutil from pathlib import Path from PIL import Image, ImageOps import subprocess, json TARGET_WIDTH, TARGET_HEIGHT = 1080, 1440 try: from pillow_heif import register_heif_opener register_heif_opener() except ImportError: pass def process_files(files): if not files: return None, "No files uploaded." out_dir = Path(tempfile.mkdtemp()) stats = {"padded": 0, "copied": 0, "failed": 0} for f in files: p = Path(f.name) ext = p.suffix.lower() try: if ext in {'.jpg','.jpeg','.png','.heic','.webp'}: result = pad_image(p, out_dir) elif ext in {'.mp4','.mov','.avi','.mkv'}: result = pad_video(p, out_dir) else: continue stats[result] += 1 except Exception as e: stats["failed"] += 1 # Zip everything zip_path = out_dir / "instagram_ready.zip" with zipfile.ZipFile(zip_path, 'w') as zf: for f in out_dir.iterdir(): if f.name != "instagram_ready.zip": zf.write(f, f.name) summary = f"✅ Padded: {stats['padded']} | 📋 Copied as-is: {stats['copied']} | ❌ Failed: {stats['failed']}" return str(zip_path), summary def pad_image(input_path, out_dir): img = ImageOps.exif_transpose(Image.open(input_path)) w, h = img.size out_path = out_dir / f"padded_{input_path.stem}.jpg" if h >= w: shutil.copy2(input_path, out_dir / input_path.name) return "copied" scale = TARGET_WIDTH / w img = img.resize((TARGET_WIDTH, int(h * scale)), Image.LANCZOS) if img.mode != 'RGB': img = img.convert('RGB') canvas = Image.new('RGB', (TARGET_WIDTH, TARGET_HEIGHT), (0,0,0)) canvas.paste(img, (0, (TARGET_HEIGHT - img.height) // 2)) canvas.save(out_path, quality=95) return "padded" def pad_video(input_path, out_dir): r = subprocess.run(['ffprobe','-v','error','-select_streams','v:0', '-show_entries','stream=width,height','-of','json',str(input_path)], capture_output=True, text=True) d = json.loads(r.stdout)['streams'][0] out_path = out_dir / f"padded_{input_path.name}" if d['height'] >= d['width']: shutil.copy2(input_path, out_dir / input_path.name) return "copied" subprocess.run(['ffmpeg','-i',str(input_path), '-vf',f"scale={TARGET_WIDTH}:-2,pad={TARGET_WIDTH}:{TARGET_HEIGHT}:0:(oh-ih)/2:black", '-c:v','libx264','-preset','fast','-crf','23', '-c:a','copy','-map_metadata','0','-y',str(out_path)], capture_output=True) return "padded" with gr.Blocks(title="Instagram Padder 📸", theme=gr.themes.Soft()) as demo: gr.Markdown("# 📸 Instagram Portrait Padder\nUploads landscape photos & videos → pads to 1080×1440 (3:4) with black bars") files = gr.File(file_count="multiple", label="Drop photos & videos here") btn = gr.Button("Process & Download ZIP", variant="primary") out = gr.File(label="Download your ZIP") status = gr.Textbox(label="Summary", interactive=False) btn.click(process_files, inputs=files, outputs=[out, status]) demo.launch()