Spaces:
Build error
Build error
| from __future__ import annotations | |
| import hashlib | |
| from pathlib import Path | |
| import typer | |
| from pageparse.config import settings | |
| from pageparse.extract import Extractor | |
| from pageparse.ingest import IMAGE_EXTENSIONS, load_image | |
| from pageparse.ocr.handwriting import HandwritingOCR | |
| from pageparse.ocr.printed import PrintedOCR | |
| from pageparse.preprocess import preprocess | |
| from pageparse.store import Store | |
| app = typer.Typer() | |
| store = Store() | |
| def process( | |
| path: str = typer.Argument(..., help="Path to image or directory"), | |
| batch: bool = typer.Option(False, "--batch", help="Process all images in directory"), | |
| airgap: bool = typer.Option(False, "--airgap", help="Fail if any network call attempted"), | |
| parallel: bool = typer.Option(False, "--parallel", help="Process files in parallel"), | |
| schema: str = typer.Option("auto", "--schema", help="Schema type (auto, todo, meeting, recipe)"), | |
| diff_sync: bool = typer.Option(True, "--diff-sync/--no-diff-sync", help="Skip duplicate files"), | |
| ) -> None: | |
| if airgap: | |
| settings.airgap = True | |
| settings.auto_schema = schema == "auto" | |
| settings.diff_sync_enabled = diff_sync | |
| paths = [Path(path)] | |
| if batch and Path(path).is_dir(): | |
| paths = [] | |
| for ext in IMAGE_EXTENSIONS: | |
| paths.extend(Path(path).glob(f"*{ext}")) | |
| paths.extend(Path(path).glob(f"*{ext.upper()}")) | |
| store.init_db() | |
| if parallel and len(paths) > 1: | |
| from pageparse import pipelines | |
| typer.echo(f"Processing {len(paths)} files in parallel ({settings.max_workers} workers)...") | |
| results = pipelines.process_batch(paths) | |
| for r in results: | |
| if "error" in r: | |
| typer.echo(f" FAILED: {Path(r['path']).name} — {r['error']}") | |
| else: | |
| typer.echo(f" Processed: {Path(r['path']).name} — {len(r.get('raw_text', ''))} chars") | |
| return | |
| ocr = HandwritingOCR() | |
| printed_ocr = PrintedOCR() | |
| extractor = Extractor() | |
| store.init_db() | |
| for p in paths: | |
| typer.echo(f"Processing: {p.name}") | |
| content_bytes = p.read_bytes() | |
| content_hash = hashlib.sha256(content_bytes).hexdigest() | |
| if diff_sync: | |
| existing = store.get_source_by_hash(content_hash) | |
| if existing: | |
| typer.echo(f" Skipped (duplicate): {p.name}") | |
| continue | |
| img = load_image(p) | |
| cleaned = preprocess(img) | |
| raw_text = ocr.recognize(cleaned) | |
| if not raw_text.strip(): | |
| raw_text = printed_ocr.recognize(cleaned) | |
| if schema == "auto": | |
| schema = extractor.detect_schema_type(raw_text) | |
| result = extractor.extract(raw_text, p.name, schema) | |
| source_id = store.save(result, raw_text, content_hash=content_hash) | |
| typer.echo(f" Saved source #{source_id} with {len(result.records)} records") | |
| def list( | |
| priority: str | None = typer.Option(None, "--priority", help="Filter by priority"), | |
| type: str | None = typer.Option(None, "--type", help="Filter by record type"), | |
| ) -> None: | |
| records = store.get_records(priority=priority, type=type) | |
| for r in records: | |
| prio = r["priority"] or "None" | |
| typer.echo(f" [{r['type']}] [{prio}] {r['content']} — {r['filename']}") | |
| def search( | |
| query: str = typer.Argument(..., help="Search query"), | |
| ) -> None: | |
| from pageparse.search import SemanticSearch | |
| searcher = SemanticSearch() | |
| results = searcher.search(query) | |
| for r in results: | |
| typer.echo(f" [{r['type']}] {r['content']} (confidence: {r.get('confidence', 'N/A')})") | |
| def export( | |
| format: str = typer.Option("json", "--format", help="Export format (json or csv)"), | |
| ) -> None: | |
| import csv | |
| import io | |
| import json | |
| sources = store.list_sources() | |
| for s in sources: | |
| s["records"] = store.get_records(source_id=s["id"]) | |
| if format == "json": | |
| typer.echo(json.dumps(sources, indent=2)) | |
| elif format == "csv": | |
| records = store.get_records() | |
| if not records: | |
| typer.echo("No records to export.") | |
| return | |
| output = io.StringIO() | |
| writer = csv.DictWriter(output, fieldnames=records[0].keys()) | |
| writer.writeheader() | |
| writer.writerows(records) | |
| typer.echo(output.getvalue().rstrip()) | |
| def serve( | |
| host: str = "127.0.0.1", | |
| port: int = 8000, | |
| ) -> None: | |
| import os | |
| import uvicorn | |
| env_port = os.environ.get("PORT") | |
| if env_port: | |
| try: | |
| port = int(env_port) | |
| except ValueError: | |
| pass | |
| uvicorn.run("pageparse.web:app", host=host, port=port, reload=False) | |
| def benchmark( | |
| path: str = typer.Argument(..., help="Path to image or directory"), | |
| iterations: int = typer.Option(3, "--iterations", "-n", help="Number of iterations"), | |
| ) -> None: | |
| import time | |
| from pageparse.ocr.handwriting import HandwritingOCR | |
| from pageparse.ocr.printed import PrintedOCR | |
| from pageparse.preprocess import preprocess | |
| typer.echo(f"Benchmarking: {path} ({iterations} iterations)") | |
| img = load_image(path) | |
| ocr = HandwritingOCR() | |
| printed_ocr = PrintedOCR() | |
| times = {"preprocess": [], "handwriting_ocr": [], "printed_ocr": []} | |
| for i in range(iterations): | |
| t0 = time.time() | |
| cleaned = preprocess(img) | |
| times["preprocess"].append(time.time() - t0) | |
| t0 = time.time() | |
| ocr.recognize(cleaned) | |
| times["handwriting_ocr"].append(time.time() - t0) | |
| t0 = time.time() | |
| printed_ocr.recognize(cleaned) | |
| times["printed_ocr"].append(time.time() - t0) | |
| for stage, durations in times.items(): | |
| avg = sum(durations) / len(durations) | |
| typer.echo(f" {stage}: avg={avg:.3f}s, min={min(durations):.3f}s, max={max(durations):.3f}s") | |
| def stats() -> None: | |
| store = Store() | |
| stats = store.get_stats() | |
| typer.echo(f"Sources: {stats['sources']}") | |
| typer.echo(f"Records: {stats['records']}") | |
| if __name__ == "__main__": | |
| app() | |