VENV ?= .venv PY := $(VENV)/bin/python PIP := $(PY) -m pip UV := uv CONFIG ?= config.toml # detect uv, fallback to pip HAS_UV := $(shell command -v $(UV) >/dev/null 2>&1 && echo 1 || echo 0) ifeq ($(HAS_UV),1) VENV_CREATE = $(UV) venv $(VENV) --python 3.11 2>/dev/null || $(UV) venv $(VENV) PIP_INSTALL = $(UV) pip install --python $(PY) else VENV_CREATE = python3 -m venv $(VENV) PIP_INSTALL = $(PIP) install endif .PHONY: default install install-cpu install-rocm install-cuda extract ast sync verify clean help default: ast help: @echo "Targets:" @echo " make install # auto-detect GPU (ROCm>CUDA>CPU) via rocm-smi/nvidia-smi/torch, create venv, install deps" @echo " make install-cpu # force CPU torch" @echo " make install-rocm # force ROCm torch (RX 7900 XTX gfx1100)" @echo " make install-cuda # force CUDA torch" @echo " make extract # run extract.py (uses config.toml if present)" @echo " make ast # run sync_audio_test.py (audio_sync_test, PyQtGraph GUI)" @echo " make sync LAG=0.042 # run apply_sync.py to mux synced output (LAG in seconds)" @echo " make verify LAG=0.042 # verify sync: video + dual waveform player" @echo " make clean" install: @if [ ! -d $(VENV) ]; then echo "=> creating venv $(VENV)"; $(VENV_CREATE); else echo "=> venv exists $(VENV)"; fi @echo "=> detecting GPU..." @HAS_ROCM=0; HAS_CUDA=0; PYVER=$$($(PY) -c "import sys;print(f'{sys.version_info.major}{sys.version_info.minor}')" 2>/dev/null || echo 314); \ if command -v rocm-smi >/dev/null 2>&1 && [ -c /dev/kfd ] && lspci 2>/dev/null | grep -qi amd; then HAS_ROCM=1; fi; \ if command -v nvidia-smi >/dev/null 2>&1; then HAS_CUDA=1; fi; \ $(PIP_INSTALL) --upgrade pip; \ if [ $$HAS_ROCM -eq 1 ]; then \ echo " ROCm detected (AMD) -> torch rocm6.3 (fallback 6.2, cp314 needs 6.3+)"; \ $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/rocm6.3 || \ ( echo " rocm6.3 failed, trying rocm6.2"; $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/rocm6.2 ) || \ ( echo " ROCm torch failed for py$$PYVER -> fallback cpu (numpy still works)"; $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cpu || echo " torch install failed, continuing with numpy CPU fallback"); \ elif [ $$HAS_CUDA -eq 1 ]; then \ echo " CUDA detected (NVIDIA) -> torch cu121 (fallback cu124)"; \ $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cu121 || \ $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cu124 || \ ( echo " CUDA torch failed -> fallback cpu"; $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cpu || true); \ else \ echo " No GPU -> torch cpu"; \ $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cpu || echo " torch cpu install failed (py$$PYVER), continuing with numpy CPU fallback"; \ fi @echo "=> installing common deps..." @$(PIP_INSTALL) soundfile scipy numpy pyqtgraph PyQt6 || echo " common deps failed" @echo "=> optional: sounddevice (playback), openai-whisper (language detection), mpv (video player)..." @$(PIP_INSTALL) sounddevice openai-whisper python-mpv || echo " optional deps failed (sounddevice/whisper/mpv)" @$(PY) -c "import torch, sys; print(f'torch {torch.__version__} cuda_available={torch.cuda.is_available()}', end=''); print(f' device={torch.cuda.get_device_name(0)}' if torch.cuda.is_available() else ' (cpu)')" 2>&1 || echo "torch not available, using numpy CPU fallback (still functional for 90s xcorr)" @echo "=> done. Activate: source $(VENV)/bin/activate" install-cpu: @if [ ! -d $(VENV) ]; then $(VENV_CREATE); fi $(PIP_INSTALL) --upgrade pip $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cpu || echo "torch cpu failed, numpy fallback" $(PIP_INSTALL) soundfile scipy numpy pyqtgraph PyQt6 sounddevice || true $(PY) -c "import torch; print(torch.__version__, torch.cuda.is_available())" 2>&1 || echo "torch not installed, numpy fallback" install-rocm: @if [ ! -d $(VENV) ]; then $(VENV_CREATE); fi $(PIP_INSTALL) --upgrade pip $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/rocm6.3 || $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/rocm6.2 || $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cpu || echo "torch install failed, using numpy fallback" $(PIP_INSTALL) soundfile scipy numpy pyqtgraph PyQt6 sounddevice || true $(PY) -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else '')" 2>&1 || echo "torch not installed, numpy fallback" install-cuda: @if [ ! -d $(VENV) ]; then $(VENV_CREATE); fi $(PIP_INSTALL) --upgrade pip $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cu121 || $(PIP_INSTALL) torch torchaudio --index-url https://download.pytorch.org/whl/cu124 || echo "torch cuda failed, fallback cpu" $(PIP_INSTALL) soundfile scipy numpy pyqtgraph PyQt6 sounddevice || true $(PY) -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else '')" 2>&1 || echo "torch not installed" extract: @if [ -x $(PY) ]; then $(PY) extract.py; else python3 extract.py; fi sync: @if [ -x $(PY) ]; then PYTHONPATH=. $(PY) apply_sync.py --config $(CONFIG) --lag $(LAG); else PYTHONPATH=. python3 apply_sync.py --config $(CONFIG) --lag $(LAG); fi verify: @if [ -x $(PY) ]; then LC_NUMERIC=C PYTHONPATH=. QT_QPA_PLATFORM=xcb QT_STYLE_OVERRIDE=Fusion $(PY) verify_sync.py --config $(CONFIG) --lag $(LAG) --mode short; else LC_NUMERIC=C PYTHONPATH=. QT_QPA_PLATFORM=xcb QT_STYLE_OVERRIDE=Fusion python3 verify_sync.py --config $(CONFIG) --lag $(LAG) --mode short; fi ast: @AUDIO_A=$$(python3 -c "import tomllib; c=tomllib.load(open('config.toml','rb')); print(c['extract'].get('file_a',''))" 2>/dev/null); \ AUDIO_B=$$(python3 -c "import tomllib; c=tomllib.load(open('config.toml','rb')); print(c['extract'].get('file_b',''))" 2>/dev/null); \ OUT=$$(python3 -c "import tomllib; c=tomllib.load(open('config.toml','rb')); print(c['extract'].get('out_dir','extracted'))" 2>/dev/null); \ STEM_A=$$(basename "$$AUDIO_A" | sed 's/\.[^.]*$$//'); \ STEM_B=$$(basename "$$AUDIO_B" | sed 's/\.[^.]*$$//'); \ NEED_EXTRACT=0; \ if [ ! -f "$$OUT/$${STEM_A}_audio.wav" ] || [ ! -f "$$OUT/$${STEM_B}_audio.wav" ]; then NEED_EXTRACT=1; fi; \ if [ $$NEED_EXTRACT -eq 1 ]; then \ echo "=> extracted wavs not found, running make extract first..."; \ $(MAKE) extract; \ else \ echo "=> extracted wavs exist ($$OUT/$${STEM_A}_audio.wav, $$OUT/$${STEM_B}_audio.wav), skipping extract"; \ fi @if [ -x $(PY) ]; then LC_NUMERIC=C PYTHONPATH=. QT_STYLE_OVERRIDE=Fusion $(PY) sync_audio_test.py --config $(CONFIG); else LC_NUMERIC=C PYTHONPATH=. QT_STYLE_OVERRIDE=Fusion python3 sync_audio_test.py --config $(CONFIG); fi clean: rm -rf $(VENV) __pycache__ .pytest_cache