Compare commits
66 Commits
test-versi
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
619159cd33 | |
|
|
c3fb9dc7cb | |
|
|
df015c9f97 | |
|
|
4b4118fe70 | |
|
|
88c327c977 | |
|
|
b41554f1f1 | |
|
|
393d00d631 | |
|
|
d217893dcb | |
|
|
c2b2589dd0 | |
|
|
7e0866ba4b | |
|
|
f28f090b06 | |
|
|
35fcc5a576 | |
|
|
d89cc0290d | |
|
|
1bcc3d0f57 | |
|
|
565fe097dd | |
|
|
bacf1798f9 | |
|
|
9ee56440f5 | |
|
|
555272bcf6 | |
|
|
cd187585dd | |
|
|
23968bb911 | |
|
|
2e259589c0 | |
|
|
991384a362 | |
|
|
51ed31b1e8 | |
|
|
4675cfd2a0 | |
|
|
271f54ecf4 | |
|
|
5dd33c6ff8 | |
|
|
b52d2a3714 | |
|
|
58e589aa7a | |
|
|
3dcb293ba4 | |
|
|
e6897bd263 | |
|
|
34251a7ad2 | |
|
|
d2d09054d2 | |
|
|
308cddedef | |
|
|
1851ed7074 | |
|
|
71bb9885c6 | |
|
|
ac233baa2f | |
|
|
f92c4ffac6 | |
|
|
3832d5cdf2 | |
|
|
3e0975102f | |
|
|
df81c011d3 | |
|
|
29034d8501 | |
|
|
cd2bd56377 | |
|
|
5a9929aa9e | |
|
|
9a7dca1498 | |
|
|
27f6d8bab7 | |
|
|
8a7990f95f | |
|
|
19cdff820d | |
|
|
1992c30991 | |
|
|
c07eb860fa | |
|
|
efdee4cfd6 | |
|
|
36af456cd8 | |
|
|
e4f2315179 | |
|
|
656040b64b | |
|
|
16566ef858 | |
|
|
3a621eb805 | |
|
|
3b3a2dce90 | |
|
|
0dd9916079 | |
|
|
d7b851ae36 | |
|
|
6ecd59e5ea | |
|
|
ae986b4f3f | |
|
|
4181346f4a | |
|
|
994ad03387 | |
|
|
eb7aa73653 | |
|
|
a0d80c5d08 | |
|
|
ae5126898e | |
|
|
88e75f71c9 |
|
|
@ -1,2 +1,10 @@
|
|||
venv
|
||||
models
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.git
|
||||
.superpowers/
|
||||
docs/
|
||||
tests/
|
||||
pytest.ini
|
||||
requirements-dev.txt
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
venv
|
||||
models
|
||||
.superpowers/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Nur lokal - nicht zum Betrieb des Tools noetig (Tests, Test-Config, Planungsdoku)
|
||||
docs/
|
||||
tests/
|
||||
pytest.ini
|
||||
requirements-dev.txt
|
||||
.claude/
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[server]
|
||||
enableStaticServing = true
|
||||
90
README.md
90
README.md
|
|
@ -2,76 +2,64 @@
|
|||
|
||||
## Description
|
||||
|
||||
Dieses Tool konvertiert Rechnungen im PDF-Format vollautomatisch in das offizielle XRechnung-XML-Format (UBL-Standard, Version 3.0). Das Tool verarbeitet sensible Finanzdaten vollständig lokal.
|
||||
Dieses Tool konvertiert Rechnungen im PDF-Format über eine Weboberfläche in die offiziellen E-Rechnungsformate XRechnung (UBL 3.0) und ZUGFeRD 2.4 (hybride PDF mit eingebettetem CII-XML). Die Datenextraktion erfolgt über ein LLM, das intern im Firmennetzwerk läuft – sensible Finanzdaten verlassen das lokale Netzwerk nicht.
|
||||
|
||||
## Anleitung für die app.py (Client Lokal)
|
||||
## Supported Model
|
||||
## Zugriff
|
||||
|
||||
Das System ist auf das folgende Modell optimiert, welches sich im Verzeichnis `models/` befinden muss:
|
||||
Die Anwendung läuft als Docker-Container und ist im lokalen Firmennetzwerk erreichbar unter:
|
||||
|
||||
* **Modell:** Qwen3.5-4B-Instruct
|
||||
* **Format:** GGUF
|
||||
* **Quantisierung:** Q8_0
|
||||
http://erechnung
|
||||
|
||||
https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/blob/main/Qwen3.5-4B-Q8_0.gguf
|
||||
## Aufbau
|
||||
|
||||
## Installation (Windows)
|
||||
Das System besteht aus zwei Komponenten:
|
||||
|
||||
### 1. Verzeichnisstruktur vorbereiten
|
||||
- **Web-Oberfläche** (`main.py`, Streamlit): PDF-Upload, Datenvalidierung, Export
|
||||
- **vLLM-Backend**: separat betriebener Inferenz-Server (Standard: `gemma431b`), der die strukturierten Metadaten aus dem Rechnungstext extrahiert
|
||||
|
||||
Laden sie die requirements.txt und app.py herunter und plazieren Sie die in das Projektverzeichniss.
|
||||
## Installation (Docker)
|
||||
|
||||
- models
|
||||
- requirements.txt
|
||||
- app.py
|
||||
|
||||
### 2. Virtuelle Umgebung (venv) erstellen und aktivieren
|
||||
```cmd
|
||||
python -m venv venv
|
||||
.\venv\Scripts\activate.bat
|
||||
```
|
||||
|
||||
### 3. Modell platzieren
|
||||
Laden Sie die Modelldatei `Qwen3.5-4B-Instruct-Q8_0.gguf` herunter und platzieren Sie diese im Ordner `models/`.
|
||||
|
||||
### 4. Abhängigkeiten installieren
|
||||
|
||||
**Option A: CPU-Modus**
|
||||
```cmd
|
||||
pip install -r requirements.txt --force-reinstall --no-cache-dir
|
||||
```
|
||||
|
||||
**Option B: Mit GPU-Beschleunigung (Nvidia CUDA)**
|
||||
Falls Sie die GPU-Beschleunigung nutzen möchten, müssen vorab folgende Komponenten auf dem System installiert sein:
|
||||
* **Nvidia CUDA Toolkit**
|
||||
* **Visual Studio Build Tools**
|
||||
### 1. Repository holen
|
||||
|
||||
```cmd
|
||||
set GGML_CUDA=1
|
||||
pip install -r requirements.txt --force-reinstall --no-cache-dir
|
||||
git clone https://gitea.scitech-gmbh.de:8443/dfuhrmann/E-Rechnung.git
|
||||
cd E-Rechnung
|
||||
```
|
||||
|
||||
### 2. Umgebungsvariablen prüfen
|
||||
|
||||
In `docker-compose.yml` sind das vLLM-Backend und Modell hinterlegt:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- VLLM_URL=http://10.192.0.140:8002
|
||||
- LLM_MODEL=gemma431b
|
||||
```
|
||||
|
||||
Bei Bedarf hier anpassen (z.B. anderes Modell oder Backend-Host).
|
||||
|
||||
### 3. Container bauen und starten
|
||||
|
||||
```cmd
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Die Anwendung ist danach unter `http://localhost:8040` (bzw. im Firmennetz unter `http://erechnung`) erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## Use
|
||||
|
||||
Starten Sie die Applikation aus Ihrer aktivierten virtuellen Umgebung heraus:
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
Der Konversionsprozess gliedert sich in folgende Schritte:
|
||||
1. **Dokumenten-Upload:** Laden Sie die Rechnungs-PDF über die Weboberfläche hoch.
|
||||
2. **Extraktion:** Starten Sie die hybride Analyse über den dafür vorgesehenen Button.
|
||||
3. **Validierung:** Prüfen Sie die extrahierten Kopfdaten und die Positionstabelle in der Benutzeroberfläche. Da alle Felder editierbar sind, können Korrekturen direkt vorgenommen werden.
|
||||
4. **Export:** Generieren Sie die finale XML-Datei über die Export-Funktion und starten Sie den Download.
|
||||
1. **Dokumenten-Upload:** Laden Sie die Rechnungs-PDF über die Weboberfläche hoch. Die PDF wird direkt zur Kontrolle angezeigt.
|
||||
2. **Extraktion:** Starten Sie die Analyse über den Button "Daten extrahieren".
|
||||
3. **Validierung:** Prüfen Sie die extrahierten Kopfdaten und die Positionstabelle in der Benutzeroberfläche. Da alle Felder editierbar sind, können Korrekturen direkt vorgenommen werden. Pflichtfelder sind mit :red[*] markiert; der Export wird blockiert, solange Pflichtangaben fehlen.
|
||||
4. **Export:** Wählen Sie das gewünschte Format (XRechnung-XML oder ZUGFeRD-PDF) und starten Sie den Download.
|
||||
|
||||
## Notes
|
||||
|
||||
### Human-in-the-Loop
|
||||
Das Tool dient als Automations-Assistent. Vor dem finalen XML-Export ist eine visuelle Endkontrolle der extrahierten Daten (insbesondere Rechnungsnummer und Empfängeradresse) durch den Sachbearbeiter zwingend erforderlich, um KI-Fehlinterpretationen auszuschließen.
|
||||
Das Tool dient als Automations-Assistent. Vor dem finalen Export ist eine visuelle Endkontrolle der extrahierten Daten (insbesondere Rechnungsnummer und Empfängeradresse) durch den Sachbearbeiter zwingend erforderlich, um KI-Fehlinterpretationen auszuschließen.
|
||||
|
||||
### GoBD-Konformität
|
||||
Das Tool generiert ausschließlich das strukturierte XML-Format. Für die Einhaltung der GoBD-Richtlinien (u. a. die revisionssichere Archivierung der Original-PDF sowie der generierten XML-Datei über den gesetzlichen Zeitraum von 10 Jahren) ist der Betreiber der Software selbst verantwortlich.
|
||||
|
||||
Das Tool generiert ausschließlich das strukturierte XML- bzw. hybride PDF-Format. Für die Einhaltung der GoBD-Richtlinien (u. a. die revisionssichere Archivierung der Original-PDF sowie der generierten E-Rechnung über den gesetzlichen Zeitraum von 10 Jahren) ist der Betreiber der Software selbst verantwortlich.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -0,0 +1,136 @@
|
|||
import streamlit.components.v1 as components
|
||||
|
||||
# Sicherheitsnetz-Timer (ms): das Ausblenden geschieht primaer ueber das 'ended'-Event des
|
||||
# Videos (exakt am Ende). Dieser Timer greift nur, falls 'ended' mal nicht feuert
|
||||
# (z. B. Autoplay blockiert). Grosszuegig ueber der Videolaenge (~15,8 s) waehlen.
|
||||
INTRO_FALLBACK_MS = 20000
|
||||
# Statische URL (Streamlit Static Serving). Beim Live-Test ggf. auf "./static/intro.mp4"
|
||||
# oder "/static/intro.mp4" korrigieren, falls "app/static/..." 404 liefert.
|
||||
_INTRO_SRC = "app/static/intro.mp4"
|
||||
_SEEN_KEY = "einrechnung_intro_seen"
|
||||
|
||||
|
||||
def render_intro_overlay(fallback_ms: int = INTRO_FALLBACK_MS,
|
||||
intro_src: str = _INTRO_SRC,
|
||||
seen_key: str = _SEEN_KEY) -> None:
|
||||
"""Injiziert ein einmaliges Intro-Overlay (MP4) in die Streamlit-Elternseite (Ansatz A).
|
||||
|
||||
Ein unsichtbarer components.html-Baustein (Hoehe 0) fuehrt JS aus, das ein
|
||||
position:fixed-Overlay in window.parent.document einfuegt - ein echtes Overlay ueber der
|
||||
Seite. Die Animation ist ein <video> (autoplay, muted, ohne Steuerleiste, ohne Loop):
|
||||
Ausblenden exakt am Ende via 'ended'-Event (Timer nur als Sicherheitsnetz). Autoplay nur
|
||||
einmal pro Browser (localStorage), "Ueberspringen"-Button + Replay-Button oben rechts
|
||||
(Replay aendert den "gesehen"-Merker nicht).
|
||||
"""
|
||||
html = f"""
|
||||
<script>
|
||||
(function() {{
|
||||
var doc = window.parent.document;
|
||||
if (window.parent.__introInstalled) return; // Rerun-Schutz: nur einmal pro Seitenladen
|
||||
window.parent.__introInstalled = true;
|
||||
|
||||
var FALLBACK = {fallback_ms};
|
||||
var SRC = {intro_src!r};
|
||||
var SEEN_KEY = {seen_key!r};
|
||||
|
||||
var style = doc.createElement('style');
|
||||
style.textContent = `
|
||||
#eir-backdrop {{
|
||||
position: fixed; inset: 0; z-index: 2147483646;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(8,8,10,0.82); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
|
||||
opacity: 0; transition: opacity .45s ease; pointer-events: none;
|
||||
}}
|
||||
#eir-backdrop.eir-show {{ opacity: 1; pointer-events: auto; }}
|
||||
#eir-panel {{
|
||||
position: relative; width: min(50vw, 1100px); aspect-ratio: 16 / 9;
|
||||
max-width: 94vw; max-height: 88vh;
|
||||
background: #0a0a0a; border-radius: 16px; overflow: hidden;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(245,126,26,0.25);
|
||||
}}
|
||||
@media (max-width: 820px) {{ #eir-panel {{ width: 92vw; }} }}
|
||||
#eir-video {{ width: 100%; height: 100%; display: block; object-fit: contain; background: #0a0a0a; }}
|
||||
#eir-skip {{
|
||||
position: absolute; top: 10px; right: 12px; z-index: 2;
|
||||
font: 600 13px/1 -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: #eee; background: rgba(0,0,0,0.45); border: 1px solid rgba(255,255,255,0.18);
|
||||
padding: 7px 12px; border-radius: 999px; cursor: pointer; user-select: none;
|
||||
}}
|
||||
#eir-skip:hover {{ background: rgba(0,0,0,0.7); }}
|
||||
#eir-replay {{
|
||||
position: fixed; top: 12px; right: 72px; z-index: 2147483647;
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
||||
background: #17171a; border: 1px solid rgba(245,126,26,0.55);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.35);
|
||||
}}
|
||||
#eir-replay:hover {{ background: #202024; }}
|
||||
#eir-replay svg {{ width: 16px; height: 16px; }}
|
||||
`;
|
||||
doc.head.appendChild(style);
|
||||
|
||||
var replay = doc.createElement('div');
|
||||
replay.id = 'eir-replay';
|
||||
replay.title = 'Intro erneut abspielen';
|
||||
replay.innerHTML = '<svg viewBox="0 0 24 24" fill="#F57E1A"><path d="M8 5v14l11-7z"/></svg>';
|
||||
doc.body.appendChild(replay);
|
||||
|
||||
// Den ▶-Button links NEBEN Streamlits Menue-Leiste (oben rechts) platzieren, statt darueber.
|
||||
// Die Toolbar (Deploy + ⋮-Menue) rendert asynchron -> mehrfach nachjustieren + bei Resize.
|
||||
function positionReplay() {{
|
||||
var win = window.parent, gap = 12, fallback = 72;
|
||||
var tb = doc.querySelector('[data-testid="stToolbar"]');
|
||||
var rightPx = fallback;
|
||||
if (tb) {{
|
||||
var cluster = win.innerWidth - tb.getBoundingClientRect().left; // Breite des Menue-Clusters
|
||||
if (cluster > 8 && cluster < win.innerWidth * 0.6) rightPx = Math.round(cluster) + gap;
|
||||
}}
|
||||
replay.style.right = rightPx + 'px';
|
||||
}}
|
||||
positionReplay();
|
||||
[150, 500, 1200].forEach(function(t) {{ setTimeout(positionReplay, t); }});
|
||||
window.parent.addEventListener('resize', positionReplay);
|
||||
|
||||
var backdrop = doc.createElement('div');
|
||||
backdrop.id = 'eir-backdrop';
|
||||
backdrop.innerHTML =
|
||||
'<div id="eir-panel">' +
|
||||
'<div id="eir-skip">Überspringen ✕</div>' +
|
||||
'<video id="eir-video" muted playsinline preload="none" src="' + SRC + '"></video>' +
|
||||
'</div>';
|
||||
doc.body.appendChild(backdrop);
|
||||
|
||||
var video = doc.getElementById('eir-video');
|
||||
var skip = doc.getElementById('eir-skip');
|
||||
var hideTimer = null;
|
||||
|
||||
function hide() {{
|
||||
clearTimeout(hideTimer);
|
||||
backdrop.classList.remove('eir-show');
|
||||
setTimeout(function() {{ try {{ video.pause(); }} catch (e) {{}} }}, 500);
|
||||
}}
|
||||
|
||||
function show() {{
|
||||
backdrop.classList.add('eir-show');
|
||||
try {{ video.currentTime = 0; }} catch (e) {{}} // Neustart von vorn (auch bei Replay)
|
||||
var p = video.play();
|
||||
if (p && p.catch) p.catch(function() {{}}); // Autoplay-Ablehnung ignorieren
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(hide, FALLBACK); // Sicherheitsnetz, falls 'ended' ausbleibt
|
||||
}}
|
||||
|
||||
video.addEventListener('ended', hide); // exaktes Ausblenden am Videoende
|
||||
skip.addEventListener('click', hide);
|
||||
backdrop.addEventListener('click', function(e) {{ if (e.target === backdrop) hide(); }});
|
||||
replay.addEventListener('click', show);
|
||||
|
||||
var seen;
|
||||
try {{ seen = window.parent.localStorage.getItem(SEEN_KEY); }} catch (e) {{ seen = null; }}
|
||||
if (seen !== '1') {{
|
||||
try {{ window.parent.localStorage.setItem(SEEN_KEY, '1'); }} catch (e) {{}}
|
||||
show();
|
||||
}}
|
||||
}})();
|
||||
</script>
|
||||
"""
|
||||
components.html(html, height=0)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Loading…
Reference in New Issue