- 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
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
"""PaperPod command-line interface.
|
|
|
|
Usage:
|
|
python -m paperpod probe <video>
|
|
python -m paperpod detect <video> [--out DIR] [--config config.yaml]
|
|
python -m paperpod extract-audio <video> [--out FILE] [--config config.yaml]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from paperpod.capture.audio import extract_audio
|
|
from paperpod.capture.video import probe_video
|
|
from paperpod.config import load_config
|
|
from paperpod.pipeline import run_detection
|
|
|
|
|
|
def _add_config_arg(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument(
|
|
"--config",
|
|
default=None,
|
|
help="Path to config.yaml (defaults to ./config.yaml if present)",
|
|
)
|
|
|
|
|
|
def _resolve_config(arg: str | None):
|
|
if arg is not None:
|
|
return load_config(arg)
|
|
default = Path("config.yaml")
|
|
return load_config(default if default.exists() else None)
|
|
|
|
|
|
def cmd_probe(args: argparse.Namespace) -> int:
|
|
meta = probe_video(args.video)
|
|
print(f"path: {meta.path}")
|
|
print(f"fps: {meta.fps:.3f}")
|
|
print(f"frames: {meta.frame_count}")
|
|
print(f"duration: {meta.duration_s:.2f}s")
|
|
print(f"size: {meta.width}x{meta.height}")
|
|
return 0
|
|
|
|
|
|
def cmd_detect(args: argparse.Namespace) -> int:
|
|
cfg = _resolve_config(args.config)
|
|
out_dir = Path(args.out or Path(cfg.output.dir) / Path(args.video).stem)
|
|
print(f"Analyzing {args.video} -> {out_dir}")
|
|
|
|
report = run_detection(args.video, cfg, out_dir)
|
|
|
|
windows = report["stable_windows"]
|
|
found = [w for w in windows if w["document_found"]]
|
|
print(f"\nVideo: {report['video_meta']['duration_s']}s, "
|
|
f"{len(report['motion_events'])} motion events, "
|
|
f"{len(windows)} stable windows, "
|
|
f"{len(found)} documents detected\n")
|
|
header = f"{'win':>3} {'start':>7} {'end':>7} {'sharp':>8} {'doc':>4} crop"
|
|
print(header)
|
|
print("-" * len(header))
|
|
for w in windows:
|
|
print(
|
|
f"{w['window_id']:>3} "
|
|
f"{w['t_start']:>7.2f} "
|
|
f"{w['t_end']:>7.2f} "
|
|
f"{w.get('sharpness', 0):>8.1f} "
|
|
f"{'yes' if w['document_found'] else 'no':>4} "
|
|
f"{w.get('crop_path', '-')}"
|
|
)
|
|
print(f"\nFull report: {out_dir / 'report.json'}")
|
|
print(f"Motion scores (for threshold tuning): {out_dir / 'motion_scores.csv'}")
|
|
return 0
|
|
|
|
|
|
def cmd_extract_audio(args: argparse.Namespace) -> int:
|
|
cfg = _resolve_config(args.config)
|
|
out = Path(args.out or Path(cfg.output.dir) / (Path(args.video).stem + ".wav"))
|
|
wav = extract_audio(args.video, out, sample_rate=cfg.audio.sample_rate)
|
|
print(f"Audio written to {wav}")
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="paperpod",
|
|
description="Convert an overhead video of documents into cropped, named PDFs.",
|
|
)
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p_probe = sub.add_parser("probe", help="Print video metadata")
|
|
p_probe.add_argument("video")
|
|
p_probe.set_defaults(func=cmd_probe)
|
|
|
|
p_detect = sub.add_parser(
|
|
"detect", help="Detect stable windows and document candidates"
|
|
)
|
|
p_detect.add_argument("video")
|
|
p_detect.add_argument("--out", default=None, help="Output directory")
|
|
_add_config_arg(p_detect)
|
|
p_detect.set_defaults(func=cmd_detect)
|
|
|
|
p_audio = sub.add_parser("extract-audio", help="Extract audio track as WAV")
|
|
p_audio.add_argument("video")
|
|
p_audio.add_argument("--out", default=None, help="Output WAV path")
|
|
_add_config_arg(p_audio)
|
|
p_audio.set_defaults(func=cmd_extract_audio)
|
|
|
|
args = parser.parse_args(argv)
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|