Metadata never fails a drop: deterministic title/author detection (metadata, first-page text, OCR of scanned first pages, file name), sources recorded in book.json; decrypt scratch copy before OCR

This commit is contained in:
Audiobook Producer
2026-10-18 13:25:01 -05:00
parent be069d3c22
commit b72c0464ef
6 changed files with 583 additions and 61 deletions
+13 -33
View File
@@ -27,6 +27,7 @@ import acx_check
import assemble
import bookio
import ingest
import meta
import settings
import voice
@@ -59,12 +60,7 @@ def sidecar(pdf):
def guess_from_filename(pdf):
"""'The Title - by Glenn Clark (Extra).pdf' or 'Title by Glenn Clark.pdf' -> (title, author), else (None, None)."""
stem = os.path.splitext(os.path.basename(pdf))[0]
m = re.match(r"^(?P<title>.+?)[\s\-–—]+by\s+(?P<author>[^()]+?)\s*(\(.*\))?\s*$", stem, re.I)
if m:
return m.group("title").strip(" -–—"), m.group("author").strip()
return None, None
return meta.guess_from_filename(pdf)
def voice_fingerprint(cfg, script_path):
@@ -112,24 +108,9 @@ def step(name):
print("\n=== %s ===" % name, flush=True)
def resolve_meta(pdf, title=None, author=None):
"""(title, author, note). Order: explicit arguments, the .json next to the PDF, the PDF itself, the file name.
Raises Failed, telling the operator what to add, if none of them works."""
meta = sidecar(pdf)
title, author = title or meta.get("title"), author or meta.get("author")
try:
title, author = ingest.detect(pdf, title, author)
except Exception as e:
raise Failed("could not read the PDF: %s" % e)
note = None
if not (title and author):
t, a = guess_from_filename(pdf)
title, author = title or t, author or a
note = "title/author not found in the PDF; taking them from the file name: '%s' by '%s'" % (title, author)
if not (title and author):
raise Failed("ingest: could not find title and author in the PDF or its file name. Put a file with the same name and "
'.json next to the PDF: {"title": "...", "author": "..."}')
return title, author, note
def resolve_meta(cfg, pdf, title=None, author=None, name_hints=None):
"""Title and author with their sources (see meta.py). Never fails: gaps are filled and logged."""
return meta.resolve(cfg, pdf, title, author, sidecar(pdf), name_hints)
def check_ocr(cfg, book_dir):
@@ -158,19 +139,17 @@ def check_ocr(cfg, book_dir):
"OCR found no text (blank page or picture only)")])
def run_text_stages(cfg, pdf, title, author):
def run_text_stages(cfg, pdf, title, author, source_url=None, name_hints=None):
"""Ingest through license for every chapter. Returns (book_dir, book)."""
import cli
title, author, note = resolve_meta(pdf, title, author)
step("metadata")
m = resolve_meta(cfg, pdf, title, author, name_hints)
print("\n".join(m["log"]))
step("ingest")
if note:
print(note)
try:
book_dir = ingest.run(cfg, pdf, title, author)
book_dir = ingest.run(cfg, pdf, meta=m, source_url=source_url)
except SystemExit as e:
raise Failed("ingest: %s" % e)
if note:
bookio.write_warnings(book_dir, "ingest-metadata", [("metadata-from-filename", os.path.basename(pdf), "title '%s', author '%s'" % (title, author))])
print("ingest done ->", book_dir)
check_ocr(cfg, book_dir)
for name in ("structure",):
@@ -238,7 +217,8 @@ def run_audio_stages(cfg, book_dir, book, force):
return rendered
def process(cfg, pdf, dry_run=False, force=False, title=None, author=None, compare=None, result_file=None, compare_strict=False):
def process(cfg, pdf, dry_run=False, force=False, title=None, author=None, compare=None, result_file=None, compare_strict=False,
source_url=None, name_hints=None):
"""Returns 0 on success, 1 on failure (message printed)."""
pdf = os.path.abspath(pdf)
if not os.path.isfile(pdf):
@@ -251,7 +231,7 @@ def process(cfg, pdf, dry_run=False, force=False, title=None, author=None, compa
rendered = False
try:
print("process %s%s" % (os.path.basename(pdf), " [DRY RUN: text stages only]" if dry_run else ""))
book_dir, book = run_text_stages(cfg, pdf, title, author)
book_dir, book = run_text_stages(cfg, pdf, title, author, source_url, name_hints)
if compare:
old = os.path.join(compare, os.path.relpath(book_dir, settings.resolve(cfg, "books_dir")))
step("compare with the existing book folder %s" % old)