File size: 6,124 Bytes
8c3e275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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()


@app.command()
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")


@app.command()
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']}")


@app.command()
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')})")


@app.command()
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())


@app.command()
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)


@app.command()
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")


@app.command()
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()