claude-video/tests/test_local_whisperx.py
bradautomates e90ef44c32 Implement v0.3.0 reliability fixes and managed WhisperX transcription
Add opt-in local transcription with managed installation, cache warm-up,
and a one-time backend choice while preserving native-caption priority.

Correct frame timing and coverage, caption provenance and download
ownership, configuration parsing, provider pairing, and partial reports.
Update cross-host instructions, packaging, and test-gated release CI.

Validate 198 isolated tests, real local English and Spanish inference,
network-denied warm-cache inference, live captions/media, and packaging.
Record platform and account-dependent acceptance limits separately.
2026-09-12 22:09:43 +10:00

227 lines
9.9 KiB
Python

"""Exercise real child processes without downloading models or importing Torch."""
import json
import os
import sys
from pathlib import Path
import pytest
import config
import local_whisperx
import setup
import whisper
@pytest.fixture
def fake_whisperx(tmp_path):
root = tmp_path / 'venv with spaces'
bin_dir = root / ('Scripts' if os.name == 'nt' else 'bin')
bin_dir.mkdir(parents=True)
script = bin_dir / 'fake.py'
script.write_text('''import json, os, pathlib, sys, time
if '--help' in sys.argv:
print('--no_align --model'); sys.exit(0)
mode = os.environ.get('WATCH_TEST_MODE', 'good')
if mode == 'slow': time.sleep(10)
if mode == 'crash':
sys.stderr.buffer.write(b'CUDA out of memory\\xff'); sys.exit(1)
if mode == 'offline':
print('HuggingFace offline cache missing'); sys.exit(1)
out = pathlib.Path(sys.argv[sys.argv.index('--output_dir')+1])
path = out / (pathlib.Path(sys.argv[1]).stem + '.json')
if mode == 'missing': sys.exit(0)
if mode == 'malformed': path.write_text('{'); sys.exit(0)
assert os.environ['PYTHONIOENCODING'] == 'utf-8'
assert os.environ['PYTHONWARNINGS'] == 'ignore'
assert '--no_align' in sys.argv and '--task' in sys.argv and 'silero' in sys.argv
segments = [] if mode == 'empty' else [{'start': 1.0, 'end': 2.0, 'text': ' hola ', 'avg_logprob': -0.1}]
path.write_text(json.dumps({'segments': segments, 'language': 'en'}))
''', encoding='utf-8')
exe = bin_dir / ('whisperx.cmd' if os.name == 'nt' else 'whisperx')
if os.name == 'nt':
exe.write_text(f'@"{sys.executable}" "%~dp0fake.py" %*\r\n')
else:
exe.write_text(f'#!{sys.executable}\n' + script.read_text())
exe.chmod(0o755)
(root / '.deps-ok').write_text('3.8.6')
config.write_settings({'WATCH_WHISPERX_BIN': str(exe), 'WATCH_WHISPER_BACKEND': 'whisperx'})
audio = tmp_path / 'audio with spaces.mp3'
audio.write_bytes(b'audio')
return exe, audio
def test_local_child_normalizes_segments_and_ignores_language(fake_whisperx):
_, audio = fake_whisperx
assert local_whisperx.transcribe_audio(audio) == [{'start': 1, 'end': 2, 'text': 'hola'}]
assert not list(audio.parent.glob('watch-whisperx-*'))
def test_empty_is_no_speech(fake_whisperx, monkeypatch):
monkeypatch.setenv('WATCH_TEST_MODE', 'empty')
segments = local_whisperx.transcribe_audio(fake_whisperx[1])
assert not segments and segments.no_speech
@pytest.mark.parametrize('mode,message', [('malformed', 'malformed'), ('missing', 'missing'), ('crash', 'Memory or device'), ('offline', 'Model download')])
def test_local_failure_categories(fake_whisperx, monkeypatch, mode, message):
monkeypatch.setenv('WATCH_TEST_MODE', mode)
with pytest.raises(SystemExit, match=message):
local_whisperx.transcribe_audio(fake_whisperx[1])
def test_timeout_kills_child_and_cleans_output(fake_whisperx, monkeypatch):
monkeypatch.setenv('WATCH_TEST_MODE', 'slow')
monkeypatch.setenv('WATCH_WHISPERX_TIMEOUT', '0.2')
with pytest.raises(SystemExit, match='timed out'):
local_whisperx.transcribe_audio(fake_whisperx[1])
assert not list(fake_whisperx[1].parent.glob('watch-whisperx-*'))
def test_incomplete_install_and_missing_executable(fake_whisperx):
exe, audio = fake_whisperx
local_whisperx.sentinel_for(exe).unlink()
with pytest.raises(SystemExit, match='incomplete'):
local_whisperx.transcribe_audio(audio)
exe.unlink()
with pytest.raises(SystemExit, match='missing'):
local_whisperx.transcribe_audio(audio)
def test_local_route_never_resolves_a_cloud_key(fake_whisperx, monkeypatch):
exe, audio = fake_whisperx
monkeypatch.setattr(whisper, 'load_api_key', lambda *a, **k: pytest.fail('no cloud credentials for local inference'))
monkeypatch.setattr(whisper, 'extract_audio', lambda *a: audio)
assert whisper.transcribe_video('video.mp4', audio, backend='whisperx')[1] == 'whisperx'
@pytest.mark.parametrize('key,value', [('WATCH_WHISPERX_TIMEOUT', 'nan'), ('WATCH_WHISPERX_BATCH_SIZE', '1.5'),
('WATCH_WHISPERX_DEVICE', 'mps'), ('WATCH_WHISPERX_LANGUAGE', 'english')])
def test_invalid_local_settings(monkeypatch, key, value):
monkeypatch.setenv(key, value)
with pytest.raises(config.ConfigError):
local_whisperx.settings()
def test_installer_rebuilds_partial_and_is_idempotent(monkeypatch):
commands = []
venv = Path.home() / '.cache/watch/whisperx-venv'
venv.mkdir(parents=True)
(venv / 'half-built').write_text('partial')
monkeypatch.setattr(setup, '_ensure_uv', lambda: '/fake/uv')
monkeypatch.setattr(setup, '_which', lambda name: '/fake/' + name)
monkeypatch.setattr(setup.platform, 'system', lambda: 'Darwin')
def step(cmd, label):
commands.append(cmd)
if 'venv' in cmd:
(venv / 'bin').mkdir(parents=True)
(venv / 'bin/python').touch()
(venv / 'bin/whisperx').touch()
if cmd[0] == 'ffmpeg':
Path(cmd[-1]).write_bytes(b'audio')
monkeypatch.setattr(setup, '_install_step', step)
monkeypatch.setattr(local_whisperx, 'transcribe_audio', lambda *a, **kw: [])
import subprocess
monkeypatch.setattr(setup, 'run_text', lambda *a, **kw: subprocess.CompletedProcess([], 0, '[]', ''))
assert setup.install_whisperx() == 0
assert not (venv / 'half-built').exists()
assert (venv / '.deps-ok').exists()
assert config.get_config()['whisper_backend'] == 'whisperx'
assert not setup.is_first_run()
setup.install_whisperx()
assert sum('venv' in cmd for cmd in commands) == 1
def test_failed_warmup_never_marks_ready(monkeypatch):
venv = Path.home() / '.cache/watch/whisperx-venv'
(venv / 'bin').mkdir(parents=True)
for name in ['python', 'whisperx']:
(venv / 'bin' / name).touch()
(venv / '.deps-ok').write_text('old')
monkeypatch.setattr(setup, '_ensure_uv', lambda: '/fake/uv')
monkeypatch.setattr(setup, '_which', lambda name: '/fake/' + name)
monkeypatch.setattr(setup.platform, 'system', lambda: 'Darwin')
monkeypatch.setattr(setup, '_install_step', lambda *a: None)
def fail(*a, **kw):
raise SystemExit('warmup failed')
monkeypatch.setattr(local_whisperx, 'transcribe_audio', fail)
with pytest.raises(SystemExit, match='warmup failed'):
setup.install_whisperx()
assert not (venv / '.deps-ok').exists() and setup.is_first_run()
assert config.get_config()['whisper_backend'] == 'auto'
def test_cancellation_stops_the_process(fake_whisperx, monkeypatch):
monkeypatch.setenv('WATCH_TEST_MODE', 'slow')
original_get = local_whisperx.queue.Queue.get
interrupted = False
def interrupt_once(self, *args, **kwargs):
nonlocal interrupted
if not interrupted:
interrupted = True
raise KeyboardInterrupt()
return original_get(self, *args, **kwargs)
monkeypatch.setattr(local_whisperx.queue.Queue, 'get', interrupt_once)
with pytest.raises(SystemExit, match='cancelled and stopped'):
local_whisperx.transcribe_audio(fake_whisperx[1])
assert not list(fake_whisperx[1].parent.glob('watch-whisperx-*'))
@pytest.mark.parametrize('system', ['Linux', 'Windows'])
def test_cpu_installer_recipe_keeps_torch_out_of_cuda_index(monkeypatch, system):
import subprocess
commands = []
venv = Path.home() / '.cache/watch/whisperx-venv'
monkeypatch.setattr(setup.platform, 'system', lambda: system)
monkeypatch.setattr(setup, '_ensure_uv', lambda: '/fake/uv')
monkeypatch.setattr(setup, '_which', lambda name: '/fake/' + name)
def step(cmd, label):
commands.append([str(a) for a in cmd])
if 'venv' in cmd:
bin_dir = venv / ('Scripts' if system == 'Windows' else 'bin')
bin_dir.mkdir(parents=True)
for name in (('python.exe', 'whisperx.exe') if system == 'Windows' else ('python', 'whisperx')):
(bin_dir / name).touch()
monkeypatch.setattr(setup, '_install_step', step)
monkeypatch.setattr(local_whisperx, 'transcribe_audio', lambda *a, **kw: [])
monkeypatch.setattr(setup, 'run_text', lambda *a, **kw: subprocess.CompletedProcess([], 0, '[]', ''))
assert setup.install_whisperx() == 0
torch = next(cmd for cmd in commands if 'torch==2.8.0' in cmd)
assert 'torchaudio==2.8.0' in torch and 'https://download.pytorch.org/whl/cpu' in torch
assert any('whisperx==3.8.6' in cmd for cmd in commands)
@pytest.mark.parametrize('system', ['Darwin', 'Linux', 'Windows'])
def test_uv_bootstrap_discovers_new_executable_without_path_refresh(monkeypatch, system):
commands = []
monkeypatch.setattr(setup.platform, 'system', lambda: system)
monkeypatch.setattr(setup, '_which', lambda name: '/fake/brew' if name == 'brew' else None)
fallback = Path.home() / '.local/bin' / ('uv.exe' if system == 'Windows' else 'uv')
def step(cmd, label):
commands.append(cmd)
if label == 'Install uv':
fallback.parent.mkdir(parents=True, exist_ok=True)
fallback.touch()
monkeypatch.setattr(setup, '_install_step', step)
assert setup._ensure_uv() == str(fallback)
assert all('sudo' not in cmd for cmd in commands)
assert commands[0][0] == {'Darwin': 'brew', 'Linux': 'curl', 'Windows': 'powershell'}[system]
def test_failed_uv_download_does_not_execute_shell(monkeypatch):
monkeypatch.setattr(setup.platform, 'system', lambda: 'Linux')
monkeypatch.setattr(setup, '_which', lambda name: None)
commands = []
def fail(cmd, label):
commands.append(cmd)
raise SystemExit('download failed')
monkeypatch.setattr(setup, '_install_step', fail)
with pytest.raises(SystemExit, match='download failed'):
setup._ensure_uv()
assert len(commands) == 1 and commands[0][0] == 'curl'
def test_uv_bootstrap_reports_missing_homebrew(monkeypatch):
monkeypatch.setattr(setup.platform, 'system', lambda: 'Darwin')
monkeypatch.setattr(setup, '_which', lambda name: None)
with pytest.raises(SystemExit, match='Homebrew'):
setup._ensure_uv()