Compare commits

...
10 Commits
Author SHA1 Message Date
Michael StanglandClaude Sonnet 5 6757996704 feat: bin/build_html - HTML-Build-Wrapper analog zu build_pdf
Bisher gab es nur bin/build_pdf.* fuer den PDF-Build; der HTML-Build lief
nur als manueller sphinx-build-Aufruf ohne Wrapper-Skript.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:25:17 +02:00
Michael StanglandClaude Sonnet 5 982879d9f0 fix: Ueberschriften-Erkennung, Bild-Referenzierung und Symbol-Qualitaet
Behebt mehrere Extraktionsprobleme in lib/pdf_extract.py, die zu doppelter
Nummerierung, sinnentleertem Text und falsch dimensionierten Symbolen fuehrten:

- Ueberschriften nur noch bei grosser/fetter Quellschrift (>= 12.5pt oder bold)
  statt bei jeder Zeile mit Nummern-Praefix. Vorher wurden hunderte normale
  Textzeilen (Querverweise, Aufzaehlungen) faelschlich zu Ueberschriften.
- Bildunterschriften ("Abbildung N: ...") werden nie mehr als Ueberschrift
  gerendert (Caption-Erkennung vor Heading-Check, Merge-Regel verschaerft),
  sondern als kursiver Absatz bzw. figure-Caption.
- Symbole (Warn-/Gebots-/Verbotspiktogramme) werden per Piktogramm-Heuristik
  (Signalfarben + wenige Flaechenfarben) erkannt statt nur ueber Haeufigkeit -
  findet auch Symbole, die im Dokument nur 1x vorkommen (z.B. Warndreiecke).
  Auf quadratische Canvas normalisiert (normalize_symbol) und einheitlich
  klein referenziert (:height: 10mm, :align: left), damit alle Symbole
  unabhaengig vom Seitenverhaeltnis der Quelle gleich gross erscheinen.
- Junk-Erkennung erweitert: gerenderte Kapitelnummern-Bilder, Barcodes und
  Foto-Ausschnitte werden nicht mehr als Symbol/Inhaltsbild extrahiert.
- Manuelle SYMBOL_BLOCKLIST (aHash-basiert) fuer sichtgepruefte Fehlklassi-
  fikationen (Foto-/3D-/Diagramm-Fragmente), damit sie bei erneuter
  Konvertierung nicht wieder auftauchen.
- Verzeichnislisten (Lieferantendoku etc.) werden im LaTeX-Build quer
  (landscape via pdflscape) gesetzt, da die langen Quellpfade in Hochformat
  nicht passten.

assets/shared/symbols/: auf normalisierte .png umgestellt, 7 Fehlklassi-
fikationen (Fotos/3D-Renderings/Diagramme statt Piktogramme) entfernt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:23:29 +02:00
Michael StanglandClaude Opus 4.8 3ebfe06139 feat: Bild-Routing, Logo/Symbol/Fragment-Filter, Produktprogramm-Bilder
Bild-Verarbeitung der Anleitungen (lib/pdf_extract.py) erweitert:

- Routing nach Kapitel-Typ: generelle Kapitel (Steuerung) -> assets/shared/
  images/<subdir>, projektbezogene (Betrieb/Wartung) -> projekte/fortna/images.
  Fotos (.jpg) IMMER projektspezifisch, unabhaengig vom Kapitel.
- Benennung aus Bildunterschrift: <Stichwort>_NN.png (z.B. Anlage_Uebersicht_01);
  Fotos img_NNN.jpg, SW-Skizzen >10KB sketch_NNN.png, Rest img_NNN.png.
- Logos: nur wiederkehrende (>=3x) breite Randgrafiken gelten als Logo -> alle
  Aufloesungen nach assets/shared/logos/logo_<n>_<breite>px, nicht inline.
- Wertlose Fragmente (flach <=55px, breit, SW = gerenderte Kapitelnummern wie
  "5.2.6") werden verworfen.
- Ziel-Bildordner werden vor jedem Lauf geleert.

extract_products.py: Produktprogramm-Bilder -> assets/shared/images/
produktprogramm/{omniflo,ils,cpc,geruest,trolley} (generelle Produktbilder).

.gitignore: extrahierte Bildmassen ausgeschlossen, nur kuratierte symbols/ +
logos/ versioniert.

Voller PDF-Build sauber (lualatex exit 0, 589 Seiten, 638 Bild-Refs, 0 fehlend).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 14:35:19 +02:00
Michael StanglandClaude Opus 4.8 bbfe17ec37 feat: Symbole zentralisieren + Fragment-Ueberschriften filtern
Perceptual-Hash-basierte Symbol-/Duplikaterkennung in lib/pdf_extract.py:

- average_hash() (aHash) + Hamming-Distanz statt exaktem Byte-Hash: erkennt
  visuell gleiche Bilder trotz unterschiedlicher Skalierung/Kompression
- Symbole (klein <=260px UND mehrfach vorkommend, z.B. Warndreiecke) werden
  EINMAL zentral nach assets/shared/symbols/symbol_<hash>.png abgelegt und im
  RST als .. image:: (12mm, zentriert) auf die eine Quelle referenziert -
  dokumentuebergreifend (registry). Ein Warndreieck: 1x gespeichert statt 58x
- grossformatige Inhaltsbild-Duplikate ebenfalls ueber aHash zusammengefuehrt
- Fragment-"Ueberschriften" (POS, li., re., "bar, max. 10 bar") unterdrueckt
- Altbestand assets/shared/symbols/img_*.png (durcheinander benannt) entfernt

Wirkung: omniflo images/ 83 -> 62, 14 zentrale Symbole statt zig Duplikate.
Voller PDF-Build sauber (lualatex exit 0, 719 Seiten).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 14:10:33 +02:00
Michael StanglandClaude Opus 4.8 c912b1e9d5 fix: saubere PDF-Extraktion der Fortna-Anleitungen (lib/pdf_extract.py)
Neues Modul lib/pdf_extract.py fuer die Volltext-Konvertierung der 5
Hauptanleitungen, behebt die Qualitaetsprobleme der ersten Version:

- Doppelte Nummerierung entfernt: PDF-Gliederungsnummer wird nur fuer die
  Ueberschriften-Ebene ausgewertet, im RST-Text weggelassen (Sphinx nummeriert)
- Bilder positionsbasiert (bbox) extrahiert und als .. figure:: mit Caption
  an der richtigen Textstelle referenziert (625 Refs, 0 fehlende Bilder)
- Fliesstext zu Absaetzen zusammengefuehrt statt je PDF-Zeile ein Absatz
- Aufzaehlungen als RST-Listen (Bullet-only-Zeilen mit Folgezeile gemergt,
  zusammengelaufene Bullets aufgesplittet)
- Aenderungshistorie-/Dokumentinfo-Tabellen und TOC-Seiten uebersprungen
- Symbolfont-Zeichen (PUA) auf Unicode gemappt

fortna_convert_chapters.py: nutzt pdf_extract statt extract_products, lesbare
Dokumenttitel je Kapitel, Verzeichnislisten fuellen Kapitel ohne DE-Quelle mit
vorhandenen Fremdsprachen (Ersatzteilliste EN).
build_index.py / conf.py: Fremd-Altdateien aus dem Build ausgeschlossen.
Alte Test-.rst aus fruehen convert_to_rst.py-Laeufen entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 13:44:09 +02:00
Michael StanglandClaude Opus 4.8 e7aa3b4e4b feat: Fortna-Extraktion nach chapters/ + Sphinx-PDF-Build
Skripte zum Befuellen der Kapitelstruktur aus dem Fortna-Rohmaterial und
Aufbau eines vollstaendigen Sphinx/LaTeX-Builds (nur Code/Config/Doku,
generierte RST/Bilder/PDFs und Rohmaterial via .gitignore ausgeschlossen).

- lib/fortna_inventory.py: Rohmaterial scannen, Sprache+Zielkapitel+Aktion
  zuordnen, results/fortna_inventory.json schreiben
- lib/fortna_convert_chapters.py: 5 Hauptanleitungen volltextig (Text+Bilder)
  konvertieren, Verzeichnislisten (index.de.rst) fuer uebrige Kapitel
- lib/fortna_init_project.py: projekte/fortna/ (anlage.json, overrides, images)
- lib/build_index.py: index.rst mit toctrees aus chapters/**/*.de.rst generieren
- lib/extract_products.py: Symbolfont-Zeichen (PUA) auf Unicode mappen (clean_pua)
- conf.py: Sphinx-Build (lualatex, babel statt polyglossia, iconpackage=none)
- bin/: .bat/.sh-Wrapper fuer alle Skripte + build_pdf
- doc/: Konzept-Plan (plan_fortna_extraktion.md) und lib-Skript-Uebersicht
- requirements.txt: Sphinx + myst-parser ergaenzt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-23 13:23:32 +02:00
Michael Stangl eb9237217e aktuelles Dokument für das Management hinzu 2026-09-23 11:09:13 +02:00
Michael Stangl 76000e29cf Neue MVO eingearbeitet 2026-09-22 17:10:15 +02:00
Michael Stangl 8998cd3e02 Summump Schalter erstellt 2026-05-19 17:44:35 +02:00
Michael Stangl 038b51d35d extract_ids.py impl. Bauen der json Files so dass die Bezeichnungen immer mit dabei stehen ausser in den Blättern. 2026-05-19 16:19:01 +02:00
68 changed files with 4033 additions and 164 deletions
+26
View File
@@ -42,6 +42,11 @@ cover/
.env
.venv
env/
# Sphinx-Build-Ausgabe (generiert, nicht versionieren)
/_build/
# generierter Build-Index (aus lib/build_index.py)
/index.rst
env/
venv/
ENV/
env.bak/
@@ -69,3 +74,24 @@ ipython_config.py
/log
/results
/_output
# IDE- / Tool-Ordner
/.vscode
/.iis
/.claude
# Fremd-/Rohmaterial und grosse Binaerdaten (nicht versionieren)
/examples
doc/*.pdf
# Generierte Dokumentations-Bausteine (reproduzierbar via lib/fortna_convert_chapters.py)
# und extrahierte Bilder. Nur Code/Config/Doku wird versioniert.
chapters/**/*.rst
chapters/**/images/
# Extrahierte Bilder (reproduzierbar) - NICHT versionieren ...
assets/shared/images/
projekte/fortna/images/
# ... AUSSER den kuratierten, wiederverwendbaren Symbolen und Logos.
!assets/shared/symbols/
!assets/shared/logos/
+10 -4
View File
@@ -36,7 +36,7 @@ Dieses Repo (`docu_build`) dient als Entwicklungs- und Planungsumgebung. Das Zie
| Komponente | Technologie |
|------------------|--------------------------------------------------|
| Build-System | Sphinx (Python) |
| Textformat | reStructuredText (rST) |
| Textformat | reStructuredText (rST) und/oder Markdown (MyST) |
| Templating | Jinja2 (Rahmenkapitel, Titelseite) |
| PDF-Erzeugung | LaTeX (pdflatex/lualatex) |
| Versionierung | Git (Gitea auf Hetzner) |
@@ -53,7 +53,9 @@ Dieses Repo (`docu_build`) dient als Entwicklungs- und Planungsumgebung. Das Zie
**Override-Reihenfolge**: `projekt/overrides/` > `transport-docs-base/modules/` bzw. `chapters/`
**Mehrsprachigkeit**: Dateinamenskonvention `{name}.{lang}.rst`, Fallback auf `de`. Bilder via `localized-figure`-Direktive (`assets/localized/{lang}/`). Glossar aus `terms.{lang}.csv` (Semikolon-getrennt).
**Mehrsprachigkeit**: Dateinamenskonvention `{name}.{lang}.{rst|md}`, Fallback auf `de`. Bilder via `localized-figure`-Direktive (`assets/localized/{lang}/`). Glossar aus `terms.{lang}.csv` (Semikolon-getrennt).
**rST/Markdown-Mischbetrieb**: Sphinx liest über die Extension MyST-Parser `.rst`- und `.md`-Dateien gleichwertig ein. Beide Formate können projektweit gemischt werden (z. B. `beschreibung.de.rst` neben `wartung.de.md` im selben Modul) – toctree-Einbindung, Direktiven und die Sprachsuffix-Konvention funktionieren identisch. Pro Modul/Kapitel wird ein einheitliches Format empfohlen, ein Mix ist aber technisch unproblematisch.
**anlage.json**: Steuert Zusammenstellung – enthält `projekt` (Metadaten, Sprachen, Normenregion), `maschinen` (Modulliste mit Positionen/Varianten/Optionen), `zusatz_kapitel`.
@@ -65,8 +67,10 @@ Details siehe `doc/Kernkonzepte.md`.
```text
00_inhaltsverzeichnis – Inhaltsverzeichnis (wird generiert)
01_zertifikate – CE/UKCA-Konformität, Konfigurationsbeschreibung
02_sicherheit – Allgemein, Normen, Risikobewertung
01_zertifikate – CE/UKCA-Konformität, Konfigurationsbeschreibung,
Einbauerklärungen, Komponenten-Konformitätserklärungen
02_sicherheit – Allgemein, Normen (inkl. gemeinsame/sonstige Spezifikationen),
Risikobewertung (inkl. Restrisiken)
03_anlagenbeschreibung – Module werden hier via meta.yaml eingefügt
04_betrieb – Betriebsanleitung
05_wartung – Wartungsanleitung, Schmierplan
@@ -77,6 +81,8 @@ Details siehe `doc/Kernkonzepte.md`.
10_anhang – Prüfberichte, Abnahmeprotokolle
```
Kapitel `01_zertifikate` und `02_sicherheit` wurden um Unterkapitel ergänzt, die von Anhang IV Teil A der VO (EU) 2023/1230 (neue Maschinenverordnung, ersetzt Anhang VII MRL 2006/42/EG) gefordert werden: Einbauerklärungen/Montageanleitungen für eingebaute unvollständige Maschinen, Konformitätserklärungen zugekaufter Komponenten, gemeinsame Spezifikationen, sonstige technische Spezifikationen sowie eine explizite Restrisiken-Section.
Details siehe `doc/kapitelstruktur.md`.
### Sphinx-Extensions (zu implementieren in `_ext/`)
+9 -3
View File
@@ -15,7 +15,7 @@ Das Zielsystem besteht aus zwei Repos:
| Komponente | Technologie |
|------------------|--------------------------------------------------|
| Build-System | Sphinx (Python) |
| Textformat | reStructuredText (rST) |
| Textformat | reStructuredText (rST) und/oder Markdown (MyST) |
| Templating | Jinja2 (Rahmenkapitel, Titelseite) |
| PDF-Erzeugung | LaTeX (pdflatex/lualatex) |
| Versionierung | Git (Gitea auf Hetzner) |
@@ -107,8 +107,8 @@ Details zur Konfiguration und Aufbau einer Doku siehe [Kernkonzepte](doc/Kernko
| Kapitel | Inhalt |
| ------------------------- | --------------------------------------------- |
| `00_inhaltsverzeichnis` | Wird generiert |
| `01_zertifikate` | CE/UKCA-Konformität, Konfigurationsbeschreibung |
| `02_sicherheit` | Sicherheitshinweise, Normen, Risikobewertung |
| `01_zertifikate` | CE/UKCA-Konformität, Konfigurationsbeschreibung, Einbauerklärungen, Komponenten-DoCs |
| `02_sicherheit` | Sicherheitshinweise, Normen (inkl. gemeinsame/sonstige Spezifikationen), Risikobewertung (inkl. Restrisiken) |
| `03_anlagenbeschreibung` | Module (via meta.yaml eingefügt) |
| `04_betrieb` | Betriebsanleitung |
| `05_wartung` | Wartungsanleitung, Schmierplan |
@@ -118,6 +118,12 @@ Details zur Konfiguration und Aufbau einer Doku siehe [Kernkonzepte](doc/Kernko
| `09_technische_unterlagen` | Layouts, Schaltpläne, Notstopp |
| `10_anhang` | Prüfberichte, Abnahmeprotokolle |
Details und Begründung der Unterkapitel siehe [Kapitelstruktur](doc/kapitelstruktur.md).
### Dateiformat der Textbausteine: rST und Markdown gleichwertig
Sphinx liest über die Extension **MyST-Parser** sowohl `.rst`- als auch `.md`-Dateien gleichwertig ein. Beide Formate können projektweit gemischt werden: ein Modul kann `beschreibung.de.rst` verwenden, ein anderes `beschreibung.de.md` – der Build behandelt beide identisch (toctree-Einbindung, Direktiven, Sprachsuffix-Konvention `{name}.{lang}.{rst|md}`). Empfehlung: pro Modul/Kapitel ein einheitliches Format wählen, um Lesbarkeit im Repo zu erhalten; eine Mischung ist aber technisch unproblematisch.
## Umgebungsvariablen
| Variable | Beschreibung |
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+31
View File
@@ -0,0 +1,31 @@
@echo off
REM ================================================================
REM HTML-Build der Fortna-Dokumentation (DE)
REM
REM Ablauf:
REM 1. index.rst aus vorhandenen chapters/**/*.de.rst generieren
REM 2. sphinx-build -b html -> _build/html/index.html
REM
REM Voraussetzung: chapters/ ist befuellt (fortna_convert_chapters.bat --apply)
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
call "%PROJECT%\.venv\Scripts\activate.bat"
echo.
echo [1/2] index.rst generieren ...
python "%PV_LIB%\build_index.py"
echo.
echo [2/2] HTML erzeugen (sphinx-build -b html) ...
python -m sphinx -b html "%PROJECT%" "%PROJECT%\_build\html"
echo.
echo Fertig. HTML: %PROJECT%\_build\html\index.html
deactivate
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# ================================================================
# HTML-Build der Fortna-Dokumentation (DE)
# 1. index.rst generieren
# 2. sphinx-build -b html
# Voraussetzung: chapters/ ist befuellt (fortna_convert_chapters.sh --apply)
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PROJECT/.venv" ]; then
echo "FEHLER: .venv nicht gefunden. Bitte zuerst install_py.sh ausfuehren."
exit 1
fi
source "$PROJECT/.venv/bin/activate" 2>/dev/null || source "$PROJECT/.venv/Scripts/activate"
echo ""
echo "[1/2] index.rst generieren ..."
python "$PV_LIB/build_index.py"
echo ""
echo "[2/2] HTML erzeugen (sphinx-build -b html) ..."
python -m sphinx -b html "$PROJECT" "$PROJECT/_build/html"
echo ""
echo "Fertig. HTML: $PROJECT/_build/html/index.html"
deactivate
+38
View File
@@ -0,0 +1,38 @@
@echo off
REM ================================================================
REM Vollstaendiger PDF-Build der Fortna-Dokumentation (DE)
REM
REM Ablauf:
REM 1. index.rst aus vorhandenen chapters/**/*.de.rst generieren
REM 2. sphinx-build -b latex -> _build/latex/*.tex
REM 3. lualatex (via latexmk) -> _build/latex/Fortna_Dokumentation_DE.pdf
REM
REM Voraussetzung: chapters/ ist befuellt (fortna_convert_chapters.bat --apply)
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
call "%PROJECT%\.venv\Scripts\activate.bat"
echo.
echo [1/3] index.rst generieren ...
python "%PV_LIB%\build_index.py"
echo.
echo [2/3] LaTeX-Quellen erzeugen (sphinx-build -b latex) ...
python -m sphinx -b latex "%PROJECT%" "%PROJECT%\_build\latex"
echo.
echo [3/3] PDF erzeugen (lualatex) ...
pushd "%PROJECT%\_build\latex"
latexmk -lualatex -interaction=nonstopmode Fortna_Dokumentation_DE.tex
popd
echo.
echo Fertig. PDF: %PROJECT%\_build\latex\Fortna_Dokumentation_DE.pdf
deactivate
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# ================================================================
# Vollstaendiger PDF-Build der Fortna-Dokumentation (DE)
# 1. index.rst generieren
# 2. sphinx-build -b latex
# 3. lualatex (via latexmk)
# Voraussetzung: chapters/ ist befuellt (fortna_convert_chapters.sh --apply)
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PROJECT/.venv" ]; then
echo "FEHLER: .venv nicht gefunden. Bitte zuerst install_py.sh ausfuehren."
exit 1
fi
source "$PROJECT/.venv/bin/activate" 2>/dev/null || source "$PROJECT/.venv/Scripts/activate"
echo ""
echo "[1/3] index.rst generieren ..."
python "$PV_LIB/build_index.py"
echo ""
echo "[2/3] LaTeX-Quellen erzeugen (sphinx-build -b latex) ..."
python -m sphinx -b latex "$PROJECT" "$PROJECT/_build/latex"
echo ""
echo "[3/3] PDF erzeugen (lualatex) ..."
( cd "$PROJECT/_build/latex" && latexmk -lualatex -interaction=nonstopmode Fortna_Dokumentation_DE.tex )
echo ""
echo "Fertig. PDF: $PROJECT/_build/latex/Fortna_Dokumentation_DE.pdf"
deactivate
+26
View File
@@ -0,0 +1,26 @@
@echo off
REM ================================================================
REM Sivas-IDs aus JSON-Baeumen extrahieren
REM
REM Liest JSON-Dateien aus results/ und extrahiert 9-stellige
REM Sivas-Nummern eine Ebene ueber den Blaettern.
REM
REM Verwendung:
REM extractids.bat --filename KA135776
REM extractids.bat --filename KA135776 --tab soll
REM extractids.bat --all
REM ================================================================
call "%~dp0setenv.bat"
REM Pruefe ob venv existiert
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
call "%PROJECT%\.venv\Scripts\activate.bat"
python "%PV_LIB%\extract_ids.py" %*
deactivate
+22
View File
@@ -0,0 +1,22 @@
@echo off
REM ================================================================
REM chapters/ aus dem Fortna-Inventar befuellen
REM (5 Hauptanleitungen Volltext + Verzeichnislisten)
REM
REM Verwendung:
REM fortna_convert_chapters.bat (Dry-Run)
REM fortna_convert_chapters.bat --apply
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
if "%PV_FORTNA_SOURCE%"=="" set "PV_FORTNA_SOURCE=C:\10-Develop\projekte\Fortna"
call "%PROJECT%\.venv\Scripts\activate.bat"
python "%PV_LIB%\fortna_convert_chapters.py" %*
deactivate
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# ================================================================
# chapters/ aus dem Fortna-Inventar befuellen
# (5 Hauptanleitungen Volltext + Verzeichnislisten)
# Verwendung:
# bash bin/fortna_convert_chapters.sh (Dry-Run)
# bash bin/fortna_convert_chapters.sh --apply
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PROJECT/.venv" ]; then
echo "FEHLER: .venv nicht gefunden. Bitte zuerst install_py.sh ausfuehren."
exit 1
fi
export PV_FORTNA_SOURCE="${PV_FORTNA_SOURCE:-C:/10-Develop/projekte/Fortna}"
source "$PROJECT/.venv/bin/activate" 2>/dev/null || source "$PROJECT/.venv/Scripts/activate"
python "$PV_LIB/fortna_convert_chapters.py" "$@"
deactivate
+19
View File
@@ -0,0 +1,19 @@
@echo off
REM ================================================================
REM Projektspezifischen Ordner projekte/fortna/ anlegen
REM
REM Verwendung:
REM fortna_init_project.bat
REM fortna_init_project.bat --force
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
call "%PROJECT%\.venv\Scripts\activate.bat"
python "%PV_LIB%\fortna_init_project.py" %*
deactivate
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# ================================================================
# Projektspezifischen Ordner projekte/fortna/ anlegen
# Verwendung:
# bash bin/fortna_init_project.sh
# bash bin/fortna_init_project.sh --force
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PROJECT/.venv" ]; then
echo "FEHLER: .venv nicht gefunden. Bitte zuerst install_py.sh ausfuehren."
exit 1
fi
source "$PROJECT/.venv/bin/activate" 2>/dev/null || source "$PROJECT/.venv/Scripts/activate"
python "$PV_LIB/fortna_init_project.py" "$@"
deactivate
+21
View File
@@ -0,0 +1,21 @@
@echo off
REM ================================================================
REM Fortna-Rohmaterial inventarisieren + Kapitel-Mapping pruefen
REM
REM Verwendung:
REM fortna_inventory.bat
REM fortna_inventory.bat --lang de
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: .venv nicht gefunden. Bitte zuerst install_py.bat ausfuehren.
exit /b 1
)
if "%PV_FORTNA_SOURCE%"=="" set "PV_FORTNA_SOURCE=C:\10-Develop\projekte\Fortna"
call "%PROJECT%\.venv\Scripts\activate.bat"
python "%PV_LIB%\fortna_inventory.py" %*
deactivate
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# ================================================================
# Fortna-Rohmaterial inventarisieren + Kapitel-Mapping pruefen
# Verwendung: bash bin/fortna_inventory.sh [--lang de]
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PROJECT/.venv" ]; then
echo "FEHLER: .venv nicht gefunden. Bitte zuerst install_py.sh ausfuehren."
exit 1
fi
export PV_FORTNA_SOURCE="${PV_FORTNA_SOURCE:-C:/10-Develop/projekte/Fortna}"
source "$PROJECT/.venv/bin/activate" 2>/dev/null || source "$PROJECT/.venv/Scripts/activate"
python "$PV_LIB/fortna_inventory.py" "$@"
deactivate
+20
View File
@@ -0,0 +1,20 @@
@echo off
REM ================================================================
REM DOCU_BUILD - Projektdaten aus Excel-Stuecklisten extrahieren
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PROJECT%\.venv" (
echo FEHLER: Virtual environment nicht gefunden.
echo Bitte zuerst bin\install_py.bat ausfuehren.
exit /b 1
)
call "%PROJECT%\.venv\Scripts\activate.bat"
echo.
echo Extrahiere Projektdaten aus Excel-Stuecklisten ...
echo.
python "%PV_LIB%\project_from_xlsx.py" %*
+15 -15
View File
@@ -66,21 +66,21 @@ echo ================================================================
echo DOCU_BUILD ENVIRONMENT SETUP COMPLETE
echo ================================================================
echo PROJECT = %PROJECT%
echo PV_BIN = %PV_BIN%
echo PV_CFG = %PV_CFG%
echo PV_LIB = %PV_LIB%
echo PV_DATA = %PV_DATA%
echo PV_RESULTS = %PV_RESULTS%
echo PV_LOG = %PV_LOG%
echo PV_EXAMPLES = %PV_EXAMPLES%
echo PV_EXT = %PV_EXT%
echo PV_TEMPLATES = %PV_TEMPLATES%
echo PV_ASSETS = %PV_ASSETS%
echo PV_MODULES = %PV_MODULES%
echo PV_CHAPTERS = %PV_CHAPTERS%
echo PV_GLOSSARY = %PV_GLOSSARY%
echo PV_STYLES = %PV_STYLES%
echo PV_SCRIPTS = %PV_SCRIPTS%
REM echo PV_BIN = %PV_BIN%
REM echo PV_CFG = %PV_CFG%
REM echo PV_LIB = %PV_LIB%
REM echo PV_DATA = %PV_DATA%
REM echo PV_RESULTS = %PV_RESULTS%
REM echo PV_LOG = %PV_LOG%
REM echo PV_EXAMPLES = %PV_EXAMPLES%
REM echo PV_EXT = %PV_EXT%
REM echo PV_TEMPLATES = %PV_TEMPLATES%
REM echo PV_ASSETS = %PV_ASSETS%
REM echo PV_MODULES = %PV_MODULES%
REM echo PV_CHAPTERS = %PV_CHAPTERS%
REM echo PV_GLOSSARY = %PV_GLOSSARY%
REM echo PV_STYLES = %PV_STYLES%
REM echo PV_SCRIPTS = %PV_SCRIPTS%
echo PYTHONPATH = %PYTHONPATH%
echo ================================================================
echo.
+108
View File
@@ -0,0 +1,108 @@
"""Sphinx-Konfiguration fuer den Fortna-Dokumentations-Build (DE).
Vorlaeufige Konfiguration fuer den ersten vollstaendigen Build aus dem Base-Repo
heraus. Die spaetere Zwei-Repo-Architektur (conf_base.py + Projekt-conf.py, siehe
doc/Kernkonzepte.md) ist hier noch nicht umgesetzt.
"""
from __future__ import annotations
# -- Projekt ----------------------------------------------------------------
project = "Anlagendokumentation Fortna (500592 / KA 135776)"
author = "Schoenenberger Systeme GmbH / Mista GmbH"
copyright = "2026, Schoenenberger Systeme GmbH"
release = "0.1"
language = "de"
# -- Allgemein --------------------------------------------------------------
extensions = [
"myst_parser", # erlaubt .md gleichwertig zu .rst (Kernkonzepte.md)
]
# Master-Dokument
root_doc = "index"
source_suffix = {
".rst": "restructuredtext",
".md": "markdown",
}
# Nur die deutschsprachigen Bausteine in den Build aufnehmen; fremdsprachige
# {name}.en.rst / {name}.cz.rst (aktuell keine im Baum) werden ignoriert.
# Sphinx sammelt per Default ALLE .rst/.md unter dem Wurzelverzeichnis ein.
# Alles ausserhalb von chapters/ ausschliessen, damit nur die Doku-Bausteine
# in den Build gelangen (CLAUDE.md, README.md, doc/, Git-Worktrees etc. raus).
exclude_patterns = [
"_build",
"**/*.en.rst",
"**/*.cz.rst",
"*.md",
".claude",
".venv",
".git",
"doc",
"examples",
"results",
"data",
"lib",
"bin",
"cfg",
"log",
"tests",
"projekte",
"_ext",
"_templates",
"assets",
"modules",
"glossary",
"styles",
"scripts",
# Defekte Altbestaende aus fruehen Testlaeufen (nicht von den fortna_*-Skripten
# erzeugt): unvollstaendiger Bildbestand bzw. fehlende RST-Ueberschrift.
# TODO: mit fortna_convert_chapters.py sauber neu erzeugen, dann Exclude entfernen.
"chapters/02_sicherheit/risikobewertung/rb_fortna_h_m_500592_ev12_20260311.de.rst",
"chapters/00_inhaltsverzeichnis/contens_obsah_500592_135776_v2_1_en_cz.de.rst",
"chapters/10_anhang/01_getriebe_loadingloopantr_de.de.rst",
"chapters/10_anhang/02_motor_loadingloopantr_de.de.rst",
]
# -- HTML -------------------------------------------------------------------
html_theme = "alabaster"
# -- LaTeX / PDF ------------------------------------------------------------
# lualatex verkraftet die Unicode-Zeichen (tschechische Diakritik, ue/oe/ae,
# Sonderzeichen aus der PDF-Extraktion) deutlich besser als pdflatex.
latex_engine = "lualatex"
latex_documents = [
(root_doc,
"Fortna_Dokumentation_DE.tex",
"Anlagendokumentation Fortna\\\\500592 / KA 135776",
author,
"manual"),
]
# iconpackage=none: FontAwesome nicht laden. fontawesome7 (MiKTeX) bricht unter
# lualatex beim Font-Analyze ab (\faDice undefined). Ohne Icons werden Admonitions
# (.. note:: in den Verzeichnislisten) einfach ohne Symbol gesetzt.
latex_elements = {
"sphinxsetup": "iconpackage=none",
"papersize": "a4paper",
"pointsize": "11pt",
# babel statt polyglossia erzwingen: polyglossia 2.13 (MiKTeX 09/2026) wirft
# unter lualatex einen bidi-Initialisierungsfehler (\g__xpg_unibidi_str), der
# fuer rein deutschen Text irrelevant ist. babel vermeidet ihn.
"polyglossia": "",
"babel": r"\usepackage[ngerman]{babel}",
# Robustheit gegen ueberlange Zeilen aus der PDF-Textextraktion:
# - pdflscape: erlaubt \begin{landscape} fuer quergestellte Verzeichnistabellen
# - \UrlBreaks: lange Quellpfade in ``...`` duerfen umbrechen
"preamble": r"""
\usepackage{microtype}
\usepackage{pdflscape}
\emergencystretch=3em
\setcounter{tocdepth}{2}
\sloppy
\renewcommand{\sphinxbreaksbeforeactivelist}{\do\-\do\/\do\_\do\.}
""",
}
+504
View File
@@ -0,0 +1,504 @@
[ 512] .
├── [ 512] 00_CONTENTS_NOTES-OBSAH_POZNÁMKY
│   ├── [355K] Contens-Obsah_500592_135776_V2.1_EN-CZ.pdf
│   └── [348K] Contens_Obsah_500592_135776_V2.1_20260311_Rev_2.pdf
├── [ 512] 01_CERTIFICATES-CERTIFIKÁTY
│   ├── [ 512] 01_SCHÖNENBERGER
│   │   ├── [ 512] 01_EN_original
│   │   │   └── [ 74K] 00_EC Declaration of Conformity_V2_EN.pdf
│   │   └── [ 512] 02_CZ
│   │   └── [ 76K] 00_Prohlášení o shodě ES_V2_CZ.pdf
│   └── [ 512] 02_KONFIGURATIONSBESCHREIBUNG_CONFIG-DESCRIPTION_POPIS KONFIG
│   ├── [ 512] 01_DE
│   │   └── [252K] Konfigurationsbeschreibung_für_CE_V2.pdf
│   ├── [ 512] 02_EN
│   │   └── [249K] Configuration Description_CE_V2_EN.pdf
│   └── [ 512] 03_CZ
│   └── [248K] Popis konfigurace_für_CE_V2_CZ.pdf
├── [ 512] 02_INSTRUCTIONS-NÁVOD_K_POUŽITÍ
│   ├── [ 512] 01_DE_original
│   │   ├── [5.5M] 01_BETRIEBSANLEITUNG_500592_V2.3.pdf
│   │   ├── [5.5M] 02_WARTUNGSANLEITUNG_500592_V2.2.pdf
│   │   ├── [7.4M] 03_FUNKTION_STEUERUNG_ILS_500592_V2.1.pdf
│   │   ├── [4.3M] 04_FUNKTION_STEUERUNG_OMNIFLO_500592_V1.0.pdf
│   │   └── [2.5M] 05_FUNKTION_STEUERUNG_LoadingLoop_500592_V2.2.pdf
│   ├── [ 512] 02_EN
│   │   └── [2.5M] 05_FUNKTION_CONTROL_LoadingLoop_500592_V2.2_EN.pdf
│   └── [ 512] 03_CZ
│   ├── [5.4M] 01_NÁVOD_K_POUŽÍVÁNÍ_500592_135776_V2.3_CZ.pdf
│   ├── [5.5M] 02_NÁVOD_K_ÚDRŽBĚ_500592_135776_V2.2_CZ.pdf
│   ├── [7.5M] 03_FUNKCE_ŘÍZENÍ_ILS_500592_V2.1_CZ.pdf
│   └── [4.8M] 04_ŘÍZENÍ_FUNKCÍ_OMNIFLO_500592_V1.0._CZ.pdf
├── [ 512] 03_SPARE_PARTS-NÁHRADNÍ_DÍLY
│   └── [3.3M] 01_SPARE_PARTS_500592_EN_V2.1.pdf
├── [ 512] 04_TECHNICAL_DOC-TECHNICKÉ_DOKUMENTY
│   ├── [ 512] 01_LAYOUT-DISPOZICE
│   │   ├── [ 512] 01_MECHANICAL-MECHANICKÝ
│   │   │   ├── [ 512] 01_EN_original
│   │   │   │   ├── [196M] 01_500592_10_5.14_Layout_V2.1_EN.pdf
│   │   │   │   └── [ 37M] 02_500592_10_5.14_Bereiche_V2.1_EN.pdf
│   │   │   └── [ 512] 02_CZ
│   │   │   ├── [196M] 01_500592_10_5.14_dispozice_V2.1_CZ.pdf
│   │   │   └── [ 36M] 02_500592_10_5.14_oblasti_V2.1_CZ.pdf
│   │   └── [ 512] 02_ELECTRICAL-ELEKTRIKA
│   │   ├── [ 512] 01_EN_original
│   │   │   ├── [3.0M] 01_ST500592_10_5.14_Omniflo-AMR+Highway-(ST6)-[EN].pdf
│   │   │   ├── [2.4M] 02_ST500592_10_5.14_Omniflo-Outbound+Highway-(ST7)-[EN].pdf
│   │   │   ├── [2.1M] 03_ST500592_10_5-14_ILS-AMR-ILS (ST4)-[EN].pdf
│   │   │   ├── [2.3M] 04_ST500592_10_5-14_ILS-F2H-Workstation (ST5)-[EN].pdf
│   │   │   ├── [1.8M] 05_ST500592_10_5-14_ILS-Highway (ST1-ST3)-[EN].pdf
│   │   │   ├── [4.6M] 06_ST500592_10_5-14_ILS-Inbound level 0 (ST1)-[EN].pdf
│   │   │   ├── [5.6M] 07_ST500592_10_5-14_ILS-Storage Level 1 (ST2)-[EN].pdf
│   │   │   └── [5.4M] 08_ST500592_10_5-14_ILS-Storage level 2 (ST3)-[EN].pdf
│   │   └── [ 512] 02_CZ
│   │   ├── [3.7M] 01_ST500592_10_5.14_Omniflo_CZ-AMR+Highway(ST6)-[CZ].pdf
│   │   ├── [2.4M] 02_ST500592_10_5.14_Omniflo_CZ-Outbound+Highway (ST7)-[CZ].pdf
│   │   ├── [5.4M] 03_ST500592_10_5-14_ILS_CZ-Storage level 2 (ST3)-[CZ].pdf
│   │   ├── [1.8M] 04_ST500592_10_5-14_ILS_CZ-AMR-ILS (ST4)-[CZ].pdf
│   │   ├── [1.9M] 05_ST500592_10_5-14_ILS_CZ-F2H-Workstation (ST5)-[CZ].pdf
│   │   ├── [1.4M] 06_ST500592_10_5-14_ILS_CZ-Highway (ST1-ST3)-[CZ].pdf
│   │   ├── [2.7M] 07_ST500592_10_5-14_ILS_CZ-Inbound level 0 (ST1)-[CZ].pdf
│   │   └── [6.0M] 08_ST500592_10_5-14_ILS_CZ-Storage Level 1 (ST2)-[CZ].pdf
│   ├── [ 512] 02_AREA_STOP_ZASTAVENI_OBLASTI
│   │   ├── [ 512] 02_CZ
│   │   │   └── [ 22M] 01_ES500592_10_5-14_ILS-OF_Zastavení oblasti_cpl-[CZ].pdf
│   │   └── [ 512] 1_EN_original
│   │   └── [ 23M] 01_ES500592_10_5-14_ILS-OF-Area-Stop-cpl.-[EN].pdf
│   ├── [ 512] 03_EMERGENCY_STOP-NOUZOVÉ_ZASTAVENÍ
│   │   ├── [ 512] 01_EMERGENCY_STOP_NOUZOVE_ZASTAVENI
│   │   │   ├── [ 512] 01_EN_original
│   │   │   │   └── [ 30M] 01_ES500592_10_5-14_ILS-OF_cpl.-E-Stop-[EN].pdf
│   │   │   └── [ 512] 02_CZ
│   │   │   └── [ 26M] 01_ES500592_10_5-14_ILS-OF-cpl._NV-[CZ].pdf
│   │   └── [ 512] 02_EMERGENCY_STOP_MATRIX
│   │   └── [837K] 01_Emergency_Stop_Matrix_V1.5_20260213-[EN].pdf
│   ├── [ 512] 04_DIAGRAMS-SCHÉMATA
│   │   ├── [ 512] 01_EN_original
│   │   │   ├── [ 32M] 01_Circuit diagrams_Controlarea 1_EN_Update_20260416.pdf
│   │   │   ├── [ 94M] 02_Circuit diagrams_Controlarea 2_EN_Update_20260416.pdf
│   │   │   ├── [ 98M] 03_Circuit diagrams_Controlarea 3_EN_Update_20260416.pdf
│   │   │   ├── [ 17M] 04_Circuit diagrams_Controlarea 4_EN_Update_20260416.pdf
│   │   │   ├── [ 18M] 05_Circuit diagrams_Controlarea 5_EN_Update_20260416.pdf
│   │   │   ├── [ 39M] 06_Circuit diagrams_Controlarea 6_EN_Update_20260416.pdf
│   │   │   └── [ 58M] 07_Circuit diagrams_Controlarea 7_EN_Update_20260416.pdf
│   │   └── [ 512] 02_CZ
│   │   ├── [ 32M] 01_Schéma zapojení_Ovládací oblast 1_CZ_Aktualizace_20260416.pdf
│   │   ├── [ 93M] 02_Schéma zapojení_Ovládací oblast 2_CZ_Aktualizace_20260416.pdf
│   │   ├── [ 96M] 03_Schéma zapojení_Ovládací oblast 3_CZ_Aktualizace_20260416.pdf
│   │   ├── [ 17M] 04_Schéma zapojení_Ovládací oblast 4_CZ_Aktualizace_20260416.pdf
│   │   ├── [ 18M] 05_Schéma zapojení_Ovládací oblast 5_CZ_Aktualizace_20260416.pdf
│   │   ├── [ 39M] 06_Schéma zapojení_Ovládací oblast 6_CZ_Aktualizace_20260416.pdf
│   │   └── [ 58M] 07_Schéma zapojení_Ovládací oblast 7_CZ_Aktualizace_20260416.pdf
│   └── [ 512] 05_REPORTS-ZPRÁVY
│   ├── [ 512] 01_Protokoll_Wiederanlauf_20260227
│   │   └── [149K] 01_Schutz_gegen_automatischen_Wiederanlauf_V1.1.pdf
│   ├── [ 512] 02_Prüfung_Drehrichtung_Asyncronmotoren_20260227
│   │   └── [153K] 01_Drehrichtung_Asynchronmotoren_20260420.pdf
│   └── [ 512] 03_Erprobung_Anzeige_Bediengeräte_20260227
│   └── [161K] 01_Erprobung_Anzeige_Bediengeräte_20260420.pdf
├── [ 512] 05_SUPPLIER_DOC-DOKUMENTY_DODAVATELE
│   ├── [ 512] 01_MiTek_steel_structure_ocelová konstrukce
│   │   ├── [ 512] 01_As Built_drawing
│   │   │   └── [ 85M] MMS 2307-025 (As Built).pdf
│   │   ├── [ 512] 02_Structural_Calcs
│   │   │   └── [ 16M] Design_Calculation_Report_Rev_002_23.01.25_EN.pdf
│   │   ├── [ 512] 03_Instructions_Pokyny
│   │   │   ├── [ 512] 01_EN_original
│   │   │   │   ├── [ 90K] 01_Sestavení dat_CZ-Data_Assembly_EN.pdf
│   │   │   │   ├── [168K] 02_Operations and Maintenance_rev.1_EN.pdf
│   │   │   │   └── [241K] 03_marathon-prime-finish-tds_EN.pdf
│   │   │   └── [ 512] 02_CZ
│   │   │   └── [106K] 02_Provozní a údržbové postupy_rev.1_CZ.pdf
│   │   ├── [ 512] 04_MiTek DOP
│   │   │   ├── [240K] Declaration_Performance_Certificate_MMS-2307-025_EN.pdf
│   │   │   └── [204K] MMS-2307-025_Design_Compliance_Statement_EN.pdf
│   │   ├── [ 512] 05_Fabricator DOP
│   │   │   └── [228K] BRW0C96E613B534_000616.pdf
│   │   ├── [ 512] 06_Steel Test Cert
│   │   │   ├── [ 512] Lot 1
│   │   │   │   ├── [4.4M] k093444 - Test Certs.pdf
│   │   │   │   ├── [1.2M] k093445 - Test Certs.pdf
│   │   │   │   ├── [718K] k093446 - Test Certs.pdf
│   │   │   │   ├── [220K] k093452 - Test Certs.pdf
│   │   │   │   ├── [192K] k093713 - Test Certs.pdf
│   │   │   │   └── [147K] k093771 - Test Certs.pdf
│   │   │   ├── [ 512] Lot 10
│   │   │   │   ├── [1.2M] TC_7545925_001_2668830_001_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_002_2668830_002_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_003_2668830_003_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_004_2668830_004_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_005_2668830_005_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_006_2668830_006_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_007_2668830_007_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_008_2668830_008_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_009_2668830_009_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545925_010_2668830_010_0188358.pdf
│   │   │   │   └── [1.2M] TC_7545925_011_2668830_011_0188358.pdf
│   │   │   ├── [ 512] Lot 11
│   │   │   │   ├── [1.2M] TC_7545926_001_2668831_001_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_002_2668831_002_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_003_2668831_003_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_004_2668831_004_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_005_2668831_005_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_006_2668831_006_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_007_2668831_007_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_008_2668831_008_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_009_2668831_009_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_010_2668831_010_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_011_2668831_011_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_012_2668831_012_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_013_2668831_013_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_014_2668831_014_0188358.pdf
│   │   │   │   ├── [1.2M] TC_7545926_015_2668831_015_0188358.pdf
│   │   │   │   └── [1.2M] TC_7545926_016_2668831_016_0188358.pdf
│   │   │   ├── [ 512] Lot 2
│   │   │   │   ├── [2.1M] k093448 - Test Certs.pdf
│   │   │   │   ├── [1.5M] k093449 - Test Certs.pdf
│   │   │   │   ├── [2.0M] k093450 - Test Certs.pdf
│   │   │   │   ├── [220K] k093452 - Test Certs.pdf
│   │   │   │   ├── [189K] k093605 - Test Certs.pdf
│   │   │   │   ├── [192K] k093714 - Test Certs.pdf
│   │   │   │   └── [147K] k093771 - Test Certs.pdf
│   │   │   ├── [ 512] Lot 3
│   │   │   │   ├── [ 29M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (10).zip
│   │   │   │   ├── [ 29M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (11).zip
│   │   │   │   ├── [ 31M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (12).zip
│   │   │   │   ├── [ 31M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (13).zip
│   │   │   │   ├── [ 29M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (14).zip
│   │   │   │   ├── [ 29M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (15).zip
│   │   │   │   ├── [ 28M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (16).zip
│   │   │   │   ├── [ 18M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (17).zip
│   │   │   │   └── [ 28M] SO-7537535-Test-Certs-03-12-2025-15-41-46 (9).zip
│   │   │   ├── [251M] Lot 3.zip
│   │   │   ├── [ 512] Lot 4
│   │   │   │   ├── [ 30M] SO-7537547-Test-Certs-03-12-2025-16-04-28 (1).zip
│   │   │   │   ├── [ 30M] SO-7537547-Test-Certs-03-12-2025-16-04-28 (2).zip
│   │   │   │   ├── [ 30M] SO-7537547-Test-Certs-03-12-2025-16-04-28 (3).zip
│   │   │   │   ├── [ 29M] SO-7537547-Test-Certs-03-12-2025-16-04-28 (4).zip
│   │   │   │   ├── [ 30M] SO-7537547-Test-Certs-03-12-2025-16-04-28 (5).zip
│   │   │   │   └── [ 29M] SO-7537547-Test-Certs-03-12-2025-16-04-28.zip
│   │   │   ├── [ 512] Lot 5
│   │   │   │   ├── [ 30M] SO-7538540-Test-Certs-10-12-2025-11-55-15 (1).zip
│   │   │   │   ├── [ 30M] SO-7538540-Test-Certs-10-12-2025-11-55-15 (2).zip
│   │   │   │   ├── [ 27M] SO-7538540-Test-Certs-10-12-2025-11-55-15 (3).zip
│   │   │   │   ├── [ 30M] SO-7538540-Test-Certs-10-12-2025-11-55-15 (4).zip
│   │   │   │   ├── [ 28M] SO-7538540-Test-Certs-10-12-2025-11-55-15 (5).zip
│   │   │   │   └── [ 29M] SO-7538540-Test-Certs-10-12-2025-11-55-15.zip
│   │   │   ├── [ 512] Lot 6
│   │   │   │   ├── [ 29M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (1).zip
│   │   │   │   ├── [ 29M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (2).zip
│   │   │   │   ├── [ 28M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (3).zip
│   │   │   │   ├── [ 29M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (4).zip
│   │   │   │   ├── [ 30M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (5).zip
│   │   │   │   ├── [ 27M] SO-7539198-Test-Certs-10-12-2025-12-54-38 (6).zip
│   │   │   │   └── [ 28M] SO-7539198-Test-Certs-10-12-2025-12-54-38.zip
│   │   │   ├── [ 512] Lot 7
│   │   │   │   ├── [ 29M] SO-7540495-Test-Certs-10-12-2025-14-22-20 (1).zip
│   │   │   │   ├── [ 28M] SO-7540495-Test-Certs-10-12-2025-14-22-20 (2).zip
│   │   │   │   ├── [ 30M] SO-7540495-Test-Certs-10-12-2025-14-22-20 (3).zip
│   │   │   │   ├── [ 18M] SO-7540495-Test-Certs-10-12-2025-14-22-20 (4).zip
│   │   │   │   └── [ 29M] SO-7540495-Test-Certs-10-12-2025-14-22-20.zip
│   │   │   ├── [ 512] Lot 8
│   │   │   └── [ 512] Lot 9
│   │   │   ├── [1.2M] TC_7545924_001_2668829_001_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_002_2668829_002_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_003_2668829_003_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_004_2668829_004_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_005_2668829_005_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_006_2668829_006_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_007_2668829_007_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_008_2668829_008_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_009_2668829_009_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_010_2668829_010_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_011_2668829_011_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_012_2668829_012_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_013_2668829_013_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_014_2668829_014_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_015_2668829_015_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_016_2668829_016_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_017_2668829_017_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_018_2668829_018_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_019_2668829_019_0188358.pdf
│   │   │   ├── [1.2M] TC_7545924_020_2668829_020_0188358.pdf
│   │   │   └── [1.2M] TC_7545924_021_2668829_021_0188358.pdf
│   │   ├── [ 512] 07_Joist Test Cert
│   │   │   ├── [ 512] Lot 13
│   │   │   │   ├── [332K] Mitek 96119739_1 H3.309.pdf
│   │   │   │   ├── [ 21K] Mitek 96119739_2 H4.87.pdf
│   │   │   │   └── [ 21K] Mitek 96119739_3 H4.134.pdf
│   │   │   ├── [ 512] Lot 14
│   │   │   │   ├── [ 89K] Mitek 96120095_1 H3.309.pdf
│   │   │   │   ├── [ 21K] Mitek 96120095_2 H4.87.pdf
│   │   │   │   └── [ 21K] Mitek 96120095_3 H4.134.pdf
│   │   │   ├── [ 512] Lot 15
│   │   │   │   ├── [238K] Mitek 96120721_1 H3.309.pdf
│   │   │   │   ├── [ 21K] Mitek 96120721_2 H4.87.pdf
│   │   │   │   └── [238K] Mitek 96120721_3 H4.79.pdf
│   │   │   ├── [ 512] Lot 16
│   │   │   │   ├── [237K] Mitek 96120781_1 H3.309.pdf
│   │   │   │   ├── [ 21K] Mitek 96120781_2 H4.87.pdf
│   │   │   │   └── [ 21K] Mitek 96120781_3 H4.80.pdf
│   │   │   └── [ 512] Lot 17
│   │   │   ├── [237K] Mitek 96121082_1 H3.309.pdf
│   │   │   ├── [ 21K] Mitek 96121082_2 H4.87.pdf
│   │   │   └── [322K] Mitek 96121082_3 H4.134.pdf
│   │   ├── [ 512] 08_Bolt Test Cert
│   │   │   ├── [ 512] BAPP
│   │   │   │   ├── [201K] 35801 LOC.pdf
│   │   │   │   ├── [201K] 35935 LOC.pdf
│   │   │   │   ├── [ 98K] FPC Hollo-Bolt 2812-CPR-0208 18-12-2023.pdf
│   │   │   │   ├── [805K] M12X100 EN 15048 8.8SB HDG - Locked.pdf
│   │   │   │   ├── [1.2M] M12X40 EN15048 8.8SB HDG - Locked.pdf
│   │   │   │   ├── [1.2M] M12X45 EN15048 8.8SB HDG.pdf
│   │   │   │   ├── [182K] M12X50 EN 15048 8.8SB HDG.pdf
│   │   │   │   ├── [2.5M] M16X50 EN15048 8.8SB HDG.pdf
│   │   │   │   └── [804K] M20X50 EN 15048 8.8SB HDG.pdf
│   │   │   └── [ 512] Senco
│   │   │   └── [ 77K] RSM562BFL.pdf
│   │   ├── [ 512] 09_Paint Cert
│   │   │   └── [268K] Marathon Prime Finish- SDS.pdf
│   │   ├── [ 512] 10_Decking Data Sheet
│   │   │   └── [ 512] Berghoef
│   │   │   └── [513K] DOP_Mezzanine U7 Deluxe TG_EN.pdf
│   │   ├── [ 512] 11_RAMS
│   │   │   ├── [279K] MMS-2307-025-HS-1055 Method Statement Schonenberger Cheb.pdf
│   │   │   ├── [648K] MMS-HS-1495 Paint Touch Up Risk Assessment_V2.pdf
│   │   │   ├── [580K] MMS-HS-815 Decking Risk Assessment_V4.pdf
│   │   │   ├── [582K] MMS-HS-820 Mobilisation & De-Mobilisation Risk Assessment_V3.pdf
│   │   │   ├── [595K] MMS-HS-825 Setting Out Risk Assessment_V3.pdf
│   │   │   ├── [602K] MMS-HS-830 Site Deliveries Risk Assessment_V3.pdf
│   │   │   └── [619K] MMS-HS-835 Steel Erection Risk Assessment_V4.pdf
│   │   └── [ 512] 12_Handover
│   │   ├── [1.5M] 1_LOT 1-3 GOH FLOOR HANDOVER.pdf
│   │   ├── [1.5M] 2_LOT 4-5 GOH FLOOR HANDOVER.pdf
│   │   ├── [1.5M] 3_LOTS 6-9 GOH FLOOR HANDOVER..pdf
│   │   ├── [2.7M] 4_LOT 1-11 COMPLETION & FINAL HANDOVER ALL LOTS..pdf
│   │   └── [244K] MMS-2307-025_PM_Handover Check Sheet.pdf
│   ├── [ 512] 02_ABS_safety_line-bezpečnostní_lano
│   │   ├── [ 512] 01_Instructions_pokyny
│   │   │   ├── [ 512] 01_DE_original
│   │   │   │   ├── [4.2M] 01_ABS-Safety-Sicherheitshinweise-DE-EN.pdf
│   │   │   │   ├── [ 29M] 02_Montagedokumentation_DE.pdf
│   │   │   │   ├── [292K] 03_ABS_Basisinformationen_DE.pdf
│   │   │   │   └── [1006K] 04_Pruefbuch_InspectionLog_DE_EN.pdf
│   │   │   └── [ 512] 02_CZ
│   │   │   ├── [1.3M] 01_ABS-Safety-Bezpečnostní pokyny_CZ.pdf
│   │   │   ├── [7.7M] 02_Montážní dokumentace_CZ.pdf
│   │   │   ├── [214K] 03_ABS_Základní informace_CZ.pdf
│   │   │   └── [1.1M] 04_Kontrolní kniha_InspectionLog_CZ.pdf
│   │   ├── [ 512] 02_Drawing
│   │   │   └── [2.0M] 24007341P-25012501Z-3-Plan 4_DE.pdf
│   │   └── [ 512] 03_Certificates
│   │   ├── [584K] Acceptance_report_rev.2_DE.pdf
│   │   ├── [973K] Certification_construction-product_DE.pdf
│   │   └── [289K] Certification_installation_DE.pdf
│   ├── [ 512] 03_DTS_Loading_Loop
│   │   └── [966K] Zertifikat_3834_DE.pdf
│   ├── [ 512] 04_Leantechnik_Loading_Loop
│   │   ├── [ 512] 01_DE_original
│   │   │   ├── [4.8M] 01_Leantechnik_Anleitung_DE.pdf
│   │   │   └── [ 11M] 02_Leantechnik_Gesamtkatalog_DE.pdf
│   │   ├── [ 512] 02_EN
│   │   │   ├── [1.2M] 01_Leantechnik_instructions_EN.pdf
│   │   │   └── [8.7M] 02_Leantechnik-complete-catalogue_EN.pdf
│   │   └── [ 512] 03_CZ
│   │   └── [1.3M] 01_Leantechnik_pokyny_CZ.pdf
│   ├── [ 512] 06_Electrics_elektrika
│   │   ├── [ 512] 01_Instructions_pokyny
│   │   │   └── [ 512] 01_MOTORS_MOTORY
│   │   │   ├── [ 512] 01_SEW
│   │   │   │   ├── [ 512] 01_DE_für_Gesamtanlage
│   │   │   │   │   ├── [ 34M] 01_Motor_25957066_DE.pdf
│   │   │   │   │   ├── [3.5M] 01_Motor_info_30589452_DE.pdf
│   │   │   │   │   ├── [ 10M] 01_Motor_korr_26857987_DE.pdf
│   │   │   │   │   └── [ 21M] 02_Getriebe_31978061_DE.pdf
│   │   │   │   ├── [ 512] 02_EN_for_entire_system
│   │   │   │   │   ├── [ 34M] 01_Motor_25957074_EN.pdf
│   │   │   │   │   ├── [3.6M] 01_Motor_info_30589460_EN.pdf
│   │   │   │   │   ├── [10.0M] 01_Motor_rev_26861313_EN.pdf
│   │   │   │   │   └── [ 20M] 02_Gear_31978088_EN.pdf
│   │   │   │   ├── [ 512] 03_CZ_pro_celé_zařízení
│   │   │   │   │   ├── [ 34M] 01_Motory_25957228_CZ.pdf
│   │   │   │   │   ├── [5.6M] 01_Motory_info_30589606_CZ.pdf
│   │   │   │   │   ├── [8.0M] 01_Motory_oprava_26861453_CZ.pdf
│   │   │   │   │   └── [ 24M] 02_Převody_31988741_CZ.pdf
│   │   │   │   └── [ 512] 04_Movimot_Loading_Loop
│   │   │   │   ├── [ 512] 01_DE_original
│   │   │   │   │   ├── [4.5M] 03_Movi_26865513_DE.pdf
│   │   │   │   │   └── [9.6M] 03_Movimot_DBC_31985297_DE.pdf
│   │   │   │   ├── [ 512] 02_EN_original
│   │   │   │   │   ├── [6.6M] 03_Movi_C_info_26870770_EN.pdf
│   │   │   │   │   └── [9.7M] 03_Movimot_DBC_31985300_EN.pdf
│   │   │   │   └── [ 512] 03_CZ
│   │   │   │   ├── [2.2M] 03_Movi_C_info_26870770_CZ.pdf
│   │   │   │   └── [ 13M] 03_Movimot_DBC_31985300_CZ.pdf
│   │   │   └── [ 512] 02_NORD
│   │   │   ├── [ 512] 01_DE_original
│   │   │   │   ├── [3.9M] 01_Getriebe_LoadingLoopAntr_DE.pdf
│   │   │   │   └── [2.3M] 02_Motor_LoadingLoopAntr_DE.pdf
│   │   │   ├── [ 512] 02_EN_original
│   │   │   │   ├── [3.8M] 01_Gear_LoadingLoopDrive_EN.pdf
│   │   │   │   └── [2.5M] 02_Motor_LoadingLoopDrive_EN.pdf
│   │   │   └── [ 512] 03_CZ
│   │   │   ├── [3.9M] 01_Převod_LoadingLoopPohon_CZ.pdf
│   │   │   └── [2.4M] 02_Motory_LoadingLoopPohon_CZ.pdf
│   │   └── [ 512] 02_Data-sheets
│   │   ├── [ 512] 01_Siemens
│   │   │   ├── [436K] 3LD22031TL53_datasheet_en.pdf
│   │   │   ├── [1.1M] 3LD23051TL13_datasheet_en.pdf
│   │   │   ├── [407K] 3LD28040TK53_datasheet_en.pdf
│   │   │   ├── [539K] 3RA21101AA151BB4_datasheet_en.pdf
│   │   │   ├── [539K] 3RA21101DA151BB4_datasheet_en.pdf
│   │   │   ├── [393K] 3RH29112FA22_datasheet_en.pdf
│   │   │   ├── [484K] 3RT20151BB41_datasheet_en.pdf
│   │   │   ├── [460K] 3RT20162BB42_datasheet_en.pdf
│   │   │   ├── [485K] 3RT20272BB40_datasheet_en.pdf
│   │   │   ├── [278K] 3RT29261BB00_datasheet_en.pdf
│   │   │   ├── [311K] 3RV29255AB_datasheet_en.pdf
│   │   │   ├── [301K] 3SU10000AB100AA0_datasheet_en.pdf
│   │   │   ├── [304K] 3SU10000BB600AA0_datasheet_en.pdf
│   │   │   ├── [320K] 3SU10001GB200AA0_datasheet_en.pdf
│   │   │   ├── [312K] 3SU10010AB500AA0_datasheet_en.pdf
│   │   │   ├── [305K] 3SU10011BA300AA0_datasheet_en.pdf
│   │   │   ├── [312K] 3SU10016AA400AA0_datasheet_en.pdf
│   │   │   ├── [320K] 3SU10022BF100AA0_datasheet_en.pdf
│   │   │   ├── [316K] 3SU10022BM100AA0_datasheet_en.pdf
│   │   │   ├── [357K] 3SU11000AB103CA0_datasheet_en.pdf
│   │   │   ├── [370K] 3SU11001HB203CH0_datasheet_en.pdf
│   │   │   ├── [376K] 3SU11002BF603BA0_datasheet_en.pdf
│   │   │   ├── [389K] 3SU11004BF113BA0_datasheet_en.pdf
│   │   │   ├── [367K] 3SU11020AB403BA0_datasheet_en.pdf
│   │   │   ├── [366K] 3SU11020AB503BA0_datasheet_en.pdf
│   │   │   ├── [367K] 3SU11020AB703BA0_datasheet_en.pdf
│   │   │   ├── [351K] 3SU11026AA203AA0_datasheet_en.pdf
│   │   │   ├── [352K] 3SU11026AA303AA0_datasheet_en.pdf
│   │   │   ├── [320K] 3SU14002AA103BA0_datasheet_en.pdf
│   │   │   ├── [320K] 3SU14002AA103CA0_datasheet_en.pdf
│   │   │   ├── [322K] 3SU14012BB303AA0_datasheet_en.pdf
│   │   │   ├── [322K] 3SU14012BB403AA0_datasheet_en.pdf
│   │   │   ├── [323K] 3SU14012BB503AA0_datasheet_en.pdf
│   │   │   ├── [271K] 3UG46152CR20_datasheet_en.pdf
│   │   │   ├── [416K] 5ST3010_datasheet_en.pdf
│   │   │   ├── [647K] 5SU13246FA10_datasheet_en.pdf
│   │   │   ├── [424K] 5SY42107_datasheet_en.pdf
│   │   │   ├── [447K] 5SY43027_datasheet_en.pdf
│   │   │   ├── [447K] 5SY43067_datasheet_en.pdf
│   │   │   ├── [447K] 5SY43107_datasheet_en.pdf
│   │   │   ├── [446K] 5SY43167_datasheet_en.pdf
│   │   │   ├── [447K] 5SY43327_datasheet_en.pdf
│   │   │   ├── [420K] 5SY62107_datasheet_en.pdf
│   │   │   ├── [239K] 5SY63027_datasheet_en.pdf
│   │   │   ├── [443K] 5SY63067_datasheet_en.pdf
│   │   │   ├── [239K] 5SY63107_datasheet_en.pdf
│   │   │   ├── [239K] 5SY63167_datasheet_en.pdf
│   │   │   ├── [444K] 5SY63257_datasheet_en.pdf
│   │   │   ├── [443K] 5SY63327_datasheet_en.pdf
│   │   │   ├── [369K] 6AV21283UB060AX1_datasheet_en.pdf
│   │   │   ├── [289K] 6EP34368SB000AY0_datasheet_en.pdf
│   │   │   ├── [281K] 6EP34378SB000AY0_datasheet_en.pdf
│   │   │   ├── [306K] 6ES71316BF010AA0_datasheet_en.pdf
│   │   │   ├── [306K] 6ES71316BH010BA0_datasheet_en.pdf
│   │   │   ├── [332K] 6ES71326BH010BA0_datasheet_en.pdf
│   │   │   ├── [285K] 6ES71366BA010CA0_datasheet_en.pdf
│   │   │   ├── [295K] 6ES71366DB000CA0_datasheet_en.pdf
│   │   │   ├── [190K] 6ES71376BD000BA0_datasheet_en.pdf
│   │   │   ├── [303K] 6ES71556AU010BN0_datasheet_en.pdf
│   │   │   ├── [208K] 6ES71936BP200BA0_datasheet_en.pdf
│   │   │   ├── [289K] 6ES71936BP200DA0_datasheet_en.pdf
│   │   │   ├── [327K] 6ES75184FP000AB0_datasheet_en.pdf
│   │   │   ├── [ 94K] 6ES79548LL040AA0_datasheet_en.pdf
│   │   │   ├── [338K] 6GK51160BA002AC2_datasheet_en.pdf
│   │   │   └── [502K] Siemens_3SU_3275_05_202107032151368800.pdf
│   │   ├── [ 512] 02_Leuze
│   │   │   ├── [452K] Datasheet_HT25CI-4P-M12_50134231_en.pdf
│   │   │   ├── [388K] Datasheet_IS-218MM-4NO-8E0-S12_50109701_en.pdf
│   │   │   ├── [393K] Datasheet_IS-218MM-4NO-8N0-S12_50109697_en.pdf
│   │   │   ├── [1.2M] Datasheet_PAL_HRT25B_2500_50134386_en.pdf
│   │   │   ├── [ 74K] Leuze_50109697_CER_CE_IS218(x)_M_de_en_fr.pdf
│   │   │   ├── [ 74K] Leuze_50109701_CER_CE_IS218(x)_M_de_en_fr.pdf
│   │   │   ├── [ 54K] Leuze_50134231_CER_CE_SR25C_de_en_fr.pdf
│   │   │   └── [ 54K] Leuze_50142302_CER_CE_HRT(x)25B_de_en_fr.pdf
│   │   ├── [ 512] 03_Cognex
│   │   │   ├── [ 512] 01_EN_original
│   │   │   │   ├── [670K] Cognex_DM280_Scanner.pdf
│   │   │   │   ├── [6.8M] DM280_Reference_Manual_EN.pdf
│   │   │   │   ├── [8.7M] DM370_Series_Reference_Manual_EN.pdf
│   │   │   │   ├── [3.6M] Datasheet_DM280_en.pdf
│   │   │   │   └── [1.8M] Datasheet_DM370_en.pdf
│   │   │   └── [ 512] 02_CZ
│   │   │   ├── [670K] Cognex_DM280_Scanner.pdf
│   │   │   ├── [8.1M] DM280_Referenční příručka_CZ.pdf
│   │   │   └── [10.0M] DM370_Series_Referenční příručka_CZ.pdf
│   │   ├── [ 512] 04_Helukabel
│   │   │   ├── [1.3M] Datasheet_HELUKABEL_51796_EN.pdf
│   │   │   └── [198K] Datasheet_HELUKABEL_EN.pdf
│   │   ├── [ 512] 05_Werma
│   │   │   ├── [270K] Datasheet_26131070_en.pdf
│   │   │   ├── [419K] Datasheet_64081000_en.pdf
│   │   │   ├── [316K] Datasheet_64580075_en.pdf
│   │   │   ├── [258K] Datasheet_64711075_en.pdf
│   │   │   ├── [240K] Datasheet_64721075_en.pdf
│   │   │   ├── [275K] Datasheet_64731075_en.pdf
│   │   │   └── [277K] Datasheet_64751075_en.pdf
│   │   ├── [ 512] 06_Phoenix
│   │   │   ├── [363K] Datasheet_0804027_en.pdf
│   │   │   ├── [186K] Datasheet_2900299_en.pdf
│   │   │   └── [101K] Datasheet_2908215_en.pdf
│   │   ├── [ 512] 07_Murrelektronik
│   │   │   ├── [223K] Datasheet_58902_en.pdf
│   │   │   ├── [230K] Datasheet_58904_en.pdf
│   │   │   ├── [190K] Datasheet_8000-88590-0000000_en.pdf
│   │   │   └── [233K] Datasheet_9000-41034-0401000_en.pdf
│   │   ├── [ 512] 08_SEW
│   │   │   ├── [ 93K] Datasheet_RF57_DRN80MK4_ASE1_Z_en.pdf
│   │   │   ├── [ 88K] Datasheet_SA57_DRN80MK4_BE1_en.pdf
│   │   │   ├── [ 93K] Datasheet_SA57_DRN80MK4_en.pdf
│   │   │   ├── [ 88K] Datasheet_SA57_DRN80MK4_li_en.pdf
│   │   │   ├── [ 93K] Datasheet_SA57_DRN90S4BE2_en.pdf
│   │   │   ├── [ 88K] Datasheet_SF67_DRN80MK4_Z_en.pdf
│   │   │   ├── [ 90K] Datasheet_WAF59_R37_DRN71M4_BE05_en.pdf
│   │   │   └── [140K] SEW_DRNxx_23114118_Conformity_CE.pdf
│   │   ├── [ 512] 09_Rittal
│   │   │   ├── [102K] Datasheet_2500210-LED system light_en.pdf
│   │   │   ├── [ 75K] Datasheet_2500470-Door-Operated Switch_en.pdf
│   │   │   ├── [357K] Rittal_KX_declaration_of_conformity_20_1852.pdf
│   │   │   └── [621K] Rittal_VX_20_2659.pdf
│   │   ├── [ 512] 10_Nord
│   │   │   └── [ 99K] Datasheet_Motor_SK1SI40-IEC71-71RP_6 TF_IE3_en.pdf
│   │   ├── [ 512] 11_Pepperl+Fuchs
│   │   │   ├── [661K] Datasheet_NBN12-18GM35-E2-0_en.pdf
│   │   │   └── [1.2M] Pepperl_326163-0134_doc4844a.pdf
│   │   ├── [ 512] 12_Bailey
│   │   │   └── [577K] Datasheet_145502_en.pdf
│   │   ├── [ 512] 13_Sick
│   │   │   ├── [544K] Datasheet_WLL180T-F232_6039098_en.pdf
│   │   │   ├── [482K] Datasheet_WLL180T-M432_6039097_en.pdf
│   │   │   └── [156K] Sick_WLL180_Konformität.pdf
│   │   ├── [ 512] 14_Schmersal
│   │   │   └── [1.1M] Datasheet_Z4V7-336-11z-M20_en.pdf
│   │   ├── [ 512] 15_ifm electronic
│   │   │   └── [352K] Datasheet_JN2201-02_en.pdf
│   │   ├── [ 512] 16_Balluff
│   │   │   └── [214K] Balluff_BES M08MH1-PSC30B-S04G-101_CE_BES02PW_PV116429_sig.pdf
│   │   └── [ 512] 17_TRelectronic
│   │   └── [ 61K] TR-Electronic_CES582M-00004_TR-ECE-KE-DGB-0374-06.pdf
│   └── [ 512] 07_Pneumatic_pneumatika
│   ├── [ 512] 01_Aventics
│   │   ├── [ 512] 01_DE-EN_original
│   │   │   ├── [1.0M] 01_IU_Filter_DE_EN.pdf
│   │   │   ├── [999K] 02_IU_Pressure_regulator_valve_DE_EN.pdf
│   │   │   ├── [891K] 03_IU-3-2-shut_off_valve_DE_EN.pdf
│   │   │   └── [562K] 04_IU-Distributor_DE_EN.pdf
│   │   └── [ 512] 02_CZ
│   │   ├── [1.8M] 01_IU_Filter_filtr_CZ.pdf
│   │   ├── [2.1M] 02_IU_Press_reg_valve_Regulační ventil tlaku_CZ.pdf
│   │   ├── [2.0M] 03_IU-3-2-Uzavírací ventil_CZ.pdf
│   │   └── [1.3M] 04_IU-Distrib_Rozdělovač_CZ.pdf
│   └── [ 512] 02_ITS
│   └── [151K] KC-0167-00-01-4 Air distributor.pdf
├── [ 11M] FUNKTION_CONTROL_LoadingLoop_500592_V2.2_EN.docx
├── [ 11M] FUNKTION_STEUERUNG_LoadingLoop_500592_V2.2.docx
├── [ 35M] RB_Fortna_H+M_500592_EV12_20260311.docx
└── [2.7M] RB_Fortna_H+M_500592_V12_20260311.docx
123 directories, 378 files
+17 -3
View File
@@ -21,7 +21,7 @@ Umsetzung: Mista GmbH
| Komponente | Technologie |
|------------------|--------------------------------------------------|
| Build-System | Sphinx (Python) |
| Textformat | reStructuredText (rST) |
| Textformat | reStructuredText (rST) und/oder Markdown (MyST) |
| Templating | Jinja2 (Rahmenkapitel, Titelseite) |
| PDF-Erzeugung | LaTeX (pdflatex/lualatex) |
| Versionierung | Git (Gitea auf Hetzner) |
@@ -102,11 +102,19 @@ transport-docs-base/
│ ├── 01_einleitung/
│ │ ├── template.de.rst # Jinja2-Template mit Platzhaltern
│ │ └── template.en.rst
│ ├── 01_zertifikate/
│ │ ├── konformitaetserklaerung/
│ │ ├── konfigurationsbeschreibung/
│ │ ├── einbauerklaerungen/ # Einbauerklärung + Montageanleitung unvollständiger Maschinen
│ │ └── konformitaetserklaerungen_komponenten/ # DoCs zugekaufter Komponenten
│ ├── 02_sicherheit/
│ │ ├── allgemein.de.rst # Immer dabei
│ │ ├── allgemein.en.rst
│ │ ├── normen_ce.de.rst # EU-Konformität
│ │ └── normen_ukca.en.rst # UK-spezifisch
│ │ ├── normen_ukca.en.rst # UK-spezifisch
│ │ ├── normen/gemeinsame_spezifikationen/ # "gemeinsame Spezifikationen" (VO (EU) 2023/1230, Art. 20)
│ │ ├── normen/sonstige_spezifikationen/ # sonstige angewandte techn. Spezifikationen
│ │ └── risikobewertung/restrisiken/ # explizite Restrisiken-Section
│ ├── 03_anlagenbeschreibung/
│ │ └── template.de.rst # Hier werden Module eingefügt
│ ├── 04_betrieb/
@@ -241,6 +249,8 @@ requires:
- antrieb_sew
```
Module können in `02_sicherheit` neben `risikomatrix` optional auch die Section `restrisiken` referenzieren, um modulspezifische Restrisiken gemäß Anhang IV Teil A VO (EU) 2023/1230 auszuweisen. Einbauerklärungen und Komponenten-Konformitätserklärungen (`01_zertifikate/einbauerklaerungen/`, `01_zertifikate/konformitaetserklaerungen_komponenten/`) sind i. d. R. keine Modul-Sections, sondern projekt-/lieferantenweite Dokumente und werden direkt im Projekt-Repo (`overrides/` bzw. `additional/`) oder in `08_lieferantendoku` abgelegt und von dort referenziert.
### conf.py (Projekt-Konfiguration)
Importiert die Basiskonfiguration vom gemounteten Base-Repo und setzt projektspezifische Overrides.
@@ -316,7 +326,7 @@ Base-Repo hat Semver in `VERSION`. Projekt-Repo kann `"base_version": ">=2.1.0"`
### Drei Ebenen
1. **Textbausteine**: Dateinamenskonvention `{name}.{lang}.rst`. Fallback auf `de` bei fehlender Übersetzung.
1. **Textbausteine**: Dateinamenskonvention `{name}.{lang}.{rst|md}`. Fallback auf `de` bei fehlender Übersetzung.
2. **Bilder**: `localized-figure`-Direktive löst `assets/localized/{lang}/pfad/bild.png` auf. Fallback auf `de`. Optional: Jinja2-SVG-Templates für Diagramme mit Text.
3. **Glossar**: CSV pro Sprache (`terms.{lang}.csv`), `glossary_loader.py` erzeugt Sphinx-Glossardirektiven. Im Text via `:term:`Fachbegriff`` verlinkt.
@@ -324,6 +334,10 @@ Base-Repo hat Semver in `VERSION`. Projekt-Repo kann `"base_version": ">=2.1.0"`
Gesteuert über `normen_region` in anlage.json. Kapitel 02_sicherheit enthält z.B. `normen_ce.de.rst` (EU) und `normen_ukca.en.rst` (UK), Build wählt nach Region.
### rST und Markdown gleichwertig (MyST)
Sphinx liest über die Extension **MyST-Parser** `.rst`- und `.md`-Dateien gleichwertig ein. Beide Dateiformate können projektweit gemischt bzw. gleichwertig verwendet werden – z. B. `beschreibung.de.rst` in einem Modul und `wartung.de.md` in einem anderen, oder auch innerhalb desselben Moduls. Toctree-Einbindung, Sphinx-Direktiven (in MyST via ```` ```{direktive} ```` -Syntax) und die Sprachsuffix-Konvention `{name}.{lang}.{rst|md}` funktionieren für beide Formate identisch. Voraussetzung ist `myst-parser` als Sphinx-Extension in `conf_base.py`. Empfehlung: pro Modul/Kapitel ein einheitliches Format wählen, um die Lesbarkeit im Repo zu erhalten – eine Mischung ist aber technisch unproblematisch und erzwingt keine Migration bestehender rST-Bausteine.
## Bildverwaltung
| Kategorie | Pfad | Beispiele |
+37 -7
View File
@@ -8,7 +8,7 @@ Angepasst auf deutsche Ordnernamen, Sprachtrennung auf Top-Level.
| Nr | Ordner | Inhalt | Fortna-Quelle |
|----|--------|--------|---------------|
| 00 | `00_inhaltsverzeichnis` | Inhaltsverzeichnis, Dokumentennotizen | 00_CONTENTS_NOTES |
| 01 | `01_zertifikate` | CE/UKCA-Konformitaetserklaerungen, Konfigurationsbeschreibung | 01_CERTIFICATES |
| 01 | `01_zertifikate` | CE/UKCA-Konformitaetserklaerungen, Konfigurationsbeschreibung, Einbauerklaerungen, Komponenten-DoCs | 01_CERTIFICATES |
| 02 | `02_sicherheit` | Allgemeine Sicherheit, Normen, Risikobewertung | (neu, teilweise 04_TECHNICAL_DOC) |
| 03 | `03_anlagenbeschreibung` | Maschinenmodule (via meta.yaml eingefuegt) | (neu) |
| 04 | `04_betrieb` | Betriebsanleitung | 02_INSTRUCTIONS (Betriebsanleitung) |
@@ -29,23 +29,34 @@ Wird vom Build-Prozess generiert. Enthaelt Gesamtinhaltsverzeichnis und ggf. Dok
```text
01_zertifikate/
konformitaetserklaerung/ # CE-/UKCA-Erklaerungen (Fortna: 01_SCHOENENBERGER)
konfigurationsbeschreibung/ # Konfigurationsbeschreibung fuer CE (Fortna: 02_KONFIGURATIONSBESCHREIBUNG)
konformitaetserklaerung/ # CE-/UKCA-Erklaerungen (Fortna: 01_SCHOENENBERGER)
konfigurationsbeschreibung/ # Konfigurationsbeschreibung fuer CE (Fortna: 02_KONFIGURATIONSBESCHREIBUNG)
einbauerklaerungen/ # NEU: Einbauerklaerungen + Montageanleitungen fuer eingebaute unvollstaendige Maschinen
konformitaetserklaerungen_komponenten/ # NEU: Sammlung der DoCs zugekaufter Komponenten (Querverweis auf 08_lieferantendoku)
```
Fortna-Beispieldokumente:
- EC Declaration of Conformity_V2_EN.pdf
- Konfigurationsbeschreibung_fuer_CE_V2.pdf
**Ergaenzung nach Anhang IV, Teil A, VO (EU) 2023/1230:** Die neuen Unterordner `einbauerklaerungen/` und `konformitaetserklaerungen_komponenten/` decken zwei Pflichtangaben ab, die im bisherigen Kapitelzuschnitt keinen festen Platz hatten:
- Einbauerklaerung + Montageanleitung fuer eingebaute unvollstaendige Maschinen (z. B. zugekaufte Antriebe/Steuerungen, die selbst als unvollstaendige Maschine geliefert werden)
- Konformitaetserklaerungen zugekaufter Komponenten – bisher nur implizit verstreut in `08_lieferantendoku/*` (nach Gewerk sortiert, nicht nach Dokumenttyp)
### 02_sicherheit
```text
02_sicherheit/
allgemein/ # Allgemeine Sicherheitshinweise
normen/ # CE (EU), UKCA (UK) - je nach normen_region in anlage.json
risikobewertung/ # Risikomatrizen, Gefahrenanalysen
allgemein/ # Allgemeine Sicherheitshinweise
normen/ # Harmonisierte Normen, CE (EU), UKCA (UK) - je nach normen_region in anlage.json
gemeinsame_spezifikationen/ # NEU: "gemeinsame Spezifikationen" (Begriff aus VO (EU) 2023/1230, Art. 20)
sonstige_spezifikationen/ # NEU: sonstige angewandte techn. Spezifikationen, falls keine harmonisierte Norm existiert
risikobewertung/ # Risikomatrizen, Gefahrenanalysen
restrisiken/ # NEU: explizite Restrisiken-Section (bisher nur implizit in der Risikomatrix)
```
**Ergaenzung nach Anhang IV, Teil A:** Anhang IV verlangt explizit die Referenzen der angewandten harmonisierten Normen *oder gemeinsamen Spezifikationen* (bei Teilanwendung mit Angabe der verwendeten Teile), sowie – falls keine harmonisierten Normen angewandt wurden – die sonstigen angewandten technischen Spezifikationen. Bisher gab es dafuer nur den allgemeinen Ordner `normen/`. Ebenso verlangt Anhang IV eine Beschreibung der Restrisiken als Teil der Risikobeurteilungsunterlagen – dafuer bisher keine eigene Section, nur implizit in der Risikomatrix enthalten.
### 03_anlagenbeschreibung
Keine feste Unterstruktur - Module werden hier durch den Build-Prozess via meta.yaml eingefuegt. Jedes Modul liefert seine Beschreibung, technische Daten etc.
@@ -253,4 +264,23 @@ Unterschriebene Validierungsprotokolle, bestehend aus:
- [ ] Wie läuft die Ersatzteilfindung nach der Dokumentation statt?
- [ ] Wie die Behandlung einer Anlage bei Fehler oder Störungen?
- [ ] Welche Daten des Produktprogrammes sollen in die Dokumentation eingang finden?
- [ ] Welche Abteilung liefert normalerweise was in der Dokumentation (Liste Automation ist vorhanden, Was passiert bei Sonderkonstruktionen)?
- [ ] Welche Abteilung liefert normalerweise was in der Dokumentation (Liste Automation ist vorhanden, Was passiert bei Sonderkonstruktionen)?
- [ ] Soll die Rechtsgrundlage (MRL 2006/42/EG vs. VO (EU) 2023/1230) als Feld in `anlage.json` gefuehrt werden (analog zu `normen_region`), damit Textbausteine und Kapitelverweise projektspezifisch zwischen "Anhang I MRL" und "Anhang III/IV MVO" umschalten koennen? Relevant spaetestens ab Pflichtanwendung der MVO am 20.01.2027; Altprojekte (z. B. Fortna) bleiben MRL-referenziert.
## Ergaenzungsbedarf durch VO (EU) 2023/1230, Anhang IV Teil A
Anhang IV Teil A ersetzt Anhang VII der MRL 2006/42/EG und listet die Mindestinhalte der technischen Unterlagen fuer vollstaendige Maschinen. Abgleich mit der bestehenden Kapitelstruktur (Details siehe Abschnitte `01_zertifikate` und `02_sicherheit` oben):
| Anhang IV Teil A – Anforderung | Bereits abgedeckt durch | Ergaenzungsbedarf |
|---|---|---|
| Beschreibung der Maschine + bestimmungsgemaesse Verwendung | `03_anlagenbeschreibung` | Bestimmungsgemaesse Verwendung als Pflichtabschnitt je Modul sicherstellen |
| Risikobeurteilungsunterlagen inkl. GSGA-Liste, Schutzmassnahmen, Restrisiken | `02_sicherheit/risikobewertung` | Restrisiken als eigene Section (`restrisiken/`, s. o.) |
| Entwurfs-/Fertigungszeichnungen, Plaene | `09_technische_unterlagen/layouts`, `schaltplaene` | keiner |
| Erlaeuterungen zu Zeichnungen/Funktionsweise | `06_steuerung/*` | fuer rein mechanische Baugruppen ohne Steuerung ggf. in `03_anlagenbeschreibung` ergaenzen |
| Referenzen harmonisierter Normen/gemeinsamer Spezifikationen | `02_sicherheit/normen` | Unterordner `gemeinsame_spezifikationen/` (s. o.) |
| Sonstige angewandte technische Spezifikationen | – | Unterordner `sonstige_spezifikationen/` (s. o.) |
| Pruefberichte | `10_anhang/pruefberichte` | keiner |
| Exemplar der Betriebsanleitung | `04_betrieb/betriebsanleitung` | keiner |
| Einbauerklaerung + Montageanleitung fuer eingebaute unvollstaendige Maschinen | – | Neuer Ordner `01_zertifikate/einbauerklaerungen/` (s. o.) |
| Konformitaetserklaerungen zugekaufter Komponenten | bisher verstreut in `08_lieferantendoku/*` | Neuer Ordner `01_zertifikate/konformitaetserklaerungen_komponenten/` (s. o.) |
| Kopie der eigenen EU-Konformitaetserklaerung | `01_zertifikate/konformitaetserklaerung` | Ordner existiert, aktuell nur Platzhalter (`.gitkeep`) – inhaltlich zu befuellen |
+99
View File
@@ -0,0 +1,99 @@
# Skripte in lib/
Übersicht über die Python-Hilfsskripte zur Datenaufbereitung (Excel/PDF/Word -> JSON/CSV/RST).
Alle Skripte werden über `bin/*.bat` bzw. `bin/*.sh` aufgerufen und nutzen die
Umgebungsvariablen aus `bin/setenv.*` (`PROJECT`, `PV_EXAMPLES`, `PV_DATA`, ...).
## jsonFromXlsx.py
Baut aus der Positionsspalte einer Excel-Stückliste (`Stk.-Liste SOLL` / `Stk.-Liste IST`)
eine hierarchische JSON-Baumstruktur. Positionspfade wie `10/5/1` bestimmen die Schachtelung,
als Schlüssel dient die Teilenummer (bei Duplikaten ergänzt um Bezeichnung). Sonderfälle:
- **Längenabhängige Teile** (`LAENGENABHAENGIG`-Liste, z. B. Kettenprofile): Menge wird als
`laenge` aufsummiert statt gezählt.
- **9-stellige Sivas-Nummern**: doppelte Vorkommen werden zu `{"anzahl": n, "beschreibung": ...}`
aggregiert.
```
python jsonFromXlsx.py --filename Fortna --tab soll
python jsonFromXlsx.py --all
```
Eingabe: `examples/*.xlsx` · Ausgabe: `results/<name>_<tab>.json`
## extract_ids.py
Liest die von `jsonFromXlsx.py` erzeugten JSON-Bäume und sammelt 9-stellige Sivas-Nummern,
die genau eine Ebene über den Blättern liegen (d. h. Baugruppen, deren Kinder alle Blätter sind).
Kann Ergebnisse mehrerer Projekte zu einer Summary zusammenfassen.
```
python extract_ids.py --filename KA135776
python extract_ids.py --all --sum2json
python extract_ids.py --all --sum2txt
```
Eingabe: `results/*_<tab>.json` · Ausgabe: `results/<name>_ids_<tab>.txt` bzw.
`results/summary_ids_<tab>.{json,txt}`
## project_from_xlsx.py
Durchsucht rekursiv Unterordner in `PV_EXAMPLES`, liest je Ordner die erste `.xlsx` aus dem
Reiter `Stk.-Liste IST` (Position/Teilenummer/Bezeichnung/Menge) und filtert auf Positionspfade
mit 3 oder 4 Hierarchieebenen. Optional werden pro Ordner JSON/CSV/XLSX-Ergebnisdateien erzeugt;
die XLSX-Variante aggregiert Mengen je Teilenummer und markiert bekannte Produktnummern aus
`cfg/product_numbers.cfg`.
```
python project_from_xlsx.py --example-dir KA135776 --2csv
python project_from_xlsx.py --2json --2xlsx
```
## extract_products.py
Extrahiert Bilder und Fließtext aus Produktprogramm-PDFs (`data/Produktprogramm/*.pdf`) mittels
PyMuPDF. Bilder werden nach Zielordner (Omniflo/ILS/CPC/Gerüst/Trolley, siehe `PDF_TARGETS`)
sortiert und per MD5 dedupliziert. Text wird anhand von Schriftgröße, Positions- und
Nummerierungsheuristiken (inkl. TOC-Abgleich) in `.de.rst`-Dateien mit Überschriftenhierarchie
umgewandelt.
```
python extract_products.py --data-dir data
```
Ausgabe: `data/Produktprogramm/images/<ziel>/` und `data/Produktprogramm/<pdf-name>.de.rst`
## collect_partnumbers.py
Sammelt 9-stellige Teilenummern aus den von `extract_products.py` erzeugten RST-Dateien,
inklusive Bereichsnotation (`834372007-011`) und OCR-bedingt verklebter Zahlen. Gleicht die
Nummern gegen eine Excel-Stückliste (`data/KA*.xlsx`, Reiter `Stk.-Liste SOLL`) ab und schreibt
eine CSV mit Fundstelle, Kapitel und Bezeichnung.
```
python collect_partnumbers.py --data-dir data
```
Ausgabe: `data/Produktprogramm/teilenummern.csv`
## convert_to_rst.py
Konvertiert Word-Dokumente (`.docx`, via `mammoth`) und Word-erstellte PDFs (via `pdfminer`,
erkannt über PDF-Metadaten Creator/Producer) nach `.rst`. Ordnet Quelldateien anhand des
Dateinamens automatisch einem Zielkapitel unter `chapters/` zu (`CHAPTER_MAPPING`), erkennt
benutzerdefinierte Word-Formatvorlagen (`Ü1`-`Ü4` etc.) als Überschriftenebenen und speichert
eingebettete Bilder neben der Ziel-RST.
```
python convert_to_rst.py <input_dir> chapters --dry-run
python convert_to_rst.py <input_dir> chapters --lang de
```
## Typischer Ablauf
1. `jsonFromXlsx.py --all` – Stücklisten als JSON-Bäume aufbereiten
2. `extract_ids.py --all --sum2json` – Sivas-Nummern projektübergreifend sammeln
3. `extract_products.py` – Produktprogramm-PDFs in Bilder + RST zerlegen
4. `collect_partnumbers.py` – Teilenummern aus den RSTs mit der Stückliste abgleichen
5. `convert_to_rst.py` – vorhandene Word/PDF-Dokumentation in die Kapitelstruktur überführen
+295
View File
@@ -0,0 +1,295 @@
# Konzept: Fortna-Extraktion nach chapters/ + Projektordner fortna/
Status: **Umgesetzt** (erster vollständiger Durchlauf inkl. PDF-Build). Antworten aus der
Rückfragerunde sind eingearbeitet (siehe Abschnitt "Entscheidungen"). Der tatsächliche
Ist-Stand ist unten unter "Umsetzung / Ist-Stand" dokumentiert.
## Umsetzung / Ist-Stand
Implementiert wurden vier Python-Skripte in `lib/` mit `bin/`-Wrappern (`.bat` + `.sh`):
| Skript | Zweck | Wrapper |
|---|---|---|
| `lib/fortna_inventory.py` | Scannt das Rohmaterial, ordnet jede Datei Sprache + Zielkapitel + Aktion zu, schreibt `results/fortna_inventory.json`. | `bin/fortna_inventory.*` |
| `lib/fortna_convert_chapters.py` | Volltext+Bilder für die 5 DE-Hauptanleitungen, Verzeichnislisten (`index.de.rst`) für alle übrigen Kapitel. `--dry-run` (Default) / `--apply`. | `bin/fortna_convert_chapters.*` |
| `lib/fortna_init_project.py` | Legt `projekte/fortna/` an (`anlage.json`, `overrides/`, `images/`, `README.md`). | `bin/fortna_init_project.*` |
| `lib/build_index.py` | Generiert `index.rst` mit toctrees aus allen `chapters/**/*.de.rst`. | (in `build_pdf.*`) |
| `bin/build_pdf.*` | Kompletter PDF-Build: index → `sphinx-build -b latex` → `latexmk -lualatex`. | — |
Ergebnis des ersten Laufs:
- 5 Hauptanleitungen volltextig konvertiert (Betriebs-/Wartungsanleitung, Funktionsbeschreibung
ILS/OMNIFLO/LoadingLoop), zusammen ~13.500 Textblöcke + mehrere Hundert entduplizierte Bilder.
- 9 Verzeichnislisten erzeugt (Zertifikate, Lieferantendoku nach Gewerk, Notstopp, Prüfberichte).
- `projekte/fortna/` mit `anlage.json`-Platzhalter angelegt.
- **Vollständiger PDF-Build erfolgreich**: `_build/latex/Fortna_Dokumentation_DE.pdf`, ~670 Seiten,
`latexmk`-Exit 0, keine LaTeX-Fehler.
### Umgebung / Build-Voraussetzungen (bereits erfüllt)
- Sphinx 9.1.0 + myst-parser 5.1.0 im `.venv` (ergänzt in `requirements.txt`).
- LaTeX: MiKTeX unter `C:\Program Files\MiKTeX\` (lualatex/latexmk vorhanden).
- `conf.py` erzeugt: LaTeX-Engine **lualatex**, **babel** statt polyglossia
(polyglossia 2.13 wirft unter lualatex einen bidi-Fehler), **`iconpackage=none`**
(FontAwesome7 in MiKTeX bricht unter lualatex ab). Beides in `conf.py` kommentiert.
### Extraktion: neues Modul `lib/pdf_extract.py`
Die Volltext-Konvertierung der 5 Hauptanleitungen läuft über das neue Modul
`lib/pdf_extract.py` (`extract_manual()`), das gegenüber der ersten Version deutlich
sauberere `.rst` erzeugt:
- **Keine doppelte Nummerierung mehr:** Die im PDF vorhandene Gliederungsnummer
(z. B. „1.7 Wichtige Informationen") wird nur zur Bestimmung der Überschriften-Ebene
ausgewertet und im RST-Text **entfernt**. Sphinx/LaTeX nummeriert selbst → vorher
„3.2.1 1.1 Wichtige Informationen", jetzt nur noch „3.2.1 Wichtige Informationen".
- **Überschriften-Erkennung** nur bei führender PDF-Gliederungsnummer (kein „fett allein"),
damit Tabellenzellen nicht fälschlich zu Überschriften werden.
- **Bilder positionsbasiert extrahiert und referenziert:** pro Seite werden Text und Bilder
nach Y-Position verwoben; jedes Bild wird als `.. figure::` mit korrektem Pfad und – falls
vorhanden – der folgenden „Abbildung N: …"-Zeile als Caption eingebaut. (625 figure-Refs,
0 fehlende Bilddateien.) Duplikate werden per MD5 entfernt, Titelseiten-Deko und
Vollseiten-Hintergründe herausgefiltert.
- **Symbole/Warnzeichen zentral:** Kleine (≤ 260 px), mehrfach vorkommende Bilder (Warndreiecke,
Piktogramme) werden per Perceptual-Hash (aHash + Hamming-Distanz ≤ 6) erkannt, **einmal** zentral
nach `assets/shared/symbols/symbol_<hash>.png` abgelegt und im RST als `.. image::` (12 mm,
zentriert) auf die eine Quelle verwiesen – dokumentübergreifend. Auch großformatige
Inhaltsbild-Duplikate werden über den aHash zusammengeführt (nur einmal abgelegt, mehrfach
referenziert).
- **Bild-Routing nach Kapitel-Typ + Dateityp:** Generelle Kapitel (Steuerungs-Funktionsbeschreibung
ILS/OMNIFLO/LoadingLoop) → `assets/shared/images/<subdir>` (wiederverwendbar). Projektbezogene
Kapitel (Betrieb/Wartung) → `projekte/fortna/images/<subdir>`. **Fotos (.jpg) immer**
projektspezifisch (`projekte/fortna/images/`), unabhängig vom Kapitel. Ziel-Bildordner werden
vor jedem Lauf geleert.
- **Bild-Benennung:** PNG mit direkt folgender Bildunterschrift → `<Stichwort>_NN.png` (z. B.
`Anlage_Uebersicht_01.png`, `Komponentenstatus_Barcode_Leser_01.png`). Fotos → `img_NNN.jpg`,
schwarz-weiße Skizzen (> 10 KB) → `sketch_NNN.png`, Rest → `img_NNN.png`.
- **Logos gesondert:** Nur wiederkehrende (≥ 3×) breite Grafiken im Seitenrand gelten als Logo
(unterscheidet Kopf-/Fußzeilen-Logo von einmaligen breiten Diagrammen) → alle Auflösungen nach
`assets/shared/logos/logo_<n>_<breite>px.png`, **nicht** inline referenziert (Deko).
- **Wertlose Fragmente verworfen:** flache (≤ 55 px), breite, schwarz-weiße Schnipsel sind meist
gerenderte Kapitelnummern („5.2.6", „3.2.3") und werden gar nicht extrahiert.
- **Produktprogramm-Bilder:** `lib/extract_products.py` legt die Bilder der Produktprogramm-PDFs
(`data/Produktprogramm/*.pdf`) als generelle Produktbilder nach
`assets/shared/images/produktprogramm/{omniflo,ils,cpc,geruest,trolley}` ab.
- **Versionierung:** Extrahierte Bildmassen (`assets/shared/images/`, `projekte/fortna/images/`)
sind reproduzierbar und via `.gitignore` ausgeschlossen; nur die kuratierten
`assets/shared/symbols/` und `assets/shared/logos/` werden versioniert.
- **Fragment-„Überschriften" unterdrückt:** Bildlegenden/Tabellenzellen mit führender Nummer
(„POS", „li.", „re.", „bar, max. 10 bar") werden nicht mehr als Überschrift gesetzt.
- **Fließtext zu Absätzen zusammengeführt:** PDF-Zeilenumbrüche werden nicht mehr zu je einem
Absatz; zusammenhängende Zeilen bilden einen Absatz, Satzende beendet ihn.
- **Aufzählungen als RST-Listen:** einzeln stehende Bullet-Zeichen werden mit der Folgezeile
zusammengeführt, in eine Zeile zusammengelaufene Bullets aufgesplittet → `-`-Listenpunkte.
- **Änderungshistorie-/Dokumentinfo-Tabellen** am Dokumentanfang werden übersprungen (zerfielen
vorher in sinnlose Einzelzellen), ebenso Inhaltsverzeichnis-Seiten und Kopf-/Fußzeilen.
### Behobene Extraktionsprobleme
- **Symbolfont-Zeichen** (Wingdings/Symbol Private-Use-Area, z. B. Aufzählungs-Bullet U+F0B7):
via `clean_pua()` auf echte Unicode-Zeichen (•, →, ✓) gemappt, restliche PUA-Zeichen entfernt.
Vorher erschienen leere Kästchen im PDF. (In `lib/pdf_extract.py`; die Variante in
`extract_products.py` bleibt für die Produktprogramm-Extraktion bestehen.)
- **Fremd-Altdateien** aus früheren `convert_to_rst.py`-Testläufen (NORD-Getriebe/-Motor im
Anhang, alte Risikobewertung/Contents/Konfig/Ersatzteile-Volltexte) wurden entfernt; die
betroffenen Kapitel liegen konzeptkonform als Verzeichnisliste vor.
### Bekannte Restpunkte (nicht Teil des Auftrags-Scopes)
- Zwei **Fremd-Altdateien** aus früheren `convert_to_rst.py`-Testläufen sind noch im Baum und
vom Build ausgeschlossen (`conf.py` + `build_index.py` SKIP-Liste):
`02_sicherheit/risikobewertung/rb_fortna_...de.rst` (unvollständiger Bildbestand) und
`00_inhaltsverzeichnis/contens_obsah_...de.rst` (keine RST-Überschrift). Zwei weitere
Altdateien in `10_anhang/` (Motor/Getriebe) sind noch im toctree und zeigen typische
Extraktionsartefakte – bei Bedarf mit `fortna_convert_chapters.py` sauber neu erzeugen.
- **Tabellen** (z. B. Änderungshistorie) werden weiterhin als Einzelzeilen extrahiert, nicht als
RST-Tabelle rekonstruiert (wie im Konzept angekündigt).
---
## Ausgangslage
- Reales Fortna-Rohmaterial liegt in `c:\10-Develop\projekte\Fortna\` (zusätzliches
Arbeitsverzeichnis dieser Session, **nicht** im Repo). Struktur wie in `doc/Fortna.txt`
dokumentiert (00_CONTENTS_NOTES … 05_SUPPLIER_DOC), Versionsstände dort sind aktueller als in
`doc/Fortna.txt` (z. B. Betriebsanleitung V2.4 statt V2.3, Wartungsanleitung V2.3, ILS V2.2,
OMNIFLO V1.1, LoadingLoop V2.3). `doc/Fortna.txt`/`doc/Fortna-size.txt` sind nur noch als
Struktur-Referenz zu verstehen, nicht als Quelle für den Extraktionslauf.
- `chapters/` (Base-Repo-Gerüst) existiert bereits vollständig mit `.gitkeep`-Platzhaltern gemäß
`doc/kapitelstruktur.md`. `06_steuerung/loading_loop/` wurde testweise mit `convert_to_rst.py`
aus der LoadingLoop-Word-Datei befüllt (2 RST + 74 Bilder). Qualität ist noch unzureichend:
Kopf-/Fußzeilen (Firmenadresse, Kontaktdaten) landen als normale Absätze im Fließtext,
Tabellen (Änderungshistorie) werden zu einzelnen Zeilen ohne Tabellenstruktur zerlegt. Das ist
ein bekanntes Problem der aktuellen `_html_to_blocks`-Logik in `convert_to_rst.py` und wird im
Zuge dieser Arbeit mit adressiert (siehe Abschnitt "Bekannte Schwachstellen").
- Vorhandene Skripte in `lib/` (siehe `doc/lib_skripte.md`):
- `convert_to_rst.py`: `.docx`/Word-PDF → RST, erkennt Zielkapitel nur anhand Dateiname-Pattern
im Root eines Ordners, kein Verständnis für den verschachtelten Fortna-Sprachordner-Baum.
- `extract_products.py`: Produktprogramm-PDFs → Bilder + RST (bereits fertig genutzt für
`data/Produktprogramm`).
- `jsonFromXlsx.py`, `extract_ids.py`, `project_from_xlsx.py`: Stücklisten-Auswertung, für
diese Aufgabe nicht direkt relevant.
- Kein Skript kennt bisher die komplette Fortna-Ordnerstruktur (Lieferantendoku-Baum, Layouts,
Schaltpläne, Prüfberichte) und keines erzeugt den projektspezifischen Ordner (`anlage.json`,
`overrides/`, `images/`) gemäß `doc/Kernkonzepte.md`.
## Entscheidungen (aus Rückfragen)
| Frage | Entscheidung |
|---|---|
| Lieferantendoku (~250 PDFs: SEW, Siemens, Leuze, Cognex, MiTek, Aventics, …) | **Nur Verzeichnisliste**, keine Kopie. RST-Referenztabelle mit Pfaden auf das Originalverzeichnis. |
| Sonstige PDFs (Zertifikate, Konfigurationsbeschreibung, Layouts, Schaltpläne, Notstopp/Bereichsstopp, Prüfberichte, Ersatzteilliste) | Gleiche Behandlung wie Lieferantendoku: **nur Verzeichnisliste**, keine Kopie nach `chapters/`. |
| Volltext-Konvertierung (Text + Bilder → RST) | **Nur die 5 Hauptanleitungen**: Betriebsanleitung, Wartungsanleitung, Funktionsbeschreibung ILS, Funktionsbeschreibung OMNIFLO, Funktionsbeschreibung LoadingLoop. |
| Projektordner-Ort | `projekte/fortna/` **innerhalb dieses Repos** (docu_build), pragmatisch für jetzt; spätere Auslagerung in eigenes Repo bleibt möglich. |
| Sprachumfang | **Nur Deutsch** in diesem Durchlauf. Namenskonvention `{name}.de.rst` einhalten, EN/CZ als späterer zweiter Durchlauf vorgesehen. |
| Quelle | `c:\10-Develop\projekte\Fortna\` (aktueller Stand), nicht `doc/Fortna.txt`. |
| Umgang mit bestehendem `chapters/06_steuerung/loading_loop/` | **Überschreiben/neu erzeugen** – konsistent aus einem Guss statt Altbestand stehen lassen. |
## Zielstruktur (Ergebnis nach Lauf)
```text
chapters/
01_zertifikate/
konformitaetserklaerung/index.de.rst # Verzeichnisliste -> Originalpfade
konfigurationsbeschreibung/index.de.rst
02_sicherheit/ # unverändert (kein Fortna-Rohmaterial 1:1 zuordenbar)
03_anlagenbeschreibung/ # unverändert (Modul-Mechanismus, nicht Teil dieser Aufgabe)
04_betrieb/betriebsanleitung/
betriebsanleitung_500592_135776_v2_4.de.rst # Volltext + Bilder
images/img_001.png ...
05_wartung/wartungsanleitung/
wartungsanleitung_500592_v2_3.de.rst # Volltext + Bilder
images/...
06_steuerung/
ils/funktion_steuerung_ils_500592_v2_2.de.rst # Volltext + Bilder
omniflo/funktion_steuerung_omniflo_500592_v1_1.de.rst
loading_loop/funktion_steuerung_loadingloop_500592_v2_3.de.rst # neu erzeugt, ersetzt Altbestand
07_ersatzteile/index.de.rst # Verzeichnisliste -> Originalpfad SPARE_PARTS PDF
08_lieferantendoku/
antriebe/index.de.rst # Verzeichnisliste SEW, NORD, Leantechnik, DTS
elektrik/index.de.rst # Siemens, Leuze, Cognex, Werma, ...
pneumatik/index.de.rst # Aventics, ITS
stahlbau/index.de.rst # MiTek (As-Built, Statik, Zertifikate, ...)
sicherheitseinrichtungen/index.de.rst # ABS Sicherheitslinie
09_technische_unterlagen/
layouts/mechanisch/index.de.rst
layouts/elektrisch/index.de.rst
schaltplaene/index.de.rst
notstopp/index.de.rst
bereichsstopp/index.de.rst
10_anhang/
pruefberichte/index.de.rst
projekte/
fortna/
anlage.json # Projektmetadaten (Platzhalter/Vorschlag, manuell nachzuschärfen)
overrides/ # leer angelegt, Struktur gemäß Kernkonzepte.md
images/ # leer angelegt (projektspezifische Bilder, Kundenlogo etc.)
README.md # kurze Erläuterung was hier reinkommt
```
Jede `index.de.rst`-Verzeichnisliste enthält eine Tabelle: Dateiname, Hersteller/Kategorie
(aus Ordnername abgeleitet), Originalpfad (relativ zu einer konfigurierbaren Basis, z. B.
`PV_FORTNA_SOURCE`), Dateigröße. Kein PDF wird kopiert oder eingebettet – das hält den
Kapitelbaum schlank und vermeidet Redundanz zum Rohmaterial, das ohnehin an anderer Stelle
(Nextcloud/Projektordner) verwaltet wird.
## Geplante Skripte (in `lib/`)
### 1. `fortna_inventory.py` (neu)
Scannt `c:\10-Develop\projekte\Fortna\` rekursiv und baut ein Inventar (JSON) aller Dateien mit:
Pfad, Größe, erkannte Sprache (aus Ordnername: `*_DE*`/`*_EN*`/`*_CZ*`/`*original*` → de),
vermutetem Fortna-Kapitel (oberste zwei Ebenen) und vermutetem Ziel-Kapitel gemäß Mapping-Tabelle
aus `doc/kapitelstruktur.md`. Dient als Kontrollartefakt (`results/fortna_inventory.json`), auf
dessen Basis sich beide folgenden Skripte konfigurieren bzw. dessen Output sich manuell prüfen
lässt, bevor tatsächlich geschrieben wird (`--dry-run` Default, `--apply` zum Ausführen).
Grund für ein separates Inventarskript: 387 Dateien über 6 Top-Level-Ordner mit inkonsistenter
Sprachordner-Benennung (`01_DE_original`, `01_EN_original`, `1_EN_original` [Tippfehler im
Quellbaum], `02_CZ`, `03_CZ_pro_celé_zařízení`, …) – ein Trockenlauf mit Report ist nötig, um
Mapping-Lücken zu erkennen, bevor Dateien verschoben/verarbeitet werden.
### 2. `fortna_convert_chapters.py` (neu)
Nutzt das Inventar aus Schritt 1 (oder scannt bei Bedarf direkt) und führt zwei Aktionsarten aus:
- **Volltext-Konvertierung** für die 5 Hauptanleitungen: ruft die vorhandene Konvertierungslogik
aus `convert_to_rst.py` wieder, aber mit expliziter Quelldatei-Liste (kein Verzeichnis-Scan)
und fest zugeordnetem Zielkapitel (aus der Tabelle unten), statt der generischen
`CHAPTER_MAPPING`-Heuristik. Überschreibt bestehende Ziel-RSTs/Bilder vollständig
(Ordnerinhalt vorher leeren, dann neu schreiben).
- **Verzeichnislisten-Generierung** für alle übrigen Kapitel (Zertifikate, Layouts, Schaltpläne,
Notstopp/Bereichsstopp, Prüfberichte, Ersatzteile, Lieferantendoku): läuft über die im
Inventar erkannten Dateien je Zielordner und rendert eine `index.de.rst` mit
RST-list-table (Dateiname, Pfad, Größe).
Fest zugeordnete Hauptanleitungen (Quelle → Ziel):
| Quelldatei (DE, aktuellster Stand in `projekte/Fortna`) | Zielkapitel |
|---|---|
| `02_INSTRUCTIONS.../01_DE_original/01_BETRIEBSANLEITUNG_500592_135776_V2.4.pdf` | `04_betrieb/betriebsanleitung/` |
| `02_INSTRUCTIONS.../01_DE_original/02_WARTUNGSANLEITUNG_500592_V2.3.pdf` | `05_wartung/wartungsanleitung/` |
| `02_INSTRUCTIONS.../01_DE_original/03_FUNKTION_STEUERUNG_ILS_500592_V2.2.pdf` | `06_steuerung/ils/` |
| `02_INSTRUCTIONS.../01_DE_original/04_FUNKTION_STEUERUNG_OMNIFLO_500592_V1.1.pdf` | `06_steuerung/omniflo/` |
| `02_INSTRUCTIONS.../01_DE_original/05_FUNKTION_STEUERUNG_LOADING_LOOP_500592_V2.3.pdf` | `06_steuerung/loading_loop/` (ersetzt Altbestand) |
Alle fünf sind PDFs (keine `.docx` im Quellordner mehr vorhanden), daher wird der
`is_word_pdf()`-Pfad aus `convert_to_rst.py` verwendet (pdfminer-Textextraktion). Die extra
`extract_products.py`-Textheuristik (Schriftgrößen-basierte Überschriftenerkennung,
Kopf-/Fußzeilen-Ausschluss nach Y-Position) ist für Word-erstellte PDFs wie diese *robuster* als
die aktuelle `convert_to_rst.py`-Textheuristik (siehe Bekannte Schwachstellen) – Vorschlag: die
Extraktionsroutine aus `extract_products.py` (`extract_text_to_rst`, `_is_noise`,
`extract_images`) wiederverwenden/verallgemeinern statt der schwächeren `pdfminer`-Variante aus
`convert_to_rst.py`.
### 3. `fortna_init_project.py` (neu)
Legt `projekte/fortna/` an: `anlage.json` (Platzhalter mit Projektnummer 500592/KA135776,
Sprache `["de"]`, `normen_region` offen als TODO), leere `overrides/`- und `images/`-Ordner mit
`.gitkeep`, sowie ein kurzes `README.md`. Einmaliger Lauf, kein Datei-Scan nötig – im
Wesentlichen ein Gerüst-Generator.
## Bekannte Schwachstellen der bisherigen Konvertierung (zu beheben)
Der Altbestand in `chapters/06_steuerung/loading_loop/` zeigt zwei konkrete Probleme der
aktuellen `convert_to_rst.py`-Logik, die für die 5 Hauptanleitungen behoben werden sollten:
1. **Kopf-/Fußzeilen als Fließtext**: Firmenadresse, Kontaktdaten, Projektnummer erscheinen als
normale Absätze mitten im Dokument (weil `pdfminer.high_level.extract_text` keine
Positionsinformation liefert, mit der man Kopf-/Fußzeilen herausfiltern könnte).
`extract_products.py` löst das bereits über `page.get_text("dict")` mit Y-Positions-Filter
(`bbox[1] < 100` / `bbox[3] > page_height - 50`) – diese Technik übernehmen.
2. **Tabellen zerfallen in Einzelzeilen**: Die Änderungshistorie-Tabelle wird zu einer Folge
einzelner Absätze ohne Tabellenstruktur. Vorschlag für ersten Wurf: Tabellen nicht als
RST-Table rekonstruieren (zu fehleranfällig aus PDF-Text), sondern als eingerückten
`.. code-block::` oder einfache Aufzählung kennzeichnen, mit `.. todo:: Tabelle manuell
nachbearbeiten`-Marker – ehrlich statt falsch-schön.
## Offene Punkte / Annahmen, die ich getroffen habe
- Ich gehe davon aus, dass `PV_FORTNA_SOURCE` (oder ein `--source-dir`-Argument) als neue
Umgebungsvariable/Parameter eingeführt wird, die auf `c:\10-Develop\projekte\Fortna\` zeigt,
da dieser Pfad außerhalb des Repos liegt und nicht über `PV_DATA`/`PV_EXAMPLES` erreichbar ist.
- `02_sicherheit/` und `03_anlagenbeschreibung/` bleiben in diesem Durchlauf unangetastet – es
gibt keine 1:1-Quelle dafür in `projekte/Fortna/` (Risikobewertung liegt als separate
`.docx`-Dateien direkt im Repo-Root von `chapters/`, siehe Git-Status, nicht Teil dieser
Aufgabe).
- Die generierten `index.de.rst`-Verzeichnislisten verweisen auf absolute oder
repo-relative-konfigurierbare Pfade zum Rohmaterial – das bedeutet, das Zielsystem (Sphinx)
bekommt keine eigenständig lauffähige Doku ohne Zugriff auf `projekte/Fortna/`. Das ist so
gewollt gemäß der Entscheidung "nur Verzeichnisliste", sollte aber vor dem produktiven
PDF-Build nochmal bewusst bestätigt werden (spätestens beim ersten `sphinx-build`-Versuch
fehlen dann die Downloads/Links).
## Nächste Schritte
1. Dieses Konzept abstimmen (insb. Umgang mit `02_sicherheit`/Risikobewertung-Docx im Root von
`chapters/`, sowie die Pfad-Referenzierung in den Verzeichnislisten).
2. `fortna_inventory.py` implementieren und Trockenlauf-Report gegen `projekte/Fortna/` prüfen.
3. `fortna_convert_chapters.py` implementieren (inkl. Wiederverwendung der
`extract_products.py`-Textextraktion), auf 5 Hauptanleitungen anwenden, Ergebnis stichprobenhaft
gegenlesen.
4. `fortna_init_project.py` implementieren, `projekte/fortna/` anlegen.
5. `bin/`-Wrapper-Skripte (`.bat`/`.sh`) für alle drei ergänzen, analog bestehender Konvention.
@@ -0,0 +1,504 @@
# Automatische Dokumentationserstellung für Firma Schönenberger
**Konzept und technische Architektur**
Sphinx-basierte Zwei-Repo-Architektur mit JSON-gesteuertem Build
Erstellt von: Mista GmbH
Datum: April 2026
Version 1.0 – Entwurf
---
## Inhaltsverzeichnis
1. [Vorteile einer automatischen Erstellung](#1-vorteile-einer-automatischen-erstellung)
1. [Zeitersparnis und Effizienz](#11-zeitersparnis-und-effizienz)
2. [Konsistenz und Qualität](#12-konsistenz-und-qualität)
3. [Mehrsprachigkeit ohne Mehraufwand](#13-mehrsprachigkeit-ohne-mehraufwand)
4. [Wartbarkeit und Skalierung](#14-wartbarkeit-und-skalierung)
5. [Vergleich: Manuell vs. Automatisiert](#15-vergleich-manuell-vs-automatisiert)
2. [Annahmen und Voraussetzungen](#2-annahmen-und-voraussetzungen)
1. [Ausgabeformat und Umfang](#21-ausgabeformat-und-umfang)
2. [Mehrsprachigkeit](#22-mehrsprachigkeit)
3. [Datenquelle und Steuerung](#23-datenquelle-und-steuerung)
4. [Bildverwaltung](#24-bildverwaltung)
5. [Firmenglossar](#25-firmenglossar)
6. [Versionierung und Zusammenarbeit](#26-versionierung-und-zusammenarbeit)
3. [Technische Umsetzung](#3-technische-umsetzung)
1. [Zwei-Repo-Architektur](#31-zwei-repo-architektur)
2. [Technologie](#32-technologie)
3. [Build-Prozess](#33-build-prozess)
4. [Modul-Architektur (meta.yaml)](#34-modul-architektur-metayaml)
5. [Mehrsprachigkeitskonzept](#35-mehrsprachigkeitskonzept)
6. [Verzeichnisstruktur](#36-verzeichnisstruktur)
4. [Projektplan](#4-projektplan)
---
## 1. Vorteile einer automatischen Erstellung
Die Erstellung technischer Dokumentation für Transport- und Hängefördersysteme ist ein komplexer, wiederkehrender Prozess. Jede Anlage besteht aus einer individuellen Kombination von Maschinentypen, die jeweils eigene Kapitel in der Dokumentation erfordern – von Sicherheitshinweisen über Risikomatrizen bis hin zu Wartungsplänen. Die manuelle Zusammenstellung dieser Dokumente ist fehleranfällig, zeitaufwändig und schwer konsistent zu halten.
### 1.1 Zeitersparnis und Effizienz
Eine automatisierte Dokumentationspipeline reduziert den Aufwand pro Anlagendokumentation drastisch:
- Einmalige Pflege von Textbausteinen statt wiederholtes Copy-Paste aus alten Projekten
- Automatische Zusammenstellung basierend auf dem ERP-Export (JSON) – kein manuelles Kapitel-Zusammensuchen
- Parallele Erzeugung aller Sprachversionen in einem Build-Lauf
- Sofortige Aktualisierung aller zukünftigen Projekte bei Änderung eines Textbausteins
### 1.2 Konsistenz und Qualität
Das größte Risiko manueller Dokumentation ist Inkonsistenz: unterschiedliche Formulierungen für dieselbe Maschine, veraltete Sicherheitshinweise in einzelnen Projekten, fehlende Kapitel.
- Zentrale Textbausteine garantieren einheitliche Formulierungen über alle Projekte
- Firmenspezifisches Glossar mit automatischer Verlinkung im gesamten Dokument
- Versionierte Quellen in Git – jede Änderung ist nachvollziehbar und rückverfolgbar
- Validierung beim Build: fehlende Module, Bilder oder Übersetzungen werden sofort erkannt
### 1.3 Mehrsprachigkeit ohne Mehraufwand
Mit dem vorgeschlagenen System wird Mehrsprachigkeit systemisch gelöst, nicht als nachträgliche Übersetzungsaufgabe:
- Jeder Textbaustein existiert als sprachspezifische Datei (z. B. `beschreibung.de.rst`, `beschreibung.en.rst`)
- Sprachspezifische Bilder (HMI-Screenshots, Risikographen) werden automatisch nach Build-Sprache aufgelöst
- Normenverweise können regionsspezifisch gesteuert werden (CE vs. UKCA)
- Neue Sprachen lassen sich sukzessive ergänzen, ohne bestehende Projekte zu beeinträchtigen
### 1.4 Wartbarkeit und Skalierung
Das System ist darauf ausgelegt, von einem einzelnen technischen Redakteur verwaltet zu werden, aber auch für wachsende Teams zu funktionieren:
- Klare Trennung: allgemeine Ressourcen (Base-Repo) vs. projektspezifische Anpassungen (Projekt-Repo)
- Override-Mechanismus: Projekte können einzelne Bausteine überschreiben, ohne das Base-Repo zu verändern
- Git-basierte Versionierung mit Semver – Projektrepos können eine Mindestversion des Base-Repos fordern (Bsp. 2.3.10 aus Major.Minor.Patch)
- Neue Maschinentypen werden als Module hinzugefügt – sofort für alle zukünftigen Projekte verfügbar
### 1.5 Vergleich: Manuell vs. Automatisiert
| Kriterium | Manuell | Automatisiert |
|---|---|---|
| Zeitaufwand pro Projekt | 2–4 Wochen | 1–2 Tage (nach Ersteinrichtung) |
| Konsistenz | Abhängig vom Bearbeiter | Garantiert durch zentrale Quellen |
| Mehrsprachigkeit | Separater Übersetzungsprozess | Integriert im Build-Prozess |
| Fehlerrisiko | Hoch (fehlende Kapitel, Versionen) | Niedrig (Build-Validierung) |
| Nachvollziehbarkeit | Schwierig (Dateiversionen) | Vollständig (Git-Historie) |
| Aktualisierung bei Änderungen | Alle Projekte manuell anpassen | Ein Commit, alle Projekte aktuell |
---
## 2. Annahmen und Voraussetzungen
Die folgenden Annahmen und Rahmenbedingungen bilden die Grundlage für die technische Architektur. Sie basieren auf der Analyse der bestehenden Dokumentationsprozesse und der geplanten Systemlandschaft.
### 2.1 Ausgabeformat und Umfang
- Endformat: PDF via LaTeX (hohe typographische Qualität, professionelles Layout)
- Erwarteter Umfang: ca. 250+ Seiten pro Anlagendokumentation
- Komplexe Abhängigkeiten: eine Maschine erzeugt Inhalte in mehreren Kapiteln (z. B. Risikomatrix in Kapitel Sicherheit, Beschreibung in Kapitel Anlagenbeschreibung, Schmierplan in Kapitel Wartung)
- Reines Markdown reicht nicht aus – erforderlich sind automatische Querverweise, Abbildungsnummerierung, Glossarverlinkung und komplexe Tabellenstrukturen
### 2.2 Mehrsprachigkeit
Die Dokumentation muss mehrsprachig erstellt werden können. Folgende Anforderungen wurden identifiziert:
- Primärsprachen: Deutsch und Englisch
- Weitere europäische Sprachen nach Bedarf (Französisch, Polnisch, etc.)
- Sprachspezifische Bilder: HMI-Screenshots, Risikographen und Typenschilder existieren je Sprache
- Normenverweise können je nach Zielregion abweichen (z. B. CE-Kennzeichnung vs. UKCA für UK)
- Fallback-Mechanismus: wenn eine Übersetzung fehlt, wird die deutsche Version verwendet
### 2.3 Datenquelle und Steuerung
Die Zusammenstellung der Dokumentation wird durch einen strukturierten Export aus dem ERP-System gesteuert:
- Format: JSON-Export, der von Mista GmbH spezifisch erstellt wird
- Inhalt: Liste der verbauten Maschinen mit Positionen, Varianten und Optionen
- Abhängigkeiten: Eine Maschine referenziert via `meta.yaml`, welche Kapitel und Unterkapitel sie benötigt
- Zusatzkapitel: Der JSON-Export kann projektspezifische Sonderkapitel referenzieren
### 2.4 Bildverwaltung
Bilder werden in drei Kategorien verwaltet:
| Kategorie | Beschreibung | Beispiele |
|---|---|---|
| Sprachneutral | Technische Zeichnungen, Fotos, Schaltpläne ohne eingebetteten Text | DXF/SVG-Zeichnungen, Maschinenfotos, ISO-7010-Symbole |
| Sprachspezifisch | Bilder mit eingebettetem Text, der je Sprache variiert | HMI-Screenshots, Risikographen, Typenschilder, Bedienpanel-Aufkleber |
| Projektspezifisch | Einmalbilder, die nur für eine konkrete Anlage gelten | Aufstellpläne, Abnahmefotos, kundenspezifische Layouts |
### 2.5 Firmenglossar
- Zentrales mehrsprachiges Glossar mit firmenspezifischen Fachbegriffen
- Verwaltung als CSV-Dateien (je Sprache), pflegbar auch für Nicht-Entwickler
- Automatische Verlinkung im Dokumenttext (Sphinx-Glossar-Direktive)
- Erscheint als Glossar-Verzeichnis im fertigen PDF
### 2.6 Versionierung und Zusammenarbeit
- Verwaltung in Git (Gitea auf Hetzner-Infrastruktur)
- Pflege durch einen technischen Redakteur (erweiterbar für Teams)
- Klare Trennung: allgemeines Ressourcen-Repo vs. projektspezifisches Repo
- Semver-Versionierung des Base-Repos mit Kompatibilitätsprüfung
---
## 3. Technische Umsetzung
Die Umsetzung basiert auf einer Zwei-Repo-Architektur mit Sphinx als Build-System. Das System wird vollständig in Python und reStructuredText realisiert und läuft auf der bestehenden Hetzner-Infrastruktur. Die Erzeugung des PDFs selbst erfolgt über Sphinx und einen LaTeX-Compiler.
### 3.1 Zwei-Repo-Architektur
Die zentrale Designentscheidung ist die Trennung in zwei unabhängige Repositories:
**Repo 1: `transport-docs-base` (Allgemeine Ressourcen)**
Dieses Repository enthält alle wiederverwendbaren Inhalte und wird auf Gitea gepflegt. Es wird beim Build als Netzlaufwerk oder Mount-Punkt bereitgestellt.
- Sphinx-Extensions: `anlagen_builder.py`, `localized_figure.py`, `glossary_loader.py`
- LaTeX-Templates: Firmen-CI (Fonts, Farben, Kopf-/Fußzeilen, Titelseite)
- Module: Textbausteine pro Maschinentyp mit `meta.yaml` (Abhängigkeiten und Kapitelzuordnung)
- Assets: sprachneutrale und sprachspezifische Bilder (`shared/` und `localized/`)
- Glossar: CSV-Dateien pro Sprache
- Rahmenkapitel: Gerüstkapitel mit Jinja2-Templates
- Hilfsskripte: DXF→SVG-Konvertierung, Bildoptimierung, Modul-Validierung
**Repo 2: `projekt-kunde-xyz` (Projektspezifisch)**
Pro Kundenanlage wird ein eigenes Repository angelegt. Es enthält nur die projektspezifischen Dateien:
- `anlage.json`: ERP-Export mit der Liste verbauter Maschinen, Sprachen und Normenregion
- Projektbilder: Aufstellpläne, Abnahmefotos, Kundenlogo
- Overrides: Projektspezifische Überschreibungen einzelner Textbausteine oder Kapitel
- Zusatzkapitel: Sonderkapitel, die nur für dieses Projekt gelten
- `conf.py`: Importiert die Basiskonfiguration und setzt Projektmetadaten
- `build.py` / `Makefile`: Steuert den Build-Prozess
### 3.2 Technologie
| Komponente | Technologie und Zweck |
|---|---|
| Build-System | Sphinx (Python) – Dokumentationsgenerator mit LaTeX-Backend |
| Textformat | reStructuredText (rST) – Querverweise, Glossar, Direktiven |
| Templating | Jinja2 – Dynamische Inhalte in Rahmenkapiteln und Titelseite |
| PDF-Erzeugung | LaTeX (pdflatex/lualatex) – Professioneller Satz |
| Versionierung | Git (Gitea) – Nachvollziehbarkeit, Branching, Reviews |
| Konfiguration | JSON (ERP-Export) + YAML (Modul-Metadaten) |
| Infrastruktur | Hetzner-Server mit Gitea, n8n (CI-Automatisierung), Nextcloud |
| Bildkonvertierung | Python (ezdxf → SVG), Pillow (Optimierung) |
Links zu den eingesetzten Technologien:
- LaTeX: https://www.latex-project.org/get/
- Sphinx: https://www.sphinx-doc.org/en/master/
- rST: https://de.wikipedia.org/wiki/ReStructuredText
### 3.3 Build-Prozess
Der Build-Prozess läuft vollautomatisch ab und besteht aus vier Phasen:
**Phase 1: Konfiguration einlesen**
- `build.py` liest `anlage.json` (ERP-Export)
- Prüft Kompatibilität der Base-Repo-Version (`VERSION` vs. `base_version`)
- Lädt `meta.yaml` aller referenzierten Module aus dem Base-Repo
**Phase 2: Abhängigkeiten auflösen**
- Erstellt die vollständige Kapitelliste basierend auf den `meta.yaml`-Einträgen
- Löst transitive Abhängigkeiten auf (z. B. Weiche benötigt Antrieb-Modul)
- Prüft auf fehlende Module, Bilder und Übersetzungen
- Wendet Override-Logik an: Projekt-Dateien überschreiben Base-Dateien
**Phase 3: Sphinx-Quellen generieren**
- Generiert `index.rst` mit vollständigen toctree-Einträgen
- Kopiert/verlinkt Module und Bilder in den Build-Ordner
- Erzeugt Glossar-Direktiven aus CSV-Dateien
- Setzt Projektmetadaten (Kunde, Projektnummer, Datum) in Jinja2-Templates ein
**Phase 4: PDF erzeugen (pro Sprache)**
- `sphinx-build -b latex` erzeugt LaTeX-Quellen mit Firmen-CI
- `pdflatex`/`lualatex` compiliert das finale PDF
- Ausgabe: `P-2026-0042_Dokumentation_DE.pdf`, `P-2026-0042_Documentation_EN.pdf`
### 3.4 Modul-Architektur (meta.yaml)
Jedes Maschinenmodul wird durch eine `meta.yaml`-Datei beschrieben, die seine Einordnung in die Buchstruktur und seine Abhängigkeiten definiert. Beispiel für eine Weiche Typ A:
| Feld | Beschreibung |
|---|---|
| `module` | Eindeutiger Bezeichner (z. B. `weiche_typ_a`) |
| `display_name` | Mehrsprachiger Anzeigename (de: Weiche Typ A, en: Switch Type A) |
| `chapters` | Zuordnung zu Buchkapiteln mit Angabe der Sections (z. B. `02_sicherheit` → `risikomatrix`) |
| `images` | Referenzen auf shared, localized und modulspezifische Bilder |
| `requires` | Abhängigkeiten auf andere Module (z. B. `antrieb_sew`) |
### 3.5 Mehrsprachigkeitskonzept
Die Mehrsprachigkeit wird auf drei Ebenen gelöst:
**Ebene 1: Textbausteine**
Jeder Textbaustein existiert als sprachspezifische Datei mit dem Schema `dateiname.{lang}.rst`. Der Build wählt automatisch die richtige Sprachversion basierend auf der Zielsprache. Fehlende Übersetzungen fallen auf die deutsche Version zurück.
**Ebene 2: Bilder**
Eine Custom-Sphinx-Direktive `localized-figure` löst Bildpfade automatisch auf: `assets/localized/{lang}/pfad/bild.png`. Bei fehlender Sprachversion wird das deutsche Bild verwendet. Für SVG-Diagramme mit Text wird geprüft, ob ein Jinja2-basierter SVG-Generator sinnvoller ist als separate Bilddateien.
**Ebene 3: Glossar**
Pro Sprache existiert eine CSV-Datei mit dem Schema `Begriff;Definition;Kategorie`. Die `glossary_loader.py`-Extension erzeugt daraus automatisch Sphinx-Glossardirektiven. Im Text wird mit der Rolle `:term:`Fachbegriff`` verlinkt.
### 3.6 Verzeichnisstruktur
#### 3.6.1 Allgemein
Mehrsprachige Bilder, LaTeX-Vorlagen und die Skripte zur Verwaltung der Basisstruktur; Kapitelstruktur, Glossar, Logo und Stile; Helferprogramme zur Aufbereitung der Bilder:
```text
transport-docs-base/
├── conf_base.py # Sphinx-Basiskonfiguration (wird importiert)
├── _ext/
│ ├── anlagen_builder.py # Hauptextension: JSON-Buchstruktur
│ ├── localized_figure.py # Sprachauflösung für Bilder
│ └── glossary_loader.py # CSV-Glossar-Direktive
├── _templates/
│ ├── latex/
│ │ ├── preamble.tex # Firmen-CI: Fonts, Farben, Kopfzeilen
│ │ ├── titlepage.tex # Titelseite-Template (Jinja2)
│ │ └── maketitle.tex
│ └── common/
│ └── header_footer.rst
├── assets/
│ ├── shared/
│ │ ├── photos/
│ │ │ ├── weiche_typ_a_foto_01.jpg
│ │ │ ├── hubstation_foto_01.jpg
│ │ │ └── antrieb_sew_foto_01.jpg
│ │ ├── drawings/
│ │ │ ├── weiche_typ_a_layout.svg
│ │ │ └── hubstation_schnitt.pdf
│ │ ├── symbols/ # ISO 7010 Sicherheitssymbole
│ │ │ ├── w001_warnung.svg
│ │ │ └── m004_augenschutz.svg
│ │ └── diagrams/
│ │ └── stromlaufplan_weiche_a.pdf
│ └── localized/
│ ├── de/
│ │ ├── screenshots/
│ │ │ └── hmi_hauptmenu.png
│ │ └── diagramme/
│ │ └── risikograph_weiche_a.svg
│ ├── en/
│ │ ├── screenshots/
│ │ │ └── hmi_hauptmenu.png
│ │ └── diagramme/
│ │ └── risikograph_weiche_a.svg
│ └── fr/
│ └── ...
├── modules/ # ALLE MASCHINENTYPEN-BAUSTEINE
│ ├── weiche_typ_a/
│ │ ├── meta.yaml
│ │ ├── beschreibung.de.rst
│ │ ├── beschreibung.en.rst
│ │ ├── risikomatrix.de.rst
│ │ ├── risikomatrix.en.rst
│ │ ├── wartung.de.rst
│ │ └── images/
│ │ └── montage_detail_01.jpg
│ ├── hubstation/
│ │ ├── meta.yaml
│ │ ├── beschreibung.de.rst
│ │ ├── technische_daten.de.rst
│ │ ├── wartung.de.rst
│ │ └── images/
│ │ └── hubmechanik_detail.jpg
│ ├── antrieb_sew/
│ │ ├── meta.yaml
│ │ └── ...
│ └── steuerung_siemens_s7/
│ ├── meta.yaml
│ └── ...
├── chapters/ # RAHMENKAPITEL (Gerüst)
│ ├── 01_einleitung/
│ │ ├── template.de.rst
│ │ └── template.en.rst # Jinja2-Template mit Platzhaltern
│ ├── 02_sicherheit/
│ │ ├── allgemein.de.rst # Immer dabei
│ │ ├── allgemein.en.rst
│ │ ├── normen_ce.de.rst # EU-Konformität
│ │ └── normen_ukca.en.rst # UK-spezifisch
│ ├── 03_anlagenbeschreibung/
│ │ └── template.de.rst # Hier werden Module eingefügt
│ ├── 04_betrieb/
│ │ └── template.de.rst
│ ├── 05_wartung/
│ │ ├── allgemein.de.rst
│ │ └── schmierplan_template.de.rst
│ ├── 06_ersatzteile/
│ │ └── template.de.rst
│ └── 07_anhang/
│ └── template.de.rst
├── glossary/
│ ├── terms.de.csv # "Begriff;Definition;Kategorie"
│ ├── terms.en.csv
│ └── terms.fr.csv
├── styles/
│ ├── firma_logo.pdf
│ ├── firma_logo_sw.pdf
│ └── color_scheme.yaml # CI-Farben für LaTeX und SVG-Generierung
├── scripts/
│ ├── dxf2svg.py # DXF-SVG Konvertierung
│ ├── optimize_images.py # Bildoptimierung vor Commit
│ └── validate_modules.py # Prüft meta.yaml Konsistenz
└── VERSION # Semver der Dokumentationsbasis
```
#### 3.6.2 Projektspezifische Angaben
Angaben, die projektspezifisch Dinge beschreiben oder ergänzen:
```text
projekt-kunde-xyz-2026/
├── conf.py # importiert conf_base.py
├── anlage.json # ERP-Export
├── images/ # Projektspezifische Bilder
│ ├── aufstellplan_halle3.pdf
│ ├── foto_abnahme_01.jpg
│ └── kundenlogo.pdf
├── overrides/ # Projektspezifische Überschreibungen
│ ├── modules/
│ │ └── weiche_typ_a/
│ │ └── beschreibung.de.rst # Überschreibt Basis wenn vorhanden
│ └── chapters/
│ └── 01_einleitung/
│ └── template.de.rst # Kundenspezifische Einleitung
├── additional/ # Kapitel die NUR dieses Projekt hat
│ ├── sonderausfuehrung_xyz.de.rst
│ └── sonderausfuehrung_xyz.en.rst
├── build.py # Hauptbuild-Skript
├── Makefile
└── README.md
```
---
## 4. Projektplan
**Personen:**
| Kürzel | Name |
|---|---|
| A.Ja | Anton Jakob |
| A.Sch | Annette Schopper |
| D.Fe | Doris Fetzer |
| F.Ve | Ferdinand Veith |
| M.St | Michael Stangl |
| M.Ro | Max Rost |
| N.Sch | Nikolai Schwechten |
| S.Hensch | Simon Hensch |
| Y.Wang | Yuelin Wang |
**Stakeholder** (diese Leute geht das Ergebnis an, bzw. entscheiden über Form und Inhalt): M. Rost, N. Schwechten, Y. Wang / S. Hensch
→ Hier muss definiert werden, welches Ergebnis man erwartet bzw. entschieden werden, ob das Ziel erreicht wurde.
**Development Team:** Redakteure und Entwickler
### 0. Use-Case-Definition & Anforderungen
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 0.1 | Story & Teaser: Zieldokumentation beschreiben, Prosa-Story je Produktfamilie | 2 Tage | D.Fe, A.Sch, M.St |
| 0.2 | Stakeholder-Analyse: Alle Akteure und deren Interessen/Ziele auflisten | 1 Tag | A.Sch, D.Fe, F.Ve, M.St |
| 0.3 | Standard-Ablauf entwerfen: Build-Pipeline, Redaktionsworkflow, Freigabeprozess | 2 Tage | M.St |
| 0.4 | Ausnahmen sammeln: Sonderfälle (Mehrfachverwendung der 129 Nummern, Varianten, fehlende Übersetzungen) | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
| 0.5 | Ausnahmen detaillieren, in User Stories schneiden | 2 Tage | A.Sch, F.Ve, M.St |
| 0.6 | Aufwandsschätzung & Business Value für alle Stories | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
| 0.7 | Review & DOR: Prüfung aller Interessen, Freigabe für Umsetzung | 1 Tag | Stakeholder |
### 1. PoC: Proof of Concept
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 1.1 | Ein Modul (z. B. OMNIFLO Weiche), ein Kapitel, zwei Sprachen (DE/EN), PDF-Ausgabe | 2 Tage | M.St, F.Ve |
| 1.2 | LaTeX-Template mit Firmen-CI erstellen und validieren | 2 Tage | M.St, F.Ve, A.Sch |
| 1.3 | PoC-Review mit Stakeholdern | 0,5 Tage | Stakeholder |
### 2. Base-Repo & Infrastruktur
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 2.1 | Repo-Struktur anlegen (transport-docs-base), Sphinx-Projekt initialisieren | 1 Tag | M.St |
| 2.2 | Sphinx-Extensions implementieren (anlagen_builder, localized_figure, glossary_loader) | 4 Tage | M.St |
| 2.3 | build.py: JSON-Parsing, Abhängigkeitsauflösung, Override-Mechanismus | 3 Tage | F.Ve, M.St |
| 2.4 | CI/CD: Gitea Actions für automatischen Build | 2 Tage | M.St |
### 3. Produktprogramm-Strukturierung
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 3.1 | Modulstruktur & meta.yaml-Schema je Produktfamilie definieren (5 Familien) | 3 Tage | A.Sch, M.St, D.Fe |
| 3.2 | Nummernkreis-Mapping: 1388 Produkte auf Module abbilden, Gruppierung festlegen | 3 Tage | A.Sch, F.Ve, M.St |
| 3.3 | Vorlage-Templates pro Kapiteltyp erstellen (Beschreibung, Wartung, Ersatzteile) | 2 Tage | A.Sch, M.St |
### 4. Content-Migration (Hauptaufwand)
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 4.1 | CPC-Familie: 126 Produkte in 11 Gruppen – rST-Bausteine erstellen | 2 Wochen | A.Sch, F.Ve |
| 4.2 | GERÜST-Familie: 263 Produkte in 14 Gruppen – rST-Bausteine erstellen | 1 Woche | A.Sch, F.Ve |
| 4.3 | ILS 2100-Familie: 396 Produkte in 27 Gruppen – rST-Bausteine erstellen | 3 Wochen | A.Sch, F.Ve |
| 4.4 | OMNIFLO-Familie: 453 Produkte in 14 Gruppen – rST-Bausteine erstellen | 4 Wochen | A.Sch, F.Ve |
| 4.5 | Trolleyprogramm: 191 Produkte in 4 Gruppen – rST-Bausteine erstellen | 2 Wochen | A.Sch, F.Ve |
| 4.6 | KI-gestützte Textgenerierung: Vorlagen automatisiert befüllen (Beschreibungen, techn. Daten, Logische Schalter) | parallel | F.Ve, M.St |
| 4.7 | Bildmaterial: Fotos/Zeichnungen zuordnen, optimieren, Pfadstruktur aufbauen | parallel | A.Sch, F.Ve |
### 5. Glossar & Mehrsprachigkeit
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 5.1 | Glossar DE aufbauen (Fachbegriffe aus allen 5 Produktfamilien) | 1 Woche | A.Sch, D.Fe, M.St |
| 5.2 | Glossar EN übersetzen | 1 Woche | A.Sch, D.Fe, M.St, V.Fe |
| 5.3 | Stichproben-Übersetzung: 10 % der Module auf EN migrieren | 2 Wochen | A.Sch, D.Fe, F.Ve |
### 6. ERP-Integration & Automatisierung
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 6.1 | JSON-Export-Format mit ERP abstimmen (anlage.json-Schema) | 2 Tage | M.St, A.Jakob |
| 6.2 | Validierungsskripte: meta.yaml-Konsistenz, fehlende Dateien/Übersetzungen | 2 Tage | M.St |
| 6.3 | Hilfsskripte: dxf2svg, optimize_images | 2 Tage | F.Ve |
### 7. Pilotprojekt
| Phase | Beschreibung | Aufwand (gesch.) | Beteiligte |
|---|---|---|---|
| 7.1 | Erste vollständige Anlagendokumentation (reales Kundenprojekt) generieren | 1 Woche | A.Sch, M.St, F.Ve |
| 7.2 | Review mit Stakeholdern, Korrekturrunde | 3 Tage | Stakeholder |
| 7.3 | Lessons Learned, Backlog-Pflege | 1 Tag | F.Ve, A.Sch, M.St, D.Fe |
### Aufwandszusammenfassung
| Phase | Dauer (geschätzt) |
|---|---|
| 0 – Use-Case-Definition | ~2 Wochen |
| 1 – PoC | ~1 Woche |
| 2 – Base-Repo & Infrastruktur | ~2 Wochen |
| 3 – Produktprogramm-Strukturierung | ~2 Wochen |
| 4 – Content-Migration | ~10–12 Wochen (2 Redakteure parallel + KI-Unterstützung) |
| 5 – Glossar & Mehrsprachigkeit | ~4 Wochen |
| 6 – ERP-Integration | ~1 Woche |
| 7 – Pilotprojekt | ~2 Wochen |
| **Gesamt** | **~24 Wochen (6 Monate)** |
**Anmerkungen:**
- Phase 4 ist der Hauptaufwand. Bei 1388 Produkten und 2 Redakteurinnen (A.Sch, F.Ve) parallel ergibt sich ca. 10 Wochen netto für die Content-Erstellung. Dies ist aktuell nur eine Schätzung aufgrund des heutigen Produktprogrammes. Wird als Detaillierungsgrad für den Kunden eine geringe Anzahl von Produkten gewählt (z. B. Nummern, die als Rückmeldung bei Reparaturen genannt werden sollen), kann dieser auch drastisch reduziert sein.
- KI-gestützte Textgenerierung und Standardisierung (F.Ve) kann den Aufwand in Phase 4 um geschätzt 30 % reduzieren.
- Es sollten die bereits vorhandenen Glossare aus der Automation ebenfalls eingebunden werden.
- Phasen 4, 5 und 6 können teilweise parallelisiert werden, wodurch die Gesamtlaufzeit auf ca. 5 Monate verkürzt werden kann.
- Die Schätzung basiert auf ~20 Produkte/Woche pro Redakteur bei mittlerer Komplexität (Beschreibung, techn. Daten, Wartungshinweise als rST-Bausteine).
- Man sollte zu Beginn noch prüfen, inwieweit das Team zur Überarbeitung der Inhalte anstatt durch D. Fetzer durch S. Hensch und Y. Wang unterstützt werden kann, da D. Fetzer durch diverse Anfragen an Konstruktionen ausgelastet wird.
Binary file not shown.
+99
View File
@@ -0,0 +1,99 @@
"""Generiert index.rst mit toctrees aus allen vorhandenen chapters/**/*.de.rst.
Baut pro Top-Level-Kapitel (00..10) einen Abschnitt mit einer toctree-Direktive,
die alle deutschsprachigen RST-Bausteine dieses Kapitels einbindet. So bleibt der
Build robust gegenueber neuen/entfernten Bausteinen (kein Handpflegen der toctrees).
Verwendung:
python build_index.py
"""
from __future__ import annotations
import logging
import os
import re
from pathlib import Path
log = logging.getLogger(__name__)
# Defekte Altbestaende aus fruehen Testlaeufen, die nicht in die toctree
# aufgenommen werden (siehe exclude_patterns in conf.py). Relativ zu chapters/.
SKIP = {
"02_sicherheit/risikobewertung/rb_fortna_h_m_500592_ev12_20260311.de.rst",
"00_inhaltsverzeichnis/contens_obsah_500592_135776_v2_1_en_cz.de.rst",
# NORD-Antriebsdoku aus fruehem Testlauf: gehoert als Lieferantendoku in die
# Verzeichnisliste 08_lieferantendoku/antriebe, nicht als Volltext ins Anhang.
"10_anhang/01_getriebe_loadingloopantr_de.de.rst",
"10_anhang/02_motor_loadingloopantr_de.de.rst",
}
# Lesbare Titel je Top-Level-Kapitelordner
CHAPTER_TITLES = {
"00_inhaltsverzeichnis": "Inhaltsverzeichnis",
"01_zertifikate": "Zertifikate",
"02_sicherheit": "Sicherheit",
"03_anlagenbeschreibung": "Anlagenbeschreibung",
"04_betrieb": "Betrieb",
"05_wartung": "Wartung",
"06_steuerung": "Steuerung",
"07_ersatzteile": "Ersatzteile",
"08_lieferantendoku": "Lieferantendokumentation",
"09_technische_unterlagen": "Technische Unterlagen",
"10_anhang": "Anhang",
}
def rst_underline(text: str, char: str) -> str:
return f"{text}\n{char * max(len(text), 3)}\n"
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s")
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
chapters = project_root / "chapters"
# RST-Dokumentnamen (ohne .rst-Endung, posix, relativ zum project_root)
# je Top-Level-Kapitel einsammeln.
by_chapter: dict[str, list[str]] = {}
for rst in sorted(chapters.rglob("*.de.rst")):
rel = rst.relative_to(project_root)
if rst.relative_to(chapters).as_posix() in SKIP:
continue
top = rel.parts[1] # chapters/<top>/...
docname = rel.as_posix()[: -len(".rst")] # Sphinx-Docname ohne Endung
by_chapter.setdefault(top, []).append(docname)
lines: list[str] = []
lines.append(".. Automatisch generiert von lib/build_index.py - nicht manuell editieren.")
lines.append("")
title = "Anlagendokumentation Fortna"
lines.append("#" * len(title))
lines.append(title)
lines.append("#" * len(title))
lines.append("")
lines.append("Projekt 500592 / KA 135776 - H&M Cheb (Eger), CZ")
lines.append("")
total = 0
for top in sorted(by_chapter):
docs = by_chapter[top]
total += len(docs)
heading = CHAPTER_TITLES.get(top, top)
lines.append("")
lines.append(rst_underline(heading, "*"))
lines.append(".. toctree::")
lines.append(" :maxdepth: 2")
lines.append("")
for doc in sorted(docs):
lines.append(f" {doc}")
lines.append("")
index_path = project_root / "index.rst"
index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
log.info("index.rst geschrieben: %d Kapitel, %d Bausteine", len(by_chapter), total)
if __name__ == "__main__":
main()
+201
View File
@@ -0,0 +1,201 @@
"""Extract 9-digit Sivas IDs from JSON tree files generated by jsonFromXlsx.py.
Collects Sivas numbers that sit one hierarchy level above the leaves
and writes them as 'nummer: bezeichnung' into a text file.
Usage:
python extract_ids.py --filename KA135776
python extract_ids.py --filename KA135776 --tab soll
python extract_ids.py --all
python extract_ids.py --all --sum2json
python extract_ids.py --all --sum2txt
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
RE_SIVAS = re.compile(r"^(\d{9})")
def is_leaf_value(v) -> bool:
"""True if v is a leaf entry (string, anzahl-dict, or laenge-dict)."""
if isinstance(v, str):
return True
if isinstance(v, dict) and ("anzahl" in v or "laenge" in v):
return True
return False
def extract_ids_from_tree(tree: dict) -> dict[str, str]:
"""Collect 9-digit Sivas numbers one level above leaves.
Returns dict of nummer -> bezeichnung.
"""
ids: dict[str, str] = {}
def walk(node: dict):
for key, value in node.items():
if not isinstance(value, dict) or "anzahl" in value or "laenge" in value:
continue # leaf, skip
# Check if all children are leaves
all_leaves = all(is_leaf_value(v) for v in value.values())
m = RE_SIVAS.match(key)
if all_leaves and value and m:
nummer = m.group(1)
# Bezeichnung from key: part after "nummer-", underscores back to spaces
if len(key) > 9:
bez = key[10:].replace("_", " ")
else:
bez = nummer
ids[nummer] = bez
else:
walk(value)
walk(tree)
return ids
def get_project_name(json_path: Path) -> str:
"""Derive project name from JSON filename by stripping _ist/_soll suffix."""
project_name = json_path.stem
for suffix in ("_ist", "_soll"):
if project_name.endswith(suffix):
project_name = project_name[: -len(suffix)]
break
return project_name
def process_json(json_path: Path, tab: str, results_dir: Path) -> tuple[dict[str, str], str, Path]:
"""Extract IDs from one JSON file. Returns (ids dict, project_name, ids_path)."""
with open(json_path, encoding="utf-8") as f:
tree = json.load(f)
ids = extract_ids_from_tree(tree)
project_name = get_project_name(json_path)
ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
with open(ids_path, "w", encoding="utf-8") as f:
for nummer, bez in sorted(ids.items()):
f.write(f"{nummer}: {bez}\n")
print(f" {json_path.name} -> {ids_path.name} ({len(ids)} IDs)")
return ids, project_name, ids_path
def write_sum2json(all_ids: dict[str, str], projekte: dict[str, list[str]], tab: str, results_dir: Path):
"""Write aggregated summary as JSON file."""
# projekte dict: convert lists to comma-separated strings
projekte_str = {k: ", ".join(v) for k, v in sorted(projekte.items())}
data = {
"ids": dict(sorted(all_ids.items())),
"projekte": projekte_str,
}
out_path = results_dir / f"summary_ids_{tab}.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"\n Summary JSON -> {out_path.name} ({len(all_ids)} IDs, {len(projekte)} mit Projektzuordnung)")
def write_sum2txt(all_ids: dict[str, str], projekte: dict[str, list[str]], tab: str, results_dir: Path):
"""Write aggregated summary as TXT file with [Sivasnummern] and [Anzahl] sections."""
out_path = results_dir / f"summary_ids_{tab}.txt"
with open(out_path, "w", encoding="utf-8") as f:
f.write("[Sivasnummern]\n")
for nummer, bez in sorted(all_ids.items()):
f.write(f"{nummer}: {bez}\n")
f.write(f"\n[Anzahl]\n")
for nummer, prj_list in sorted(projekte.items(), key=lambda x: len(x[1]), reverse=True):
f.write(f"{nummer}: {len(prj_list)}\n")
print(f"\n Summary TXT -> {out_path.name} ({len(all_ids)} IDs)")
def main():
parser = argparse.ArgumentParser(
description="Extrahiert 9-stellige Sivas-Nummern aus JSON-Baeumen im results/-Ordner."
)
parser.add_argument(
"--filename",
default=None,
help="Suchmuster fuer JSON-Dateien im results/-Ordner (z.B. 'KA135776')",
)
parser.add_argument(
"--tab",
choices=["soll", "ist"],
default="ist",
help="Welches Tab gelesen wird: 'ist' oder 'soll' (default: ist)",
)
parser.add_argument(
"--all",
action="store_true",
dest="process_all",
help="Alle *_<tab>.json Dateien im results/-Ordner verarbeiten",
)
parser.add_argument(
"--sum2json",
action="store_true",
help="Alle IDs in eine summary_ids_<tab>.json zusammenfassen (ids + projekte)",
)
parser.add_argument(
"--sum2txt",
action="store_true",
help="Alle IDs in eine summary_ids_<tab>.txt zusammenfassen ([Sivasnummern] + [Anzahl])",
)
args = parser.parse_args()
if not args.process_all and not args.filename:
parser.error("Entweder --filename oder --all muss angegeben werden.")
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
results_dir = project_root / "results"
if not results_dir.exists():
print(f"FEHLER: results/-Ordner nicht gefunden: {results_dir}")
sys.exit(1)
pattern = f"*_{args.tab}.json"
json_files = sorted(results_dir.glob(pattern))
if args.filename:
json_files = [f for f in json_files if args.filename.lower() in f.name.lower()]
if not json_files:
print(f"FEHLER: Keine JSON-Dateien mit Muster '{pattern}' in {results_dir} gefunden.")
if args.filename:
print(f" (Filter: '{args.filename}')")
sys.exit(1)
print(f"Verarbeite {len(json_files)} JSON-Dateien aus {results_dir}\n")
total_ids = 0
# Aggregation dicts for --sum2json / --sum2txt
all_ids: dict[str, str] = {}
projekte: dict[str, list[str]] = {}
individual_txt_files: list[Path] = []
for json_path in json_files:
ids, project_name, ids_path = process_json(json_path, args.tab, results_dir)
total_ids += len(ids)
individual_txt_files.append(ids_path)
for nummer, bez in ids.items():
all_ids[nummer] = bez
projekte.setdefault(nummer, []).append(project_name)
print(f"\nFertig: {total_ids} IDs aus {len(json_files)} Dateien extrahiert.")
if args.sum2json:
write_sum2json(all_ids, projekte, args.tab, results_dir)
if args.sum2txt:
write_sum2txt(all_ids, projekte, args.tab, results_dir)
# Einzelne _ids_ txt-Dateien loeschen wenn Summary erzeugt wurde
if args.sum2json or args.sum2txt:
for txt_path in individual_txt_files:
txt_path.unlink(missing_ok=True)
print(f" {len(individual_txt_files)} einzelne _ids_ Dateien geloescht.")
if __name__ == "__main__":
main()
+64 -11
View File
@@ -29,6 +29,36 @@ PDF_TARGETS: list[tuple[str, str]] = [
# rST Ueberschriften-Zeichen nach Level
RST_HEADING_CHARS = {1: "=", 2: "-", 3: "~", 4: "^"}
# Private-Use-Area-Zeichen aus Word-Symbolfonts (Wingdings/Symbol) auf sinnvolle
# Unicode-Aequivalente abbilden. Diese Zeichen haben in Standard-LaTeX-Fonts keine
# Glyphe (erscheinen sonst als leere Kaestchen im PDF).
PUA_REPLACEMENTS = {
"": "•", # bullet -> •
"": "•", # square bullet -> •
"": "•", # bullet variante
"": "→", # rightwards arrow -> →
"": "→", # rightwards arrow -> →
"": "←", # leftwards arrow -> ←
"": "✓", # checkmark -> ✓
"": " ", # space
}
_PUA_RE = re.compile("|".join(re.escape(k) for k in PUA_REPLACEMENTS))
# Restliche Private-Use-Area-Zeichen (U+E000..U+F8FF), die keine bekannte
# Entsprechung haben, werden entfernt - sie haben in LaTeX-Fonts keine Glyphe.
_PUA_REST_RE = re.compile(r"[-]")
def clean_pua(text: str) -> str:
"""Ersetzt Symbolfont-Zeichen (Private Use Area) durch Unicode-Aequivalente.
Bekannte Symbole (Bullet, Pfeile, Haken) werden gemappt, alle uebrigen
PUA-Zeichen entfernt, damit im PDF keine leeren Kaestchen erscheinen.
"""
text = _PUA_RE.sub(lambda m: PUA_REPLACEMENTS[m.group(0)], text)
text = _PUA_REST_RE.sub("", text)
return text
def find_target(pdf_name: str) -> str | None:
"""Findet den Zielordner-Namen fuer eine PDF-Datei."""
@@ -202,7 +232,7 @@ def extract_text_to_rst(pdf_path: Path, rst_path: Path) -> int:
text = ""
max_size = 0.0
for span in line.get("spans", []):
span_text = span.get("text", "").strip()
span_text = clean_pua(span.get("text", "")).strip()
if span_text:
text += span_text + " "
size = span.get("size", 0)
@@ -272,7 +302,12 @@ def extract_text_to_rst(pdf_path: Path, rst_path: Path) -> int:
# --- Phase 3: Ueberschriften erkennen und RST generieren ---
# Heading: Nummerierung wie "1 Text", "2.3 Text", "2.3.1.4 Text"
# Text muss mit einem Buchstaben beginnen (verhindert "610 014 001" etc.)
RE_HEADING = re.compile(r"^(\d+(?:\.\d+)*)\s+([A-Za-zÄÖÜäöüß].+)$")
# Jeder Nummerierungsteil max 3 Stellen (verhindert Datumsangaben wie "10.12.2020")
# Titeltext muss mit einem Wort von mind. 3 Buchstaben beginnen
# (verhindert "2 B. def.", "19.8 mm (LW13)")
RE_HEADING = re.compile(
r"^(\d{1,3}(?:\.\d{1,3})*)\s+([A-Za-zÄÖÜäöüß]{3,}.*)$"
)
rst_lines: list[str] = []
title = pdf_path.stem
rst_lines.append(RST_HEADING_CHARS[1] * len(title))
@@ -310,10 +345,12 @@ def extract_text_to_rst(pdf_path: Path, rst_path: Path) -> int:
toc_num = toc_numbers.get(text)
if toc_num:
# Nummer aus TOC voranstellen
text = f"{toc_num} {text}"
hm = RE_HEADING.match(text)
is_heading = True
log.debug(" TOC-Nummer ergaenzt: %s", text)
candidate = f"{toc_num} {text}"
hm = RE_HEADING.match(candidate)
if hm:
text = candidate
is_heading = True
log.debug(" TOC-Nummer ergaenzt: %s", text)
if is_heading:
num = hm.group(1)
@@ -380,10 +417,18 @@ def dedup_images(img_dir: Path) -> int:
return removed
def process_all(data_dir: Path) -> None:
"""Verarbeitet alle konfigurierten PDFs."""
def process_all(data_dir: Path, assets_dir: Path | None = None) -> None:
"""Verarbeitet alle konfigurierten Produktprogramm-PDFs.
Bilder sind generelle Produktbilder und werden nach
assets/shared/images/produktprogramm/<ziel> abgelegt (wiederverwendbar).
Die extrahierte RST bleibt zur Information neben dem PDF.
"""
pp_dir = data_dir / "Produktprogramm"
images_base = pp_dir / "images"
if assets_dir is None:
# data/ liegt unter PROJECT -> assets/shared/images/produktprogramm
assets_dir = data_dir.parent / "assets" / "shared" / "images" / "produktprogramm"
images_base = assets_dir
if not pp_dir.exists():
log.error("Verzeichnis nicht gefunden: %s", pp_dir)
@@ -395,6 +440,7 @@ def process_all(data_dir: Path) -> None:
sys.exit(1)
log.info("Gefunden: %d PDF-Dateien in %s", len(pdf_files), pp_dir)
log.info("Bilder -> %s", images_base)
for pdf_path in sorted(pdf_files):
target = find_target(pdf_path.name)
@@ -404,8 +450,11 @@ def process_all(data_dir: Path) -> None:
log.info("Verarbeite: %s -> %s", pdf_path.name, target)
# Bilder extrahieren und Duplikate entfernen
# Bilder extrahieren und Duplikate entfernen. Vorher Zielordner leeren.
img_dir = images_base / target
if img_dir.exists():
import shutil
shutil.rmtree(img_dir)
img_count = extract_images(pdf_path, img_dir)
dup_count = dedup_images(img_dir)
log.info(" %d Bilder extrahiert, %d Duplikate entfernt -> %d einzigartig in %s",
@@ -426,6 +475,9 @@ def main() -> None:
)
parser.add_argument("--data-dir", type=Path, default=None,
help="data/ Verzeichnis (default: aus PV_DATA oder ./data)")
parser.add_argument("--assets-dir", type=Path, default=None,
help="Zielordner fuer Bilder (default: "
"assets/shared/images/produktprogramm unter PROJECT)")
parser.add_argument("--verbose", "-v", action="store_true",
help="Ausfuehrliche Ausgabe")
args = parser.parse_args()
@@ -443,8 +495,9 @@ def main() -> None:
else:
data_dir = Path("data").resolve()
assets_dir = args.assets_dir.resolve() if args.assets_dir else None
log.info("Data-Verzeichnis: %s", data_dir)
process_all(data_dir)
process_all(data_dir, assets_dir=assets_dir)
if __name__ == "__main__":
+377
View File
@@ -0,0 +1,377 @@
"""Befuellt chapters/ aus dem Fortna-Rohmaterial anhand des Inventars.
Zwei Aktionsarten (siehe doc/plan_fortna_extraktion.md):
- action="convert": Die 5 Hauptanleitungen (Betriebs-/Wartungsanleitung,
Funktionsbeschreibung ILS/OMNIFLO/LoadingLoop) werden volltextig mit Bildern
nach .rst konvertiert. Genutzt wird die positionsbasierte Extraktion aus
extract_products.py (Kopf-/Fusszeilen-Filter, Schriftgroessen-Heuristik),
die fuer Word-erstellte PDFs robuster ist als die pdfminer-Variante.
- action="list": Alle uebrigen Kapitel (Zertifikate, Layouts, Schaltplaene,
Notstopp/Bereichsstopp, Pruefberichte, Ersatzteile, Lieferantendoku) erhalten
eine generierte index.de.rst mit einer RST-list-table (Dateiname, Kategorie,
Groesse, Originalpfad). Es werden KEINE PDFs kopiert.
Nur deutschsprachige Quellen (lang == "de") werden verarbeitet.
Verwendung:
python fortna_convert_chapters.py --dry-run
python fortna_convert_chapters.py --apply
python fortna_convert_chapters.py --apply --only convert
python fortna_convert_chapters.py --apply --only list
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import shutil
import sys
from collections import defaultdict
from pathlib import Path
# pdf_extract stellt die positionsbasierte Text+Bild-Extraktion bereit
# (verwebt Text und Bilder, entfernt PDF-Ueberschriftennummern).
import pdf_extract
log = logging.getLogger(__name__)
RST_H = {1: "=", 2: "-", 3: "~", 4: "^"}
# ---------------------------------------------------------------------------
# Hilfen
# ---------------------------------------------------------------------------
def slugify(name: str) -> str:
"""Erzeugt einen sauberen Dateinamen-Stamm aus einem PDF-Namen."""
stem = Path(name).stem.lower()
stem = re.sub(r"[^a-z0-9]+", "_", stem)
stem = re.sub(r"_+", "_", stem).strip("_")
return stem
def rst_heading(text: str, level: int = 1) -> list[str]:
"""Erzeugt eine RST-Ueberschrift als Zeilenliste."""
char = RST_H[level]
lines = []
if level == 1:
lines.append(char * max(len(text), 3))
lines.append(text)
lines.append(char * max(len(text), 3))
lines.append("")
return lines
def load_inventory(path: Path) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
# Lesbarer Dokumenttitel je Zielkapitel (statt PDF-Dateiname als Ueberschrift).
CHAPTER_DOC_TITLE = {
"04_betrieb/betriebsanleitung": "Betriebsanleitung",
"05_wartung/wartungsanleitung": "Wartungsanleitung",
"06_steuerung/ils": "Funktionsbeschreibung Steuerung ILS",
"06_steuerung/omniflo": "Funktionsbeschreibung Steuerung OMNIFLO",
"06_steuerung/loading_loop": "Funktionsbeschreibung Steuerung Loading Loop",
}
# Kapitel-Typ: "general" -> Bilder sind wiederverwendbar (Maschinenelemente,
# Steuerungs-Funktionsbeschreibungen) -> assets/shared
# "project" -> projektspezifisch (Betrieb/Wartung der Anlage)
# -> projekte/fortna/images
# Fotos (.jpg) landen unabhaengig davon IMMER projektspezifisch (siehe
# convert_manual: Routing ueber Klassifizierung im Aufrufer wird durch
# pdf_extract je Bild nicht getrennt - daher legen wir generelle Kapitel nach
# assets, projektbezogene nach projekte/fortna). Feinsteuerung .jpg->projekt
# erfolgt zusaetzlich unten.
CHAPTER_TYPE = {
"04_betrieb/betriebsanleitung": "project",
"05_wartung/wartungsanleitung": "project",
"06_steuerung/ils": "general",
"06_steuerung/omniflo": "general",
"06_steuerung/loading_loop": "general",
}
# kurzer Unterordner-Name je Kapitel fuer die Bildablage
CHAPTER_IMG_SUBDIR = {
"04_betrieb/betriebsanleitung": "betrieb",
"05_wartung/wartungsanleitung": "wartung",
"06_steuerung/ils": "steuerung_ils",
"06_steuerung/omniflo": "steuerung_omniflo",
"06_steuerung/loading_loop": "steuerung_loading_loop",
}
# ---------------------------------------------------------------------------
# action="convert": Volltext + Bilder
# ---------------------------------------------------------------------------
def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
dry_run: bool, symbols_dir: Path | None = None,
symbol_registry: dict | None = None,
logos_dir: Path | None = None,
logo_registry: dict | None = None,
project_dir: Path | None = None,
assets_dir: Path | None = None) -> None:
"""Konvertiert eine Hauptanleitung nach chapters/<kapitel>/.
Bild-Routing je Kapitel:
general -> assets/shared/images/<subdir>
project -> projekte/fortna/images/<subdir>
Fotos (.jpg) gehen unabhaengig davon IMMER nach projekte/fortna/images/<subdir>.
"""
chapter = item["chapter"]
src_pdf = source_root / item["rel_path"]
chapter_dir = chapters_dir / chapter
stem = slugify(item["filename"])
rst_path = chapter_dir / f"{stem}.de.rst"
subdir = CHAPTER_IMG_SUBDIR.get(chapter, slugify(chapter))
ctype = CHAPTER_TYPE.get(chapter, "project")
assets_base = assets_dir or (chapters_dir.parent / "assets" / "shared" / "images")
project_base = (project_dir or (chapters_dir.parent / "projekte" / "fortna")) / "images"
# generelle Grafiken -> assets, projektbezogene -> projekt; Fotos immer projekt
image_dest = (assets_base if ctype == "general" else project_base) / subdir
photo_dest = project_base / subdir
log.info("CONVERT %s (%s)", chapter, ctype)
log.info(" Quelle: %s (%d MB)", item["rel_path"],
item["size_bytes"] // (1024 * 1024))
log.info(" Bilder -> %s (Fotos -> %s)",
image_dest.relative_to(chapters_dir.parent),
photo_dest.relative_to(chapters_dir.parent))
if dry_run:
return
if not src_pdf.exists():
log.error(" Quelle fehlt: %s", src_pdf)
return
# Zielordner vorbereiten und ALLE vorher existierenden Bilder darin loeschen.
chapter_dir.mkdir(parents=True, exist_ok=True)
for d in (image_dest, photo_dest, chapter_dir / "images"):
if d.exists():
shutil.rmtree(d)
for old in chapter_dir.glob("*.de.rst"):
old.unlink()
doc_title = CHAPTER_DOC_TITLE.get(chapter, slugify(item["filename"]))
blocks, img_count = pdf_extract.extract_manual(
src_pdf, rst_path, chapter_dir / "images", title=doc_title,
symbols_dir=symbols_dir, symbol_registry=symbol_registry,
logos_dir=logos_dir, logo_registry=logo_registry,
image_dest=image_dest, photo_dest=photo_dest)
log.info(" %d Textbloecke, %d Bilder -> %s",
blocks, img_count, rst_path.name)
# ---------------------------------------------------------------------------
# action="list": Verzeichnisliste index.de.rst
# ---------------------------------------------------------------------------
def human_size(n: int) -> str:
if n >= 1024 * 1024:
return f"{n / (1024 * 1024):.1f} MB"
if n >= 1024:
return f"{n / 1024:.0f} KB"
return f"{n} B"
def category_from_path(rel_path: str, chapter: str) -> str:
"""Leitet eine Kategorie (z.B. Hersteller) aus dem Quellpfad ab.
Nimmt das Ordnersegment direkt unterhalb des Fortna-Top-Level bzw. ein
aussagekraeftiges Zwischensegment (Herstellername bei Lieferantendoku).
"""
parts = Path(rel_path).parts
# Lieferantendoku: Herstellername steckt meist tiefer im Pfad
# Heuristik: letztes Segment vor der Datei, das keine Sprach-/Nummernkennung ist
lang_dir = re.compile(r"^\d*_?(DE|EN|CZ)(_|$)", re.I)
skip = re.compile(r"^\d+_", )
candidates = [p for p in parts[1:-1] if not lang_dir.match(p)]
if candidates:
# bevorzugt ein Segment mit Buchstaben (Herstellername)
for c in reversed(candidates):
cleaned = skip.sub("", c)
if re.search(r"[A-Za-z]{3,}", cleaned):
return cleaned.split("-")[0].strip()
return ""
def generate_index(chapter: str, items: list[dict], source_root: Path,
chapters_dir: Path, dry_run: bool) -> None:
"""Erzeugt chapters/<kapitel>/index.de.rst mit RST-list-table."""
chapter_dir = chapters_dir / chapter
rst_path = chapter_dir / "index.de.rst"
title_map = {
"00_inhaltsverzeichnis": "Inhaltsverzeichnis / Dokumentennotizen",
"01_zertifikate/konformitaetserklaerung": "Konformitaetserklaerungen",
"01_zertifikate/konfigurationsbeschreibung": "Konfigurationsbeschreibung",
"07_ersatzteile": "Ersatzteillisten",
"08_lieferantendoku/antriebe": "Lieferantendoku - Antriebe",
"08_lieferantendoku/elektrik": "Lieferantendoku - Elektrik",
"08_lieferantendoku/pneumatik": "Lieferantendoku - Pneumatik",
"08_lieferantendoku/stahlbau": "Lieferantendoku - Stahlbau",
"08_lieferantendoku/sicherheitseinrichtungen": "Lieferantendoku - Sicherheitseinrichtungen",
"09_technische_unterlagen/layouts/mechanisch": "Layout mechanisch",
"09_technische_unterlagen/layouts/elektrisch": "Layout elektrisch",
"09_technische_unterlagen/schaltplaene": "Schaltplaene",
"09_technische_unterlagen/notstopp": "Not-Stopp",
"09_technische_unterlagen/bereichsstopp": "Bereichsstopp",
"10_anhang/pruefberichte": "Pruefberichte",
}
title = title_map.get(chapter, chapter)
log.info("LIST %-45s (%d Dateien) -> %s",
chapter, len(items), rst_path.relative_to(chapters_dir))
if dry_run:
return
chapter_dir.mkdir(parents=True, exist_ok=True)
lines: list[str] = []
lines += rst_heading(title, 1)
lines.append(".. note::")
lines.append("")
lines.append(" Automatisch generierte Verzeichnisliste aus dem Fortna-Quellmaterial.")
lines.append(" Die referenzierten Dokumente liegen im Projekt-Rohmaterial und werden")
lines.append(" hier nicht dupliziert.")
lines.append("")
# Die Verzeichnistabelle enthaelt lange Quellpfade und ist fuer Hochformat zu
# breit -> im LaTeX/PDF-Build quer (landscape) setzen. Fuer HTML wird der
# raw:latex-Block ignoriert (dort bricht die Tabelle ohnehin um).
lines.append(".. raw:: latex")
lines.append("")
lines.append(" \\begin{landscape}")
lines.append("")
lines.append(".. list-table::")
lines.append(" :header-rows: 1")
lines.append(" :widths: 30 16 8 46")
lines.append("")
lines.append(" * - Dokument")
lines.append(" - Kategorie")
lines.append(" - Groesse")
lines.append(" - Quellpfad")
for it in sorted(items, key=lambda x: x["rel_path"]):
cat = category_from_path(it["rel_path"], chapter)
lines.append(f" * - {it['filename']}")
lines.append(f" - {cat}")
lines.append(f" - {human_size(it['size_bytes'])}")
lines.append(f" - ``{it['rel_path']}``")
lines.append("")
lines.append(".. raw:: latex")
lines.append("")
lines.append(" \\end{landscape}")
lines.append("")
rst_path.write_text("\n".join(lines), encoding="utf-8")
# ---------------------------------------------------------------------------
# Hauptlogik
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Befuellt chapters/ aus dem Fortna-Inventar (Volltext + Verzeichnislisten)."
)
parser.add_argument("--inventory", default=None,
help="Pfad zum Inventar-JSON (default: results/fortna_inventory.json)")
parser.add_argument("--source-dir", default=None,
help="Fortna-Quellverzeichnis (default: aus Inventar-JSON)")
parser.add_argument("--chapters-dir", default=None,
help="Ziel chapters/-Verzeichnis (default: PROJECT/chapters)")
parser.add_argument("--lang", default="de", choices=["de", "en", "cz"],
help="Zu verarbeitende Sprache (default: de)")
parser.add_argument("--only", choices=["convert", "list"], default=None,
help="Nur eine Aktionsart ausfuehren")
grp = parser.add_mutually_exclusive_group()
grp.add_argument("--dry-run", action="store_true", default=True,
help="Nur anzeigen (Default)")
grp.add_argument("--apply", action="store_true",
help="Aenderungen tatsaechlich schreiben")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)-7s %(message)s",
)
dry_run = not args.apply
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
inv_path = Path(args.inventory) if args.inventory else project_root / "results" / "fortna_inventory.json"
if not inv_path.exists():
log.error("Inventar nicht gefunden: %s (erst fortna_inventory.py laufen lassen)", inv_path)
sys.exit(1)
inv = load_inventory(inv_path)
source_root = Path(args.source_dir) if args.source_dir else Path(inv["source"])
chapters_dir = Path(args.chapters_dir) if args.chapters_dir else project_root / "chapters"
log.info("Quelle: %s", source_root)
log.info("Ziel: %s", chapters_dir)
log.info("Sprache: %s | Modus: %s", args.lang, "APPLY" if args.apply else "DRY-RUN")
items = [i for i in inv["items"] if i["lang"] == args.lang and i["chapter"]]
# 1. Volltext-Konvertierung (nur Zielsprache)
if args.only in (None, "convert"):
conv = [i for i in items if i["action"] == "convert"]
log.info("--- %d Hauptanleitungen konvertieren ---", len(conv))
# Symbole + Logos dokumentuebergreifend zentral in assets/shared ablegen
# und nur einmal referenzieren. Registries ueber alle Dokumente teilen.
base = chapters_dir.parent
symbols_dir = base / "assets" / "shared" / "symbols"
logos_dir = base / "assets" / "shared" / "logos"
assets_img_dir = base / "assets" / "shared" / "images"
project_dir = base / "projekte" / "fortna"
symbol_registry: dict = {}
logo_registry: dict = {}
# Vor dem Lauf zentrale Ziel-Bildordner leeren (frischer Stand).
if not dry_run:
for d in (symbols_dir, logos_dir, assets_img_dir, project_dir / "images"):
if d.exists():
shutil.rmtree(d)
for it in conv:
convert_manual(it, source_root, chapters_dir, dry_run,
symbols_dir=symbols_dir, symbol_registry=symbol_registry,
logos_dir=logos_dir, logo_registry=logo_registry,
project_dir=project_dir, assets_dir=assets_img_dir)
if not dry_run:
log.info("Symbole zentral: %d | Logos: %d -> assets/shared/",
len(symbol_registry), len(logo_registry))
# 2. Verzeichnislisten
if args.only in (None, "list"):
# Zielsprache bevorzugen; Kapitel, die in der Zielsprache KEINE list-Datei
# haben (z.B. Ersatzteilliste nur EN), mit den vorhandenen Fremdsprachen
# auffuellen, damit das Kapitel nicht leer bleibt.
all_list = [i for i in inv["items"]
if i["chapter"] and i["action"] == "list"]
chapters_with_lang = {i["chapter"] for i in all_list if i["lang"] == args.lang}
list_items = [i for i in all_list
if i["lang"] == args.lang or i["chapter"] not in chapters_with_lang]
by_chapter: dict[str, list[dict]] = defaultdict(list)
for it in list_items:
by_chapter[it["chapter"]].append(it)
log.info("--- %d Verzeichnislisten erzeugen ---", len(by_chapter))
for chapter in sorted(by_chapter):
generate_index(chapter, by_chapter[chapter], source_root, chapters_dir, dry_run)
if dry_run:
log.info("DRY-RUN abgeschlossen. Mit --apply tatsaechlich schreiben.")
else:
log.info("Fertig.")
if __name__ == "__main__":
main()
+117
View File
@@ -0,0 +1,117 @@
"""Legt den projektspezifischen Ordner projekte/fortna/ an.
Erzeugt das Projekt-Repo-Geruest gemaess doc/Kernkonzepte.md:
projekte/fortna/
anlage.json # Projektmetadaten (Platzhalter, manuell zu schaerfen)
overrides/.gitkeep # projektspezifische Ueberschreibungen (leer)
images/.gitkeep # projektspezifische Bilder (leer)
README.md # kurze Erlaeuterung
Einmaliger Generator, kein Datei-Scan. Bestehende Dateien werden per Default NICHT
ueberschrieben (--force zum Erzwingen).
Verwendung:
python fortna_init_project.py
python fortna_init_project.py --force
"""
from __future__ import annotations
import argparse
import json
import logging
import os
from pathlib import Path
log = logging.getLogger(__name__)
ANLAGE_JSON = {
"projekt": {
"nummer": "500592",
"ka_nummer": "KA 135776",
"kunde": "Fortna / H&M",
"standort": "Cheb (Eger), CZ",
"sprachen": ["de"],
"_kommentar_sprachen": "EN/CZ vorhanden im Rohmaterial, aktuell nur DE gebaut",
"normen_region": "EU",
"_todo_rechtsgrundlage": "MRL 2006/42/EG (Altprojekt) vs. VO (EU) 2023/1230 - klaeren",
"base_version": ">=0.1.0",
},
"quelle": {
"_kommentar": "Rohmaterial-Verzeichnis (ausserhalb des Repos), Basis der Verzeichnislisten",
"rohmaterial": "c:/10-Develop/projekte/Fortna",
},
"maschinen": [
{"_kommentar": "Modul-Liste noch zu befuellen (03_anlagenbeschreibung via meta.yaml)"},
],
"zusatz_kapitel": [],
}
README = """\
# Projekt Fortna (500592 / KA 135776)
Projektspezifischer Ordner fuer die Fortna/H&M-Anlage in Cheb (CZ).
Struktur gemaess `doc/Kernkonzepte.md` (Zwei-Repo-Architektur):
- `anlage.json` - Projektmetadaten, Sprachen, Normenregion, Maschinenliste (ERP-Export-Platzhalter).
- `overrides/` - projektspezifische Ueberschreibungen von Base-Modulen/Kapiteln.
- `images/` - projektspezifische Bilder (Aufstellplan, Abnahmefotos, Kundenlogo).
## Herkunft der Inhalte
Die eigentliche Dokumentation wird aus dem Fortna-Rohmaterial
(`c:/10-Develop/projekte/Fortna`) erzeugt:
1. `lib/fortna_inventory.py` - inventarisiert das Rohmaterial (`results/fortna_inventory.json`).
2. `lib/fortna_convert_chapters.py --apply` - befuellt `chapters/`:
- Volltext + Bilder fuer die 5 Hauptanleitungen (Betriebs-/Wartungsanleitung, Funktionsbeschreibung ILS/OMNIFLO/LoadingLoop).
- Verzeichnislisten (`index.de.rst`) fuer alle uebrigen Kapitel (Zertifikate, Layouts, Schaltplaene, Lieferantendoku, Ersatzteile, Pruefberichte).
Hinweis: Die Verzeichnislisten referenzieren Pfade im Rohmaterial und kopieren keine PDFs.
Fuer einen produktiven PDF-Build mit eingebundenen Downloads muss das Rohmaterial zugaenglich
gemacht bzw. die referenzierten Dokumente ins Projekt uebernommen werden.
"""
def write_if_absent(path: Path, content: str, force: bool) -> None:
if path.exists() and not force:
log.info(" vorhanden (uebersprungen): %s", path.name)
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
log.info(" geschrieben: %s", path.name)
def main() -> None:
parser = argparse.ArgumentParser(description="Legt projekte/fortna/ an.")
parser.add_argument("--project-dir", default=None,
help="Zielordner (default: PROJECT/projekte/fortna)")
parser.add_argument("--force", action="store_true",
help="Bestehende Dateien ueberschreiben")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)-7s %(message)s",
)
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
target = Path(args.project_dir) if args.project_dir else project_root / "projekte" / "fortna"
log.info("Projektordner: %s", target)
target.mkdir(parents=True, exist_ok=True)
write_if_absent(target / "anlage.json",
json.dumps(ANLAGE_JSON, ensure_ascii=False, indent=2) + "\n", args.force)
write_if_absent(target / "README.md", README, args.force)
write_if_absent(target / "overrides" / ".gitkeep", "", args.force)
write_if_absent(target / "images" / ".gitkeep", "", args.force)
log.info("Fertig.")
if __name__ == "__main__":
main()
+317
View File
@@ -0,0 +1,317 @@
"""Inventarisiert das Fortna-Rohmaterial und ordnet jede Datei einem Zielkapitel zu.
Scannt das Fortna-Quellverzeichnis (Standard: PV_FORTNA_SOURCE oder --source-dir)
rekursiv, erkennt fuer jede PDF/DOCX-Datei die Sprache (aus dem Sprachordner) und
das Ziel-Kapitel unter chapters/ (anhand einer praefix-basierten Mapping-Tabelle,
abgeleitet aus doc/kapitelstruktur.md).
Ergebnis wird als results/fortna_inventory.json geschrieben und tabellarisch auf
stdout ausgegeben. Reines Lese-/Analyseskript - schreibt nichts nach chapters/.
Verwendung:
python fortna_inventory.py
python fortna_inventory.py --source-dir "c:/10-Develop/projekte/Fortna"
python fortna_inventory.py --lang de # nur deutsche Dateien im Report
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import sys
from collections import Counter
from dataclasses import dataclass, asdict
from pathlib import Path
log = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Sprach-Erkennung aus Ordnernamen
# ---------------------------------------------------------------------------
# Reihenfolge wichtig: spezifische Muster zuerst. Ein Segment des relativen
# Pfades wird gegen diese Muster geprueft.
LANG_PATTERNS: list[tuple[re.Pattern[str], str]] = [
(re.compile(r"_DE(_|$)|_DE_original|DE_f[uü]r|01_DE\b", re.I), "de"),
(re.compile(r"_EN(_|$)|_EN_original|EN_for|1_EN_original", re.I), "en"),
(re.compile(r"_CZ(_|$)|02_CZ|03_CZ|CZ_pro", re.I), "cz"),
]
# Sprach-Suffixe im Dateinamen (Fallback, wenn Ordner nichts hergibt)
FILENAME_LANG = [
(re.compile(r"[_\-\[ ]DE[_\-\.\] ]|_DE\b", re.I), "de"),
(re.compile(r"[_\-\[ ]EN[_\-\.\] ]|_EN\b", re.I), "en"),
(re.compile(r"[_\-\[ ]CZ[_\-\.\] ]|_CZ\b", re.I), "cz"),
]
def detect_lang(rel_path: Path) -> str:
"""Bestimmt die Sprache einer Datei anhand Pfadsegmenten und Dateiname.
Fallback auf 'de', wenn nichts eindeutig erkennbar (z.B. sprachneutrale
Datenblaetter oder gemischte DE-EN-Dokumente).
"""
# Zuerst Ordnersegmente pruefen (aussagekraeftiger als Dateiname)
for part in rel_path.parts[:-1]:
for pattern, lang in LANG_PATTERNS:
if pattern.search(part):
return lang
# Dann Dateiname
name = rel_path.name
for pattern, lang in FILENAME_LANG:
if pattern.search(name):
return lang
return "de"
# ---------------------------------------------------------------------------
# Kapitel-Mapping: Fortna-Quellpfad-Praefix -> (Ziel-Kapitel, Aktion)
# ---------------------------------------------------------------------------
# Aktion: "convert" = Volltext+Bilder nach RST, "list" = nur Verzeichnisliste.
# Regeln werden der Reihe nach geprueft; erste passende gewinnt (praefix-Match
# gegen den mit '/' normalisierten relativen Pfad, case-insensitive).
@dataclass
class MapRule:
pattern: re.Pattern[str]
chapter: str
action: str # "convert" | "list"
MAPPING: list[MapRule] = [
# --- Volltext-Konvertierung: die 5 Hauptanleitungen (nur DE_original) ---
MapRule(re.compile(r"02_INSTRUCTIONS.*/01_BETRIEBSANLEITUNG", re.I),
"04_betrieb/betriebsanleitung", "convert"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/02_WARTUNGSANLEITUNG", re.I),
"05_wartung/wartungsanleitung", "convert"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/03_FUNKTION_STEUERUNG_ILS", re.I),
"06_steuerung/ils", "convert"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/04_FUNKTION_STEUERUNG_OMNIFLO", re.I),
"06_steuerung/omniflo", "convert"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/05_FUNKTION_STEUERUNG_LOADING_LOOP", re.I),
"06_steuerung/loading_loop", "convert"),
# Fremdsprach-Varianten der Anleitungen (EN: USER_MANUAL / MAINTENANCE /
# FUNKTION_CONTROL, CZ: NAVOD / RIZENI). In diesem Durchlauf (nur DE) werden
# sie NICHT konvertiert (action="list"), aber sie sollen nicht als UNMAPPED
# erscheinen. Zuordnung nach Anleitungstyp anhand der fuehrenden Nummer.
MapRule(re.compile(r"02_INSTRUCTIONS.*/01_", re.I),
"04_betrieb/betriebsanleitung", "list"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/02_", re.I),
"05_wartung/wartungsanleitung", "list"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/03_", re.I),
"06_steuerung/ils", "list"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/04_", re.I),
"06_steuerung/omniflo", "list"),
MapRule(re.compile(r"02_INSTRUCTIONS.*/05_", re.I),
"06_steuerung/loading_loop", "list"),
# --- Verzeichnislisten: alles uebrige nach Kapitel ---
# 00 Inhaltsverzeichnis / Notes
MapRule(re.compile(r"^00_CONTENTS", re.I),
"00_inhaltsverzeichnis", "list"),
# 01 Zertifikate
MapRule(re.compile(r"^01_CERTIFICATES.*/01_SCH[OÖ]NENBERGER", re.I),
"01_zertifikate/konformitaetserklaerung", "list"),
MapRule(re.compile(r"^01_CERTIFICATES.*/(02_)?CONFIG", re.I),
"01_zertifikate/konfigurationsbeschreibung", "list"),
MapRule(re.compile(r"^01_CERTIFICATES", re.I),
"01_zertifikate/konformitaetserklaerung", "list"),
# 03 Ersatzteile (Fortna 03_SPARE_PARTS)
MapRule(re.compile(r"^03_SPARE_PARTS", re.I),
"07_ersatzteile", "list"),
# 04 Technical Doc -> 09 / 10
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/01_LAYOUT.*/01_MECHANICAL", re.I),
"09_technische_unterlagen/layouts/mechanisch", "list"),
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/01_LAYOUT.*/02_ELECTRICAL", re.I),
"09_technische_unterlagen/layouts/elektrisch", "list"),
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/02_AREA_STOP", re.I),
"09_technische_unterlagen/bereichsstopp", "list"),
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/03_EMERGENCY_STOP", re.I),
"09_technische_unterlagen/notstopp", "list"),
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/04_DIAGRAMS", re.I),
"09_technische_unterlagen/schaltplaene", "list"),
MapRule(re.compile(r"^04_TECHNICAL_DOC.*/05_REPORTS", re.I),
"10_anhang/pruefberichte", "list"),
# 05 Supplier Doc -> 08 Lieferantendoku (nach Gewerk)
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/01_MiTek", re.I),
"08_lieferantendoku/stahlbau", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/02_ABS", re.I),
"08_lieferantendoku/sicherheitseinrichtungen", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/(03_DTS|04_Leantechnik)", re.I),
"08_lieferantendoku/antriebe", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/05_Electrics.*/01_Instructions.*/01_MOTORS", re.I),
"08_lieferantendoku/antriebe", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/05_Electrics", re.I),
"08_lieferantendoku/elektrik", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC.*/(06_|07_)?Pneumatic", re.I),
"08_lieferantendoku/pneumatik", "list"),
MapRule(re.compile(r"^05_SUPPLIER_DOC", re.I),
"08_lieferantendoku", "list"),
]
def classify(rel_path: Path) -> tuple[str | None, str | None]:
"""Ordnet einen relativen Pfad Kapitel + Aktion zu.
Returns (chapter, action) oder (None, None), wenn keine Regel greift.
"""
norm = rel_path.as_posix()
for rule in MAPPING:
if rule.pattern.search(norm):
return rule.chapter, rule.action
return None, None
# ---------------------------------------------------------------------------
# Inventar-Aufbau
# ---------------------------------------------------------------------------
@dataclass
class InvItem:
rel_path: str
filename: str
ext: str
size_bytes: int
lang: str
fortna_top: str # oberster Fortna-Ordner
chapter: str | None # Ziel-Kapitel unter chapters/
action: str | None # "convert" | "list" | None
def resolve_source(arg_dir: str | None) -> Path:
"""Bestimmt das Fortna-Quellverzeichnis."""
if arg_dir:
src = Path(arg_dir).expanduser().resolve()
elif os.environ.get("PV_FORTNA_SOURCE"):
src = Path(os.environ["PV_FORTNA_SOURCE"]).expanduser().resolve()
else:
# Sinnvoller Default fuer diese Umgebung
src = Path("c:/10-Develop/projekte/Fortna").resolve()
return src
def build_inventory(source: Path) -> list[InvItem]:
"""Scannt das Quellverzeichnis und baut die Inventarliste."""
items: list[InvItem] = []
exts = {".pdf", ".docx"}
for path in sorted(source.rglob("*")):
if not path.is_file():
continue
if path.suffix.lower() not in exts:
continue
rel = path.relative_to(source)
chapter, action = classify(rel)
items.append(InvItem(
rel_path=rel.as_posix(),
filename=path.name,
ext=path.suffix.lower(),
size_bytes=path.stat().st_size,
lang=detect_lang(rel),
fortna_top=rel.parts[0] if rel.parts else "",
chapter=chapter,
action=action,
))
return items
def human_size(n: int) -> str:
"""Formatiert Bytes menschenlesbar (KB/MB)."""
if n >= 1024 * 1024:
return f"{n / (1024 * 1024):.1f}M"
if n >= 1024:
return f"{n / 1024:.0f}K"
return f"{n}B"
def print_summary(items: list[InvItem], lang_filter: str | None) -> None:
"""Gibt eine tabellarische Zusammenfassung auf stdout aus."""
shown = [i for i in items if lang_filter is None or i.lang == lang_filter]
# Uebersicht nach Kapitel + Aktion
by_chapter: Counter = Counter()
for it in shown:
key = f"{it.chapter or '(UNMAPPED)'} [{it.action or '-'}]"
by_chapter[key] += 1
print("\n=== Dateien je Ziel-Kapitel ===")
for key in sorted(by_chapter):
print(f" {by_chapter[key]:4d} {key}")
# Sprachverteilung
by_lang: Counter = Counter(i.lang for i in items)
print("\n=== Sprachverteilung (alle) ===")
for lang in sorted(by_lang):
print(f" {by_lang[lang]:4d} {lang}")
# Nicht zugeordnete Dateien explizit auflisten
unmapped = [i for i in shown if i.chapter is None]
if unmapped:
print(f"\n=== NICHT ZUGEORDNET ({len(unmapped)}) ===")
for it in unmapped:
print(f" {it.rel_path}")
# Konvertierungs-Kandidaten (Volltext) explizit auflisten
conv = [i for i in items if i.action == "convert"]
print(f"\n=== Volltext-Konvertierung ({len(conv)} Kandidaten, alle Sprachen) ===")
for it in sorted(conv, key=lambda x: (x.chapter or "", x.lang)):
print(f" [{it.lang}] {it.chapter:32s} <- {it.rel_path} ({human_size(it.size_bytes)})")
print(f"\nGesamt: {len(items)} Dateien, davon {len(shown)} in Filter '{lang_filter or 'alle'}'.")
def main() -> None:
parser = argparse.ArgumentParser(
description="Inventarisiert Fortna-Rohmaterial und ordnet Zielkapitel zu."
)
parser.add_argument("--source-dir", default=None,
help="Fortna-Quellverzeichnis (default: PV_FORTNA_SOURCE "
"oder c:/10-Develop/projekte/Fortna)")
parser.add_argument("--lang", default=None, choices=["de", "en", "cz"],
help="Report auf eine Sprache filtern (JSON enthaelt immer alles)")
parser.add_argument("--output", default=None,
help="Ausgabe-JSON (default: results/fortna_inventory.json)")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)-7s %(message)s",
)
source = resolve_source(args.source_dir)
if not source.exists() or not source.is_dir():
log.error("Quellverzeichnis nicht gefunden: %s", source)
sys.exit(1)
log.info("Scanne: %s", source)
items = build_inventory(source)
log.info("Gefunden: %d Dateien (.pdf/.docx)", len(items))
# JSON schreiben
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
results_dir = project_root / "results"
results_dir.mkdir(exist_ok=True)
out_path = Path(args.output) if args.output else results_dir / "fortna_inventory.json"
if not out_path.is_absolute():
out_path = results_dir / out_path
payload = {
"source": str(source),
"count": len(items),
"items": [asdict(i) for i in items],
}
with open(out_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
log.info("Inventar geschrieben: %s", out_path)
print_summary(items, args.lang)
if __name__ == "__main__":
main()
+81 -121
View File
@@ -3,8 +3,7 @@
Usage:
python jsonFromXlsx.py --filename Fortna --tab soll
python jsonFromXlsx.py --filename Fortna --tab ist --output result.json
python jsonFromXlsx.py --filename KA135776 --extract-ids
python jsonFromXlsx.py --from-results --extract-ids
python jsonFromXlsx.py --all
"""
import argparse
@@ -23,11 +22,36 @@ SHEET_NAMES = {
"ist": "Stk.-Liste IST",
}
# Patterns to match sheet names (case-insensitive), ordered by priority
# Matches: Stk.-Liste SOLL, Stck.-Liste Soll, Stückliste SOLL, Stk. Liste IST, etc.
SHEET_PATTERNS = {
"soll": [
re.compile(r"st[ck]+[\.\-]?\s*liste[n]?\s+soll", re.IGNORECASE),
re.compile(r"st[üu]ckliste[n]?\s+soll", re.IGNORECASE),
],
"ist": [
re.compile(r"st[ck]+[\.\-]?\s*liste[n]?\s+ist", re.IGNORECASE),
re.compile(r"st[üu]ckliste[n]?\s+ist", re.IGNORECASE),
],
}
# Column indices (0-based) in the header row
COL_POSITION = 0
COL_TEILENUMMER = 1
COL_BEZEICHNUNG = 2
HEADER_ROW = 3 # 1-based row number of the header
COL_MENGE = 3
HEADER_ROW = 3 # 1-based row number of the header (minimum, actual may be later)
RE_POSITION = re.compile(r"^\d+(/\d+)*$") # valid position pattern: "10" or "10/5/1"
# Sivas numbers where Menge represents length (meters) instead of piece count
LAENGENABHAENGIG = {
"821106001", "821106002",
"825084000", "825084007",
"825104000", "825104007",
"825144000", "825144007",
"821006026", "821006034", "821006035", "821006045",
"825004000",
}
def find_xlsx(examples_dir: str, pattern: str) -> Path:
@@ -78,15 +102,20 @@ def build_tree(ws, sheet_type: str) -> dict:
pos = row[COL_POSITION]
teil = row[COL_TEILENUMMER]
bez = row[COL_BEZEICHNUNG]
menge = row[COL_MENGE] if len(row) > COL_MENGE else None
if pos is None or teil is None:
continue
pos_str = str(pos).strip()
if pos_str.lower() == "position":
continue # skip header row
if not RE_POSITION.match(pos_str):
continue # skip header rows and non-position entries
teil_str = str(teil).strip()
bez_str = str(bez).strip() if bez else ""
try:
menge_val = float(menge) if menge is not None else 0.0
except (ValueError, TypeError):
menge_val = 0.0
teil_counter[teil_str] += 1
rows_data.append((pos_str, teil_str, bez_str))
rows_data.append((pos_str, teil_str, bez_str, menge_val))
unique_teile = {t for t, count in teil_counter.items() if count == 1}
@@ -94,9 +123,9 @@ def build_tree(ws, sheet_type: str) -> dict:
tree: dict = {}
pos_to_node: dict[str, dict] = {} # maps position path -> dict node
# Track metadata per node for leaf post-processing
node_meta: dict[int, tuple[str, str]] = {} # id(child) -> (teil_str, bez_str)
node_meta: dict[int, tuple[str, str, float]] = {} # id(child) -> (teil_str, bez_str, menge)
for pos_str, teil_str, bez_str in rows_data:
for pos_str, teil_str, bez_str, menge_val in rows_data:
key = make_key(teil_str, bez_str, teil_str in unique_teile)
segments = pos_str.split("/")
@@ -115,7 +144,7 @@ def build_tree(ws, sheet_type: str) -> dict:
child: dict = {}
parent[final_key] = child
pos_to_node[pos_str] = child
node_meta[id(child)] = (teil_str, bez_str)
node_meta[id(child)] = (teil_str, bez_str, menge_val)
is_sivas = re.compile(r"^\d{9}$").match
@@ -125,8 +154,17 @@ def build_tree(ws, sheet_type: str) -> dict:
for key, value in node.items():
if isinstance(value, dict) and len(value) == 0:
# Leaf node: use only Teilenummer as key, Bezeichnung as value
teil, bez = node_meta.get(id(value), ("", ""))
if teil in result and is_sivas(teil):
teil, bez, menge = node_meta.get(id(value), ("", "", 0.0))
if teil in LAENGENABHAENGIG:
# Length-dependent part: sum up Menge as laenge
if teil in result and isinstance(result[teil], dict) and "laenge" in result[teil]:
result[teil]["laenge"] += menge
elif teil in result and isinstance(result[teil], str):
result[teil] = {"laenge": menge, "beschreibung": result[teil]}
else:
leaf_key = unique_key(result, teil)
result[leaf_key] = {"laenge": menge, "beschreibung": bez}
elif teil in result and is_sivas(teil):
# Aggregate duplicate 9-digit Sivas numbers
existing = result[teil]
if isinstance(existing, str):
@@ -137,7 +175,16 @@ def build_tree(ws, sheet_type: str) -> dict:
leaf_key = unique_key(result, teil)
result[leaf_key] = bez
elif isinstance(value, dict):
result[key] = simplify_leaves(value)
# Branch node: enrich bare 9-digit keys with Bezeichnung
branch_key = key
if is_sivas(key):
teil, bez, _ = node_meta.get(id(value), ("", "", 0.0))
if bez:
branch_key = f"{teil}-{bez}".replace(" ", "_")
branch_key = unique_key(result, branch_key)
else:
branch_key = unique_key(result, key)
result[branch_key] = simplify_leaves(value)
else:
result[key] = value
return result
@@ -145,49 +192,6 @@ def build_tree(ws, sheet_type: str) -> dict:
return simplify_leaves(tree)
RE_SIVAS = re.compile(r"^(\d{9})")
def is_leaf_value(v) -> bool:
"""True if v is a leaf entry (string or anzahl-dict)."""
if isinstance(v, str):
return True
if isinstance(v, dict) and "anzahl" in v:
return True
return False
def extract_ids_from_tree(tree: dict) -> dict[str, str]:
"""Collect 9-digit Sivas numbers one level above leaves.
Returns dict of nummer -> bezeichnung.
"""
ids: dict[str, str] = {}
def walk(node: dict):
for key, value in node.items():
if not isinstance(value, dict) or "anzahl" in value:
continue # leaf, skip
# Check if all children are leaves
all_leaves = all(is_leaf_value(v) for v in value.values())
m = RE_SIVAS.match(key)
if all_leaves and value and m:
nummer = m.group(1)
# Bezeichnung from key: part after "nummer-", underscores back to spaces
if "-" in key[9:]:
bez = key[10:].replace("_", " ")
elif len(key) > 9:
bez = key[10:].replace("_", " ")
else:
bez = nummer
ids[nummer] = bez
else:
walk(value)
walk(tree)
return ids
def process_file(xlsx_path: Path, sheet_name: str, tab: str, results_dir: Path,
out_path: Path | None = None) -> bool:
"""Process a single xlsx file. Returns True on success."""
@@ -195,11 +199,25 @@ def process_file(xlsx_path: Path, sheet_name: str, tab: str, results_dir: Path,
print(f"Sheet: {sheet_name}")
wb = openpyxl.load_workbook(str(xlsx_path), data_only=True)
if sheet_name not in wb.sheetnames:
print(f" SKIP: Sheet '{sheet_name}' nicht vorhanden.")
# Find matching sheet: try exact (case-insensitive), then pattern matching
actual_name = None
for name in wb.sheetnames:
if name.lower() == sheet_name.lower():
actual_name = name
break
if actual_name is None:
for pattern in SHEET_PATTERNS.get(tab, []):
for name in wb.sheetnames:
if pattern.search(name):
actual_name = name
break
if actual_name:
break
if actual_name is None:
print(f" SKIP: Kein passendes Sheet fuer '{tab}' gefunden. Vorhanden: {wb.sheetnames}")
return False
ws = wb[sheet_name]
ws = wb[actual_name]
tree = build_tree(ws, tab)
if out_path is None:
@@ -239,22 +257,10 @@ def main():
dest="process_all",
help="Alle .xlsx-Dateien im examples/-Ordner in _ist und _soll JSON konvertieren",
)
parser.add_argument(
"--extract-ids",
action="store_true",
dest="extract_ids",
help="9-stellige Sivas-Nummern eine Ebene ueber den Blaettern extrahieren",
)
parser.add_argument(
"--from-results",
action="store_true",
dest="from_results",
help="JSON-Dateien aus results/ lesen statt aus .xlsx zu erzeugen",
)
args = parser.parse_args()
if not args.process_all and not args.filename and not args.from_results:
parser.error("Entweder --filename, --all oder --from-results muss angegeben werden.")
if not args.process_all and not args.filename:
parser.error("Entweder --filename oder --all muss angegeben werden.")
# Resolve paths relative to project root
project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
@@ -262,44 +268,16 @@ def main():
results_dir = project_root / "results"
results_dir.mkdir(exist_ok=True)
# Default tab to "ist" when --extract-ids without explicit --tab
tab = args.tab
if args.extract_ids and args.tab == "soll" and "--tab" not in sys.argv:
tab = "ist"
if args.from_results:
# Read existing JSON files from results/
pattern = f"*_{tab}.json"
json_files = sorted(results_dir.glob(pattern))
if args.filename:
json_files = [f for f in json_files if args.filename.lower() in f.name.lower()]
if not json_files:
print(f"FEHLER: Keine JSON-Dateien mit Muster '{pattern}' in {results_dir} gefunden.")
sys.exit(1)
print(f"Lese {len(json_files)} JSON-Dateien aus {results_dir}\n")
for json_path in json_files:
with open(json_path, encoding="utf-8") as f:
tree = json.load(f)
# Derive project name from filename: strip _ist/_soll.json suffix
project_name = json_path.stem
for suffix in ("_ist", "_soll"):
if project_name.endswith(suffix):
project_name = project_name[: -len(suffix)]
break
if args.extract_ids:
ids = extract_ids_from_tree(tree)
ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
with open(ids_path, "w", encoding="utf-8") as f:
for nummer, bez in sorted(ids.items()):
f.write(f"{nummer}: {bez}\n")
print(f" {json_path.name} -> {ids_path.name} ({len(ids)} IDs)")
elif args.process_all:
if args.process_all:
xlsx_files = sorted(f for f in Path(examples_dir).glob("*.xlsx")
if not f.name.startswith("~$"))
print(f"Verarbeite {len(xlsx_files)} Dateien aus {examples_dir}\n")
ok, skip = 0, 0
tabs = [tab] if tab != "soll" or "--tab" in sys.argv else ["soll", "ist"]
for xlsx_path in xlsx_files:
for t in ("soll", "ist"):
for t in tabs:
sheet_name = SHEET_NAMES[t]
if process_file(xlsx_path, sheet_name, t, results_dir):
ok += 1
@@ -317,27 +295,9 @@ def main():
if not out_path.is_absolute():
out_path = results_dir / out_path
json_out = None
if args.extract_ids:
stem = xlsx_path.stem.replace(" ", "_")
json_out = results_dir / f"{stem}_{tab}.json"
if not process_file(xlsx_path, sheet_name, tab, results_dir, out_path):
sys.exit(1)
if args.extract_ids:
# Read back the generated JSON
read_path = out_path or json_out or results_dir / f"{xlsx_path.stem.replace(' ', '_')}_{tab}.json"
with open(read_path, encoding="utf-8") as f:
tree = json.load(f)
ids = extract_ids_from_tree(tree)
project_name = xlsx_path.stem.replace(" ", "_")
ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
with open(ids_path, "w", encoding="utf-8") as f:
for nummer, bez in sorted(ids.items()):
f.write(f"{nummer}: {bez}\n")
print(f" IDs: {ids_path} ({len(ids)} IDs)")
if __name__ == "__main__":
main()
+803
View File
@@ -0,0 +1,803 @@
"""Positionsbasierte PDF-Extraktion fuer die Fortna-Hauptanleitungen.
Erzeugt aus einem Word-generierten PDF eine .rst-Datei, in der Text und Bilder
in der richtigen Lesereihenfolge (nach Y-Position pro Seite) verwoben sind.
Verbesserungen gegenueber extract_products.extract_text_to_rst:
1. Ueberschriften: Die im PDF vorhandene Nummerierung (z.B. "1.7 Wichtige
Informationen") wird NUR zur Bestimmung der Gliederungstiefe ausgewertet,
im RST-Ausgabetext aber ENTFERNT. Sphinx/LaTeX nummeriert selbst - dadurch
keine doppelte Nummerierung (alt 1.7 + neu 3.2.1) mehr.
2. Bilder: werden positionsbasiert (bbox) pro Seite eingelesen, dedupliziert
und an der korrekten Textstelle als `.. figure::`-Direktive referenziert.
Bildunterschriften ("Abbildung N: ...") werden als figure-caption uebernommen.
3. Kopf-/Fusszeilen und Inhaltsverzeichnis-Seiten werden herausgefiltert.
Oeffentliche Funktion: extract_manual(pdf_path, rst_path, img_dir) -> (blocks, images)
"""
from __future__ import annotations
import hashlib
import io
import logging
import re
from pathlib import Path
log = logging.getLogger(__name__)
# Schwellwerte fuer die Symbol-/Icon-Erkennung
SYMBOL_MAX_EDGE = 340 # Bilder <= dieser Kantenlaenge gelten als Symbol-Kandidat
# (Sicherheitspiktogramme sind oft ~300 px)
SYMBOL_MIN_COUNT = 2 # ... wenn dasselbe Bild mind. so oft vorkommt
AHASH_THRESHOLD = 6 # max. Hamming-Distanz, ab der zwei aHashes als gleich gelten
# Manuelle Blockliste: untere 32 Bit des aHash von Bildern, die faelschlich als
# Symbol erkannt wurden (Fotos/3D-Renderings/Diagramm-Fragmente, KEINE echten
# Sicherheitspiktogramme). Diese werden nie als Symbol behandelt und nicht in
# assets/shared/symbols abgelegt/referenziert. (Kuratiert nach Sichtpruefung.)
SYMBOL_BLOCKLIST = {
0x1d0580bf, 0x71c70f3f, 0x808081ff, 0x818183ff,
0x81f1f3f3, 0xc3c1f7ff, 0xdbc3e7e7,
}
def average_hash(image_bytes: bytes, size: int = 8) -> int:
"""Perceptual Average-Hash (aHash) eines Bildes als Integer.
Graustufen -> size x size -> Bit je Pixel (>= Mittelwert). Zwei visuell
aehnliche Bilder (auch bei leicht anderer Skalierung/Kompression, wie
mehrfach eingebettete Warnsymbole aus Word) haben eine kleine Hamming-
Distanz. Robuster als ein exakter Byte-/Pixel-Hash.
"""
from PIL import Image
try:
im = Image.open(io.BytesIO(image_bytes)).convert("L").resize((size, size))
px = list(im.getdata())
avg = sum(px) / len(px)
bits = 0
for i, p in enumerate(px):
if p >= avg:
bits |= (1 << i)
return bits
except Exception:
# Fallback: Byte-Hash als Integer, wenn PIL das Bild nicht oeffnen kann
return int(hashlib.md5(image_bytes).hexdigest()[:16], 16)
def hamming(a: int, b: int) -> int:
"""Hamming-Distanz zweier Integer-Hashes (Anzahl unterschiedlicher Bits)."""
return bin(a ^ b).count("1")
def normalize_symbol(image_bytes: bytes, canvas: int = 200) -> bytes:
"""Zentriert ein Symbol auf einer quadratischen weissen Canvas.
So erscheinen alle Warn-/Gebots-/Verbotssymbole - unabhaengig vom Seiten-
verhaeltnis und der Aufloesung der Quelle - bei gleicher RST-Hoehe gleich
gross. Gibt PNG-Bytes zurueck.
"""
from PIL import Image
try:
im = Image.open(io.BytesIO(image_bytes)).convert("RGB")
w, h = im.size
scale = (canvas - 16) / max(w, h) # 8 px Rand ringsum
new = im.resize((max(1, int(w * scale)), max(1, int(h * scale))))
bg = Image.new("RGB", (canvas, canvas), (255, 255, 255))
bg.paste(new, ((canvas - new.width) // 2, (canvas - new.height) // 2))
buf = io.BytesIO()
bg.save(buf, format="PNG")
return buf.getvalue()
except Exception:
return image_bytes
# --- Bild-Klassifizierung -------------------------------------------------
SKETCH_MIN_BYTES = 10_000 # SW-Grafik groesser als ~10 KB gilt als Sketch
def white_ratio(image_bytes: bytes) -> float:
"""Anteil (nahezu) weisser Pixel (Helligkeit > 230) im Bild [0..1]."""
from PIL import Image
try:
im = Image.open(io.BytesIO(image_bytes)).convert("L")
data = list(im.getdata())
if not data:
return 0.0
step = max(1, len(data) // 400)
sample = data[::step]
return sum(1 for p in sample if p > 230) / len(sample)
except Exception:
return 0.0
def _dominant_color_count(im, thresh: float = 0.02) -> int:
"""Anzahl 'dominanter' Farben (>= thresh Anteil) in grober 16er-Quantisierung.
Piktogramme haben wenige Flaechenfarben (<= ~7), Fotos/Barcodes viele (>= 8).
"""
from collections import Counter
small = im.resize((48, 48))
c: Counter = Counter((r // 48, g // 48, b // 48) for r, g, b in small.getdata())
total = sum(c.values()) or 1
return sum(1 for v in c.values() if v / total >= thresh)
def _few_colors(image_bytes: bytes, max_colors: int = 7) -> bool:
"""True, wenn das Bild wenige dominante Flaechenfarben hat (Piktogramm).
Fotos und Barcodes haben viele Farbabstufungen und werden ausgeschlossen.
"""
from PIL import Image
try:
im = Image.open(io.BytesIO(image_bytes)).convert("RGB")
return _dominant_color_count(im) <= max_colors
except Exception:
return False
def looks_like_pictogram(image_bytes: bytes) -> bool:
"""True, wenn ein Bild wie ein Warn-/Gebots-/Verbotssymbol aussieht.
Piktogramme (Warndreieck gelb, Gebot blau, Verbot rot/weiss) bestehen aus
WENIGEN kraeftigen Flaechenfarben + viel Weiss/Schwarz. Kennzeichen:
- hoher Anteil an Signalfarben (gelb/orange, kraeftiges blau, rot) UND
- wenige dominante Farben (<= 7) -> schliesst Fotos & Barcodes aus, die zwar
blaue/farbige Anteile haben, aber viele Farbabstufungen.
"""
from PIL import Image
try:
im = Image.open(io.BytesIO(image_bytes)).convert("RGB")
data = list(im.getdata())
if not data:
return False
# Foto/Barcode -> kein Piktogramm. Grenze 9 (nicht 7): JPEG-komprimierte
# Verbots-/Gebotssymbole haben durch Randartefakte etwas mehr Farben.
if _dominant_color_count(im) > 9:
return False
step = max(1, len(data) // 500)
sample = data[::step]
n = len(sample)
signal = 0
for r, g, b in sample:
# gelb/orange: hohes R+G, niedriges B
if r > 180 and g > 150 and b < 110:
signal += 1
# kraeftiges blau (Gebotssymbol): b deutlich groesser als r
elif b > 110 and b - r > 50 and r < 130:
signal += 1
# rot (Verbotssymbol): r deutlich groesser als g und b
elif r > 150 and r - g > 70 and r - b > 70:
signal += 1
return signal / n > 0.12
except Exception:
return False
def is_junk_fragment(image_bytes: bytes, w: int, h: int) -> bool:
"""True fuer wertlose Text-/Nummern-Fragmente (kein echtes Symbol/Bild).
Erkennt:
a) flache (Hoehe <= 55 px), breite, schwarz-weisse Schnipsel (gerenderte
Beschriftungen/Kapitelnummern wie "5.2.6").
b) kleine, schwarz-weisse Bilder mit sehr hohem Weissanteil und geringer
Datenmenge - das sind gerenderte Kapitelnummern ("3.2.3", "3.2.4"), die
als eigenstaendige Grafik im PDF liegen (viel Weissraum, duenne Ziffern).
"""
# a) flache breite SW-Schnipsel
if h <= 55 and w / max(h, 1) >= 1.6 and is_grayscale(image_bytes):
return True
# b) kleine SW-Nummerngrafik: kleine Flaeche, SW, sehr wenig "Tinte"
if (max(w, h) <= 200 and len(image_bytes) < 4000
and is_grayscale(image_bytes) and white_ratio(image_bytes) > 0.80):
return True
return False
def is_grayscale(image_bytes: bytes) -> bool:
"""True, wenn das Bild (nahezu) schwarz-weiss/graustufig ist."""
from PIL import Image
try:
rgb = Image.open(io.BytesIO(image_bytes)).convert("RGB")
data = list(rgb.getdata())
step = max(1, len(data) // 200)
for r, g, b in data[::step]:
if abs(r - g) > 12 or abs(g - b) > 12:
return False
return True
except Exception:
return False
def classify_image(image_bytes: bytes, ext: str, w: int, h: int) -> str:
"""Klassifiziert ein Bild: 'photo' | 'sketch' | 'graphic'.
- photo: JPEG/JPG (Fotos sind meist projektspezifisch)
- sketch: schwarz-weisse Grafik groesser als ~10 KB (Skizze/Zeichnung)
- graphic: alles uebrige (farbige PNG-Grafik, Diagramm)
"""
if ext.lower() in ("jpg", "jpeg"):
return "photo"
if len(image_bytes) > SKETCH_MIN_BYTES and is_grayscale(image_bytes):
return "sketch"
return "graphic"
# Logo-Erkennung: grosse, breite Grafik (Schriftzug-Logo) auf oberen/unteren
# Seitenbereichen. Wir erkennen sie ueber Seitenposition + Seitenverhaeltnis,
# da OCR des Schriftzugs zu aufwendig waere.
def looks_like_logo(w: int, h: int, y_top: float, page_height: float) -> bool:
"""Heuristik: breites Bild (ratio > 2.2) im Kopf-/Fussbereich der Seite."""
if h == 0:
return False
ratio = w / h
in_margin = y_top < page_height * 0.18 or y_top > page_height * 0.82
return ratio > 2.2 and in_margin and w >= 200
RST_H = {1: "=", 2: "-", 3: "~", 4: "^", 5: '"'}
# Symbolfont-Zeichen (Private Use Area) auf Unicode abbilden bzw. entfernen.
PUA_MAP = {
"": "•", "": "•", "": "•",
"": "→", "": "→", "": "←", "": "✓",
"": " ",
}
_PUA_RE = re.compile("|".join(re.escape(k) for k in PUA_MAP))
_PUA_REST_RE = re.compile(r"[-]")
# Rausch-Muster (Kopf/Fuss/Metadaten), die nicht in den Text gehoeren.
_NOISE = {
"services cz s.r.o", "schönenberger systeme gmbh",
"justus-von-liebig-str", "info@schoenenberger.de",
"www.schoenenberger.de", "www.schoenenberger-systems.com",
"karolinska 661/4",
}
RE_DATE = re.compile(r"^\d{2}\.\d{2}\.\d{4}$")
RE_PAGENO = re.compile(r"^(seite|page)\s+\d+$", re.I)
RE_PHONE = re.compile(r"^[TF]:\s*\+?\d")
# Ueberschrift mit fuehrender Nummer: "1.7 Wichtige Informationen"
# Nummernteile max 3-stellig (verhindert Datum/Artikelnummer als Ueberschrift).
RE_HEADING = re.compile(r"^(\d{1,3}(?:\.\d{1,3}){0,4})\s+([A-Za-zÄÖÜäöüß].{2,})$")
# reine Nummernzeile (Nummer und Titel stehen im PDF auf getrennten Zeilen)
RE_NUM_ONLY = re.compile(r"^(\d{1,3}(?:\.\d{1,3}){0,4})\.?$")
# Bildunterschrift "Abbildung 17: ..." / "Abb. 17 ..." / "Figure 3: ..."
RE_CAPTION = re.compile(r"^(Abbildung|Abb\.?|Bild|Figure|Tabelle)\s*\d+\s*[:.]?\s*(.*)$", re.I)
def clean_pua(text: str) -> str:
"""Symbolfont-Zeichen ersetzen, restliche PUA-Zeichen entfernen."""
text = _PUA_RE.sub(lambda m: PUA_MAP[m.group(0)], text)
return _PUA_REST_RE.sub("", text)
def _is_noise(text: str) -> bool:
low = text.lower().strip()
if not low:
return True
if RE_DATE.match(low) or RE_PAGENO.match(low) or RE_PHONE.match(text):
return True
return any(kw in low for kw in _NOISE)
def _strip_heading_number(num: str, rest: str) -> str:
"""Gibt den Ueberschriftentext ohne fuehrende Gliederungsnummer zurueck."""
return rest.strip()
def caption_slug(caption_text: str) -> str:
"""Leitet ein kurzes Datei-Stichwort aus einer Bildunterschrift ab.
"Abbildung 17: Weiche Typ A links" -> "Weiche_Typ_A"
Nimmt die ersten Woerter nach der "Abbildung N:"-Kennung.
"""
m = RE_CAPTION.match(caption_text)
body = (m.group(2) if m else caption_text).strip()
# Umlaute/Sonderzeichen -> ASCII-nah, Wortgrenzen zu _
body = (body.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue")
.replace("Ä", "Ae").replace("Ö", "Oe").replace("Ü", "Ue")
.replace("ß", "ss"))
words = re.findall(r"[A-Za-z0-9]+", body)
if not words:
return ""
slug = "_".join(words[:3]) # max. 3 Woerter fuer kompakten Namen
return slug[:40]
def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
title: str | None = None,
symbols_dir: Path | None = None,
symbol_registry: dict | None = None,
logos_dir: Path | None = None,
logo_registry: dict | None = None,
image_dest: Path | None = None,
photo_dest: Path | None = None) -> tuple[int, int]:
"""Extrahiert Text + Bilder eines Anleitungs-PDF in eine .rst-Datei.
Bild-Routing (siehe doc/plan_fortna_extraktion.md):
- Symbole (klein, mehrfach): einmal zentral -> symbols_dir
(assets/shared/symbols), im RST als `.. image::` referenziert.
- Logos (breite Schriftzug-Grafik im Seitenrand): pro visueller Variante
alle Aufloesungen -> logos_dir (assets/shared/logos), NICHT inline
referenziert (reine Deko).
- Inhaltsbilder: nach image_dest (assets/... fuer generelle Kapitel bzw.
projekte/fortna/images/... fuer projektspezifische). Fotos (.jpg) landen
IMMER projektspezifisch; das steuert der Aufrufer ueber image_dest.
Benennung: PNG mit direkt folgender Bildunterschrift -> <Stichwort>_NN.png,
Fotos -> img_NNN.jpg, SW-Skizzen (>10KB) -> sketch_NNN.png, Rest img_NNN.png.
symbol_registry / logo_registry (aHash -> Datei) werden dokumentuebergreifend
wiederverwendet.
Returns:
(anzahl_textbloecke, anzahl_bilder)
"""
import fitz
if symbol_registry is None:
symbol_registry = {}
if logo_registry is None:
logo_registry = {}
# image_dest: Zielordner fuer Grafiken/Skizzen. photo_dest: fuer Fotos (.jpg,
# immer projektspezifisch). Default beider: img_dir.
if image_dest is None:
image_dest = img_dir
if photo_dest is None:
photo_dest = image_dest
doc = fitz.open(str(pdf_path))
image_dest.mkdir(parents=True, exist_ok=True)
# --- Seiten identifizieren, die uebersprungen werden ---
# a) TOC-Seiten (Punktfueller / "Inhaltsverzeichnis")
# b) Aenderungshistorie-/Dokumentinfo-Tabellen ("Aenderungsinformation",
# Spaltenkombination Version+Datum+Aenderungsbeschreibung). Diese Tabellen
# zerfallen beim linearen Auslesen in sinnlose Einzelzellen.
skip_pages: set[int] = set()
for pn in range(len(doc)):
t = doc[pn].get_text()
low = t.lower()
if t.count("....") >= 3 or "\ninhaltsverzeichnis\n" in low:
skip_pages.add(pn)
elif "änderungsinformation" in low or "dokumenteninformation" in low:
skip_pages.add(pn)
elif ("änderungsbeschreibung" in low and "version" in low
and ("datum" in low or "kapitel" in low)):
skip_pages.add(pn)
# --- Pass 1: Text- und Bild-Elemente sammeln (Bilder nur puffern) ---
# Bilder werden zunaechst nicht geschrieben, sondern per Perceptual-Hash
# (aHash) einem Cluster zugeordnet. Visuell aehnliche Bilder (gleiches Symbol
# in anderer Skalierung) landen im selben Cluster. Erst nach dem Scan steht
# fest, welches Cluster mehrfach vorkommt (Symbol-Kriterium).
elements: list[dict] = []
clusters: list[dict] = [] # [{ahash, data, ext, w, h, count, cls, margin_hits, variants}]
def _find_cluster(ah: int) -> int:
for ci, c in enumerate(clusters):
if hamming(ah, c["ahash"]) <= AHASH_THRESHOLD:
return ci
return -1
for pn in range(len(doc)):
if pn in skip_pages:
continue
page = doc[pn]
ph = page.rect.height
pw = page.rect.width
# Textzeilen mit Position
page_dict = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE)
for block in page_dict.get("blocks", []):
if block.get("type") != 0:
continue
for line in block.get("lines", []):
bbox = line.get("bbox", [0, 0, 0, 0])
if bbox[1] < 70 or bbox[3] > ph - 45: # Kopf-/Fusszeile
continue
text = ""
max_size = 0.0
bold = False
for span in line.get("spans", []):
st = clean_pua(span.get("text", ""))
if st.strip():
text += st
max_size = max(max_size, span.get("size", 0))
if span.get("flags", 0) & 2**4: # bold flag
bold = True
text = text.strip()
if not text or _is_noise(text):
continue
elements.append({
"kind": "text", "y": bbox[1], "page": pn + 1,
"text": text, "size": round(max_size, 1), "bold": bold,
})
# Bilder mit Position. Titelseiten (erste 2 Seiten) = nur Logo/Deko.
if pn < 2:
continue
for im in page.get_image_info(xrefs=True):
xref = im.get("xref", 0)
if not xref:
continue
b = im["bbox"]
w, h = im.get("width", 0), im.get("height", 0)
# Vollseiten-Deko / Hintergrundbilder ueberspringen
if (b[2] - b[0]) > pw * 0.95 and (b[3] - b[1]) > ph * 0.95:
continue
if w < 40 or h < 40: # winzige Icons
continue
base = doc.extract_image(xref)
if not base:
continue
data = base["image"]
if len(data) < 3000:
continue
# Wertlose Text-/Nummern-Fragmente (flach, SW) verwerfen
if is_junk_fragment(data, w, h):
continue
ext = base.get("ext", "png")
ah = average_hash(data)
in_margin = looks_like_logo(w, h, b[1], ph)
ci = _find_cluster(ah)
if ci < 0:
clusters.append({"ahash": ah, "data": data, "ext": ext,
"w": w, "h": h, "count": 1,
"cls": classify_image(data, ext, w, h),
"margin_hits": 1 if in_margin else 0,
# groesste Auflvariante fuer Logo-Ablage merken
"variants": [(w * h, w, h, data, ext)]})
ci = len(clusters) - 1
else:
clusters[ci]["count"] += 1
if in_margin:
clusters[ci]["margin_hits"] += 1
clusters[ci]["variants"].append((w * h, w, h, data, ext))
elements.append({
"kind": "image", "y": b[1], "page": pn + 1, "cluster": ci,
})
doc.close()
import os as _os
# --- Zielverzeichnisse + relative Pfad-Praefixe (vom RST aus) ---
rst_parent = rst_path.parent
if symbols_dir is None:
symbols_dir = rst_parent.parent.parent / "assets" / "shared" / "symbols"
if logos_dir is None:
logos_dir = symbols_dir.parent / "logos"
def _rel(target: Path) -> str:
try:
return Path(_os.path.relpath(target, rst_parent)).as_posix()
except ValueError:
return target.as_posix()
sym_rel_prefix = _rel(symbols_dir)
dest_rel_prefix = _rel(image_dest)
photo_rel_prefix = _rel(photo_dest)
# --- Pass 2a: Cluster klassifizieren (Logo / Symbol / Inhaltsbild) ---
# Logo: breit (ratio > 2.2), im Seitenrand UND wiederkehrend (>=3x) - das
# unterscheidet ein Kopf-/Fusszeilen-Logo von einem einmaligen breiten
# Diagramm. Alle Aufloesungen -> assets/shared/logos, NICHT inline.
# Symbol: klein (<=260px) UND mehrfach (>=2x) -> assets/shared/symbols.
cluster_is_symbol: list[bool] = [False] * len(clusters)
cluster_is_logo: list[bool] = [False] * len(clusters)
cluster_sympath: list[str] = [""] * len(clusters)
for ci, c in enumerate(clusters):
ratio = c["w"] / c["h"] if c["h"] else 0
is_logo = (ratio > 2.2 and c["margin_hits"] >= 3 and c["count"] >= 3
and c["w"] >= 200)
if is_logo:
cluster_is_logo[ci] = True
# bereits bekannte Logo-Variante? -> nicht neu schreiben
if any(hamming(c["ahash"], rah) <= AHASH_THRESHOLD + 2
for rah in logo_registry):
continue
logos_dir.mkdir(parents=True, exist_ok=True)
idx = len(logo_registry) + 1
for (_area, w, h, data, ext) in sorted(set(c["variants"]), reverse=True):
(logos_dir / f"logo_{idx:02d}_{w}px.{ext}").write_bytes(data)
logo_registry[c["ahash"]] = idx
continue
# Symbol-Erkennung (Warn-/Gebots-/Verbotszeichen, Bedien-Piktogramme):
# klein (<= 260px) UND quadratisch-aehnlich (ratio 0.55..1.8) UND
# entweder Piktogramm-Form (Signalfarben) ODER (mehrfach + heller Grund).
# Piktogramme werden so auch erkannt, wenn sie im Dokument nur 1x vorkommen
# (Warndreiecke stehen oft einzeln), Foto-Ausschnitte dagegen nicht.
# Manuell geblockte Fehlklassifikationen nie als Symbol behandeln.
# Vergleich ueber die unteren 32 Bit des aHash (= Wert im Dateinamen);
# bis zu 2 abweichende Bits werden toleriert (JPEG-/Skalierungsvarianz).
low = c["ahash"] & 0xffffffff
blocked = any(bin(low ^ b).count("1") <= 2 for b in SYMBOL_BLOCKLIST)
small = max(c["w"], c["h"]) <= SYMBOL_MAX_EDGE
squarish = 0.55 <= ratio <= 1.8
few_colors = _few_colors(c["data"], max_colors=9) # Piktogramm: wenige Farben
is_symbol = (not blocked) and small and squarish and few_colors and (
looks_like_pictogram(c["data"])
or (c["count"] >= SYMBOL_MIN_COUNT and white_ratio(c["data"]) > 0.35)
)
cluster_is_symbol[ci] = is_symbol
if is_symbol:
match = None
for reg_ah, reg_file in symbol_registry.items():
if hamming(c["ahash"], reg_ah) <= AHASH_THRESHOLD:
match = reg_file
break
if match is None:
symbols_dir.mkdir(parents=True, exist_ok=True)
# auf quadratische Canvas normalisieren -> immer als PNG ablegen
match = f"symbol_{c['ahash'] & 0xffffffff:08x}.png"
(symbols_dir / match).write_bytes(normalize_symbol(c["data"]))
symbol_registry[c["ahash"]] = match
cluster_sympath[ci] = f"{sym_rel_prefix}/{match}"
# --- pro Seite nach Y-Position sortieren (Lesereihenfolge) ---
elements.sort(key=lambda e: (e["page"], e["y"]))
# --- Zeilen zusammenfuehren, die im PDF getrennt stehen ---
# a) Nummer-only-Zeile ("2.3.1") + folgende Titelzeile
# b) Bullet-only-Zeile ("•") + folgende Textzeile -> "• Text"
merged: list[dict] = []
i = 0
while i < len(elements):
e = elements[i]
if e["kind"] == "text":
nxt = elements[i + 1] if i + 1 < len(elements) else None
# a) Gliederungsnummer + Titel. NUR mehrteilige Nummern (z.B. "2.3.1"),
# nicht einzelne lose Ziffern (Bildlegenden-Nummern wie "2", "24"),
# und NICHT, wenn die Folgezeile eine Bildunterschrift ist.
m = RE_NUM_ONLY.match(e["text"])
if (m and "." in m.group(1) and nxt and nxt["kind"] == "text"
and re.match(r"[A-Za-zÄÖÜäöüß]", nxt["text"])
and not RE_CAPTION.match(nxt["text"])):
merged.append({**e, "text": f"{m.group(1)} {nxt['text']}",
"size": max(e["size"], nxt["size"])})
i += 2
continue
# b) einzelnes Bullet-Zeichen + Text der Folgezeile
if e["text"].strip() in ("•", "-", "–", "*") and nxt and nxt["kind"] == "text":
merged.append({**e, "text": f"• {nxt['text']}"})
i += 2
continue
merged.append(e)
i += 1
# --- Pass 2b: Inhaltsbilder schreiben (mit Caption-Benennung + Klasse) ---
# Jetzt (nach dem Merge) ist die Folgezeile jedes Bildes bekannt -> Caption.
# Pro Cluster genau einmal schreiben; Zielname aus Caption oder Klasse.
# Fotos (.jpg) -> photo_dest (projektspezifisch), Rest -> image_dest.
image_dest.mkdir(parents=True, exist_ok=True)
photo_dest.mkdir(parents=True, exist_ok=True)
counters = {"img": 0, "sketch": 0}
used_names: set[str] = set()
slug_counters: dict[str, int] = {}
cluster_path: dict[int, str] = {} # ci -> RST-Pfad (relativ)
def _next_name(cls: str, ext: str, slug: str) -> str:
if slug:
slug_counters[slug] = slug_counters.get(slug, 0) + 1
name = f"{slug}_{slug_counters[slug]:02d}.{ext}"
elif cls == "sketch":
counters["sketch"] += 1
name = f"sketch_{counters['sketch']:03d}.{ext}"
else: # photo / graphic
counters["img"] += 1
name = f"img_{counters['img']:03d}.{ext}"
while name in used_names:
counters["img"] += 1
name = f"img_{counters['img']:03d}.{ext}"
used_names.add(name)
return name
n_m = len(merged)
for idx, e in enumerate(merged):
if e["kind"] != "image":
continue
ci = e["cluster"]
if cluster_is_logo[ci]:
e["path"] = "" # Logos werden nicht inline referenziert
e["is_symbol"] = False
continue
if cluster_is_symbol[ci]:
e["path"] = cluster_sympath[ci]
e["is_symbol"] = True
continue
e["is_symbol"] = False
if ci in cluster_path: # Duplikat -> bereits geschrieben
e["path"] = cluster_path[ci]
continue
c = clusters[ci]
# Caption der Folgezeile? -> Stichwort fuer Benennung
slug = ""
if idx + 1 < n_m and merged[idx + 1]["kind"] == "text":
cm = RE_CAPTION.match(merged[idx + 1]["text"])
if cm:
slug = caption_slug(merged[idx + 1]["text"])
name = _next_name(c["cls"], c["ext"], slug)
# Fotos immer projektspezifisch (photo_dest), sonst image_dest
if c["cls"] == "photo":
(photo_dest / name).write_bytes(c["data"])
rel = f"{photo_rel_prefix}/{name}"
else:
(image_dest / name).write_bytes(c["data"])
rel = f"{dest_rel_prefix}/{name}"
cluster_path[ci] = rel
e["path"] = rel
img_counter = len(cluster_path)
# --- RST rendern ---
if title is None:
title = pdf_path.stem
out: list[str] = [RST_H[1] * len(title), title, RST_H[1] * len(title), ""]
# Absatz-Puffer: aufeinanderfolgende Fliesstextzeilen zu einem Absatz
# zusammenfuehren (PDF-Zeilenumbrueche sind kein Absatzende).
para: list[str] = []
def _join_para(lines: list[str]) -> str:
"""Fuegt Absatzzeilen zusammen und entfernt PDF-Silbentrennung.
Endet eine Zeile auf "wort-" (Trennstrich nach Kleinbuchstabe), wird sie
ohne Leerzeichen und ohne Bindestrich mit dem naechsten Wort verbunden
("Anlagenkompo-" + "nente" -> "Anlagenkomponente"). Ein Bindestrich nach
Grossbuchstabe/Ziffer (Kompositum wie "ILS-", "24-") bleibt erhalten.
"""
result = ""
for i, ln in enumerate(lines):
ln = ln.strip()
if not result:
result = ln
continue
if re.search(r"[a-zäöüß]-$", result):
result = result[:-1] + ln # Silbentrennung aufloesen
else:
result += " " + ln
return result.strip()
def flush_para() -> None:
if para:
out.append(_join_para(para))
out.append("")
para.clear()
RE_BULLET = re.compile(r"^\s*[•\-–*]\s+")
blocks = 0
n = len(merged)
for idx, e in enumerate(merged):
if e["kind"] == "image":
flush_para()
path = e.get("path", "")
if not path:
continue
if e.get("is_symbol"):
# Symbol/Warnzeichen: einheitlich kleine Grafik, links ausgerichtet
# (Piktogramm-Charakter, daneben steht der Warntext). Feste Hoehe UND
# Breite begrenzen, damit alle Symbole - unabhaengig vom Seiten-
# verhaeltnis der Quelle - gleich gross erscheinen.
out.append("")
out.append(f".. image:: {path}")
out.append(" :align: left")
out.append(" :height: 10mm")
out.append(" :class: warnsymbol")
out.append("")
blocks += 1
continue
caption = ""
if idx + 1 < n and merged[idx + 1]["kind"] == "text":
cm = RE_CAPTION.match(merged[idx + 1]["text"])
if cm:
caption = merged[idx + 1]["text"]
merged[idx + 1]["_consumed"] = True
out.append("")
out.append(f".. figure:: {path}")
out.append(" :align: center")
out.append(" :width: 80%")
if caption:
out.append("")
out.append(f" {caption}")
out.append("")
blocks += 1
continue
if e.get("_consumed"):
continue
text = e["text"]
# Bildunterschriften ("Abbildung N: ...") sind NIE Ueberschriften. Wenn sie
# nicht direkt als figure-Caption konsumiert wurden (z.B. weil Text
# dazwischen stand), als eigenen (kursiven) Absatz ausgeben.
if RE_CAPTION.match(text):
flush_para()
out.append(f"*{text}*")
out.append("")
blocks += 1
continue
hm = RE_HEADING.match(text)
# Ueberschrift NUR, wenn ALLE Kriterien erfuellt sind:
# 1. fuehrende Gliederungsnummer ("N", "N.N", ...),
# 2. Quell-Schrift ist gross (>= 12.5pt) ODER fett -> unterscheidet echte
# Ueberschriften von normalen Textzeilen, die zufaellig mit einer Nummer
# beginnen (Kapitelverweise "gemaess 2.8.2 ...", nummerierte Aufzaehlungen).
# Das war die Hauptursache falscher Ueberschriften.
# 3. kein offensichtliches Fragment/Bildlegende ("POS", "li.", "5 mm", ...).
is_heading = False
if hm and len(hm.group(2)) < 120:
num, rest = hm.group(1), hm.group(2)
rest_stripped = rest.strip()
looks_like_title = e.get("size", 0) >= 12.5 or e.get("bold", False)
is_frag = (
len(rest_stripped) < 4 # zu kurz ("POS", "li.")
or rest_stripped.endswith((",",)) # abgeschnittene Zelle
or re.match(r"^(mm|cm|m|kg|x|l|h|min|s|bar|POS|li|re|Pos)\b",
rest_stripped, re.I)
or re.match(r"^(li|re|oben|unten|vorne|hinten|links|rechts)\.?$",
rest_stripped, re.I)
)
if looks_like_title and not is_frag:
is_heading = True
if is_heading:
flush_para()
num, rest = hm.group(1), hm.group(2)
heading = _strip_heading_number(num, rest) # NUMMER ENTFERNEN
level = min(num.count(".") + 1, 5)
char = RST_H[level]
out.append("")
if level == 1:
out.append(char * max(len(heading), 3))
out.append(heading)
out.append(char * max(len(heading), 3))
out.append("")
blocks += 1
continue
# Aufzaehlung: Zeile beginnt mit Bullet ODER enthaelt mehrere Bullets
# (mehrere Listenpunkte sind im PDF oft in eine Zeile zusammengelaufen).
if RE_BULLET.match(text) or text.count("•") >= 2:
flush_para()
# An Bullet-Zeichen aufsplitten, jeder Teil wird ein Listenpunkt.
parts = re.split(r"\s*•\s*", text)
for part in parts:
item = RE_BULLET.sub("", part).strip().rstrip(",")
if item:
out.append(f"- {item}")
out.append("")
blocks += 1
continue
# Fliesstext: an Absatz anhaengen. Endet die Zeile mit Satzzeichen, gilt
# der Absatz als abgeschlossen und wird geflusht.
para.append(text)
if re.search(r"[.:!?]$", text):
flush_para()
blocks += 1
flush_para()
rst = "\n".join(out)
rst = re.sub(r"\n{3,}", "\n\n", rst).strip() + "\n"
rst_path.parent.mkdir(parents=True, exist_ok=True)
rst_path.write_text(rst, encoding="utf-8")
return blocks, img_counter
+23
View File
@@ -0,0 +1,23 @@
# Projekt Fortna (500592 / KA 135776)
Projektspezifischer Ordner fuer die Fortna/H&M-Anlage in Cheb (CZ).
Struktur gemaess `doc/Kernkonzepte.md` (Zwei-Repo-Architektur):
- `anlage.json` - Projektmetadaten, Sprachen, Normenregion, Maschinenliste (ERP-Export-Platzhalter).
- `overrides/` - projektspezifische Ueberschreibungen von Base-Modulen/Kapiteln.
- `images/` - projektspezifische Bilder (Aufstellplan, Abnahmefotos, Kundenlogo).
## Herkunft der Inhalte
Die eigentliche Dokumentation wird aus dem Fortna-Rohmaterial
(`c:/10-Develop/projekte/Fortna`) erzeugt:
1. `lib/fortna_inventory.py` - inventarisiert das Rohmaterial (`results/fortna_inventory.json`).
2. `lib/fortna_convert_chapters.py --apply` - befuellt `chapters/`:
- Volltext + Bilder fuer die 5 Hauptanleitungen (Betriebs-/Wartungsanleitung, Funktionsbeschreibung ILS/OMNIFLO/LoadingLoop).
- Verzeichnislisten (`index.de.rst`) fuer alle uebrigen Kapitel (Zertifikate, Layouts, Schaltplaene, Lieferantendoku, Ersatzteile, Pruefberichte).
Hinweis: Die Verzeichnislisten referenzieren Pfade im Rohmaterial und kopieren keine PDFs.
Fuer einen produktiven PDF-Build mit eingebundenen Downloads muss das Rohmaterial zugaenglich
gemacht bzw. die referenzierten Dokumente ins Projekt uebernommen werden.
+25
View File
@@ -0,0 +1,25 @@
{
"projekt": {
"nummer": "500592",
"ka_nummer": "KA 135776",
"kunde": "Fortna / H&M",
"standort": "Cheb (Eger), CZ",
"sprachen": [
"de"
],
"_kommentar_sprachen": "EN/CZ vorhanden im Rohmaterial, aktuell nur DE gebaut",
"normen_region": "EU",
"_todo_rechtsgrundlage": "MRL 2006/42/EG (Altprojekt) vs. VO (EU) 2023/1230 - klaeren",
"base_version": ">=0.1.0"
},
"quelle": {
"_kommentar": "Rohmaterial-Verzeichnis (ausserhalb des Repos), Basis der Verzeichnislisten",
"rohmaterial": "c:/10-Develop/projekte/Fortna"
},
"maschinen": [
{
"_kommentar": "Modul-Liste noch zu befuellen (03_anlagenbeschreibung via meta.yaml)"
}
],
"zusatz_kapitel": []
}
View File
+2
View File
@@ -31,6 +31,8 @@ rstcheck==6.2.5
rstcheck-core==1.2.2
shellingham==1.5.4
six==1.17.0
Sphinx==9.1.0
myst-parser==5.1.0
stevedore==5.7.0
typer==0.25.0
typing-inspection==0.4.2