diff --git a/app.py b/app.py new file mode 100644 index 0000000..36d5170 --- /dev/null +++ b/app.py @@ -0,0 +1,319 @@ +""" +Belegscanner - KI-Dokumentenanalyse +Python Flask Web App mit CORS-Unterstützung und Poweron Design +""" + +from flask import Flask, render_template, request, jsonify +from flask_cors import CORS +import requests +import base64 +import json +import re +import io + +# PDF Support +try: + import fitz # PyMuPDF + PDF_SUPPORT = True +except ImportError: + PDF_SUPPORT = False + print("WARNUNG: PyMuPDF nicht installiert. PDF-Support deaktiviert.") + print("Installieren mit: pip install pymupdf") + +app = Flask(__name__) +CORS(app) # CORS für alle Routen aktivieren + + +# ============================================================================ +# PDF Helper Functions +# ============================================================================ + +def _extractImagesFromPdf(pdfBytes, maxPages=5): + """ + Extrahiert Bilder aus einem PDF. + Gibt eine Liste von Base64-kodierten Bildern zurück. + """ + if not PDF_SUPPORT: + raise Exception("PDF-Support nicht verfügbar. Bitte PyMuPDF installieren.") + + images = [] + + # PDF öffnen + doc = fitz.open(stream=pdfBytes, filetype="pdf") + + # Anzahl der Seiten begrenzen + numPages = min(len(doc), maxPages) + + for pageNum in range(numPages): + page = doc[pageNum] + + # Seite als Bild rendern (höhere Auflösung für bessere OCR) + mat = fitz.Matrix(2.0, 2.0) # 2x Zoom für bessere Qualität + pix = page.get_pixmap(matrix=mat) + + # In PNG konvertieren + imgBytes = pix.tobytes("png") + imgBase64 = base64.b64encode(imgBytes).decode('utf-8') + + images.append({ + 'page': pageNum + 1, + 'base64': imgBase64, + 'width': pix.width, + 'height': pix.height + }) + + doc.close() + + return images + + +def _renderPdfPageAsImage(pdfBytes, pageNum=0, zoom=2.0): + """ + Rendert eine einzelne PDF-Seite als Bild. + """ + if not PDF_SUPPORT: + raise Exception("PDF-Support nicht verfügbar.") + + doc = fitz.open(stream=pdfBytes, filetype="pdf") + + if pageNum >= len(doc): + pageNum = len(doc) - 1 + + page = doc[pageNum] + mat = fitz.Matrix(zoom, zoom) + pix = page.get_pixmap(matrix=mat) + + imgBytes = pix.tobytes("png") + imgBase64 = base64.b64encode(imgBytes).decode('utf-8') + + result = { + 'base64': imgBase64, + 'width': pix.width, + 'height': pix.height, + 'page': pageNum + 1, + 'totalPages': len(doc) + } + + doc.close() + + return result + +# ============================================================================ +# Model Helper Functions +# ============================================================================ + +def _isVisionModel(modelName): + """ + Prüft ob ein Modell ein Vision-Modell ist basierend auf Namenskonventionen. + Vision-Modelle enthalten typischerweise 'vision', 'vl', 'llava', 'bakllava' im Namen. + """ + if not modelName: + return False + + modelLower = modelName.lower() + visionIndicators = ['vision', 'vl', 'llava', 'bakllava'] + + return any(indicator in modelLower for indicator in visionIndicators) + + +# ============================================================================ +# Routes +# ============================================================================ + +@app.route('/') +def _index(): + """Hauptseite mit dem Belegscanner UI""" + return render_template('index.html') + + +@app.route('/api/analyze', methods=['POST']) +def _analyzeDocument(): + """ + Analysiert ein Dokument mit Ollama Vision API oder verarbeitet Text mit Non-Vision Modellen + Erwartet: { imageBase64 (optional bei Non-Vision), prompt, ollamaUrl, modelName } + """ + try: + data = request.get_json() + + imageBase64 = data.get('imageBase64') + prompt = data.get('prompt') + ollamaUrl = data.get('ollamaUrl', 'http://localhost:11434') + modelName = data.get('modelName', 'qwen2.5vl:72b') + + # Prüfe ob es ein Vision-Modell ist (basierend auf Namenskonvention) + isVisionModel = _isVisionModel(modelName) + + # Bei Vision-Modellen ist ein Bild erforderlich + if isVisionModel and not imageBase64: + return jsonify({'error': 'Kein Bild übermittelt (erforderlich für Vision-Modelle)'}), 400 + + if not prompt: + return jsonify({'error': 'Kein Prompt übermittelt'}), 400 + + # Request-Body erstellen + requestBody = { + 'model': modelName, + 'prompt': prompt, + 'stream': False + } + + # Bilder nur hinzufügen wenn vorhanden (für Vision-Modelle) + if imageBase64: + requestBody['images'] = [imageBase64] + + # Ollama API aufrufen (Timeout: 60 Minuten für grosse Modelle) + response = requests.post( + f'{ollamaUrl}/api/generate', + json=requestBody, + timeout=3600 # 60 Minuten + ) + + if response.status_code == 404: + return jsonify({ + 'error': f'Modell "{modelName}" nicht gefunden. Bitte installieren Sie es mit: ollama pull {modelName}' + }), 404 + + if response.status_code != 200: + return jsonify({ + 'error': f'Ollama API Fehler: {response.status_code} - {response.text[:200]}' + }), response.status_code + + responseData = response.json() + responseText = responseData.get('response', '') + + # Versuche JSON aus der Antwort zu extrahieren + extractedData = None + jsonMatch = re.search(r'\{[\s\S]*\}', responseText) + + if jsonMatch: + try: + extractedData = json.loads(jsonMatch.group()) + except json.JSONDecodeError: + # JSON-ähnlicher Text gefunden, aber ungültig + extractedData = None + + # Wenn kein JSON gefunden, Antwort in JSON-Objekt verpacken + if extractedData is None: + extractedData = { + 'response': responseText.strip() + } + + return jsonify({ + 'success': True, + 'data': extractedData, + 'rawResponse': responseText + }) + + except requests.exceptions.Timeout: + return jsonify({'error': 'Zeitüberschreitung bei der Ollama API'}), 504 + except requests.exceptions.ConnectionError: + return jsonify({'error': 'Verbindung zu Ollama fehlgeschlagen. Ist Ollama gestartet?'}), 503 + except json.JSONDecodeError as e: + return jsonify({'error': f'JSON Parse-Fehler: {str(e)}'}), 400 + except Exception as e: + return jsonify({'error': f'Unerwarteter Fehler: {str(e)}'}), 500 + + +@app.route('/api/health', methods=['GET']) +def _healthCheck(): + """Health Check Endpoint""" + return jsonify({'status': 'ok', 'service': 'belegscanner', 'pdfSupport': PDF_SUPPORT}) + + +@app.route('/api/pdf/extract', methods=['POST']) +def _extractPdfImages(): + """ + Extrahiert Bilder aus einem PDF. + Erwartet: { pdfBase64, page (optional, default: alle) } + """ + if not PDF_SUPPORT: + return jsonify({ + 'error': 'PDF-Support nicht verfügbar. Bitte PyMuPDF installieren: pip install pymupdf' + }), 501 + + try: + data = request.get_json() + pdfBase64 = data.get('pdfBase64') + pageNum = data.get('page') # Optional: spezifische Seite + + if not pdfBase64: + return jsonify({'error': 'Kein PDF übermittelt'}), 400 + + # Base64 dekodieren + pdfBytes = base64.b64decode(pdfBase64) + + if pageNum is not None: + # Einzelne Seite extrahieren + result = _renderPdfPageAsImage(pdfBytes, pageNum - 1) # 0-basiert + return jsonify({ + 'success': True, + 'image': result + }) + else: + # Alle Seiten extrahieren (max 5) + images = _extractImagesFromPdf(pdfBytes, maxPages=5) + return jsonify({ + 'success': True, + 'images': images, + 'totalExtracted': len(images) + }) + + except Exception as e: + return jsonify({'error': f'PDF-Verarbeitungsfehler: {str(e)}'}), 500 + + +@app.route('/api/ollama/status', methods=['GET']) +def _ollamaStatus(): + """Prüft ob Ollama erreichbar ist und listet verfügbare Modelle""" + ollamaUrl = request.args.get('url', 'http://localhost:11434') + + try: + # Prüfe ob Ollama läuft + response = requests.get(f'{ollamaUrl}/api/tags', timeout=5) + + if response.status_code != 200: + return jsonify({ + 'connected': False, + 'error': f'Ollama antwortet mit Status {response.status_code}' + }) + + data = response.json() + models = [m.get('name', '') for m in data.get('models', [])] + + # Filtere Vision-Modelle (enthalten oft 'vision', 'vl', 'llava' im Namen) + visionModels = [m for m in models if any(x in m.lower() for x in ['vision', 'vl', 'llava', 'bakllava'])] + + return jsonify({ + 'connected': True, + 'models': models, + 'visionModels': visionModels, + 'totalModels': len(models) + }) + + except requests.exceptions.ConnectionError: + return jsonify({ + 'connected': False, + 'error': 'Keine Verbindung zu Ollama. Ist Ollama gestartet?' + }) + except Exception as e: + return jsonify({ + 'connected': False, + 'error': str(e) + }) + + +# ============================================================================ +# Main +# ============================================================================ + +if __name__ == '__main__': + print("\n" + "="*60) + print(" Belegscanner - KI-Dokumentenanalyse") + print(" Powered by Poweron") + print("="*60) + print("\n Server läuft auf: http://localhost:5000") + print(" CORS ist aktiviert für alle Origins") + print("\n Drücke Ctrl+C zum Beenden") + print("="*60 + "\n") + + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f718672 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask>=3.0.0 +flask-cors>=4.0.0 +requests>=2.31.0 +werkzeug>=3.0.0 +pymupdf>=1.24.0 diff --git a/start-python.bat b/start-python.bat new file mode 100644 index 0000000..7a5b4ad --- /dev/null +++ b/start-python.bat @@ -0,0 +1,41 @@ +@echo off +chcp 65001 >nul +echo ============================================================ +echo Belegscanner - KI-Dokumentenanalyse +echo Powered by Poweron +echo ============================================================ +echo. + +REM Ollama starten mit CORS-Freigabe +echo [1/3] Starte Ollama mit CORS-Freigabe... +set OLLAMA_ORIGINS=* + +REM Versuche Ollama zu finden +where ollama >nul 2>&1 +if %errorlevel%==0 ( + start /min ollama serve + echo Ollama gestartet +) else ( + if exist "%LOCALAPPDATA%\Programs\Ollama\ollama.exe" ( + start /min "" "%LOCALAPPDATA%\Programs\Ollama\ollama.exe" serve + echo Ollama gestartet + ) else ( + echo Ollama nicht gefunden - bitte manuell starten! + echo Setze OLLAMA_ORIGINS=* vor dem Start + ) +) + +timeout /t 2 /nobreak > nul + +REM Dependencies installieren +echo [2/3] Installiere Python Dependencies... +pip install -r requirements.txt --quiet + +echo [3/3] Starte Python Flask Server... +echo. +echo Server URL: http://localhost:5000 +echo Druecke Ctrl+C zum Beenden +echo. + +REM Flask starten +python app.py diff --git a/start-python.ps1 b/start-python.ps1 new file mode 100644 index 0000000..7feab99 --- /dev/null +++ b/start-python.ps1 @@ -0,0 +1,54 @@ +# Belegscanner - Python Web App Starter +# Poweron Design + +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host " Belegscanner - KI-Dokumentenanalyse" -ForegroundColor White +Write-Host " Powered by Poweron" -ForegroundColor Magenta +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" + +# Ollama mit CORS starten (optional) +Write-Host "[1/3] Starte Ollama mit CORS-Freigabe..." -ForegroundColor Yellow +$env:OLLAMA_ORIGINS = "*" + +# Versuche Ollama zu finden und zu starten +$ollamaPath = Get-Command ollama -ErrorAction SilentlyContinue +if ($ollamaPath) { + Start-Process -FilePath $ollamaPath.Source -ArgumentList "serve" -WindowStyle Minimized + Write-Host " Ollama gestartet" -ForegroundColor Green +} else { + # Fallback: Standard-Installationspfade pruefen + $defaultPaths = @( + "$env:LOCALAPPDATA\Programs\Ollama\ollama.exe", + "$env:ProgramFiles\Ollama\ollama.exe", + "C:\Users\$env:USERNAME\AppData\Local\Programs\Ollama\ollama.exe" + ) + $found = $false + foreach ($path in $defaultPaths) { + if (Test-Path $path) { + Start-Process -FilePath $path -ArgumentList "serve" -WindowStyle Minimized + Write-Host " Ollama gestartet von: $path" -ForegroundColor Green + $found = $true + break + } + } + if (-not $found) { + Write-Host " Ollama nicht gefunden - bitte manuell starten!" -ForegroundColor Red + Write-Host " Setze OLLAMA_ORIGINS=* vor dem Start" -ForegroundColor Gray + } +} + +Start-Sleep -Seconds 2 + +# Dependencies installieren +Write-Host "[2/3] Installiere Python Dependencies..." -ForegroundColor Yellow +pip install -r requirements.txt --quiet + +Write-Host "[3/3] Starte Flask Server..." -ForegroundColor Yellow +Write-Host "" +Write-Host "Server URL: http://localhost:5000" -ForegroundColor Green +Write-Host "Druecke Ctrl+C zum Beenden" -ForegroundColor Gray +Write-Host "" + +# Flask Server starten +python app.py diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..9d36f52 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/poweron-logo.png b/static/poweron-logo.png new file mode 100644 index 0000000..2a7aea3 Binary files /dev/null and b/static/poweron-logo.png differ diff --git a/t1.png b/t1.png new file mode 100644 index 0000000..fc0618a Binary files /dev/null and b/t1.png differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2abfbcd --- /dev/null +++ b/templates/index.html @@ -0,0 +1,1316 @@ + + + + + + Belegscanner – PowerOn + + + + +
+
+ +

KI-gestützte Dokumentenanalyse

+
+
+ +
+ +
+ +
+
+ Dokument +
+
+
+
+
📤
+

Bild oder PDF

+

Drag & Drop

+
+ + +
+ +
+
+ + +
+
+ Einstellungen +
+
+
+ + + +
+
+ + +
+ + +
+
+ Prompt + +
+ +
+ +
+ +
+
+
+ + +
+
+ Extrahierte Daten + + + Warte auf Eingabe + +
+
+
+
+
🔍
+

Laden Sie ein Dokument hoch und klicken Sie auf "Analysieren"

+
+ + +
+
+
+
+ + +
+ + + +