wiki/z-archive/test-local-llm/app.py

319 lines
10 KiB
Python

"""
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)