feat: add apply and verify sync workflow

This commit is contained in:
Honney OC committed 2026-09-13 22:32:52 +00:00
1 parent 917771547a
commit 05d6c6539b
5 files changed
+947 -11

No files matched your search

+57 -10
View File
@@ -113,12 +113,16 @@ def find_wavs_from_config(cfg: dict) -> tuple[str, str, int, float, float]:
class SyncWindow(QtWidgets.QMainWindow):
def __init__(self, a_path: str, b_path: str, sr: int = 48000, method: str = "phat", device: str = "auto"):
def __init__(self, a_path: str, b_path: str, sr: int = 48000, method: str = "phat", device: str = "auto",
video_a: str = "", video_b: str = "", out_dir: str = "output"):
super().__init__()
self.setWindowTitle(f"movie_sync — audio sync test — {device_info()}")
self.resize(1400, 900)
self.a_path = a_path
self.b_path = b_path
self.video_a = video_a
self.video_b = video_b
self.out_dir = out_dir
self.sr = sr
self.method = method
self.device_pref = device
@@ -237,6 +241,12 @@ class SyncWindow(QtWidgets.QMainWindow):
btn_export = QtWidgets.QPushButton("Export data")
ctr.addWidget(btn_export)
btn_verify = QtWidgets.QPushButton("Verify (30s)")
ctr.addWidget(btn_verify)
btn_apply = QtWidgets.QPushButton("Apply sync")
ctr.addWidget(btn_apply)
# volume + playback
ctr2 = QtWidgets.QHBoxLayout()
lay.addLayout(ctr2)
@@ -285,6 +295,8 @@ class SyncWindow(QtWidgets.QMainWindow):
btn_auto.clicked.connect(self.on_auto)
btn_swap.clicked.connect(self.on_swap)
btn_export.clicked.connect(self.on_export)
btn_verify.clicked.connect(self.on_verify)
btn_apply.clicked.connect(self.on_apply_sync)
self.btn_play_a.clicked.connect(lambda: self.play("a"))
self.btn_play_b.clicked.connect(lambda: self.play("b"))
self.btn_play_mix.clicked.connect(lambda: self.play("mix"))
@@ -315,18 +327,17 @@ class SyncWindow(QtWidgets.QMainWindow):
self._updating = False
def update_labels(self):
# ffmpeg adelay for mux: add silence for delay (no sound for build-in delay)
# If lag>0: B delayed vs A => to mux B onto A video, delay B audio by lag (pad silence)
# adelay expects ms per channel, e.g. adelay=1234|1234
lag_ms = self.lag_sec * 1000
stem = os.path.splitext(os.path.basename(self.video_a))[0]
out_file = os.path.join(self.out_dir, f"{stem}_synced.mkv")
script = os.path.basename(__file__) if not self.video_a else "apply_sync.py"
cmd = (f'python {script} --config config.toml --lag {self.lag_sec:.6f}')
if lag_ms >= 0:
ffcmd = f'ffmpeg -i video.mkv -i audio.wav -filter_complex "[1:a]adelay={lag_ms:.0f}:all=1[ad]" -map 0:v -map "[ad]" -c:v copy -c:a pcm_s16le out.mkv'
note = f"B delayed by {self.lag_sec:.6f}s ({self.lag_samples} samp) → pad B start with silence {lag_ms:.0f} ms"
else:
# B earlier => pad A (or trim B). For adding B to A file, we need to delay video or pad A? Simplest pad A? But we control only audio: delay B negative means B earlier, so we delay A or use -itsoffset
note = f"B earlier by {abs(self.lag_sec):.6f}s → pad A / use -itsoffset {abs(lag_ms):.0f}ms or trim B by {abs(self.lag_samples)} samp"
ffcmd = f'# B early: ffmpeg -itsoffset {abs(lag_ms)/1000:.6f} -i audio.wav -i video.mkv ... OR pad video audio with silence'
self.ffmpeg_label.setText(f"{note}\n{ffcmd}")
note = f"B earlier by {abs(self.lag_sec):.6f}s → pad A / use -itsoffset {abs(lag_ms):.0f}ms"
note += f"\nOutput: {out_file}"
self.ffmpeg_label.setText(f"{note}\n{cmd}")
# slots
def on_slider(self, v):
@@ -404,6 +415,37 @@ class SyncWindow(QtWidgets.QMainWindow):
print(f"exported to {path}")
self.setWindowTitle(f"movie_sync — exported to {os.path.basename(path)}")
def on_verify(self):
"""Launch verify_sync.py in short (30s) mode with extracted parts."""
import subprocess
script = os.path.join(os.path.dirname(__file__), "verify_sync.py")
cmd = [sys.executable, script, "--config", "config.toml",
"--lag", f"{self.lag_sec:.6f}", "--mode", "short"]
print(f"Launching: {' '.join(cmd)}")
subprocess.Popen(cmd, cwd=os.path.dirname(os.path.abspath(__file__)))
def on_apply_sync(self):
"""Run apply_sync.py on full files, then launch verify_sync.py in full mode."""
import subprocess
script_dir = os.path.dirname(os.path.abspath(__file__))
apply_script = os.path.join(script_dir, "apply_sync.py")
verify_script = os.path.join(script_dir, "verify_sync.py")
# Run apply_sync
cmd = [sys.executable, apply_script, "--config", "config.toml",
"--lag", f"{self.lag_sec:.6f}"]
print(f"Running: {' '.join(cmd)}")
r = subprocess.run(cmd, cwd=script_dir)
if r.returncode != 0:
print(f"apply_sync failed with code {r.returncode}", file=sys.stderr)
return
# Launch verify in full mode
cmd2 = [sys.executable, verify_script, "--config", "config.toml",
"--lag", f"{self.lag_sec:.6f}", "--mode", "full"]
print(f"Launching: {' '.join(cmd2)}")
subprocess.Popen(cmd2, cwd=script_dir)
def play(self, which: str):
if not HAS_SD:
return
@@ -478,6 +520,10 @@ def main():
elif device in ("cuda", "gpu"):
device = "cuda"
video_a = cfg.get("extract", {}).get("file_a", "")
video_b = cfg.get("extract", {}).get("file_b", "")
out_dir = cfg.get("sync", {}).get("out_dir", "output")
if not a_path or not b_path:
print(f"ERROR: no input wavs. Set file_a/file_b in {args.config} or --a/--b", file=sys.stderr)
sys.exit(1)
@@ -507,7 +553,8 @@ def main():
dark.setColor(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, QtGui.QColor(120, 120, 120))
app.setPalette(dark)
pg.setConfigOptions(antialias=True, background="#1e1e1e", foreground="#ddd")
w = SyncWindow(a_path, b_path, sr=sr, method=method, device=device)
w = SyncWindow(a_path, b_path, sr=sr, method=method, device=device,
video_a=video_a, video_b=video_b, out_dir=out_dir)
w.show()
sys.exit(app.exec())