shiveshnavin's picture
UPDATE
22144cf
#!/usr/bin/env python3
import argparse
import shlex
import subprocess
import sys
from pathlib import Path
from PIL import Image
import shutil
# pip install glitch-this pillow
try:
from glitch_this import ImageGlitcher
except Exception as e:
print("Missing dependency: pip install glitch-this pillow", file=sys.stderr)
raise
def log(msg: str):
print(msg, flush=True)
def run_cmd(cmd):
pretty = shlex.join(cmd) if isinstance(cmd, list) else cmd
log(f"[CMD] {pretty}")
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True
)
for line in proc.stdout:
print(line.rstrip())
ret = proc.wait()
if ret != 0:
raise subprocess.CalledProcessError(ret, cmd)
return ret
def ensure_parent(p: Path):
p.parent.mkdir(parents=True, exist_ok=True)
def safe_delete(*paths: Path):
for p in paths:
try:
if p and Path(p).exists():
Path(p).unlink()
log(f"[CLEAN] removed {p}")
except Exception as e:
log(f"[CLEAN][warn] could not remove {p}: {e}")
# ---------------- builders return True if they CREATED the output ----------------
def make_glitch_gif(img_path: Path, out_gif: Path, fps: int, n_frames: int,
mode: str = "constant", amt_start: float = 0.7, amt_end: float = 0.7) -> bool:
"""Make GIF. Return True if created (file didn't exist)."""
if out_gif.exists():
log(f"[SKIP] {out_gif} already exists (not overwriting)")
return False
ensure_parent(out_gif)
log(f"[GLITCH] source={img_path} -> {out_gif} | fps={fps} frames={n_frames} mode={mode} amt={amt_start}->{amt_end}")
img = Image.open(img_path).convert("RGBA")
glitcher = ImageGlitcher()
frames = []
if n_frames < 2:
n_frames = 2
for i in range(n_frames):
if mode == "ramp" and n_frames > 1:
amt = amt_start + (amt_end - amt_start) * (i / (n_frames - 1))
else:
amt = amt_start
try:
frame = glitcher.glitch_image(img, amt, color_offset=True, scan_lines=False, seed=i)
except TypeError:
frame = glitcher.glitch_image(img, amt, color_offset=True, scan_lines=False)
frames.append(frame.convert("P", palette=Image.ADAPTIVE))
if n_frames <= 120 or i % max(1, n_frames // 60) == 0:
log(f"[GLITCH] frame {i+1}/{n_frames} amt={amt:.3f}")
delay_ms = max(1, round(1000 / fps))
frames[0].save(
out_gif,
save_all=True,
append_images=frames[1:],
duration=delay_ms,
loop=0,
disposal=2,
optimize=False,
transparency=0,
)
log(f"[GLITCH] wrote {out_gif}")
return True
def build_concat_raw(gif1: Path, gif2: Path, out_mp4: Path, fps: int, dur_total: float, dur_g2: float) -> bool:
"""Concat with looped GIF1; return True if created."""
if out_mp4.exists():
log(f"[SKIP] {out_mp4} already exists (not overwriting)")
return False
ensure_parent(out_mp4)
F = int(fps)
D1 = max(0.0, float(dur_total) - float(dur_g2))
D2 = float(dur_g2)
filter_complex = (
f"[0:v]fps={F},setpts=N/({F}*TB),trim=duration={D1}[a];"
f"[1:v]fps={F},setpts=N/({F}*TB),trim=duration={D2}[b];"
f"[a][b]concat=n=2:v=1:a=0[v]"
)
cmd = [
"ffmpeg", "-n",
"-stream_loop", "-1", "-i", str(gif1),
"-ignore_loop", "1", "-i", str(gif2),
"-filter_complex", filter_complex,
"-map", "[v]",
"-r", str(F),
"-t", str(dur_total),
"-c:v", "libx264", "-pix_fmt", "yuv420p",
str(out_mp4)
]
run_cmd(cmd)
return True
def apply_vfx(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float) -> bool:
"""
Softer VFX: gentler zoom, smaller XY wobble, smaller rotation sway.
Keeps more of the picture visible.
"""
if out_mp4.exists():
log(f"[SKIP] {out_mp4} already exists (not overwriting)")
return False
ensure_parent(out_mp4)
F = int(fps)
L = float(dur_total)
# toned-down params
base_zoom = 1.01
zoom_amp = 0.01
x_wobble_1 = 10
x_wobble_2 = 4
y_wobble_1 = 8
y_wobble_2 = 3
rot_main = 0.006
rot_jitter = 0.002
pre_scale_h = 2400
overscan_w = 1152
overscan_h = 2048
filter_complex = (
f"[0:v]fps={F},setpts=N/({F}*TB),"
f"scale=-1:{pre_scale_h},"
f"zoompan="
f"z='{base_zoom}+{zoom_amp}*sin(2*PI*(on/{F})/{L})':"
f"x='(iw-iw/zoom)/2 + {x_wobble_1}*sin(2*PI*(on/{F})/{L}*3) + {x_wobble_2}*sin(2*PI*(on/{F})/{L}*7)':"
f"y='(ih-ih/zoom)/2 + {y_wobble_1}*sin(2*PI*(on/{F})/{L}*2) + {y_wobble_2}*sin(2*PI*(on/{F})/{L}*5)':"
f"d=1:s={overscan_w}x{overscan_h}:fps={F},"
f"rotate='{rot_main}*sin(2*PI*t/{L}) + {rot_jitter}*sin(2*PI*t/{L}*7)':"
f"ow=rotw(iw):oh=roth(ih),"
f"crop=1080:1920[v]"
)
cmd = [
"ffmpeg", "-n",
"-i", str(in_mp4),
"-filter_complex", filter_complex,
"-map", "[v]",
"-r", str(F),
"-c:v", "libx264", "-pix_fmt", "yuv420p",
str(out_mp4)
]
log("[CMD] " + shlex.join(cmd))
run_cmd(cmd)
return True
def apply_vfxHigh(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float) -> bool:
"""Original stronger VFX; return True if created."""
if out_mp4.exists():
log(f"[SKIP] {out_mp4} already exists (not overwriting)")
return False
ensure_parent(out_mp4)
F = int(fps)
L = float(dur_total)
filter_complex = (
f"[0:v]fps={F},setpts=N/({F}*TB),"
f"scale=-1:2880,"
f"zoompan="
f"z='1.10+0.08*sin(2*PI*(on/{F})/{L})':"
f"x='(iw-iw/zoom)/2 + 24*sin(2*PI*(on/{F})/{L}*3) + 10*sin(2*PI*(on/{F})/{L}*7)':"
f"y='(ih-ih/zoom)/2 + 18*sin(2*PI*(on/{F})/{L}*2) + 9*sin(2*PI*(on/{F})/{L}*5)':"
f"d=1:s=1296x2304:fps={F},"
f"rotate='0.012*sin(2*PI*t/{L}) + 0.004*sin(2*PI*t/{L}*7)':ow=rotw(iw):oh=roth(ih),"
f"crop=1080:1920[v]"
)
cmd = [
"ffmpeg", "-n",
"-i", str(in_mp4),
"-filter_complex", filter_complex,
"-map", "[v]",
"-r", str(F),
"-c:v", "libx264", "-pix_fmt", "yuv420p",
str(out_mp4)
]
run_cmd(cmd)
return True
def add_transitions(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float,
wobble_main: float = 0.028, wobble_jitter: float = 0.012,
wobble_f1: float = 5.0, wobble_f2: float = 11.0,
blur_sigma: int = 42) -> bool:
"""
Wobble/sway IN (0–0.5s) and OUT (last 0.5s). Heavy blur only during transitions.
"""
if out_mp4.exists():
log(f"[SKIP] {out_mp4} already exists (not overwriting)")
return False
ensure_parent(out_mp4)
F = int(fps)
L = float(dur_total)
end_start = max(0.0, L - 0.5)
angle_expr = (
f"( if(lte(t,0.5),1,0) + if(gte(t,{end_start}),1,0) ) * "
f"({wobble_main}*sin(2*PI*t*{wobble_f1}) + {wobble_jitter}*sin(2*PI*t*{wobble_f2}))"
)
blur_enable = f"between(t,0,0.5)+between(t,{end_start},{end_start}+0.5)"
filt = (
f"[0:v]fps={F},scale=1296:2304,"
f"rotate='{angle_expr}':ow=rotw(iw):oh=roth(ih),"
f"gblur=sigma={blur_sigma}:steps=3:enable='{blur_enable}',"
f"crop=1080:1920[v]"
)
cmd = [
"ffmpeg", "-n",
"-i", str(in_mp4),
"-t", f"{L:.3f}",
"-filter_complex", filt,
"-map", "[v]", "-map", "0:a?",
"-c:v", "libx264", "-r", str(F), "-pix_fmt", "yuv420p",
"-c:a", "copy",
str(out_mp4)
]
run_cmd(cmd)
return True
# --------------------------------- main ---------------------------------
def main():
ap = argparse.ArgumentParser(description="Glitch → loop+concat → VFX → wobble-blur transitions (no overwrite, verbose)")
ap.add_argument("image", type=Path, help="Input image path")
ap.add_argument("duration", type=float, help="Total output duration in seconds (e.g., 8.0)")
ap.add_argument("--fps", type=int, default=60, help="Frames per second (default: 60)")
ap.add_argument("--base", type=Path, default=None, help="Output basename (default: image stem)")
ap.add_argument("--out", type=Path, default=None, help="Output filename")
ap.add_argument("--glitch2_secs", type=float, default=2.0, help="Duration of heavy glitch segment (default: 2.0s)")
# Transition tuning
ap.add_argument("--wobble_main", type=float, default=0.008, help="Main wobble radians amplitude during transitions")
ap.add_argument("--wobble_jitter", type=float, default=0.002, help="Jitter wobble radians amplitude during transitions")
ap.add_argument("--wobble_f1", type=float, default=1.0, help="Wobble frequency 1 (Hz)")
ap.add_argument("--wobble_f2", type=float, default=1.0, help="Wobble frequency 2 (Hz)")
ap.add_argument("--blur", type=int, default=6, help="Gaussian blur sigma during transitions")
args = ap.parse_args()
img_path = args.image
duration = float(args.duration)
fps = int(args.fps)
base = args.base or img_path.with_suffix("")
base = Path(str(base))
glitch2_secs = float(args.glitch2_secs)
# Durations
seg1_secs = max(0.0, duration - glitch2_secs)
seg2_secs = glitch2_secs
# Frames to generate initially (GIF1 loops later)
gif1_frames = max(2, int(round(min(seg1_secs if seg1_secs > 0 else 2.0, 2.0) * fps)))
gif2_frames = max(2, int(round(seg2_secs * fps)))
gif1 = Path(f"{base}_glitch1.gif")
gif2 = Path(f"{base}_glitch2.gif")
concat_raw = Path(f"{base}_raw.mp4")
vfx_mp4 = Path(f"{base}_vfx.mp4")
final_mp4 = args.base or Path(f"{base}_final.mp4")
log(f"[SETUP] image={img_path} duration={duration}s fps={fps} glitch2_secs={glitch2_secs}s")
log(f"[PLAN] seg1(loop)={seg1_secs:.3f}s seg2(heavy)={seg2_secs:.3f}s")
log(f"[FRAMES] gif1={gif1_frames} gif2={gif2_frames}")
log(f"[OUTPUTS] {gif1}, {gif2}, {concat_raw}, {vfx_mp4}, {final_mp4}")
# 1) GIFs
created_g1 = make_glitch_gif(img_path, gif1, fps=fps, n_frames=gif1_frames, mode="constant", amt_start=0.7, amt_end=0.7)
created_g2 = make_glitch_gif(img_path, gif2, fps=fps, n_frames=gif2_frames, mode="ramp", amt_start=3.0, amt_end=5.0)
# If either GIF was (re)generated, downstream is stale
if created_g1 or created_g2:
safe_delete(concat_raw, vfx_mp4, final_mp4)
# 2) Concat
created_concat = build_concat_raw(gif1, gif2, concat_raw, fps=fps, dur_total=duration, dur_g2=seg2_secs)
if created_concat:
safe_delete(vfx_mp4, final_mp4)
# 3) VFX
created_vfx = apply_vfx(concat_raw, vfx_mp4, fps=fps, dur_total=duration)
if created_vfx:
safe_delete(final_mp4)
# 4) Transitions
add_transitions(vfx_mp4, final_mp4, fps=fps, dur_total=duration,
wobble_main=args.wobble_main, wobble_jitter=args.wobble_jitter,
wobble_f1=args.wobble_f1, wobble_f2=args.wobble_f2,
blur_sigma=args.blur)
shutil.copy(final_mp4, args.out)
log("[DONE]")
log(f" - GIF 1: {gif1}")
log(f" - GIF 2: {gif2}")
log(f" - MP4 raw (looped+concat): {concat_raw}")
log(f" - MP4 with VFX: {vfx_mp4}")
log(f" - MP4 final with transitions: {final_mp4}")
log(f" - Out File: {args.out}")
if __name__ == "__main__":
main()