feat: add apply and verify sync workflow
This commit is contained in:
1 parent
3a3c2936ff
commit
c552b3e4b1
5 files changed
+947
-11
No files matched your search
+57
-10
@@ -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())
|
||||
|
||||
|
||||
Reference in new issue
Block a user