Pipeline: extract (pdftotext) -> scan (OCR-defect report + fix template) -> clean (noise/header removal, hyphenation, exact-string/regex OCR repairs, chapter-heading detection) -> voices (sample + comparison) -> chunk (sentence-aware) -> synth (resumable edge-tts) -> stitch (ffmpeg concat + RMS verify) -> chapters (ID3v2.4 CHAP). Config-driven per book; validated end-to-end on a clean digital PDF and on a 546-page scanned book (byte-identical clean output to the reference run, 95 auto-detected chapters).
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
"""Text extraction from PDF (poppler pdftotext)."""
|
|
import os
|
|
import subprocess
|
|
|
|
FFFD = "\ufffd"
|
|
|
|
|
|
def extract(pdf_path: str, out_path: str) -> dict:
|
|
"""Run pdftotext and report basic text-layer statistics.
|
|
|
|
Returns a dict with path, chars, words, fffd_count, and a short
|
|
quality verdict (useful for deciding how much cleanup a book needs).
|
|
"""
|
|
if not os.path.exists(pdf_path):
|
|
raise FileNotFoundError(pdf_path)
|
|
if not os.path.exists(out_path):
|
|
subprocess.run(
|
|
["pdftotext", pdf_path, out_path],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
text = open(out_path, encoding="utf-8").read()
|
|
words = len(text.split())
|
|
fffd = text.count(FFFD)
|
|
if words == 0:
|
|
verdict = "NO TEXT LAYER: the PDF appears to be a pure image scan. " \
|
|
"OCR it first (e.g. ocrmypdf) or the pipeline has nothing to read."
|
|
elif fffd > words * 0.01:
|
|
verdict = "HEAVY OCR DAMAGE: many unreadable glyphs (U+FFFD). " \
|
|
"Run `scan` and build a fix config before synthesizing."
|
|
elif fffd > 0:
|
|
verdict = "MINOR OCR DAMAGE: some unreadable glyphs. " \
|
|
"Run `scan` to see them; a fix config is recommended."
|
|
else:
|
|
verdict = "CLEAN TEXT LAYER: no replacement characters found."
|
|
return {
|
|
"path": out_path,
|
|
"chars": len(text),
|
|
"words": words,
|
|
"fffd": fffd,
|
|
"verdict": verdict,
|
|
}
|