Initial scaffold: capture + vision modules with detect CLI
- capture/: frame sampling at configurable fps, ffmpeg audio extraction (WAV, whisper-ready) - vision/: changed-pixel motion scoring, stable/moving segmentation, contour + perspective document detection, Laplacian sharpness scoring, optional CLAHE enhancement - pipeline: two-pass detect (motion timeline, then best-frame crop per stable window) writing crops, debug frames, report.json, and motion_scores.csv - CLI: probe / detect / extract-audio subcommands - config.yaml with tunable thresholds; placeholder packages for events, transcribe, ocr, naming, pdf, review_cli - synthetic sample video generator + 16 unit tests
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""Configuration loading with built-in defaults, overridable via config.yaml."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, fields, is_dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaptureConfig:
|
||||
sample_fps: float = 8.0
|
||||
processing_width: int = 960
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioConfig:
|
||||
sample_rate: int = 16000
|
||||
|
||||
|
||||
@dataclass
|
||||
class MotionConfig:
|
||||
blur_ksize: int = 21
|
||||
pixel_threshold: int = 12
|
||||
threshold: float = 0.02
|
||||
stable_min_duration_s: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocumentConfig:
|
||||
min_area_ratio: float = 0.04
|
||||
canny_low: int = 50
|
||||
canny_high: int = 150
|
||||
enhance: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpeechConfig:
|
||||
window_before_s: float = 3.0
|
||||
window_after_s: float = 5.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
dir: str = "./output"
|
||||
consume_dir: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
capture: CaptureConfig = field(default_factory=CaptureConfig)
|
||||
audio: AudioConfig = field(default_factory=AudioConfig)
|
||||
motion: MotionConfig = field(default_factory=MotionConfig)
|
||||
document: DocumentConfig = field(default_factory=DocumentConfig)
|
||||
speech: SpeechConfig = field(default_factory=SpeechConfig)
|
||||
output: OutputConfig = field(default_factory=OutputConfig)
|
||||
|
||||
|
||||
def _merge_into(instance: Any, data: dict[str, Any]) -> Any:
|
||||
"""Overlay a dict of overrides onto a dataclass instance, recursively."""
|
||||
for f in fields(instance):
|
||||
if f.name not in data:
|
||||
continue
|
||||
value = data[f.name]
|
||||
current = getattr(instance, f.name)
|
||||
if is_dataclass(current) and isinstance(value, dict):
|
||||
_merge_into(current, value)
|
||||
else:
|
||||
setattr(instance, f.name, value)
|
||||
return instance
|
||||
|
||||
|
||||
def load_config(path: str | Path | None = None) -> Config:
|
||||
"""Load config.yaml if present; unknown keys are ignored, missing keys use defaults."""
|
||||
cfg = Config()
|
||||
if path is None:
|
||||
return cfg
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {path}")
|
||||
data = yaml.safe_load(path.read_text()) or {}
|
||||
return _merge_into(cfg, data)
|
||||
|
||||
|
||||
def config_to_dict(cfg: Config) -> dict[str, Any]:
|
||||
"""Serialize the effective config (for embedding in run reports)."""
|
||||
from dataclasses import asdict
|
||||
|
||||
return asdict(cfg)
|
||||
Reference in New Issue
Block a user