Files
PaperPod/paperpod/cli.py
T
ilia 6a78c84bcd 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
2026-07-07 16:24:46 -04:00

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())