{html.escape(item.source.name)}
+ {confidence} +Recognized text
+{transcription}
+ diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 0d6a983..772b04e 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -21,4 +21,4 @@ jobs:
- run: python -m pip install --upgrade pip
- run: python -m pip install -r requirements-dev.txt
- run: ruff check main.py tests
- - run: pytest -q
+ - run: python -m pytest -q
diff --git a/README.md b/README.md
index 1f4a3a2..d97b939 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,8 @@ I built this because writing by hand is often the fastest way for me to think, b
- preserves the image order as PDF page order
- resizes, denoises, increases contrast, and binarizes each page
- runs local Tesseract OCR without uploading notes to a cloud service
+- calculates average word confidence for each page
+- writes a self-contained visual review report beside the transcription and PDF
- writes a UTF-8 text transcription and a printable multipage PDF
- supports alternate Tesseract languages and page-segmentation modes
- can save processed images for recognition troubleshooting
@@ -40,6 +42,8 @@ resize -> denoise -> contrast enhancement -> adaptive threshold
|
v
local Tesseract OCR
+ |
+ +----> source + OCR confidence review (HTML)
|
+----> UTF-8 transcription
|
@@ -95,9 +99,12 @@ python main.py page-1.jpg page-2.jpg page-3.jpg --output output/class-notes --ti
This creates:
+- `output/class-notes.html` — visual source/transcription review
- `output/class-notes.txt`
- `output/class-notes.pdf`
+Open the HTML report in any browser to compare every original page with the OCR result and average word confidence. The report is self-contained, so it can be moved without breaking its images. Use `--no-review-report` when only text and PDF output are needed.
+
Save the processed images when tuning recognition:
```bash
@@ -114,7 +121,7 @@ ruff check main.py tests
pytest -q
```
-The tests isolate OCR itself so they remain deterministic, while still exercising preprocessing, multipage result handling, text output, and real PDF creation.
+The tests isolate OCR itself so they remain deterministic, while still exercising preprocessing, confidence calculation, the self-contained review report, multipage result handling, text output, and real PDF creation.
## Privacy
diff --git a/main.py b/main.py
index 7525242..2cb02bf 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,11 @@
-"""Convert photographed handwriting into searchable text and a printable PDF."""
+"""Convert photographed handwriting into searchable text and reviewable documents."""
from __future__ import annotations
import argparse
+import base64
+import html
+import mimetypes
import shutil
from dataclasses import dataclass
from pathlib import Path
@@ -20,6 +23,7 @@
class ConversionResult:
source: Path
text: str
+ confidence: float | None = None
processed_image: Path | None = None
@@ -69,7 +73,22 @@ def convert_images(
if not source.is_file():
raise FileNotFoundError(f"Input image does not exist: {source}")
processed = preprocess_image(source)
- text = pytesseract.image_to_string(processed, lang=language, config=config).strip()
+ ocr_data = pytesseract.image_to_data(
+ processed,
+ lang=language,
+ config=config,
+ output_type=pytesseract.Output.DICT,
+ )
+ words = [word.strip() for word in ocr_data["text"] if word.strip()]
+ confidences = [
+ float(value)
+ for value in ocr_data["conf"]
+ if float(value) >= 0
+ ]
+ text = " ".join(words)
+ confidence = (
+ sum(confidences) / len(confidences) if confidences else None
+ )
debug_path = None
if debug_dir:
@@ -77,13 +96,23 @@ def convert_images(
debug_path = debug_dir / f"{index:02d}-{source.stem}-processed.png"
cv2.imwrite(str(debug_path), processed)
- results.append(ConversionResult(source=source, text=text, processed_image=debug_path))
+ results.append(
+ ConversionResult(
+ source=source,
+ text=text,
+ confidence=confidence,
+ processed_image=debug_path,
+ )
+ )
return results
def write_text(results: list[ConversionResult], output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
- sections = [f"# {item.source.name}\n\n{item.text or '[No text detected]'}" for item in results]
+ sections = [
+ f"# {item.source.name}\n\n{item.text or '[No text detected]'}"
+ for item in results
+ ]
output.write_text("\n\n".join(sections) + "\n", encoding="utf-8")
@@ -96,7 +125,7 @@ def write_pdf(results: list[ConversionResult], output: Path, title: str) -> None
[
Paragraph(item.source.name, styles["Heading2"]),
Paragraph(
- (item.text or "[No text detected]").replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
"),
+ html.escape(item.text or "[No text detected]").replace("\n", "
"),
styles["BodyText"],
),
]
@@ -104,15 +133,153 @@ def write_pdf(results: list[ConversionResult], output: Path, title: str) -> None
if index < len(results) - 1:
story.append(PageBreak())
SimpleDocTemplate(
- str(output), pagesize=letter, title=title, author="SimpleCaci",
- rightMargin=0.65 * inch, leftMargin=0.65 * inch,
- topMargin=0.65 * inch, bottomMargin=0.65 * inch,
+ str(output),
+ pagesize=letter,
+ title=title,
+ author="SimpleCaci",
+ rightMargin=0.65 * inch,
+ leftMargin=0.65 * inch,
+ topMargin=0.65 * inch,
+ bottomMargin=0.65 * inch,
).build(story)
+def _image_data_uri(path: Path) -> str:
+ media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
+ encoded = base64.b64encode(path.read_bytes()).decode("ascii")
+ return f"data:{media_type};base64,{encoded}"
+
+
+def write_review_report(
+ results: list[ConversionResult], output: Path, title: str
+) -> None:
+ """Write a self-contained visual OCR review report."""
+ output.parent.mkdir(parents=True, exist_ok=True)
+ cards = []
+ for index, item in enumerate(results, start=1):
+ confidence = (
+ "Confidence unavailable"
+ if item.confidence is None
+ else f"Average OCR confidence {item.confidence:.0f}%"
+ )
+ transcription = html.escape(item.text or "[No text detected]")
+ cards.append(
+ f"""
+ {html.escape(item.source.name)}
+ {confidence}
+
+
Recognized text
+ {transcription}
+
Compare every original page with its recognized text before editing, + sharing, or printing the transcription.
+