shiveshnavin commited on
Commit
c219a4c
·
unverified ·
1 Parent(s): aae47f7

Create glitch.py

Browse files
Files changed (1) hide show
  1. scripts/glitch.py +319 -0
scripts/glitch.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import shlex
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+ from PIL import Image
8
+
9
+ # pip install glitch-this pillow
10
+ try:
11
+ from glitch_this import ImageGlitcher
12
+ except Exception as e:
13
+ print("Missing dependency: pip install glitch-this pillow", file=sys.stderr)
14
+ raise
15
+
16
+ def log(msg: str):
17
+ print(msg, flush=True)
18
+
19
+ def run_cmd(cmd):
20
+ pretty = shlex.join(cmd) if isinstance(cmd, list) else cmd
21
+ log(f"[CMD] {pretty}")
22
+ proc = subprocess.Popen(
23
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True
24
+ )
25
+ for line in proc.stdout:
26
+ print(line.rstrip())
27
+ ret = proc.wait()
28
+ if ret != 0:
29
+ raise subprocess.CalledProcessError(ret, cmd)
30
+ return ret
31
+
32
+ def ensure_parent(p: Path):
33
+ p.parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ def safe_delete(*paths: Path):
36
+ for p in paths:
37
+ try:
38
+ if p and Path(p).exists():
39
+ Path(p).unlink()
40
+ log(f"[CLEAN] removed {p}")
41
+ except Exception as e:
42
+ log(f"[CLEAN][warn] could not remove {p}: {e}")
43
+
44
+ # ---------------- builders return True if they CREATED the output ----------------
45
+
46
+ def make_glitch_gif(img_path: Path, out_gif: Path, fps: int, n_frames: int,
47
+ mode: str = "constant", amt_start: float = 0.7, amt_end: float = 0.7) -> bool:
48
+ """Make GIF. Return True if created (file didn't exist)."""
49
+ if out_gif.exists():
50
+ log(f"[SKIP] {out_gif} already exists (not overwriting)")
51
+ return False
52
+ ensure_parent(out_gif)
53
+ log(f"[GLITCH] source={img_path} -> {out_gif} | fps={fps} frames={n_frames} mode={mode} amt={amt_start}->{amt_end}")
54
+ img = Image.open(img_path).convert("RGBA")
55
+ glitcher = ImageGlitcher()
56
+
57
+ frames = []
58
+ if n_frames < 2:
59
+ n_frames = 2
60
+ for i in range(n_frames):
61
+ if mode == "ramp" and n_frames > 1:
62
+ amt = amt_start + (amt_end - amt_start) * (i / (n_frames - 1))
63
+ else:
64
+ amt = amt_start
65
+ try:
66
+ frame = glitcher.glitch_image(img, amt, color_offset=True, scan_lines=False, seed=i)
67
+ except TypeError:
68
+ frame = glitcher.glitch_image(img, amt, color_offset=True, scan_lines=False)
69
+ frames.append(frame.convert("P", palette=Image.ADAPTIVE))
70
+ if n_frames <= 120 or i % max(1, n_frames // 60) == 0:
71
+ log(f"[GLITCH] frame {i+1}/{n_frames} amt={amt:.3f}")
72
+
73
+ delay_ms = max(1, round(1000 / fps))
74
+ frames[0].save(
75
+ out_gif,
76
+ save_all=True,
77
+ append_images=frames[1:],
78
+ duration=delay_ms,
79
+ loop=0,
80
+ disposal=2,
81
+ optimize=False,
82
+ transparency=0,
83
+ )
84
+ log(f"[GLITCH] wrote {out_gif}")
85
+ return True
86
+
87
+ def build_concat_raw(gif1: Path, gif2: Path, out_mp4: Path, fps: int, dur_total: float, dur_g2: float) -> bool:
88
+ """Concat with looped GIF1; return True if created."""
89
+ if out_mp4.exists():
90
+ log(f"[SKIP] {out_mp4} already exists (not overwriting)")
91
+ return False
92
+ ensure_parent(out_mp4)
93
+ F = int(fps)
94
+ D1 = max(0.0, float(dur_total) - float(dur_g2))
95
+ D2 = float(dur_g2)
96
+
97
+ filter_complex = (
98
+ f"[0:v]fps={F},setpts=N/({F}*TB),trim=duration={D1}[a];"
99
+ f"[1:v]fps={F},setpts=N/({F}*TB),trim=duration={D2}[b];"
100
+ f"[a][b]concat=n=2:v=1:a=0[v]"
101
+ )
102
+
103
+ cmd = [
104
+ "ffmpeg", "-n",
105
+ "-stream_loop", "-1", "-i", str(gif1),
106
+ "-ignore_loop", "1", "-i", str(gif2),
107
+ "-filter_complex", filter_complex,
108
+ "-map", "[v]",
109
+ "-r", str(F),
110
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
111
+ str(out_mp4)
112
+ ]
113
+ run_cmd(cmd)
114
+ return True
115
+
116
+ def apply_vfx(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float) -> bool:
117
+ """
118
+ Softer VFX: gentler zoom, smaller XY wobble, smaller rotation sway.
119
+ Keeps more of the picture visible.
120
+ """
121
+ if out_mp4.exists():
122
+ log(f"[SKIP] {out_mp4} already exists (not overwriting)")
123
+ return False
124
+
125
+ ensure_parent(out_mp4)
126
+ F = int(fps)
127
+ L = float(dur_total)
128
+
129
+ # toned-down params
130
+ base_zoom = 1.01
131
+ zoom_amp = 0.01
132
+ x_wobble_1 = 10
133
+ x_wobble_2 = 4
134
+ y_wobble_1 = 8
135
+ y_wobble_2 = 3
136
+ rot_main = 0.006
137
+ rot_jitter = 0.002
138
+
139
+ pre_scale_h = 2400
140
+ overscan_w = 1152
141
+ overscan_h = 2048
142
+
143
+ filter_complex = (
144
+ f"[0:v]fps={F},setpts=N/({F}*TB),"
145
+ f"scale=-1:{pre_scale_h},"
146
+ f"zoompan="
147
+ f"z='{base_zoom}+{zoom_amp}*sin(2*PI*(on/{F})/{L})':"
148
+ 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)':"
149
+ 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)':"
150
+ f"d=1:s={overscan_w}x{overscan_h}:fps={F},"
151
+ f"rotate='{rot_main}*sin(2*PI*t/{L}) + {rot_jitter}*sin(2*PI*t/{L}*7)':"
152
+ f"ow=rotw(iw):oh=roth(ih),"
153
+ f"crop=1080:1920[v]"
154
+ )
155
+
156
+ cmd = [
157
+ "ffmpeg", "-n",
158
+ "-i", str(in_mp4),
159
+ "-filter_complex", filter_complex,
160
+ "-map", "[v]",
161
+ "-r", str(F),
162
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
163
+ str(out_mp4)
164
+ ]
165
+ log("[CMD] " + shlex.join(cmd))
166
+ run_cmd(cmd)
167
+ return True
168
+
169
+ def apply_vfxHigh(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float) -> bool:
170
+ """Original stronger VFX; return True if created."""
171
+ if out_mp4.exists():
172
+ log(f"[SKIP] {out_mp4} already exists (not overwriting)")
173
+ return False
174
+ ensure_parent(out_mp4)
175
+ F = int(fps)
176
+ L = float(dur_total)
177
+
178
+ filter_complex = (
179
+ f"[0:v]fps={F},setpts=N/({F}*TB),"
180
+ f"scale=-1:2880,"
181
+ f"zoompan="
182
+ f"z='1.10+0.08*sin(2*PI*(on/{F})/{L})':"
183
+ f"x='(iw-iw/zoom)/2 + 24*sin(2*PI*(on/{F})/{L}*3) + 10*sin(2*PI*(on/{F})/{L}*7)':"
184
+ f"y='(ih-ih/zoom)/2 + 18*sin(2*PI*(on/{F})/{L}*2) + 9*sin(2*PI*(on/{F})/{L}*5)':"
185
+ f"d=1:s=1296x2304:fps={F},"
186
+ f"rotate='0.012*sin(2*PI*t/{L}) + 0.004*sin(2*PI*t/{L}*7)':ow=rotw(iw):oh=roth(ih),"
187
+ f"crop=1080:1920[v]"
188
+ )
189
+
190
+ cmd = [
191
+ "ffmpeg", "-n",
192
+ "-i", str(in_mp4),
193
+ "-filter_complex", filter_complex,
194
+ "-map", "[v]",
195
+ "-r", str(F),
196
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
197
+ str(out_mp4)
198
+ ]
199
+ run_cmd(cmd)
200
+ return True
201
+
202
+ def add_transitions(in_mp4: Path, out_mp4: Path, fps: int, dur_total: float,
203
+ wobble_main: float = 0.028, wobble_jitter: float = 0.012,
204
+ wobble_f1: float = 5.0, wobble_f2: float = 11.0,
205
+ blur_sigma: int = 42) -> bool:
206
+ """
207
+ Wobble/sway IN (0–0.5s) and OUT (last 0.5s). Heavy blur only during transitions.
208
+ """
209
+ if out_mp4.exists():
210
+ log(f"[SKIP] {out_mp4} already exists (not overwriting)")
211
+ return False
212
+ ensure_parent(out_mp4)
213
+ F = int(fps)
214
+ L = float(dur_total)
215
+ end_start = max(0.0, L - 0.5)
216
+
217
+ angle_expr = (
218
+ f"( if(lte(t,0.5),1,0) + if(gte(t,{end_start}),1,0) ) * "
219
+ f"({wobble_main}*sin(2*PI*t*{wobble_f1}) + {wobble_jitter}*sin(2*PI*t*{wobble_f2}))"
220
+ )
221
+ blur_enable = f"between(t,0,0.5)+between(t,{end_start},{end_start}+0.5)"
222
+
223
+ filt = (
224
+ f"[0:v]fps={F},scale=1296:2304,"
225
+ f"rotate='{angle_expr}':ow=rotw(iw):oh=roth(ih),"
226
+ f"gblur=sigma={blur_sigma}:steps=3:enable='{blur_enable}',"
227
+ f"crop=1080:1920[v]"
228
+ )
229
+
230
+ cmd = [
231
+ "ffmpeg", "-n",
232
+ "-i", str(in_mp4),
233
+ "-t", f"{L:.3f}",
234
+ "-filter_complex", filt,
235
+ "-map", "[v]", "-map", "0:a?",
236
+ "-c:v", "libx264", "-r", str(F), "-pix_fmt", "yuv420p",
237
+ "-c:a", "copy",
238
+ str(out_mp4)
239
+ ]
240
+ run_cmd(cmd)
241
+ return True
242
+
243
+ # --------------------------------- main ---------------------------------
244
+
245
+ def main():
246
+ ap = argparse.ArgumentParser(description="Glitch → loop+concat → VFX → wobble-blur transitions (no overwrite, verbose)")
247
+ ap.add_argument("image", type=Path, help="Input image path")
248
+ ap.add_argument("duration", type=float, help="Total output duration in seconds (e.g., 8.0)")
249
+ ap.add_argument("--fps", type=int, default=60, help="Frames per second (default: 60)")
250
+ ap.add_argument("--base", type=Path, default=None, help="Output basename (default: image stem)")
251
+ ap.add_argument("--out", type=Path, default=None, help="Final Output filename")
252
+ ap.add_argument("--glitch2_secs", type=float, default=2.0, help="Duration of heavy glitch segment (default: 2.0s)")
253
+ # Transition tuning
254
+ ap.add_argument("--wobble_main", type=float, default=0.008, help="Main wobble radians amplitude during transitions")
255
+ ap.add_argument("--wobble_jitter", type=float, default=0.002, help="Jitter wobble radians amplitude during transitions")
256
+ ap.add_argument("--wobble_f1", type=float, default=1.0, help="Wobble frequency 1 (Hz)")
257
+ ap.add_argument("--wobble_f2", type=float, default=1.0, help="Wobble frequency 2 (Hz)")
258
+ ap.add_argument("--sigma", type=int, default=6, help="Gaussian blur sigma during transitions")
259
+ args = ap.parse_args()
260
+
261
+ img_path = args.image
262
+ duration = float(args.duration)
263
+ fps = int(args.fps)
264
+ base = args.base or img_path.with_suffix("")
265
+ base = Path(str(base))
266
+
267
+ glitch2_secs = float(args.glitch2_secs)
268
+
269
+ # Durations
270
+ seg1_secs = max(0.0, duration - glitch2_secs)
271
+ seg2_secs = glitch2_secs
272
+
273
+ # Frames to generate initially (GIF1 loops later)
274
+ gif1_frames = max(2, int(round(min(seg1_secs if seg1_secs > 0 else 2.0, 2.0) * fps)))
275
+ gif2_frames = max(2, int(round(seg2_secs * fps)))
276
+
277
+ gif1 = Path(f"{base}_glitch1.gif")
278
+ gif2 = Path(f"{base}_glitch2.gif")
279
+ concat_raw = Path(f"{base}_raw.mp4")
280
+ vfx_mp4 = Path(f"{base}_vfx.mp4")
281
+ final_mp4 = args.out or Path(f"{base}_final.mp4")
282
+
283
+ log(f"[SETUP] image={img_path} duration={duration}s fps={fps} glitch2_secs={glitch2_secs}s")
284
+ log(f"[PLAN] seg1(loop)={seg1_secs:.3f}s seg2(heavy)={seg2_secs:.3f}s")
285
+ log(f"[FRAMES] gif1={gif1_frames} gif2={gif2_frames}")
286
+ log(f"[OUTPUTS] {gif1}, {gif2}, {concat_raw}, {vfx_mp4}, {final_mp4}")
287
+
288
+ # 1) GIFs
289
+ created_g1 = make_glitch_gif(img_path, gif1, fps=fps, n_frames=gif1_frames, mode="constant", amt_start=0.7, amt_end=0.7)
290
+ created_g2 = make_glitch_gif(img_path, gif2, fps=fps, n_frames=gif2_frames, mode="ramp", amt_start=3.0, amt_end=5.0)
291
+ # If either GIF was (re)generated, downstream is stale
292
+ if created_g1 or created_g2:
293
+ safe_delete(concat_raw, vfx_mp4, final_mp4)
294
+
295
+ # 2) Concat
296
+ created_concat = build_concat_raw(gif1, gif2, concat_raw, fps=fps, dur_total=duration, dur_g2=seg2_secs)
297
+ if created_concat:
298
+ safe_delete(vfx_mp4, final_mp4)
299
+
300
+ # 3) VFX
301
+ created_vfx = apply_vfx(concat_raw, vfx_mp4, fps=fps, dur_total=duration)
302
+ if created_vfx:
303
+ safe_delete(final_mp4)
304
+
305
+ # 4) Transitions
306
+ add_transitions(vfx_mp4, final_mp4, fps=fps, dur_total=duration,
307
+ wobble_main=args.wobble_main, wobble_jitter=args.wobble_jitter,
308
+ wobble_f1=args.wobble_f1, wobble_f2=args.wobble_f2,
309
+ blur_sigma=args.sigma)
310
+
311
+ log("[DONE]")
312
+ log(f" - GIF 1: {gif1}")
313
+ log(f" - GIF 2: {gif2}")
314
+ log(f" - MP4 raw (looped+concat): {concat_raw}")
315
+ log(f" - MP4 with VFX: {vfx_mp4}")
316
+ log(f" - MP4 final with transitions: {final_mp4}")
317
+
318
+ if __name__ == "__main__":
319
+ main()