diff --git a/sync_audio_test.py b/sync_audio_test.py index 0e5be1e..59e2f6f 100644 --- a/sync_audio_test.py +++ b/sync_audio_test.py @@ -41,6 +41,79 @@ from sync_audio import load_wav, correlate, device_info, lag_to_seconds from waveform_cache import cached_load_wav +class ApplySyncWorker(QtCore.QThread): + """Background thread for apply_sync.py — runs ffmpeg and parses frame progress.""" + progress = QtCore.pyqtSignal(int, int) # frames_done, frames_total + status = QtCore.pyqtSignal(str) + finished = QtCore.pyqtSignal(bool) # success + + def __init__(self, lag_sec: float, video_a: str, video_b: str, config: str = "config.toml"): + super().__init__() + self.lag_sec = lag_sec + self.video_a = video_a + self.video_b = video_b + self.config = config + self._abort = False + + def abort(self): + self._abort = True + + def run(self): + import subprocess + script_dir = os.path.dirname(os.path.abspath(__file__)) + apply_script = os.path.join(script_dir, "apply_sync.py") + + cmd = [sys.executable, apply_script, "--config", self.config, + "--a", self.video_a, "--b", self.video_b, + "--lag", f"{self.lag_sec:.6f}"] + self.status.emit("starting apply_sync...") + + # Probe total frames from file A for progress + total_frames = 0 + try: + r = subprocess.run( + ["ffprobe", "-v", "error", "-count_frames", + "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames", + "-of", "default=noprint_wrappers=1:nokey=1", self.video_a], + capture_output=True, text=True, timeout=15 + ) + if r.returncode == 0: + total_frames = int(r.stdout.strip()) + except Exception: + pass + + proc = subprocess.Popen( + cmd, cwd=script_dir, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True + ) + + frames_done = 0 + for line in proc.stderr: + if self._abort: + proc.kill() + self.finished.emit(False) + return + line = line.strip() + # ffmpeg outputs "frame= 123 fps=..." on stderr + if line.startswith("frame="): + try: + parts = line.split() + for p in parts: + if p.isdigit(): + frames_done = int(p) + break + except Exception: + pass + self.progress.emit(frames_done, total_frames) + self.status.emit(f"encoding frame {frames_done}/{total_frames or '?'}") + elif "error" in line.lower() or "failed" in line.lower(): + self.status.emit(line) + + rc = proc.wait() + self.finished.emit(rc == 0) + + def load_config(path: str) -> dict: if not os.path.exists(path): @@ -252,8 +325,21 @@ class SyncWindow(QtWidgets.QMainWindow): ctr.addWidget(btn_verify) btn_apply = QtWidgets.QPushButton("Apply sync") + self.btn_apply = btn_apply ctr.addWidget(btn_apply) + self.apply_progress = QtWidgets.QProgressBar() + self.apply_progress.setRange(0, 100) + self.apply_progress.setValue(0) + self.apply_progress.setMaximumWidth(150) + self.apply_progress.setVisible(False) + ctr.addWidget(self.apply_progress) + + self.apply_status = QtWidgets.QLabel("") + self.apply_status.setStyleSheet("color: #aaa; font-size: 11px;") + self.apply_status.setMaximumWidth(300) + ctr.addWidget(self.apply_status) + # volume + playback ctr2 = QtWidgets.QHBoxLayout() lay.addLayout(ctr2) @@ -449,27 +535,47 @@ class SyncWindow(QtWidgets.QMainWindow): 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.py on a background thread with progress tracking.""" + self.btn_apply.setEnabled(False) + self.apply_progress.setVisible(True) + self.apply_progress.setRange(0, 0) # indeterminate until we get frame count + self.apply_progress.setValue(0) + self.apply_status.setText("starting...") - # 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 + self._apply_worker = ApplySyncWorker( + self.lag_sec, self.video_a, self.video_b + ) + self._apply_worker.progress.connect(self._on_apply_progress) + self._apply_worker.status.connect(lambda s: self.apply_status.setText(s)) + self._apply_worker.finished.connect(self._on_apply_done) + self._apply_worker.start() - # Launch verify in full mode - cmd2 = [sys.executable, verify_script, "--config", "config.toml", - "--a", self.video_a, "--b", self.video_b, - "--lag", f"{self.lag_sec:.6f}", "--mode", "full"] - print(f"Launching: {' '.join(cmd2)}") - subprocess.Popen(cmd2, cwd=script_dir) + def _on_apply_progress(self, frames_done: int, frames_total: int): + if frames_total > 0: + self.apply_progress.setRange(0, frames_total) + self.apply_progress.setValue(frames_done) + pct = frames_done * 100 // frames_total + self.apply_status.setText(f"encoding frame {frames_done}/{frames_total} ({pct}%)") + else: + self.apply_progress.setRange(0, 0) # indeterminate + self.apply_status.setText(f"encoding frame {frames_done}...") + + def _on_apply_done(self, success: bool): + self.btn_apply.setEnabled(True) + self.apply_progress.setVisible(False) + if success: + self.apply_status.setText("done") + # Launch verify in full mode + import subprocess + script_dir = os.path.dirname(os.path.abspath(__file__)) + verify_script = os.path.join(script_dir, "verify_sync.py") + cmd = [sys.executable, verify_script, "--config", "config.toml", + "--a", self.video_a, "--b", self.video_b, + "--lag", f"{self.lag_sec:.6f}", "--mode", "full"] + print(f"Launching: {' '.join(cmd)}") + subprocess.Popen(cmd, cwd=script_dir) + else: + self.apply_status.setText("apply_sync failed") def play(self, which: str): if not HAS_SD: