refactor: solidify startup UX and engine-aware preprocessing

- Slim core deps: move ML stack to optional extras (paddle/tables/figures/scientific/full)
- Lazy settings proxy with config search paths (env var, cwd, user dir)
- New commands: init, demo, setup [basic|full], first-run guard on run/watch
- Engine-aware preprocessing: Paddle gets original image (fixes dark mode 0.83->0.95)
- Results table shows Skipped count; lazy run-dir creation
- kg_ocr marked experimental with extra, Docker defaults with OCR_PIPELINE_CONFIG
- 25/25 tests, ruff clean
This commit is contained in:
2026-07-19 22:14:54 +02:00
parent deab02610b
commit a0211155fa
59 changed files with 2426 additions and 629 deletions

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import platform
import shutil
from pathlib import Path
import yaml
from ocr_pipeline.config import DEFAULT_CONFIG_FILENAME, user_config_dir
from ocr_pipeline.utils.logging import get_logger
logger = get_logger(__name__)
CONFIG_HEADER = "# ocr-pipeline configuration (generated by `ocr-pipeline init`)\n"
def default_screenshots_dir() -> Path:
"""The directory where this OS puts screenshots by default."""
system = platform.system().lower()
if system == "darwin":
return Path.home() / "Desktop"
if system == "windows":
return Path.home() / "Pictures" / "Screenshots"
return Path.home() / "Pictures"
def tesseract_path() -> str | None:
"""Path to the tesseract executable, or None when it is not on PATH."""
return shutil.which("tesseract")
def tesseract_install_hint() -> str:
"""One-line install instruction for the current platform."""
system = platform.system().lower()
if system == "darwin":
return "brew install tesseract"
if system == "windows":
return "choco install tesseract # or https://github.com/UB-Mannheim/tesseract/wiki"
return "sudo apt-get install tesseract-ocr # or your distro's package manager"
def default_config(target_dir: Path, screenshots_dir: Path | None = None) -> dict:
"""Minimal config content: safe Tesseract defaults, everything optional off."""
shots = screenshots_dir or default_screenshots_dir()
return {
"input": {"paths": [str(shots)]},
"ocr": {"engine": "tesseract"},
"output": {"write_consolidated": True},
}
def write_default_config(target_dir: Path, screenshots_dir: Path | None = None) -> Path:
"""Write a minimal config.yaml into target_dir and return its path."""
target_dir.mkdir(parents=True, exist_ok=True)
config_path = target_dir / DEFAULT_CONFIG_FILENAME
body = yaml.safe_dump(default_config(target_dir, screenshots_dir), sort_keys=False)
config_path.write_text(CONFIG_HEADER + body, encoding="utf-8")
logger.info("config_written", path=str(config_path))
return config_path
def default_config_location(local: bool) -> Path:
"""Where `init` writes config: cwd with --local, else the per-user config dir."""
return Path.cwd() if local else user_config_dir()