mirror of
https://github.com/Luchexx/claude-video.git
synced 2026-11-02 17:29:52 +00:00
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.
227 lines
9.9 KiB
Python
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()
|