diff --git a/src/skill_seekers/cli/source_detector.py b/src/skill_seekers/cli/source_detector.py index 9ce464a..724f342 100644 --- a/src/skill_seekers/cli/source_detector.py +++ b/src/skill_seekers/cli/source_detector.py @@ -148,9 +148,16 @@ class SourceDetector: lower = source.lower() # YouTube patterns - youtube_keywords = ["youtube.com/watch", "youtu.be/", "youtube.com/playlist", - "youtube.com/@", "youtube.com/channel/", "youtube.com/c/", - "youtube.com/shorts/", "youtube.com/embed/"] + youtube_keywords = [ + "youtube.com/watch", + "youtu.be/", + "youtube.com/playlist", + "youtube.com/@", + "youtube.com/channel/", + "youtube.com/c/", + "youtube.com/shorts/", + "youtube.com/embed/", + ] if any(kw in lower for kw in youtube_keywords): # Determine suggested name if "playlist" in lower: diff --git a/src/skill_seekers/cli/video_scraper.py b/src/skill_seekers/cli/video_scraper.py index 3fd096a..f719e82 100644 --- a/src/skill_seekers/cli/video_scraper.py +++ b/src/skill_seekers/cli/video_scraper.py @@ -84,9 +84,7 @@ def check_video_dependencies(require_full: bool = False) -> None: deps = ", ".join(missing) extra = "[video-full]" if require_full else "[video]" setup_hint = ( - "\nFor visual deps (GPU-aware): skill-seekers video --setup" - if require_full - else "" + "\nFor visual deps (GPU-aware): skill-seekers video --setup" if require_full else "" ) raise RuntimeError( f"Missing video dependencies: {deps}\n" diff --git a/src/skill_seekers/cli/video_setup.py b/src/skill_seekers/cli/video_setup.py index d73b1c3..60c3333 100644 --- a/src/skill_seekers/cli/video_setup.py +++ b/src/skill_seekers/cli/video_setup.py @@ -463,9 +463,7 @@ def install_torch(gpu_info: GPUInfo, python_exe: str | None = None) -> bool: return False -def install_visual_deps( - modules: SetupModules | None = None, python_exe: str | None = None -) -> bool: +def install_visual_deps(modules: SetupModules | None = None, python_exe: str | None = None) -> bool: """Install visual extraction dependencies. Returns True on success, False on failure. @@ -672,9 +670,7 @@ def run_setup(interactive: bool = True) -> int: venv_path = ".venv" if interactive: try: - answer = input( - f" Create venv at ./{venv_path}? [Y/n] " - ).strip().lower() + answer = input(f" Create venv at ./{venv_path}? [Y/n] ").strip().lower() except (EOFError, KeyboardInterrupt): print("\nSetup cancelled.") return 1 @@ -785,7 +781,9 @@ def run_setup(interactive: bool = True) -> int: print(" Installing PyTorch...") if not install_torch(gpu_info, python_exe): print(" FAILED: PyTorch installation failed.") - print(f" Try: {python_exe} -m pip install torch torchvision --index-url {gpu_info.index_url}") + print( + f" Try: {python_exe} -m pip install torch torchvision --index-url {gpu_info.index_url}" + ) return 1 print(" PyTorch installed.") diff --git a/tests/test_video_setup.py b/tests/test_video_setup.py index b5c7049..d336e76 100644 --- a/tests/test_video_setup.py +++ b/tests/test_video_setup.py @@ -129,9 +129,7 @@ class TestGPUDetection(unittest.TestCase): @patch("skill_seekers.cli.video_setup.subprocess.run") def test_nvidia_smi_error(self, mock_run, mock_which): """nvidia-smi returns non-zero → skip to next check.""" - mock_which.side_effect = lambda cmd: ( - "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None - ) + mock_which.side_effect = lambda cmd: "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None mock_run.return_value = MagicMock(returncode=1, stdout="") gpu = detect_gpu() assert gpu.vendor == GPUVendor.NONE @@ -140,9 +138,7 @@ class TestGPUDetection(unittest.TestCase): @patch("skill_seekers.cli.video_setup.subprocess.run") def test_nvidia_smi_timeout(self, mock_run, mock_which): """nvidia-smi times out → skip to next check.""" - mock_which.side_effect = lambda cmd: ( - "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None - ) + mock_which.side_effect = lambda cmd: "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None mock_run.side_effect = subprocess.TimeoutExpired(cmd="nvidia-smi", timeout=10) gpu = detect_gpu() assert gpu.vendor == GPUVendor.NONE @@ -218,13 +214,11 @@ class TestVenv(unittest.TestCase): def test_is_in_venv_detects_prefix_mismatch(self): # If sys.prefix != sys.base_prefix, we're in a venv - with patch.object(sys, "prefix", "/some/venv"), \ - patch.object(sys, "base_prefix", "/usr"): + with patch.object(sys, "prefix", "/some/venv"), patch.object(sys, "base_prefix", "/usr"): assert is_in_venv() is True def test_is_in_venv_detects_no_venv(self): - with patch.object(sys, "prefix", "/usr"), \ - patch.object(sys, "base_prefix", "/usr"): + with patch.object(sys, "prefix", "/usr"), patch.object(sys, "base_prefix", "/usr"): assert is_in_venv() is False def test_create_venv_in_tempdir(self): @@ -376,14 +370,20 @@ class TestModuleSelection(unittest.TestCase): def test_build_no_optional_deps(self): """Even with all optional modules off, base video deps are included.""" m = SetupModules( - torch=False, easyocr=False, opencv=False, - tesseract=False, scenedetect=False, whisper=False, + torch=False, + easyocr=False, + opencv=False, + tesseract=False, + scenedetect=False, + whisper=False, ) deps = _build_visual_deps(m) assert deps == list(_BASE_VIDEO_DEPS) def test_build_partial_deps(self): - m = SetupModules(easyocr=True, opencv=True, tesseract=False, scenedetect=False, whisper=False) + m = SetupModules( + easyocr=True, opencv=True, tesseract=False, scenedetect=False, whisper=False + ) deps = _build_visual_deps(m) assert "yt-dlp" in deps assert "youtube-transcript-api" in deps @@ -454,7 +454,9 @@ class TestInstallation(unittest.TestCase): @patch("skill_seekers.cli.video_setup.subprocess.run") def test_install_visual_deps_partial_modules(self, mock_run): mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") - modules = SetupModules(easyocr=True, opencv=False, tesseract=False, scenedetect=False, whisper=False) + modules = SetupModules( + easyocr=True, opencv=False, tesseract=False, scenedetect=False, whisper=False + ) install_visual_deps(modules) call_args = mock_run.call_args[0][0] assert "easyocr" in call_args @@ -464,7 +466,9 @@ class TestInstallation(unittest.TestCase): def test_install_visual_deps_base_only(self, mock_run): """Even with all optional modules off, base video deps get installed.""" mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") - modules = SetupModules(easyocr=False, opencv=False, tesseract=False, scenedetect=False, whisper=False) + modules = SetupModules( + easyocr=False, opencv=False, tesseract=False, scenedetect=False, whisper=False + ) result = install_visual_deps(modules) assert result is True call_args = mock_run.call_args[0][0] @@ -488,7 +492,15 @@ class TestVerification(unittest.TestCase): def test_expected_keys(self): results = verify_installation() - for key in ("yt-dlp", "youtube-transcript-api", "torch", "torch.cuda", "torch.rocm", "easyocr", "opencv"): + for key in ( + "yt-dlp", + "youtube-transcript-api", + "torch", + "torch.cuda", + "torch.rocm", + "easyocr", + "opencv", + ): assert key in results, f"Missing key: {key}" @@ -505,15 +517,29 @@ class TestRunSetup(unittest.TestCase): @patch("skill_seekers.cli.video_setup.install_torch", return_value=True) @patch("skill_seekers.cli.video_setup.check_tesseract") @patch("skill_seekers.cli.video_setup.detect_gpu") - def test_non_interactive_success(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify): + def test_non_interactive_success( + self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify + ): mock_detect.return_value = GPUInfo( - vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu", + vendor=GPUVendor.NONE, + name="CPU-only", + index_url=f"{_PYTORCH_BASE}/cpu", ) - mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"} + mock_tess.return_value = { + "installed": True, + "has_eng": True, + "install_cmd": "", + "version": "5.3.0", + } mock_verify.return_value = { - "torch": True, "torch.cuda": False, "torch.rocm": False, - "easyocr": True, "opencv": True, "pytesseract": True, - "scenedetect": True, "faster-whisper": True, + "torch": True, + "torch.cuda": False, + "torch.rocm": False, + "easyocr": True, + "opencv": True, + "pytesseract": True, + "scenedetect": True, + "faster-whisper": True, } rc = run_setup(interactive=False) assert rc == 0 @@ -525,9 +551,16 @@ class TestRunSetup(unittest.TestCase): @patch("skill_seekers.cli.video_setup.detect_gpu") def test_failure_returns_nonzero(self, mock_detect, mock_tess, mock_torch): mock_detect.return_value = GPUInfo( - vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu", + vendor=GPUVendor.NONE, + name="CPU-only", + index_url=f"{_PYTORCH_BASE}/cpu", ) - mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"} + mock_tess.return_value = { + "installed": True, + "has_eng": True, + "install_cmd": "", + "version": "5.3.0", + } rc = run_setup(interactive=False) assert rc == 1 @@ -537,9 +570,16 @@ class TestRunSetup(unittest.TestCase): @patch("skill_seekers.cli.video_setup.detect_gpu") def test_visual_deps_failure(self, mock_detect, mock_tess, mock_deps, mock_torch): mock_detect.return_value = GPUInfo( - vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu", + vendor=GPUVendor.NONE, + name="CPU-only", + index_url=f"{_PYTORCH_BASE}/cpu", ) - mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"} + mock_tess.return_value = { + "installed": True, + "has_eng": True, + "install_cmd": "", + "version": "5.3.0", + } rc = run_setup(interactive=False) assert rc == 1 @@ -551,13 +591,25 @@ class TestRunSetup(unittest.TestCase): def test_rocm_configures_env(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify): """AMD GPU → configure_rocm_env called and env vars set.""" mock_detect.return_value = GPUInfo( - vendor=GPUVendor.AMD, name="RX 7900", index_url=f"{_PYTORCH_BASE}/rocm6.3", + vendor=GPUVendor.AMD, + name="RX 7900", + index_url=f"{_PYTORCH_BASE}/rocm6.3", ) - mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"} + mock_tess.return_value = { + "installed": True, + "has_eng": True, + "install_cmd": "", + "version": "5.3.0", + } mock_verify.return_value = { - "torch": True, "torch.cuda": False, "torch.rocm": True, - "easyocr": True, "opencv": True, "pytesseract": True, - "scenedetect": True, "faster-whisper": True, + "torch": True, + "torch.cuda": False, + "torch.rocm": True, + "easyocr": True, + "opencv": True, + "pytesseract": True, + "scenedetect": True, + "faster-whisper": True, } rc = run_setup(interactive=False) assert rc == 0 @@ -574,6 +626,7 @@ class TestTesseractCircuitBreaker(unittest.TestCase): def test_circuit_breaker_flag_exists(self): import skill_seekers.cli.video_visual as vv + assert hasattr(vv, "_tesseract_broken") def test_circuit_breaker_skips_after_failure(self):