From 2062476252eade2058e014fdf262d339caede411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20H=C3=B6fling?= Date: Tue, 22 Sep 2026 21:24:56 +0200 Subject: [PATCH] feat: Installer fragt OCR-Sprachen und Archiv pro Instanz ab (v0.5.0) - Sprach-Abfrage pro Instanz (Default-Vorschlag deu+eng), bewusst instanz-lokal: ein Hotfolder kann mit "deu" laufen, ein anderer mit "deu+eng+fra". Hinweis im Prompt, dass jede zusaetzliche Sprache Laufzeit und Erkennungsqualitaet kostet. - Jeder Sprachcode wird gegen "tesseract --list-langs" geprueft, fehlende Pakete (tesseract-ocr-) werden zur Installation angeboten; lehnt der User ab oder scheitert apt, wird gewarnt und erneut gefragt. - Abfrage "Original archivieren?" mit $BASE/archive als Default; Archiv ausserhalb von $BASE wird eigens angelegt und gechownt. Ein Pfad auf incoming/outgoing/working/error wird abgewiesen. - sed-Kette der Config-Erzeugung jetzt verankert (^key =) und escaped, setzt zusaetzlich languages, original_on_success und archive_dir; die erzeugte Config wird gegen die Eingabe nachgeprueft. - README und Briefing um "Sprachen pro Instanz" ergaenzt Co-Authored-By: Claude Opus 5 (1M context) --- AI_AGENT_BRIEFING.md | 38 +++++++- CHANGELOG.md | 43 +++++++++ README.md | 39 +++++++- VERSION | 2 +- install.sh | 165 +++++++++++++++++++++++++++++++++- pdf_ocr_hotfolder/__init__.py | 2 +- 6 files changed, 277 insertions(+), 12 deletions(-) diff --git a/AI_AGENT_BRIEFING.md b/AI_AGENT_BRIEFING.md index 15b02fd..a37b41c 100644 --- a/AI_AGENT_BRIEFING.md +++ b/AI_AGENT_BRIEFING.md @@ -1,7 +1,7 @@ # AI Agent Briefing — PDF OCR Hotfolder **Zuletzt aktualisiert:** 2026-09-22 -**Version:** 0.4.1 +**Version:** 0.5.0 **Status:** Multi-Instanz-Betrieb, Preflight-Checks und Fehlerzählung vorhanden, Test-Suite grün (95 pytest-Tests). Ein Produktiv-Einsatz ist im Repo (README/CHANGELOG) nicht dokumentiert — die bisherigen Fixes stammen aus Issues #1–#6, nicht aus einem belegten Dauerbetrieb. ## 🎯 Projektziel @@ -92,9 +92,41 @@ journalctl -u 'pdf-ocr-hotfolder@*' --since today # alle Instanzen, heute - Erster Lauf: Basis-Install + erste Instanz anlegen (Pflicht) - Folgender Lauf: Basis-Install wird übersprungen (erkannt an `venv` + Template-Unit), bestehende Instanzen werden gelistet, weitere Instanzen können ergänzt werden -- Eingaben pro Instanz: Name (`[a-z0-9][a-z0-9-]*`), Basis-Pfad (default `/var/lib/pdf-ocr-hotfolder/`), Service-User +- Eingaben pro Instanz (seit 0.5.0 fünf statt drei): + 1. Name (`[a-z0-9][a-z0-9-]*`) + 2. Basis-Pfad (default `/var/lib/pdf-ocr-hotfolder/`) + 3. Service-User (default `pdfocr`) + 4. **OCR-Sprachen** (default `deu+eng`) — Format `^[a-z]{3}(_[A-Za-z]+)?(\+…)*$`, + bei Unsinn wird erneut gefragt. Jeder Code wird gegen `tesseract --list-langs` + geprüft; fehlt einer, bietet der Installer `tesseract-ocr-` an + (Unterstrich → Bindestrich, `chi_sim` → `tesseract-ocr-chi-sim`). Ablehnung + oder fehlgeschlagene Installation → Warnung, dass OCR mit dieser Sprache + **pro Datei** scheitert, und die Sprach-Abfrage beginnt von vorn (kein + harter Abbruch). Ist `tesseract` nicht aufrufbar, wird die Prüfung + übersprungen und die Eingabe unverändert übernommen. + 5. **Original nach erfolgreichem OCR archivieren?** (default **nein** → + `original_on_success = "delete"`). Bei ja wird der Archiv-Pfad abgefragt + (Vorschlag `$BASE/archive`, absoluter Pfad Pflicht), angelegt und auf + `$SVC_USER:$SVC_GROUP` gechownt — innerhalb von `$BASE` erledigt das + bestehende `chown -R` das schon, nur ein Archiv **außerhalb** bekommt ein + eigenes `chown -R`. +- **Sprachen sind bewusst instanz-lokal**, nicht global: ein Hotfolder + `buchhaltung` läuft mit `deu`, ein Hotfolder `export` mit `deu+eng+fra`. + `LANGS`/`ORIG_MODE`/`ARCHIVE_DIR` sind `local` in `create_instance()` — jeder + Durchlauf fragt neu, `deu+eng` ist nur der vorgeschlagene Default. Die Liste + gehört eng gehalten: jede zusätzliche Sprache kostet Laufzeit **und** + Erkennungsqualität. - Basis-Install prüft zusätzlich die Ghostscript-Version und bietet auf Debian 12 bookworm-backports an; erkennt Container (`systemd-detect-virt --container`) und bietet das LXC-Drop-in an -- `.toml` wird aus `config.example.toml` mit sed-substituierten Pfaden generiert +- `.toml` wird aus `config.example.toml` per `sed` generiert. Substituiert + werden die vier `[paths]`-Zeilen **sowie** (seit 0.5.0) `[ocr].languages`, + `[output].original_on_success` und `[output].archive_dir`. Die Ausdrücke sind + am Zeilenanfang verankert (`^key[[:space:]]*=`), damit die deutschen + Kommentarzeilen über den Keys nicht getroffen werden (im Beispiel steht z.B. + `"archive" : Original wird in archive_dir verschoben` als Kommentar); + Pfad-Variablen laufen vorher durch `sed_escape_repl()` (maskiert `\`, `&`, `|`). + Nach dem sed-Lauf liest `config_value()` die drei Keys zurück und vergleicht + sie mit der Eingabe; erst wenn das passt, nennt die Zusammenfassung Sprachen + und Archiv-Verzeichnis. - Instanz wird sofort `enable --now` gestartet Manuelles Löschen einer Instanz: diff --git a/CHANGELOG.md b/CHANGELOG.md index efd308e..dfe3595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## [0.5.0] - 2026-09-22 + +### Added +- Der Installer weist einen Archiv-Pfad ab, der auf `incoming/`, `outgoing/`, + `working/` oder `error/` der Instanz zeigt — im Eingang wuerde das Original + sonst endlos neu aufgegriffen. +- **`install.sh` fragt beim Anlegen einer Instanz die OCR-Sprachen ab** + (`Tesseract-Sprachen [deu+eng]:`). Die Wahl gilt bewusst **pro Instanz** — + ein Hotfolder `buchhaltung` kann mit `deu` laufen, ein Hotfolder `export` mit + `deu+eng+fra`. Der Installer weist vorher darauf hin, dass jede zusaetzliche + Sprache Laufzeit **und** Erkennungsqualitaet kostet, die Liste also eng + gehalten werden sollte. Das Eingabeformat wird geprueft (Sprachcodes mit `+` + verbunden, `chi_sim` & Co. erlaubt); bei Unsinn wird erneut gefragt statt + abzubrechen. +- **Sprachpakete werden nachinstalliert.** Jeder eingegebene Code wird gegen + `tesseract --list-langs` geprueft. Fehlt eine Sprachdatei, bietet der + Installer das passende apt-Paket an (`tesseract-ocr-`, Unterstrich wird + zum Bindestrich: `chi_sim` → `tesseract-ocr-chi-sim`). Lehnt der User ab oder + laesst sich das Paket nicht installieren, warnt der Installer, dass OCR mit + dieser Sprache **bei jeder Datei** scheitern wuerde, und fragt die Sprachen + erneut ab — so kann die Sprache einfach wieder rausgeworfen werden. Ist + `tesseract` nicht aufrufbar, wird die Pruefung uebersprungen und die Eingabe + unveraendert uebernommen. +- **Abfrage `Original nach erfolgreichem OCR archivieren? [j/N]:`** — Default + nein, also weiterhin `original_on_success = "delete"`. Bei ja wird der + Archiv-Pfad abgefragt (Vorschlag `/archive`), angelegt und auf den + Service-User gechownt; ein Archiv ausserhalb des Instanz-Basis-Pfads bekommt + ein eigenes `chown -R`. + +### Changed +- Die Instanz-Config wird weiterhin per `sed` aus `config.example.toml` + erzeugt, substituiert jetzt aber zusaetzlich `[ocr].languages`, + `[output].original_on_success` und `[output].archive_dir` — bisher waren das + die Beispiel-Defaults, `archive_dir` musste von Hand nachgetragen werden. + Die Ausdruecke sind am Zeilenanfang verankert (`^key[[:space:]]*=`), damit die + deutschen Kommentarzeilen ueber den Keys unangetastet bleiben, und + Pfad-Variablen laufen durch `sed_escape_repl()` (maskiert `\`, `&`, `|`) — + Pfade mit Sonderzeichen landen damit korrekt in der Config. +- Nach dem sed-Lauf liest der Installer die drei Keys aus der erzeugten Config + zurueck und vergleicht sie mit der Eingabe. Erst wenn das passt, nennt die + Abschluss-Zusammenfassung zusaetzlich die gewaehlten **Sprachen** und (bei + Archivierung) das **Archiv-Verzeichnis**; sonst gibt es eine Warnung. + ## [0.4.1] - 2026-09-22 ### Fixed diff --git a/README.md b/README.md index 5b89ac0..15e43dd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,14 @@ sudo ./install.sh Der Installer: 1. Installiert einmalig Code + venv + systemd-Template-Unit -2. Fragt nach Instanz-Name, Basis-Pfad, Service-User +2. Fragt **pro Instanz** ab: + - Instanz-Name + - Basis-Pfad für die Daten + - Service-User + - **OCR-Sprachen** (Tesseract, Default `deu+eng`) — fehlende Sprachpakete + (`tesseract-ocr-`) werden erkannt und auf Wunsch nachinstalliert + - **Original nach erfolgreichem OCR archivieren?** (Default nein = löschen; + bei ja zusätzlich der Archiv-Pfad, vorgeschlagen `/archive`) 3. Legt so viele Hotfolder-Instanzen an, wie du willst (`Weitere Instanz anlegen? [j/N]`) Bei jedem erneuten Aufruf erkennt der Installer bestehende Instanzen und fragt nur nach neuen. @@ -47,6 +54,27 @@ Das Tool arbeitet komplett **instanzbasiert** über eine systemd Template-Unit ` - eigene Datenverzeichnisse: `/var/lib/pdf-ocr-hotfolder//{incoming,working,outgoing,error}/` - eigene systemd-Unit: `pdf-ocr-hotfolder@.service` - optional eigenen Service-User (via Drop-in `/etc/systemd/system/pdf-ocr-hotfolder@.service.d/user.conf`) +- **eigene OCR-Sprachen und eigene Original-Behandlung** (löschen oder archivieren) + +### Sprachen pro Instanz + +Die Tesseract-Sprachen werden bewusst **je Instanz** abgefragt, nicht global: +Hotfolder haben unterschiedliche Post. Ein Buchhaltungs-Hotfolder sieht nur +deutsche Belege, ein Export-Hotfolder internationale Korrespondenz: + +```toml +# /etc/pdf-ocr-hotfolder/buchhaltung.toml +languages = "deu" + +# /etc/pdf-ocr-hotfolder/export.toml +languages = "deu+eng+fra" +``` + +**Die Liste so eng wie möglich halten.** Jede zusätzliche Sprache kostet +Laufzeit *und* Erkennungsqualität: Tesseract muss mehr Modelle gegeneinander +abwägen und verwechselt dabei Wörter, die in der einen Sprache eindeutig wären. +`deu+eng+fra` auf reinen Deutsch-Scans ist also kein Sicherheitsnetz, sondern +ein Rückschritt. Beispiel für 3 Hotfolder: @@ -77,9 +105,13 @@ Manuell eine weitere Instanz anlegen geht auch — einfach `install.sh` erneut s Vollständiges Beispiel: [`config.example.toml`](config.example.toml). Wichtigste Sektionen: +Der Installer fragt `[ocr].languages`, `[output].original_on_success` und +`[output].archive_dir` pro Instanz ab und schreibt sie direkt in die +Instanz-Config — die Werte unten sind nur die Beispiel-Defaults. + ### `[ocr]` ```toml -languages = "deu+eng" # Tesseract-Sprachen +languages = "deu+eng" # Tesseract-Sprachen (Installer fragt pro Instanz) jobs = 4 # Threads pro PDF skip_text = true # bereits OCR-haltige Seiten überspringen pdfa_level = "" # "1", "2", "3" oder "" für reines PDF (Default "" wegen Ghostscript-Bug, s.u.) @@ -100,6 +132,7 @@ name_tag = "OCR_" # Nach erfolgreichem OCR mit dem Original: # "delete" → löschen # "archive" → in archive_dir verschieben +# Beides fragt der Installer beim Anlegen der Instanz ab: original_on_success = "delete" archive_dir = "" # absoluter Pfad, Pflicht bei "archive" ``` @@ -248,5 +281,5 @@ MIT — © Sonith UG --- -**Version:** 0.4.1 +**Version:** 0.5.0 **Repo:** https://gitea.sonith.de/sonith_ug/pdf-ocr-hotfolder diff --git a/VERSION b/VERSION index 267577d..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.1 +0.5.0 diff --git a/install.sh b/install.sh index 5f2cc3b..5f2271e 100755 --- a/install.sh +++ b/install.sh @@ -169,6 +169,66 @@ show_existing_instances() { echo } +# Liest den Wert eines Keys (erste Zuweisung am Zeilenanfang) aus einer Config +config_value() { + local file="$1" key="$2" + sed -n "s|^${key}[[:space:]]*=[[:space:]]*\"\(.*\)\"[[:space:]]*$|\1|p" "$file" | head -n1 +} + +# Maskiert Sonderzeichen, damit ein Pfad gefahrlos in eine sed-Ersetzung darf +# (Trennzeichen '|', Rueckverweis '&', Backslash). +sed_escape_repl() { + printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' +} + +# Prueft jeden Tesseract-Sprachcode gegen die installierten Sprachdateien und +# bietet fehlende Pakete zur Installation an. +# Rueckgabe: 0 = alle Sprachen verfuegbar (oder Pruefung nicht moeglich), +# 1 = mindestens eine Sprache fehlt weiterhin. +ensure_tesseract_langs() { + local langs="$1" + local raw installed code pkg answer rc=0 + local -a codes + + if ! command -v tesseract >/dev/null 2>&1; then + log_warn "tesseract ist nicht aufrufbar — Sprachpruefung wird uebersprungen." + log_warn "Eingabe '$langs' wird unveraendert uebernommen." + return 0 + fi + if ! raw="$(tesseract --list-langs 2>/dev/null)"; then + log_warn "'tesseract --list-langs' schlug fehl — Sprachpruefung wird uebersprungen." + log_warn "Eingabe '$langs' wird unveraendert uebernommen." + return 0 + fi + installed="$(printf '%s\n' "$raw" | grep -vi '^List of available' || true)" + + IFS='+' read -r -a codes <<< "$langs" + for code in "${codes[@]}"; do + [ -n "$code" ] || continue + if printf '%s\n' "$installed" | grep -qxF "$code"; then + log_info "Sprache '$code' ist installiert ✓" + continue + fi + pkg="tesseract-ocr-${code//_/-}" + log_warn "Sprache '$code' ist nicht installiert (Paket: $pkg)." + read -r -p "Paket '$pkg' jetzt installieren? [J/n]: " answer + answer="${answer:-J}" + if [[ "$answer" =~ ^[JjYy]$ ]]; then + if ! apt-get install -y --no-install-recommends "$pkg"; then + log_error "Paket '$pkg' liess sich nicht installieren." + elif tesseract --list-langs 2>/dev/null | grep -qxF "$code"; then + log_info "Paket '$pkg' installiert ✓" + continue + else + log_error "Paket '$pkg' ist da, aber tesseract kennt '$code' weiterhin nicht." + fi + fi + log_warn "Ohne die Sprachdatei '$code' scheitert das OCR bei JEDER Datei." + rc=1 + done + return $rc +} + create_instance() { echo read -r -p "Instanz-Name (nur a-z, 0-9, -): " INST @@ -206,20 +266,111 @@ create_instance() { fi fi + # --- OCR-Sprachen --- + echo + log_info "Tesseract-Sprachen — gelten NUR fuer diese Instanz '$INST'." + log_info "Jede zusaetzliche Sprache kostet Laufzeit und verschlechtert zugleich" + log_info "die Erkennung — also so eng wie moeglich waehlen (z.B. nur 'deu')." + local LANGS + while true; do + read -r -p "Tesseract-Sprachen [deu+eng]: " LANGS + LANGS="${LANGS:-deu+eng}" + if [[ ! "$LANGS" =~ ^[a-z]{3}(_[A-Za-z]+)?(\+[a-z]{3}(_[A-Za-z]+)?)*$ ]]; then + log_error "Ungueltiges Format. Erwartet: Sprachcodes mit '+' verbunden," + log_error "z.B. 'deu', 'deu+eng' oder 'chi_sim+eng'." + continue + fi + if ensure_tesseract_langs "$LANGS"; then + break + fi + log_warn "Bitte Sprachen erneut angeben (fehlende Sprache einfach weglassen)." + echo + done + + # --- Original archivieren? --- + echo + local ORIG_MODE="delete" + local ARCHIVE_DIR="" + local ARCHIVE_ANS + read -r -p "Original nach erfolgreichem OCR archivieren? [j/N]: " ARCHIVE_ANS + ARCHIVE_ANS="${ARCHIVE_ANS:-N}" + if [[ "$ARCHIVE_ANS" =~ ^[JjYy]$ ]]; then + ORIG_MODE="archive" + local default_archive="$BASE/archive" + while true; do + read -r -p "Archiv-Verzeichnis [$default_archive]: " ARCHIVE_DIR + ARCHIVE_DIR="${ARCHIVE_DIR:-$default_archive}" + if [[ "$ARCHIVE_DIR" != /* ]]; then + log_error "Bitte einen absoluten Pfad angeben (beginnt mit '/')." + continue + fi + # Das Archiv darf keines der Arbeitsverzeichnisse sein: im Eingang + # wuerde das Original endlos neu aufgegriffen, in den uebrigen + # kollidiert es mit der Verarbeitung. + case "${ARCHIVE_DIR%/}" in + "$BASE/incoming"|"$BASE/outgoing"|"$BASE/working"|"$BASE/error") + log_error "Das Archiv darf nicht incoming/outgoing/working/error sein." + continue + ;; + esac + break + done + else + log_info "Original wird nach erfolgreichem OCR geloescht (original_on_success = \"delete\")." + fi + log_info "Lege Datenverzeichnisse unter $BASE an..." mkdir -p "$BASE"/{incoming,outgoing,working,error} + if [ -n "$ARCHIVE_DIR" ]; then + mkdir -p "$ARCHIVE_DIR" + fi chown -R "$SVC_USER":"$SVC_GROUP" "$BASE" + # Innerhalb von $BASE erledigt das chown -R oben schon alles; nur ein Archiv + # ausserhalb braucht eigenes mkdir/chown. + if [ -n "$ARCHIVE_DIR" ] && [[ "$ARCHIVE_DIR" != "$BASE"/* ]] && [ "$ARCHIVE_DIR" != "$BASE" ]; then + chown -R "$SVC_USER":"$SVC_GROUP" "$ARCHIVE_DIR" + log_info "Archiv-Verzeichnis $ARCHIVE_DIR angelegt (liegt ausserhalb von $BASE)" + fi log_info "Erstelle Config $CONFIG_DIR/$INST.toml..." + # Verankerte Ausdruecke (Zeilenanfang + Key + '='), damit die deutschen + # Kommentarzeilen ueber den Keys unangetastet bleiben. + local ESC_BASE ESC_ARCHIVE ESC_LANGS + ESC_BASE="$(sed_escape_repl "$BASE")" + ESC_ARCHIVE="$(sed_escape_repl "$ARCHIVE_DIR")" + ESC_LANGS="$(sed_escape_repl "$LANGS")" sed \ - -e "s|/var/lib/pdf-ocr-hotfolder/incoming|$BASE/incoming|" \ - -e "s|/var/lib/pdf-ocr-hotfolder/outgoing|$BASE/outgoing|" \ - -e "s|/var/lib/pdf-ocr-hotfolder/working|$BASE/working|" \ - -e "s|/var/lib/pdf-ocr-hotfolder/error|$BASE/error|" \ + -e "s|^incoming[[:space:]]*=.*|incoming = \"$ESC_BASE/incoming\"|" \ + -e "s|^outgoing[[:space:]]*=.*|outgoing = \"$ESC_BASE/outgoing\"|" \ + -e "s|^working[[:space:]]*=.*|working = \"$ESC_BASE/working\"|" \ + -e "s|^error[[:space:]]*=.*|error = \"$ESC_BASE/error\"|" \ + -e "s|^languages[[:space:]]*=.*|languages = \"$ESC_LANGS\"|" \ + -e "s|^original_on_success[[:space:]]*=.*|original_on_success = \"$ORIG_MODE\"|" \ + -e "s|^archive_dir[[:space:]]*=.*|archive_dir = \"$ESC_ARCHIVE\"|" \ "$INSTALL_DIR/config.example.toml" > "$CONFIG_DIR/$INST.toml" chown root:"$SVC_GROUP" "$CONFIG_DIR/$INST.toml" chmod 640 "$CONFIG_DIR/$INST.toml" + # Erzeugte Config gegenpruefen: tragen die drei Keys wirklich die Auswahl? + local CFG_OK=1 got key want + for key in languages original_on_success archive_dir; do + case "$key" in + languages) want="$LANGS" ;; + original_on_success) want="$ORIG_MODE" ;; + archive_dir) want="$ARCHIVE_DIR" ;; + esac + got="$(config_value "$CONFIG_DIR/$INST.toml" "$key")" + if [ "$got" != "$want" ]; then + log_error "Config-Pruefung: $key ist \"$got\", erwartet \"$want\"" + CFG_OK=0 + fi + done + if [ "$CFG_OK" -eq 1 ]; then + log_info "Config-Pruefung ok ✓ (languages / original_on_success / archive_dir)" + else + log_warn "Bitte $CONFIG_DIR/$INST.toml von Hand nachziehen." + fi + # Drop-in für abweichenden Service-User if [ "$SVC_USER" != "$DEFAULT_USER" ]; then local DROPIN_DIR="/etc/systemd/system/pdf-ocr-hotfolder@${INST}.service.d" @@ -246,6 +397,12 @@ EOF echo " Eingang: $BASE/incoming" echo " Ausgang: $BASE/outgoing" echo " User: $SVC_USER ($SVC_GROUP)" + if [ "$CFG_OK" -eq 1 ]; then + echo " Sprachen: $LANGS" + if [ "$ORIG_MODE" = "archive" ]; then + echo " Archiv: $ARCHIVE_DIR" + fi + fi echo } diff --git a/pdf_ocr_hotfolder/__init__.py b/pdf_ocr_hotfolder/__init__.py index 13c2e91..ce5b42f 100644 --- a/pdf_ocr_hotfolder/__init__.py +++ b/pdf_ocr_hotfolder/__init__.py @@ -1,3 +1,3 @@ """PDF OCR Hotfolder — Scanner-PDFs automatisch durchsuchbar machen.""" -__version__ = "0.4.1" +__version__ = "0.5.0"