feat: QMediaPlayer video playback and whisper audio rename tool
This commit is contained in:
1 parent
e8be3e0919
commit
deb5f073fa
3 files changed
+523
-193
No files matched your search
+118
-191
@@ -1,18 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Verify sync: video player + dual waveform display for A/B audio comparison.
|
||||
Verify sync: QMediaPlayer video player + dual waveform display.
|
||||
|
||||
Modes:
|
||||
--mode short : extracted 30s parts (video_a + wav_a + wav_b)
|
||||
--mode full : synced output MKV (extracts audio streams from it)
|
||||
|
||||
Features:
|
||||
- Video playback with timeline seek bar
|
||||
- Audio playback: A only, B only, or A+B mix
|
||||
- Smooth video playback via QMediaPlayer/QVideoWidget
|
||||
- Audio playback: A only, B only, or A+B mix via sounddevice
|
||||
- Combined waveform overlay (A and B in different colors)
|
||||
- Two separate waveform graphs with playhead
|
||||
- Zoomable waveforms (Ctrl+scroll) with linked x-axes
|
||||
- Toggle A/B on/off, volume, play/pause
|
||||
- Zoomable waveforms (scroll) with linked x-axes
|
||||
- Timeline seek bar synced to video position
|
||||
- Toggle A/B on/off, volume
|
||||
|
||||
Usage:
|
||||
make verify LAG=0.042
|
||||
@@ -31,7 +32,7 @@ except ModuleNotFoundError:
|
||||
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from PyQt6 import QtWidgets, QtCore, QtGui
|
||||
from PyQt6 import QtWidgets, QtCore, QtGui, QtMultimedia, QtMultimediaWidgets
|
||||
HAS_PG = True
|
||||
except ImportError as e:
|
||||
print("ERROR: pyqtgraph/PyQt6 not installed — run make install", file=sys.stderr)
|
||||
@@ -61,37 +62,7 @@ def load_config(path: str) -> dict:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def probe_video_info(path: str) -> dict:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=width,height,r_frame_rate,duration",
|
||||
"-show_entries", "format=duration",
|
||||
"-of", "json", path],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
data = json.loads(r.stdout)
|
||||
stream = data.get("streams", [{}])[0]
|
||||
fmt = data.get("format", {})
|
||||
fps_str = stream.get("r_frame_rate", "24000/1001")
|
||||
if "/" in fps_str:
|
||||
num, den = fps_str.split("/")
|
||||
fps = float(num) / float(den)
|
||||
else:
|
||||
fps = float(fps_str)
|
||||
duration = float(fmt.get("duration", stream.get("duration", 0)))
|
||||
return {
|
||||
"width": int(stream.get("width", 1920)),
|
||||
"height": int(stream.get("height", 1080)),
|
||||
"fps": fps,
|
||||
"duration": duration,
|
||||
}
|
||||
except Exception:
|
||||
return {"width": 1920, "height": 1080, "fps": 24000/1001, "duration": 0}
|
||||
|
||||
|
||||
def probe_audio_streams(path: str) -> list[dict]:
|
||||
"""Probe audio streams for full mode."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "a",
|
||||
@@ -113,7 +84,7 @@ def probe_audio_streams(path: str) -> list[dict]:
|
||||
"codec": s.get("codec_name", ""),
|
||||
"channels": s.get("channels", 0),
|
||||
"sample_rate": int(sr) if sr else 0,
|
||||
"language": tags.get("language", "und"),
|
||||
"language": tags.get("language", ""),
|
||||
"title": tags.get("title", ""),
|
||||
})
|
||||
return streams
|
||||
@@ -122,7 +93,6 @@ def probe_audio_streams(path: str) -> list[dict]:
|
||||
|
||||
|
||||
def extract_audio_from_mkv(mkv_path: str, stream_index: int, out_wav: str, sr: int = 48000) -> bool:
|
||||
"""Extract a single audio stream from MKV to mono WAV."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-y", "-i", mkv_path,
|
||||
@@ -136,32 +106,7 @@ def extract_audio_from_mkv(mkv_path: str, stream_index: int, out_wav: str, sr: i
|
||||
return False
|
||||
|
||||
|
||||
def extract_frame(video_path: str, timestamp: float, width: int, height: int) -> bytes | None:
|
||||
"""Extract a single frame at timestamp as raw RGB bytes."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-ss", f"{timestamp:.4f}", "-i", video_path,
|
||||
"-vframes", "1", "-vf", f"scale={width}:{height}",
|
||||
"-f", "rawvideo", "-pix_fmt", "rgb24",
|
||||
"-v", "error", "-"],
|
||||
capture_output=True, timeout=5
|
||||
)
|
||||
if r.returncode != 0 or not r.stdout:
|
||||
return None
|
||||
return r.stdout
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def raw_to_qimage(raw: bytes, width: int, height: int) -> QtGui.QImage | None:
|
||||
if len(raw) < width * height * 3:
|
||||
return None
|
||||
img = QtGui.QImage(raw, width, height, width * 3, QtGui.QImage.Format.Format_RGB888)
|
||||
return img.copy()
|
||||
|
||||
|
||||
def apply_lag(a: np.ndarray, b: np.ndarray, lag_samples: int) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Align A and B with lag applied (B delayed by lag_samples)."""
|
||||
if lag_samples >= 0:
|
||||
a_out = a[lag_samples:]
|
||||
b_out = np.concatenate([np.zeros(lag_samples, dtype=np.float32), b])
|
||||
@@ -221,36 +166,13 @@ class AudioPlayer:
|
||||
self.playing = False
|
||||
|
||||
|
||||
# ─── Zoomable plot widget ───
|
||||
|
||||
class ZoomPlot(pg.PlotWidget):
|
||||
"""PlotWidget with Ctrl+scroll zoom on x-axis, right-drag to pan."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setMouseEnabled(x=True, y=False)
|
||||
# Enable wheel zoom on x-axis only
|
||||
self.getViewBox().setMouseEnabled(x=True, y=False)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
delta = event.angleDelta().y()
|
||||
factor = 1.1 if delta > 0 else 1 / 1.1
|
||||
vb = self.getViewBox()
|
||||
# zoom x-axis around cursor
|
||||
mouse_pos = vb.mapSceneToView(event.position())
|
||||
x_range = vb.viewRange()[0]
|
||||
new_width = (x_range[1] - x_range[0]) / factor
|
||||
center = mouse_pos.x()
|
||||
vb.setXRange(center - new_width / 2, center + new_width / 2, padding=0)
|
||||
event.accept()
|
||||
|
||||
|
||||
# ─── Main Window ───
|
||||
|
||||
class VerifyWindow(QtWidgets.QMainWindow):
|
||||
def __init__(self, video_path: str, wav_a_path: str, wav_b_path: str,
|
||||
sr: int = 48000, lag_sec: float = 0.0,
|
||||
mode: str = "short", out_dir: str = "output",
|
||||
temp_dir: str | None = None):
|
||||
temp_dir: str | None = None, volume: int = 70):
|
||||
super().__init__()
|
||||
self.video_path = video_path
|
||||
self.wav_a_path = wav_a_path
|
||||
@@ -261,7 +183,10 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.mode = mode
|
||||
self.out_dir = out_dir
|
||||
self.temp_dir = temp_dir
|
||||
self.audio = AudioPlayer()
|
||||
self.audio_player = AudioPlayer()
|
||||
self.default_volume = volume
|
||||
self.duration = 0.0
|
||||
self.playing_a_b = False # our custom audio mix playing
|
||||
|
||||
title = "verify_sync"
|
||||
if mode == "short":
|
||||
@@ -282,26 +207,11 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.a_aligned, self.b_aligned = apply_lag(self.a_orig, self.b_orig, self.lag_samples)
|
||||
self.duration = len(self.a_aligned) / sr
|
||||
|
||||
# Video info
|
||||
vinfo = probe_video_info(video_path)
|
||||
self.v_width = vinfo["width"]
|
||||
self.v_height = vinfo["height"]
|
||||
self.v_fps = vinfo["fps"]
|
||||
self.v_duration = vinfo["duration"] if vinfo["duration"] > 0 else self.duration
|
||||
|
||||
# Playback state
|
||||
self.playing = False
|
||||
self.play_start_time = 0.0
|
||||
self.play_offset = 0.0
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.setInterval(33)
|
||||
self.timer.timeout.connect(self._on_tick)
|
||||
self._seeking = False
|
||||
self._last_frame_t = -1.0
|
||||
|
||||
self._setup_ui()
|
||||
self._draw_waveforms()
|
||||
self._update_frame(0.0)
|
||||
|
||||
# Set up QMediaPlayer for video
|
||||
self._setup_video_player()
|
||||
|
||||
def _setup_ui(self):
|
||||
central = QtWidgets.QWidget()
|
||||
@@ -309,14 +219,13 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
lay = QtWidgets.QVBoxLayout(central)
|
||||
lay.setSpacing(2)
|
||||
|
||||
# ── Video display ──
|
||||
self.video_label = QtWidgets.QLabel()
|
||||
self.video_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.video_label.setMinimumHeight(300)
|
||||
self.video_label.setStyleSheet("background: #000;")
|
||||
lay.addWidget(self.video_label, stretch=4)
|
||||
# ── Video display (QVideoWidget) ──
|
||||
self.video_widget = QtMultimediaWidgets.QVideoWidget()
|
||||
self.video_widget.setMinimumHeight(300)
|
||||
self.video_widget.setStyleSheet("background: #000;")
|
||||
lay.addWidget(self.video_widget, stretch=4)
|
||||
|
||||
# ── Timeline seek bar (video editor style) ──
|
||||
# ── Timeline seek bar ──
|
||||
time_row = QtWidgets.QHBoxLayout()
|
||||
lay.addLayout(time_row)
|
||||
|
||||
@@ -343,6 +252,7 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.seek.sliderPressed.connect(self._on_seek_start)
|
||||
self.seek.sliderMoved.connect(self._on_seek_move)
|
||||
self.seek.sliderReleased.connect(self._on_seek_end)
|
||||
self._seeking = False
|
||||
|
||||
# ── Combined waveform overlay ──
|
||||
self.gw_comb = pg.GraphicsLayoutWidget()
|
||||
@@ -353,6 +263,7 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.curve_a_comb = self.p_comb.plot(pen=pg.mkPen("#4ecdc4", width=1), name="A")
|
||||
self.curve_b_comb = self.p_comb.plot(pen=pg.mkPen("#ff6b6b", width=1, style=QtCore.Qt.PenStyle.DashLine), name="B")
|
||||
self.playhead_comb = self.p_comb.addLine(x=0, pen=pg.mkPen("#ffff00", width=2, style=QtCore.Qt.PenStyle.DashLine))
|
||||
self.p_comb.addLegend(offset=(60, 10))
|
||||
lay.addWidget(self.gw_comb, stretch=1)
|
||||
|
||||
# ── Two separate waveforms ──
|
||||
@@ -371,7 +282,6 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.curve_b_sep = self.p_b.plot(pen=pg.mkPen("#ff6b6b", width=1))
|
||||
self.playhead_b = self.p_b.addLine(x=0, pen=pg.mkPen("#ffff00", width=2, style=QtCore.Qt.PenStyle.DashLine))
|
||||
|
||||
# Link all x-axes for synchronized zoom/pan
|
||||
self.p_a.setXLink(self.p_comb)
|
||||
self.p_b.setXLink(self.p_comb)
|
||||
|
||||
@@ -381,7 +291,6 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
ctrl = QtWidgets.QHBoxLayout()
|
||||
lay.addLayout(ctrl)
|
||||
|
||||
# Play mode
|
||||
ctrl.addWidget(QtWidgets.QLabel("Play:"))
|
||||
self.play_mode = QtWidgets.QComboBox()
|
||||
self.play_mode.addItems(["A+B mix", "A only", "B only"])
|
||||
@@ -389,21 +298,19 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.play_mode.setFixedWidth(90)
|
||||
ctrl.addWidget(self.play_mode)
|
||||
|
||||
# Volume
|
||||
ctrl.addWidget(QtWidgets.QLabel("Vol:"))
|
||||
self.vol_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
|
||||
self.vol_slider.setRange(0, 100)
|
||||
self.vol_slider.setValue(70)
|
||||
self.vol_slider.setValue(self.default_volume)
|
||||
self.vol_slider.setMaximumWidth(100)
|
||||
ctrl.addWidget(self.vol_slider)
|
||||
self.vol_label = QtWidgets.QLabel("70%")
|
||||
self.vol_label = QtWidgets.QLabel(f"{self.default_volume}%")
|
||||
self.vol_label.setMinimumWidth(35)
|
||||
ctrl.addWidget(self.vol_label)
|
||||
self.vol_slider.valueChanged.connect(lambda v: self.vol_label.setText(f"{v}%"))
|
||||
|
||||
ctrl.addSpacing(10)
|
||||
|
||||
# Toggle A/B
|
||||
self.chk_a = QtWidgets.QCheckBox("Show A")
|
||||
self.chk_a.setChecked(True)
|
||||
self.chk_a.toggled.connect(self._update_visibility)
|
||||
@@ -416,7 +323,6 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
|
||||
ctrl.addSpacing(10)
|
||||
|
||||
# Reset zoom button
|
||||
btn_reset_zoom = QtWidgets.QPushButton("Reset zoom")
|
||||
btn_reset_zoom.setFixedWidth(90)
|
||||
btn_reset_zoom.clicked.connect(self._reset_zoom)
|
||||
@@ -424,16 +330,64 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
|
||||
ctrl.addStretch()
|
||||
|
||||
# Info
|
||||
mode_str = "30s preview" if self.mode == "short" else "full length"
|
||||
self.info_label = QtWidgets.QLabel(
|
||||
f"[{mode_str}] Lag: {self.lag_sec:+.6f}s ({self.lag_samples} samp) | "
|
||||
f"A: {len(self.a_orig)/self.sr:.1f}s | B: {len(self.b_orig)/self.sr:.1f}s | "
|
||||
f"Video: {self.v_duration:.1f}s"
|
||||
f"Audio duration: {self.duration:.1f}s"
|
||||
)
|
||||
self.info_label.setStyleSheet("color: #aaa; font-size: 11px;")
|
||||
ctrl.addWidget(self.info_label)
|
||||
|
||||
def _setup_video_player(self):
|
||||
"""Set up QMediaPlayer with QVideoWidget for smooth video playback."""
|
||||
self.media_player = QtMultimedia.QMediaPlayer()
|
||||
self.video_output = QtMultimediaWidgets.QVideoWidget()
|
||||
# Reparent video_output into our video_widget container
|
||||
layout = self.video_widget.layout()
|
||||
if layout is None:
|
||||
layout = QtWidgets.QVBoxLayout(self.video_widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.video_output)
|
||||
|
||||
self.media_player.setVideoOutput(self.video_output)
|
||||
# Mute the video player's own audio — we play audio via sounddevice
|
||||
self.audio_output = QtMultimedia.QAudioOutput()
|
||||
self.audio_output.setMuted(True)
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
|
||||
# Load the video file
|
||||
from PyQt6.QtCore import QUrl
|
||||
self.media_player.setSource(QUrl.fromLocalFile(os.path.abspath(self.video_path)))
|
||||
|
||||
# Timer to sync waveform playheads with video position
|
||||
self.sync_timer = QtCore.QTimer()
|
||||
self.sync_timer.setInterval(33) # ~30fps
|
||||
self.sync_timer.timeout.connect(self._sync_from_video)
|
||||
|
||||
self.media_player.playbackStateChanged.connect(self._on_playback_state)
|
||||
|
||||
# Wait for media loaded to get actual duration
|
||||
self.media_player.durationChanged.connect(self._on_duration_changed)
|
||||
|
||||
def _on_duration_changed(self, dur_ms):
|
||||
"""QMediaPlayer reports actual video duration in ms."""
|
||||
vid_dur = dur_ms / 1000.0
|
||||
if vid_dur > 0:
|
||||
self.info_label.setText(
|
||||
self.info_label.text().replace(
|
||||
f"Audio duration: {self.duration:.1f}s",
|
||||
f"Video: {vid_dur:.1f}s"
|
||||
)
|
||||
)
|
||||
|
||||
def _on_playback_state(self, state):
|
||||
if state == QtMultimedia.QMediaPlayer.PlaybackState.StoppedState:
|
||||
self.sync_timer.stop()
|
||||
self.playing_a_b = False
|
||||
self.btn_play.setText("Play")
|
||||
self.audio_player.stop()
|
||||
|
||||
def _draw_waveforms(self):
|
||||
max_pts = 200_000 if self.mode == "full" else 80_000
|
||||
|
||||
@@ -459,40 +413,12 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.p_a.setYRange(-y_max, y_max)
|
||||
self.p_b.setYRange(-y_max, y_max)
|
||||
|
||||
# Set initial view to full duration
|
||||
self.p_comb.setXRange(0, self.duration, padding=0)
|
||||
|
||||
def _update_frame(self, t: float):
|
||||
t = max(0, min(t, self.v_duration))
|
||||
# Avoid re-extracting same frame
|
||||
if abs(t - self._last_frame_t) < 0.04:
|
||||
return
|
||||
self._last_frame_t = t
|
||||
raw = extract_frame(self.video_path, t, self.v_width, self.v_height)
|
||||
if raw:
|
||||
img = raw_to_qimage(raw, self.v_width, self.v_height)
|
||||
if img:
|
||||
pixmap = QtGui.QPixmap.fromImage(img)
|
||||
scaled = pixmap.scaled(self.video_label.size(),
|
||||
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
|
||||
QtCore.Qt.TransformationMode.SmoothTransformation)
|
||||
self.video_label.setPixmap(scaled)
|
||||
|
||||
def _fmt_time(self, t: float) -> str:
|
||||
h = int(t // 3600)
|
||||
m = int((t % 3600) // 60)
|
||||
s = t % 60
|
||||
return f"{h:02d}:{m:02d}:{s:06.3f}"
|
||||
|
||||
def _current_pos(self) -> float:
|
||||
if self.playing:
|
||||
elapsed = time.time() - self.play_start_time
|
||||
return min(self.play_offset + elapsed, self.duration)
|
||||
return self.play_offset
|
||||
for p in [self.p_comb, self.p_a, self.p_b]:
|
||||
p.setXRange(0, self.duration, padding=0)
|
||||
p.getViewBox().setLimits(xMin=0, xMax=self.duration)
|
||||
|
||||
def _set_pos(self, t: float):
|
||||
t = max(0, min(t, self.duration))
|
||||
self.play_offset = t
|
||||
self.playhead_comb.setPos(t)
|
||||
self.playhead_a.setPos(t)
|
||||
self.playhead_b.setPos(t)
|
||||
@@ -503,22 +429,23 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.seek.blockSignals(False)
|
||||
self.time_label.setText(f"{self._fmt_time(t)} / {self._fmt_time(self.duration)}")
|
||||
|
||||
def _on_tick(self):
|
||||
if not self.playing:
|
||||
return
|
||||
t = self._current_pos()
|
||||
if t >= self.duration:
|
||||
self.stop_play()
|
||||
return
|
||||
def _fmt_time(self, t: float) -> str:
|
||||
h = int(t // 3600)
|
||||
m = int((t % 3600) // 60)
|
||||
s = t % 60
|
||||
return f"{h:02d}:{m:02d}:{s:06.3f}"
|
||||
|
||||
def _sync_from_video(self):
|
||||
"""Timer callback: sync waveform playheads and seek bar to video position."""
|
||||
pos_ms = self.media_player.position()
|
||||
t = pos_ms / 1000.0
|
||||
if t > self.duration:
|
||||
t = self.duration
|
||||
self._set_pos(t)
|
||||
# Update video frame (limit to ~8fps)
|
||||
frame_interval = 1.0 / 8.0
|
||||
now = time.time()
|
||||
if int(t / frame_interval) != int((t - 0.033) / frame_interval):
|
||||
self._update_frame(t)
|
||||
|
||||
def toggle_play(self):
|
||||
if self.playing:
|
||||
state = self.media_player.playbackState()
|
||||
if state == QtMultimedia.QMediaPlayer.PlaybackState.PlayingState:
|
||||
self.stop_play()
|
||||
else:
|
||||
self.start_play()
|
||||
@@ -526,11 +453,16 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
def start_play(self):
|
||||
if self.duration <= 0:
|
||||
return
|
||||
pos = self._current_pos()
|
||||
if pos >= self.duration:
|
||||
pos = 0.0
|
||||
|
||||
start_samp = int(pos * self.sr)
|
||||
# Start video playback
|
||||
self.media_player.play()
|
||||
|
||||
# Start audio mix via sounddevice
|
||||
t = self.media_player.position() / 1000.0
|
||||
start_samp = int(t * self.sr)
|
||||
if start_samp >= len(self.a_aligned):
|
||||
start_samp = 0
|
||||
|
||||
a_seg = self.a_aligned[start_samp:]
|
||||
b_seg = self.b_aligned[start_samp:]
|
||||
vol = self.vol_slider.value() / 100.0
|
||||
@@ -544,23 +476,21 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
else: # B only
|
||||
mix = b_seg * vol
|
||||
|
||||
self.audio.play(mix, self.sr)
|
||||
self.play_offset = pos
|
||||
self.play_start_time = time.time()
|
||||
self.playing = True
|
||||
self.audio_player.play(mix, self.sr)
|
||||
self.playing_a_b = True
|
||||
self.btn_play.setText("Pause")
|
||||
self.timer.start()
|
||||
self.sync_timer.start()
|
||||
|
||||
def stop_play(self):
|
||||
self.audio.stop()
|
||||
self.play_offset = self._current_pos()
|
||||
self.playing = False
|
||||
self.media_player.pause()
|
||||
self.audio_player.stop()
|
||||
self.playing_a_b = False
|
||||
self.btn_play.setText("Play")
|
||||
self.timer.stop()
|
||||
self.sync_timer.stop()
|
||||
|
||||
def _on_seek_start(self):
|
||||
self._seeking = True
|
||||
was_playing = self.playing
|
||||
was_playing = self.media_player.playbackState() == QtMultimedia.QMediaPlayer.PlaybackState.PlayingState
|
||||
if was_playing:
|
||||
self.stop_play()
|
||||
self._was_playing = was_playing
|
||||
@@ -569,14 +499,13 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
frac = value / 1_000_000.0
|
||||
t = frac * self.duration
|
||||
self._set_pos(t)
|
||||
self._update_frame(t)
|
||||
self.media_player.setPosition(int(t * 1000))
|
||||
|
||||
def _on_seek_end(self):
|
||||
frac = self.seek.value() / 1_000_000.0
|
||||
t = frac * self.duration
|
||||
self.play_offset = t
|
||||
self.media_player.setPosition(int(t * 1000))
|
||||
self._set_pos(t)
|
||||
self._update_frame(t)
|
||||
self._seeking = False
|
||||
if getattr(self, '_was_playing', False):
|
||||
self.start_play()
|
||||
@@ -591,9 +520,9 @@ class VerifyWindow(QtWidgets.QMainWindow):
|
||||
self.p_comb.setXRange(0, self.duration, padding=0)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.audio.stop()
|
||||
self.timer.stop()
|
||||
# Clean up temp files if any
|
||||
self.media_player.stop()
|
||||
self.audio_player.stop()
|
||||
self.sync_timer.stop()
|
||||
if self.temp_dir and os.path.isdir(self.temp_dir):
|
||||
try:
|
||||
shutil.rmtree(self.temp_dir)
|
||||
@@ -620,14 +549,13 @@ def main():
|
||||
audio_cfg = cfg.get("audio", {})
|
||||
|
||||
sr = int(audio_cfg.get("sample_rate", 48000))
|
||||
volume = int(audio_cfg.get("volume", 70))
|
||||
out_dir = args.out or sync_cfg.get("out_dir", "output")
|
||||
lag_sec = args.lag
|
||||
|
||||
if args.mode == "full":
|
||||
# Full mode: use synced MKV, extract audio streams from it
|
||||
synced = args.synced
|
||||
if not synced:
|
||||
# Auto-derive from config
|
||||
video_a = ext.get("file_a", "")
|
||||
stem = os.path.splitext(os.path.basename(video_a))[0]
|
||||
synced = os.path.join(out_dir, f"{stem}_synced.mkv")
|
||||
@@ -645,7 +573,6 @@ def main():
|
||||
print("ERROR: synced MKV needs at least 2 audio streams (A + B)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Extract both audio streams to temp WAVs
|
||||
temp_dir = tempfile.mkdtemp(prefix="verify_sync_")
|
||||
wav_a = os.path.join(temp_dir, "audio_a.wav")
|
||||
wav_b = os.path.join(temp_dir, "audio_b.wav")
|
||||
@@ -663,7 +590,6 @@ def main():
|
||||
video_path = synced
|
||||
|
||||
else:
|
||||
# Short mode: use original video + extracted WAVs
|
||||
video_a = args.a or ext.get("file_a", "")
|
||||
video_b = args.b or ext.get("file_b", "")
|
||||
|
||||
@@ -699,7 +625,8 @@ def main():
|
||||
apply_dark_theme(app)
|
||||
|
||||
w = VerifyWindow(video_path, wav_a, wav_b, sr=sr, lag_sec=lag_sec,
|
||||
mode=args.mode, out_dir=out_dir, temp_dir=temp_dir)
|
||||
mode=args.mode, out_dir=out_dir, temp_dir=temp_dir,
|
||||
volume=volume)
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
Reference in new issue
Block a user