joeygambino commited on
Commit
322e395
·
verified ·
1 Parent(s): 55e3d40

v1.3: VHS glitch reaches the master (worker port + transition sidecar); v2a_grad_scale note corrected

Browse files
Files changed (1) hide show
  1. autofinish_worker.py +178 -0
autofinish_worker.py CHANGED
@@ -56,6 +56,118 @@ def api(base, path, data=None, timeout=30):
56
  return json.loads(r.read())
57
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  def main() -> int:
60
  ap = argparse.ArgumentParser()
61
  ap.add_argument("--shots-dir", required=True)
@@ -65,6 +177,12 @@ def main() -> int:
65
  ap.add_argument("--quality", default="ULTRA")
66
  ap.add_argument("--batch-size", type=int, default=4)
67
  ap.add_argument("--comfy", default="http://127.0.0.1:8188")
 
 
 
 
 
 
68
  a = ap.parse_args()
69
 
70
  stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -103,6 +221,25 @@ def main() -> int:
103
  stale = len(glob.glob(os.path.join(a.shots_dir, "shot_[0-9][0-9][0-9].mp4"))) - len(shots)
104
  say(f"current run = {len(shots)} shots (shot_000..shot_{len(shots)-1:03d}); "
105
  f"{stale} stale leftover(s) excluded")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  if not shots:
107
  say(f"FATAL: no current-run shot masters in {a.shots_dir}"); return 1
108
 
@@ -203,6 +340,36 @@ def main() -> int:
203
  allv = os.path.join(snap, "allv.mp4")
204
  subprocess.run([ff, "-y", "-v", "error", "-f", "concat", "-safe", "0",
205
  "-i", vtxt, "-c", "copy", "-an", allv], check=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  final = os.path.join(a.shots_dir, f"{a.name}_{stamp}_MASTER.mp4")
207
  if alist:
208
  atxt = os.path.join(snap, "a.txt"); open(atxt, "w").write("\n".join(alist))
@@ -218,6 +385,17 @@ def main() -> int:
218
  alla_p = os.path.join(snap, "alla_pad.wav")
219
  subprocess.run([ff, "-y", "-v", "error", "-i", alla, "-af", "apad",
220
  "-t", vdur, "-c:a", "pcm_f32le", alla_p], check=True)
 
 
 
 
 
 
 
 
 
 
 
221
  subprocess.run([ff, "-y", "-v", "error", "-i", allv, "-i", alla_p,
222
  "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", final],
223
  check=True)
 
56
  return json.loads(r.read())
57
 
58
 
59
+ def _probe_dur(fp, path):
60
+ out = subprocess.run([fp, "-v", "error", "-select_streams", "v:0",
61
+ "-show_entries", "stream=duration",
62
+ "-of", "default=noprint_wrappers=1:nokey=1", path],
63
+ capture_output=True, text=True).stdout.strip()
64
+ return float(out)
65
+
66
+
67
+ def _glitch_master_video(ff, fp, allv, snap, boundaries_f, n, amt_base, say):
68
+ """Port of the in-graph VHS glitch (nodes.py vhs_glitch transition) so the
69
+ MASTER carries it too. The graph applies it to the concatenated tensor
70
+ that SaveVideo writes; this worker rebuilds the master from the per-shot
71
+ files, which are PRE-glitch, so without this pass the master is the one
72
+ file that never glitches (found 2026-07-22). Same recipe: snow mix,
73
+ horizontal tearing bands, dropout scanlines, triangular envelope over n
74
+ frames centered on each boundary. Deterministic per boundary."""
75
+ import numpy as np
76
+ pr = subprocess.run([fp, "-v", "error", "-select_streams", "v:0",
77
+ "-show_entries", "stream=width,height,r_frame_rate",
78
+ "-of", "csv=p=0", allv],
79
+ capture_output=True, text=True).stdout.strip().split(",")
80
+ W, H = int(pr[0]), int(pr[1])
81
+ num, den = pr[2].split("/")
82
+ fps = float(num) / float(den or 1)
83
+ out = os.path.join(snap, "allv_glitch.mp4")
84
+ dec = subprocess.Popen([ff, "-v", "error", "-i", allv, "-f", "rawvideo",
85
+ "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
86
+ enc = subprocess.Popen([ff, "-y", "-v", "error", "-f", "rawvideo",
87
+ "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(fps),
88
+ "-i", "-", "-c:v", "libx264", "-crf", "16",
89
+ "-preset", "medium", "-pix_fmt", "yuv420p", "-an", out],
90
+ stdin=subprocess.PIPE)
91
+ plan = {}
92
+ for bi, b in enumerate(boundaries_f):
93
+ start = max(0, b - n // 2)
94
+ end = start + n
95
+ span = max(1, end - start - 1)
96
+ for k, fidx in enumerate(range(start, end)):
97
+ plan[fidx] = (bi, k, span)
98
+ fsz = W * H * 3
99
+ idx = 0
100
+ while True:
101
+ buf = dec.stdout.read(fsz)
102
+ if len(buf) < fsz:
103
+ break
104
+ if idx in plan:
105
+ bi, k, span = plan[idx]
106
+ rng = np.random.RandomState(1009 * (bi + 1) + k)
107
+ env = 1.0 - abs((k - span / 2.0) / (span / 2.0 or 1.0))
108
+ amt = amt_base * (0.35 + 0.65 * max(0.0, env))
109
+ f = np.frombuffer(buf, dtype=np.uint8).reshape(H, W, 3).astype(np.float32) / 255.0
110
+ snow = rng.rand(H, W, 1).astype(np.float32)
111
+ f = f * (1.0 - amt * 0.8) + snow * (amt * 0.8)
112
+ for _ in range(int(1 + amt * 6)):
113
+ y0 = rng.randint(0, max(1, H - 8))
114
+ bh = rng.randint(2, max(3, H // 20))
115
+ dx = rng.randint(-W // 6, W // 6 + 1)
116
+ f[y0:y0 + bh] = np.roll(f[y0:y0 + bh], dx, axis=1)
117
+ for _ in range(int(amt * 4)):
118
+ y = rng.randint(0, H)
119
+ f[y:y + 1] = rng.rand()
120
+ buf = (np.clip(f, 0.0, 1.0) * 255.0).astype(np.uint8).tobytes()
121
+ enc.stdin.write(buf)
122
+ idx += 1
123
+ dec.stdout.close()
124
+ enc.stdin.close()
125
+ dec.wait()
126
+ enc.wait()
127
+ if enc.returncode != 0 or not os.path.isfile(out):
128
+ say("WARNING: glitch video encode failed; master left clean")
129
+ return None
130
+ return out
131
+
132
+
133
+ def _glitch_master_audio(ff, fp, wav_in, snap, boundaries_t, n, fps, amt_base, say):
134
+ """Tape-static bed at each boundary, ported from the graph: window is the
135
+ WIDER of the glitch burst or 1.2s (JoyEcho room tone fades at shot edges;
136
+ the static must span that dead seam), raised-cosine envelope."""
137
+ import numpy as np
138
+ pr = subprocess.run([fp, "-v", "error", "-select_streams", "a:0",
139
+ "-show_entries", "stream=sample_rate,channels",
140
+ "-of", "csv=p=0", wav_in],
141
+ capture_output=True, text=True).stdout.strip().split(",")
142
+ sr, ch = int(pr[0]), int(pr[1])
143
+ raw = subprocess.run([ff, "-v", "error", "-i", wav_in, "-f", "f32le", "-"],
144
+ capture_output=True).stdout
145
+ x = np.frombuffer(raw, dtype=np.float32).reshape(-1, ch).copy()
146
+ for bi, bt in enumerate(boundaries_t):
147
+ rng = np.random.RandomState(2027 * (bi + 1))
148
+ c = int(round(bt * sr))
149
+ n_s = max(int(round(n / fps * sr)), int(round(1.2 * sr)))
150
+ s0 = max(0, c - n_s // 2)
151
+ s1 = min(len(x), s0 + n_s)
152
+ if s1 <= s0:
153
+ continue
154
+ ln = s1 - s0
155
+ t = np.linspace(0.0, 1.0, ln, dtype=np.float32)
156
+ env = (0.5 - 0.5 * np.cos(t * 2.0 * np.pi)).astype(np.float32)[:, None]
157
+ noise = rng.rand(ln, ch).astype(np.float32) * 2.0 - 1.0
158
+ x[s0:s1] = np.clip(x[s0:s1] * (1.0 - 0.35 * amt_base * env)
159
+ + noise * (0.10 * amt_base) * env, -1.0, 1.0)
160
+ out = os.path.join(snap, "alla_glitch.wav")
161
+ p = subprocess.Popen([ff, "-y", "-v", "error", "-f", "f32le", "-ar", str(sr),
162
+ "-ac", str(ch), "-i", "-", "-c:a", "pcm_f32le", out],
163
+ stdin=subprocess.PIPE)
164
+ p.communicate(x.tobytes())
165
+ if p.returncode != 0 or not os.path.isfile(out):
166
+ say("WARNING: glitch audio encode failed; audio left clean")
167
+ return None
168
+ return out
169
+
170
+
171
  def main() -> int:
172
  ap = argparse.ArgumentParser()
173
  ap.add_argument("--shots-dir", required=True)
 
177
  ap.add_argument("--quality", default="ULTRA")
178
  ap.add_argument("--batch-size", type=int, default=4)
179
  ap.add_argument("--comfy", default="http://127.0.0.1:8188")
180
+ # Mirror the Generate node's transition defaults. The AutoFinish node does
181
+ # not see those widgets, so these are worker-side settings; pass
182
+ # --transition cut to keep the master clean.
183
+ ap.add_argument("--transition", default="vhs_glitch")
184
+ ap.add_argument("--glitch-frames", type=int, default=8)
185
+ ap.add_argument("--glitch-intensity", type=float, default=0.7)
186
  a = ap.parse_args()
187
 
188
  stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 
221
  stale = len(glob.glob(os.path.join(a.shots_dir, "shot_[0-9][0-9][0-9].mp4"))) - len(shots)
222
  say(f"current run = {len(shots)} shots (shot_000..shot_{len(shots)-1:03d}); "
223
  f"{stale} stale leftover(s) excluded")
224
+
225
+ # Transition sidecar written by the Generate node: overrides the CLI
226
+ # defaults so the master always matches the graph's actual widgets.
227
+ # Same-run guard: ignore a sidecar older than this run's shot_000.
228
+ sidecar = os.path.join(a.shots_dir, "_transition.json")
229
+ if os.path.isfile(sidecar) and os.path.getmtime(sidecar) >= t_anchor:
230
+ try:
231
+ with open(sidecar, "r", encoding="utf-8") as fh:
232
+ tj = json.load(fh)
233
+ a.transition = str(tj.get("transition", a.transition))
234
+ a.glitch_frames = int(tj.get("frames", a.glitch_frames))
235
+ a.glitch_intensity = float(tj.get("intensity", a.glitch_intensity))
236
+ say(f"transition sidecar: {a.transition}, {a.glitch_frames} frames, "
237
+ f"intensity {a.glitch_intensity}")
238
+ except Exception as e: # noqa: BLE001
239
+ say(f"WARNING: transition sidecar unreadable ({e}); using defaults")
240
+ else:
241
+ say(f"no same-run transition sidecar; defaults: {a.transition}, "
242
+ f"{a.glitch_frames} frames, intensity {a.glitch_intensity}")
243
  if not shots:
244
  say(f"FATAL: no current-run shot masters in {a.shots_dir}"); return 1
245
 
 
340
  allv = os.path.join(snap, "allv.mp4")
341
  subprocess.run([ff, "-y", "-v", "error", "-f", "concat", "-safe", "0",
342
  "-i", vtxt, "-c", "copy", "-an", allv], check=True)
343
+
344
+ # 3b. VHS glitch at shot boundaries. The graph glitches the stream
345
+ # SaveVideo writes; this master is rebuilt from pre-glitch shot files
346
+ # and needs its own pass or it comes out clean (found 2026-07-22).
347
+ boundaries_t = []
348
+ if a.transition == "vhs_glitch" and len(ups) > 1:
349
+ try:
350
+ durs = [_probe_dur(fp, up) for up in ups]
351
+ acc = 0.0
352
+ for d in durs[:-1]:
353
+ acc += d
354
+ boundaries_t.append(acc)
355
+ prf = subprocess.run([fp, "-v", "error", "-select_streams", "v:0",
356
+ "-show_entries", "stream=r_frame_rate",
357
+ "-of", "csv=p=0", allv],
358
+ capture_output=True, text=True).stdout.strip()
359
+ num, den = prf.split("/")
360
+ fps_v = float(num) / float(den or 1)
361
+ boundaries_f = [int(round(t * fps_v)) for t in boundaries_t]
362
+ gv = _glitch_master_video(ff, fp, allv, snap, boundaries_f,
363
+ max(1, a.glitch_frames),
364
+ max(0.1, min(1.0, a.glitch_intensity)), say)
365
+ if gv:
366
+ allv = gv
367
+ say(f"VHS glitch applied to master video at {len(boundaries_f)} "
368
+ f"boundaries ({a.glitch_frames} frames, intensity {a.glitch_intensity})")
369
+ except Exception as e: # noqa: BLE001
370
+ say(f"WARNING: glitch pass skipped ({type(e).__name__}: {e}); master left clean")
371
+ boundaries_t = []
372
+
373
  final = os.path.join(a.shots_dir, f"{a.name}_{stamp}_MASTER.mp4")
374
  if alist:
375
  atxt = os.path.join(snap, "a.txt"); open(atxt, "w").write("\n".join(alist))
 
385
  alla_p = os.path.join(snap, "alla_pad.wav")
386
  subprocess.run([ff, "-y", "-v", "error", "-i", alla, "-af", "apad",
387
  "-t", vdur, "-c:a", "pcm_f32le", alla_p], check=True)
388
+ if boundaries_t:
389
+ try:
390
+ ga = _glitch_master_audio(ff, fp, alla_p, snap, boundaries_t,
391
+ max(1, a.glitch_frames), 25.0,
392
+ max(0.1, min(1.0, a.glitch_intensity)), say)
393
+ if ga:
394
+ alla_p = ga
395
+ say(f"tape-static bed applied to master audio at "
396
+ f"{len(boundaries_t)} boundaries")
397
+ except Exception as e: # noqa: BLE001
398
+ say(f"WARNING: audio glitch skipped ({type(e).__name__}: {e})")
399
  subprocess.run([ff, "-y", "-v", "error", "-i", allv, "-i", alla_p,
400
  "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", final],
401
  check=True)