Compare commits

..
74 Commits
Author SHA1 Message Date
honney e4470be110 Update README.md
clean up a graph
2026-10-11 19:07:53 +02:00
Honney OC bb1c45a19f chore: sanitize personal paths, add README 2026-10-11 17:02:13 +00:00
Honney OC b0e362f44e fixes 2026-10-11 16:59:09 +00:00
Honney OC f5af9c534a fix: use ffmpeg -progress pipe for reliable apply progress 2026-09-17 18:14:07 +00:00
Honney OC d0b7d1e07b fix: reuse table items when updating sync results 2026-09-16 16:38:25 +00:00
Honney OC 25fde25e25 fix: set image levels atomically in heatmap update 2026-09-16 16:35:22 +00:00
Honney OC ba6c6c8d21 feat: add video xcorr lab with gray proxy support 2026-09-16 15:51:34 +00:00
Honney OC 2119ea36ff feat: add background apply_sync with progress bar 2026-09-15 21:15:31 +00:00
Honney OC f328edb16c feat: add start-offset validation against file durations 2026-09-15 21:09:17 +00:00
Honney OC 06730a1be9 fix: skip loudnorm when measured loudness is -inf (silent segment) 2026-09-15 21:06:27 +00:00
Honney OC 1a2deb88a0 fix: pad delayed signal instead of trimming leading samples in apply_lag 2026-09-15 21:03:37 +00:00
Honney OC a8ff659bd3 fix: cap playback duration to actual audio length 2026-09-15 19:32:21 +00:00
Honney OC 2ed7a6306b chore: increase default volume to 70 and switch play mode to stereo L/R split 2026-09-15 19:21:48 +00:00
Honney OC 8ba24a97e2 refactor: remove Play L/R button, route A/B to stereo channels 2026-09-15 19:07:57 +00:00
Honney OC de00a23f47 feat: add split L/R playback mode for audio sync comparison 2026-09-15 19:04:55 +00:00
Honney OC bc5bbb5e23 feat: background waveform/thumbnail loading with RMS envelope 2026-09-15 18:38:59 +00:00
Honney OC 4147310706 feat: add visual picker for extraction settings 2026-09-15 18:30:16 +00:00
Honney OC d3fe3f9092 fix: skip mpv gracefully when video file unavailable 2026-09-15 18:10:16 +00:00
Honney OC e61c4a449d feat: add independent volume controls and refine audio filters 2026-09-15 13:06:14 +00:00
Honney OC 514158c29b fix: add lowpass filter at 3000Hz before agate in audio extraction 2026-09-15 13:00:25 +00:00
Honney OC e341aae658 feat: add loud-only extraction mode with agate gating 2026-09-15 12:52:16 +00:00
Honney OC 46f8998f21 feat: add overlay plot showing A and B waveforms together 2026-09-15 12:08:30 +00:00
Honney OC eaa23604a5 feat: pass WAV paths as CLI args to verify_sync 2026-09-14 02:36:34 +00:00
Honney OC 30015d5cad refactor: persist stream selection in .stream_state instead of config.toml 2026-09-14 02:09:02 +00:00
Honney OC 8e3db0d7f4 feat: improve audio stream selection and caching 2026-09-14 02:02:26 +00:00
Honney OC 6050e81be1 fix: apply_sync ffmpeg negative delay 2026-09-14 01:39:51 +00:00
Honney OC 4cf7227a6c fix: apply_sync ffmpeg streamcpy and filter 2026-09-14 01:38:17 +00:00
Honney OC 06dd13d907 fix: audio in verify_sync 2026-09-14 01:14:38 +00:00
Honney OC ccceacdbe4 fix: audio in verify_sync 2026-09-14 01:12:39 +00:00
Honney OC b72616fb7f fix: mpv in verify_sync 2026-09-14 01:00:58 +00:00
Honney OC f883c78b4b fix: mpv in verify_sync 2026-09-14 00:38:31 +00:00
Honney OC 963e95c37a fix: mpv in verify_sync 2026-09-14 00:28:34 +00:00
Honney OC 61d2caff22 fix: mpv in verify_sync 2026-09-14 00:27:01 +00:00
Honney OC 2f86d00bce fix: mpv in verify_sync 2026-09-14 00:23:56 +00:00
Hannes 5bc73074e3 fix: make runs make ast 2026-09-14 02:17:16 +02:00
Honney OC 20abfea931 fix: make runs make ast 2026-09-14 00:16:25 +00:00
Honney OC 4f41deacbb Merge remote-tracking branch 'refs/remotes/origin/main' 2026-09-14 00:04:14 +00:00
Honney OC 9fcdf4a73a fix: mpv --wid error 2026-09-14 00:04:02 +00:00
Hannes 2564029389 fix ignore cache files 2026-09-14 02:02:20 +02:00
Honney OC ea993bcf96 fix: waveform cache 2026-09-14 01:59:00 +02:00
Honney OC 05d6c20691 fix: waveform cache 2026-09-13 23:58:04 +00:00
Honney OC 669967eed3 fix: waveform cache 2026-09-13 23:57:57 +00:00
Honney OC ad3a41b013 fix: verify_sync 2026-09-13 23:51:22 +00:00
Honney OC 82f9e6baea fix: made crtl+c work 2026-09-13 23:47:51 +00:00
Honney OC 4c8ac4f9b7 fix: set C locale for mpv compatibility 2026-09-13 23:42:44 +00:00
Honney OC 27a1fc7851 chore: remove unused imports and dead code 2026-09-13 23:39:18 +00:00
Honney OC 4404dda926 feat: add waveform cache to avoid re-reading WAV files 2026-09-13 23:32:36 +00:00
Honney OC 37f19c5fd3 refactor: replace QMediaPlayer with mpv for video playback 2026-09-13 23:28:15 +00:00
Honney OC 5fe90af246 fix: pre-transcode video to H.264 and try VAAPI encoder 2026-09-13 23:23:41 +00:00
Honney OC 3bc3c9e433 feat: handle video decode errors with transcode fallback 2026-09-13 23:20:16 +00:00
Honney OC 99bbc3e126 fix: transcode video for QMediaPlayer and force xcb platform 2026-09-13 23:18:24 +00:00
Honney OC 8c9270c432 fix: black video 2026-09-13 23:03:39 +00:00
Honney OC 58d4b24bd1 fix: use QWidget video container and disable linked plot auto-range 2026-09-13 22:59:50 +00:00
Honney OC deb5f073fa feat: QMediaPlayer video playback and whisper audio rename tool 2026-09-13 22:54:54 +00:00
Hannes e8be3e0919 added default volume 2026-09-14 00:47:15 +02:00
Honney OC c552b3e4b1 feat: add apply and verify sync workflow 2026-09-13 22:32:52 +00:00
Honney OC 3a3c2936ff feat: add adjustable playback duration for A/B samples 2026-09-13 22:00:36 +00:00
Honney OC f3a166ef70 fix: use ColorGroup enum and disable dark palette via QT_STYLE_OVERRIDE 2026-09-13 21:56:04 +00:00
Honney OC 1469d16931 fix: QPalette lives in QtGui, not QtWidgets 2026-09-13 21:52:54 +00:00
Honney OC 4fb0b11cee fix: correctly fixed same basename issue 2026-09-13 21:50:09 +00:00
Honney OC 693bd8f9a3 fix: removed some errors and made sure that same filenames but different files do not break 2026-09-13 21:43:44 +00:00
Honney OC a01cf79447 fix: made sure the highest value are shown in cross correlation 2026-09-13 21:35:45 +00:00
Honney OC b76b7394cf fix: audio list 2026-09-13 21:14:57 +00:00
Honney OC 00790fb18c feat: let user choose which audiostream to extract 2026-09-13 21:05:25 +00:00
Honney OC ae32628cdb feat: run extract before ast 2026-09-13 20:51:32 +00:00
Honney OC 154aeebb01 feat: add two-pass EBU R128 loudness normalization for audio extraction 2026-09-13 20:37:40 +00:00
Honney OC d038882824 fix: make install robust with torch fallbacks for gpu and py3.14 2026-09-13 20:18:39 +00:00
Hannes 308ad79a9b fix: added the wav to gitignore 2026-09-13 22:15:50 +02:00
Honney OC 7d5fed2f17 feat: add config-driven extraction and audio sync tooling 2026-09-13 20:15:13 +00:00
Honney OC 6229bea7f4 fix: replace deprecated -vsync with -fps_mode for ffmpeg 7 2026-09-13 19:50:34 +00:00
Honney OC fe0c3af9a4 fix: normalize video fps to 24000/1001 with ffprobe check 2026-09-13 19:49:23 +00:00
Honney OC c7ca45696e feat: use lossless FFV1 and PCM for analysis clips 2026-09-13 19:36:45 +00:00
Hannes 2bfe696f2d feat: refactor: extract.py to accept multiple input files and improve output directory handling 2026-09-13 21:23:15 +02:00
Hannes db4531dd24 first commit 2026-09-13 21:16:18 +02:00
3 changed files with 241 additions and 3 deletions

No files matched your search

+238
View File
@@ -0,0 +1,238 @@
# movie_sync
Sync two releases/cuts of the same movie (different language dub, different
master, different container delay) by measuring content-to-content offset and
muxing a synced output.
Pipeline: pick a window → extract lossless clips → measure audio/video lag →
mux delayed audio → verify A/B.
```
file_a ─┐
├─► extract.py ─► extracted/{stem}_{video,audio,proxy} ─► sync_audio_test.py (AST) ─┐
file_b ─┘ ├─► lag (seconds) ─► apply_sync.py ─► output/{stem}_synced.mkv ─► verify_sync.py
sync_video_test.py (VST) ─┘
```
Lag convention (used everywhere, feeds `apply_sync --lag` directly):
`lag > 0` means **B is delayed vs A** (B starts later). `lag < 0` means B is
early. All estimators (`sync_audio.correlate`, VST methods) share this sign.
## Requirements
- Python 3.11+ (`tomllib` from stdlib; 3.14 also works)
- `ffmpeg` + `ffprobe` on `PATH` (extraction, proxy, mux, all probing)
- `mpv` for `verify_sync.py` video playback (IPC-controlled, `--no-audio`)
- Python deps (installed via `make install`): `torch`, `torchaudio`,
`soundfile`, `scipy`, `numpy`, `pyqtgraph`, `PyQt6`
- Optional: `sounddevice` (audio playback in AST/verify),
`openai-whisper` (only for `rename_audio.py`), `python-mpv`
No GPU is required. `torch.cuda` (ROCm or CUDA) is used when available,
otherwise everything falls back to NumPy CPU.
## Install
```bash
make install # auto-detect: ROCm > CUDA > CPU, create .venv, install deps
make install-cpu # force CPU torch
make install-rocm # force ROCm torch (e.g. RX 7900 XTX)
make install-cuda # force CUDA torch
```
Activate manually with `source .venv/bin/activate`. Python entry points also
work via `make <target>` without activating.
## Configuration
All paths/settings live in `config.toml` (repo root). Copy the shipped
template and set your two sources:
```toml
[extract]
file_a = "/path/to/movie_a.mkv"
file_b = "/path/to/movie_b.mp4"
duration = 30 # analysis window, seconds (the ONLY duration needed)
start = 0.0 # start offset, seconds (ffmpeg -ss before -i)
out_dir = "extracted"
target_fps = "24000/1001"
target_fps_eps = 0.02
proxy = true # build {stem}_proxy.mkv (192px gray FFV1) for VST
proxy_width = 192
[audio]
sample_rate = 48000
channels = 1
normalize = true # two-pass EBU R128 loudnorm
target_lufs = -14
target_tp = -1.0
target_lra = 11
# stream = null # null = prompt, int = index, "skip" = no audio
volume = 70
[sync]
method = "phat" # "phat" (GCC-PHAT, robust) or "standard"
use_gpu = "auto" # "auto" | "cpu" | "cuda" (ROCm+CUDA both via torch.cuda)
out_dir = "output"
[video]
grid = "32x18"
bars = true # auto-crop static letterbox/pillarbox before scaling
max_lag = 0.0 # search half-window, seconds (0 = full range)
prefer_proxy = true
decode_width = 192
```
Use generic local filenames (`movie_a`, `movie_b`) — never commit real
paths or copyrighted titles.
## Workflow
### 1. Pick a window (optional GUI)
```bash
make picker
# .venv/bin/python picker.py --config config.toml [--a … --b …]
```
Shows RMS waveforms + thumbnails for `file_a`, draggable start/duration
region, audio-stream selector. `Save to config.toml` writes back
`[extract] start/duration` (preserves other sections).
### 2. Extract analysis clips
```bash
make extract # normal: FFV1 video + PCM WAV + gray proxy
make extract-loud # same + agate transient gating (explosions/dialogue only)
make proxy # rebuild {stem}_proxy.mkv from local masters only (no source access)
```
Per input, `extract.py` writes into `extracted/`:
- `{stem}_video.mkv` — FFV1 lossless intra-only, `yuv444p`, CFR-normalized to
`target_fps` only if probed fps differs (avoids needless re-timing)
- `{stem}_audio.wav` — `pcm_f32le`, 48 kHz mono, two-pass `loudnorm`
(skipped with fallback on silent/`-inf` segments)
- `{stem}_proxy.mkv` — 192 px `gray8` FFV1 from the **local master**, for fast VST decode
Stems: differing filenames → bare stem; identical filenames → parent dirs
are walked up until they diverge. Multi-audio files prompt for
`0:a:N` (or set `[audio] stream`); the choice is saved to `.stream_state`
(gitignored) for later `verify_sync --mode full` mapping.
FPS note: if both inputs mismatch the target and differ from each other
(e.g. PAL 25 vs NTSC film 23.976), both are normalized to the target and a
warning suggests compensating audio tempo as well.
### 3. Measure lag
Audio (primary):
```bash
make ast
# LC_NUMERIC=C PYTHONPATH=. .venv/bin/python sync_audio_test.py --config config.toml
```
PyQtGraph GUI: overlay + per-track waveforms + GCC-PHAT xcorr, slider /
seconds / samples lag controls, Auto peak, Swap A/B, Export JSON, stereo
split playback (A left / B+lag right), `Apply sync` (background ffmpeg with
frame progress bar) and `Verify (short)` launcher. `make ast` auto-runs
`make extract` first if the expected WAVs are missing.
Video (cross-check / lab):
```bash
make vst
# .venv/bin/python sync_video_test.py --config config.toml
# headless: .venv/bin/python sync_video_test.py --no-gui [--audio-lag 0.042] [--export report.json]
```
Reads **only** `extracted/` (proxy preferred, master fallback — never the
originals). One decode + bar-crop + grid, then one batched per-pixel FFT.
Methods: M1 global-luma, M2 3×3 grid-diff, M3 pixel-diff-sum (main),
M4 median (logo-robust), M5 PHAT, M6 scene-cut spikes, M7 raw (no diff).
GUI shows overlaid xcorr curves, per-pixel heatmap, 3×3 zone energy, frame
scrub A|B, method table with median agreement, chunk-fit drift check
(slope ≈ 0 confirms pure offset, no speed mismatch), JSON export.
Library use:
```python
from sync_audio import load_wav, correlate
a, sr = load_wav("extracted/movie_a_audio.wav")
b, sr = load_wav("extracted/movie_b_audio.wav")
lag_samples, peak, corr = correlate(a, b, sr=48000, method="phat", device="auto")
```
### 4. Mux synced output
```bash
make sync LAG=0.042
# .venv/bin/python apply_sync.py --config config.toml --lag 0.042 [--a … --b … --out … --progress]
```
`apply_sync.py` builds: `ffmpeg -i A -i B -filter_complex … -map 0:v -map 0:a
-map [dN] -c:v copy -c:a:<A> copy -c:a:<B> <same-codec-or-aac> out.mkv`.
- `lag ≥ 0`: `adelay` (silence pad at B start); `lag < 0`: `atrim+asetpts` cut.
- All A audio streams are copied; all B audio streams are re-encoded with
their original codec where possible and appended after A's streams.
- Single undetermined-language B stream is tagged `ger`; existing
language/title/forced flags are preserved. `--progress` streams
`frame=/progress=end` on stdout for the AST background worker.
### 5. Verify
```bash
make verify LAG=0.042
# .venv/bin/python verify_sync.py --config config.toml --lag 0.042 --mode short
# --mode short: extracted 30 s parts (video + wav_a + wav_b)
# --mode full: synced output MKV (audio streams re-extracted from it)
```
mpv window (video, IPC seek/pause) + PyQtGraph overlay/dual waveforms with
playhead, timeline seek synced to mpv, A/B show toggles, per-track volume,
stereo or solo playback. Full mode resolves output stream indices as
`A: sel_a`, `B: n_a_streams + sel_b` using `.stream_state`.
### Utility: audio language tagging
```bash
.venv/bin/python rename_audio.py movie.mkv # dry run
.venv/bin/python rename_audio.py movie.mkv --apply [--stream 0,2] [--model base] [--segment 60] [--start 0]
```
Probes audio streams, runs Whisper language detection (confidence + top-5),
then rewrites MKV language/title metadata with stream copy (no re-encode).
## Files
| File | Purpose |
|---|---|
| `config.toml` | Single source of truth: inputs, window, audio, sync, video settings |
| `Makefile` | `install*`, `extract`, `extract-loud`, `proxy`, `picker`, `ast`, `vst`, `sync LAG=…`, `verify LAG=…`, `clean` |
| `extract.py` | Lossless clip extraction (FFV1 + PCM + proxy), loudnorm, stream picker, start validation |
| `picker.py` | Qt picker GUI: waveforms, thumbnails, start/duration → `config.toml` |
| `sync_audio.py` | Core lib: `load_wav`, `correlate` (GCC-PHAT torch/CPU), `lag_to_seconds`, `device_info` |
| `sync_audio_test.py` | AST GUI: waveforms + xcorr + tuning + playback + background apply + verify launcher |
| `sync_video_test.py` | VST lab: 7 video methods, heatmap, scrub, chunk-fit, headless `--no-gui` reports |
| `apply_sync.py` | Mux `A video + A audio + delayed B audio` with `--lag`, `--progress` protocol |
| `verify_sync.py` | mpv + dual-waveform verify player (`short` preview / `full` synced MKV) |
| `rename_audio.py` | Whisper language detect + MKV metadata rewrite (dry-run by default) |
| `waveform_cache.py` | `cache/waveforms/*.npy` disk cache for loaded WAVs (mtime-keyed) |
| `extracted/` | Generated clips (`*_video.mkv`, `*_audio.wav`, `*_proxy.mkv`); contents gitignored |
| `cache/` | Waveform + decoded-video caches; gitignored |
| `output/` | Default `apply_sync` destination (`{stem}_synced.mkv`); gitignored via `cache/`-style flow, create as needed |
Generated/ignored: `extracted/*.{mkv,wav,mp3}`, `cache/`, `.stream_state`,
`.venv/`, `__pycache__/`, `output/` (create per `sync.out_dir`).
## Notes
- Content-to-content sync: container `start_time`/edit-list mux delay is
ignored; alignment is achieved by padding silence on mux.
- Analysis codecs are deliberately CPU-only (FFV1/PCM have no GPU
implementation; VAAPI/AMF are lossy delivery codecs, wrong for measurement).
- PAL-speedup cases (25 fps vs 23.976) need audio tempo compensation beyond
frame-rate normalization — `extract.py` warns when it detects this.
- `LC_NUMERIC=C` is set for Qt/ffmpeg decimal parsing in `ast`/`vst`/`verify`.
+2 -2
View File
@@ -106,8 +106,8 @@ if _cfg_inputs is not None:
inputs = _cfg_inputs inputs = _cfg_inputs
else: else:
inputs = [ inputs = [
"/home/honney/Videos/Drachenzähmen/Drachenzaehmen.mp4", "/path/to/movie_a.mp4",
"/home/honney/Videos/Drachenzähmen/How to Train Your Dragon.mkv" "/path/to/movie_b.mkv"
] ]
INPUT_FILES = [] INPUT_FILES = []
for input_file in inputs: for input_file in inputs:
+1 -1
View File
@@ -8,7 +8,7 @@ Extracted wavs are pcm_f32le 48k mono (extract.py:41), so correlation is phase-a
Usage: Usage:
from sync_audio import load_wav, correlate, find_lag from sync_audio import load_wav, correlate, find_lag
a, sr = load_wav("extracted/Drachenzaehmen_audio.wav") a, sr = load_wav("extracted/movie_a_audio.wav")
lag_samples, peak, corr = correlate(a, b, sr=48000, method="phat", device="auto") lag_samples, peak, corr = correlate(a, b, sr=48000, method="phat", device="auto")
""" """