Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- app.py +87 -0
- packages.txt +1 -0
- requirements.txt +3 -0
app.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import zipfile
|
| 3 |
+
import tempfile
|
| 4 |
+
import shutil
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from PIL import Image, ImageOps
|
| 7 |
+
import subprocess, json
|
| 8 |
+
|
| 9 |
+
TARGET_WIDTH, TARGET_HEIGHT = 1080, 1440
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from pillow_heif import register_heif_opener
|
| 13 |
+
register_heif_opener()
|
| 14 |
+
except ImportError:
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
def process_files(files):
|
| 18 |
+
if not files:
|
| 19 |
+
return None, "No files uploaded."
|
| 20 |
+
|
| 21 |
+
out_dir = Path(tempfile.mkdtemp())
|
| 22 |
+
stats = {"padded": 0, "copied": 0, "failed": 0}
|
| 23 |
+
|
| 24 |
+
for f in files:
|
| 25 |
+
p = Path(f.name)
|
| 26 |
+
ext = p.suffix.lower()
|
| 27 |
+
try:
|
| 28 |
+
if ext in {'.jpg','.jpeg','.png','.heic','.webp'}:
|
| 29 |
+
result = pad_image(p, out_dir)
|
| 30 |
+
elif ext in {'.mp4','.mov','.avi','.mkv'}:
|
| 31 |
+
result = pad_video(p, out_dir)
|
| 32 |
+
else:
|
| 33 |
+
continue
|
| 34 |
+
stats[result] += 1
|
| 35 |
+
except Exception as e:
|
| 36 |
+
stats["failed"] += 1
|
| 37 |
+
|
| 38 |
+
# Zip everything
|
| 39 |
+
zip_path = out_dir / "instagram_ready.zip"
|
| 40 |
+
with zipfile.ZipFile(zip_path, 'w') as zf:
|
| 41 |
+
for f in out_dir.iterdir():
|
| 42 |
+
if f.name != "instagram_ready.zip":
|
| 43 |
+
zf.write(f, f.name)
|
| 44 |
+
|
| 45 |
+
summary = f"โ
Padded: {stats['padded']} | ๐ Copied as-is: {stats['copied']} | โ Failed: {stats['failed']}"
|
| 46 |
+
return str(zip_path), summary
|
| 47 |
+
|
| 48 |
+
def pad_image(input_path, out_dir):
|
| 49 |
+
img = ImageOps.exif_transpose(Image.open(input_path))
|
| 50 |
+
w, h = img.size
|
| 51 |
+
out_path = out_dir / f"padded_{input_path.stem}.jpg"
|
| 52 |
+
if h >= w:
|
| 53 |
+
shutil.copy2(input_path, out_dir / input_path.name)
|
| 54 |
+
return "copied"
|
| 55 |
+
scale = TARGET_WIDTH / w
|
| 56 |
+
img = img.resize((TARGET_WIDTH, int(h * scale)), Image.LANCZOS)
|
| 57 |
+
if img.mode != 'RGB': img = img.convert('RGB')
|
| 58 |
+
canvas = Image.new('RGB', (TARGET_WIDTH, TARGET_HEIGHT), (0,0,0))
|
| 59 |
+
canvas.paste(img, (0, (TARGET_HEIGHT - img.height) // 2))
|
| 60 |
+
canvas.save(out_path, quality=95)
|
| 61 |
+
return "padded"
|
| 62 |
+
|
| 63 |
+
def pad_video(input_path, out_dir):
|
| 64 |
+
r = subprocess.run(['ffprobe','-v','error','-select_streams','v:0',
|
| 65 |
+
'-show_entries','stream=width,height','-of','json',str(input_path)],
|
| 66 |
+
capture_output=True, text=True)
|
| 67 |
+
d = json.loads(r.stdout)['streams'][0]
|
| 68 |
+
out_path = out_dir / f"padded_{input_path.name}"
|
| 69 |
+
if d['height'] >= d['width']:
|
| 70 |
+
shutil.copy2(input_path, out_dir / input_path.name)
|
| 71 |
+
return "copied"
|
| 72 |
+
subprocess.run(['ffmpeg','-i',str(input_path),
|
| 73 |
+
'-vf',f"scale={TARGET_WIDTH}:-2,pad={TARGET_WIDTH}:{TARGET_HEIGHT}:0:(oh-ih)/2:black",
|
| 74 |
+
'-c:v','libx264','-preset','fast','-crf','23',
|
| 75 |
+
'-c:a','copy','-map_metadata','0','-y',str(out_path)],
|
| 76 |
+
capture_output=True)
|
| 77 |
+
return "padded"
|
| 78 |
+
|
| 79 |
+
with gr.Blocks(title="Instagram Padder ๐ธ", theme=gr.themes.Soft()) as demo:
|
| 80 |
+
gr.Markdown("# ๐ธ Instagram Portrait Padder\nUploads landscape photos & videos โ pads to 1080ร1440 (3:4) with black bars")
|
| 81 |
+
files = gr.File(file_count="multiple", label="Drop photos & videos here")
|
| 82 |
+
btn = gr.Button("Process & Download ZIP", variant="primary")
|
| 83 |
+
out = gr.File(label="Download your ZIP")
|
| 84 |
+
status = gr.Textbox(label="Summary", interactive=False)
|
| 85 |
+
btn.click(process_files, inputs=files, outputs=[out, status])
|
| 86 |
+
|
| 87 |
+
demo.launch()
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
Pillow
|
| 3 |
+
pillow-heif
|