--keep-lines für Gedichtbände, OCR-Neuerkennung repariert, OCR-Rauschen entfernen

- --ocr always / schiefe OCR-Ebenen: alte unsichtbare Textebene vor der
  OCR entfernen (nur in der Kopie im Speicher); vorher blieb der alte
  Text erhalten bzw. stand doppelt da ("bliht" statt "blüht")
- --keep-lines: jede PDF-Zeile bleibt eine Zeile, Strophen als Absätze,
  Einrückungen bleiben, nichts wird zusammengefügt (auch keine Trennung)
- OCR-Text selbst erkennen (unsichtbar, wie pymupdf4llm 1.28 intern);
  Buchkante "|" entfernen, Rauschzeilen und Fetzen neben dem Textbereich
  verwerfen, Zeilen mit verschiedenen Grundlinien trennen, Überschriften
  ohne Buchstaben/Ziffern verwerfen
- Fraktur: frk+deu dokumentiert, Grenzen in TODO
- Tests: Scan mit falscher OCR-Ebene, Gedicht in schmaler Spalte,
  OCR-Rauschen; README, Bedienungsanleitung, TODO

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-02 22:13:20 +02:00
commit 3e5195efc2

View file

@ -204,3 +204,84 @@ def test_single_code_like_register_entry_stays_text(tmp_path):
])
assert "```" not in md
assert "@classmethod, 190\\" in md
# --- --keep-lines (Gedichtbände) ----------------------------------------------------
POEM = [
"O wie blüht mein Leib aus jeder Ader",
"duftender, seitdem ich dich erkenn;",
"sieh, ich gehe schlanker und gerader,",
"und du wartest nur: wer bist du denn?",
None, # Strophenabstand
"Sieh: ich fühle, wie ich mich entferne,",
"wie ich Altes, Blatt um Blatt, verlier. Lei-",
"be und Seele sind wie lauter Sterne.",
]
def make_poem_pdf(path):
"""Schmale Seite, Verse füllen die Zeile fast (wie im Rilke-Band)."""
doc = pymupdf.open()
page = doc.new_page(width=272, height=400)
y = 60
for line in POEM:
if line is None:
y += 14
continue
page.insert_text((44, y), line, fontname="tiro", fontsize=10)
y += 13
doc.save(path)
return path
def test_poem_without_keep_lines_becomes_prose(tmp_path):
out = tmp_path / "gedicht.md"
run(make_poem_pdf(tmp_path / "gedicht.pdf"), out)
assert "aus jeder Ader duftender" in out.read_text(encoding="utf-8")
def test_keep_lines_keeps_every_line_and_stanza(tmp_path):
out = tmp_path / "gedicht.md"
run(make_poem_pdf(tmp_path / "gedicht.pdf"), out, "--keep-lines")
md = out.read_text(encoding="utf-8")
assert (
"O wie blüht mein Leib aus jeder Ader\\\n"
"duftender, seitdem ich dich erkenn;\\\n"
"sieh, ich gehe schlanker und gerader,\\\n"
"und du wartest nur: wer bist du denn?\n\n"
"Sieh: ich fühle, wie ich mich entferne,\\\n"
"wie ich Altes, Blatt um Blatt, verlier. Lei-\\\n"
"be und Seele sind wie lauter Sterne."
) in md
assert "```" not in md # Zeilen auf ";" sind kein Code
def test_ocr_noise_outside_text_area_is_dropped():
line = {"bbox": (0, 10, 12, 20), "spans": [{"text": "aoe", "size": 10}]}
p._page_text[0] = (44, 212)
try:
assert p._outside_text_area([line], 10)
assert not p._outside_text_area([{**line, "bbox": (44, 10, 200, 20)}], 10)
finally:
p._page_text[0] = None
def _ocr_span(text, x, y, size=10):
return {"text": text, "size": size, "flags": 0, "char_flags": 0, "font": "GlyphLessFont",
"origin": (x, y), "bbox": (x, y - size, x + 6 * len(text), y)}
def test_clean_ocr_lines_removes_edge_noise_and_splits_baselines():
lines = [
{"bbox": (0, 0, 400, 20), "spans": [_ocr_span("||", 10, 20), _ocr_span("Und", 60, 20),
_ocr_span("Don", 62, 36), _ocr_span("tauchte", 90, 20),
_ocr_span("|", 390, 20)]},
{"bbox": (0, 40, 30, 50), "spans": [_ocr_span("„+|", 5, 50)]}, # nur Rauschen
]
cleaned = p._clean_ocr_lines(lines)
assert ["".join(s["text"] for s in line["spans"]) for line in cleaned] == ["Undtauchte", "Don"]
def test_heading_without_letters_is_dropped():
assert p.normalize_markdown("# ||\n\nText.\n") == "Text.\n"